After using Angular2+ RouteReuseStrategy, how to call a method in return to cache components to update part? - angular2-routing

I have a list page, and click a column to enter the details page. Edit and return. Because using RouteReuseStrategy, it can maintain the list page of the scene remains the same. But I'd like to partially update, However, I don't know how to trigger it.
Here is my RouteReuseStrategy service and most of the same.
export class SimpleReuseStrategy implements RouteReuseStrategy {
_cacheRouters: { [key: string]: any } = {};
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return true;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this._cacheRouters[route.routeConfig.path] = {
snapshot: route,
handle: handle
};
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this._cacheRouters[route.routeConfig.path];
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this._cacheRouters[route.routeConfig.path].handle;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr:
ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}

In your list page component you can listen to route changes:
Detect if the navigation start came from the detail route i.e. /detail/1
if so, get the 'id' param of the route /detail/1
replace that element in the list
// List component
ngOnInit(): void {
this.subscription = this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
// checkIfDetailPage and store ID
}
if (event instanceof NavigationEnd) {
// checkIfThisPage and get the ID
// replace element with ID
}
});
}
An alternative approach for detecting something changed (instead of observing the route changes) you could implement Service to which your list component subscribes and your detail component notifies.

Related

SwiftUI Navigation automatically closing/pop - Realm

I'm populating a List with a Realm Result set.
When navigating from this list it opens a new view then automatically closes that view.
Using a struct presents no issue.
Why would the second view automatically close?
I have a screen recording but cant post here.
import SwiftUI
import Combine
struct TestStruct:Identifiable{
let id = UUID()
let firstname: String
}
extension TestStruct {
static func all() -> [TestStruct]{
return[
TestStruct(firstname: "Joe"),
TestStruct(firstname: "Jane"),
TestStruct(firstname: "Johns")
]
}
}
struct TestListView: View {
let realmList = Horoscope.getHoroscopes() //Fetches from Realm
let structList = TestStruct.all()
var body: some View {
NavigationView{
// This owrks
// List(structList) { item in
// MyItemRow(itemTxt: item.firstname)
// }
//This automatically closes the view
List(realmList) { item in
MyItemRow(itemTxt: item.firstname)
}
.navigationBarTitle("Charts", displayMode: .automatic)
.navigationBarItems(trailing: EditButton())
}
}
}
struct MyItemRow: View {
var itemTxt:String
var body: some View {
NavigationLink(destination: Text("Test")) {
Text(itemTxt)
}
}
}
struct TestListView_Previews: PreviewProvider {
static var previews: some View {
TestListView()
}
}
I think the answer can be found here
In short, do not generate the id of the collection on which the ForEach iterates. It would detect a change and navigate back.
Realm object has an auto generated id property with each reference, try replacing it with a consistent id
The following solution worked for me.
The code with an issue (specifying id: \.self is the root cause since it uses the hash calculated from all objects the Stream object consists of, including the data that lies in a subarray).
...
List(streams, id: \.self) { stream in
...
The code with no issues:
...
List(streams, id: \._id) { stream in
// or even List(streams) { stream in
...
The streams is a #ObservedResults(Stream.self) var streams and the object scheme is:
final class Stream: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var title: String
#Persisted var subtitle: String?
#Persisted var topics = RealmSwift.List<Topic>()
// tags, etc.
}
The issue happened when I added new topic at the topics list in the first stack of the navigationView.

Realm.firstFirstAsync().asObservable() isn't consistently working with RxJava.switchIfEmpty

I am trying to create function which reads a object from realm and emit an empty observable if the object isn't found. The code below works to some degree because I can stop it with the debugger and see it hit the Observable.empty():
fun readFromRealm(id: String): Observable<Player> {
return realm.where(Player::class.java)
.equalTo("id", id)
.findFirstAsync()
.asObservable<Player>()
.filter { it.isLoaded }
.flatMap {
if (it.isValid)
Observable.just(it)
else
Observable.empty()
}
}
But when I try to use a switchIfEmpty on the Observable the code never emits defaultPlayer when it is not found in realm.
return readFromRealm(playerId)
.take(1)
.map{ // do something with emitted observable }
.switchIfEmpty(Observable.just(defaultPlayer)) // use this if no player found
The strange thing is that if I update the original method to include a first() prior to the flatMap :
fun readFromRealm(id: String): Observable<Player> {
return realm.where(Player::class.java)
.equalTo("id", id)
.findFirstAsync()
.asObservable<Player>()
.filter { it.isLoaded }
.first() // add first
.flatMap {
if (it.isValid)
Observable.just(it)
else
Observable.empty()
}
}
Everything starts working as expected, but I believe this version will kill auto updating because it will only capture the first result emitted after the filter.
I'm still trying to grok Realm and Rx so I'm probably doing something dumb.
EDIT: I have created a sample project which highlights the issue I'm seeing - https://github.com/donaldlittlepie/realm-async-issue
For reasons I don't totally understand. If you move take(1) just above the
flatMap and below the filter it should work correctly:
realm.where(Dog.class)
.equalTo("id", 0L)
.findFirstAsync()
.asObservable()
.cast(Dog.class)
.filter(new Func1<RealmObject, Boolean>() {
#Override
public Boolean call(RealmObject realmObject) {
return realmObject.isLoaded();
}
})
.take(1) // <== here
.flatMap(new Func1<Dog, Observable<Dog>>() {
#Override
public Observable<Dog> call(Dog realmObject) {
if (realmObject.isValid()) {
return Observable.just(realmObject);
} else {
return Observable.empty();
}
}
})
.map(new Func1<Dog, Dog>() {
#Override
public Dog call(Dog dog) {
dog.setName("mapped " + dog.getName());
return dog;
}
})
.switchIfEmpty(Observable.just(createDefaultDog()))
.subscribe(new Action1<Dog>() {
#Override
public void call(Dog dog) {
textView.setText(dog.getName());
}
}, new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
textView.setText(throwable.toString());
}
});
My best guess is that before, flatMap was called repeatedly, returning Observable.empty() multiple times. Perhaps that effects the Observable chain in some unexpected way.

