Skip to content

Commit a74c55a

Browse files
authored
Added assign(to: on: ownership:) operator (#30)
1 parent 485d80e commit a74c55a

File tree

7 files changed

+339
-45
lines changed

7 files changed

+339
-45
lines changed

CombineExt.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
/* End PBXAggregateTarget section */
2222

2323
/* Begin PBXBuildFile section */
24+
712E87BD2465DEDE00431F5C /* ObjectOwnership.swift in Sources */ = {isa = PBXBuildFile; fileRef = 712E87BC2465DEDE00431F5C /* ObjectOwnership.swift */; };
25+
71E6F4EC24655F3A00FB4103 /* AssignOwnership.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E6F4EB24655F3A00FB4103 /* AssignOwnership.swift */; };
26+
71E6F4EE2465616100FB4103 /* AssignOwnershipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E6F4ED2465616100FB4103 /* AssignOwnershipTests.swift */; };
2427
78002BB5241E910C0018AA28 /* Relay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78002BB4241E910C0018AA28 /* Relay.swift */; };
2528
78002BB7241E915E0018AA28 /* CurrentValueRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78002BB6241E915E0018AA28 /* CurrentValueRelay.swift */; };
2629
78002BB9241E91D70018AA28 /* PassthroughRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78002BB8241E91D70018AA28 /* PassthroughRelay.swift */; };
@@ -76,6 +79,9 @@
7679
/* End PBXContainerItemProxy section */
7780

