Apple Photos: observing MLMediaGroup changes using MediaLibrary on macOS - objective-c

I need to display Album structure of Apple Photos in my application on macOS. The only way to do it that I could find is MediaLibrary framework. I can traverse the tree structure using MLMediaGroup class without problems. But watching changes in it makes me puzzled. I would expect that I need to subscribe to KVO-notificatios for each individual media group and watch the changes in attributes.
But in reality when I create a new album/folder in Photos I get a notification that the rootMediaGroup has changed, and the addresses of this group as well as all child groups also change. This seems strange because now I have to rebuild the whole album-hierarchy. This is like watching directory changes and getting opaque notifications like "something in the filesystem has changed" without further details each time a user modifies a directory somewhere deep inside $HOME.
I'm using objective-c (in fact I need to integrate it with c++), although swift examples may also give me a hint. Below I provide a minimal example to illustrate the question.
PhotosObserver.h
#import <Foundation/Foundation.h>
#import <MediaLibrary/MediaLibrary.h>
#interface PhotosObserver: NSObject
{
#private
MLMediaLibrary *ml;
MLMediaSource *src;
MLMediaGroup *root;
}
#property(nonatomic, retain, nullable, readonly) MLMediaLibrary *ml;
#property(nonatomic, retain, nullable, readonly) MLMediaSource *src;
#property(nonatomic, retain, nullable, readonly) MLMediaGroup *root;
#end
PhotosObserver.m
#include "PhotosObserver.h"
static void* CTX_MEDIASOURCE = &CTX_MEDIASOURCE;
static void* CTX_MEDIAGROUP = &CTX_MEDIAGROUP;
static NSString* KPATH_SOURCES = #"mediaSources";
static NSString* KPATH_GROUPS = #"rootMediaGroup";
#implementation PhotosObserver
#synthesize ml;
#synthesize src;
#synthesize root;
-(id)init {
if(self = [super init]) {
self->ml = [[MLMediaLibrary alloc] init];
[self->ml addObserver: self forKeyPath:KPATH_SOURCES options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:CTX_MEDIASOURCE];
[self->ml mediaSources];
}
return self;
}
-(void)dealloc {
#try {
[self.ml removeObserver:self forKeyPath:KPATH_SOURCES context:CTX_MEDIASOURCE];
[self.src removeObserver:self forKeyPath:KPATH_GROUPS context:CTX_MEDIAGROUP];
}
#catch(NSException *) {}
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if(CTX_MEDIASOURCE == context)
{
NSLog(#"Loaded mediaSources");
self->src = [self->ml.mediaSources objectForKey:/*#"com.apple.Photos"*/MLMediaSourcePhotosIdentifier];
assert(self->src);
[self.src addObserver: self forKeyPath:KPATH_GROUPS options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:CTX_MEDIAGROUP];
[self.src rootMediaGroup];
}
else if(CTX_MEDIAGROUP == context)
{
MLMediaGroup *old = self->root;
if(!old)
NSLog(#"Loaded rootMediaGroup");
self->root = self.src.rootMediaGroup;
if(old && old != self.root)
{
NSLog(#"Oops, root group has changed: %# -> %#", old, self.root);
}
}
}
#end
When I run the program and create a folder/album, or rename an album in Photos, I get a notification that the root group is changed. For some reason I don't get notifications in 100% of cases but anyway quite often.
Q: Why does rootMediaGroup get changed at all, since the user cannot change it in Photos UI?
Q: How to watch for the changes without the need to rescan the whole MLMediaGroup hierarchy after each small user modification?

Related

KVO for one-to-many but NSNull object passed into observeValueForKeyPath

I have one managed object with a one-to-many relationship to member class. When I add the observers for members, it worked. When one new member is added to the relationship, the observeValueForKeyPath will be invoked with the new object and change dictionary contains the new member object. However, observeValueForKeyPath will be triggered second time with all values nil and change dictionary new="NULL". What is the second trigger? I set a breakpoint, but not sure who made the trigger.
#interface FooObject : NSManagedObject {}
#property (nonatomic, strong) NSString *fooId;
#property (nonatomic, strong) NSSet* members;
#end
#implementation FooObject
#dynamic fooId;
#dynamic members;
- (NSMutableSet*)membersSet {
[self willAccessValueForKey:#"members"];
NSMutableSet *result = (NSMutableSet*)[self mutableSetValueForKey:#"members"];
[self didAccessValueForKey:#"members"];
return result;
}
- (void)registerObservers {
[self addObserver:self
forKeyPath:#"members"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)unregisterObservers {
#try{
[self removeObserver:self forKeyPath:#"members"];
}#catch(id anException){
//do nothing, obviously it wasn't attached because an exception was thrown
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
id valueNewSet = [change objectForKey:NSKeyValueChangeNewKey];
if (![valueNewSet isKindOfClass:[NSSet class]]) {
// not a NSSet, it contains <null> value
NSLog(#"%#", change);
NSLog(#"%#", object);
}
if ([[change objectForKey:NSKeyValueChangeKindKey] intValue] == NSKeyValueChangeInsertion) {
// insert change is valid, process the changes
}
}
#end
Log output:
{
kind = 1;
new = "<null>";
}
<FooObject: 0xfa9cc60> (entity: FooObject; id: 0xfa9be00 <x-coredata://39DB31FD-6795-4FDE-B700-819AB22E5170/SHInterest/p6> ; data: {
fooId = nil;
members = nil;
})
EDIT 1
I set a breakpoint at NSLog(#"%#", change);
This is the stack trace but not really helpful to figure who makes this call.
main -> UIApplicationMain -> NSKeyValueNotifyObserver -> observeValueForKeyPath:ofObject:change:context
EDIT 2
Maybe this is still a bug?
http://www.cocoabuilder.com/archive/cocoa/182567-kvo-observevalueforkeypath-not-reflecting-changes.html
I am running into the same issue where the "implicit" assignment (and hence the notification of the KVO observer) apparently occurs only as part of deallocating MOs: I save a child MOC, then release it, then iOS releases its MOs. I assume that it then sets one-to-many relationships temporarily to NSNull in the process of deallocating related MOs (where delete rule cascade applies).
So next to change kinds for insertion and deletion my KVO observer now also accepts NSKeyValueChangeSetting and asserts change[NSKeyValueChangeNewKey] == NSNull.null. Call it a pragmatic solution.

KVO with Run-to-Completion semantics - Is it possible?

I recently ran into reentrancy issues with KVO. To visualize the problem, I would like to show a minimal example. Consider the interface of an AppDelegate class
#interface AppDelegate : UIResponder <UIApplicationDelegate>
#property (strong, nonatomic) UIWindow *window;
#property (nonatomic) int x;
#end
as well as its implementation
#implementation AppDelegate
- (BOOL) application:(__unused UIApplication *)application
didFinishLaunchingWithOptions:(__unused NSDictionary *)launchOptions
{
__unused BigBugSource *b = [[BigBugSource alloc] initWithAppDelegate:self];
self.x = 42;
NSLog(#"%d", self.x);
return YES;
}
#end
Unexpectedly, this program prints 43 to the console.
Here's why:
#interface BigBugSource : NSObject {
AppDelegate *appDelegate;
}
#end
#implementation BigBugSource
- (id)initWithAppDelegate:(AppDelegate *)anAppDelegate
{
self = [super init];
if (self) {
appDelegate = anAppDelegate;
[anAppDelegate addObserver:self
forKeyPath:#"x"
options:NSKeyValueObservingOptionNew
context:nil];
}
return self;
}
- (void)dealloc
{
[appDelegate removeObserver:self forKeyPath:#"x"];
}
- (void)observeValueForKeyPath:(__unused NSString *)keyPath
ofObject:(__unused id)object
change:(__unused NSDictionary *)change
context:(__unused void *)context
{
if (appDelegate.x == 42) {
appDelegate.x++;
}
}
#end
As you see, some different class (that may be in third-party code you do not have access to) may register an invisible observer to a property. This observer is then called synchronously, whenever the property's value has changed.
Because the call happens during the execution of another function, this introduces all sort of concurrency / multithreading bugs although the program runs on a single thread. Worse, the change happens without an explicit notice in the client-code (OK, you could expect that concurrency issues arise whenever you set a property...).
What is the best practice to solve this problem in Objective-C?
Is there some common solution to regain run-to-completion semantics automatically, meaning that KVO-Observation messages go through an event-queue, AFTER the current method finishes executing and invariants / postconditions are restored?
Not exposing any properties?
Guarding every critical function of an object with a boolean variable to ensure that reentrancy is not possible?
For example: assert(!opInProgress); opInProgress = YES; at the beginning of the methods, and opInProgress = NO; at the end of the methods. This would at least reveal those kind of bugs directly during runtime.
Or is it possible to opt out of KVO somehow?
Update
Based on the answer by CRD, here is the updated code:
BigBugSource
- (void)observeValueForKeyPath:(__unused NSString *)keyPath
ofObject:(__unused id)object
change:(__unused NSDictionary *)change
context:(__unused void *)context
{
if (appDelegate.x == 42) {
[appDelegate willChangeValueForKey:#"x"]; // << Easily forgotten
appDelegate.x++; // Also requires knowledge of
[appDelegate didChangeValueForKey:#"x"]; // whether or not appDelegate
} // has automatic notifications
}
AppDelegate
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:#"x"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
- (BOOL) application:(__unused UIApplication *)application
didFinishLaunchingWithOptions:(__unused NSDictionary *)launchOptions
{
__unused BigBugSource *b = [[BigBugSource alloc] initWithAppDelegate:self];
[self willChangeValueForKey:#"x"];
self.x = 42;
NSLog(#"%d", self.x); // now prints 42 correctly
[self didChangeValueForKey:#"x"];
NSLog(#"%d", self.x); // prints 43, that's ok because one can assume that
// state changes after a "didChangeValueForKey"
return YES;
}
What you are asking for is manual change notification and is supported by KVO. It is a three stage process:
Your class overrides + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey returning NO for any property you wish to defer notifications for and deferring to super otherwise;
Before changing a property you call [self willChangeValueForKey:key]; and
When you are ready for the notification to occur you call [self didChangeValueForKey:key]
You can build on this protocol quite easily, e.g. it is easy to keep a record of keys you have changed and trigger them all before you exit.
You can also use willChangeValueForKey: and didChangeValueForKey with automatic notifications turned on if you directly alter the backing variable of a property and need to trigger KVO.
The process along with an examples is described in Apple's documentation.

If I write a custom property getter method, will KVO still operate if the getter returns a value by accessing a value from another object?

Assume that I have a class with a readonly property on it.
//MyClass.h
#interface MyClass
#property (readonly) NSInteger MonitorMe;
#end
Now, let's assume the point of this property is to monitor changes of another property, within another object, and when the property is "observed" it returns a derived value by inspecting a value from the other, external object.
//MyClass.m
#implementation
#synthesize MonitorMe;
-(NSInteger) getMonitorMe
{
return globalStaticClass.OtherNSInteger;
}
... Inits and Methods ...
#end
Now, let's assume that some where I create an instance of the MyClass object, and I want to add a KVO observer on the MonitorMe property.
//AnotherClass.m
#implementation AnotherClass.m
#synthesize instanceOfMyClass;
-(id)init
{
...
instanceOfMyMethod = [MyClass init];
[MyClass addObserver: self
forKeyPath: #"MonitorMe"
options: NSKeyValuObservingOptionNew
context: nil];
...
}
My question is, since the MonitorMe property only monitors the changes of values in an external object, will the observer method execute when the value of globalStaticClass.OtherNSInteger changes? Also, if the answer is yes, how is this done?
If this works, it would seem like compiler voodoo to me.
Note
I don't think it makes a difference, but I am using ARC for this implementation and I'm compiling for an iOS device. I doubt there are compilation differences between OS X and iOS for this type of question but, if it matters, I have an iOS project that requires such an implementation outlined above.
Also, the example outlined above is a very basic setup of my actual needs. It could be argued that I could/should add an observation to the globalStaticClass.OtherNSInteger value instead of the readonly property, MonitorMe. In my actual circumstance that answer is not sufficient because my readonly property is much more complex than my example.
will the observer method execute when the value of globalStaticClass.OtherNSInteger changes?
No, but you can make that happen, via +keyPathsForValuesAffectingMonitorMe (or the more generic +keyPathsForValuesAffectingValueForKey:, if the "globalStaticClass" is actually a property of MyClass. See "Registering Dependent Keys" in the KVO Guide.
Here's a quick mockup:
#import <Foundation/Foundation.h>
#interface Monitored : NSObject
#property NSInteger otherInteger;
#end
#implementation Monitored
#synthesize otherInteger;
#end
#interface Container : NSObject
#property (readonly) NSInteger monitorMe;
#property (strong) Monitored * theMonitored;
- (void)changeMonitoredInteger;
#end
#implementation Container
#synthesize theMonitored;
+ (NSSet *)keyPathsForValuesAffectingMonitorMe {
return [NSSet setWithObject:#"theMonitored.otherInteger"];
}
- (id) init {
self = [super init];
if( !self ) return nil;
theMonitored = [[Monitored alloc] init];
[theMonitored setOtherInteger:25];
return self;
}
- (NSInteger)monitorMe
{
return [[self theMonitored] otherInteger];
}
- (void)changeMonitoredInteger {
[[self theMonitored] setOtherInteger:arc4random()];
}
#end
#interface Observer : NSObject
#end
#implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"Observing change in: %# %#", keyPath, object);
}
#end
int main(int argc, const char * argv[])
{
#autoreleasepool {
Observer * o = [[Observer alloc] init];
Container * c = [[Container alloc] init];
[c addObserver:o
forKeyPath:#"monitorMe"
options:NSKeyValueObservingOptionNew
context:NULL];
[c changeMonitoredInteger];
[c changeMonitoredInteger];
}
return 0;
}
P.S. Cocoa style notes: properties/variables should have lowercase initial letters, and (this is actually more important now because of ARC) don't name accessor methods to start with "get" -- that has a specific meaning in Cocoa involving passing in a buffer and getting data back by reference.

What is best practice to interaction with object in objective-c?

My questions is next:
For example I have object A (this is data model object). Assume that object A have some property (for example request property). Also I have object B (this is my view object).
So my problem is next: when my data model will be changed (the value for request property changed) I want to know about this events in my view (object B)
How to create this interaction between object.
For example in request is written to "some_value" and after this object B immediately know about it.
Thanks for response!
You can use delegation pattern, NSNotifications, callback blocks and even KVO. Choice depends on situation, in your case delegate or callback block would work.
I would use Key Value Observing. Your view controller (not the view itself) would set itself up as an observer for the data model object and when it gets observer notifications, it would update the view.
[myDataObject addObserver: myViewController
forKeyPath: #"request"
options: NSKeyValueObservingOptionNew
context: nil];
// in the view controller you need
-(void) observeValueForKeyPath: (NSString*) path
ofObject: (id) aDataObject
change: (NSDictionary*) changeDictionary
context: (void*) context]
{
if (aDataObject == myDataObject
&& [path isEqualToString: #"request"])
{
// change you are interested in
}
// Call suoer implementation of this method if it implements it
}
Don't forget to remove the observer when you are done with it.
Also, be careful in a threaded environment. Observations are notified on the same thread that the change happens on. If this is not the main thread, you'll need to use -performSelectorOnMainThread:withObject:waitUntilDone: to make any changes to the UI.
If you just want object B to know whats up I would suggest using delegation.
If maybe later you want object C, D and E to know too what happend in object A i would suggest using NSNotification.
For example I have class DataModel. In this step I add observer for my property str. For object I will send my view controller.
.h
#import <Foundation/Foundation.h>
#interface DataModel : NSObject
#property (strong, nonatomic) NSString *str;
- (void)setUpObserver:(id)object;
#end
.m
#import "DataModel.h"
#implementation DataModel
#synthesize str;
- (void)setUpObserver:(id)object
{
[self addObserver:object forKeyPath: #"str" options: NSKeyValueObservingOptionNew context: nil];
}
#end
In my view controller
#import "DataModel.h"
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
dm = [[DataModel alloc] init];
[dm setUpObserver:self];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (object == dm && [keyPath isEqualToString: #"str"])
{
NSLog(#"it's work");
}
}
- (IBAction)changeValue:(id)sender {
dm.str = #"test change value";
}
#end
This is my realization of KVO. Thanks JeremyP for explanation.

How to change all my application texts (UILabel, UITableCell.....) color?

I would like to do something similar in CSS than "BODY {color:red;}" but in objectif C. I mean if I have 10 differents UIView, I would like to change all the UIView texts color in one time.
Cheers
Simple case - you somehow gathered them together
for (UIView *v in styledViews) {
// apply current style here
}
I doubt that this is your case
Complex case - there are tons of styled views everywhere.
Disclaimer: I can't guarantee anything about following code, it works on my simulator, which doesn't mean it will not blow up in user's hands. I wrote it because it was fun and may help Thomas to solve his problem. I didn't check documentation thoroughly because it's already 5 a.m. here
1) Encapsulate style stuff in some StyleManager class (in this example applyCurrentStyle: will apply current style to any view passed to it). It should post notification each time style is changed (e.g. kStyleManagerNotificationStyleChanged)
2) Make UIView category (like UIView+Style) with public setStyleManager: method.
3) Implement it:
#import "UIView+Style.h"
#import <objc/runtime.h>
#interface StyleSubscription : NSObject {
StyleManager *styleManager;
NSObject *subscriber;
}
#property (readonly) StyleManager *styleManager;
- (id)initWithStyleManager:(NSObject*)p subscriber:(NSObject*)s;
#end
#implementation StyleSubscription
#synthesize styleManager;
- (id)initWithStyleManager:(StyleManager*)sManager subscriber:(NSObject*)s {
if (self = [super init]) {
styleManager = [sManager retain];
subscriber = s;
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:subscriber
name:kStyleManagerNotificationStyleChanged
object:styleManager];
[styleManager release];
[super dealloc];
}
#end
#implementation UIView (Style)
static char styleSubsriptionKey;
- (StyleManager*)styleManager {
StyleSubscription *s = objc_getAssociatedObject(self, &styleSubsriptionKey);
return s.styleManager;
}
- (void)styleChanged:(NSNotification*)n {
[[self styleManager] applyCurrentStyle:self];
}
- (void)setStyleManager:(StyleManager*)sManager {
if ([self styleManager] == sManager) {
return;
}
StyleSubscription *subscr = nil;
if (sManager != nil) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(styleChanged:)
name:kStyleManagerNotificationStyleChanged
object:sManager];
subscr = [[[StyleSubscription alloc] initWithStyleManager:sManager
subscriber:self] autorelease];
}
objc_setAssociatedObject(self, &styleSubsriptionKey, subscr, OBJC_ASSOCIATION_RETAIN);
[sManager applyCurrentStyle:self];
}
#end
Each time style manager posts notification correspondent views will be updated with a new style. View will unsubscribe from style notifications automatically upon deallocation. Style manager can be removed explicitly [view setStyleManager:nil].
you can fix this in a nice way.here is a tutorial
http://dot-ios.blogspot.com/2013/02/design-uilabel-in-optimize-way-for.html