Inline Kvo of a Property in Another View Controller

Inline KVO of a Property in another view controller

The observer is destroyed because there is no reference
to it after the other view controller has been presented.
You have to store it

observer = vc.observe(\.value) { ... }

where observer is a property of the calling view controller.

A self-contained command-line project example: This prints "new string" as expected:

class A: NSObject {
@objc dynamic var value: String = ""
}

let a = A()
let observer = a.observe(\.value) { (_, _) in print("new string") } // (*)
a.value = "Hello world"

But nothing is printed if (*) is replaced by

_ = a.observe(\.value) { (_, _) in print("new string") }

iOS: How do I know if a property is KVO-compliant?

Short answer: No.

Long answer: Nothing in UIKit is guaranteed to be KVO-compliant. If you happen to find that KVO-ing a property works, be grateful, it's unintentional. Also: be wary. It could very well break in the future.

If you find that this is something you need, please file an enhancement request.


About your actual code, it's inherently flawed. Do NOT attempt to add a "rootViewController" setter to UIWindow this way. It will break when you compile your code on iOS 4 but someone runs it on an iOS 5 device. Because you compiled using the 4.x SDK, the #if statements will evaluate to true, meaning your category method smasher will be included in the binary. However, when you run it on an iOS 5 device, you're now going to get a method conflict because two methods on UIWindow will have the same method signature, and there's no guarantee as to which one will be used.

Don't screw with the frameworks like this. If you have to have this, use a subclass. THIS IS WHY SUBCLASSING EXISTS.


Your subclass would look something like this:

@interface CustomWindow : UIWindow

@property (nonatomic, retain) UIViewController *rootViewController;

@end

@implementation CustomWindow : UIWindow

static BOOL UIWindowHasRootViewController = NO;

@dynamic rootViewController;

- (void)_findRootViewControllerMethod {
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
IMP uiwindowMethod = [UIWindow instanceMethodForSelector:@selector(setRootViewController:)];
IMP customWindowMethod = [CustomWindow instanceMethodForSelector:@selector(setRootViewController:)];
UIWindowHasRootViewController = (uiwindowMethod != NULL && uiwindowMethod != customWindowMethod);
});
}

- (UIViewController *)rootViewController {
[self _findRootViewControllerMethod];
if (UIWindowHasRootViewController) {
// this will be a compile error unless you forward declare the property
// i'll leave as an exercise to the reader ;)
return [super rootViewController];
}
// return the one here on your subclass
}

- (void)setRootViewController:(UIViewController *)rootViewController {
[self _findRootViewControllerMethod];
if (UIWindowHasRootViewController) {
// this will be a compile error unless you forward declare the property
// i'll leave as an exercise to the reader ;)
[super setRootViewController:rootViewController];
} else {
// set the one here on your subclass
}
}

Caveat Implementor: I typed this in a browser window

getting NSException while passing data from one viewController to another viewController

plz use this you are using storyboard

pass your value like this

 [self performSegueWithIdentifier:@"DashBoardViewController" sender:[dataDict objectForKey:@"EmployeeId"]];

use this method when you use performSegueWithIdentifier

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
if ([segue.identifier isEqualToString:@"DashBoardViewController"])
{
DashBoardViewController *dash= segue.destinationViewController;
dash.empid =(NSString *)sender;

}
}

How to add a new view controller in UIPageViewController after UIButton was tapped?

The UIPageViewController uses a datasource to obtain the next or previous view controller. So the solution is to add your new view controller to the datasource once the button is touched.

