Skip to content

Added assign(to: on: ownership:) operator #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CombineExt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
/* End PBXAggregateTarget section */

/* Begin PBXBuildFile section */
712E87BD2465DEDE00431F5C /* ObjectOwnership.swift in Sources */ = {isa = PBXBuildFile; fileRef = 712E87BC2465DEDE00431F5C /* ObjectOwnership.swift */; };
71E6F4EC24655F3A00FB4103 /* AssignOwnership.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E6F4EB24655F3A00FB4103 /* AssignOwnership.swift */; };
71E6F4EE2465616100FB4103 /* AssignOwnershipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E6F4ED2465616100FB4103 /* AssignOwnershipTests.swift */; };
78002BB5241E910C0018AA28 /* Relay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78002BB4241E910C0018AA28 /* Relay.swift */; };
78002BB7241E915E0018AA28 /* CurrentValueRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78002BB6241E915E0018AA28 /* CurrentValueRelay.swift */; };
78002BB9241E91D70018AA28 /* PassthroughRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78002BB8241E91D70018AA28 /* PassthroughRelay.swift */; };
Expand Down Expand Up @@ -76,6 +79,9 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
712E87BC2465DEDE00431F5C /* ObjectOwnership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectOwnership.swift; sourceTree = "<group>"; };
71E6F4EB24655F3A00FB4103 /* AssignOwnership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignOwnership.swift; sourceTree = "<group>"; };
71E6F4ED2465616100FB4103 /* AssignOwnershipTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignOwnershipTests.swift; sourceTree = "<group>"; };
78002BB4241E910C0018AA28 /* Relay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relay.swift; sourceTree = "<group>"; };
78002BB6241E915E0018AA28 /* CurrentValueRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValueRelay.swift; sourceTree = "<group>"; };
78002BB8241E91D70018AA28 /* PassthroughRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassthroughRelay.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -162,6 +168,7 @@
isa = PBXGroup;
children = (
78C193D6241C2E580001B7FD /* Event.swift */,
712E87BC2465DEDE00431F5C /* ObjectOwnership.swift */,
);
path = Models;
sourceTree = "<group>";
Expand All @@ -188,6 +195,7 @@
children = (
OBJ_12 /* WithLatestFromTests.swift */,
78AA9296241B8532009BD68B /* AssignToManyTests.swift */,
71E6F4ED2465616100FB4103 /* AssignOwnershipTests.swift */,
78C193D0241C1B450001B7FD /* FlatMapLatestTests.swift */,
78C193D8241CEEA80001B7FD /* CreateTests.swift */,
78C193DF241D4D8D0001B7FD /* MaterializeTests.swift */,
Expand Down Expand Up @@ -245,6 +253,7 @@
isa = PBXGroup;
children = (
OBJ_9 /* AssignToMany.swift */,
71E6F4EB24655F3A00FB4103 /* AssignOwnership.swift */,
OBJ_10 /* WithLatestFrom.swift */,
78C193CE241C16C40001B7FD /* FlatMapLatest.swift */,
78C193D3241C2DE00001B7FD /* Create.swift */,
Expand Down Expand Up @@ -380,11 +389,13 @@
78988A23241FFE2400F3A4AF /* Partition.swift in Sources */,
BF8121BC241FF42C006A93B8 /* ZipMany.swift in Sources */,
BF84B7412426B786001BFA88 /* RemoveAllDuplicates.swift in Sources */,
71E6F4EC24655F3A00FB4103 /* AssignOwnership.swift in Sources */,
78C193D4241C2DE00001B7FD /* Create.swift in Sources */,
OBJ_22 /* AssignToMany.swift in Sources */,
BF8EDF4C2453529000B0CC75 /* PrefixDuration.swift in Sources */,
BF9D85D32444BB92001783E6 /* ReplaySubject.swift in Sources */,
AAEAF0E72436D346007C35E0 /* SetOutputType.swift in Sources */,
712E87BD2465DEDE00431F5C /* ObjectOwnership.swift in Sources */,
78C193D7241C2E580001B7FD /* Event.swift in Sources */,
OBJ_23 /* WithLatestFrom.swift in Sources */,
BFB4EA132428256B0096E9E9 /* CombineLatestMany.swift in Sources */,
Expand All @@ -407,6 +418,7 @@
78C193D2241C1B750001B7FD /* FlatMapLatestTests.swift in Sources */,
78AA9297241B8532009BD68B /* AssignToManyTests.swift in Sources */,
BFB4EA1524283ECF0096E9E9 /* CombineLatestManyTests.swift in Sources */,
71E6F4EE2465616100FB4103 /* AssignOwnershipTests.swift in Sources */,
BF9D85D52444D12F001783E6 /* ReplaySubjectTests.swift in Sources */,
AAEAF0E92436D785007C35E0 /* SetOutputTypeTests.swift in Sources */,
78988A25241FFE2E00F3A4AF /* PartitionTests.swift in Sources */,
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,20 @@ var text: UITextField
and: \.text, on: text)
```

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`.

```swift
// Retain `self` strongly
subscription = subject.assign(to: \.value, on: self)
subscription = subject.assign(to: \.value, on: self, ownership: .strong)

// Use a `weak` reference to `self`
subscription = subject.assign(to: \.value, on: self, ownership: .weak)

// Use an `unowned` reference to `self`
subscription = subject.assign(to: \.value, on: self, ownership: .unowned)
```

------

### amb
Expand Down
26 changes: 26 additions & 0 deletions Sources/Models/ObjectOwnership.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// ObjectOwnership.swift
// CombineExt
//
// Created by Dmitry Kuznetsov on 08/05/2020.
// Copyright © 2020 Combine Community. All rights reserved.
//

import Foundation

/// The ownership of an object
///
/// - seealso: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID52
public enum ObjectOwnership {
/// Keep a strong hold of the object, preventing ARC
/// from disposing it until its released or has no references.
case strong

/// Weakly owned. Does not keep a strong hold of the object,
/// allowing ARC to dispose it even if its referenced.
case weak

/// Unowned. Similar to weak, but implicitly unwrapped so may
/// crash if the object is released beore being accessed.
case unowned
}
113 changes: 113 additions & 0 deletions Sources/Operators/AssignOwnership.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// AssignOwnership.swift
// CombineExt
//
// Created by Dmitry Kuznetsov on 08/05/2020.
// Copyright © 2020 Combine Community. All rights reserved.
//

#if canImport(Combine)
import Combine

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension Publisher where Self.Failure == Never {
/// Assigns a publisher’s output to a property of an object.
///
/// - parameter keyPath: A key path that indicates the property to assign.
/// - parameter object: The object that contains the property.
/// The subscriber assigns the object’s property every time
/// it receives a new value.
/// - parameter ownership: The retainment / ownership strategy for the object, defaults to `strong`.
///
/// - returns: An AnyCancellable instance. Call cancel() on this instance when you no longer want
/// the publisher to automatically assign the property. Deinitializing this instance
/// will also cancel automatic assignment.
func assign<Root: AnyObject>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>,
on object: Root,
ownership: ObjectOwnership = .strong) -> AnyCancellable {
switch ownership {
case .strong:
return assign(to: keyPath, on: object)
case .weak:
return sink { [weak object] value in
object?[keyPath: keyPath] = value
}
case .unowned:
return sink { [unowned object] value in
object[keyPath: keyPath] = value
}
}
}

/// Assigns each element from a Publisher to properties of the provided objects
///
/// - Parameters:
/// - keyPath1: The key path of the first property to assign.
/// - object1: The first object on which to assign the value.
/// - keyPath2: The key path of the second property to assign.
/// - object2: The second object on which to assign the value.
/// - ownership: The retainment / ownership strategy for the object, defaults to `strong`.
///
/// - Returns: A cancellable instance; used when you end assignment of the received value.
/// Deallocation of the result will tear down the subscription stream.
func assign<Root1: AnyObject, Root2: AnyObject>(
to keyPath1: ReferenceWritableKeyPath<Root1, Output>, on object1: Root1,
and keyPath2: ReferenceWritableKeyPath<Root2, Output>, on object2: Root2,
ownership: ObjectOwnership = .strong
) -> AnyCancellable {
switch ownership {
case .strong:
return assign(to: keyPath1, on: object1, and: keyPath2, on: object2)
case .weak:
return sink { [weak object1, weak object2] value in
object1?[keyPath: keyPath1] = value
object2?[keyPath: keyPath2] = value
}
case .unowned:
return sink { [unowned object1, unowned object2] value in
object1[keyPath: keyPath1] = value
object2[keyPath: keyPath2] = value
}
}
}

/// Assigns each element from a Publisher to properties of the provided objects
///
/// - Parameters:
/// - keyPath1: The key path of the first property to assign.
/// - object1: The first object on which to assign the value.
/// - keyPath2: The key path of the second property to assign.
/// - object2: The second object on which to assign the value.
/// - keyPath3: The key path of the third property to assign.
/// - object3: The third object on which to assign the value.
/// - ownership: The retainment / ownership strategy for the object, defaults to `strong`.
///
/// - Returns: A cancellable instance; used when you end assignment of the received value.
/// Deallocation of the result will tear down the subscription stream.
func assign<Root1: AnyObject, Root2: AnyObject, Root3: AnyObject>(
to keyPath1: ReferenceWritableKeyPath<Root1, Output>, on object1: Root1,
and keyPath2: ReferenceWritableKeyPath<Root2, Output>, on object2: Root2,
and keyPath3: ReferenceWritableKeyPath<Root3, Output>, on object3: Root3,
ownership: ObjectOwnership = .strong
) -> AnyCancellable {
switch ownership {
case .strong:
return assign(to: keyPath1, on: object1,
and: keyPath2, on: object2,
and: keyPath3, on: object3)
case .weak:
return sink { [weak object1, weak object2, weak object3] value in
object1?[keyPath: keyPath1] = value
object2?[keyPath: keyPath2] = value
object3?[keyPath: keyPath3] = value
}
case .unowned:
return sink { [unowned object1, unowned object2, unowned object3] value in
object1[keyPath: keyPath1] = value
object2[keyPath: keyPath2] = value
object3[keyPath: keyPath3] = value
}
}
}
}
#endif
4 changes: 2 additions & 2 deletions Sources/Operators/AssignToMany.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Combine

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

