├── .github └── workflows │ └── cocoapods.yml ├── LICENSE ├── Package.swift ├── PublishedObject.podspec ├── README.md ├── Sources └── PublishedObject │ └── PublishedObject.swift └── Tests ├── LinuxMain.swift └── PublishedObjectTests ├── PublishedObjectTests.swift └── XCTestManifests.swift /.github/workflows/cocoapods.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Cocoapods release 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | "PublishedObject.podspec" 8 | 9 | jobs: 10 | build: 11 | runs-on: macOS-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Install Cocoapods 16 | run: gem install cocoapods 17 | - name: Publish to CocoaPod register 18 | env: 19 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 20 | run: | 21 | pod trunk push PublishedObject.podspec --allow-warnings --verbose 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Casper "Amzd" Zandbergen 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.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PublishedObject", 7 | platforms: [ 8 | .macOS(.v10_13), 9 | .iOS(.v11), 10 | .tvOS(.v11) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 14 | .library( 15 | name: "PublishedObject", 16 | targets: ["PublishedObject"] 17 | ), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target(name: "PublishedObject", dependencies: []), 23 | .testTarget(name: "PublishedObjectTests", dependencies: ["PublishedObject"]) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /PublishedObject.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'PublishedObject' 3 | s.version = '0.1.5' 4 | s.license = { type: 'MIT' } 5 | s.summary = 'A property wrapper that forwards the objectWillChange of the wrapped ObservableObject to the enclosing ObservableObjects objectWillChange.' 6 | s.homepage = 'https://github.com/Amzd/PublishedObject' 7 | s.author = { 'Casper "Amzd" Zandbergen' => 'info@casperzandbergen.nl' } 8 | s.source = { :git => 'https://github.com/Amzd/PublishedObject.git', :tag => s.version.to_s } 9 | 10 | s.source_files = 'Sources/PublishedObject/*.swift' 11 | 12 | s.swift_version = '5.2' 13 | s.ios.deployment_target = '11.0' 14 | s.tvos.deployment_target = '11.0' 15 | s.osx.deployment_target = '10.13' 16 | end 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PublishedObject 2 | 3 | A property wrapper that forwards the objectWillChange of the wrapped ObservableObject to the enclosing ObservableObject's objectWillChange. 4 | 5 | 6 | Just like @Published this sends willSet events to the enclosing ObservableObject's ObjectWillChangePublisher but unlike @Published it also sends the wrapped value's published changes on to the enclosing ObservableObject. 7 | 8 | ```swift 9 | class Outer: ObservableObject { 10 | @PublishedObject var innerPublishedObject: Inner 11 | @Published var innerPublished: Inner 12 | 13 | init(_ value: Int) { 14 | self.innerPublishedObject = Inner(value) 15 | self.innerPublished = Inner(value) 16 | } 17 | } 18 | 19 | class Inner: ObservableObject { 20 | @Published var value: Int 21 | 22 | init(_ int: Int) { 23 | self.value = int 24 | } 25 | } 26 | 27 | func example() { 28 | let outer = Outer(1) 29 | 30 | // Setting property on Outer (This will send an update with either @Published or @PublishedObject) 31 | outer.innerPublishedObject = Inner(2) // outer.objectWillChange will be called 32 | outer.innerPublished = Inner(2) // outer.objectWillChange will be called 33 | 34 | // Setting property on Inner (This will only send an update when using @PublishedObject) 35 | outer.innerPublishedObject.value = 3 // outer.objectWillChange will be called !!! 36 | outer.innerPublished.value = 3 // outer.objectWillChange will NOT be called 37 | } 38 | ``` 39 | 40 | It's only one file so you could just copy it. Also has Swift Package Manager support. 41 | -------------------------------------------------------------------------------- /Sources/PublishedObject/PublishedObject.swift: -------------------------------------------------------------------------------- 1 | // https://github.com/Amzd/PublishedObject 2 | // https://twitter.com/Amzdme 3 | 4 | import Combine 5 | import Foundation 6 | 7 | /// Just like @Published this sends willSet events to the enclosing ObservableObject's ObjectWillChangePublisher 8 | /// but unlike @Published it also sends the wrapped value's published changes on to the enclosing ObservableObject 9 | @propertyWrapper @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 10 | public struct PublishedObject { 11 | 12 | public init(wrappedValue: Value) where Value: ObservableObject, Value.ObjectWillChangePublisher == ObservableObjectPublisher { 13 | self.wrappedValue = wrappedValue 14 | self.cancellable = nil 15 | _startListening = { futureSelf, wrappedValue in 16 | let publisher = futureSelf._projectedValue 17 | let parent = futureSelf.parent 18 | futureSelf.cancellable = wrappedValue.objectWillChange.sink { [parent] in 19 | parent.objectWillChange?() 20 | DispatchQueue.main.async { 21 | publisher.send(wrappedValue) 22 | } 23 | } 24 | publisher.send(wrappedValue) 25 | } 26 | startListening(to: wrappedValue) 27 | } 28 | 29 | public init(wrappedValue: V?) where V? == Value, V: ObservableObject, V.ObjectWillChangePublisher == ObservableObjectPublisher { 30 | self.wrappedValue = wrappedValue 31 | self.cancellable = nil 32 | _startListening = { futureSelf, wrappedValue in 33 | let publisher = futureSelf._projectedValue 34 | let parent = futureSelf.parent 35 | futureSelf.cancellable = wrappedValue?.objectWillChange.sink { [parent] in 36 | parent.objectWillChange?() 37 | DispatchQueue.main.async { 38 | publisher.send(wrappedValue) 39 | } 40 | } 41 | publisher.send(wrappedValue) 42 | } 43 | startListening(to: wrappedValue) 44 | } 45 | 46 | public init(wrappedValue: Value) where Value == [V], V: ObservableObject, V.ObjectWillChangePublisher == ObservableObjectPublisher { 47 | self.wrappedValue = wrappedValue 48 | self.cancellable = nil 49 | _startListening = { futureSelf, wrappedValue in 50 | let publisher = futureSelf._projectedValue 51 | let parent = futureSelf.parent 52 | for element in wrappedValue { 53 | futureSelf.cancellable = element.objectWillChange.sink { [parent] in 54 | parent.objectWillChange?() 55 | DispatchQueue.main.async { 56 | publisher.send(wrappedValue) 57 | } 58 | } 59 | } 60 | publisher.send(wrappedValue) 61 | } 62 | startListening(to: wrappedValue) 63 | } 64 | 65 | public var wrappedValue: Value { 66 | willSet { parent.objectWillChange?() } 67 | didSet { startListening(to: wrappedValue) } 68 | } 69 | 70 | public static subscript( 71 | _enclosingInstance observed: EnclosingSelf, 72 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 73 | storage storageKeyPath: ReferenceWritableKeyPath 74 | ) -> Value where EnclosingSelf.ObjectWillChangePublisher == ObservableObjectPublisher { 75 | get { 76 | observed[keyPath: storageKeyPath].setParent(observed) 77 | return observed[keyPath: storageKeyPath].wrappedValue 78 | } 79 | set { 80 | observed[keyPath: storageKeyPath].setParent(observed) 81 | observed[keyPath: storageKeyPath].wrappedValue = newValue 82 | } 83 | } 84 | 85 | public static subscript( 86 | _enclosingInstance observed: EnclosingSelf, 87 | projected wrappedKeyPath: KeyPath, 88 | storage storageKeyPath: ReferenceWritableKeyPath 89 | ) -> Publisher where EnclosingSelf.ObjectWillChangePublisher == ObservableObjectPublisher { 90 | observed[keyPath: storageKeyPath].setParent(observed) 91 | return observed[keyPath: storageKeyPath].projectedValue 92 | } 93 | 94 | private let parent = Holder() 95 | private var cancellable: AnyCancellable? 96 | private class Holder { 97 | var objectWillChange: (() -> Void)? 98 | init() {} 99 | } 100 | 101 | private func setParent(_ parentObject: Parent) where Parent.ObjectWillChangePublisher == ObservableObjectPublisher { 102 | guard parent.objectWillChange == nil else { return } 103 | parent.objectWillChange = { [weak parentObject] in 104 | parentObject?.objectWillChange.send() 105 | } 106 | } 107 | 108 | private var _startListening: (inout Self, _ toValue: Value) -> Void 109 | private mutating func startListening(to wrappedValue: Value) { 110 | _startListening(&self, wrappedValue) 111 | } 112 | 113 | public typealias Publisher = AnyPublisher 114 | 115 | private lazy var _projectedValue = CurrentValueSubject(wrappedValue) 116 | public var projectedValue: Publisher { 117 | mutating get { _projectedValue.eraseToAnyPublisher() } 118 | } 119 | } 120 | 121 | #if FORCE_PUBLISHED_OBJECT_WRAPPER 122 | /// Force PublishedObject when using ObservableObjects 123 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 124 | extension Published { 125 | public init(wrappedValue: Value) where Value: ObservableObject { 126 | fatalError("Use @PublishedObject with ObservableObjects") 127 | } 128 | public init(wrappedValue: Value) where Value: _PublishedObjectInternalObservableObjectOptional { 129 | fatalError("Use @PublishedObject with optional ObservableObjects") 130 | } 131 | public init(wrappedValue: Value) where Value: _PublishedObjectInternalObservableObjectArray { 132 | fatalError("Use @PublishedObject with arrays of ObservableObjects") 133 | } 134 | } 135 | 136 | public protocol _PublishedObjectInternalObservableObjectOptional {} 137 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 138 | extension Optional: _PublishedObjectInternalObservableObjectOptional where Wrapped: ObservableObject {} 139 | public protocol _PublishedObjectInternalObservableObjectArray {} 140 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 141 | extension Array: _PublishedObjectInternalObservableObjectArray where Element: ObservableObject {} 142 | #endif 143 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import PublishedObjectTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += PublishedObjectTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/PublishedObjectTests/PublishedObjectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Combine 3 | @testable import PublishedObject 4 | 5 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 6 | class Outer: ObservableObject { 7 | @PublishedObject var innerPublishedObject: Inner 8 | @Published var innerPublished: Inner 9 | 10 | init(_ value: Int) { 11 | self.innerPublishedObject = Inner(value) 12 | self.innerPublished = Inner(value) 13 | } 14 | } 15 | 16 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 17 | class Inner: ObservableObject { 18 | @Published var value: Int 19 | 20 | init(_ int: Int) { 21 | self.value = int 22 | } 23 | } 24 | 25 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 26 | class OuterOptional: ObservableObject { 27 | @PublishedObject var innerPublishedObject: Inner? 28 | @Published var innerPublished: Inner? 29 | 30 | init(_ value: Int?) { 31 | self.innerPublishedObject = value.map(Inner.init) 32 | self.innerPublished = value.map(Inner.init) 33 | } 34 | } 35 | 36 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 37 | class OuterArray: ObservableObject { 38 | @PublishedObject var innerPublishedObject: [Inner] 39 | @Published var innerPublished: [Inner] 40 | 41 | init(_ value: Int) { 42 | self.innerPublishedObject = [Inner(value)] 43 | self.innerPublished = [Inner(value)] 44 | } 45 | } 46 | 47 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 48 | final class PublishedObjectTests: XCTestCase { 49 | var cancellables: Set = [] 50 | 51 | var outer: Outer! = Outer(1) 52 | 53 | func testObjectWillChange() { 54 | // Setting property on Outer (This will send an update with either @Published or @PublishedObject) 55 | 56 | let exp1 = XCTestExpectation(description: "outer.objectWillChange will be called") 57 | outer.objectWillChange.first().sink { exp1.fulfill() } .store(in: &cancellables) 58 | outer.innerPublishedObject = Inner(2) 59 | wait(for: [exp1], timeout: 0.1) 60 | 61 | let exp2 = XCTestExpectation(description: "outer.objectWillChange will be called") 62 | outer.objectWillChange.first().sink { exp2.fulfill() } .store(in: &cancellables) 63 | outer.innerPublished = Inner(2) 64 | wait(for: [exp2], timeout: 0.1) 65 | 66 | // Setting property on Inner (This will only send an update when using @PublishedObject) 67 | 68 | let exp3 = XCTestExpectation(description: "outer.objectWillChange will be called") 69 | outer.objectWillChange.first().sink { exp3.fulfill() } .store(in: &cancellables) 70 | outer.innerPublishedObject.value = 3 71 | wait(for: [exp3], timeout: 0.1) 72 | 73 | let exp4 = XCTestExpectation(description: "outer.objectWillChange will NOT be called") 74 | exp4.isInverted = true 75 | outer.objectWillChange.first().sink { exp4.fulfill() } .store(in: &cancellables) 76 | outer.innerPublished.value = 3 77 | wait(for: [exp4], timeout: 0.1) 78 | } 79 | 80 | func testProjectedValue() { 81 | 82 | // Initial value 83 | 84 | let exp0 = XCTestExpectation(description: "projectedValue will be called") 85 | outer.$innerPublishedObject.first().sink { inner in 86 | if inner.value == 1 { 87 | exp0.fulfill() 88 | } 89 | }.store(in: &cancellables) 90 | wait(for: [exp0], timeout: 0.1) 91 | 92 | let exp1 = XCTestExpectation(description: "projectedValue will be called") 93 | outer.$innerPublished.first().sink { inner in 94 | if inner.value == 1 { 95 | exp1.fulfill() 96 | } 97 | }.store(in: &cancellables) 98 | wait(for: [exp1], timeout: 0.1) 99 | 100 | 101 | // Setting property on Outer (This will send an update with either @Published or @PublishedObject) 102 | 103 | let exp2 = XCTestExpectation(description: "projectedValue will be called") 104 | outer.$innerPublishedObject.dropFirst().first().sink { inner in 105 | if inner.value == 2 { 106 | exp2.fulfill() 107 | } 108 | }.store(in: &cancellables) 109 | outer.innerPublishedObject = Inner(2) 110 | wait(for: [exp2], timeout: 0.1) 111 | 112 | let exp3 = XCTestExpectation(description: "projectedValue will be called") 113 | outer.$innerPublished.dropFirst().first().sink { inner in 114 | if inner.value == 2 { 115 | exp3.fulfill() 116 | } 117 | }.store(in: &cancellables) 118 | outer.innerPublished = Inner(2) 119 | wait(for: [exp3], timeout: 0.1) 120 | 121 | // Setting property on Inner (This will only send an update when using @PublishedObject) 122 | 123 | let exp4 = XCTestExpectation(description: "projectedValue will be called") 124 | outer.$innerPublishedObject.dropFirst().first().sink { inner in 125 | if inner.value == 3 { 126 | exp4.fulfill() 127 | } 128 | }.store(in: &cancellables) 129 | outer.innerPublishedObject.value = 3 130 | wait(for: [exp4], timeout: 0.1) 131 | 132 | let exp5 = XCTestExpectation(description: "projectedValue will NOT be called") 133 | exp5.isInverted = true 134 | outer.$innerPublished.dropFirst().first().sink { inner in 135 | if inner.value == 3 { 136 | exp5.fulfill() 137 | } 138 | }.store(in: &cancellables) 139 | outer.innerPublished.value = 3 140 | wait(for: [exp5], timeout: 0.1) 141 | } 142 | 143 | func testOptionalWithValue() throws { 144 | let outer = OuterOptional(1) 145 | 146 | // Setting property on Inner from the optional init (This will only send an update when using @PublishedObject) 147 | 148 | let exp5 = XCTestExpectation(description: "outer.objectWillChange will be called") 149 | outer.objectWillChange.first().sink { exp5.fulfill() } .store(in: &cancellables) 150 | outer.innerPublishedObject?.value = 3 151 | wait(for: [exp5], timeout: 0.1) 152 | 153 | let exp6 = XCTestExpectation(description: "outer.objectWillChange will NOT be called") 154 | exp6.isInverted = true 155 | outer.objectWillChange.first().sink { exp6.fulfill() } .store(in: &cancellables) 156 | outer.innerPublished?.value = 3 157 | wait(for: [exp6], timeout: 0.1) 158 | 159 | // Setting property on Outer (This will send an update with either @Published or @PublishedObject) 160 | 161 | let exp1 = XCTestExpectation(description: "outer.objectWillChange will be called") 162 | outer.objectWillChange.first().sink { exp1.fulfill() } .store(in: &cancellables) 163 | outer.innerPublishedObject = Inner(2) 164 | wait(for: [exp1], timeout: 0.1) 165 | 166 | let exp2 = XCTestExpectation(description: "outer.objectWillChange will be called") 167 | outer.objectWillChange.first().sink { exp2.fulfill() } .store(in: &cancellables) 168 | outer.innerPublished = Inner(2) 169 | wait(for: [exp2], timeout: 0.1) 170 | 171 | // Setting property on Inner (This will only send an update when using @PublishedObject) 172 | 173 | let exp3 = XCTestExpectation(description: "outer.objectWillChange will be called") 174 | outer.objectWillChange.first().sink { exp3.fulfill() } .store(in: &cancellables) 175 | outer.innerPublishedObject?.value = 3 176 | wait(for: [exp3], timeout: 0.1) 177 | 178 | let exp4 = XCTestExpectation(description: "outer.objectWillChange will NOT be called") 179 | exp4.isInverted = true 180 | outer.objectWillChange.first().sink { exp4.fulfill() } .store(in: &cancellables) 181 | outer.innerPublished?.value = 3 182 | wait(for: [exp4], timeout: 0.1) 183 | } 184 | 185 | func testOptionalWithoutValue() throws { 186 | let outer = OuterOptional(nil) 187 | 188 | // Setting property on Inner while it is nil 189 | // (this should never call objectWillChange because the Inner obj is not there so nothing is changed) 190 | 191 | let exp5 = XCTestExpectation(description: "uhhhhm") 192 | exp5.isInverted = true 193 | outer.objectWillChange.first().sink { exp5.fulfill() } .store(in: &cancellables) 194 | outer.innerPublishedObject?.value = 3 195 | wait(for: [exp5], timeout: 0.1) 196 | 197 | let exp6 = XCTestExpectation(description: "uhhhhm") 198 | exp6.isInverted = true 199 | outer.objectWillChange.first().sink { exp6.fulfill() } .store(in: &cancellables) 200 | outer.innerPublished?.value = 3 201 | wait(for: [exp6], timeout: 0.1) 202 | 203 | // Setting property on Outer (This will send an update with either @Published or @PublishedObject) 204 | 205 | let exp1 = XCTestExpectation(description: "outer.objectWillChange will be called") 206 | outer.objectWillChange.first().sink { exp1.fulfill() } .store(in: &cancellables) 207 | outer.innerPublishedObject = Inner(2) 208 | wait(for: [exp1], timeout: 0.1) 209 | 210 | let exp2 = XCTestExpectation(description: "outer.objectWillChange will be called") 211 | outer.objectWillChange.first().sink { exp2.fulfill() } .store(in: &cancellables) 212 | outer.innerPublished = Inner(2) 213 | wait(for: [exp2], timeout: 0.1) 214 | 215 | // Setting property on Inner (This will only send an update when using @PublishedObject) 216 | 217 | let exp3 = XCTestExpectation(description: "outer.objectWillChange will be called") 218 | outer.objectWillChange.first().sink { exp3.fulfill() } .store(in: &cancellables) 219 | outer.innerPublishedObject?.value = 3 220 | wait(for: [exp3], timeout: 0.1) 221 | 222 | let exp4 = XCTestExpectation(description: "outer.objectWillChange will NOT be called") 223 | exp4.isInverted = true 224 | outer.objectWillChange.first().sink { exp4.fulfill() } .store(in: &cancellables) 225 | outer.innerPublished?.value = 3 226 | wait(for: [exp4], timeout: 0.1) 227 | } 228 | 229 | func testArrayObjectWillChange() { 230 | let outer = OuterArray(1) 231 | 232 | // Setting property on Outer (This will send an update with either @Published or @PublishedObject) 233 | 234 | let exp1 = XCTestExpectation(description: "outer.objectWillChange will be called") 235 | outer.objectWillChange.first().sink { exp1.fulfill() } .store(in: &cancellables) 236 | outer.innerPublishedObject = [Inner(2)] 237 | wait(for: [exp1], timeout: 0.1) 238 | 239 | let exp2 = XCTestExpectation(description: "outer.objectWillChange will be called") 240 | outer.objectWillChange.first().sink { exp2.fulfill() } .store(in: &cancellables) 241 | outer.innerPublished = [Inner(2)] 242 | wait(for: [exp2], timeout: 0.1) 243 | 244 | // Setting property on Inner (This will only send an update when using @PublishedObject) 245 | 246 | let exp3 = XCTestExpectation(description: "outer.objectWillChange will be called") 247 | outer.objectWillChange.first().sink { exp3.fulfill() } .store(in: &cancellables) 248 | outer.innerPublishedObject[0].value = 3 249 | wait(for: [exp3], timeout: 0.1) 250 | 251 | let exp4 = XCTestExpectation(description: "outer.objectWillChange will NOT be called") 252 | exp4.isInverted = true 253 | outer.objectWillChange.first().sink { exp4.fulfill() } .store(in: &cancellables) 254 | outer.innerPublished[0].value = 3 255 | wait(for: [exp4], timeout: 0.1) 256 | } 257 | 258 | func testArrayProjectedValue() { 259 | let outer = OuterArray(1) 260 | 261 | // Initial value 262 | 263 | let exp0 = XCTestExpectation(description: "projectedValue will be called") 264 | outer.$innerPublishedObject.compactMap(\.first).first().sink { inner in 265 | if inner.value == 1 { 266 | exp0.fulfill() 267 | } 268 | }.store(in: &cancellables) 269 | wait(for: [exp0], timeout: 0.1) 270 | 271 | let exp1 = XCTestExpectation(description: "projectedValue will be called") 272 | outer.$innerPublished.compactMap(\.first).first().sink { inner in 273 | if inner.value == 1 { 274 | exp1.fulfill() 275 | } 276 | }.store(in: &cancellables) 277 | wait(for: [exp1], timeout: 0.1) 278 | 279 | 280 | // Setting property on Outer (This will send an update with either @Published or @PublishedObject) 281 | 282 | let exp2 = XCTestExpectation(description: "projectedValue will be called") 283 | outer.$innerPublishedObject.compactMap(\.first).dropFirst().first().sink { inner in 284 | if inner.value == 2 { 285 | exp2.fulfill() 286 | } 287 | }.store(in: &cancellables) 288 | outer.innerPublishedObject[0] = Inner(2) 289 | wait(for: [exp2], timeout: 0.1) 290 | 291 | let exp3 = XCTestExpectation(description: "projectedValue will be called") 292 | outer.$innerPublished.compactMap(\.first).dropFirst().first().sink { inner in 293 | if inner.value == 2 { 294 | exp3.fulfill() 295 | } 296 | }.store(in: &cancellables) 297 | outer.innerPublished[0] = Inner(2) 298 | wait(for: [exp3], timeout: 0.1) 299 | 300 | // Setting property on Inner (This will only send an update when using @PublishedObject) 301 | 302 | let exp4 = XCTestExpectation(description: "projectedValue will be called") 303 | outer.$innerPublishedObject.compactMap(\.first).dropFirst().first().sink { inner in 304 | if inner.value == 3 { 305 | exp4.fulfill() 306 | } 307 | }.store(in: &cancellables) 308 | outer.innerPublishedObject[0].value = 3 309 | wait(for: [exp4], timeout: 0.1) 310 | 311 | let exp5 = XCTestExpectation(description: "projectedValue will NOT be called") 312 | exp5.isInverted = true 313 | outer.$innerPublished.compactMap(\.first).dropFirst().first().sink { inner in 314 | if inner.value == 3 { 315 | exp5.fulfill() 316 | } 317 | }.store(in: &cancellables) 318 | outer.innerPublished[0].value = 3 319 | wait(for: [exp5], timeout: 0.1) 320 | } 321 | 322 | func testMemoryLeak() { 323 | weak var weakOuter = outer 324 | testObjectWillChange() 325 | outer = nil 326 | XCTAssertNil(weakOuter) 327 | outer = Outer(1) 328 | weakOuter = outer 329 | testProjectedValue() 330 | outer = nil 331 | XCTAssertNil(weakOuter) 332 | } 333 | 334 | static var allTests = [ 335 | ("testObjectWillChange", testObjectWillChange), 336 | ("testProjectedValue", testProjectedValue), 337 | ("testOptionalWithValue", testOptionalWithValue), 338 | ("testOptionalWithoutValue", testOptionalWithoutValue), 339 | ("testMemoryLeak", testMemoryLeak), 340 | ] 341 | } 342 | -------------------------------------------------------------------------------- /Tests/PublishedObjectTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(PublishedObjectTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------