HTTP: getById creates new instance, which is missing an Array (Angular 2 RC1 + TS)

I got some really good help to a previous question at: "TypeError.parent.context.car.getBrands is not a function": s
and that is related to my current question. As can seen in that answer to my previous error, my app won't work, unless I create an new instance of "car", but hen I call that method:
getById(id: string) {
return this.http.get('app/car.json'+id)
/*
if I log the incoming data here to the console,
the correct data from server comes, eg: 'id: id, name: name, brands: Array[2]'
*/
.map(data => data.json())
.map(car => new Car(car.id, car.name)); //<== this line causes problem!
}
receiving component:
routerOnActivate(curr: RouteSegment): void {
let id = curr.getParam('id');
this._service.getById(id)
.subscribe(car => {
// this code is executed when the response from the server arrives
this.car = car;
console.log("res: ", this.car);// <=== correct car, without the array of brands
});
// code here is executed before code from the server arrives
// event though it is written below
}
it creates a new instance "Car". Well that is all good, but the Car also contains an Array of Brands.
My service looks like this:
#Injectable()
export class Service {
constructor(private http: Http) { }
getCars(){
return this.http.get...
}
getById(id: string) {
return this.http.get...
}
}
and my Car class like:
export class Car {
private brands: Array<Brand>;
constructor(public id: string, public name: string) {
this.brands = new Array<Brand>();
}
public getBrands(): Array<Brand> {
return this.brands;
}
//some other methods.
}
So I also have some data in the brands Array, but since the getById method creates a new car, it only takes the parameter id and name, and the brands array becomes empty! I don't know how to get the data from the server side so that it includes the array of brands!
I've (desperately) tried creating an Car in my service, which does log the correct data... but obviously doesn't work.
getById(id: string) {
this.http.get('app/car.json'+id)
.map((res: Response) => res.json())
.subscribe(car => {
//this code is executed when the response from the server arrives
this.car = car;
console.log("res: ", this.car); // <==== correct data!
return this.car;
});
//return this.car placed here doesn't give void error, but returns an undefined car, since the code gets executed before subscribe!
}
and receiving component:
routerOnActivate(curr: RouteSegment){
let id = curr.getParam('id');
this.car = this._service.getById(id); //error: Type 'void' is not assignable to type 'Car'
}
Any advice to give? Thanks!
It's been ages, but I thought I would post the solution to my problem. I had to create a static method to get the app to work. As follows:
getById(id:string)
return this.http.get('app/car.json'+id)
.map(data => data.json())
.map(data => Car.carFromJSON(data))
Then in my Car class:
static carFromJSON(json) {
let id = json.id
let name = json.name
let brands: Brand[] =
json.brands.map (brand => new Brand())
return new Car(id, name, brands)
}
You're initializing your car with empty array every time:
export class Car {
private brands: Array<Brand>;
constructor(public id: string, public name: string) {
this.brands = new Array<Brand>(); <-- every new Car() will end up with empty array
}
public getBrands(): Array<Brand> {
return this.brands;
}
//some other methods.
}
You have to extend your constructor with brands:
constructor(public id: string, public name: string, public brands: Brand[]) {}
And then call:
getById(id: string) {
return this.http.get('app/car.json'+id)
.map(data => data.json())
.map(car => new Car(car.id, car.name, car.brands)); // <-- add brands
}

