-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from ReactiveCocoa/anders/swiftui
@LoopBinding and @EnvironmentLoop for SwiftUI.
- Loading branch information
Showing
14 changed files
with
413 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
Example/SwiftUIBasicBindingExample/EnvironmentLoopExampleView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import SwiftUI | ||
import Loop | ||
|
||
struct EnvironmentLoopExampleView: View { | ||
let loop: Loop<Int, Int> | ||
|
||
init(loop: Loop<Int, Int>) { | ||
self.loop = loop | ||
} | ||
|
||
var body: some View { | ||
EnvironmentLoopContentView() | ||
.environmentLoop(self.loop) | ||
.navigationBarTitle("@EnvironmentLoop") | ||
} | ||
} | ||
|
||
private struct EnvironmentLoopContentView: View { | ||
@EnvironmentLoop<Int, Int> var state: Int | ||
|
||
var body: some View { | ||
SimpleCounterView(binding: $state) | ||
} | ||
} | ||
|
||
struct EnvironmentLoopExampleView_Previews: PreviewProvider { | ||
static var previews: some View { | ||
NavigationView { | ||
EnvironmentLoopExampleView(loop: simpleCounterStore) | ||
} | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
Example/SwiftUIBasicBindingExample/LoopBindingExampleView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import SwiftUI | ||
import Loop | ||
|
||
struct LoopBindingExampleView: View { | ||
@LoopBinding<Int, Int> var state: Int | ||
|
||
init(state: LoopBinding<Int, Int>) { | ||
_state = state | ||
} | ||
|
||
var body: some View { | ||
SimpleCounterView(binding: $state) | ||
.navigationBarTitle("@LoopBinding") | ||
} | ||
} | ||
|
||
struct LoopBindingExampleView_Previews: PreviewProvider { | ||
static var previews: some View { | ||
NavigationView { | ||
LoopBindingExampleView(state: simpleCounterStore.binding) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import Loop | ||
|
||
let simpleCounterStore = Loop(initial: 0, reducer: { state, event in state += event }, feedbacks: []) |
35 changes: 35 additions & 0 deletions
35
Example/SwiftUIBasicBindingExample/SimpleCounterView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import SwiftUI | ||
import Loop | ||
|
||
struct SimpleCounterView: View { | ||
@LoopBinding<Int, Int> var state: Int | ||
|
||
init(binding: LoopBinding<Int, Int>) { | ||
_state = binding | ||
} | ||
|
||
var body: some View { | ||
VStack { | ||
Spacer() | ||
.layoutPriority(1.0) | ||
|
||
Button( | ||
action: { self.$state.send(-1) }, | ||
label: { Image(systemName: "minus.circle") } | ||
) | ||
.padding() | ||
|
||
Text("\(self.state)") | ||
.font(.system(.largeTitle, design: .monospaced)) | ||
|
||
Button( | ||
action: { self.$state.send(1) }, | ||
label: { Image(systemName: "plus.circle") } | ||
) | ||
.padding() | ||
|
||
Spacer() | ||
.layoutPriority(1.0) | ||
} | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
Example/SwiftUIBasicBindingExample/SwiftUIBasicBindingHomeView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import SwiftUI | ||
import Loop | ||
|
||
struct SwiftUIBasicBindingHomeView: View { | ||
var body: some View { | ||
ScrollView { | ||
CardNavigationLink(label: "@LoopBinding", color: .orange) { | ||
LoopBindingExampleView(state: simpleCounterStore.binding) | ||
} | ||
|
||
CardNavigationLink(label: "@EnvironmentLoop", color: .orange) { | ||
EnvironmentLoopExampleView(loop: simpleCounterStore) | ||
} | ||
} | ||
} | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
#if canImport(SwiftUI) && canImport(Combine) | ||
|
||
import SwiftUI | ||
import Combine | ||
import ReactiveSwift | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
@propertyWrapper | ||
public struct EnvironmentLoop<State, Event>: DynamicProperty { | ||
@Environment(\.loops[LoopType(Loop<State, Event>.self)]) | ||
var erasedLoop: AnyObject? | ||
|
||
@ObservedObject | ||
private var subscription: SwiftUIHotSwappableSubscription<State, Event> | ||
|
||
@inlinable | ||
public var wrappedValue: State { | ||
acknowledgedState | ||
} | ||
|
||
public var projectedValue: LoopBinding<State, Event> { | ||
guard let loop = erasedLoop as! Loop<State, Event>? else { | ||
fatalError("Scoped bindings can only be created inside the view body.") | ||
} | ||
|
||
return LoopBinding(loop) | ||
} | ||
|
||
@usableFromInline | ||
internal var acknowledgedState: State! | ||
|
||
public init() { | ||
self.subscription = SwiftUIHotSwappableSubscription() | ||
} | ||
|
||
public mutating func update() { | ||
guard let loop = erasedLoop as! Loop<State, Event>? else { | ||
fatalError("Expect parent view to inject a `Loop<\(State.self), \(Event.self)>` through `View.environmentLoop(_:)`. Found none.") | ||
} | ||
|
||
acknowledgedState = subscription.currentState(in: loop) | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
#if canImport(SwiftUI) | ||
|
||
import SwiftUI | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
extension View { | ||
@inlinable | ||
public func environmentLoop<State, Event>(_ loop: Loop<State, Event>) -> some View { | ||
let typeId = LoopType(type(of: loop)) | ||
|
||
return transformEnvironment(\.loops) { loops in | ||
loops[typeId] = loop | ||
} | ||
} | ||
} | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
extension EnvironmentValues { | ||
@usableFromInline | ||
internal var loops: [LoopType: AnyObject] { | ||
get { self[LoopEnvironmentKey.self] } | ||
set { self[LoopEnvironmentKey.self] = newValue } | ||
} | ||
} | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
internal enum LoopEnvironmentKey: EnvironmentKey { | ||
static var defaultValue: [LoopType: AnyObject] { | ||
return [:] | ||
} | ||
} | ||
|
||
@usableFromInline | ||
struct LoopType: Hashable { | ||
let id: ObjectIdentifier | ||
|
||
@usableFromInline | ||
init(_ type: Any.Type) { | ||
id = ObjectIdentifier(type) | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
#if canImport(SwiftUI) && canImport(Combine) | ||
|
||
import SwiftUI | ||
import Combine | ||
import ReactiveSwift | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
@propertyWrapper | ||
public struct LoopBinding<State, Event>: DynamicProperty { | ||
@ObservedObject | ||
private var subscription: SwiftUISubscription<State, Event> | ||
|
||
private let loop: Loop<State, Event> | ||
|
||
@inlinable | ||
public var wrappedValue: State { | ||
acknowledgedState | ||
} | ||
|
||
public var projectedValue: LoopBinding<State, Event> { | ||
self | ||
} | ||
|
||
@usableFromInline | ||
internal var acknowledgedState: State | ||
|
||
public init(_ loop: Loop<State, Event>) { | ||
// The subscription can be copied without restrictions. | ||
let subscription = SwiftUISubscription(loop: loop) | ||
|
||
self.subscription = subscription | ||
self.acknowledgedState = subscription.latestValue | ||
self.loop = loop | ||
} | ||
|
||
public mutating func update() { | ||
// Move latest value from the subscription only when SwiftUI has requested an update. | ||
acknowledgedState = subscription.latestValue | ||
} | ||
|
||
public func scoped<ScopedState, ScopedEvent>( | ||
to value: KeyPath<State, ScopedState>, | ||
event: @escaping (ScopedEvent) -> Event | ||
) -> LoopBinding<ScopedState, ScopedEvent> { | ||
LoopBinding<ScopedState, ScopedEvent>(loop.scoped(to: value, event: event)) | ||
} | ||
|
||
public func send(_ event: Event) { | ||
loop.send(event) | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
#if canImport(Combine) | ||
|
||
import Combine | ||
import ReactiveSwift | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
internal final class SwiftUIHotSwappableSubscription<State, Event>: ObservableObject { | ||
|
||
@Published private var latestValue: State! | ||
private weak var attachedLoop: Loop<State, Event>? | ||
private var disposable: Disposable? | ||
|
||
init() {} | ||
|
||
deinit { | ||
disposable?.dispose() | ||
} | ||
|
||
func currentState(in loop: Loop<State, Event>) -> State { | ||
if attachedLoop !== loop { | ||
disposable?.dispose() | ||
|
||
latestValue = loop.box._current | ||
|
||
disposable = loop.producer | ||
.observe(on: UIScheduler()) | ||
.startWithValues { [weak self] state in | ||
guard let self = self else { return } | ||
self.latestValue = state | ||
} | ||
} | ||
|
||
return latestValue | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
#if canImport(Combine) | ||
|
||
import Combine | ||
import ReactiveSwift | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
internal final class SwiftUISubscription<State, Event>: ObservableObject { | ||
|
||
@Published var latestValue: State | ||
private var disposable: Disposable? | ||
|
||
init(loop: Loop<State, Event>) { | ||
latestValue = loop.box._current | ||
disposable = loop.producer | ||
.observe(on: UIScheduler()) | ||
.startWithValues { [weak self] state in | ||
guard let self = self else { return } | ||
self.latestValue = state | ||
} | ||
} | ||
|
||
deinit { | ||
disposable?.dispose() | ||
} | ||
} | ||
|
||
#endif |