From 0e845ad1e3f876db419649d534de79b392dfc6bf Mon Sep 17 00:00:00 2001 From: Anders Ha Date: Mon, 18 May 2020 22:17:29 +0100 Subject: [PATCH 1/4] @LoopBinding and @EnvironmentLoop for SwiftUI. --- Example/Root/RootView.swift | 4 ++ .../EnvironmentLoopExampleView.swift | 32 +++++++++ .../LoopBindingExampleView.swift | 23 +++++++ .../SimpleCounterStore.swift | 3 + .../SimpleCounterView.swift | 35 ++++++++++ .../SwiftUIBasicBindingHomeView.swift | 16 +++++ Loop.xcodeproj/project.pbxproj | 68 +++++++++++++++++++ Loop/LoopBox.swift | 13 ++++ Loop/Public/Loop.swift | 7 +- Loop/Public/SwiftUI/EnvironmentLoop.swift | 49 +++++++++++++ Loop/Public/SwiftUI/EnvironmentValues.swift | 28 ++++++++ Loop/Public/SwiftUI/LoopBinding.swift | 49 +++++++++++++ Loop/Public/SwiftUI/SwiftUISubscription.swift | 32 +++++++++ 13 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 Example/SwiftUIBasicBindingExample/EnvironmentLoopExampleView.swift create mode 100644 Example/SwiftUIBasicBindingExample/LoopBindingExampleView.swift create mode 100644 Example/SwiftUIBasicBindingExample/SimpleCounterStore.swift create mode 100644 Example/SwiftUIBasicBindingExample/SimpleCounterView.swift create mode 100644 Example/SwiftUIBasicBindingExample/SwiftUIBasicBindingHomeView.swift create mode 100644 Loop/Public/SwiftUI/EnvironmentLoop.swift create mode 100644 Loop/Public/SwiftUI/EnvironmentValues.swift create mode 100644 Loop/Public/SwiftUI/LoopBinding.swift create mode 100644 Loop/Public/SwiftUI/SwiftUISubscription.swift diff --git a/Example/Root/RootView.swift b/Example/Root/RootView.swift index a9c0061..75c1586 100644 --- a/Example/Root/RootView.swift +++ b/Example/Root/RootView.swift @@ -20,6 +20,10 @@ struct RootView: View { CardNavigationLink(label: "Unified Store + UIKit", color: .blue) { UnifiedStoreUIKitHomeView() } + + CardNavigationLink(label: "SwiftUI: Basic Binding", color: .orange) { + SwiftUIBasicBindingHomeView() + } } .navigationBarTitle("Loop Examples") .navigationBarHidden(true) diff --git a/Example/SwiftUIBasicBindingExample/EnvironmentLoopExampleView.swift b/Example/SwiftUIBasicBindingExample/EnvironmentLoopExampleView.swift new file mode 100644 index 0000000..d913f6f --- /dev/null +++ b/Example/SwiftUIBasicBindingExample/EnvironmentLoopExampleView.swift @@ -0,0 +1,32 @@ +import SwiftUI +import Loop + +struct EnvironmentLoopExampleView: View { + let loop: Loop + + init(loop: Loop) { + self.loop = loop + } + + var body: some View { + EnvironmentLoopContentView() + .environmentLoop(self.loop) + .navigationBarTitle("@EnvironmentLoop") + } +} + +private struct EnvironmentLoopContentView: View { + @EnvironmentLoop var state: Int + + var body: some View { + SimpleCounterView(binding: $state) + } +} + +struct EnvironmentLoopExampleView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + EnvironmentLoopExampleView(loop: simpleCounterStore) + } + } +} diff --git a/Example/SwiftUIBasicBindingExample/LoopBindingExampleView.swift b/Example/SwiftUIBasicBindingExample/LoopBindingExampleView.swift new file mode 100644 index 0000000..cd77c5c --- /dev/null +++ b/Example/SwiftUIBasicBindingExample/LoopBindingExampleView.swift @@ -0,0 +1,23 @@ +import SwiftUI +import Loop + +struct LoopBindingExampleView: View { + @LoopBinding var state: Int + + init(state: LoopBinding) { + _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) + } + } +} diff --git a/Example/SwiftUIBasicBindingExample/SimpleCounterStore.swift b/Example/SwiftUIBasicBindingExample/SimpleCounterStore.swift new file mode 100644 index 0000000..0be0694 --- /dev/null +++ b/Example/SwiftUIBasicBindingExample/SimpleCounterStore.swift @@ -0,0 +1,3 @@ +import Loop + +let simpleCounterStore = Loop(initial: 0, reducer: { state, event in state += event }, feedbacks: []) diff --git a/Example/SwiftUIBasicBindingExample/SimpleCounterView.swift b/Example/SwiftUIBasicBindingExample/SimpleCounterView.swift new file mode 100644 index 0000000..2144bef --- /dev/null +++ b/Example/SwiftUIBasicBindingExample/SimpleCounterView.swift @@ -0,0 +1,35 @@ +import SwiftUI +import Loop + +struct SimpleCounterView: View { + @LoopBinding var state: Int + + init(binding: LoopBinding) { + _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) + } + } +} diff --git a/Example/SwiftUIBasicBindingExample/SwiftUIBasicBindingHomeView.swift b/Example/SwiftUIBasicBindingExample/SwiftUIBasicBindingHomeView.swift new file mode 100644 index 0000000..3f5d875 --- /dev/null +++ b/Example/SwiftUIBasicBindingExample/SwiftUIBasicBindingHomeView.swift @@ -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) + } + } + } +} diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 21e320b..f76d751 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -38,6 +38,15 @@ 5B017D2C246F33DE00400BFE /* ArrayCollectionViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B017D2B246F33DE00400BFE /* ArrayCollectionViewAdapter.swift */; }; 5B017D30246F344500400BFE /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B017D2F246F344500400BFE /* CardView.swift */; }; 5B017D32246F466600400BFE /* RACHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B017D31246F466600400BFE /* RACHeaderView.swift */; }; + 5B8F920C24731F2800C1C90E /* LoopBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F920B24731F2800C1C90E /* LoopBinding.swift */; }; + 5B8F920D24731F2800C1C90E /* LoopBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F920B24731F2800C1C90E /* LoopBinding.swift */; }; + 5B8F920E24731F2800C1C90E /* LoopBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F920B24731F2800C1C90E /* LoopBinding.swift */; }; + 5B8F92102473242900C1C90E /* SwiftUISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F920F2473242900C1C90E /* SwiftUISubscription.swift */; }; + 5B8F92112473242900C1C90E /* SwiftUISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F920F2473242900C1C90E /* SwiftUISubscription.swift */; }; + 5B8F92122473242900C1C90E /* SwiftUISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F920F2473242900C1C90E /* SwiftUISubscription.swift */; }; + 5B8F922124732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922024732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift */; }; + 5B8F922324732C9500C1C90E /* LoopBindingExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922224732C9500C1C90E /* LoopBindingExampleView.swift */; }; + 5B8F922724732E1700C1C90E /* SimpleCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922624732E1700C1C90E /* SimpleCounterView.swift */; }; 5BC88F842469CBA300394C63 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BC88F832469CBA300394C63 /* Nimble.framework */; }; 5BC88F862469CBAB00394C63 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BC88F852469CBAB00394C63 /* Nimble.framework */; }; 5BC88F88246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC88F87246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift */; }; @@ -55,6 +64,14 @@ 5BC88F9C246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC88F9B246B1CDE00394C63 /* SignalProducer+Loop.swift */; }; 5BC88F9D246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC88F9B246B1CDE00394C63 /* SignalProducer+Loop.swift */; }; 5BC88F9E246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC88F9B246B1CDE00394C63 /* SignalProducer+Loop.swift */; }; + 5BDEDA3B2473357A00A13013 /* EnvironmentLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */; }; + 5BDEDA3C2473357A00A13013 /* EnvironmentLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */; }; + 5BDEDA3D2473357B00A13013 /* EnvironmentLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */; }; + 5BDEDA3E2473359C00A13013 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F921B247325C300C1C90E /* EnvironmentValues.swift */; }; + 5BDEDA3F2473359D00A13013 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F921B247325C300C1C90E /* EnvironmentValues.swift */; }; + 5BDEDA402473359D00A13013 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F921B247325C300C1C90E /* EnvironmentValues.swift */; }; + 5BDEDA41247335C700A13013 /* EnvironmentLoopExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922424732CA000C1C90E /* EnvironmentLoopExampleView.swift */; }; + 5BDEDA432473377A00A13013 /* SimpleCounterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDEDA422473377A00A13013 /* SimpleCounterStore.swift */; }; 656A9C9323D0813500EFB2F8 /* FeedbackLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */; }; 656A9C9423D0813500EFB2F8 /* FeedbackLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */; }; 656A9C9523D0813500EFB2F8 /* FeedbackLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */; }; @@ -203,6 +220,14 @@ 5B017D2B246F33DE00400BFE /* ArrayCollectionViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayCollectionViewAdapter.swift; sourceTree = ""; }; 5B017D2F246F344500400BFE /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; 5B017D31246F466600400BFE /* RACHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RACHeaderView.swift; sourceTree = ""; }; + 5B8F920B24731F2800C1C90E /* LoopBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopBinding.swift; sourceTree = ""; }; + 5B8F920F2473242900C1C90E /* SwiftUISubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUISubscription.swift; sourceTree = ""; }; + 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentLoop.swift; sourceTree = ""; }; + 5B8F921B247325C300C1C90E /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; + 5B8F922024732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIBasicBindingHomeView.swift; sourceTree = ""; }; + 5B8F922224732C9500C1C90E /* LoopBindingExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopBindingExampleView.swift; sourceTree = ""; }; + 5B8F922424732CA000C1C90E /* EnvironmentLoopExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentLoopExampleView.swift; sourceTree = ""; }; + 5B8F922624732E1700C1C90E /* SimpleCounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleCounterView.swift; sourceTree = ""; }; 5BC88F832469CBA300394C63 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5BC88F852469CBAB00394C63 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5BC88F87246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReactiveSwift+EnqueueTo.swift"; sourceTree = ""; }; @@ -210,6 +235,7 @@ 5BC88F91246B17B200394C63 /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = ""; }; 5BC88F97246B191200394C63 /* Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loop.swift; sourceTree = ""; }; 5BC88F9B246B1CDE00394C63 /* SignalProducer+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SignalProducer+Loop.swift"; sourceTree = ""; }; + 5BDEDA422473377A00A13013 /* SimpleCounterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleCounterStore.swift; sourceTree = ""; }; 656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackLoop.swift; sourceTree = ""; }; 656A9C9623D0826100EFB2F8 /* FeedbackEventConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackEventConsumer.swift; sourceTree = ""; }; 65761B2523CF20EF004D5506 /* Floodgate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Floodgate.swift; sourceTree = ""; }; @@ -341,6 +367,7 @@ 25E1D21F1F5493D000D90192 /* Example */ = { isa = PBXGroup; children = ( + 5B8F921F24732C0300C1C90E /* SwiftUIBasicBindingExample */, 585CD886239E904E004BE9CC /* UnifiedStoreUIKitExample */, 5B017D2D246F340900400BFE /* Root */, 5B017D21246F2CBF00400BFE /* Misc */, @@ -434,9 +461,33 @@ path = SwiftUI; sourceTree = ""; }; + 5B8F920A24731F1800C1C90E /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 5B8F920B24731F2800C1C90E /* LoopBinding.swift */, + 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */, + 5B8F921B247325C300C1C90E /* EnvironmentValues.swift */, + 5B8F920F2473242900C1C90E /* SwiftUISubscription.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 5B8F921F24732C0300C1C90E /* SwiftUIBasicBindingExample */ = { + isa = PBXGroup; + children = ( + 5B8F922024732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift */, + 5B8F922224732C9500C1C90E /* LoopBindingExampleView.swift */, + 5B8F922424732CA000C1C90E /* EnvironmentLoopExampleView.swift */, + 5B8F922624732E1700C1C90E /* SimpleCounterView.swift */, + 5BDEDA422473377A00A13013 /* SimpleCounterStore.swift */, + ); + path = SwiftUIBasicBindingExample; + sourceTree = ""; + }; 5BC88F95246B188300394C63 /* Public */ = { isa = PBXGroup; children = ( + 5B8F920A24731F1800C1C90E /* SwiftUI */, 5BC88F97246B191200394C63 /* Loop.swift */, 5BC88F9B246B1CDE00394C63 /* SignalProducer+Loop.swift */, 656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */, @@ -759,6 +810,7 @@ buildActionMask = 2147483647; files = ( A9509BE4551098F4A5503820 /* Feedback.swift in Sources */, + 5B8F920C24731F2800C1C90E /* LoopBinding.swift in Sources */, A950943401765BB90FA846B2 /* SignalProducer+System.swift in Sources */, 65761B2E23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */, 5BC88F9C246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */, @@ -767,10 +819,13 @@ 9AD5D42D1F97375E00E6AE5A /* Property+System.swift in Sources */, 5BC88F88246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */, 656A9C9323D0813500EFB2F8 /* FeedbackLoop.swift in Sources */, + 5B8F92102473242900C1C90E /* SwiftUISubscription.swift in Sources */, + 5BDEDA3E2473359C00A13013 /* EnvironmentValues.swift in Sources */, 656A9C9723D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */, 5BC88F8E246B11DE00394C63 /* LoopBox.swift in Sources */, 65761B2623CF20EF004D5506 /* Floodgate.swift in Sources */, 5BC88F98246B191200394C63 /* Loop.swift in Sources */, + 5BDEDA3B2473357A00A13013 /* EnvironmentLoop.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -781,6 +836,9 @@ 5B017D27246F2EDB00400BFE /* UnifiedStoreUIKitHomeView.swift in Sources */, 5B017D29246F2F7E00400BFE /* ContentViewController.swift in Sources */, 585CD88A239E9077004BE9CC /* Counter.swift in Sources */, + 5BDEDA432473377A00A13013 /* SimpleCounterStore.swift in Sources */, + 5B8F922324732C9500C1C90E /* LoopBindingExampleView.swift in Sources */, + 5BDEDA41247335C700A13013 /* EnvironmentLoopExampleView.swift in Sources */, 5810F03A239EBEA100708F62 /* UnifiedStore.swift in Sources */, 5810F038239EB5A700708F62 /* MovieCell.swift in Sources */, 5B017D23246F2CD600400BFE /* RootView.swift in Sources */, @@ -793,6 +851,8 @@ 5810F03F239FAB0200708F62 /* ColorPickerView.swift in Sources */, 5810F03D239FA9F200708F62 /* ColorPicker.swift in Sources */, 25E1D2211F5493D000D90192 /* AppDelegate.swift in Sources */, + 5B8F922124732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift in Sources */, + 5B8F922724732E1700C1C90E /* SimpleCounterView.swift in Sources */, 585CD88C239E90EB004BE9CC /* CounterView.swift in Sources */, 5B017D30246F344500400BFE /* CardView.swift in Sources */, 5B017D2C246F33DE00400BFE /* ArrayCollectionViewAdapter.swift in Sources */, @@ -804,6 +864,7 @@ buildActionMask = 2147483647; files = ( 65F8C260218371A800924657 /* Feedback.swift in Sources */, + 5B8F920D24731F2800C1C90E /* LoopBinding.swift in Sources */, 65F8C261218371A800924657 /* SignalProducer+System.swift in Sources */, 65761B2F23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */, 5BC88F9D246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */, @@ -812,10 +873,13 @@ 65F8C262218371A800924657 /* Property+System.swift in Sources */, 5BC88F89246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */, 656A9C9423D0813500EFB2F8 /* FeedbackLoop.swift in Sources */, + 5B8F92112473242900C1C90E /* SwiftUISubscription.swift in Sources */, + 5BDEDA3F2473359D00A13013 /* EnvironmentValues.swift in Sources */, 656A9C9823D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */, 5BC88F8F246B11DE00394C63 /* LoopBox.swift in Sources */, 65761B2723CF20EF004D5506 /* Floodgate.swift in Sources */, 5BC88F99246B191200394C63 /* Loop.swift in Sources */, + 5BDEDA3C2473357A00A13013 /* EnvironmentLoop.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -824,6 +888,7 @@ buildActionMask = 2147483647; files = ( 65F8C26F218371AC00924657 /* Feedback.swift in Sources */, + 5B8F920E24731F2800C1C90E /* LoopBinding.swift in Sources */, 65F8C270218371AC00924657 /* SignalProducer+System.swift in Sources */, 65761B3023CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */, 5BC88F9E246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */, @@ -832,10 +897,13 @@ 65F8C271218371AC00924657 /* Property+System.swift in Sources */, 5BC88F8A246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */, 656A9C9523D0813500EFB2F8 /* FeedbackLoop.swift in Sources */, + 5B8F92122473242900C1C90E /* SwiftUISubscription.swift in Sources */, + 5BDEDA402473359D00A13013 /* EnvironmentValues.swift in Sources */, 656A9C9923D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */, 5BC88F90246B11DE00394C63 /* LoopBox.swift in Sources */, 65761B2823CF20EF004D5506 /* Floodgate.swift in Sources */, 5BC88F9A246B191200394C63 /* Loop.swift in Sources */, + 5BDEDA3D2473357B00A13013 /* EnvironmentLoop.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Loop/LoopBox.swift b/Loop/LoopBox.swift index 59e1e39..469cdcd 100644 --- a/Loop/LoopBox.swift +++ b/Loop/LoopBox.swift @@ -9,6 +9,11 @@ internal class ScopedLoopBox: Lo root.lifetime } + /// Loop Internal SPI + override var _current: ScopedState { + root._current[keyPath: value] + } + private let root: LoopBoxBase private let value: KeyPath private let eventTransform: (ScopedEvent) -> RootEvent @@ -44,6 +49,11 @@ internal class RootLoopBox: LoopBoxBase { _lifetime } + /// Loop Internal SPI + override var _current: State { + floodgate.withValue { state, _ in state } + } + let floodgate: Floodgate private let _lifetime: Lifetime private let token: Lifetime.Token @@ -89,6 +99,9 @@ internal class RootLoopBox: LoopBoxBase { } internal class LoopBoxBase { + /// Loop Internal SPI + var _current: State { subclassMustImplement() } + var lifetime: Lifetime { subclassMustImplement() } var producer: SignalProducer { subclassMustImplement() } diff --git a/Loop/Public/Loop.swift b/Loop/Public/Loop.swift index 7852227..d7a562b 100644 --- a/Loop/Public/Loop.swift +++ b/Loop/Public/Loop.swift @@ -19,7 +19,12 @@ public final class Loop { } } - private let box: LoopBoxBase + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public var binding: LoopBinding { + LoopBinding(self) + } + + internal let box: LoopBoxBase private init(box: LoopBoxBase) { self.box = box diff --git a/Loop/Public/SwiftUI/EnvironmentLoop.swift b/Loop/Public/SwiftUI/EnvironmentLoop.swift new file mode 100644 index 0000000..5c68a2b --- /dev/null +++ b/Loop/Public/SwiftUI/EnvironmentLoop.swift @@ -0,0 +1,49 @@ +import SwiftUI +import Combine +import ReactiveSwift + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +@propertyWrapper +public struct EnvironmentLoop: DynamicProperty { + @Environment(\.loops[ObjectIdentifier(Loop.self)]) + var erasedLoop: Any? + + @ObservedObject + private var subscription: SwiftUISubscription + + @inlinable + public var wrappedValue: State { + acknowledgedState + } + + public var projectedValue: LoopBinding { + guard let loop = erasedLoop as! Loop? else { + fatalError("Scoped bindings can only be created inside the view body.") + } + + return LoopBinding(loop) + } + + @usableFromInline + internal var acknowledgedState: State! + + public init() { + self.subscription = SwiftUISubscription() + } + + public mutating func update() { + if isKnownUniquelyReferenced(&subscription) == false { + subscription = SwiftUISubscription() + } + + if subscription.hasStarted == false { + guard let loop = erasedLoop as! Loop? else { + fatalError("Expect parent view to inject a `Loop<\(State.self), \(Event.self)>` through `View.environmentLoop(_:)`. Found none.") + } + + subscription.attach(to: loop) + } + + acknowledgedState = subscription.latestValue + } +} diff --git a/Loop/Public/SwiftUI/EnvironmentValues.swift b/Loop/Public/SwiftUI/EnvironmentValues.swift new file mode 100644 index 0000000..a8020c5 --- /dev/null +++ b/Loop/Public/SwiftUI/EnvironmentValues.swift @@ -0,0 +1,28 @@ +import SwiftUI + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension View { + @inlinable + public func environmentLoop(_ loop: Loop) -> some View { + let typeId = ObjectIdentifier(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 { + public var loops: [ObjectIdentifier: Any] { + 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: [ObjectIdentifier: Any] { + return [:] + } +} diff --git a/Loop/Public/SwiftUI/LoopBinding.swift b/Loop/Public/SwiftUI/LoopBinding.swift new file mode 100644 index 0000000..9de22cf --- /dev/null +++ b/Loop/Public/SwiftUI/LoopBinding.swift @@ -0,0 +1,49 @@ +import SwiftUI +import Combine +import ReactiveSwift + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +@propertyWrapper +public struct LoopBinding: DynamicProperty { + @ObservedObject + private var subscription: SwiftUISubscription + + private let loop: Loop + + @inlinable + public var wrappedValue: State { + acknowledgedState + } + + public var projectedValue: LoopBinding { + self + } + + @usableFromInline + internal var acknowledgedState: State! + + public init(_ loop: Loop) { + // The subscription can be copied without restrictions. + self.subscription = SwiftUISubscription() + self.loop = loop + } + + public mutating func update() { + if subscription.hasStarted == false { + subscription.attach(to: loop) + } + + acknowledgedState = subscription.latestValue + } + + public func scoped( + to value: KeyPath, + event: @escaping (ScopedEvent) -> Event + ) -> LoopBinding { + LoopBinding(loop.scoped(to: value, event: event)) + } + + public func send(_ event: Event) { + loop.send(event) + } +} diff --git a/Loop/Public/SwiftUI/SwiftUISubscription.swift b/Loop/Public/SwiftUI/SwiftUISubscription.swift new file mode 100644 index 0000000..cf7ffd3 --- /dev/null +++ b/Loop/Public/SwiftUI/SwiftUISubscription.swift @@ -0,0 +1,32 @@ +import SwiftUI +import Combine +import ReactiveSwift + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +internal final class SwiftUISubscription: ObservableObject { + let objectWillChange = ObservableObjectPublisher() + var latestValue: State! + private(set) var hasStarted = false + + private var disposable: Disposable? + + init() {} + + deinit { + disposable?.dispose() + } + + func attach(to loop: Loop) { + guard hasStarted == false else { return } + hasStarted = true + + latestValue = loop.box._current + disposable = loop.producer + .observe(on: UIScheduler()) + .startWithValues { [weak self] state in + guard let self = self else { return } + self.latestValue = state + self.objectWillChange.send() + } + } +} From b6beb6b28e779db024b60a67a7e015a41b7af72b Mon Sep 17 00:00:00 2001 From: Anders Ha Date: Thu, 28 May 2020 14:32:39 +0100 Subject: [PATCH 2/4] Conditional import for building with earlier SDKs. --- Loop/Public/SwiftUI/EnvironmentLoop.swift | 4 ++++ Loop/Public/SwiftUI/EnvironmentValues.swift | 4 ++++ Loop/Public/SwiftUI/LoopBinding.swift | 4 ++++ Loop/Public/SwiftUI/SwiftUISubscription.swift | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/Loop/Public/SwiftUI/EnvironmentLoop.swift b/Loop/Public/SwiftUI/EnvironmentLoop.swift index 5c68a2b..64d9558 100644 --- a/Loop/Public/SwiftUI/EnvironmentLoop.swift +++ b/Loop/Public/SwiftUI/EnvironmentLoop.swift @@ -1,3 +1,5 @@ +#if canImport(SwiftUI) && canImport(Combine) + import SwiftUI import Combine import ReactiveSwift @@ -47,3 +49,5 @@ public struct EnvironmentLoop: DynamicProperty { acknowledgedState = subscription.latestValue } } + +#endif diff --git a/Loop/Public/SwiftUI/EnvironmentValues.swift b/Loop/Public/SwiftUI/EnvironmentValues.swift index a8020c5..9d18779 100644 --- a/Loop/Public/SwiftUI/EnvironmentValues.swift +++ b/Loop/Public/SwiftUI/EnvironmentValues.swift @@ -1,3 +1,5 @@ +#if canImport(SwiftUI) + import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @@ -26,3 +28,5 @@ internal enum LoopEnvironmentKey: EnvironmentKey { return [:] } } + +#endif diff --git a/Loop/Public/SwiftUI/LoopBinding.swift b/Loop/Public/SwiftUI/LoopBinding.swift index 9de22cf..c46881c 100644 --- a/Loop/Public/SwiftUI/LoopBinding.swift +++ b/Loop/Public/SwiftUI/LoopBinding.swift @@ -1,3 +1,5 @@ +#if canImport(SwiftUI) && canImport(Combine) + import SwiftUI import Combine import ReactiveSwift @@ -47,3 +49,5 @@ public struct LoopBinding: DynamicProperty { loop.send(event) } } + +#endif diff --git a/Loop/Public/SwiftUI/SwiftUISubscription.swift b/Loop/Public/SwiftUI/SwiftUISubscription.swift index cf7ffd3..647af9b 100644 --- a/Loop/Public/SwiftUI/SwiftUISubscription.swift +++ b/Loop/Public/SwiftUI/SwiftUISubscription.swift @@ -1,4 +1,6 @@ import SwiftUI +#if canImport(Combine) + import Combine import ReactiveSwift @@ -30,3 +32,5 @@ internal final class SwiftUISubscription: ObservableObject { } } } + +#endif From e4d65d788c73ecd46e0afd49d015da46d3f8ac3a Mon Sep 17 00:00:00 2001 From: Anders Ha Date: Thu, 28 May 2020 14:36:56 +0100 Subject: [PATCH 3/4] Use `@Published` in SwiftUISubscription. --- Loop/Public/SwiftUI/SwiftUISubscription.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Loop/Public/SwiftUI/SwiftUISubscription.swift b/Loop/Public/SwiftUI/SwiftUISubscription.swift index 647af9b..a0abd8f 100644 --- a/Loop/Public/SwiftUI/SwiftUISubscription.swift +++ b/Loop/Public/SwiftUI/SwiftUISubscription.swift @@ -6,8 +6,7 @@ import ReactiveSwift @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) internal final class SwiftUISubscription: ObservableObject { - let objectWillChange = ObservableObjectPublisher() - var latestValue: State! + @Published var latestValue: State! private(set) var hasStarted = false private var disposable: Disposable? @@ -28,7 +27,6 @@ internal final class SwiftUISubscription: ObservableObject { .startWithValues { [weak self] state in guard let self = self else { return } self.latestValue = state - self.objectWillChange.send() } } } From 2a29ae753441889a1678af92bfc6d275497cde08 Mon Sep 17 00:00:00 2001 From: Anders Ha Date: Thu, 28 May 2020 15:11:07 +0100 Subject: [PATCH 4/4] Split subscription logic. --- Loop.xcodeproj/project.pbxproj | 8 ++++ Loop/Public/SwiftUI/EnvironmentLoop.swift | 22 ++++------- Loop/Public/SwiftUI/EnvironmentValues.swift | 17 +++++++-- Loop/Public/SwiftUI/LoopBinding.swift | 12 +++--- .../SwiftUIHotSwappableSubscription.swift | 37 +++++++++++++++++++ Loop/Public/SwiftUI/SwiftUISubscription.swift | 19 +++------- 6 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 Loop/Public/SwiftUI/SwiftUIHotSwappableSubscription.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index f76d751..cfa4fa1 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -47,6 +47,9 @@ 5B8F922124732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922024732C4600C1C90E /* SwiftUIBasicBindingHomeView.swift */; }; 5B8F922324732C9500C1C90E /* LoopBindingExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922224732C9500C1C90E /* LoopBindingExampleView.swift */; }; 5B8F922724732E1700C1C90E /* SimpleCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F922624732E1700C1C90E /* SimpleCounterView.swift */; }; + 5BAB9750247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */; }; + 5BAB9751247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */; }; + 5BAB9752247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */; }; 5BC88F842469CBA300394C63 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BC88F832469CBA300394C63 /* Nimble.framework */; }; 5BC88F862469CBAB00394C63 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BC88F852469CBAB00394C63 /* Nimble.framework */; }; 5BC88F88246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC88F87246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift */; }; @@ -228,6 +231,7 @@ 5B8F922224732C9500C1C90E /* LoopBindingExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopBindingExampleView.swift; sourceTree = ""; }; 5B8F922424732CA000C1C90E /* EnvironmentLoopExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentLoopExampleView.swift; sourceTree = ""; }; 5B8F922624732E1700C1C90E /* SimpleCounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleCounterView.swift; sourceTree = ""; }; + 5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHotSwappableSubscription.swift; sourceTree = ""; }; 5BC88F832469CBA300394C63 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5BC88F852469CBAB00394C63 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5BC88F87246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReactiveSwift+EnqueueTo.swift"; sourceTree = ""; }; @@ -468,6 +472,7 @@ 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */, 5B8F921B247325C300C1C90E /* EnvironmentValues.swift */, 5B8F920F2473242900C1C90E /* SwiftUISubscription.swift */, + 5BAB974F247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift */, ); path = SwiftUI; sourceTree = ""; @@ -815,6 +820,7 @@ 65761B2E23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */, 5BC88F9C246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */, 5BC88F92246B17B200394C63 /* Context.swift in Sources */, + 5BAB9750247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */, 585CD87B239E6A39004BE9CC /* Reducer.swift in Sources */, 9AD5D42D1F97375E00E6AE5A /* Property+System.swift in Sources */, 5BC88F88246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */, @@ -869,6 +875,7 @@ 65761B2F23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */, 5BC88F9D246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */, 5BC88F93246B17B200394C63 /* Context.swift in Sources */, + 5BAB9751247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */, 585CD87C239E6A3E004BE9CC /* Reducer.swift in Sources */, 65F8C262218371A800924657 /* Property+System.swift in Sources */, 5BC88F89246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */, @@ -893,6 +900,7 @@ 65761B3023CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */, 5BC88F9E246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */, 5BC88F94246B17B200394C63 /* Context.swift in Sources */, + 5BAB9752247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */, 585CD87D239E6A3E004BE9CC /* Reducer.swift in Sources */, 65F8C271218371AC00924657 /* Property+System.swift in Sources */, 5BC88F8A246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */, diff --git a/Loop/Public/SwiftUI/EnvironmentLoop.swift b/Loop/Public/SwiftUI/EnvironmentLoop.swift index 64d9558..301f58f 100644 --- a/Loop/Public/SwiftUI/EnvironmentLoop.swift +++ b/Loop/Public/SwiftUI/EnvironmentLoop.swift @@ -7,11 +7,11 @@ import ReactiveSwift @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @propertyWrapper public struct EnvironmentLoop: DynamicProperty { - @Environment(\.loops[ObjectIdentifier(Loop.self)]) - var erasedLoop: Any? + @Environment(\.loops[LoopType(Loop.self)]) + var erasedLoop: AnyObject? @ObservedObject - private var subscription: SwiftUISubscription + private var subscription: SwiftUIHotSwappableSubscription @inlinable public var wrappedValue: State { @@ -30,23 +30,15 @@ public struct EnvironmentLoop: DynamicProperty { internal var acknowledgedState: State! public init() { - self.subscription = SwiftUISubscription() + self.subscription = SwiftUIHotSwappableSubscription() } public mutating func update() { - if isKnownUniquelyReferenced(&subscription) == false { - subscription = SwiftUISubscription() - } - - if subscription.hasStarted == false { - guard let loop = erasedLoop as! Loop? else { - fatalError("Expect parent view to inject a `Loop<\(State.self), \(Event.self)>` through `View.environmentLoop(_:)`. Found none.") - } - - subscription.attach(to: loop) + guard let loop = erasedLoop as! Loop? else { + fatalError("Expect parent view to inject a `Loop<\(State.self), \(Event.self)>` through `View.environmentLoop(_:)`. Found none.") } - acknowledgedState = subscription.latestValue + acknowledgedState = subscription.currentState(in: loop) } } diff --git a/Loop/Public/SwiftUI/EnvironmentValues.swift b/Loop/Public/SwiftUI/EnvironmentValues.swift index 9d18779..2a0e1f8 100644 --- a/Loop/Public/SwiftUI/EnvironmentValues.swift +++ b/Loop/Public/SwiftUI/EnvironmentValues.swift @@ -6,7 +6,7 @@ import SwiftUI extension View { @inlinable public func environmentLoop(_ loop: Loop) -> some View { - let typeId = ObjectIdentifier(type(of: loop)) + let typeId = LoopType(type(of: loop)) return transformEnvironment(\.loops) { loops in loops[typeId] = loop @@ -16,7 +16,8 @@ extension View { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension EnvironmentValues { - public var loops: [ObjectIdentifier: Any] { + @usableFromInline + internal var loops: [LoopType: AnyObject] { get { self[LoopEnvironmentKey.self] } set { self[LoopEnvironmentKey.self] = newValue } } @@ -24,9 +25,19 @@ extension EnvironmentValues { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) internal enum LoopEnvironmentKey: EnvironmentKey { - static var defaultValue: [ObjectIdentifier: Any] { + static var defaultValue: [LoopType: AnyObject] { return [:] } } +@usableFromInline +struct LoopType: Hashable { + let id: ObjectIdentifier + + @usableFromInline + init(_ type: Any.Type) { + id = ObjectIdentifier(type) + } +} + #endif diff --git a/Loop/Public/SwiftUI/LoopBinding.swift b/Loop/Public/SwiftUI/LoopBinding.swift index c46881c..a15126e 100644 --- a/Loop/Public/SwiftUI/LoopBinding.swift +++ b/Loop/Public/SwiftUI/LoopBinding.swift @@ -22,19 +22,19 @@ public struct LoopBinding: DynamicProperty { } @usableFromInline - internal var acknowledgedState: State! + internal var acknowledgedState: State public init(_ loop: Loop) { // The subscription can be copied without restrictions. - self.subscription = SwiftUISubscription() + let subscription = SwiftUISubscription(loop: loop) + + self.subscription = subscription + self.acknowledgedState = subscription.latestValue self.loop = loop } public mutating func update() { - if subscription.hasStarted == false { - subscription.attach(to: loop) - } - + // Move latest value from the subscription only when SwiftUI has requested an update. acknowledgedState = subscription.latestValue } diff --git a/Loop/Public/SwiftUI/SwiftUIHotSwappableSubscription.swift b/Loop/Public/SwiftUI/SwiftUIHotSwappableSubscription.swift new file mode 100644 index 0000000..2ee90c6 --- /dev/null +++ b/Loop/Public/SwiftUI/SwiftUIHotSwappableSubscription.swift @@ -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: ObservableObject { + + @Published private var latestValue: State! + private weak var attachedLoop: Loop? + private var disposable: Disposable? + + init() {} + + deinit { + disposable?.dispose() + } + + func currentState(in loop: Loop) -> 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 diff --git a/Loop/Public/SwiftUI/SwiftUISubscription.swift b/Loop/Public/SwiftUI/SwiftUISubscription.swift index a0abd8f..c8a40cf 100644 --- a/Loop/Public/SwiftUI/SwiftUISubscription.swift +++ b/Loop/Public/SwiftUI/SwiftUISubscription.swift @@ -1,4 +1,3 @@ -import SwiftUI #if canImport(Combine) import Combine @@ -6,21 +5,11 @@ import ReactiveSwift @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) internal final class SwiftUISubscription: ObservableObject { - @Published var latestValue: State! - private(set) var hasStarted = false + @Published var latestValue: State private var disposable: Disposable? - init() {} - - deinit { - disposable?.dispose() - } - - func attach(to loop: Loop) { - guard hasStarted == false else { return } - hasStarted = true - + init(loop: Loop) { latestValue = loop.box._current disposable = loop.producer .observe(on: UIScheduler()) @@ -29,6 +18,10 @@ internal final class SwiftUISubscription: ObservableObject { self.latestValue = state } } + + deinit { + disposable?.dispose() + } } #endif