Skip to content

Commit 2a29ae7

Browse files
committed
Split subscription logic.
1 parent e4d65d7 commit 2a29ae7

File tree

6 files changed

+78
-37
lines changed

6 files changed

+78
-37
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
5B8F922124732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922024732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift */; };
4848
5B8F922324732C9500C1C90E /* LoopBindingExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922224732C9500C1C90E /* LoopBindingExampleView.swift */; };
4949
5B8F922724732E1700C1C90E /* SimpleCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922624732E1700C1C90E /* SimpleCounterView.swift */; };
50+
5BAB9750247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */; };
51+
5BAB9751247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */; };
52+
5BAB9752247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */; };
5053
5BC88F842469CBA300394C63 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BC88F832469CBA300394C63 /* Nimble.framework */; };
5154
5BC88F862469CBAB00394C63 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BC88F852469CBAB00394C63 /* Nimble.framework */; };
5255
5BC88F88246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC88F87246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift */; };
@@ -228,6 +231,7 @@
228231
5B8F922224732C9500C1C90E /* LoopBindingExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopBindingExampleView.swift; sourceTree = "<group>"; };
229232
5B8F922424732CA000C1C90E /* EnvironmentLoopExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentLoopExampleView.swift; sourceTree = "<group>"; };
230233
5B8F922624732E1700C1C90E /* SimpleCounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleCounterView.swift; sourceTree = "<group>"; };
234+
5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHotSwappableSubscription.swift; sourceTree = "<group>"; };
231235
5BC88F832469CBA300394C63 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; };
232236
5BC88F852469CBAB00394C63 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; };
233237
5BC88F87246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReactiveSwift+EnqueueTo.swift"; sourceTree = "<group>"; };
@@ -468,6 +472,7 @@
468472
5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */,
469473
5B8F921B247325C300C1C90E /* EnvironmentValues.swift */,
470474
5B8F920F2473242900C1C90E /* SwiftUISubscription.swift */,
475+
5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */,
471476
);
472477
path = SwiftUI;
473478
sourceTree = "<group>";
@@ -815,6 +820,7 @@
815820
65761B2E23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */,
816821
5BC88F9C246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */,
817822
5BC88F92246B17B200394C63 /* Context.swift in Sources */,
823+
5BAB9750247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */,
818824
585CD87B239E6A39004BE9CC /* Reducer.swift in Sources */,
819825
9AD5D42D1F97375E00E6AE5A /* Property+System.swift in Sources */,
820826
5BC88F88246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */,
@@ -869,6 +875,7 @@
869875
65761B2F23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */,
870876
5BC88F9D246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */,
871877
5BC88F93246B17B200394C63 /* Context.swift in Sources */,
878+
5BAB9751247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */,
872879
585CD87C239E6A3E004BE9CC /* Reducer.swift in Sources */,
873880
65F8C262218371A800924657 /* Property+System.swift in Sources */,
874881
5BC88F89246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */,
@@ -893,6 +900,7 @@
893900
65761B3023CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */,
894901
5BC88F9E246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */,
895902
5BC88F94246B17B200394C63 /* Context.swift in Sources */,
903+
5BAB9752247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */,
896904
585CD87D239E6A3E004BE9CC /* Reducer.swift in Sources */,
897905
65F8C271218371AC00924657 /* Property+System.swift in Sources */,
898906
5BC88F8A246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */,

