From 38789d00877f77ad174b0d39442fc36dbdf0b53b Mon Sep 17 00:00:00 2001 From: Ben-G Date: Wed, 16 Mar 2016 19:55:14 -0700 Subject: [PATCH 1/4] [Project] Enable Code Coverage for Router's iOS Scheme --- .../xcshareddata/xcschemes/ReSwiftRouter-iOS.xcscheme | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ReSwiftRouter.xcodeproj/xcshareddata/xcschemes/ReSwiftRouter-iOS.xcscheme b/ReSwiftRouter.xcodeproj/xcshareddata/xcschemes/ReSwiftRouter-iOS.xcscheme index 8094efa..ea456c0 100644 --- a/ReSwiftRouter.xcodeproj/xcshareddata/xcschemes/ReSwiftRouter-iOS.xcscheme +++ b/ReSwiftRouter.xcodeproj/xcshareddata/xcschemes/ReSwiftRouter-iOS.xcscheme @@ -26,7 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> From d00426b813c6c57fdc757ade8b106de3d48a1313 Mon Sep 17 00:00:00 2001 From: Ben-G Date: Wed, 16 Mar 2016 19:55:36 -0700 Subject: [PATCH 2/4] [Router] Provide Action to Set Route Specific Data --- ReSwiftRouter/NavigationActions.swift | 12 +++++++- ReSwiftRouter/NavigationReducer.swift | 13 +++++++++ ReSwiftRouter/NavigationState.swift | 28 +++++++++++++++++- .../ReSwiftRouterIntegrationTests.swift | 29 +++++++++++++++++++ .../ReSwiftRouterTestsUnitTests.swift | 1 + 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/ReSwiftRouter/NavigationActions.swift b/ReSwiftRouter/NavigationActions.swift index 5adb173..aabe84f 100644 --- a/ReSwiftRouter/NavigationActions.swift +++ b/ReSwiftRouter/NavigationActions.swift @@ -29,4 +29,14 @@ public struct SetRouteAction: StandardActionConvertible { return StandardAction(type: SetRouteAction.type, payload: ["route": route], isTypedAction: true) } -} \ No newline at end of file +} + +public struct SetRouteSpecificData: Action { + let route: Route + let data: Any + + public init(route: Route, data: Any) { + self.route = route + self.data = data + } +} diff --git a/ReSwiftRouter/NavigationReducer.swift b/ReSwiftRouter/NavigationReducer.swift index c8f8a18..3f0f9d5 100644 --- a/ReSwiftRouter/NavigationReducer.swift +++ b/ReSwiftRouter/NavigationReducer.swift @@ -22,6 +22,8 @@ public struct NavigationReducer { switch action { case let action as SetRouteAction: return setRoute(state, route: action.route) + case let action as SetRouteSpecificData: + return setRouteSpecificData(state, route: action.route, data: action.data) default: break } @@ -35,4 +37,15 @@ public struct NavigationReducer { return state } + static func setRouteSpecificData( + var state: NavigationState, + route: Route, + data: Any) -> NavigationState{ + let routeHash = RouteHash(route: route) + + state.routeSpecificState[routeHash] = data + + return state + } + } diff --git a/ReSwiftRouter/NavigationState.swift b/ReSwiftRouter/NavigationState.swift index 3b8891b..22c446c 100644 --- a/ReSwiftRouter/NavigationState.swift +++ b/ReSwiftRouter/NavigationState.swift @@ -11,9 +11,35 @@ import ReSwift public typealias RouteElementIdentifier = String public typealias Route = [RouteElementIdentifier] +public struct RouteHash: Hashable { + let routeHash: String + + init(route: Route) { + self.routeHash = route.joinWithSeparator("/") + } + + public var hashValue: Int { return self.routeHash.hashValue } +} + +public func == (lhs: RouteHash, rhs: RouteHash) -> Bool { + return lhs.routeHash == rhs.routeHash +} + public struct NavigationState { public init() {} public var route: Route = [] - public var subRouteState: [StateType] = [] + public var routeSpecificState: [RouteHash: Any] = [:] +} + +extension NavigationState { + public func getRouteSpecificState(route: Route) -> T? { + let hash = RouteHash(route: route) + + return self.routeSpecificState[hash] as? T + } +} + +public protocol HasNavigationState { + var navigationState: NavigationState { get set } } diff --git a/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift b/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift index f1b9872..37aa059 100644 --- a/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift +++ b/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift @@ -178,6 +178,35 @@ class SwiftFlowRouterIntegrationTests: QuickSpec { } } + + + describe("route specific data") { + + var store: Store! + + beforeEach { + store = Store(reducer: AppReducer(), state: nil) + } + + context("when setting route specific data") { + + beforeEach { + store.dispatch(SetRouteSpecificData(route: ["part1", "part2"], data: "UserID_10")) + } + + it("allows accessing the data when providing the expected type") { + let data: String? = store.state.navigationState.getRouteSpecificState( + ["part1", "part2"] + ) + + expect(data).toEventually(equal("UserID_10")) + } + + } + + } + + } } diff --git a/ReSwiftRouterTests/ReSwiftRouterTestsUnitTests.swift b/ReSwiftRouterTests/ReSwiftRouterTestsUnitTests.swift index 0ccc172..5ca16c6 100644 --- a/ReSwiftRouterTests/ReSwiftRouterTestsUnitTests.swift +++ b/ReSwiftRouterTests/ReSwiftRouterTestsUnitTests.swift @@ -234,6 +234,7 @@ class ReSwiftRouterUnitTests: QuickSpec { } } + } } \ No newline at end of file From 95dfb84756d60e071c97d303f69dd314f719398a Mon Sep 17 00:00:00 2001 From: Ben-G Date: Wed, 16 Mar 2016 20:19:54 -0700 Subject: [PATCH 3/4] [Router] Use Symbolic Breakpoint Instead of AssertionFailure The Router will now print a warning when stuck and call a free function. This allows developers to create a symbolic breakpoint and see the callstack at the point where the issue occurs. Fixes #12. --- ReSwiftRouter/Router.swift | 9 +++++++-- ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ReSwiftRouter/Router.swift b/ReSwiftRouter/Router.swift index e928ec2..341fb0d 100644 --- a/ReSwiftRouter/Router.swift +++ b/ReSwiftRouter/Router.swift @@ -77,9 +77,12 @@ public class Router: StoreSubscriber { let result = dispatch_semaphore_wait(semaphore, waitUntil) if result != 0 { - assertionFailure("[SwiftFlowRouter]: Router is stuck waiting for a" + - " completion handler to be called. Ensure that you have called the " + + print("[SwiftFlowRouter]: Router is stuck waiting for a" + + " completion handler to be called. Ensure that you have called the" + " completion handler in each Routable element.") + print("Set a symbolic breakpoint for the `ReSwiftRouterStuck` symbol in order" + + " to halt the program when this happens") + ReSwiftRouterStuck() } } @@ -194,6 +197,8 @@ public class Router: StoreSubscriber { } +func ReSwiftRouterStuck() {} + enum RoutingActions { case Push(responsibleRoutableIndex: Int, segmentToBePushed: RouteElementIdentifier) case Pop(responsibleRoutableIndex: Int, segmentToBePopped: RouteElementIdentifier) diff --git a/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift b/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift index 37aa059..ab3528c 100644 --- a/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift +++ b/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift @@ -125,7 +125,6 @@ class SwiftFlowRouterIntegrationTests: QuickSpec { it("calls push on the root for a route with two elements") { store.dispatch( - SetRouteAction( ["TabBarViewController", "SecondViewController"] ) From fca7a2b2e9c6dd3bd18dd1d49c314f652d0b5b3e Mon Sep 17 00:00:00 2001 From: Ben-G Date: Wed, 16 Mar 2016 21:01:15 -0700 Subject: [PATCH 4/4] [Router] Enable Setting Route Unanimated Fixes #4 --- ReSwiftRouter/NavigationActions.swift | 11 +- ReSwiftRouter/NavigationReducer.swift | 7 +- ReSwiftRouter/NavigationState.swift | 1 + ReSwiftRouter/Routable.swift | 35 ++++-- ReSwiftRouter/Router.swift | 18 ++-- .../ReSwiftRouterIntegrationTests.swift | 100 +++++++++++++++--- 6 files changed, 139 insertions(+), 33 deletions(-) diff --git a/ReSwiftRouter/NavigationActions.swift b/ReSwiftRouter/NavigationActions.swift index aabe84f..9d6b598 100644 --- a/ReSwiftRouter/NavigationActions.swift +++ b/ReSwiftRouter/NavigationActions.swift @@ -15,18 +15,25 @@ public let typeMap: [String: StandardActionConvertible.Type] = public struct SetRouteAction: StandardActionConvertible { let route: Route + let animated: Bool public static let type = "RE_SWIFT_ROUTER_SET_ROUTE" - public init (_ route: Route) { + public init (_ route: Route, animated: Bool = true) { self.route = route + self.animated = animated } public init(_ action: StandardAction) { self.route = action.payload!["route"] as! Route + self.animated = action.payload!["animated"] as! Bool } public func toStandardAction() -> StandardAction { - return StandardAction(type: SetRouteAction.type, payload: ["route": route], isTypedAction: true) + return StandardAction( + type: SetRouteAction.type, + payload: ["route": route, "animated": animated], + isTypedAction: true + ) } } diff --git a/ReSwiftRouter/NavigationReducer.swift b/ReSwiftRouter/NavigationReducer.swift index 3f0f9d5..1dcf8b5 100644 --- a/ReSwiftRouter/NavigationReducer.swift +++ b/ReSwiftRouter/NavigationReducer.swift @@ -21,7 +21,7 @@ public struct NavigationReducer { switch action { case let action as SetRouteAction: - return setRoute(state, route: action.route) + return setRoute(state, setRouteAction: action) case let action as SetRouteSpecificData: return setRouteSpecificData(state, route: action.route, data: action.data) default: @@ -31,8 +31,9 @@ public struct NavigationReducer { return state } - static func setRoute(var state: NavigationState, route: Route) -> NavigationState { - state.route = route + static func setRoute(var state: NavigationState, setRouteAction: SetRouteAction) -> NavigationState { + state.route = setRouteAction.route + state.changeRouteAnimated = setRouteAction.animated return state } diff --git a/ReSwiftRouter/NavigationState.swift b/ReSwiftRouter/NavigationState.swift index 22c446c..9917022 100644 --- a/ReSwiftRouter/NavigationState.swift +++ b/ReSwiftRouter/NavigationState.swift @@ -30,6 +30,7 @@ public struct NavigationState { public var route: Route = [] public var routeSpecificState: [RouteHash: Any] = [:] + var changeRouteAnimated: Bool = true } extension NavigationState { diff --git a/ReSwiftRouter/Routable.swift b/ReSwiftRouter/Routable.swift index 40149eb..65505a9 100644 --- a/ReSwiftRouter/Routable.swift +++ b/ReSwiftRouter/Routable.swift @@ -10,31 +10,46 @@ public typealias RoutingCompletionHandler = () -> Void public protocol Routable { - func changeRouteSegment(from: RouteElementIdentifier, - to: RouteElementIdentifier, - completionHandler: RoutingCompletionHandler) -> Routable - - func pushRouteSegment(routeElementIdentifier: RouteElementIdentifier, + func pushRouteSegment( + routeElementIdentifier: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) -> Routable - func popRouteSegment(routeElementIdentifier: RouteElementIdentifier, + func popRouteSegment( + routeElementIdentifier: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) + func changeRouteSegment( + from: RouteElementIdentifier, + to: RouteElementIdentifier, + animated: Bool, + completionHandler: RoutingCompletionHandler) -> Routable + } extension Routable { - public func changeRouteSegment(from: RouteElementIdentifier, - to: RouteElementIdentifier, completionHandler: RoutingCompletionHandler) -> Routable { + + public func pushRouteSegment( + routeElementIdentifier: RouteElementIdentifier, + animated: Bool, + completionHandler: RoutingCompletionHandler) -> Routable { fatalError("This routable cannot change segments. You have not implemented it.") } - public func popRouteSegment(routeElementIdentifier: RouteElementIdentifier, + public func popRouteSegment( + routeElementIdentifier: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) { fatalError("This routable cannot change segments. You have not implemented it.") } - public func pushRouteSegment(routeElementIdentifier: RouteElementIdentifier, + public func changeRouteSegment( + from: RouteElementIdentifier, + to: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) -> Routable { fatalError("This routable cannot change segments. You have not implemented it.") } + } diff --git a/ReSwiftRouter/Router.swift b/ReSwiftRouter/Router.swift index 341fb0d..53b9755 100644 --- a/ReSwiftRouter/Router.swift +++ b/ReSwiftRouter/Router.swift @@ -44,8 +44,10 @@ public class Router: StoreSubscriber { case let .Pop(responsibleRoutableIndex, segmentToBePopped): dispatch_async(dispatch_get_main_queue()) { self.routables[responsibleRoutableIndex] - .popRouteSegment(segmentToBePopped) { - dispatch_semaphore_signal(semaphore) + .popRouteSegment( + segmentToBePopped, + animated: state.changeRouteAnimated) { + dispatch_semaphore_signal(semaphore) } self.routables.removeAtIndex(responsibleRoutableIndex + 1) @@ -55,8 +57,10 @@ public class Router: StoreSubscriber { dispatch_async(dispatch_get_main_queue()) { self.routables[responsibleRoutableIndex + 1] = self.routables[responsibleRoutableIndex] - .changeRouteSegment(segmentToBeReplaced, - to: newSegment) { + .changeRouteSegment( + segmentToBeReplaced, + to: newSegment, + animated: state.changeRouteAnimated) { dispatch_semaphore_signal(semaphore) } } @@ -65,8 +69,10 @@ public class Router: StoreSubscriber { dispatch_async(dispatch_get_main_queue()) { self.routables.append( self.routables[responsibleRoutableIndex] - .pushRouteSegment(segmentToBePushed) { - dispatch_semaphore_signal(semaphore) + .pushRouteSegment( + segmentToBePushed, + animated: state.changeRouteAnimated) { + dispatch_semaphore_signal(semaphore) } ) } diff --git a/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift b/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift index ab3528c..f9bf55c 100644 --- a/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift +++ b/ReSwiftRouterTests/ReSwiftRouterIntegrationTests.swift @@ -11,25 +11,47 @@ import Nimble import ReSwift @testable import ReSwiftRouter -class FakeRoutable: Routable { +class MockRoutable: Routable { + var callsToPushRouteSegment: [(routeElement: RouteElementIdentifier, animated: Bool)] = [] + var callsToPopRouteSegment: [(routeElement: RouteElementIdentifier, animated: Bool)] = [] + var callsToChangeRouteSegment: [( + from: RouteElementIdentifier, + to: RouteElementIdentifier, + animated: Bool + )] = [] - func pushRouteSegment(routeSegment: RouteElementIdentifier, + func pushRouteSegment( + routeElementIdentifier: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) -> Routable { + callsToPushRouteSegment.append( + (routeElement: routeElementIdentifier, animated: animated) + ) completionHandler() - return FakeRoutable() + return MockRoutable() } - func popRouteSegment(routeSegment: RouteElementIdentifier, + func popRouteSegment( + routeElementIdentifier: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) { + callsToPopRouteSegment.append( + (routeElement: routeElementIdentifier, animated: animated) + ) completionHandler() } - func changeRouteSegment(from: RouteElementIdentifier, + func changeRouteSegment( + from: RouteElementIdentifier, to: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) -> Routable { completionHandler() - return FakeRoutable() + + callsToChangeRouteSegment.append((from: from, to: to, animated: animated)) + + return MockRoutable() } } @@ -74,7 +96,7 @@ class SwiftFlowRouterIntegrationTests: QuickSpec { func pushRouteSegment(routeElementIdentifier: RouteElementIdentifier, completionHandler: RoutingCompletionHandler) -> Routable { called = true - return FakeRoutable() + return MockRoutable() } } @@ -100,12 +122,14 @@ class SwiftFlowRouterIntegrationTests: QuickSpec { self.calledWithIdentifier = calledWithIdentifier } - func pushRouteSegment(routeSegment: RouteElementIdentifier, + func pushRouteSegment( + routeSegment: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) -> Routable { calledWithIdentifier(routeSegment) completionHandler() - return FakeRoutable() + return MockRoutable() } } @@ -137,12 +161,14 @@ class SwiftFlowRouterIntegrationTests: QuickSpec { self.calledWithIdentifier = calledWithIdentifier } - func pushRouteSegment(routeSegment: RouteElementIdentifier, + func pushRouteSegment( + routeSegment: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) -> Routable { calledWithIdentifier(routeSegment) completionHandler() - return FakeRoutable() + return MockRoutable() } } @@ -160,7 +186,9 @@ class SwiftFlowRouterIntegrationTests: QuickSpec { self.injectedRoutable = injectedRoutable } - func pushRouteSegment(routeElementIdentifier: RouteElementIdentifier, + func pushRouteSegment( + routeElementIdentifier: RouteElementIdentifier, + animated: Bool, completionHandler: RoutingCompletionHandler) -> Routable { completionHandler() return injectedRoutable @@ -205,6 +233,54 @@ class SwiftFlowRouterIntegrationTests: QuickSpec { } + describe("configuring animated/unanimated navigation") { + + var store: Store! + var mockRoutable: MockRoutable! + var router: Router! + + beforeEach { + store = Store(reducer: AppReducer(), state: nil) + mockRoutable = MockRoutable() + router = Router(store: store, rootRoutable: mockRoutable) { state in + state.navigationState + } + + // silence router not read warning, need to keep router alive via reference + _ = router + } + + context("when dispatching an animated route change") { + beforeEach { + store.dispatch(SetRouteAction(["someRoute"], animated: true)) + } + + it("calls routables asking for an animated presentation") { + expect(mockRoutable.callsToPushRouteSegment.last?.animated).toEventually(beTrue()) + } + } + + context("when dispatching an unanimated route change") { + beforeEach { + store.dispatch(SetRouteAction(["someRoute"], animated: false)) + } + + it("calls routables asking for an animated presentation") { + expect(mockRoutable.callsToPushRouteSegment.last?.animated).toEventually(beFalse()) + } + } + + context("when dispatching a default route change") { + beforeEach { + store.dispatch(SetRouteAction(["someRoute"])) + } + + it("calls routables asking for an animated presentation") { + expect(mockRoutable.callsToPushRouteSegment.last?.animated).toEventually(beTrue()) + } + } + } + }