/// Assigns each element from a Publisher to properties of the provided object
/// Assigns each element from a Publisher to properties of the provided objects
///
/// - Parameters:
/// - keyPath1: The key path of the first property to assign.
Expand Down
98 changes: 98 additions & 0 deletions Tests/AssignOwnershipTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// AssignOwnershipTests.swift
// CombineExt
//
// Created by Dmitry Kuznetsov on 08/05/2020.
// Copyright © 2020 Combine Community. All rights reserved.
//

#if !os(watchOS)
import XCTest
import Combine
import CombineExt

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
class AssignOwnershipTests: XCTestCase {
var subscription: AnyCancellable!
var value1 = 0
var value2 = 0
var value3 = 0
var subject: PassthroughSubject<Int, Never>!

override func setUp() {
super.setUp()

subscription = nil
subject = PassthroughSubject<Int, Never>()
value1 = 0
value2 = 0
value3 = 0
}

func testWeakOwnership() {
let initialRetainCount = CFGetRetainCount(self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh cool, great idea moving these here


subscription = subject
.assign(to: \.value1, on: self, ownership: .weak)
subject.send(10)
let resultRetainCount1 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount, resultRetainCount1)

subscription = subject
.assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .weak)
subject.send(15)
let resultRetainCount2 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount, resultRetainCount2)

