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
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.
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.
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;
}
+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.