Loop/Public/SwiftUI/EnvironmentLoop.swift

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import ReactiveSwift
77
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
88
@propertyWrapper
99
public struct EnvironmentLoop<State, Event>: DynamicProperty {
10-
@Environment(\.loops[ObjectIdentifier(Loop<State, Event>.self)])
11-
var erasedLoop: Any?
10+
@Environment(\.loops[LoopType(Loop<State, Event>.self)])
11+
var erasedLoop: AnyObject?
1212

1313
@ObservedObject
14-
private var subscription: SwiftUISubscription<State, Event>
14+
private var subscription: SwiftUIHotSwappableSubscription<State, Event>
1515

1616
@inlinable
1717
public var wrappedValue: State {
@@ -30,23 +30,15 @@ public struct EnvironmentLoop<State, Event>: DynamicProperty {
3030
internal var acknowledgedState: State!
3131

3232
public init() {
33-
self.subscription = SwiftUISubscription()
33+
self.subscription = SwiftUIHotSwappableSubscription()
3434
}
3535

3636
public mutating func update() {
37-
if isKnownUniquelyReferenced(&subscription) == false {
38-
subscription = SwiftUISubscription()
39-
}
40-
41-
if subscription.hasStarted == false {
42-
guard let loop = erasedLoop as! Loop<State, Event>? else {
43-
fatalError("Expect parent view to inject a `Loop<\(State.self), \(Event.self)>` through `View.environmentLoop(_:)`. Found none.")
44-
}
45-
46-
subscription.attach(to: loop)
37+
guard let loop = erasedLoop as! Loop<State, Event>? else {
38+
fatalError("Expect parent view to inject a `Loop<\(State.self), \(Event.self)>` through `View.environmentLoop(_:)`. Found none.")
4739
}
4840

49-
acknowledgedState = subscription.latestValue
41+
acknowledgedState = subscription.currentState(in: loop)
5042
}
5143
}
5244

Loop/Public/SwiftUI/EnvironmentValues.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import SwiftUI
66
extension View {
77
@inlinable
88
public func environmentLoop<State, Event>(_ loop: Loop<State, Event>) -> some View {
9-
let typeId = ObjectIdentifier(type(of: loop))
9+
let typeId = LoopType(type(of: loop))
1010

1111
return transformEnvironment(\.loops) { loops in
1212
loops[typeId] = loop
@@ -16,17 +16,28 @@ extension View {
1616

1717
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
1818
extension EnvironmentValues {
19-
public var loops: [ObjectIdentifier: Any] {
19+
@usableFromInline
20+
internal var loops: [LoopType: AnyObject] {
2021
get { self[LoopEnvironmentKey.self] }
2122
set { self[LoopEnvironmentKey.self] = newValue }
2223
}
2324
}
2425

2526
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
2627
internal enum LoopEnvironmentKey: EnvironmentKey {
27-
static var defaultValue: [ObjectIdentifier: Any] {
28+
static var defaultValue: [LoopType: AnyObject] {
2829
return [:]
2930
}
3031
}
3132

33+
@usableFromInline
34+
struct LoopType: Hashable {
35+
let id: ObjectIdentifier
36+
37+
@usableFromInline
38+
init(_ type: Any.Type) {
39+
id = ObjectIdentifier(type)
40+
}
41+
}
42+
3243
#endif

Loop/Public/SwiftUI/LoopBinding.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,19 @@ public struct LoopBinding<State, Event>: DynamicProperty {
2222
}
2323

2424
@usableFromInline
25-
internal var acknowledgedState: State!
25+
internal var acknowledgedState: State
2626

2727
public init(_ loop: Loop<State, Event>) {
2828
// The subscription can be copied without restrictions.
29-
self.subscription = SwiftUISubscription()
29+
let subscription = SwiftUISubscription(loop: loop)
30+
31+
self.subscription = subscription
32+
self.acknowledgedState = subscription.latestValue
3033
self.loop = loop
3134
}
3235

3336
public mutating func update() {
34-
if subscription.hasStarted == false {
35-
subscription.attach(to: loop)
36-
}
37-
37+
// Move latest value from the subscription only when SwiftUI has requested an update.
3838
acknowledgedState = subscription.latestValue
3939
}
4040

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#if canImport(Combine)
2+
3+
import Combine
4+
import ReactiveSwift
5+
6+
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
7+
internal final class SwiftUIHotSwappableSubscription<State, Event>: ObservableObject {
8+
9+
@Published private var latestValue: State!
10+
private weak var attachedLoop: Loop<State, Event>?
11+
private var disposable: Disposable?
12+
13+
init() {}
14+
15+
deinit {
16+
disposable?.dispose()
17+
}
18+
19+
func currentState(in loop: Loop<State, Event>) -> State {
20+
if attachedLoop !== loop {
21+
disposable?.dispose()
22+
23+
latestValue = loop.box._current
24+
25+
disposable = loop.producer
26+
.observe(on: UIScheduler())
27+
.startWithValues { [weak self] state in
28+
guard let self = self else { return }
29+
self.latestValue = state
30+
}
31+
}
32+
33+
return latestValue
34+
}
35+
}
36+
37+
#endif
Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
1-
import SwiftUI
21
#if canImport(Combine)
32

43
import Combine
54
import ReactiveSwift
65

76
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
87
internal final class SwiftUISubscription<State, Event>: ObservableObject {
9-
@Published var latestValue: State!
10-
private(set) var hasStarted = false
118

9+
@Published var latestValue: State
1210
private var disposable: Disposable?
1311

14-
init() {}
15-
16-
deinit {
17-
disposable?.dispose()
18-
}
19-
20-
func attach(to loop: Loop<State, Event>) {
21-
guard hasStarted == false else { return }
22-
hasStarted = true
23-
12+
init(loop: Loop<State, Event>) {
2413
latestValue = loop.box._current
2514
disposable = loop.producer
2615
.observe(on: UIScheduler())
@@ -29,6 +18,10 @@ internal final class SwiftUISubscription<State, Event>: ObservableObject {
2918
self.latestValue = state
3019
}
3120
}
21+
22+
deinit {
23+
disposable?.dispose()
24+
}
3225
}
3326

3427
#endif

0 commit comments

Comments
 (0)