diff --git a/Changelog.md b/Changelog.md index a3a04c31..5069b8d3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,20 @@ # Changelog +## 2.0.0 + +This release mainly removes the dependency to `Queryable` because that should be treated as a fully separate package. Check it out at [https://github.com/SwiftedMind/Queryable](https://github.com/SwiftedMind/Queryable). + +Removing this should make it less likely to force major releases on Puddles. + +### Changed + +- Renamed the concept of adapters to containers since now they are simple `DynamicProperty` structs with a lot more power and generalized use! A detailed explanation will be provided on swiftedmind.com. + +### Removed + +- Removed `Signal`. This will be coming as a separate Swift package. +- Removed the dependency to `Queryable`, since that is fully separate and makes breaking changes harder to control. Simply add `Queryable` to your project, to keep using it. + ## 1.0.0 ### Added diff --git a/Examples/PuddlesExamples/PuddlesExamples.xcodeproj/project.pbxproj b/Examples/PuddlesExamples/PuddlesExamples.xcodeproj/project.pbxproj index 1cbf4c41..e398481c 100644 --- a/Examples/PuddlesExamples/PuddlesExamples.xcodeproj/project.pbxproj +++ b/Examples/PuddlesExamples/PuddlesExamples.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 9539F6DA2A5AF417009D3679 /* ExampleAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9539F6D92A5AF417009D3679 /* ExampleAdapter.swift */; }; 9539F6DC2A5AF7A3009D3679 /* NumberFactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9539F6DB2A5AF7A3009D3679 /* NumberFactView.swift */; }; 9539F6DE2A5B1538009D3679 /* NumberFactStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9539F6DD2A5B1538009D3679 /* NumberFactStackView.swift */; }; 953D02B02A5993D900D1A377 /* Core in Frameworks */ = {isa = PBXBuildFile; productRef = 953D02AF2A5993D900D1A377 /* Core */; }; @@ -15,6 +14,8 @@ 953D02B52A5A71F800D1A377 /* RootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953D02B42A5A71F800D1A377 /* RootRouter.swift */; }; 953D02B72A5A732400D1A377 /* View+mockProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953D02B62A5A732400D1A377 /* View+mockProviders.swift */; }; 953D02B92A5A81F200D1A377 /* ExampleSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953D02B82A5A81F200D1A377 /* ExampleSections.swift */; }; + 954E6CAA2A6966C000083ABA /* QueryableExampleRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954E6CA92A6966C000083ABA /* QueryableExampleRouter.swift */; }; + 955335572A9798D600D8E23A /* SortableNumberFacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955335562A9798D600D8E23A /* SortableNumberFacts.swift */; }; 955E513329CB1B1E00202BBA /* ding.wav in Resources */ = {isa = PBXBuildFile; fileRef = 955E513229CB1B1E00202BBA /* ding.wav */; }; 955E513729CB416700202BBA /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955E513629CB416700202BBA /* Strings.swift */; }; 95602F1729C8DDDC008EB800 /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95602F1629C8DDDC008EB800 /* Home.swift */; }; @@ -30,15 +31,15 @@ 95868CA029C85268001F8629 /* swiftgen-localization-template.stencil in Resources */ = {isa = PBXBuildFile; fileRef = 95868C9A29C85268001F8629 /* swiftgen-localization-template.stencil */; }; 95868CB129C857A9001F8629 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95868CAF29C857A9001F8629 /* Assets.swift */; }; 95868CC729C863A5001F8629 /* TrailingIconLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95868CC629C863A5001F8629 /* TrailingIconLabelStyle.swift */; }; + 9594A3EE2A9796A900F82FF7 /* Queryable in Frameworks */ = {isa = PBXBuildFile; productRef = 9594A3ED2A9796A900F82FF7 /* Queryable */; }; 95B746DE2A5F0CBB00615C9A /* ExperimentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B746DD2A5F0CBB00615C9A /* ExperimentProvider.swift */; }; 95C154B229C86A4700224B4B /* DisclosureItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C154B129C86A4700224B4B /* DisclosureItem.swift */; }; 95C154B729C86C7300224B4B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95C154B629C86C7300224B4B /* Assets.xcassets */; }; 95CCB9242A21E8B500E94681 /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CCB9232A21E8B500E94681 /* Root.swift */; }; 95E9CFA82A5A982100315BFA /* StaticExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFA72A5A982100315BFA /* StaticExample.swift */; }; 95E9CFAA2A5A985400315BFA /* BasicProviderExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFA92A5A985400315BFA /* BasicProviderExample.swift */; }; - 95E9CFAE2A5A986A00315BFA /* AdapterExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFAD2A5A986A00315BFA /* AdapterExample.swift */; }; + 95E9CFAE2A5A986A00315BFA /* ContainerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFAD2A5A986A00315BFA /* ContainerExample.swift */; }; 95E9CFB02A5A987300315BFA /* QueryableExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFAF2A5A987300315BFA /* QueryableExample.swift */; }; - 95E9CFB22A5A987A00315BFA /* SignalExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFB12A5A987A00315BFA /* SignalExample.swift */; }; 95E9CFB52A5A9BF400315BFA /* FavoriteNumbersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFB42A5A9BF400315BFA /* FavoriteNumbersSection.swift */; }; 95E9CFB72A5ADD4500315BFA /* CultureMindsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFB62A5ADD4500315BFA /* CultureMindsProvider.swift */; }; 95E9CFB92A5AE07C00315BFA /* NumberFactProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E9CFB82A5AE07C00315BFA /* NumberFactProvider.swift */; }; @@ -47,7 +48,6 @@ /* Begin PBXFileReference section */ 952B2B3329CBF9E0004A73F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9539F6D92A5AF417009D3679 /* ExampleAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAdapter.swift; sourceTree = ""; }; 9539F6DB2A5AF7A3009D3679 /* NumberFactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFactView.swift; sourceTree = ""; }; 9539F6DD2A5B1538009D3679 /* NumberFactStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFactStackView.swift; sourceTree = ""; }; 953D02AE2A5993C200D1A377 /* Core */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Core; sourceTree = ""; }; @@ -55,6 +55,8 @@ 953D02B42A5A71F800D1A377 /* RootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = ""; }; 953D02B62A5A732400D1A377 /* View+mockProviders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+mockProviders.swift"; sourceTree = ""; }; 953D02B82A5A81F200D1A377 /* ExampleSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleSections.swift; sourceTree = ""; }; + 954E6CA92A6966C000083ABA /* QueryableExampleRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryableExampleRouter.swift; sourceTree = ""; }; + 955335562A9798D600D8E23A /* SortableNumberFacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortableNumberFacts.swift; sourceTree = ""; }; 955E513229CB1B1E00202BBA /* ding.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ding.wav; sourceTree = ""; }; 955E513629CB416700202BBA /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 95602F1629C8DDDC008EB800 /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; @@ -76,9 +78,8 @@ 95E9CFA62A5A97F300315BFA /* Puddles */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Puddles; path = ../..; sourceTree = ""; }; 95E9CFA72A5A982100315BFA /* StaticExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticExample.swift; sourceTree = ""; }; 95E9CFA92A5A985400315BFA /* BasicProviderExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicProviderExample.swift; sourceTree = ""; }; - 95E9CFAD2A5A986A00315BFA /* AdapterExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdapterExample.swift; sourceTree = ""; }; + 95E9CFAD2A5A986A00315BFA /* ContainerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerExample.swift; sourceTree = ""; }; 95E9CFAF2A5A987300315BFA /* QueryableExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryableExample.swift; sourceTree = ""; }; - 95E9CFB12A5A987A00315BFA /* SignalExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalExample.swift; sourceTree = ""; }; 95E9CFB42A5A9BF400315BFA /* FavoriteNumbersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNumbersSection.swift; sourceTree = SOURCE_ROOT; }; 95E9CFB62A5ADD4500315BFA /* CultureMindsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CultureMindsProvider.swift; sourceTree = ""; }; 95E9CFB82A5AE07C00315BFA /* NumberFactProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFactProvider.swift; sourceTree = ""; }; @@ -93,6 +94,7 @@ 95868C8929C851F2001F8629 /* IdentifiedCollections in Frameworks */, 95868C8629C851E8001F8629 /* Puddles in Frameworks */, 953D02B02A5993D900D1A377 /* Core in Frameworks */, + 9594A3EE2A9796A900F82FF7 /* Queryable in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -104,6 +106,7 @@ children = ( 953D02B22A5A719300D1A377 /* Router.swift */, 953D02B42A5A71F800D1A377 /* RootRouter.swift */, + 954E6CA92A6966C000083ABA /* QueryableExampleRouter.swift */, ); path = Router; sourceTree = ""; @@ -111,6 +114,7 @@ 954FA2642A17F5B4006F26FC /* Providers */ = { isa = PBXGroup; children = ( + 955335552A9798C200D8E23A /* Container */, 95E9CFB62A5ADD4500315BFA /* CultureMindsProvider.swift */, 95E9CFB82A5AE07C00315BFA /* NumberFactProvider.swift */, 95B746DD2A5F0CBB00615C9A /* ExperimentProvider.swift */, @@ -125,6 +129,14 @@ name = Frameworks; sourceTree = ""; }; + 955335552A9798C200D8E23A /* Container */ = { + isa = PBXGroup; + children = ( + 955335562A9798D600D8E23A /* SortableNumberFacts.swift */, + ); + path = Container; + sourceTree = ""; + }; 95868C6829C851C6001F8629 = { isa = PBXGroup; children = ( @@ -216,15 +228,6 @@ path = Localizables; sourceTree = ""; }; - 95E9CFB32A5A98AE00315BFA /* AdapterExample */ = { - isa = PBXGroup; - children = ( - 95E9CFAD2A5A986A00315BFA /* AdapterExample.swift */, - 9539F6D92A5AF417009D3679 /* ExampleAdapter.swift */, - ); - path = AdapterExample; - sourceTree = ""; - }; 95EC65842A180717005BDDC0 /* Components */ = { isa = PBXGroup; children = ( @@ -247,9 +250,8 @@ 95602F1629C8DDDC008EB800 /* Home.swift */, 95E9CFA72A5A982100315BFA /* StaticExample.swift */, 95E9CFA92A5A985400315BFA /* BasicProviderExample.swift */, - 95E9CFB32A5A98AE00315BFA /* AdapterExample */, + 95E9CFAD2A5A986A00315BFA /* ContainerExample.swift */, 95E9CFAF2A5A987300315BFA /* QueryableExample.swift */, - 95E9CFB12A5A987A00315BFA /* SignalExample.swift */, ); path = Modules; sourceTree = ""; @@ -275,6 +277,7 @@ 95868C8529C851E8001F8629 /* Puddles */, 95868C8829C851F2001F8629 /* IdentifiedCollections */, 953D02AF2A5993D900D1A377 /* Core */, + 9594A3ED2A9796A900F82FF7 /* Queryable */, ); productName = PuddlesExamples; productReference = 95868C7129C851C6001F8629 /* PuddlesExamples.app */; @@ -307,6 +310,7 @@ packageReferences = ( 95868C8229C851E8001F8629 /* XCRemoteSwiftPackageReference "Puddles" */, 95868C8729C851F2001F8629 /* XCRemoteSwiftPackageReference "swift-identified-collections" */, + 9594A3EC2A9796A900F82FF7 /* XCRemoteSwiftPackageReference "Queryable" */, ); productRefGroup = 95868C7229C851C6001F8629 /* Products */; projectDirPath = ""; @@ -366,26 +370,26 @@ 9539F6DE2A5B1538009D3679 /* NumberFactStackView.swift in Sources */, 95E9CFB52A5A9BF400315BFA /* FavoriteNumbersSection.swift in Sources */, 953D02B32A5A719300D1A377 /* Router.swift in Sources */, - 95E9CFB22A5A987A00315BFA /* SignalExample.swift in Sources */, 95B746DE2A5F0CBB00615C9A /* ExperimentProvider.swift in Sources */, 95E9CFA82A5A982100315BFA /* StaticExample.swift in Sources */, 953D02B72A5A732400D1A377 /* View+mockProviders.swift in Sources */, 95C154B229C86A4700224B4B /* DisclosureItem.swift in Sources */, 95E9CFBB2A5AED0D00315BFA /* CultureMindNameStackView.swift in Sources */, - 9539F6DA2A5AF417009D3679 /* ExampleAdapter.swift in Sources */, 955E513729CB416700202BBA /* Strings.swift in Sources */, 953D02B92A5A81F200D1A377 /* ExampleSections.swift in Sources */, 95E9CFB02A5A987300315BFA /* QueryableExample.swift in Sources */, - 95E9CFAE2A5A986A00315BFA /* AdapterExample.swift in Sources */, + 95E9CFAE2A5A986A00315BFA /* ContainerExample.swift in Sources */, 953D02B52A5A71F800D1A377 /* RootRouter.swift in Sources */, 95868C7529C851C6001F8629 /* App.swift in Sources */, 95E9CFB72A5ADD4500315BFA /* CultureMindsProvider.swift in Sources */, + 954E6CAA2A6966C000083ABA /* QueryableExampleRouter.swift in Sources */, 95868CC729C863A5001F8629 /* TrailingIconLabelStyle.swift in Sources */, 95868CB129C857A9001F8629 /* Assets.swift in Sources */, 95E9CFB92A5AE07C00315BFA /* NumberFactProvider.swift in Sources */, 957850AE2A5F1469002C3EDC /* ExperimentListSection.swift in Sources */, 95E9CFAA2A5A985400315BFA /* BasicProviderExample.swift in Sources */, 9539F6DC2A5AF7A3009D3679 /* NumberFactView.swift in Sources */, + 955335572A9798D600D8E23A /* SortableNumberFacts.swift in Sources */, 95602F1729C8DDDC008EB800 /* Home.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -613,6 +617,14 @@ minimumVersion = 0.7.0; }; }; + 9594A3EC2A9796A900F82FF7 /* XCRemoteSwiftPackageReference "Queryable" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SwiftedMind/Queryable"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -630,6 +642,11 @@ package = 95868C8729C851F2001F8629 /* XCRemoteSwiftPackageReference "swift-identified-collections" */; productName = IdentifiedCollections; }; + 9594A3ED2A9796A900F82FF7 /* Queryable */ = { + isa = XCSwiftPackageProductDependency; + package = 9594A3EC2A9796A900F82FF7 /* XCRemoteSwiftPackageReference "Queryable" */; + productName = Queryable; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 95868C6929C851C6001F8629 /* Project object */; diff --git a/Examples/PuddlesExamples/PuddlesExamples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/PuddlesExamples/PuddlesExamples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ab4db688..86b03aa0 100644 --- a/Examples/PuddlesExamples/PuddlesExamples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/PuddlesExamples/PuddlesExamples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftedMind/Queryable", "state" : { - "revision" : "7677089e3f0db3544ed657c18ed051a18f6dd781", - "version" : "1.0.2" + "revision" : "0eb2cfdd868cd5c9b9db4c930d6410d1328f85e8", + "version" : "2.0.1" } }, { diff --git a/Examples/PuddlesExamples/PuddlesExamples/Components/ExampleSections.swift b/Examples/PuddlesExamples/PuddlesExamples/Components/ExampleSections.swift index dc05fd71..5f4801a1 100644 --- a/Examples/PuddlesExamples/PuddlesExamples/Components/ExampleSections.swift +++ b/Examples/PuddlesExamples/PuddlesExamples/Components/ExampleSections.swift @@ -57,15 +57,12 @@ struct AdvancedExamplesSection: View { var body: some View { Section { - Button("Adapters") { - interface.send(.openAdapterExample) + Button("Containers") { + interface.send(.openContainerExample) } Button("Queryables") { interface.send(.openQueryableExample) } - Button("Signals") { - interface.send(.openSignalExample) - } } header: { Text("Basic") } @@ -74,9 +71,8 @@ struct AdvancedExamplesSection: View { extension AdvancedExamplesSection { enum Action: Hashable { - case openAdapterExample + case openContainerExample case openQueryableExample - case openSignalExample } } diff --git a/Examples/PuddlesExamples/PuddlesExamples/Modules/AdapterExample/AdapterExample.swift b/Examples/PuddlesExamples/PuddlesExamples/Modules/ContainerExample.swift similarity index 70% rename from Examples/PuddlesExamples/PuddlesExamples/Modules/AdapterExample/AdapterExample.swift rename to Examples/PuddlesExamples/PuddlesExamples/Modules/ContainerExample.swift index 65ab744f..b1f1cf84 100644 --- a/Examples/PuddlesExamples/PuddlesExamples/Modules/AdapterExample/AdapterExample.swift +++ b/Examples/PuddlesExamples/PuddlesExamples/Modules/ContainerExample.swift @@ -22,27 +22,36 @@ import Puddles import SwiftUI +import Models -struct AdapterExample: View { - @ObservedObject var adapter: ExampleAdapter +@MainActor +struct ContainerExample: View { @Environment(\.dismiss) private var dismiss + + /// A "container" that accesses the NumberFactProvider from the current + /// environment and provides additional states and methods to store and + /// sort number facts. + /// + /// This logic could be place inside this view, but extracting it makes it reusable, while still staying fully mockable (since it can access + /// the environment itself. We don't have to pass it in.) + var numberFacts = SortableNumberFacts() var body: some View { NavigationStack { List { Button("Add Random Number Fact") { - adapter.fetchRandomFact() + numberFacts.fetchRandomFact() } Section { Button("Sort") { - adapter.sort() + numberFacts.sort() } - NumberFactStackView(numberFacts: adapter.facts, interface: .consume(handleViewInterface)) + NumberFactStackView(numberFacts: numberFacts.facts, interface: .consume(handleViewInterface)) } footer: { Text("Data provided by [NumbersAPI.com](https://numbersapi.com)") } } - .animation(.default, value: adapter.facts) + .animation(.default, value: numberFacts.facts) .navigationTitle("Random Facts") .toolbar { ToolbarItem(placement: .navigationBarLeading) { @@ -52,7 +61,7 @@ struct AdapterExample: View { } ToolbarItem { Button("Remove All", role: .destructive) { - adapter.reset() + numberFacts.reset() } } } @@ -63,14 +72,14 @@ struct AdapterExample: View { private func handleViewInterface(_ action: NumberFactStackView.Action) { switch action { case .deleteFacts(let indexSet): - adapter.facts.remove(atOffsets: indexSet) + numberFacts.facts.remove(atOffsets: indexSet) } } } -struct AdapterExample_Previews: PreviewProvider { +struct ContainerExample_Previews: PreviewProvider { static var previews: some View { - AdapterExample(adapter: .init(numberFactProvider: .mock)) + ContainerExample() .withMockProviders() } } diff --git a/Examples/PuddlesExamples/PuddlesExamples/Modules/Home.swift b/Examples/PuddlesExamples/PuddlesExamples/Modules/Home.swift index 1e804c0a..dbf1d793 100644 --- a/Examples/PuddlesExamples/PuddlesExamples/Modules/Home.swift +++ b/Examples/PuddlesExamples/PuddlesExamples/Modules/Home.swift @@ -44,20 +44,12 @@ struct Home: View { .fullScreenCover(isPresented: $homeRouter.isShowingBasicProviderExample) { BasicProviderExample() } - .fullScreenCover(isPresented: $homeRouter.isShowingAdapterExample) { - StateObjectHosting { - // Will be initialized as `@StateObject` inside `StateObjectHosting` and passed to the content closure - ExampleAdapter(numberFactProvider: numberFactProvider) - } content: { adapter in - AdapterExample(adapter: adapter) - } + .fullScreenCover(isPresented: $homeRouter.isShowingContainerExample) { + ContainerExample() } .fullScreenCover(isPresented: $homeRouter.isShowingQueryableExample) { QueryableExample() } - .fullScreenCover(isPresented: $homeRouter.isShowingSignalExample) { - SignalExample() - } } @MainActor @@ -73,12 +65,10 @@ struct Home: View { @MainActor private func handleAdvancedExamplesInterface(_ action: AdvancedExamplesSection.Action) { switch action { - case .openAdapterExample: - Router.shared.navigate(to: .adapterExample) + case .openContainerExample: + Router.shared.navigate(to: .containerExample) case .openQueryableExample: Router.shared.navigate(to: .queryableExample) - case .openSignalExample: - Router.shared.navigate(to: .signalExample) } } } diff --git a/Examples/PuddlesExamples/PuddlesExamples/Modules/QueryableExample.swift b/Examples/PuddlesExamples/PuddlesExamples/Modules/QueryableExample.swift index b7347af6..a35d2119 100644 --- a/Examples/PuddlesExamples/PuddlesExamples/Modules/QueryableExample.swift +++ b/Examples/PuddlesExamples/PuddlesExamples/Modules/QueryableExample.swift @@ -28,7 +28,7 @@ import Models struct QueryableExample: View { @EnvironmentObject private var experimentProvider: ExperimentProvider @Environment(\.dismiss) private var dismiss - @Queryable var buttonConfirmation + @ObservedObject private var queryableExampleRouter = Router.shared.queryableExample var body: some View { NavigationStack { @@ -43,10 +43,10 @@ struct QueryableExample: View { } } .animation(.default, value: experimentProvider.experiments) - .queryableAlert(controlledBy: buttonConfirmation, title: "Delete this experiment?") { _, query in + .queryableAlert(controlledBy: queryableExampleRouter.deletionConfirmation, title: "Delete this experiment?") { query in Button("Yes") { query.answer(with: true) } Button("No") { query.answer(with: false) } - } message: {_ in} + } message: {} .navigationTitle("Experiment Ideas") .toolbar { ToolbarItem(placement: .topBarLeading) { @@ -61,7 +61,7 @@ struct QueryableExample: View { private func requestDeletion(of experiment: Experiment) { Task { do { - if try await buttonConfirmation.query() { + if try await queryableExampleRouter.queryDeletionConfirmation() { experimentProvider.delete(experiment) } } catch {} diff --git a/Examples/PuddlesExamples/PuddlesExamples/Modules/Root.swift b/Examples/PuddlesExamples/PuddlesExamples/Modules/Root.swift index dd1b0b5f..77a23434 100644 --- a/Examples/PuddlesExamples/PuddlesExamples/Modules/Root.swift +++ b/Examples/PuddlesExamples/PuddlesExamples/Modules/Root.swift @@ -28,7 +28,7 @@ struct Root: View { Home() .onOpenURL { url in // To test, run this in a console (replace the domain with a case from Router.Destination): - // xcrun simctl openurl booted "puddlesExample://adapterExample" + // xcrun simctl openurl booted "puddlesExample://containerExample" if let destination = Router.Destination(rawValue: url.host() ?? "") { Router.shared.navigate(to: destination) } diff --git a/Examples/PuddlesExamples/PuddlesExamples/Modules/SignalExample.swift b/Examples/PuddlesExamples/PuddlesExamples/Modules/SignalExample.swift deleted file mode 100644 index 4bd4b1d7..00000000 --- a/Examples/PuddlesExamples/PuddlesExamples/Modules/SignalExample.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright © 2023 Dennis Müller and all collaborators -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import Puddles -import SwiftUI - -struct SignalExample: View { - @Environment(\.dismiss) private var dismiss - @Signal private var signal - - var body: some View { - NavigationStack { - List { - Section { - Button("I Demand An Answer!") { - signal.send(.showTheAnswer) - } - } header: { - Text("Parent view") - } - - Section { - SubModule() - } header: { - Text("Nested view") - } - } - .sendSignals(signal) - .navigationTitle("Q&A") - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Close") { - dismiss() - } - } - } - } - } -} - -struct SubModule: View { - // This view has its own state, not a binding. - // With signals you can give parent views controlled access to the state, without the parents having to own it themselves and pass it in. - @State private var showMyPrivateLittleSecret: Bool = false - - var body: some View { - content - .animation(.default, value: showMyPrivateLittleSecret) - .resolveSignals(ofType: Action.self) { action in - switch action { - case .showTheAnswer: - showMyPrivateLittleSecret = true - } - } - } - - @ViewBuilder @MainActor - private var content: some View { - Text("Why did the bicycle fall over?") - if showMyPrivateLittleSecret { - Text("Because it was two-tired!") - } - Button("Toggle Visibility") { - showMyPrivateLittleSecret.toggle() - } - } - -} - -extension SubModule { - enum Action: Hashable { - case showTheAnswer - } -} - -struct SignalExample_Previews: PreviewProvider { - static var previews: some View { - SignalExample() - .withMockProviders() - } -} - diff --git a/Examples/PuddlesExamples/PuddlesExamples/Modules/AdapterExample/ExampleAdapter.swift b/Examples/PuddlesExamples/PuddlesExamples/Providers/Container/SortableNumberFacts.swift similarity index 85% rename from Examples/PuddlesExamples/PuddlesExamples/Modules/AdapterExample/ExampleAdapter.swift rename to Examples/PuddlesExamples/PuddlesExamples/Providers/Container/SortableNumberFacts.swift index ce03603e..c52756e5 100644 --- a/Examples/PuddlesExamples/PuddlesExamples/Modules/AdapterExample/ExampleAdapter.swift +++ b/Examples/PuddlesExamples/PuddlesExamples/Providers/Container/SortableNumberFacts.swift @@ -20,18 +20,14 @@ // SOFTWARE. // +import Puddles import SwiftUI import Models -@MainActor final class ExampleAdapter: ObservableObject { - - @Published var facts: IdentifiedArrayOf = [] - private let numberFactProvider: NumberFactProvider - private var availableNumbers: Set = Set(0...200) - - init(numberFactProvider: NumberFactProvider) { - self.numberFactProvider = numberFactProvider - } +struct SortableNumberFacts: DynamicProperty { + @EnvironmentObject private var numberFactProvider: NumberFactProvider + @State var facts: IdentifiedArrayOf = [] + @State private var availableNumbers: Set = Set(0...200) func fetchFactAboutRandomNumber() async throws { if let number = randomAvailableNumber() { diff --git a/Examples/PuddlesExamples/PuddlesExamples/Router/QueryableExampleRouter.swift b/Examples/PuddlesExamples/PuddlesExamples/Router/QueryableExampleRouter.swift new file mode 100644 index 00000000..22812482 --- /dev/null +++ b/Examples/PuddlesExamples/PuddlesExamples/Router/QueryableExampleRouter.swift @@ -0,0 +1,35 @@ +// +// Copyright © 2023 Dennis Müller and all collaborators +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import SwiftUI +import Queryable + +@MainActor final class QueryableExampleRouter: ObservableObject { + + let deletionConfirmation = Queryable() + + func queryDeletionConfirmation() async throws -> Bool { + // Here, you could make sure that nothing blocks the presentation of the deletion confirmation query + try await deletionConfirmation.query() + } +} + diff --git a/Examples/PuddlesExamples/PuddlesExamples/Router/RootRouter.swift b/Examples/PuddlesExamples/PuddlesExamples/Router/RootRouter.swift index 70c09ab9..98822f0e 100644 --- a/Examples/PuddlesExamples/PuddlesExamples/Router/RootRouter.swift +++ b/Examples/PuddlesExamples/PuddlesExamples/Router/RootRouter.swift @@ -31,16 +31,14 @@ import SwiftUI @Published var path: [Destination] = [] @Published var isShowingStaticExample: Bool = false @Published var isShowingBasicProviderExample: Bool = false - @Published var isShowingAdapterExample: Bool = false + @Published var isShowingContainerExample: Bool = false @Published var isShowingQueryableExample: Bool = false - @Published var isShowingSignalExample: Bool = false func reset() { path.removeAll() isShowingStaticExample = false isShowingBasicProviderExample = false - isShowingAdapterExample = false + isShowingContainerExample = false isShowingQueryableExample = false - isShowingSignalExample = false } } diff --git a/Examples/PuddlesExamples/PuddlesExamples/Router/Router.swift b/Examples/PuddlesExamples/PuddlesExamples/Router/Router.swift index d3e64651..126318bc 100644 --- a/Examples/PuddlesExamples/PuddlesExamples/Router/Router.swift +++ b/Examples/PuddlesExamples/PuddlesExamples/Router/Router.swift @@ -21,19 +21,21 @@ // import SwiftUI +import Puddles +import Queryable @MainActor final class Router: ObservableObject { static let shared: Router = .init() var home: HomeRouter = .init() + var queryableExample: QueryableExampleRouter = .init() enum Destination: String, Hashable { case root case staticExample case basicProviderExample - case adapterExample + case containerExample case queryableExample - case signalExample } func navigate(to destination: Destination) { @@ -46,15 +48,12 @@ import SwiftUI case .basicProviderExample: home.reset() home.isShowingBasicProviderExample = true - case .adapterExample: + case .containerExample: home.reset() - home.isShowingAdapterExample = true + home.isShowingContainerExample = true case .queryableExample: home.reset() home.isShowingQueryableExample = true - case .signalExample: - home.reset() - home.isShowingSignalExample = true } } diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 16c1917d..00000000 --- a/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "queryable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftedMind/Queryable", - "state" : { - "revision" : "7677089e3f0db3544ed657c18ed051a18f6dd781", - "version" : "1.0.2" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift index f90f6405..583cd38a 100644 --- a/Package.swift +++ b/Package.swift @@ -17,15 +17,10 @@ let package = Package( targets: ["Puddles"] ) ], - dependencies: [ - .package(url: "https://github.com/SwiftedMind/Queryable", from: "1.0.0") - ], + dependencies: [], targets: [ .target( - name: "Puddles", - dependencies: [ - "Queryable" - ] + name: "Puddles" ) ] ) diff --git a/README.md b/README.md index 51aa2f4a..9d2c26d8 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Puddles supports iOS 15+, macOS 12+, watchOS 8+ and tvOS 15+. Add the following line to the dependencies in your `Package.swift` file: ```swift -.package(url: "https://github.com/SwiftedMind/Puddles", from: "1.0.0") +.package(url: "https://github.com/SwiftedMind/Puddles", from: "2.0.0") ``` ### Xcode project diff --git a/Sources/Puddles/Signal/Signal+Environment.swift b/Sources/Puddles/Signal/Signal+Environment.swift deleted file mode 100644 index c067d4f5..00000000 --- a/Sources/Puddles/Signal/Signal+Environment.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// Copyright © 2023 Dennis Müller and all collaborators -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import SwiftUI - -// MARK: - SwiftUI Environment Setup - -struct SignalWrapper: Equatable { - var identity: UUID - var id: AnyHashable? - var debugIdentifier: String? - var value: Any? - var signalResolved: @MainActor (_ id: AnyHashable?) -> Void - - public static func == (lhs: SignalWrapper, rhs: SignalWrapper) -> Bool { - lhs.identity == rhs.identity - } -} - -private struct SignalWrapperKey: EnvironmentKey { - static let defaultValue: SignalWrapper? = nil -} - -extension EnvironmentValues { - var signalWrapper: SignalWrapper? { - get { self[SignalWrapperKey.self] } - set { self[SignalWrapperKey.self] = newValue } - } -} - -// MARK: - Provider Extension - -public extension View { - - /// Configures the view to send signals down the hierarchy until a child view resolves it. - /// - /// - Parameter signal: The signal that sends the provider's target states. - /// - Parameter id: An identifier to identify a specific signal. - /// - Returns: A view with a configured signal reception. - func sendSignals( - _ signal: Signal.Wrapped, - id: AnyHashable? = nil - ) -> some View { - return environment( - \.signalWrapper, - .init( - identity: signal.identity, - id: id, - debugIdentifier: signal.debugIdentifier, - value: signal.valueForId(id), - signalResolved: signal.removeValue - ) - ) - } -} - -// MARK: - Resolve Signals - -struct SignalResolver: ViewModifier { - @Environment(\.signalWrapper) private var signalWrapper - - var action: (_ value: SignalValue) -> Void - - init(action: @escaping (_ value: SignalValue) -> Void) { - self.action = action - } - - func body(content: Content) -> some View { - content - .onFirstAppear { - if let signalValue = signalWrapper?.value as? SignalValue { - action(signalValue) - logSignal(debugIdentifier: signalWrapper?.debugIdentifier, value: signalValue) - signalWrapper?.signalResolved(signalWrapper?.id) - } - } - .onChange(of: signalWrapper) { newValue in - guard let targetState = newValue?.value as? SignalValue else { return } - action(targetState) - signalWrapper?.signalResolved(newValue?.id) - } - } - - private func logSignal(debugIdentifier: String?, value: SignalValue) { - logger.debug("Signal resolved: ».\(String(describing: value), privacy: .public)«") - } -} - -extension View { - /// Configures the view to receive and resolve signals by calling the provided `action` closure with the signal value. - /// - Parameters: - /// - signalValueType: The type of signal that should be resolved. - /// - action: The action to perform to resolve the signal. - /// - Returns: A view that is configured to receive and resolve signals. - public func resolveSignals(ofType signalValueType: SignalValue.Type, action: @escaping (_ value: SignalValue) -> Void) -> some View { - modifier(SignalResolver(action: action)) - } -} diff --git a/Sources/Puddles/Signal/Signal.swift b/Sources/Puddles/Signal/Signal.swift deleted file mode 100644 index 7a4759a3..00000000 --- a/Sources/Puddles/Signal/Signal.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// Copyright © 2023 Dennis Müller and all collaborators -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import Combine -import SwiftUI - -/// A property wrapper that lets you send one-time signals down the view hierarchy, -/// which is particularly useful for deep linking and state restoration without exposing all internal states of the child views. -/// -/// To make a `View` receive and resolve Signals, use the `.resolveSignals(ofType:action:)` view modifier. -/// -/// - Note: A resolved signal is consumed and will not travel further down the view hierarchy. -@propertyWrapper -public struct Signal: DynamicProperty { - @StateObject private var stateHolder: StateHolder - - public var wrappedValue: Wrapped { - .init( - identity: stateHolder.identity, - debugIdentifier: stateHolder.debugIdentifier, - valueForId: stateHolder.valueForId, - onSend: stateHolder.send, - removeValue: stateHolder.removeValue - ) - } - - public init( - initial value: Value? = nil, - id: AnyHashable? = nil, - debugIdentifier: String? = nil - ) { - _stateHolder = .init(wrappedValue: .init(value: value, id: id, debugIdentifier: debugIdentifier)) - } -} - -public extension Signal { - - @MainActor - final class StateHolder: ObservableObject { - private let emptyId = AnyHashable(UUID()) - private var value: [AnyHashable: Value] = [:] - let debugIdentifier: String? - @Published private(set) var identity: UUID = .init() - - init( - value: Value? = nil, - id: AnyHashable?, - debugIdentifier: String? - ) { - self.value[id ?? emptyId] = value - self.debugIdentifier = debugIdentifier - - if let value { - logSignal(value: value, isInitial: true) - } - } - - func valueForId(_ id: AnyHashable?) -> Value? { - value[id ?? emptyId] - } - - func send(_ value: Value, id: AnyHashable?) { - logSignal(value: value, isInitial: false) - self.value[id ?? emptyId] = value - identity = .init() - } - - func removeValue(id: AnyHashable?) { - self.value.removeValue(forKey: id ?? emptyId) - } - - private func logSignal(value: Value, isInitial: Bool) { - let loggerPrefix = debugIdentifier == nil ? "" : debugIdentifier! + " - " - let type = "\(isInitial ? "Initial" : "Sending")" - logger.debug("\(loggerPrefix)\(type) Signal: ».\(String(describing: value), privacy: .public)«") - } - } - - struct Wrapped: Equatable { - var identity: UUID - var debugIdentifier: String? - var valueForId: (_ id: AnyHashable?) -> Value? - var onSend: @MainActor (_ value: Value, _ id: AnyHashable?) -> Void - var removeValue: (_ id: AnyHashable?) -> Void - - /// Sends a target state. - /// - Parameter value: The target state to send. - @MainActor - public func send(_ value: Value, id: AnyHashable? = nil) { - onSend(value, id) - } - - public static func == (lhs: Signal.Wrapped, rhs: Signal.Wrapped) -> Bool { - lhs.identity == rhs.identity - } - } -}