diff --git a/Routable.xcodeproj/project.pbxproj b/Routable.xcodeproj/project.pbxproj index 799479a..bc7e7f8 100644 --- a/Routable.xcodeproj/project.pbxproj +++ b/Routable.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ F86EF9C8170C3C7B00EB726A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86EF9C7170C3C7B00EB726A /* UIKit.framework */; }; F86EF9CA170C3C7B00EB726A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86EF9C9170C3C7B00EB726A /* Foundation.framework */; }; F86EF9CC170C3C7B00EB726A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86EF9CB170C3C7B00EB726A /* CoreGraphics.framework */; }; - F86EF9E6170C3C7B00EB726A /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86EF9E5170C3C7B00EB726A /* SenTestingKit.framework */; }; F86EF9E7170C3C7B00EB726A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86EF9C7170C3C7B00EB726A /* UIKit.framework */; }; F86EF9E8170C3C7B00EB726A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86EF9C9170C3C7B00EB726A /* Foundation.framework */; }; F86EF9F0170C3C7B00EB726A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = F86EF9EE170C3C7B00EB726A /* InfoPlist.strings */; }; @@ -38,8 +37,7 @@ F86EF9C7170C3C7B00EB726A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; F86EF9C9170C3C7B00EB726A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; F86EF9CB170C3C7B00EB726A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; - F86EF9E4170C3C7B00EB726A /* RoutableTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RoutableTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; - F86EF9E5170C3C7B00EB726A /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; + F86EF9E4170C3C7B00EB726A /* RoutableTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RoutableTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F86EF9ED170C3C7B00EB726A /* RoutableTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RoutableTests-Info.plist"; sourceTree = ""; }; F86EF9EF170C3C7B00EB726A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; F86EF9F1170C3C7B00EB726A /* RoutableTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RoutableTests.h; sourceTree = ""; }; @@ -71,7 +69,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F86EF9E6170C3C7B00EB726A /* SenTestingKit.framework in Frameworks */, F86EF9E7170C3C7B00EB726A /* UIKit.framework in Frameworks */, F86EF9E8170C3C7B00EB726A /* Foundation.framework in Frameworks */, ); @@ -95,7 +92,7 @@ isa = PBXGroup; children = ( F86EF9C4170C3C7B00EB726A /* Routable.app */, - F86EF9E4170C3C7B00EB726A /* RoutableTests.octest */, + F86EF9E4170C3C7B00EB726A /* RoutableTests.xctest */, ); name = Products; sourceTree = ""; @@ -106,7 +103,6 @@ F86EF9C7170C3C7B00EB726A /* UIKit.framework */, F86EF9C9170C3C7B00EB726A /* Foundation.framework */, F86EF9CB170C3C7B00EB726A /* CoreGraphics.framework */, - F86EF9E5170C3C7B00EB726A /* SenTestingKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -199,8 +195,8 @@ ); name = RoutableTests; productName = RoutableTests; - productReference = F86EF9E4170C3C7B00EB726A /* RoutableTests.octest */; - productType = "com.apple.product-type.bundle"; + productReference = F86EF9E4170C3C7B00EB726A /* RoutableTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -209,6 +205,7 @@ isa = PBXProject; attributes = { CLASSPREFIX = UP; + LastTestingUpgradeCheck = 0630; LastUpgradeCheck = 0460; ORGANIZATIONNAME = "TurboProp Inc"; }; @@ -397,6 +394,7 @@ FRAMEWORK_SEARCH_PATHS = ( "\"$(SDKROOT)/Developer/Library/Frameworks\"", "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + "$(inherited)", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "RoutableExample/Routable-Prefix.pch"; @@ -404,7 +402,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUNDLE_LOADER)"; - WRAPPER_EXTENSION = octest; }; name = Debug; }; @@ -415,6 +412,7 @@ FRAMEWORK_SEARCH_PATHS = ( "\"$(SDKROOT)/Developer/Library/Frameworks\"", "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + "$(inherited)", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "RoutableExample/Routable-Prefix.pch"; @@ -422,7 +420,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUNDLE_LOADER)"; - WRAPPER_EXTENSION = octest; }; name = Release; }; diff --git a/Routable/Routable.h b/Routable/Routable.h index 338e2b7..80d4cff 100644 --- a/Routable/Routable.h +++ b/Routable/Routable.h @@ -43,58 +43,127 @@ typedef void (^RouterOpenCallback)(NSDictionary *params); ``` UPRouterOptions *options = [[UPRouterOptions modal] withPresentationStyle: UIModalPresentationFormSheet]; ``` + + Now, you can also use an Objective-C factory method to set everything at once + + ``` + UPRouterOptions *options = [UPRouterOptions routerOptionsWithPresentationStyle: UIModalPresentationFormSheet + transitionStyle: UIModalTransitionFormSheet + defaultParams: nil + isRoot: NO + isModal: YES]; + ``` + + Or, for most properties taking the default value: + + ``` + UPRouterOptions *options = [[UPRouterOptions alloc] init]; + [options setTransitionStyle:UIModalTransitionStyleCoverVertical]; + ``` */ @interface UPRouterOptions : NSObject +/** + @return A new instance of `UPRouterOptions` with its properties explicitly set + @param presentationStyle The `UIModalPresentationStyle` attached to the mapped `UIViewController` + @param transitionStyle The `UIModalTransitionStyle` attached to the mapped `UIViewController` + @param defaultParams The default parameters which are passed when opening the URL + @param isRoot The boolean `shouldOpenAsRootViewController` property is set to + @param isModal The boolean that sets a modal presentation format + */ ++ (instancetype)routerOptionsWithPresentationStyle: (UIModalPresentationStyle)presentationStyle + transitionStyle: (UIModalTransitionStyle)transitionStyle + defaultParams: (NSDictionary *)defaultParams + isRoot: (BOOL)isRoot + isModal: (BOOL)isModal; +/** + @return A new instance of `UPRouterOptions` with its properties set to default + */ ++ (instancetype)routerOptions; + ///------------------------------- /// @name Options DSL ///------------------------------- +/** + @return A new instance of `UPRouterOptions`, setting a modal presentation format. + */ ++ (instancetype)routerOptionsAsModal; +/** + @return A new instance of `UPRouterOptions`, setting a `UIModalPresentationStyle` style. + @param style The `UIModalPresentationStyle` attached to the mapped `UIViewController` + */ ++ (instancetype)routerOptionsWithPresentationStyle:(UIModalPresentationStyle)style; +/** + @return A new instance of `UPRouterOptions`, setting a `UIModalTransitionStyle` style. + @param style The `UIModalTransitionStyle` attached to the mapped `UIViewController` + */ ++ (instancetype)routerOptionsWithTransitionStyle:(UIModalTransitionStyle)style; +/** + @return A new instance of `UPRouterOptions`, setting the defaultParams + @param defaultParams The default parameters which are passed when opening the URL + */ ++ (instancetype)routerOptionsForDefaultParams:(NSDictionary *)defaultParams; +/** + @return A new instance of `UPRouterOptions`, setting the `shouldOpenAsRootViewController` property to `YES` + */ ++ (instancetype)routerOptionsAsRoot; +//previously supported /** + @remarks not idiomatic objective-c naming for allocation and initialization, see +routerOptionsAsModal @return A new instance of `UPRouterOptions`, setting a modal presentation format. */ + (instancetype)modal; /** + @remarks not idiomatic objective-c naming for allocation and initialization, see + routerOptionsWithPresentationStyle: @return A new instance of `UPRouterOptions`, setting a `UIModalPresentationStyle` style. @param style The `UIModalPresentationStyle` attached to the mapped `UIViewController` */ + (instancetype)withPresentationStyle:(UIModalPresentationStyle)style; /** + @remarks not idiomatic objective-c naming for allocation and initialization see +routerOptionsWithTransitionStyle: @return A new instance of `UPRouterOptions`, setting a `UIModalTransitionStyle` style. @param style The `UIModalTransitionStyle` attached to the mapped `UIViewController` */ + (instancetype)withTransitionStyle:(UIModalTransitionStyle)style; /** + @remarks not idiomatic objective-c naming for allocation and initialization, see +routerOptionsForDefaultParams: @return A new instance of `UPRouterOptions`, setting the defaultParams @param defaultParams The default parameters which are passed when opening the URL */ + (instancetype)forDefaultParams:(NSDictionary *)defaultParams; /** + @remarks not idiomatic objective-c naming for allocation and initialization, see +routerOptionsAsRoot @return A new instance of `UPRouterOptions`, setting the `shouldOpenAsRootViewController` property to `YES` */ + (instancetype)root; /** + @remarks not idiomatic objective-c naming; overrides getter to wrap around setter @return The same instance of `UPRouterOptions`, setting a modal presentation format. */ - (UPRouterOptions *)modal; /** + @remarks not idiomatic objective-c naming; wraps around setter @return The same instance of `UPRouterOptions`, setting a `UIModalPresentationStyle` style. @param style The `UIModalPresentationStyle` attached to the mapped `UIViewController` */ - (UPRouterOptions *)withPresentationStyle:(UIModalPresentationStyle)style; /** + @remarks not idiomatic objective-c naming; wraps around setter @return The same instance of `UPRouterOptions`, setting a `UIModalTransitionStyle` style. @param style The `UIModalTransitionStyle` attached to the mapped `UIViewController` */ - (UPRouterOptions *)withTransitionStyle:(UIModalTransitionStyle)style; /** + @remarks not idiomatic objective-c naming; wraps around setter @return The same instance of `UPRouterOptions`, setting the defaultParams @param defaultParams The default parameters which are passed when opening the URL */ - (UPRouterOptions *)forDefaultParams:(NSDictionary *)defaultParams; /** + @remarks not idiomatic objective-c naming; wraps around setter @return A new instance of `UPRouterOptions`, setting the `shouldOpenAsRootViewController` property to `YES` */ - (UPRouterOptions *)root; @@ -190,6 +259,13 @@ typedef void (^RouterOpenCallback)(NSDictionary *params); Pop to the last `UIViewController` mapped with the router; this will either dismiss the presented `UIViewController` (i.e. modal) or pop the top view controller in the navigationController. @param animated Whether or not the transition is animated; */ + +- (void)popViewControllerFromRouterAnimated:(BOOL)animated; +/** + Pop to the last `UIViewController` mapped with the router; this will either dismiss the presented `UIViewController` (i.e. modal) or pop the top view controller in the navigationController. + @param animated Whether or not the transition is animated; + @remarks not idiomatic objective-c naming + */ - (void)pop:(BOOL)animated; ///------------------------------- @@ -257,6 +333,17 @@ typedef void (^RouterOpenCallback)(NSDictionary *params); */ - (void)open:(NSString *)url animated:(BOOL)animated; +/** + Triggers the appropriate functionality for a mapped URL, such as an anonymous function or opening a `UIViewController` + @param url The URL being opened (i.e. "users/16") + @param animated Whether or not `UIViewController` transitions are animated. + @param extraParams more paramters to pass in while opening a `UIViewController`; take priority over route-specific default parameters + @exception RouteNotFoundException Thrown if url does not have a valid mapping + @exception NavigationControllerNotProvided Thrown if url opens a `UIViewController` and navigationController has not been assigned + @exception RoutableInitializerNotFound Thrown if the mapped `UIViewController` instance does not implement -initWithRouterParams: or +allocWithRouterParams: + */ +- (void)open:(NSString *)url animated:(BOOL)animated extraParams:(NSDictionary *)extraParams; + /** Get params of a given URL, simply return the params dictionary NOT using a block @param url The URL being detected (i.e. "users/16") @@ -276,6 +363,7 @@ typedef void (^RouterOpenCallback)(NSDictionary *params); /** A new instance of `UPRouter`, in case you want to use multiple routers in your app. + @remarks Unnecessary method; can use [[Routable alloc] init] instead @return A new instance of `UPRouter`. */ + (instancetype)newRouter; diff --git a/Routable/Routable.m b/Routable/Routable.m index 73c7716..b8ae96e 100644 --- a/Routable/Routable.m +++ b/Routable/Routable.m @@ -29,96 +29,156 @@ @implementation Routable + (instancetype)sharedRouter { static Routable *_sharedRouter = nil; - static dispatch_once_t oncePredicate; - dispatch_once(&oncePredicate, ^{ - _sharedRouter = [self newRouter]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _sharedRouter = [[Routable alloc] init]; }); return _sharedRouter; } +//really unnecessary; kept for backward compatibility. + (instancetype)newRouter { - return [self new]; + return [[self alloc] init]; } @end @interface RouterParams : NSObject + @property (readwrite, nonatomic, strong) UPRouterOptions *routerOptions; @property (readwrite, nonatomic, strong) NSDictionary *openParams; +@property (readwrite, nonatomic, strong) NSDictionary *extraParams; +@property (readwrite, nonatomic, strong) NSDictionary *controllerParams; + @end @implementation RouterParams -@synthesize routerOptions = _routerOptions; -@synthesize openParams = _openParams; -- (NSDictionary *)getControllerParams { - NSMutableDictionary *controllerParams = [NSMutableDictionary new]; - - [controllerParams addEntriesFromDictionary:self.routerOptions.defaultParams]; +- (instancetype)initWithRouterOptions: (UPRouterOptions *)routerOptions openParams: (NSDictionary *)openParams extraParams: (NSDictionary *)extraParams{ + [self setRouterOptions:routerOptions]; + [self setExtraParams: extraParams]; + [self setOpenParams:openParams]; + return self; +} + +- (NSDictionary *)controllerParams { + NSMutableDictionary *controllerParams = [NSMutableDictionary dictionaryWithDictionary:self.routerOptions.defaultParams]; + [controllerParams addEntriesFromDictionary:self.extraParams]; [controllerParams addEntriesFromDictionary:self.openParams]; - return controllerParams; } - +//fake getter. Not idiomatic Objective-C. Use accessor controllerParams instead +- (NSDictionary *)getControllerParams { + return [self controllerParams]; +} @end @interface UPRouterOptions () + @property (readwrite, nonatomic, strong) Class openClass; @property (readwrite, nonatomic, copy) RouterOpenCallback callback; @end @implementation UPRouterOptions -@synthesize modal = _modal; -@synthesize defaultParams = _defaultParams; -@synthesize openClass = _openClass; -@synthesize callback = _callback; +//Explicit construction ++ (instancetype)routerOptionsWithPresentationStyle: (UIModalPresentationStyle)presentationStyle + transitionStyle: (UIModalTransitionStyle)transitionStyle + defaultParams: (NSDictionary *)defaultParams + isRoot: (BOOL)isRoot + isModal: (BOOL)isModal { + UPRouterOptions *options = [[UPRouterOptions alloc] init]; + options.presentationStyle = presentationStyle; + options.transitionStyle = transitionStyle; + options.defaultParams = defaultParams; + options.shouldOpenAsRootViewController = isRoot; + options.modal = isModal; + return options; +} +//Default construction; like [NSArray array] ++ (instancetype)routerOptions { + return [self routerOptionsWithPresentationStyle:UIModalPresentationNone + transitionStyle:UIModalTransitionStyleCoverVertical + defaultParams:nil + isRoot:NO + isModal:NO]; +} -+ (instancetype)modal { - return [[self new] modal]; +//Custom class constructors, with heavier Objective-C accent ++ (instancetype)routerOptionsAsModal { + return [self routerOptionsWithPresentationStyle:UIModalPresentationNone + transitionStyle:UIModalTransitionStyleCoverVertical + defaultParams:nil + isRoot:NO + isModal:YES]; +} ++ (instancetype)routerOptionsWithPresentationStyle:(UIModalPresentationStyle)style { + return [self routerOptionsWithPresentationStyle:style + transitionStyle:UIModalTransitionStyleCoverVertical + defaultParams:nil + isRoot:NO + isModal:NO]; +} ++ (instancetype)routerOptionsWithTransitionStyle:(UIModalTransitionStyle)style { + return [self routerOptionsWithPresentationStyle:UIModalPresentationNone + transitionStyle:style + defaultParams:nil + isRoot:NO + isModal:NO]; +} ++ (instancetype)routerOptionsForDefaultParams:(NSDictionary *)defaultParams { + return [self routerOptionsWithPresentationStyle:UIModalPresentationNone + transitionStyle:UIModalTransitionStyleCoverVertical + defaultParams:defaultParams + isRoot:NO + isModal:NO]; +} ++ (instancetype)routerOptionsAsRoot { + return [self routerOptionsWithPresentationStyle:UIModalPresentationNone + transitionStyle:UIModalTransitionStyleCoverVertical + defaultParams:nil + isRoot:YES + isModal:NO]; } +//Exposed methods previously supported ++ (instancetype)modal { + return [self routerOptionsAsModal]; +} + (instancetype)withPresentationStyle:(UIModalPresentationStyle)style { - return [[self new] withPresentationStyle:style]; + return [self routerOptionsWithPresentationStyle:style]; } - + (instancetype)withTransitionStyle:(UIModalTransitionStyle)style { - return [[self new] withTransitionStyle:style]; + return [self routerOptionsWithTransitionStyle:style]; } - + (instancetype)forDefaultParams:(NSDictionary *)defaultParams { - return [[self new] forDefaultParams:defaultParams]; + return [self routerOptionsForDefaultParams:defaultParams]; } - + (instancetype)root { - return [[self new] root]; + return [self routerOptionsAsRoot]; } +//Wrappers around setters (to continue DSL-like syntax) - (UPRouterOptions *)modal { - self.modal = true; + [self setModal:YES]; return self; } - - (UPRouterOptions *)withPresentationStyle:(UIModalPresentationStyle)style { - self.presentationStyle = style; + [self setPresentationStyle:style]; return self; } - - (UPRouterOptions *)withTransitionStyle:(UIModalTransitionStyle)style { - self.transitionStyle = style; + [self setTransitionStyle:style]; return self; } - - (UPRouterOptions *)forDefaultParams:(NSDictionary *)defaultParams { - self.defaultParams = defaultParams; + [self setDefaultParams:defaultParams]; return self; } - - (UPRouterOptions *)root { - self.shouldOpenAsRootViewController = YES; - return self; + [self setShouldOpenAsRootViewController:YES]; + return self; } - @end @interface UPRouter () @@ -137,16 +197,11 @@ @interface UPRouter () @implementation UPRouter -@synthesize navigationController = _navigationController; -@synthesize routes = _routes; -@synthesize cachedRoutes = _cachedRoutes; - - (id)init { if ((self = [super init])) { - self.routes = [NSMutableDictionary new]; - self.cachedRoutes = [NSMutableDictionary new]; + self.routes = [NSMutableDictionary dictionary]; + self.cachedRoutes = [NSMutableDictionary dictionary]; } - return self; } @@ -155,8 +210,14 @@ - (void)map:(NSString *)format toCallback:(RouterOpenCallback)callback { } - (void)map:(NSString *)format toCallback:(RouterOpenCallback)callback withOptions:(UPRouterOptions *)options { + if (!format) { + @throw [NSException exceptionWithName:@"RouteNotProvided" + reason:@"Route #format is not initialized" + userInfo:nil]; + return; + } if (!options) { - options = [UPRouterOptions new]; + options = [UPRouterOptions routerOptions]; } options.callback = callback; [self.routes setObject:options forKey:format]; @@ -167,8 +228,14 @@ - (void)map:(NSString *)format toController:(Class)controllerClass { } - (void)map:(NSString *)format toController:(Class)controllerClass withOptions:(UPRouterOptions *)options { + if (!format) { + @throw [NSException exceptionWithName:@"RouteNotProvided" + reason:@"Route #format is not initialized" + userInfo:nil]; + return; + } if (!options) { - options = [UPRouterOptions new]; + options = [UPRouterOptions routerOptions]; } options.openClass = controllerClass; [self.routes setObject:options forKey:format]; @@ -183,12 +250,19 @@ - (void)open:(NSString *)url { } - (void)open:(NSString *)url animated:(BOOL)animated { - RouterParams *params = [self routerParamsForUrl:url]; + [self open:url animated:animated extraParams:nil]; +} + +- (void)open:(NSString *)url + animated:(BOOL)animated + extraParams:(NSDictionary *)extraParams +{ + RouterParams *params = [self routerParamsForUrl:url extraParams: extraParams]; UPRouterOptions *options = params.routerOptions; if (options.callback) { RouterOpenCallback callback = options.callback; - callback([params getControllerParams]); + callback([params controllerParams]); return; } @@ -223,26 +297,19 @@ - (void)open:(NSString *)url animated:(BOOL)animated { completion:nil]; } } + else if (options.shouldOpenAsRootViewController) { + [self.navigationController setViewControllers:@[controller] animated:animated]; + } else { - if (options.shouldOpenAsRootViewController) { - [self.navigationController setViewControllers:@[controller] animated:animated]; - } - else { - [self.navigationController pushViewController:controller animated:animated]; - } + [self.navigationController pushViewController:controller animated:animated]; } } - -- (NSDictionary*)paramsOfUrl:(NSString*)url{ - RouterParams *params = [self routerParamsForUrl:url]; - return [params getControllerParams]; -} - -- (void)pop { - [self pop:YES]; +- (NSDictionary*)paramsOfUrl:(NSString*)url { + return [[self routerParamsForUrl:url] controllerParams]; } -- (void)pop:(BOOL)animated { +//Stack operations +- (void)popViewControllerFromRouterAnimated:(BOOL)animated { if (self.navigationController.presentedViewController) { [self.navigationController dismissViewControllerAnimated:animated completion:nil]; } @@ -250,11 +317,26 @@ - (void)pop:(BOOL)animated { [self.navigationController popViewControllerAnimated:animated]; } } +- (void)pop { + [self popViewControllerFromRouterAnimated:YES]; +} +- (void)pop:(BOOL)animated { + [self popViewControllerFromRouterAnimated:animated]; +} /////// - -- (RouterParams *)routerParamsForUrl:(NSString *)url { - if ([self.cachedRoutes objectForKey:url]) { +- (RouterParams *)routerParamsForUrl:(NSString *)url extraParams: (NSDictionary *)extraParams { + if (!url) { + //if we wait, caching this as key would throw an exception + if (_ignoresExceptions) { + return nil; + } + @throw [NSException exceptionWithName:@"RouteNotFoundException" + reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url] + userInfo:nil]; + } + + if ([self.cachedRoutes objectForKey:url] && !extraParams) { return [self.cachedRoutes objectForKey:url]; } @@ -265,60 +347,54 @@ - (RouterParams *)routerParamsForUrl:(NSString *)url { givenParts = legacyParts; } - RouterParams *openParams = nil; - for (NSString *routerUrl in self.routes.allKeys) { - UPRouterOptions *routerOptions = (UPRouterOptions *)[self.routes objectForKey:routerUrl]; - NSArray *routerParts = routerUrl.pathComponents; - - if (routerParts.count != givenParts.count) { - continue; - } - - NSDictionary *givenParams = [self paramsForUrlComponents:givenParts withRouterUrlComponents:routerParts]; - if (!givenParams) { - continue; - } - - openParams = [RouterParams new]; - openParams.openParams = givenParams; - openParams.routerOptions = routerOptions; - break; - } + __block RouterParams *openParams = nil; + [self.routes enumerateKeysAndObjectsUsingBlock: + ^(NSString *routerUrl, UPRouterOptions *routerOptions, BOOL *stop) { + + NSArray *routerParts = [routerUrl pathComponents]; + if ([routerParts count] == [givenParts count]) { + + NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts]; + if (givenParams) { + openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams]; + *stop = YES; + } + } + }]; if (!openParams) { if (_ignoresExceptions) { return nil; } - @throw [NSException exceptionWithName:@"RouteNotFoundException" reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url] userInfo:nil]; } - [self.cachedRoutes setObject:openParams forKey:url]; - return openParams; } +- (RouterParams *)routerParamsForUrl:(NSString *)url { + return [self routerParamsForUrl:url extraParams: nil]; +} + - (NSDictionary *)paramsForUrlComponents:(NSArray *)givenUrlComponents - withRouterUrlComponents:(NSArray *)routerUrlComponents { - NSMutableDictionary *params = [NSMutableDictionary new]; - - for (NSUInteger i = 0; i < routerUrlComponents.count; i++) { - NSString *routerComponent = routerUrlComponents[i]; - NSString *givenComponent = givenUrlComponents[i]; - - if ([[routerComponent substringToIndex:1] isEqualToString:@":"]) { - NSString *key = [routerComponent substringFromIndex:1]; - [params setObject:givenComponent forKey:key]; - continue; - } - - if (![routerComponent isEqualToString:givenComponent]) { - return nil; - } - } + routerUrlComponents:(NSArray *)routerUrlComponents { + __block NSMutableDictionary *params = [NSMutableDictionary dictionary]; + [routerUrlComponents enumerateObjectsUsingBlock: + ^(NSString *routerComponent, NSUInteger idx, BOOL *stop) { + + NSString *givenComponent = givenUrlComponents[idx]; + if ([routerComponent hasPrefix:@":"]) { + NSString *key = [routerComponent substringFromIndex:1]; + [params setObject:givenComponent forKey:key]; + } + else if (![routerComponent isEqualToString:givenComponent]) { + params = nil; + *stop = YES; + } + }]; return params; } @@ -330,24 +406,16 @@ - (UIViewController *)controllerForRouterParams:(RouterParams *)params { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if ([controllerClass respondsToSelector:CONTROLLER_CLASS_SELECTOR]) { - controller = [controllerClass performSelector:CONTROLLER_CLASS_SELECTOR withObject:[params getControllerParams]]; + controller = [controllerClass performSelector:CONTROLLER_CLASS_SELECTOR withObject:[params controllerParams]]; } - else { - controller = [params.routerOptions.openClass alloc]; - if ([controller respondsToSelector:CONTROLLER_SELECTOR]) { - controller = [controller performSelector:CONTROLLER_SELECTOR withObject:[params getControllerParams]]; - } - else { - controller = nil; - } + else if ([params.routerOptions.openClass instancesRespondToSelector:CONTROLLER_SELECTOR]) { + controller = [[params.routerOptions.openClass alloc] performSelector:CONTROLLER_SELECTOR withObject:[params controllerParams]]; } #pragma clang diagnostic pop - - if (controller == nil) { + if (!controller) { if (_ignoresExceptions) { - return nil; + return controller; } - @throw [NSException exceptionWithName:@"RoutableInitializerNotFound" reason:[NSString stringWithFormat:INVALID_CONTROLLER_FORMAT, NSStringFromClass(controllerClass), NSStringFromSelector(CONTROLLER_CLASS_SELECTOR), NSStringFromSelector(CONTROLLER_SELECTOR)] userInfo:nil]; @@ -355,8 +423,8 @@ - (UIViewController *)controllerForRouterParams:(RouterParams *)params { controller.modalTransitionStyle = params.routerOptions.transitionStyle; controller.modalPresentationStyle = params.routerOptions.presentationStyle; - return controller; } @end + diff --git a/RoutableTests/RoutableTests.h b/RoutableTests/RoutableTests.h index 5adcc4d..3c51903 100644 --- a/RoutableTests/RoutableTests.h +++ b/RoutableTests/RoutableTests.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 TurboProp Inc. All rights reserved. // -#import +#import -@interface RoutableTests : SenTestCase +@interface RoutableTests : XCTestCase @end diff --git a/RoutableTests/RoutableTests.m b/RoutableTests/RoutableTests.m index f79e261..da1f3c5 100644 --- a/RoutableTests/RoutableTests.m +++ b/RoutableTests/RoutableTests.m @@ -9,10 +9,33 @@ #import "RoutableTests.h" #import "Routable.h" +@interface ExtraParamObject : NSObject + +@property NSInteger number; +@property NSString *string; + +- (id)initWithNumber:(NSInteger)number andString:(NSString *)string; + +@end + +@implementation ExtraParamObject + +- (id)initWithNumber:(NSInteger)number andString:(NSString *)string { + self = [super init]; + self.number = number; + self.string = string; + + return self; +} + +@end + @interface UserController : UIViewController @property (readwrite, nonatomic, copy) NSString *userId; @property (readwrite, nonatomic, copy) NSString *userName; +@property (readwrite, nonatomic, copy) NSString *extraParam; +@property (readwrite, nonatomic) ExtraParamObject *epObject; - (id)initWithRouterParams:(NSDictionary *)params; @end @@ -23,6 +46,8 @@ - (id)initWithRouterParams:(NSDictionary *)params { if ((self = [self initWithNibName:nil bundle:nil])) { self.userId = [params objectForKey:@"user_id"]; self.userName = [params objectForKey:@"user_name"]; + self.extraParam = [params objectForKey:@"keyOne"]; + self.epObject = [params objectForKey:@"epObject"]; } return self; } @@ -41,7 +66,7 @@ @implementation StaticController + (id)allocWithRouterParams:(NSDictionary *)params { StaticController *instance = [[StaticController alloc] initWithNibName:nil bundle:nil]; instance.userId = [params objectForKey:@"user_id"]; - + return instance; } @@ -54,13 +79,15 @@ @interface RoutableTests() #define USER_ID [(UserController *)self.router.navigationController.topViewController userId] #define USER_NAME [(UserController *)self.router.navigationController.topViewController userName] #define STATIC_USER_ID [(StaticController *)self.router.navigationController.topViewController userId] +#define USER_EXTRA_PARAM [(UserController *)self.router.navigationController.topViewController extraParam] +#define USER_EPOBJECT [(UserController *)self.router.navigationController.topViewController epObject] @implementation RoutableTests @synthesize router = _router; - (void)setUp { [super setUp]; - + self.router = [Routable newRouter]; self.router.navigationController = [[UINavigationController alloc] initWithNibName: nil bundle: nil]; } @@ -69,7 +96,7 @@ - (void)tearDown { // Tear-down code here. [self.router.navigationController setViewControllers:[NSArray array]]; self.router = nil; - + [super tearDown]; } @@ -78,29 +105,34 @@ - (void)test_basicRoute { [self.router open:@"users/4"]; - STAssertTrue([USER_ID isEqualToString:@"4"], @"Should have an ID of 4"); + XCTAssertTrue([USER_ID isEqualToString:@"4"], @"Should have an ID of 4"); } - (void)test_basicRouteWithMultipleComponents { - [self.router map:@"users/:user_id/:user_name" toController:[UserController class]]; - - [self.router open:@"users/4/clay"]; - - STAssertTrue([USER_ID isEqualToString:@"4"], @"Should have an ID of 4"); - STAssertTrue([USER_NAME isEqualToString:@"clay"], @"Name should be Clay"); + [self.router map:@"users/:user_id/:user_name" toController:[UserController class]]; + + [self.router open:@"users/4/clay"]; + + XCTAssertTrue([USER_ID isEqualToString:@"4"], @"Should have an ID of 4"); + XCTAssertTrue([USER_NAME isEqualToString:@"clay"], @"Name should be Clay"); } - (void)test_basicRouteWithEmptyComponents { - [self.router map:@"users/:user_id/:user_name" toController:[UserController class]]; - - [self.router open:@"users//clay"]; - - STAssertTrue([USER_ID isEqualToString:@""], @"Should have an empty ID"); - STAssertTrue([USER_NAME isEqualToString:@"clay"], @"Name should be Clay"); + [self.router map:@"users/:user_id/:user_name" toController:[UserController class]]; + + [self.router open:@"users//clay"]; + + XCTAssertTrue([USER_ID isEqualToString:@""], @"Should have an empty ID"); + XCTAssertTrue([USER_NAME isEqualToString:@"clay"], @"Name should be Clay"); +} - [self.router open:@"users/ /clay"]; - STAssertTrue([USER_ID isEqualToString:@" "], @"Should have an empty ID"); - STAssertTrue([USER_NAME isEqualToString:@"clay"], @"Name should be Clay"); + +- (void)test_basicRouteWithWhitespaceCompnents { + [self.router map:@"users/:user_id/:user_name" toController:[UserController class]]; + + [self.router open:@"users/ /clay"]; + XCTAssertTrue([USER_ID isEqualToString:@" "], @"Should have an empty ID"); + XCTAssertTrue([USER_NAME isEqualToString:@"clay"], @"Name should be Clay"); } - (void)test_emptyRoute { @@ -108,18 +140,18 @@ - (void)test_emptyRoute { [self.router open:@"users"]; - STAssertNil(USER_ID, @"The user id should be nil, not %@", USER_ID); + XCTAssertNil(USER_ID, @"The user id should be nil, not %@", USER_ID); } - (void)test_invalidRoute { - STAssertThrows([self.router open:@"users"], @"Should throw an exception when route not mapped"); + XCTAssertThrows([self.router open:@"users"], @"Should throw an exception when route not mapped"); } - (void)test_noNavigationController { [self.router map:@"users" toController:[UserController class]]; - + self.router.navigationController = nil; - STAssertThrows([self.router open:@"users"], @"Should throw an exception when there is no navigation controller"); + XCTAssertThrows([self.router open:@"users"], @"Should throw an exception when there is no navigation controller"); } - (void)test_callbacks { @@ -130,7 +162,7 @@ - (void)test_callbacks { [self.router open:@"users"]; - STAssertTrue(called, @"Callback block should have been called"); + XCTAssertTrue(called, @"Callback block should have been called"); } - (void)test_callbacksWithParams { @@ -143,8 +175,8 @@ - (void)test_callbacksWithParams { [self.router open:@"users/4"]; - STAssertTrue(called, @"Callback block should have been called"); - STAssertTrue([userId isEqualToString:@"4"], @"Callback should have correct id param; was %@", userId); + XCTAssertTrue(called, @"Callback block should have been called"); + XCTAssertTrue([userId isEqualToString:@"4"], @"Callback should have correct id param; was %@", userId); } - (void)test_defaultParamsWithCallback { @@ -156,31 +188,74 @@ - (void)test_defaultParamsWithCallback { [self.router open:@"users"]; - STAssertTrue([userId isEqualToString:@"4"], @"Callback should have correct id param; was %@", userId); + XCTAssertTrue([userId isEqualToString:@"4"], @"Callback should have correct id param; was %@", userId); } -- (void)test_staticRoute { +- (void)test_staticRoute { [self.router map:@"users/:user_id" toController:[StaticController class]]; [self.router open:@"users/4"]; - STAssertTrue([self.router.navigationController.topViewController isKindOfClass:[StaticController class]], @"Should be an instance of StaticController"); - STAssertTrue([STATIC_USER_ID isEqualToString:@"4"], @"Should have an ID of 4"); + XCTAssertTrue([self.router.navigationController.topViewController isKindOfClass:[StaticController class]], @"Should be an instance of StaticController"); + XCTAssertTrue([STATIC_USER_ID isEqualToString:@"4"], @"Should have an ID of 4"); } - (void)test_noInitializers { [self.router map:@"routable" toController:[UIViewController class]]; - STAssertThrows([self.router open:@"routable"], @"Should throw an exception when no initializer found"); + XCTAssertThrows([self.router open:@"routable"], @"Should throw an exception when no initializer found"); } - (void)test_openAsRoot { - [self.router map:@"users/:user_id" toController:[UserController class] withOptions:[UPRouterOptions root]]; - - [self.router open:@"users/4"]; - - STAssertTrue([self.router.navigationController.viewControllers[0] isKindOfClass:[UserController class]], @"Should be an instance of UserController"); - STAssertTrue(self.router.navigationController.viewControllers.count == 1, @"Navigation controller should only have 1 view controller in its stack"); + [self.router map:@"users/:user_id" toController:[UserController class] withOptions:[UPRouterOptions root]]; + + [self.router open:@"users/4"]; + + XCTAssertTrue([self.router.navigationController.viewControllers[0] isKindOfClass:[UserController class]], @"Should be an instance of UserController"); + XCTAssertTrue(self.router.navigationController.viewControllers.count == 1, @"Navigation controller should only have 1 view controller in its stack"); +} + +- (void)test_openWithExtraStringParam { + [self.router map:@"users" toController:[UserController class]]; + + NSDictionary *params = [[NSDictionary alloc] initWithObjects:@[@"objOne"] forKeys:@[@"keyOne"]]; + + [self.router open:@"users" animated:NO extraParams:params]; + + XCTAssertEqual(USER_EXTRA_PARAM, @"objOne", @"Should be able to get extra string param in view controller"); +} + +- (void)test_openWithExtraObjectParam { + [self.router map:@"users" toController:[UserController class]]; + + ExtraParamObject *epObject = [[ExtraParamObject alloc] initWithNumber:99 andString:@"secretString"]; + NSDictionary *params = [NSDictionary dictionaryWithObjects:@[epObject] forKeys:@[@"epObject"]]; + + [self.router open:@"users" animated:NO extraParams:params]; + + XCTAssertEqual((ExtraParamObject *)USER_EPOBJECT, epObject, @"Should be able to get extra param object in view controller"); + +} + +- (void)test_openWithURLParamsAndExtraParams { + [self.router map:@"users/:user_id" toController:[UserController class]]; + + ExtraParamObject *epObject = [[ExtraParamObject alloc] initWithNumber:99 andString:@"secretString"]; + NSDictionary *params = [NSDictionary dictionaryWithObjects:@[epObject] forKeys:@[@"epObject"]]; + + [self.router open:@"users/4" animated:NO extraParams:params]; + + XCTAssertTrue([USER_ID isEqualToString:@"4"], @"Should have ID of 4"); + XCTAssertEqual((ExtraParamObject *)USER_EPOBJECT, epObject, @"Should be able to get extra param object in view controller"); } +- (void)test_openWithNilExtraParams { + [self.router map:@"users" toController:[UserController class]]; + + [self.router open:@"users" animated:NO extraParams:nil]; + + XCTAssertNil(USER_EXTRA_PARAM, @"No extra params should be passed if none are specified"); +} + + @end