├── .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 |
--------------------------------------------------------------------------------