Say your datasource is implemented as an NSArray (see the XCode's Page-based Application template for an example of this) you just add your new vc to that array and set the new vc by calling [UIPageViewController setViewControllers:direction:animated:completion:].

Since you didn't provide any details on how you implemented your hierarchy I can't be more specific.

Edit:

Now that I have some code, here's what I'd do:

First I wouldn't save the labelContents but rather the view controller itself in the modelArray. Besides other things, your current design creates a new ContentVC every time you change pages. That's a lot of unnecessary overhead. Here's how I would implement that:

(btw, you should think of a more descriptive name than labelContents. Right now it might be fine if there's just one label, but what if you add more labels in the future?)

- (ContentVC *)contentViewControllerWithLabelContents:(NSString *)labelContents
{
ContentVC *vc = [[ContentVC alloc] initWithNibName:@"ContentVC" bundle:nil];
vc.labelContents = labelContents;
return vc;
}

- (void)viewDidLoad
{
NSMutableArray *vcs = [NSMutableArray array];
[vcs addObject:[self contentViewControllerWithLabelContents:@"Page One"]];
[vcs addObject:[self contentViewControllerWithLabelContents:@"Page Two"]];
//...
self.modelArray = vcs;
}

#pragma mark - UIPageViewControllerDataSource Methods

// Returns the view controller before the given view controller. (required)
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerBeforeViewController:(UIViewController *)viewController
{
NSUInteger currentIndex = [self.modelArray indexOfObject:viewController];
if(currentIndex == 0)
return nil;

ContentVC *cVC = [self.modelArray objectAtIndex:currentIndex - 1];
return cVC;
}

// Returns the view controller after the given view controller. (required)
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerAfterViewController:(UIViewController *)viewController
{
NSUInteger currentIndex = [self.modelArray indexOfObject:viewController];
if(currentIndex == self.modelArray.count - 1)
return nil;

ContentVC *cVC = [self.modelArray objectAtIndex:currentIndex + 1];
return cVC;
}

The code in your comment:

- (IBAction)newButtonTapped:(id)sender 
{
if(sender){
[self.modelArray addObject:@"Page Two"];
ContentVC *newVC = [[ContentVC alloc] initWithNibName:@"ContentVC" bundle:nil];
NSArray *arr = [NSArray arrayWithObject:newVC];
[self.pageVC setViewControllers:arr direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
NSLog(@"%@", self.modelArray);
}
}

doesn't work because you didn't set the labelContents of the ContentVC. So you basically end up calling [self.modelObject indexOfObject:nil] or [self.modelObject indexOfObject:@""] depending on your implementation of labelContents. Since neither of them is in the array, the call returns NSNotFound which on 64bit systems is translated to NSIntegerMax and that is 2147483647. Later you try to access your modelArray at index NSIntegerMax - 1 and that raises an NSRangeException.

So can fix that by either setting the labelContents in your newButtonTapped: method or if you follow my suggestion to redesign your code:

- (IBAction)newButtonTapped:(id)sender 
{
if(sender){
ContentVC *newVC = [self contentViewControllerWithLabelContents:@"Page Two"];
[self.modelArray addObject:newVC];

NSArray *newVCs = nil;

if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation) || self.modelArray.count == 1)
newVCs = @[newVC];
else
{
NSUInteger secondLastIndex = self.modelArray.count - 2;
id previousVC = [self.modelArray objectAtIndex:secondLastIndex];
newVCs = @[previousVC, newVC];
}

[self.pageVC setViewControllers:newVCs direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
}

NSArrayController and KVO

So, it turns out I can get this to work by changing by addPassenger method from

[seatedPlayers addObject:person];

to

NSMutableSet *newSeatedPlayers = [NSMutableSet setWithSet:seatedPlayers];
[newSeatedPlayers addObject:sp];
[seatedPlayers release];
[self setSeatedPlayers:newSeatedPlayers];

I guess this is because I am using [self setSeatedPlayers]. Is this the right way to do it?

First off, it's setSeatedPlayers:, with the colon. That's vitally important in Objective-C.

Using your own setters is the correct way to do it, but you're using the incorrect correct way. It works, but you're still writing more code than you need to.

What you should do is implement set accessors, such as addSeatedPlayersObject:. Then, send yourself that message. This makes adding people a short one-liner:

[self addSeatedPlayersObject:person];

And as long as you follow the KVC-compliant accessor formats, you will get KVO notifications for free, just as you do with setSeatedPlayers:.

The advantages of this over setSeatedPlayers: are:

  • Your code to mutate the set will be shorter.
  • Because it's shorter, it will be cleaner.
  • Using specific set-mutation accessors provides the possibility of specific set-mutation KVO notifications, instead of general the-whole-dang-set-changed notifications.

I also prefer this solution over mutableSetValueForKey:, both for brevity and because it's so easy to misspell the key in that string literal. (Uli Kusterer has a macro to cause a warning when that happens, which is useful when you really do need to talk to KVC or KVO itself.)

adding KVO to UITableViewCell

For background, you probably want to read the Key-Value Observing and Key-Value Coding Guides, if you haven't already. Then review the NSKeyValueObserving category methods.

http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Protocols/NSKeyValueObserving_Protocol/Reference/Reference.html

In a nutshell, you need to carefully manage adding and removing the observing object to the observed objects list of observers (pardon the seeming redundancy of that statement). You don't want to have an object going away with observers still registered, or you get complaints and possible other issues.

That said, you use -addObserver:keyPath:options:context to add an object as an observer. Context should be a statically declared string. The options argument controls what data you get back in your observation method (see below). The keyPath is the path of property names from the observed object to the observed property (this may traverse multiple objects, and will be updated when intermediate objects change, not just when the leaf property changes).

In your case, you could observe the label, and use the text keyPath, or the cell, and use the nameLabel.text key path. If the table view class were designed differently, you might observe the entire array of cells, but there is no such property on UITableView. The problem with observing the cell is that the table view might delete it at any time (if your design uses multiple cells that serve the same purpose in a variable-length list). If you know your cells are static, you can probably observe them without worry.

Once you have an observer registered, that observer must implement
-observeValueForKeyPath:ofObject:change:context:, confirm that the context matches (just compare the pointer value to your static string's address; otherwise, invoke super's implementation), then look into the change dictionary for the data you want (or just ask the object for it directly) and use it to update your model as you see fit.

There are many examples of KVO in sample code, including on Apple's developer site, and as part of the bindings samples on Malcolm Crawford (mmalc)'s site, but most of it is for Mac OS X, not iOS.



Related Topics



Leave a reply



Submit