7881
/* Begin PBXFileReference section */
82+
712E87BC2465DEDE00431F5C /* ObjectOwnership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectOwnership.swift; sourceTree = "<group>"; };
83+
71E6F4EB24655F3A00FB4103 /* AssignOwnership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignOwnership.swift; sourceTree = "<group>"; };
84+
71E6F4ED2465616100FB4103 /* AssignOwnershipTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignOwnershipTests.swift; sourceTree = "<group>"; };
7985
78002BB4241E910C0018AA28 /* Relay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relay.swift; sourceTree = "<group>"; };
8086
78002BB6241E915E0018AA28 /* CurrentValueRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValueRelay.swift; sourceTree = "<group>"; };
8187
78002BB8241E91D70018AA28 /* PassthroughRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassthroughRelay.swift; sourceTree = "<group>"; };
@@ -162,6 +168,7 @@
162168
isa = PBXGroup;
163169
children = (
164170
78C193D6241C2E580001B7FD /* Event.swift */,
171+
712E87BC2465DEDE00431F5C /* ObjectOwnership.swift */,
165172
);
166173
path = Models;
167174
sourceTree = "<group>";
@@ -188,6 +195,7 @@
188195
children = (
189196
OBJ_12 /* WithLatestFromTests.swift */,
190197
78AA9296241B8532009BD68B /* AssignToManyTests.swift */,
198+
71E6F4ED2465616100FB4103 /* AssignOwnershipTests.swift */,
191199
78C193D0241C1B450001B7FD /* FlatMapLatestTests.swift */,
192200
78C193D8241CEEA80001B7FD /* CreateTests.swift */,
193201
78C193DF241D4D8D0001B7FD /* MaterializeTests.swift */,
@@ -245,6 +253,7 @@
245253
isa = PBXGroup;
246254
children = (
247255
OBJ_9 /* AssignToMany.swift */,
256+
71E6F4EB24655F3A00FB4103 /* AssignOwnership.swift */,
248257
OBJ_10 /* WithLatestFrom.swift */,
249258
78C193CE241C16C40001B7FD /* FlatMapLatest.swift */,
250259
78C193D3241C2DE00001B7FD /* Create.swift */,
@@ -380,11 +389,13 @@
380389
78988A23241FFE2400F3A4AF /* Partition.swift in Sources */,
381390
BF8121BC241FF42C006A93B8 /* ZipMany.swift in Sources */,
382391
BF84B7412426B786001BFA88 /* RemoveAllDuplicates.swift in Sources */,
392+
71E6F4EC24655F3A00FB4103 /* AssignOwnership.swift in Sources */,
383393
78C193D4241C2DE00001B7FD /* Create.swift in Sources */,
384394
OBJ_22 /* AssignToMany.swift in Sources */,
385395
BF8EDF4C2453529000B0CC75 /* PrefixDuration.swift in Sources */,
386396
BF9D85D32444BB92001783E6 /* ReplaySubject.swift in Sources */,
387397
AAEAF0E72436D346007C35E0 /* SetOutputType.swift in Sources */,
398+
712E87BD2465DEDE00431F5C /* ObjectOwnership.swift in Sources */,
388399
78C193D7241C2E580001B7FD /* Event.swift in Sources */,
389400
OBJ_23 /* WithLatestFrom.swift in Sources */,
390401
BFB4EA132428256B0096E9E9 /* CombineLatestMany.swift in Sources */,
@@ -407,6 +418,7 @@
407418
78C193D2241C1B750001B7FD /* FlatMapLatestTests.swift in Sources */,
408419
78AA9297241B8532009BD68B /* AssignToManyTests.swift in Sources */,
409420
BFB4EA1524283ECF0096E9E9 /* CombineLatestManyTests.swift in Sources */,
421+
71E6F4EE2465616100FB4103 /* AssignOwnershipTests.swift in Sources */,
410422
BF9D85D52444D12F001783E6 /* ReplaySubjectTests.swift in Sources */,
411423
AAEAF0E92436D785007C35E0 /* SetOutputTypeTests.swift in Sources */,
412424
78988A25241FFE2E00F3A4AF /* PartitionTests.swift in Sources */,

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ var text: UITextField
143143
and: \.text, on: text)
144144
```
145145

146+
CombineExt provides an additional overload — `assign(to:on​:ownership)` — which lets you specify the kind of ownersip you want for your assign operation: `strong`, `weak` or `unowned`.
147+
148+
```swift
149+
// Retain `self` strongly
150+
subscription = subject.assign(to: \.value, on: self)
151+
subscription = subject.assign(to: \.value, on: self, ownership: .strong)
152+
153+
// Use a `weak` reference to `self`
154+
subscription = subject.assign(to: \.value, on: self, ownership: .weak)
155+
156+
// Use an `unowned` reference to `self`
157+
subscription = subject.assign(to: \.value, on: self, ownership: .unowned)
158+
```
159+
146160
------
147161

148162
### amb
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// ObjectOwnership.swift
3+
// CombineExt
4+
//
5+
// Created by Dmitry Kuznetsov on 08/05/2020.
6+
// Copyright © 2020 Combine Community. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// The ownership of an object
12+
///
13+
/// - seealso: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID52
14+
public enum ObjectOwnership {
15+
/// Keep a strong hold of the object, preventing ARC
16+
/// from disposing it until its released or has no references.
17+
case strong
18+
19+
/// Weakly owned. Does not keep a strong hold of the object,
20+
/// allowing ARC to dispose it even if its referenced.
21+
case weak
22+
23+
/// Unowned. Similar to weak, but implicitly unwrapped so may
24+
/// crash if the object is released beore being accessed.
25+
case unowned
26+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//
2+
// AssignOwnership.swift
3+
// CombineExt
4+
//
5+
// Created by Dmitry Kuznetsov on 08/05/2020.
6+
// Copyright © 2020 Combine Community. All rights reserved.
7+
//
8+
9+
#if canImport(Combine)
10+
import Combine
11+
12+
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
13+
public extension Publisher where Self.Failure == Never {
14+
/// Assigns a publisher’s output to a property of an object.
15+
///
16+
/// - parameter keyPath: A key path that indicates the property to assign.
17+
/// - parameter object: The object that contains the property.
18+
/// The subscriber assigns the object’s property every time
19+
/// it receives a new value.
20+
/// - parameter ownership: The retainment / ownership strategy for the object, defaults to `strong`.
21+
///
22+
/// - returns: An AnyCancellable instance. Call cancel() on this instance when you no longer want
23+
/// the publisher to automatically assign the property. Deinitializing this instance
24+
/// will also cancel automatic assignment.
25+
func assign<Root: AnyObject>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>,
26+
on object: Root,
27+
ownership: ObjectOwnership = .strong) -> AnyCancellable {
28+
switch ownership {
29+
case .strong:
30+
return assign(to: keyPath, on: object)
31+
case .weak:
32+
return sink { [weak object] value in
33+
object?[keyPath: keyPath] = value
34+
}
35+
case .unowned:
36+
return sink { [unowned object] value in
37+
object[keyPath: keyPath] = value
38+
}
39+
}
40+
}
41+
42+
/// Assigns each element from a Publisher to properties of the provided objects
43+
///
44+
/// - Parameters:
45+
/// - keyPath1: The key path of the first property to assign.
46+
/// - object1: The first object on which to assign the value.
47+
/// - keyPath2: The key path of the second property to assign.
48+
/// - object2: The second object on which to assign the value.
49+
/// - ownership: The retainment / ownership strategy for the object, defaults to `strong`.
50+
///
51+
/// - Returns: A cancellable instance; used when you end assignment of the received value.
52+
/// Deallocation of the result will tear down the subscription stream.
53+
func assign<Root1: AnyObject, Root2: AnyObject>(
54+
to keyPath1: ReferenceWritableKeyPath<Root1, Output>, on object1: Root1,
55+
and keyPath2: ReferenceWritableKeyPath<Root2, Output>, on object2: Root2,
56+
ownership: ObjectOwnership = .strong
57+
) -> AnyCancellable {
58+
switch ownership {
59+
case .strong:
60+
return assign(to: keyPath1, on: object1, and: keyPath2, on: object2)
61+
case .weak:
62+
return sink { [weak object1, weak object2] value in
63+
object1?[keyPath: keyPath1] = value
64+
object2?[keyPath: keyPath2] = value
65+
}
66+
case .unowned:
67+
return sink { [unowned object1, unowned object2] value in
68+
object1[keyPath: keyPath1] = value
69+
object2[keyPath: keyPath2] = value
70+
}
71+
}
72+
}
73+
74+
/// Assigns each element from a Publisher to properties of the provided objects
75+
///
76+
/// - Parameters:
77+
/// - keyPath1: The key path of the first property to assign.
78+
/// - object1: The first object on which to assign the value.
79+
/// - keyPath2: The key path of the second property to assign.
80+
/// - object2: The second object on which to assign the value.
81+
/// - keyPath3: The key path of the third property to assign.
82+
/// - object3: The third object on which to assign the value.
83+
/// - ownership: The retainment / ownership strategy for the object, defaults to `strong`.
84+
///
85+
/// - Returns: A cancellable instance; used when you end assignment of the received value.
86+
/// Deallocation of the result will tear down the subscription stream.
87+
func assign<Root1: AnyObject, Root2: AnyObject, Root3: AnyObject>(
88+
to keyPath1: ReferenceWritableKeyPath<Root1, Output>, on object1: Root1,
89+
and keyPath2: ReferenceWritableKeyPath<Root2, Output>, on object2: Root2,
90+
and keyPath3: ReferenceWritableKeyPath<Root3, Output>, on object3: Root3,
91+
ownership: ObjectOwnership = .strong
92+
) -> AnyCancellable {
93+
switch ownership {
94+
case .strong:
95+
return assign(to: keyPath1, on: object1,
96+
and: keyPath2, on: object2,
97+
and: keyPath3, on: object3)
98+
case .weak:
99+
return sink { [weak object1, weak object2, weak object3] value in
100+
object1?[keyPath: keyPath1] = value
101+
object2?[keyPath: keyPath2] = value
102+
object3?[keyPath: keyPath3] = value
103+
}
104+
case .unowned:
105+
return sink { [unowned object1, unowned object2, unowned object3] value in
106+
object1[keyPath: keyPath1] = value
107+
object2[keyPath: keyPath2] = value
108+
object3[keyPath: keyPath3] = value
109+
}
110+
}
111+
}
112+
}
113+
#endif

Sources/Operators/AssignToMany.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Combine
1111

1212
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
1313
public extension Publisher where Self.Failure == Never {
14-
/// Assigns each element from a Publisher to properties of the provided object
14+
/// Assigns each element from a Publisher to properties of the provided objects
1515
///
1616
/// - Parameters:
1717
/// - keyPath1: The key path of the first property to assign.
@@ -28,7 +28,7 @@ public extension Publisher where Self.Failure == Never {
2828
})
2929
}
3030

31-
/// Assigns each element from a Publisher to properties of the provided object
31+
/// Assigns each element from a Publisher to properties of the provided objects
3232
///
3333
/// - Parameters:
3434
/// - keyPath1: The key path of the first property to assign.

Tests/AssignOwnershipTests.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//
2+
// AssignOwnershipTests.swift
3+
// CombineExt
4+
//
5+
// Created by Dmitry Kuznetsov on 08/05/2020.
6+
// Copyright © 2020 Combine Community. All rights reserved.
7+
//
8+
9+
#if !os(watchOS)
10+
import XCTest
11+
import Combine
12+
import CombineExt
13+
14+
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
15+
class AssignOwnershipTests: XCTestCase {
16+
var subscription: AnyCancellable!
17+
var value1 = 0
18+
var value2 = 0
19+
var value3 = 0
20+
var subject: PassthroughSubject<Int, Never>!
21+
22+
override func setUp() {
23+
super.setUp()
24+
25+
subscription = nil
26+
subject = PassthroughSubject<Int, Never>()
27+
value1 = 0
28+
value2 = 0
29+
value3 = 0
30+
}
31+
32+
func testWeakOwnership() {
33+
let initialRetainCount = CFGetRetainCount(self)
34+
35+
subscription = subject
36+
.assign(to: \.value1, on: self, ownership: .weak)
37+
subject.send(10)
38+
let resultRetainCount1 = CFGetRetainCount(self)
39+
XCTAssertEqual(initialRetainCount, resultRetainCount1)
40+
41+
subscription = subject
42+
.assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .weak)
43+
subject.send(15)
44+
let resultRetainCount2 = CFGetRetainCount(self)
45+
XCTAssertEqual(initialRetainCount, resultRetainCount2)
46+
47+
subscription = subject
48+
.assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .weak)
49+
subject.send(20)
50+
let resultRetainCount3 = CFGetRetainCount(self)
51+
XCTAssertEqual(initialRetainCount, resultRetainCount3)
52+
}
53+
54+
func testUnownedOwnership() {
55+
let initialRetainCount = CFGetRetainCount(self)
56+
57+
subscription = subject
58+
.assign(to: \.value1, on: self, ownership: .unowned)
59+
subject.send(10)
60+
let resultRetainCount1 = CFGetRetainCount(self)
61+
XCTAssertEqual(initialRetainCount, resultRetainCount1)
62+
63+
subscription = subject
64+
.assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .unowned)
65+
subject.send(15)
66+
let resultRetainCount2 = CFGetRetainCount(self)
67+
XCTAssertEqual(initialRetainCount, resultRetainCount2)
68+
69+
subscription = subject
70+
.assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .unowned)
71+
subject.send(20)
72+
let resultRetainCount3 = CFGetRetainCount(self)
73+
XCTAssertEqual(initialRetainCount, resultRetainCount3)
74+
}
75+
76+
func testStrongOwnership() {
77+
let initialRetainCount = CFGetRetainCount(self)
78+
79+
subscription = subject
80+
.assign(to: \.value1, on: self, ownership: .strong)
81+
subject.send(10)
82+
let resultRetainCount1 = CFGetRetainCount(self)
83+
XCTAssertEqual(initialRetainCount + 1, resultRetainCount1)
84+
85+
subscription = subject
86+
.assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .strong)
87+
subject.send(15)
88+
let resultRetainCount2 = CFGetRetainCount(self)
89+
XCTAssertEqual(initialRetainCount + 2, resultRetainCount2)
90+
91+
subscription = subject
92+
.assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .strong)
93+
subject.send(20)
94+
let resultRetainCount3 = CFGetRetainCount(self)
95+
XCTAssertEqual(initialRetainCount + 3, resultRetainCount3)
96+
}
97+
}
98+
#endif

0 commit comments

Comments
 (0)