Skip to content

Instantly share code, notes, and snippets.

@orta
Last active August 29, 2015 13:56
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save orta/9015257 to your computer and use it in GitHub Desktop.
Save orta/9015257 to your computer and use it in GitHub Desktop.
Network models

Using a real artsy example from today.

Creating a follow button on a view controller for different types of objects Artist, Profile, Gene

Needs: networking Needs: layout Needs: interface changes based on networking

Networking

Instead of having control inside the application there's create a protocol called ARFollowable on each model

@protocol ARFollowable <NSObject>

@property (nonatomic, assign, getter=isFollowed) BOOL followed;

- (void)followWithSuccess:(void (^)(id response))onComplete failure:(void (^)(NSError *error))onFailure;
- (void)unfollowWithSuccess:(void (^)(id response))onComplete failure:(void (^)(NSError *error))onFailure;
- (void)setFollowState:(BOOL)state withSuccess:(void (^)(id))onComplete failure:(void (^)(NSError *))onFailure;
- (void)getFollowState:(void (^)(BOOL result, NSError *error))onComplete;

@end

These handle edge cases between object API.

There is a class called ARFollowableNetworkModel

@interface ARFollowableNetworkModel : NSObject

- (id)initWithFollowableObject:(id <ARFollowable>)representedObject;

@property (readonly, nonatomic, strong) id <ARFollowable> representedObject;

/// You should observe changes on this for network fallbacks
@property (nonatomic, assign, getter=isFollowing) BOOL following;

@end

which is


@implementation ARFollowableNetworkModel

- (id)initWithFollowableObject:(id <ARFollowable>)representedObject
{
    self = [super init];
    if (!self) return nil;

    _representedObject = representedObject;

    @weakify(self);
    [_representedObject getFollowState:^(BOOL result, NSError *error) {
        @strongify(self);
        [self willChangeValueForKey:@"following"];
        if(error) self->_following = NO;
        self->_following = result;
        [self didChangeValueForKey:@"following"];
    }];

    return self;
}

- (void)setFollowing:(BOOL)following
{
    if(following == _following) return;

    @weakify(self);
    if(following){
        [_representedObject followWithSuccess:nil failure:^(NSError *error) {
            @strongify(self);
            ARErrorLog(@"Error following %@ - %@", self.representedObject, error.localizedDescription);
            [self _setFollowing:NO];
        }];

    } else {
        [_representedObject unfollowWithSuccess:nil failure:^(NSError *error) {
            @strongify(self);
            ARErrorLog(@"Error following %@ - %@", self.representedObject, error.localizedDescription);
            [self _setFollowing:YES];
        }];
    }

    [self _setFollowing:following];
}

- (void)_setFollowing:(BOOL)isFollowing
{
    [self willChangeValueForKey:@"following"];
    self->_following = isFollowing;
    self.representedObject.followed = isFollowing;
    [self didChangeValueForKey:@"following"];
}

@end

It acts as a network surrogate to the ARFollowable object, using KVO to say what the state of the object's following is.

Layout

Everything is a stack. Seriously, I just throw it in a stack, make sure that the view has an intrinsic content size and the stackView will do the rest of the work.

Interface changes

This is done by having a ARFollowableButton which has a weak reference to the ARFollowableNetworkModel and sets up to observe for KVO notifications on the network model's follow keypath.


@interface ARFollowableButton ()
@property (nonatomic, weak) ARFollowableNetworkModel *model;
@end

@implementation ARFollowableButton

- (void)setup
{
    [super setup];
    self.borderColor = [UIColor artsyLightGrey];
}

- (void)setupKVOOnNetworkModel:(ARFollowableNetworkModel *)model
{
    [model addObserver:self forKeyPath:@keypath(ARFollowableNetworkModel.new, following) options:NSKeyValueObservingOptionNew context:nil];
    self.model = model;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(ARFollowableNetworkModel *)followableNetworkModel change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@keypath(ARFollowableNetworkModel.new, following)]) {
        BOOL following = followableNetworkModel.isFollowing;
        NSString *title = (following) ? @"FOLLOWING" : @"FOLLOW";
        UIColor *titleColor = (following) ? [UIColor artsyPurple] : [UIColor blackColor];

        [self setTitle:title forState:UIControlStateNormal];
        self.titleLabel.textColor = titleColor;
    }
}

- (void)dealloc
{
    [self.model removeObserver:self forKeyPath:@keypath(ARFollowableNetworkModel .new, following)];
}

@end

This means the whole code for adding the button and setting it up in the view controller is this:

- (void)addFollowButton
{
    if(!self.show.partner.hasFullProfile) return;

    ARFollowableButton *followButton = [[ARFollowableButton alloc] init];
    [self.view.stackView addSubview:followButton withTopMargin:@"20" sideMargin:@"40"];
    [followButton addTarget:self action:@selector(toggleFollowShow:) forControlEvents:UIControlEventTouchUpInside];

    Profile *profile = [[Profile alloc] initWithProfileID:self.show.partner.profileID];
    self.followableNetwork = [[ARFollowableNetworkModel alloc] initWithFollowableObject:profile];
    [followButton setupKVOOnNetworkModel:self.followableNetwork];
}


- (void)toggleFollowShow:(id)sender
{
    self.followableNetwork.following = !self.followableNetwork.following;
}

@dstnbrkr
Copy link

+1 - I like this idea of the controller constructing a wrapper for models that will handle all API interactions. Definitely interested in seeing more if you can share.

@orta
Copy link
Author

orta commented Feb 15, 2014

I've got a few examples that we have that is based on a new generic list view @robb wrote & some polymorphic API joy, I'll see what I can do tomorrow about simplifying the use case to a gist 👍

@irace
Copy link

irace commented Feb 16, 2014

I'm guessing you are not worried about the case where a user pushes deeper into a navigation controller stack, changes follow state of a profile, then pops back to reveal a follow button that is now stale? I'm guessing since you are [[Profile alloc] init] rather than fetch a managed object or lookup via some shared object cache, that there can be > 1 Profile with the same profileID.

Of course you could always just refresh button states in viewWillAppear:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment