-
Notifications
You must be signed in to change notification settings - Fork 6
@LoopBinding and @EnvironmentLoop for SwiftUI. #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} | ||
} | ||
} |
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) | ||
} | ||
} | ||
} |
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: []) |
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) | ||
} | ||
} | ||
} |
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.
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 |
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 |
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) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we extend this to also have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is an additive functionality that needs more thoughts, esp conversion/relationship of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well the common use case I guess is showing the |
||
} | ||
|
||
#endif |
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 |
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 |
Uh oh!
There was an error while loading. Please reload this page.