How to connect Model through Controller to View using bindings? - objective-c

I have an NSTextField in my view. Its value is bound to an NSNumber *number in my controller. The controller simply calls through to the model (value) to get the appropriate value.
// In the controller
- (NSNumber *)number {
return [NSNumber numberWithFloat:[model value]];
}
- (void)setNumber:(NSNumber *)aNumber {
[model setValue:[aNumber floatValue]];
}
This is fine, only the controller is not notified of model changes and thus, changing the value in the model does not update the NSTextField.
The only other option I can think of is to have the model notify the controller and the controller manually update the view through an Outlet. But this circumvents the binding.
// In the model, after value change
[[NSNotificationCenter defaultCenter] postNotificationName:#"ValueChanged" object:self];
// In the controller, after being notified
- (void)updateView:(NSNotification *)aNotification {
[myTextField setFloatValue:[model value]];
}
Is there a better, binding-aware way to implement this communication?

I have an NSTextField in my view. Its value is bound to an NSNumber *number in my controller. The controller simply calls through to the model (value) to get the appropriate value.
Why is this property wrapping the value in an NSNumber? KVC will convert primitive values, such as floats, to and from objects for you.
This is fine, only the controller is not notified of model changes and thus, changing the value in the model does not update the NSTextField.
The model changing isn't relevant here, and the controller doesn't need to know about the value of the model's property changing, unless the controller is observing that property—in which case, the controller is getting notified of those changes, because you are using the model's accessors for the property.
The problem is that you have not bound the text field through the model—you're binding it to a fake property on the controller instead.
So, assuming the controller has exposed a property for the model object, simply bind the text field to the controller with the key path model.value.

In your controller, override the keyPathsForValuesAffectingValueForKey: class method. Something like:
+(NSSet*) keyPathsForValuesAffectingValueForKey:(NSString*)key
{
if([key isEqual:#"number"])
return [NSSet setWithObject:#"model.value"];
return [NSSet set];
}
(I'm not at my Mac so this is untested; but the general idea should be sound)

Another option (and what I usually do myself, purely for personal preference) is something like (in the controller):
-(void)setModel:(id)m
{
[model removeObserver:self forKeyPath:#"value"];
[m retain];
[model release];
model = m;
[model addObserver:(self) forKeyPath:#"value" options:NSKeyValueObservingOptionNew context:NULL];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqual:#"value"])
{
[self willChangeValueForKey:#"number"];
[self didChangeValueForKey:#"number"];
}
}
Again, this code is untested, but the general idea should work.

Related

notification for a view x position

in a slide menu I'm developing for my project i would like to add a black view over the content view when it's slide out. To do this i need to create a method that check continuously the view x-position and darken or brighten up the black layer. The position of this view is the same as the content view.
I thought i can use a NSNotificationCenter like this:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(incomingNotification:) name:#"notification" object: darkViewController.view.frame.origin.x]];
and a method:
- (void) incomingNotification:(NSNotification *)notification{
// the dark layer alpha will be 0 at origin=0 and 0.8 at bounds.size.width
float alphaToUse = (darkViewController.view.frame.origin.x / self.view.bounds.size.width) * 0.8;
[darkViewController.view setAlpha:alphaToUse];
}
The problem is that i must use an object as parameter.
I'm new to notifications so i'm asking: is it wrong to use them for this kind of things?
Is it better to solve this in another way?
EDIT:
Following Denis advice i'm now trying to use the key-value-observe solution.
My app is structured like this:
MenuViewController-->ContainerViewController-->DarkViewController
In MenuViewController.m :
#interface MenuViewController ()
#property (strong,nonatomic) ContainerViewController *containerViewController;
#property (strong,nonatomic) DarkViewController *darkViewController;
#end
#implementation MenuViewController
#synthesize containerViewController,darkViewController;
# pragma mark - Views
- (void)viewDidLoad{
[super viewDidLoad];
containerViewController = [[ContainerViewController alloc]init];
[self addChildViewController:containerViewController];
[self.view addSubview:containerViewController.view];
[containerViewController didMoveToParentViewController:self];
darkViewController = [[DarkViewController alloc]init];
[containerViewController addChildViewController:darkViewController];
[containerViewController.view addSubview:darkViewController.view];
[darkViewController didMoveToParentViewController:containerViewController];
[UIView animateWithDuration:slideDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[darkViewController.view setAlpha:0.7];
containerViewController.view.frame = CGRectMake(self.view.frame.size.width - slideWidth, 0, self.view.frame.size.width, self.view.frame.size.height);
}
completion:^(BOOL finished) {
if (finished) {
}
}];
[darkViewController addObserver:self forKeyPath:#"darkViewController.view.frame.origin.x" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: (NSDictionary *)change context:(void *)context
{
NSLog(#"x is changed");
}
When i run this i get this exception:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<DarkViewController 0x10962d280> addObserver:<MenuViewController 0x10922c890> forKeyPath:#"darkViewController.view.frame.origin.x" options:1 context:0x0] was sent to an object that is not KVC-compliant for the "darkViewController" property.'
Ok, it seems that i found a solution following this example Notificationsin IOS
I just added this in the viewDidLoad of my ContainerViewController
[self addObserver:self forKeyPath:#"view.frame" options:0 context:nil];
and implemented the observer method with a for cycle to find my DarkViewController view
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
for (UIViewController * vc in self.childViewControllers) {
if ([vc isKindOfClass:[DarkViewController class]]) {
float alphaToUse = (self.view.frame.origin.x / self.view.bounds.size.width) * 0.8;
[vc.view setAlpha:alphaToUse];
}
}
}
Now i just have to understand where to put the removeObserver method, since my ContainerViewController will be always loaded...
There is another machanism in iOS for such kind of things called Key value coding and Key value observing.
From Notification Center documentation:
As you design your application, do not simply assume that you should send a notification to communicate with interested parties. You should also consider alternatives such as key-value observing, key-value binding, and delegation.
Key-value binding and key-value observing were introduced in OS X version 10.3 and provide a way of loosely coupling data. With key-value observing, you can request to be notified when the properties of another object change. Unlike regular notifications, there is no performance penalty for unobserved changes. There is also no need for the observed object to post a notification because the key-value observing system can do it for you automatically, although you can still choose do it manually.
So if you'll have another notification observers while making slide menu animation it may reduce its handling performance.
And the best solution would be to invoke incomingNotification: method inside the animation block (the method where animation performs).
Apple docs again:
Though key-value coding is efficient, it adds a level of indirection that is slightly slower than direct method invocations. You should use key-value coding only when you can benefit from the flexibility that it provides.
ANSWERING EDITED QUESTION:
This answer describes exactly what you're trying to do. When add the observer on some object's property object's name shouldn't be included in the property key path. So in you case adding an observer looks like this:
[darkViewController addObserver:self forKeyPath:#"view.frame" options:NSKeyValueObservingOptionNew context:nil];
When trying to observe some object property don't forget to ensure the object's class is KVC compliant for that property!
And also don't forget to remove the observers after job is done.

Interface Builder: Enabled1,2 property with OR operator

I need to set the property Enabled of a control in interface builder, depending on 2 booleans int he preferences.
However the operator should be OR and not AND. If one of the two is true, than my control should be enabled.
Currently, I can only make it work with an AND operator, (See screenshot).
Thanks
Unfortunately, in IB, you're stuck with and. My suggestion would be to add a new property to an object accessible to your NIB (possibly your owner for the NIB), which is dependent on changes to the other objects in order to enable your control/view.
It looks like you're using the Shared User Defaults Controller, so I would suggest that in the owner you create a new boolean property for your combined user defaults (perhaps downloadingCastOrCrew), and then you'll need to make sure that when either of the defaults change, you change the value of downloadingCastOrCrew:
In your Interface:
#property BOOL downloadingCastOrCrew;
In the implementation as you're setting up your controller or after awakeFromNib:
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self
forKeyPath: #"values.kSearchPreferencesDownloadCast"
options:NSKeyValueObservingOptionNew
context:NULL];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self
forKeyPath: #"values.kSearchPreferencesDownloadCrew"
options:NSKeyValueObservingOptionNew
context:NULL];
In the implementation as you're tearing down your controller:
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver: self
forKeyPath: #"values.kSearchPreferencesDownloadCast"];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver: self
forKeyPath: #"values.kSearchPreferencesDownloadCrew"];
Add an observer if you don't already have one:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
self.downloadingCastOrCrew = [defaults boolForKey: #"kSearchPreferencesDownloadCast"]
|| [defaults boolForKey: #"kSearchPreferencesDownloadCrew"];
}
By using the accessor method, you'll trigger kvo and you'll be able to use the controller's downloadingCastOrCrew as your boolean to check instead of the NSUserDefaults values directly.
Obviously, if you already have an observeValueForKeyPath, you will likely want to add a context value to the addObserver:forKeyPath:options:context call and check it in the observeValueForKeyPath:ofObject:change:context call.
You can do it in this way :
Create a third property
#property BOOL isFirst;
#property BOOL isSecond;
#property BOOL isTextFieldVisible;//this one is your third
- (IBAction)isSec:(id)sender;
- (IBAction)isFir:(id)sender;
In implementation
- (id)init
{
self = [super init];
if (self) {
self.isFirst=NO;
self.isSecond=NO;
}
return self;
}
- (IBAction)isSec:(id)sender {
self.isSecond=!self.isSecond;
[sender setTitle:[NSString stringWithFormat:#"isSecond: %d",self.isSecond]];
self.isTextFieldVisible=self.isFirst || self.isSecond;
self.isTextFieldVisible=!self.isTextFieldVisible;
NSLog(#"->%d",self.isTextFieldVisible);
}
- (IBAction)isFir:(id)sender {
self.isFirst=!self.isFirst;
[sender setTitle:[NSString stringWithFormat:#"isfirst: %d",self.isFirst]];
self.isTextFieldVisible=self.isFirst || self.isSecond;
self.isTextFieldVisible=!self.isTextFieldVisible;
NSLog(#"->%d",self.isTextFieldVisible);
}
#end
And in the binding just bind the textField to third property,
Check the running application here.
EDIT 1:
Change ValueTransformer NSNegateBoolean in IB. so that my two lines self.isTextFieldVisible=!self.isTextFieldVisible; is not required in both the IBAction.

Key-value observing for QLPreviewController.currentPreviewItemIndex

I have an object that need to be notified when a QLPreviewController changes the shown document. QLPreviewController have the property currentPreviewItemIndex that is updated when the document change. I've added my object as observer for currentPreviewItemIndex and it receives the notification when in my code is changed the property, so far so good.
The problem is that the user can change the shown document swiping in the screen and I've found out that in this case the notification isn't generated.
Any solution to receive the notification also in this case? I suppose that the notification is generated when is called the setter of the property currentPreviewItemIndex and probably when the user swipe the property is changed internally in the object QLPreviewController.
Another solution may be to disable the horizontal swipe in QLPreviewController but preserving the vertical swipe (there are the arrows buttons to change the shown document). How do you do that?
Thanks in advance for the help.
Giannandrea
make a category on the QLPreviewController and swizzle the appropriate method and either add the willChange/didChange for KVO ;)
seriously though:
I tried KVO and it didnt work for me either.. 1) id file a bug with apple for that saying you need this
BUT as a workaround
(id )previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
this is called ok and everytime we swipe so I would 'hack' this to FIRE your own correct KVO. something like
static NSInteger oldIndex = -1; //reset when the panel is hidden or shown
int newIndex = qlController.displayedIndex;
if(oldIndex != newIndex) {
oldIndex = newIndex;
[qlController willChangeValueForKey:#"displayedIndex"];
[qlController didChangeValueForKey:#"displayedIndex"];
}
I wrote it inline here so there are bound to be typos and mistakes but I think the general approach could work.
//1. Declare a static context:
static void *changePageContext = &changePageContext;
//2. In viewDidLoad add self as observer for currentPreviewItemIndex property of a strong ref to your QLPreviewController:
[self.previewController addObserver:self forKeyPath:#"currentPreviewItemIndex" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:changePageContext];
//3. Implement the observer method:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if (context == changePageContext)
{
NSLog(#"newValue:%ld",(long)self.previewController.currentPreviewItemIndex);
}
else
{
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
//4. Remove the observer in viewWillDisappear:
-(void)viewWillDisappear:(BOOL)animated
{
if (![[self.navigationController viewControllers] containsObject: self])
{
[self.previewController removeObserver:self forKeyPath:#"currentPreviewItemIndex"];
}
}

How to target the parent object when using performSelectorOnMainThread?

I have a custom NSOperation object that is instantiated from a UITableViewController subclass (MusicTableVC). The NSOperation object is supposed to populate an NSarray from a URL in the background so the UI doesn't freeze up, but then I need to send that array back to main thread so the MusicTableVC instance can do stuff with it.
I know I need to use performSelectorOnMainThread: to send the array back to the MusicTableVC but to do that I need a pointer to the instance of MusicTableVC.
I was thinking about creating an init method in the NSOperation e.g. initWithParent to pass on a pointer to self and use that but maybe I'm missing something?
#synthesize parent;
- (id)initWithParent:(MusicTableVC*) musicTableViewController
{
if(self = [super init])
{
self.parent = musicTableViewController;
}
return self;
}
-(void) main
}
[parent performSelectorOnMainThread:#selector(arrayFinishedLoading:)
withObject:playlist
waitUntilDone:YES];
}
I think you would do better with blocks and Grand Central Dispatch:
dispatch_async(dispatch_get_queue(DISPATCH_QUEUE_PRIORITY_NORMAL, 0), ^{
// This is called in background, not blocking the UI
[self populateArrayFromURL];
dispatch_async(dispatch_get_main_queue(), ^{
// This is called on the main thread
[self reportStuffDone];
});
});
that is one way of doing it, but a more common pattern is to have the parent of the NSOperation observe its state, and do something with the results when it is complete. so when you create your operation in the view controller, do something like this:
NSOperation *myOp = [[NSOperation alloc] init];
[myOp addObserver:self forKeyPath:#"isFinished" options:NSKeyValueObservingOptionsNew context:NULL];
then add the KVO callback method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSOperation *myOp = (NSOperation *)object;
[myOp removeObserver:self forKeyPath:keyPath];
dispatch_async(dispatch_get_main_queue(), ^{
// reload the table with the results here
});
}
Your way is fine, I would call it initWithDelegate and define a protocol thou. Just pass the delegate and then the operation is finished just ket it know if it succeded or not.
Recently I've been switching from useless delegates to GCD, so instead I would make something like initWithSuccessBlock and then dispatch it to the main queue. Attention that, if you decide to use this you would have to make sure the block was copied.

How can I get notified when a UIView becomes visible?

Is there a way to get a notification, a callback or some other means to call a method whenever a UIView becomes visible for the user, i.e. when a UIScrollview is the superview of some UIViews, and the ViewController of such a UIView shall get notified when its view is now visible to the user?
I am aware of the possible, but not so elegant solution of checking to which position the ScrollView scrolled (via UIScrollViewDelegate-methods) and compute if either one of the subviews is visible...
But I'm looking for a more universal way of doing this.
I've managed to solve the problem this way:
First, add a category for UIView with the following method:
// retrieve an array containing all super views
-(NSArray *)getAllSuperviews
{
NSMutableArray *superviews = [[NSMutableArray alloc] init];
if(self.superview == nil) return nil;
[superviews addObject:self.superview];
[superviews addObjectsFromArray:[self.superview getAllSuperviews]];
return superviews;
}
Then, in your View, check if the window-property is set:
-(void)didMoveToWindow
{
if(self.window != nil)
[self observeSuperviewsOnOffsetChange];
else
[self removeAsSuperviewObserver];
}
If it is set, we'll observe the "contentOffset" of each superview on any change. If the window is nil, we'll stop observing. You can change the keyPath to any other property, maybe "frame" if there is no UIScrollView in your superviews:
-(void)observeSuperviewsOnOffsetChange
{
NSArray *superviews = [self getAllSuperviews];
for (UIView *superview in superviews)
{
if([superview respondsToSelector:#selector(contentOffset)])
[superview addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}
}
-(void)removeAsSuperviewObserver
{
NSArray *superviews = [self getAllSuperviews];
for (UIView *superview in superviews)
{
#try
{
[superview removeObserver:self forKeyPath:#"contentOffset"];
}
#catch(id exception) { }
}
}
Now implement the "observeValueForKeyPath"-method:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:#"contentOffset"])
{
[self checkIfFrameIsVisible];
}
}
Finally, check if the view's frame is visible inside the window's frame:
-(void)checkIfFrameIsVisible
{
CGRect myFrameToWindow = [self.window convertRect:self.frame fromView:self];
if(myFrameToWindow.size.width == 0 || myFrameToWindow.size.height == 0) return;
if(CGRectContainsRect(self.window.frame, myFrameToWindow))
{
// We are visible, do stuff now
}
}
If your view is exhibiting behavior, it should be within a view controller. On a view controller, the viewDidAppear method will be called each time the view appears.
- (void)viewDidAppear:(BOOL)animated
I don't think there's a universal way to do this for views. Sounds like you're stuck with scrollViewDidEndScrolling and other ScrollViewDelegate methods. But I'm not sure why you say it's elegant, they're quite straightforward.
view's layer property should tell us if that view is visible or not
[view.layer visibleRect];
but this isnt working for me.
So work around could be to use UiScrollView contentOffset property to calculate if particular view is visible or not.