diff --git a/Cycle/Classes/Cycle.swift b/Cycle/Classes/Cycle.swift index e22e313..f4b4c9f 100644 --- a/Cycle/Classes/Cycle.swift +++ b/Cycle/Classes/Cycle.swift @@ -23,7 +23,7 @@ open class CycledApplicationDelegate: UIResponder, UIAp _ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil ) -> Bool { - window = UIWindow(frame: UIScreen.main.fixedCoordinateSpace.bounds, root: cycle.root) + window = UIWindow(frame: UIScreen.main.bounds, root: cycle.root) window?.makeKeyAndVisible() return cycle.delegate.application!( application, @@ -48,22 +48,25 @@ extension UIWindow { } public final class Cycle { - fileprivate var events: Observable? - fileprivate var eventsProxy: ReplaySubject? - fileprivate let cleanup = DisposeBag() + private var events: Observable? + private var eventsProxy: ReplaySubject? + private let cleanup = DisposeBag() fileprivate let delegate: UIApplicationDelegate fileprivate let root: UIViewController public required init(transformer: E) { eventsProxy = ReplaySubject.create( bufferSize: 1 ) - let drivers = E.Drivers() + let drivers = transformer.driversFrom(initial: E.Source()) root = drivers.screen.root delegate = drivers.application events = transformer.effectsFrom( events: eventsProxy!, drivers: drivers ) + // `.startWith` is redundant, but necessary to kickoff cycle + // Possibly removed if `events` was BehaviorSubject? + // Not sure how to `merge` observables to single BehaviorSubject though. events? .startWith(E.Source()) .subscribe { [weak self] in @@ -74,12 +77,11 @@ public final class Cycle { public protocol SinkSourceConverting { associatedtype Source: Initializable - associatedtype Drivers: CycleDrivable + associatedtype Drivers: UIApplicationDelegateProviding, ScreenDrivable + func driversFrom(initial: Source) -> Drivers func effectsFrom(events: Observable, drivers: Drivers) -> Observable } -public protocol CycleDrivable: Initializable, UIApplicationProviding, ScreenDrivable {} - public protocol Initializable { init() } @@ -93,15 +95,7 @@ public protocol UIViewControllerProviding { var root: UIViewController { get } } -public protocol UIApplicationProviding { +public protocol UIApplicationDelegateProviding { associatedtype Delegate: UIApplicationDelegate var application: Delegate { get } } - -extension UIViewController { - public static var empty: UIViewController { - let x = UIViewController() - x.view.backgroundColor = .white - return x - } -} diff --git a/Example/Cycle/Integer Mutation/IntegerMutatingApp.swift b/Example/Cycle/Integer Mutation/IntegerMutatingApp.swift index 3e17915..9322ae6 100644 --- a/Example/Cycle/Integer Mutation/IntegerMutatingApp.swift +++ b/Example/Cycle/Integer Mutation/IntegerMutatingApp.swift @@ -24,9 +24,15 @@ struct IntegerMutatingApp: SinkSourceConverting { var screen = ValueToggler.Model.empty var application = RxUIApplication.Model.empty } - struct Drivers: CycleDrivable { - let screen = ValueToggler() - let application = RxUIApplication(initial: .empty) + struct Drivers: UIApplicationDelegateProviding, ScreenDrivable { + let screen: ValueToggler + let application: RxUIApplication + } + func driversFrom(initial: IntegerMutatingApp.Model) -> IntegerMutatingApp.Drivers { return + Drivers( + screen: ValueToggler(), + application: RxUIApplication(initial: initial.application) + ) } func effectsFrom(events: Observable, drivers: Drivers) -> Observable { let value = drivers.screen diff --git a/Example/Cycle/Integer Mutation/ValueToggler.swift b/Example/Cycle/Integer Mutation/ValueToggler.swift index ac49886..ae520ca 100644 --- a/Example/Cycle/Integer Mutation/ValueToggler.swift +++ b/Example/Cycle/Integer Mutation/ValueToggler.swift @@ -13,9 +13,7 @@ import CoreLocation // import Cycle class ValueToggler: UIViewControllerProviding { - - static var shared = ValueToggler() - + struct Model { struct Button { enum State { @@ -57,7 +55,6 @@ class ValueToggler: UIViewControllerProviding { ) var cleanup = DisposeBag() - var input: Observable? let root = UIViewController.empty init() { @@ -120,3 +117,11 @@ extension ValueToggler.Model { ) } } + +extension UIViewController { + public static var empty: UIViewController { + let x = UIViewController() + x.view.backgroundColor = .white + return x + } +} diff --git a/Example/Cycle/Push Notification Registration/PushNotificationRegistration.swift b/Example/Cycle/Push Notification Registration/PushNotificationRegistration.swift index 12f58c5..1c11001 100644 --- a/Example/Cycle/Push Notification Registration/PushNotificationRegistration.swift +++ b/Example/Cycle/Push Notification Registration/PushNotificationRegistration.swift @@ -27,9 +27,15 @@ struct PushNotificationRegistration: SinkSourceConverting { struct Model: Initializable { var application = RxUIApplication.Model.empty } - struct Drivers: CycleDrivable { - let screen = ScreenDriver() - let application = RxUIApplication(initial: .empty) + struct Drivers: UIApplicationDelegateProviding, ScreenDrivable { + let screen: ScreenDriver + let application: RxUIApplication + } + func driversFrom(initial: PushNotificationRegistration.Model) -> PushNotificationRegistration.Drivers { return + Drivers( + screen: ScreenDriver(), + application: RxUIApplication(initial: initial.application) + ) } func effectsFrom(events: Observable, drivers: Drivers) -> Observable { return drivers.application diff --git a/Example/Cycle/Shortcut Items/ShortcutItems.swift b/Example/Cycle/Shortcut Items/ShortcutItems.swift index 0b2d0fa..4b0354b 100644 --- a/Example/Cycle/Shortcut Items/ShortcutItems.swift +++ b/Example/Cycle/Shortcut Items/ShortcutItems.swift @@ -28,10 +28,17 @@ struct ShortcutActionsExample: SinkSourceConverting { var application = RxUIApplication.Model.empty var async = Timer.Model.empty } - struct Drivers: CycleDrivable { - let screen = ScreenDriver() - let timer = Timer(.empty) - let application = RxUIApplication(initial: .empty) + struct Drivers: UIApplicationDelegateProviding, ScreenDrivable { + let screen: ScreenDriver + let timer: Timer + let application: RxUIApplication + } + func driversFrom(initial: ShortcutActionsExample.Model) -> ShortcutActionsExample.Drivers { + return Drivers( + screen: ScreenDriver(), + timer: Timer(initial.async), + application: RxUIApplication(initial: initial.application) + ) } func effectsFrom(events: Observable, drivers: Drivers) -> Observable { diff --git a/Example/Cycle/URLActionOutgoing/URLActionOutgoing.swift b/Example/Cycle/URLActionOutgoing/URLActionOutgoing.swift index 184d28b..23b849a 100644 --- a/Example/Cycle/URLActionOutgoing/URLActionOutgoing.swift +++ b/Example/Cycle/URLActionOutgoing/URLActionOutgoing.swift @@ -27,9 +27,15 @@ struct URLActionOutgoing: SinkSourceConverting { struct Model: Initializable { var application = RxUIApplication.Model.empty } - struct Drivers: CycleDrivable { - let screen = ScreenDriver() - let application = RxUIApplication(initial: .empty) + struct Drivers: UIApplicationDelegateProviding, ScreenDrivable { + let screen: ScreenDriver + let application: RxUIApplication + } + func driversFrom(initial: URLActionOutgoing.Model) -> URLActionOutgoing.Drivers { + return Drivers( + screen: ScreenDriver(), + application: RxUIApplication(initial: initial.application) + ) } func effectsFrom(events: Observable, drivers: Drivers) -> Observable { return drivers.application diff --git a/Example/Podfile.lock b/Example/Podfile.lock index b3cc570..336e03a 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,6 +1,6 @@ PODS: - Changeset (2.1) - - Cycle (0.0.2): + - Cycle (0.0.4): - Changeset (= 2.1) - RxSwift (= 3.2.0) - RxCocoa (3.2.0): @@ -17,7 +17,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Changeset: 52ae0451caae6d236d8ecee150ab62050c3dc826 - Cycle: 8bb6fe112af27e9ed78eecabf0c739bb0ea8d6eb + Cycle: 10dab4653adb1968be054c46f660521b0ca56f94 RxCocoa: ccdf43101a70407097a29082f648ba1676075b30 RxSwift: 46574f70d416b7923c237195939cc488a7fbf3a0 diff --git a/Example/Tests/RxUIApplicationTestCase.swift b/Example/Tests/RxUIApplicationTestCase.swift index 5102c62..4765eb3 100644 --- a/Example/Tests/RxUIApplicationTestCase.swift +++ b/Example/Tests/RxUIApplicationTestCase.swift @@ -1503,17 +1503,23 @@ class RxUIApplicationTestCase: XCTestCase { } class RxUIApplicationCycle: SinkSourceConverting { - struct Drivers: CycleDrivable { - let screen = ScreenDriverStub() - let application = RxUIApplication(initial: .empty) - } struct DriverModels: Initializable { var application = RxUIApplication.Model.empty } + struct Drivers: UIApplicationDelegateProviding, ScreenDrivable { + let screen: ScreenDriverStub + let application: RxUIApplication + } let filter: (Observable, RxUIApplication) -> Observable init(filter: @escaping (Observable, RxUIApplication) -> Observable) { self.filter = filter } + func driversFrom(initial: RxUIApplicationTestCase.RxUIApplicationCycle.DriverModels) -> RxUIApplicationTestCase.RxUIApplicationCycle.Drivers { + return Drivers( + screen: ScreenDriverStub(), + application: RxUIApplication(initial: initial.application) + ) + } func effectsFrom(events: Observable, drivers: Drivers) -> Observable { return filter(events, drivers.application) } diff --git a/README.md b/README.md index b7c0467..fb70d3e 100644 --- a/README.md +++ b/README.md @@ -25,20 +25,24 @@ For example: ```swift public protocol SinkSourceConverting { /* - Defines schema and initial values of Driver Models + Defines schema and initial values of Driver Models. */ associatedtype Source: Initializable /* - Defines schema and initial values of Drivers. (Ideally, initial values would come from Source definition above but is currently not implemented as such.) + Defines drivers that handle effects, produce events. Requires two default drivers: - Also requires two default drivers: - 1. let application: UIApplicationProviding - can serve as UIApplicationDelegate + 1. let application: UIApplicationDelegateProviding - can serve as UIApplicationDelegate 2. let screen: ScreenDrivable - can provide a root UIViewController - A default UIApplicationProviding driver, RxUIApplication, is included. + A default UIApplicationDelegateProviding driver, RxUIApplication, is included with Cycle. */ - associatedtype Drivers: CycleDrivable + associatedtype Drivers: UIApplicationDelegateProviding, ScreenDrivable + + /* + Instantiates drivers with initial model. Necessary to for drivers that require initial values. + */ + func driversFrom(initial: Source) -> Drivers /* Returns an effect stream of Driver Model, given an event stream of Driver Model. See example for intended implementation. @@ -56,44 +60,51 @@ public protocol SinkSourceConverting { super.init(handler: MyFilter()) } } - + struct MyFilter: SinkSourceConverting { - - // Serves as schema and initial state. + struct AppModel: Initializable { let network = Network.Model() let screen = Screen.Model() let application = RxUIApplication.Model() } - struct Drivers: CycleDrivable { - let network = Network() - let screen = Screen() // Anything that provides a 'root' UIViewController - let application = RxUIApplication(initial: .empty) // Anything that conforms to UIApplicationDelegate + struct Drivers: UIApplicationDelegateProviding, ScreenDrivable { + let network: Network + let screen: Screen // Anything that provides a 'root' UIViewController + let application: RxUIApplication // Anything that conforms to UIApplicationDelegate } - + + func driversFrom(initial: AppModel) -> Drivers { return + Drivers( + network = Network(model: intitial.network), + screen = Screen(model: intitial.screen), + application = RxUIApplication(model: initial.application) + ) + } + func effectFrom(events: Observable, drivers: Drivers) -> Observable { - + let network = drivers.network .rendered(events.map { $0.network }) .withLatestFrom(events) { ($0.0, $0.1) } .reducingFuctionOfYourChoice() - + let screen = drivers.screen .rendered(events.map { $0.screen }) .withLatestFrom(events) { ($0.0, $0.1) } .reduced() - + let application = drivers.application .rendered(events.map { $0.application }) .withLatestFrom(events) { ($0.0, $0.1) } .reduced() - + return Observable .of(network, screen, application) .merge() } - + } ``` @@ -104,42 +115,42 @@ public protocol SinkSourceConverting { map { event, context in var new = context switch event.state { - case .idle: - new.screen.button.color = .blue - case .awaitingStart, .awaitingResponse: - new.screen.button.color = .grey - default: - break + case .idle: + new.screen.button.color = .blue + case .awaitingStart, .awaitingResponse: + new.screen.button.color = .grey + default: + break } return new } } } - + extension ObservableType where E == (Screen.Model, AppModel) { func reduced() -> Observable { return map { event, context in var new = context switch event.button.state { - case .highlighted: - new.network.state = .awaitingStart - default: - break + case .highlighted: + new.network.state = .awaitingStart + default: + break } return new } } } - + extension ObservableType where E == (RxUIApplication.Model, AppModel) { func reduced() -> Observable { return map { event, context in var new = context switch event.session.state { - case .launching: - new.screen = Screen.Model.downloadView - default: - break + case .launching: + new.screen = Screen.Model.downloadView + default: + break } return new } @@ -150,7 +161,7 @@ public protocol SinkSourceConverting { 3. Define drivers that, given a stream of event-models, can produce streams of effect-models ```swift class MyDriver { - + struct Model { var state: State enum State { @@ -158,30 +169,36 @@ public protocol SinkSourceConverting { case receiving } } - + fileprivate let output: BehaviorSubject fileprivate let model: Model - + public init(initial: Model) { model = initial output = BehaviorSubject(value: initial) } - - public func rendered(_ input: Observable) -> Observable { + + public func rendered(_ input: Observable) -> Observable { input.subscribe { [weak self] in if let strong = self, let new = $0.element { strong.model = new // Retain for async callback (-didReceiveEvent) strong.render(model: new) } - }.disposed(by: cleanup) + }.disposed(by: cleanup) return self.output } - - func render(model: Model) { + + func render(model: Model) { if case .sending = model.state { // Perform side-effects... } } + + func didReceiveEvent() { + var edit = model + edit.state = .receiving + output.on(.next(edit)) + } func didReceiveEvent() { var edit = model