├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SharedObject │ ├── SharableObject.swift │ ├── SharedObject.swift │ └── SharedRepository.swift └── Tests ├── LinuxMain.swift └── SharedObjectTests ├── SharedObjectTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lorenzo Fiamingo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swiftui-shared-object", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v10_15), 10 | .tvOS(.v13), 11 | .watchOS(.v6) 12 | ], 13 | products: [ 14 | .library( 15 | name: "SharedObject", 16 | targets: ["SharedObject"]) 17 | ], 18 | targets: [ 19 | .target(name: "SharedObject"), 20 | .testTarget( 21 | name: "SharedObjectTests", 22 | dependencies: ["SharedObject"]) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI SharedObject 🍱 2 | 3 | `@SharedObject` is an alternative to `@StateObject`, `@ObservedObject`, `@EnvironmentObject` to handle `ObservableObject`. 4 | If you need to have multiple objects of the same class persisted among multiple view instances, it's difficult to handle the situation with other wrappers: with `@StateObject` the object will deinit with the view and be generated only for the specific view instance, with `@EnvironmentObject` you can bind only one instance of the same class for each Environment and with `@ObservedObject` is difficult to propagate object in nested views. 5 | 6 | `@SharedObject` simply stores the objects using an identifier, so you can retrieve it each time you'll need it. 7 | 8 | ## Usage 9 | 10 | Retrieve the shared object with the given id or, if not present, create a shared object with an initial value: 11 | ```swift 12 | @SharedObject("A") var letterA = Letter() 13 | ``` 14 | If you are sure that the object is already been created you can just retrieve the shared object: 15 | ```swift 16 | @SharedObject("A") var letterA: Letter 17 | ``` 18 | You can give a default initial value to the class, so you don't need to specify in each view you think the object will be created: 19 | ```swift 20 | final class Letter: SharableObject { 21 | 22 | var value: String 23 | 24 | init(_ value: String) { 25 | self.value = value 26 | } 27 | 28 | static var initialValue: Self { 29 | .init("A") 30 | } 31 | } 32 | ``` 33 | 34 | ## Installation 35 | 36 | 1. In Xcode, open your project and navigate to **File** → **Swift Packages** → **Add Package Dependency...** 37 | 2. Paste the repository URL (`https://github.com/lorenzofiamingo/swiftui-shared-object`) and click **Next**. 38 | 3. Click **Finish**. 39 | 40 | 41 | ## Other projects 42 | 43 | [SwiftUI VariadicViews 🥞](https://github.com/lorenzofiamingo/swiftui-variadic-views) 44 | 45 | [SwiftUI AsyncButton 🖲️](https://github.com/lorenzofiamingo/swiftui-async-button) 46 | 47 | [SwiftUI MapItemPicker 🗺️](https://github.com/lorenzofiamingo/swiftui-map-item-picker) 48 | 49 | [SwiftUI PhotosPicker 🌇](https://github.com/lorenzofiamingo/swiftui-photos-picker) 50 | 51 | [SwiftUI CachedAsyncImage 🗃️](https://github.com/lorenzofiamingo/swiftui-cached-async-image) 52 | 53 | [SwiftUI VerticalTabView 🔝](https://github.com/lorenzofiamingo/swiftui-vertical-tab-view) 54 | -------------------------------------------------------------------------------- /Sources/SharedObject/SharableObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharableObject.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 31/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A type of object that can provide a common initial value. 11 | @available(watchOS 6.0, tvOS 13.0, iOS 13.0, OSX 10.15, *) 12 | public protocol SharableObject: ObservableObject { 13 | 14 | static var initialValue: Self { get } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SharedObject/SharedObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedObject.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 31/07/2020. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | /// A property wrapper type for an observable object supplied with an id or created at the moment. 12 | @available(watchOS 6.0, tvOS 13.0, iOS 13.0, OSX 10.15, *) 13 | @propertyWrapper 14 | public struct SharedObject: DynamicProperty where ObjectType: ObservableObject, ID: Hashable { 15 | 16 | @ObservedObject private var object: Object 17 | 18 | /// The underlying value referenced by the shared object. 19 | public var wrappedValue: ObjectType { 20 | get { object.object } 21 | nonmutating set { object.object = newValue } 22 | } 23 | 24 | /// A projection of the shared object that creates bindings to its properties using dynamic member lookup. 25 | public var projectedValue: SharedObject.Wrapper { 26 | .init(object.object) 27 | } 28 | 29 | /// Retrieves the shared object with the given id or creates a shared object with an initial wrapped value. 30 | public init(wrappedValue: ObjectType, _ id: ID) { 31 | object = .init(wrappedValue: wrappedValue, id: id) 32 | } 33 | 34 | /// Retrieves the shared object with the given id. 35 | public init(_ id: ID) { 36 | object = .init(wrappedValue: nil, id: id) 37 | } 38 | 39 | /// Retrieves the shared object with the given id or creates a shared object with an initial wrapped value provided by the object class. 40 | public init(_ id: ID) where ObjectType: SharableObject { 41 | object = .init(wrappedValue: ObjectType.initialValue, id: id) 42 | } 43 | 44 | private final class Object: ObservableObject { 45 | 46 | private var cancellable: AnyCancellable? 47 | 48 | var object: ObjectType { didSet { subscribe() } } 49 | 50 | init(wrappedValue: ObjectType?, id: ID) { 51 | object = SharedRepository.getObject(for: id.hashValue, defaultValue: wrappedValue) 52 | subscribe() 53 | } 54 | 55 | private func subscribe() { 56 | cancellable = object.objectWillChange.sink { [unowned self] _ in 57 | self.objectWillChange.send() 58 | } 59 | } 60 | } 61 | 62 | /// A wrapper of the underlying observable object that can create bindings to its properties using dynamic member lookup. 63 | @dynamicMemberLookup 64 | public struct Wrapper { 65 | 66 | private let object: ObjectType 67 | 68 | init(_ object: ObjectType) { 69 | self.object = object 70 | } 71 | 72 | subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Binding { 73 | .init { 74 | object[keyPath: keyPath] 75 | } set: { newValue in 76 | object[keyPath: keyPath] = newValue 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/SharedObject/SharedRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedRepository.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 31/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | final class SharedRepository { 11 | 12 | private static var objects: [Int: Any] = [:] 13 | 14 | static func getObject(for key: Int, defaultValue: ObjectType?) -> ObjectType { 15 | if objects[key] == nil { 16 | guard let defaultValue = defaultValue else { 17 | preconditionFailure("SharedObject Error: The object was called for the first time without being initialized") 18 | } 19 | objects[key] = defaultValue 20 | } 21 | guard let object = objects[key] as? ObjectType else { 22 | preconditionFailure("SharedObject Error: The id was already used by another class") 23 | } 24 | return object 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SharedObjectTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SharedObjectTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SharedObjectTests/SharedObjectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SharedObject 3 | 4 | final class SharedObjectTests: XCTestCase { 5 | 6 | @SharedObject("A") var letterA = Letter() 7 | 8 | @SharedObject("B") var letterB1 = Letter() 9 | @SharedObject("B") var letterB2: Letter 10 | 11 | @SharedObject("C") var letterC: LetterWithInitialValueC 12 | 13 | func testInit() { 14 | letterA.value = "A" 15 | XCTAssertEqual(letterA.value, "A") 16 | } 17 | 18 | func testRepository() { 19 | letterB1.value = "B" 20 | XCTAssertEqual(letterB2.value, "B") 21 | } 22 | 23 | func testSharableObject() { 24 | XCTAssertEqual(letterC.value, "C") 25 | } 26 | 27 | static var allTests = [ 28 | ("testInit", testInit), 29 | ("testRepository", testRepository), 30 | ("testSharableObject", testSharableObject), 31 | ] 32 | 33 | final class Letter: ObservableObject { 34 | var value = "" 35 | } 36 | 37 | final class LetterWithInitialValueC: SharableObject { 38 | 39 | var value: String 40 | 41 | init(_ value: String) { 42 | self.value = value 43 | } 44 | 45 | static var initialValue: Self { 46 | .init("C") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/SharedObjectTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SharedObjectTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------