How to use SignalR with Angular 2?

How to use SignalR with Angular 2?
How to manually run change detection when receiving data from SignalR?
I recently wrote an article that demonstrates one way to integrate Angular 2 and SignalR using a "channel/event" model:
https://blog.sstorie.com/integrating-angular-2-and-signalr-part-2-of-2/
I don't think just linking to another site is considered appropriate, so here's the core of the Angular 2 service that exposes SignalR:
import {Injectable, Inject} from "angular2/core";
import Rx from "rxjs/Rx";
/**
* When SignalR runs it will add functions to the global $ variable
* that you use to create connections to the hub. However, in this
* class we won't want to depend on any global variables, so this
* class provides an abstraction away from using $ directly in here.
*/
export class SignalrWindow extends Window {
$: any;
}
export enum ConnectionState {
Connecting = 1,
Connected = 2,
Reconnecting = 3,
Disconnected = 4
}
export class ChannelConfig {
url: string;
hubName: string;
channel: string;
}
export class ChannelEvent {
Name: string;
ChannelName: string;
Timestamp: Date;
Data: any;
Json: string;
constructor() {
this.Timestamp = new Date();
}
}
class ChannelSubject {
channel: string;
subject: Rx.Subject<ChannelEvent>;
}
/**
* ChannelService is a wrapper around the functionality that SignalR
* provides to expose the ideas of channels and events. With this service
* you can subscribe to specific channels (or groups in signalr speak) and
* use observables to react to specific events sent out on those channels.
*/
#Injectable()
export class ChannelService {
/**
* starting$ is an observable available to know if the signalr
* connection is ready or not. On a successful connection this
* stream will emit a value.
*/
starting$: Rx.Observable<any>;
/**
* connectionState$ provides the current state of the underlying
* connection as an observable stream.
*/
connectionState$: Rx.Observable<ConnectionState>;
/**
* error$ provides a stream of any error messages that occur on the
* SignalR connection
*/
error$: Rx.Observable<string>;
// These are used to feed the public observables
//
private connectionStateSubject = new Rx.Subject<ConnectionState>();
private startingSubject = new Rx.Subject<any>();
private errorSubject = new Rx.Subject<any>();
// These are used to track the internal SignalR state
//
private hubConnection: any;
private hubProxy: any;
// An internal array to track what channel subscriptions exist
//
private subjects = new Array<ChannelSubject>();
constructor(
#Inject(SignalrWindow) private window: SignalrWindow,
#Inject("channel.config") private channelConfig: ChannelConfig
) {
if (this.window.$ === undefined || this.window.$.hubConnection === undefined) {
throw new Error("The variable '$' or the .hubConnection() function are not defined...please check the SignalR scripts have been loaded properly");
}
// Set up our observables
//
this.connectionState$ = this.connectionStateSubject.asObservable();
this.error$ = this.errorSubject.asObservable();
this.starting$ = this.startingSubject.asObservable();
this.hubConnection = this.window.$.hubConnection();
this.hubConnection.url = channelConfig.url;
this.hubProxy = this.hubConnection.createHubProxy(channelConfig.hubName);
// Define handlers for the connection state events
//
this.hubConnection.stateChanged((state: any) => {
let newState = ConnectionState.Connecting;
switch (state.newState) {
case this.window.$.signalR.connectionState.connecting:
newState = ConnectionState.Connecting;
break;
case this.window.$.signalR.connectionState.connected:
newState = ConnectionState.Connected;
break;
case this.window.$.signalR.connectionState.reconnecting:
newState = ConnectionState.Reconnecting;
break;
case this.window.$.signalR.connectionState.disconnected:
newState = ConnectionState.Disconnected;
break;
}
// Push the new state on our subject
//
this.connectionStateSubject.next(newState);
});
// Define handlers for any errors
//
this.hubConnection.error((error: any) => {
// Push the error on our subject
//
this.errorSubject.next(error);
});
this.hubProxy.on("onEvent", (channel: string, ev: ChannelEvent) => {
//console.log(`onEvent - ${channel} channel`, ev);
// This method acts like a broker for incoming messages. We
// check the interal array of subjects to see if one exists
// for the channel this came in on, and then emit the event
// on it. Otherwise we ignore the message.
//
let channelSub = this.subjects.find((x: ChannelSubject) => {
return x.channel === channel;
}) as ChannelSubject;
// If we found a subject then emit the event on it
//
if (channelSub !== undefined) {
return channelSub.subject.next(ev);
}
});
}
/**
* Start the SignalR connection. The starting$ stream will emit an
* event if the connection is established, otherwise it will emit an
* error.
*/
start(): void {
// Now we only want the connection started once, so we have a special
// starting$ observable that clients can subscribe to know know if
// if the startup sequence is done.
//
// If we just mapped the start() promise to an observable, then any time
// a client subscried to it the start sequence would be triggered
// again since it's a cold observable.
//
this.hubConnection.start()
.done(() => {
this.startingSubject.next();
})
.fail((error: any) => {
this.startingSubject.error(error);
});
}
/**
* Get an observable that will contain the data associated with a specific
* channel
* */
sub(channel: string): Rx.Observable<ChannelEvent> {
// Try to find an observable that we already created for the requested
// channel
//
let channelSub = this.subjects.find((x: ChannelSubject) => {
return x.channel === channel;
}) as ChannelSubject;
// If we already have one for this event, then just return it
//
if (channelSub !== undefined) {
console.log(`Found existing observable for ${channel} channel`)
return channelSub.subject.asObservable();
}
//
// If we're here then we don't already have the observable to provide the
// caller, so we need to call the server method to join the channel
// and then create an observable that the caller can use to received
// messages.
//
// Now we just create our internal object so we can track this subject
// in case someone else wants it too
//
channelSub = new ChannelSubject();
channelSub.channel = channel;
channelSub.subject = new Rx.Subject<ChannelEvent>();
this.subjects.push(channelSub);
// Now SignalR is asynchronous, so we need to ensure the connection is
// established before we call any server methods. So we'll subscribe to
// the starting$ stream since that won't emit a value until the connection
// is ready
//
this.starting$.subscribe(() => {
this.hubProxy.invoke("Subscribe", channel)
.done(() => {
console.log(`Successfully subscribed to ${channel} channel`);
})
.fail((error: any) => {
channelSub.subject.error(error);
});
},
(error: any) => {
channelSub.subject.error(error);
});
return channelSub.subject.asObservable();
}
// Not quite sure how to handle this (if at all) since there could be
// more than 1 caller subscribed to an observable we created
//
// unsubscribe(channel: string): Rx.Observable<any> {
// this.observables = this.observables.filter((x: ChannelObservable) => {
// return x.channel === channel;
// });
// }
/** publish provides a way for calles to emit events on any channel. In a
* production app the server would ensure that only authorized clients can
* actually emit the message, but here we're not concerned about that.
*/
publish(ev: ChannelEvent): void {
this.hubProxy.invoke("Publish", ev);
}
}
Then a component could use this service by subscribing (not in the rxjs sense...) to a specific channel, and reacting to specific events emitted:
import {Component, OnInit, Input} from "angular2/core";
import {Http, Response} from "angular2/http";
import Rx from "rxjs/Rx";
import {ChannelService, ChannelEvent} from "./services/channel.service";
class StatusEvent {
State: string;
PercentComplete: number;
}
#Component({
selector: 'task',
template: `
<div>
<h4>Task component bound to '{{eventName}}'</h4>
</div>
<div class="commands">
<textarea
class="console"
cols="50"
rows="15"
disabled
[value]="messages"></textarea>
<div class="commands__input">
<button (click)="callApi()">Call API</button>
</div>
</div>
`
})
export class TaskComponent implements OnInit {
#Input() eventName: string;
#Input() apiUrl: string;
messages = "";
private channel = "tasks";
constructor(
private http: Http,
private channelService: ChannelService
) {
}
ngOnInit() {
// Get an observable for events emitted on this channel
//
this.channelService.sub(this.channel).subscribe(
(x: ChannelEvent) => {
switch (x.Name) {
case this.eventName: { this.appendStatusUpdate(x); }
}
},
(error: any) => {
console.warn("Attempt to join channel failed!", error);
}
)
}
private appendStatusUpdate(ev: ChannelEvent): void {
// Just prepend this to the messages string shown in the textarea
//
let date = new Date();
switch (ev.Data.State) {
case "starting": {
this.messages = `${date.toLocaleTimeString()} : starting\n` + this.messages;
break;
}
case "complete": {
this.messages = `${date.toLocaleTimeString()} : complete\n` + this.messages;
break;
}
default: {
this.messages = `${date.toLocaleTimeString()} : ${ev.Data.State} : ${ev.Data.PercentComplete} % complete\n` + this.messages;
}
}
}
callApi() {
this.http.get(this.apiUrl)
.map((res: Response) => res.json())
.subscribe((message: string) => { console.log(message); });
}
}
I tried to map the SignalR concepts into observables, but I'm still learning how to effectively use RxJS. In any case I hope that helps show how this might work in the context of an Angular 2 app.
You can also try using ng2-signalr.
npm install ng2-signalr --save
takes care of ng2 change detection using zones
allows your server events to be listened to using rxjs.
Here is the link to the source.
You didn't specified wich syntax you're using to develop your Angular 2 app.
I will assume you're using typescript.
One approach is use Definitely Typed files.
1 - You'll need to download a Definitely Typed JQuery:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/jquery/jquery.d.ts
2 - After this, download a Definitely typed SignalR:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/signalr/signalr.d.ts
3 - Add the JQuery refence in your Component:
/// <reference path="../jquery.d.ts" />
4 - Now, you can call SignalR methods with intelissense. But you will need to use the Late Binding approach:
var connection = $.hubConnection();
var proxy = connection.createHubProxy(proxy.on("newOrder", (order) => console.log(order));
connection.start();
As far as examples go, there probably aren't any yet. Welcome to the beginning of a framework. But do keep checking over time because as popularity and adoption increases, there will sure to be many examples.
As far as running change detection, that's a very vague question as angular2's change detection is now very different, and much improved.
My approach is to just let angular2 handle it, and not trigger a manual change detection at all as most of the time Angular2 picks up on the change and re-renders the view.
If that does not work, then the next step is to trigger .run() on the NgZone
example:
import {NgZone, Component} from 'angular2/core';
#Component({...})
export class MyComponent{
myProperty: string = 'Hello';
constructor(myService: MyService, ngZone: NgZone){}
doSomething(){
this.myService.doSomething().then(x => {
this.ngZone.run(() => {
this.myProperty = x;
});
});
}
}
Again though, I have found that even working with asynchronous code, angular2 usually picks up on the change without using ngZone at all.

Adding observer for KVO without pointers using Swift

In Objective-C, I would normally use something like this:
static NSString *kViewTransformChanged = #"view transform changed";
// or
static const void *kViewTransformChanged = &kViewTransformChanged;
[clearContentView addObserver:self
forKeyPath:#"transform"
options:NSKeyValueObservingOptionNew
context:&kViewTransformChanged];
I have two overloaded methods to choose from to add an observer for KVO with the only difference being the context argument:
clearContentView.addObserver(observer: NSObject?, forKeyPath: String?, options: NSKeyValueObservingOptions, context: CMutableVoidPointer)
clearContentView.addObserver(observer: NSObject?, forKeyPath: String?, options: NSKeyValueObservingOptions, kvoContext: KVOContext)
With Swift not using pointers, I'm not sure how to dereference a pointer to use the first method.
If I create my own KVOContext constant for use with the second method, I wind up with it asking for this:
let test:KVOContext = KVOContext.fromVoidContext(context: CMutableVoidPointer)
EDIT: What is the difference between CMutableVoidPointer and KVOContext? Can someone give me an example how how to use them both and when I would use one over the other?
EDIT #2: A dev at Apple just posted this to the forums: KVOContext is going away; using a global reference as your context is the way to go right now.
There is now a technique officially recommended in the documentation, which is to create a private mutable variable and use its address as the context.
(Updated for Swift 3 on 2017-01-09)
// Set up non-zero-sized storage. We don't intend to mutate this variable,
// but it needs to be `var` so we can pass its address in as UnsafeMutablePointer.
private static var myContext = 0
// NOTE: `static` is not necessary if you want it to be a global variable
observee.addObserver(self, forKeyPath: …, options: [], context: &MyClass.myContext)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if context == &myContext {
…
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
Now that KVOContext is gone in Xcode 6 beta 3, you can do the following. Define a global (i.e. not a class property) like so:
let myContext = UnsafePointer<()>()
Add an observer:
observee.addObserver(observer, forKeyPath: …, options: nil, context: myContext)
In the observer:
override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafePointer<()>) {
if context == myContext {
…
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
Swift 4 - observing contentSize change on UITableViewController popover to fix incorrect size
I had been searching for an answer to change to a block based KVO because I was getting a swiftlint warning and it took me piecing quite a few different answers together to get to the right solution. Swiftlint warning:
Block Based KVO Violation: Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. (block_based_kvo).
My use case was to present a popover controller attached to a button in a Nav bar in a view controller and then resize the popover once it's showing - otherwise it would be too big and not fitting the contents of the popover. The popover itself was a UITableViewController that contained static cells, and it was displayed via a Storyboard segue with style popover.
To setup the block based observer, you need the following code inside your popover UITableViewController:
// class level variable to store the statusObserver
private var statusObserver: NSKeyValueObservation?
// Create the observer inside viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
statusObserver = tableView.observe(\UITableView.contentSize,
changeHandler: { [ weak self ] (theTableView, _) in self?.popoverPresentationController?.presentedViewController.preferredContentSize = theTableView.contentSize
})
}
// Don't forget to remove the observer when the popover is dismissed.
override func viewDidDisappear(_ animated: Bool) {
if let observer = statusObserver {
observer.invalidate()
statusObserver = nil
}
super.viewDidDisappear(animated)
}
I didn't need the previous value when the observer was triggered, so left out the options: [.new, .old] when creating the observer.
Update for Swift 4
Context is not required for block-based observer function and existing #keyPath() syntax is replaced with smart keypath to achieve swift type safety.
class EventOvserverDemo {
var statusObserver:NSKeyValueObservation?
var objectToObserve:UIView?
func registerAddObserver() -> Void {
statusObserver = objectToObserve?.observe(\UIView.tag, options: [.new, .old], changeHandler: {[weak self] (player, change) in
if let tag = change.newValue {
// observed changed value and do the task here on change.
}
})
}
func unregisterObserver() -> Void {
if let sObserver = statusObserver {
sObserver.invalidate()
statusObserver = nil
}
}
}
Complete example using Swift:
//
// AppDelegate.swift
// Photos-MediaFramework-swift
//
// Created by Phurg on 11/11/16.
//
// Displays URLs for all photos in Photos Library
//
// #see http://stackoverflow.com/questions/30144547/programmatic-access-to-the-photos-library-on-mac-os-x-photokit-photos-framewo
//
import Cocoa
import MediaLibrary
// For KVO: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-ID12
private var mediaLibraryLoaded = 1
private var rootMediaGroupLoaded = 2
private var mediaObjectsLoaded = 3
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var mediaLibrary : MLMediaLibrary!
var allPhotosAlbum : MLMediaGroup!
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSLog("applicationDidFinishLaunching:");
let options:[String:Any] = [
MLMediaLoadSourceTypesKey: MLMediaSourceType.image.rawValue, // Can't be Swift enum
MLMediaLoadIncludeSourcesKey: [MLMediaSourcePhotosIdentifier], // Array
]
self.mediaLibrary = MLMediaLibrary(options:options)
NSLog("applicationDidFinishLaunching: mediaLibrary=%#", self.mediaLibrary);
self.mediaLibrary.addObserver(self, forKeyPath:"mediaSources", options:[], context:&mediaLibraryLoaded)
NSLog("applicationDidFinishLaunching: added mediaSources observer");
// Force load
self.mediaLibrary.mediaSources?[MLMediaSourcePhotosIdentifier]
NSLog("applicationDidFinishLaunching: done");
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
NSLog("observeValue: keyPath=%#", keyPath!)
let mediaSource:MLMediaSource = self.mediaLibrary.mediaSources![MLMediaSourcePhotosIdentifier]!
if (context == &mediaLibraryLoaded) {
NSLog("observeValue: mediaLibraryLoaded")
mediaSource.addObserver(self, forKeyPath:"rootMediaGroup", options:[], context:&rootMediaGroupLoaded)
// Force load
mediaSource.rootMediaGroup
} else if (context == &rootMediaGroupLoaded) {
NSLog("observeValue: rootMediaGroupLoaded")
let albums:MLMediaGroup = mediaSource.mediaGroup(forIdentifier:"TopLevelAlbums")!
for album in albums.childGroups! {
let albumIdentifier:String = album.attributes["identifier"] as! String
if (albumIdentifier == "allPhotosAlbum") {
self.allPhotosAlbum = album
album.addObserver(self, forKeyPath:"mediaObjects", options:[], context:&mediaObjectsLoaded)
// Force load
album.mediaObjects
}
}
} else if (context == &mediaObjectsLoaded) {
NSLog("observeValue: mediaObjectsLoaded")
let mediaObjects:[MLMediaObject] = self.allPhotosAlbum.mediaObjects!
for mediaObject in mediaObjects {
let url:URL? = mediaObject.url
// URL does not extend NSObject, so can't be passed to NSLog; use string interpolation
NSLog("%#", "\(url)")
}
}
}
}

Resources