Hi,

On 4/20/18 10:15 AM, Glen Huang wrote:
I have an app where user can edit data and save to my server. I wonder what’s 
the best
way to update affected view controllers in the navigation stack?

To give an example, imagine it’s an a recipe app where users can create recipes 
and
edit other’s recipes. In the navigation controller’s root view controller, I 
show a
list of all recipes, in each cell, in addition to the recipe name, it also 
shows the
total number of ingredients the corresponding recipe requires.

When you tap a cell, I show the detail of the corresponding receipt. In this 
detail
view, I have a cell that links to a view controller that shows the list of 
ingredients,
and in that view controller, users can tap edit to show a view controller that 
allows
adding/removing ingredients.

This set up means the same data can be displayed across view controllers in the
navigation stack, and the it changes, the they need to be in sync.

So the question is, when the ingredients change, what’s the best way to update 
that
ingredient count number in the root view controller?

This sounds to me like a very good application for KVO (Key Value Observation).

All you need is a model (your representation of recipes with names, pictures, descriptions and ingredients). Each individual view controller knows what it's interested in, for example the main view controller likely doesn't care about descriptions or ingredients, just names and pictures, I imagine, while the detail view controller does display everything so it will observe all those properties.

In every view controller you observe properties of the objects you display and implement -observeValueForKeyPath::::, for example in your details view controller:

NSString *kRecipeNameContext = @"kSomeNameContext";

- (void)viewWillAppear:(BOOL)animated
{
  [super viewWillAppear:animated];

  // assuming recipe is set already
  [recipe addObserver:self
           forKeyPath:@"name"
              options:NSKeyValueObservingOptionNew
              context:kRecipeNameContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  if (context == kRecipeNameContext) {
    [self updateName];
  } else {
    [super observeValueForKeyPath::::];
  }
}

Once this is in place, every time the name changes for the given recipe, your view controller will be notified. The great thing here is that ANYONE interested in the property can use this mechanism (you could have multiple view controllers that display some aspect of the same recipe, so they all want to know when the name changes) and you will have to maintain zero code that makes sure that you know who that might be.

When you no longer need the view controller (for example if the view controller gets popped and destroyed), make sure to "unsubscribe" to changes via

- (void)viewDidDisappear:(BOOL)animated
{
  [super viewDidDisappear:animated];
  [recipe removeObserver:sefl forKeyPath:@"name" context:kRecipeNameContext];
}

One more complication is that the list of all recipes come from my server (via 
json).

It does not matter where the data comes from so long as you make sure it is added to the model in a KVO-compliant way. This basically means, always to use property setters to update a model object instead of setting instance variables directly.

There's a little more to it when you want to observe arrays (as in wanting to know when objects get added or removed). In that case you have to implement collection property accessor methods for the array in your model class that contains the array. To access the array, you always use proxy objects for arrays when you add or remove objects. Sounds complicated but it isn't.

Say you have an "ingredients" NSMutableArray in your Recipe class.


@implementation Recipe

- (NSUInteger)countOfIngredients
{
    return [ingredients count];
}

- (id)objectInIngredientsAtIndex:(unsigned)theIndex
{
    return [ingredients objectAtIndex:theIndex];
}

- (void)getIngredients:(id *)objsPtr range:(NSRange)range
{
    [ingredients getObjects:objsPtr range:range];
}

- (void)insertObject:(id)obj inIngredientsAtIndex:(NSUInteger)theIndex
{
    [ingredients insertObject:obj atIndex:theIndex];
}

- (void)removeObjectFromIngredientsAtIndex:(NSUInteger)theIndex
{
    [ingredients removeObjectAtIndex:theIndex];
}

- (void)replaceObjectInIngredientsAtIndex:(NSUInteger)theIndex 
withObject:(id)obj
{
    [ingredients replaceObjectAtIndex:theIndex withObject:obj];
}


@end

You can find more information on this here:

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueCoding/DefiningCollectionMethods.html

The point of this is that you then can observe the "ingredients" property of the recipe just like the observation above and you will get a -observeValueForKeyPath:::: "notification" if items are added, removed or replaced.

In order for this to work you just need to use a KVO proxy for the real array. So you don't add or remove objects from your instance variable "ingredients" but instead you do it like so:

NSMutableArray *kvoIngredients = [self mutableArrayValueForKey:@"ingredients"];
[kvoIngredients addObject:item];

-mutableArrayValueForKey: returns a proxy object that you can use to manipulate the array instance via the property accessor methods above.

Don't be intimidated, once you get a hang of it, KVO becomes second nature. KVO an bindings can save you millions of hours of coding.

Hope this helps.

Best Regards
Markus

--
__________________________________________
Markus Spoettl
_______________________________________________

Cocoa-dev mailing list (Cocoa-dev@lists.apple.com)

Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com

Help/Unsubscribe/Update your Subscription:
https://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to arch...@mail-archive.com

Reply via email to