subscription = subject
.assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .weak)
subject.send(20)
let resultRetainCount3 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount, resultRetainCount3)
}

func testUnownedOwnership() {
let initialRetainCount = CFGetRetainCount(self)

subscription = subject
.assign(to: \.value1, on: self, ownership: .unowned)
subject.send(10)
let resultRetainCount1 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount, resultRetainCount1)

subscription = subject
.assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .unowned)
subject.send(15)
let resultRetainCount2 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount, resultRetainCount2)

subscription = subject
.assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .unowned)
subject.send(20)
let resultRetainCount3 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount, resultRetainCount3)
}

func testStrongOwnership() {
let initialRetainCount = CFGetRetainCount(self)

subscription = subject
.assign(to: \.value1, on: self, ownership: .strong)
subject.send(10)
let resultRetainCount1 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount + 1, resultRetainCount1)

subscription = subject
.assign(to: \.value1, on: self, and: \.value2, on: self, ownership: .strong)
subject.send(15)
let resultRetainCount2 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount + 2, resultRetainCount2)

subscription = subject
.assign(to: \.value1, on: self, and: \.value2, on: self, and: \.value3, on: self, ownership: .strong)
subject.send(20)
let resultRetainCount3 = CFGetRetainCount(self)
XCTAssertEqual(initialRetainCount + 3, resultRetainCount3)
}
}
#endif
Loading