artsy / mobile

Mobile Team TODO
http://www.objc.io/issues/22-scale/artsy/
84 stars 11 forks source link

Improve URL -> VC Router for the ARSwitchboard #48

Closed orta closed 8 years ago

orta commented 8 years ago

JLRoutes does nearly everything here, but that it returns a BOOL rather than returning a VC. Could look into making a fork that does this, then see what Joel thinks.

alexito4 commented 8 years ago

Hi @orta, let me comment about JLRoutes returning a BOOL.

As I told you on twitter we use a very similar thing at what you want and also using JLRoutes. We have a wrapper on top of it that accepts our custom "Route" object. The nice thing is that those objects are created with the "url" and a handlerBlock. Each module of the app can register it's own routes creating this objects and registering them. In the handlerBlock the VC and all the necessary dependencies are created, and that block accepts a params dictionary with the params that the caller wanted to pass (via URL params or a custom dictionary, it doesn't matter). Apart from that it also receives a block that is the one that gets called with the created VC, is the "completionBlock" that the caller uses to present the new screen.

In that way we have not modified the original JLRoutes, the Router only gives us VC (doesn't do anything with presenting them) and we can also use it asynchronously.

That's what we have come up with, I'm impatient to see how your approach will look like in the end ;) Cheers.

orta commented 8 years ago

I see the pattern you're talking about, that's interesting because it also allows for asynchronous view controllers in a way that our current use case doesn't.

orta commented 8 years ago

Alright thanks a lot @alexito4 - so I've hacked up a quick example based on my interpretation:

@import Foundation;
@import UIKit;

@interface Route : NSObject
@property (nonatomic, strong) NSString *pattern;
@property (nonatomic, copy) UIViewController * (^generator)(NSDictionary *params);
@end

@interface ARSwitchboardTwo : NSObject

+ (instancetype)sharedInstance;

- (void)registerRoute:(Route *)route;
- (void)routePath:(NSString *)path completion:(void(^)(UIViewController  *controller))completion;

@end
#import "ARSwitchboardTwo.h"
@import JLRoutes;

@interface _ARInternalRoute : NSObject
@property (nonatomic, copy) NSDictionary *params;
@property (nonatomic, strong) Route *route;
@end

@implementation _ARInternalRoute
@end

@implementation Route
@end

@interface ARSwitchboardTwo()
@property (readonly, nonatomic, strong) JLRoutes *routes;
@property (readonly, nonatomic, strong) NSMutableArray <Route *> *internalRoutes;
@property (readwrite, nonatomic, strong) _ARInternalRoute *currentRoute;
@end

@implementation ARSwitchboardTwo

+ (instancetype)sharedInstance
{
    static ARSwitchboardTwo *sharedInstance;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[ARSwitchboardTwo alloc] init];
    });

    return sharedInstance;
}

- (instancetype)init
{
    self = [super init];
    if (!self) {
        return nil;
    }

    _routes = [[JLRoutes alloc] init];
    _internalRoutes = [NSMutableArray array];

    return self;
}

- (void)registerRoute:(Route *)route
{
    [self.internalRoutes addObject:route];

    __weak typeof(self) weakSelf = self;

    [self.routes addRoute:route.pattern handler:^BOOL(NSDictionary *parameters) {
        _ARInternalRoute *current = [[_ARInternalRoute alloc] init];
        current.route = route;
        current.params = parameters;
        weakSelf.currentRoute = current;
        return YES;
    }];
}

- (void)routePath:(NSString *)path completion:(void (^)(UIViewController *))completion
{
    NSURL *url = [NSURL URLWithString:path];
    if ([self.routes routeURL:url withParameters:nil]) {
        Route *route = self.currentRoute.route;
        NSDictionary *params = self.currentRoute.params;
        UIViewController *result = route.generator(params);
        completion(result);

    } else {
        self.currentRoute = nil;
        completion(nil);
    }
}

@end

Then anything can register their own Routes;

@implementation TwoViewController

+ (void)load
{
    Route *route = [[Route alloc] init];
    route.pattern = @"/two/:id";
    route.generator = ^UIViewController * (NSDictionary *params) {
        TwoViewController *twoVC = [[TwoViewController alloc] init];
        twoVC.view.backgroundColor = [UIColor redColor];
        return twoVC;
    };

    [[ARSwitchboardTwo sharedInstance] registerRoute:route];
}

@end

and any other class can use their own completion based routing:

- (IBAction)go:(id)sender
{
    [[ARSwitchboardTwo sharedInstance] routePath:@"/two/thing" completion:^(UIViewController *controller) {
        [self.navigationController pushViewController:controller animated:YES];
    }];
}

Full demo - http://cl.ly/0U0Z0l1m1N2Q/RoutingExample.zip

alloy commented 8 years ago

@orta I like the result a lot :+1: I don’t really like the ‘current route’ state that’s needed because of the BOOL return value, so a fork does seem in order to me.

orta commented 8 years ago

A lot of this is done now, and on my fork.