Skip to content

Commit

Permalink
Add withUnretained operator
Browse files Browse the repository at this point in the history
  • Loading branch information
dungntm58 committed Sep 2, 2021
1 parent 665fc63 commit 689f88a
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 0 deletions.
8 changes: 8 additions & 0 deletions CombineExt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
C387777F24E6BF8F00FAD2D8 /* NwiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C387777D24E6BF6C00FAD2D8 /* NwiseTests.swift */; };
D836234824EA9446002353AC /* MergeMany.swift in Sources */ = {isa = PBXBuildFile; fileRef = D836234724EA9446002353AC /* MergeMany.swift */; };
D836234A24EA9888002353AC /* MergeManyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D836234924EA9888002353AC /* MergeManyTests.swift */; };
E17B23B526DFBFBD008E595F /* WithUnretained.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17B23B426DFBFBD008E595F /* WithUnretained.swift */; };
E17B23B726DFFA56008E595F /* WithUnretainedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17B23B626DFFA56008E595F /* WithUnretainedTests.swift */; };
OBJ_100 /* ZipMany.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* ZipMany.swift */; };
OBJ_101 /* CurrentValueRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* CurrentValueRelay.swift */; };
OBJ_102 /* PassthroughRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_36 /* PassthroughRelay.swift */; };
Expand Down Expand Up @@ -122,6 +124,8 @@
"CombineExt::CombineExtTests::Product" /* CombineExtTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = CombineExtTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D836234724EA9446002353AC /* MergeMany.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergeMany.swift; sourceTree = "<group>"; };
D836234924EA9888002353AC /* MergeManyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergeManyTests.swift; sourceTree = "<group>"; };
E17B23B426DFBFBD008E595F /* WithUnretained.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithUnretained.swift; sourceTree = "<group>"; };
E17B23B626DFFA56008E595F /* WithUnretainedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithUnretainedTests.swift; sourceTree = "<group>"; };
OBJ_10 /* Sink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sink.swift; sourceTree = "<group>"; };
OBJ_12 /* Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Optional.swift; sourceTree = "<group>"; };
OBJ_14 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -261,6 +265,7 @@
OBJ_33 /* ZipMany.swift */,
1970A8A925246FBD00799AB6 /* FilterMany.swift */,
BFADDC8025BCE4C200465E9B /* FlatMapBatches.swift */,
E17B23B426DFBFBD008E595F /* WithUnretained.swift */,
);
path = Operators;
sourceTree = "<group>";
Expand Down Expand Up @@ -313,6 +318,7 @@
OBJ_60 /* WithLatestFromTests.swift */,
OBJ_61 /* ZipManyTests.swift */,
BFADDC8A25BCE91E00465E9B /* FlatMapBatchesTests.swift */,
E17B23B626DFFA56008E595F /* WithUnretainedTests.swift */,
);
path = Tests;
sourceTree = SOURCE_ROOT;
Expand Down Expand Up @@ -556,6 +562,7 @@
OBJ_126 /* CreateTests.swift in Sources */,
OBJ_127 /* CurrentValueRelayTests.swift in Sources */,
C387777F24E6BF8F00FAD2D8 /* NwiseTests.swift in Sources */,
E17B23B726DFFA56008E595F /* WithUnretainedTests.swift in Sources */,
OBJ_128 /* DematerializeTests.swift in Sources */,
OBJ_129 /* FlatMapLatestTests.swift in Sources */,
OBJ_130 /* MapManyTests.swift in Sources */,
Expand Down Expand Up @@ -587,6 +594,7 @@
OBJ_82 /* Event.swift in Sources */,
OBJ_83 /* ObjectOwnership.swift in Sources */,
OBJ_84 /* Amb.swift in Sources */,
E17B23B526DFBFBD008E595F /* WithUnretained.swift in Sources */,
OBJ_85 /* AssignOwnership.swift in Sources */,
OBJ_86 /* AssignToMany.swift in Sources */,
BF3D3B5D253B83F300D830ED /* IgnoreFailure.swift in Sources */,
Expand Down
112 changes: 112 additions & 0 deletions Sources/Operators/WithUnretained.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// WithUnretained.swift
// CombineExt
//
// Created by Robert on 01/09/2021.
// 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 {
/**
Provides an unretained, safe to use (i.e. not implicitly unwrapped), reference to an object along with the events published by the publisher.
In the case the provided object cannot be retained successfully, the publisher will complete.
- parameter obj: The object to provide an unretained reference on.
- parameter resultSelector: A function to combine the unretained referenced on `obj` and the value of the observable sequence.
- returns: A publisher that contains the result of `resultSelector` being called with an unretained reference on `obj` and the values of the upstream.
*/
func withUnretained<UnretainedObject: AnyObject, Output>(_ obj: UnretainedObject, resultSelector: @escaping (UnretainedObject, Self.Output) -> Output) -> Publishers.WithUnretained<UnretainedObject, Self, Output> {
Publishers.WithUnretained(unretainedObject: obj, upstream: self, resultSelector: resultSelector)
}

/**
Provides an unretained, safe to use (i.e. not implicitly unwrapped), reference to an object along with the events published by the publisher.
In the case the provided object cannot be retained successfully, the publisher will complete.
- parameter obj: The object to provide an unretained reference on.
- returns: A publisher that publishes a sequence of tuples that contains both an unretained reference on `obj` and the values of the upstream.
*/
func withUnretained<UnretainedObject: AnyObject>(_ obj: UnretainedObject) -> Publishers.WithUnretained<UnretainedObject, Self, (UnretainedObject, Output)> {
Publishers.WithUnretained(unretainedObject: obj, upstream: self) { ($0, $1) }
}

/// Attaches a subscriber with closure-based behavior.
///
/// Use ``Publisher/sink(unretainedObject:receiveCompletion:receiveValue:)`` to observe values received by the publisher and process them using a closure you specify.
/// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber.
/// The return value should be held, otherwise the stream will be canceled.
///
/// - parameter obj: The object to provide an unretained reference on.
/// - parameter receiveComplete: The closure to execute on completion.
/// - parameter receiveValue: The closure to execute on receipt of a value.
/// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream.
func sink<UnretainedObject: AnyObject>(unretainedObject obj: UnretainedObject, receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((UnretainedObject, Self.Output) -> Void)) -> AnyCancellable {
withUnretained(obj)
.sink(receiveCompletion: receiveCompletion, receiveValue: receiveValue)
}
}

// MARK: - Publisher
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension Publishers {
struct WithUnretained<UnretainedObject: AnyObject, Upstream: Publisher, Output>: Publisher {
public typealias Failure = Upstream.Failure

private weak var unretainedObject: UnretainedObject?
private let upstream: Upstream
private let resultSelector: (UnretainedObject, Upstream.Output) -> Output

public init(unretainedObject: UnretainedObject, upstream: Upstream, resultSelector: @escaping (UnretainedObject, Upstream.Output) -> Output) {
self.unretainedObject = unretainedObject
self.upstream = upstream
self.resultSelector = resultSelector
}

public func receive<S: Combine.Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
upstream.subscribe(Subscriber(unretainedObject: unretainedObject, downstream: subscriber, resultSelector: resultSelector))
}
}
}

// MARK: - Subscriber
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
private extension Publishers.WithUnretained {
class Subscriber<Downstream: Combine.Subscriber>: Combine.Subscriber where Downstream.Input == Output, Downstream.Failure == Failure {
typealias Input = Upstream.Output
typealias Failure = Downstream.Failure

private weak var unretainedObject: UnretainedObject?
private let downstream: Downstream
private let resultSelector: (UnretainedObject, Input) -> Output

init(unretainedObject: UnretainedObject?, downstream: Downstream, resultSelector: @escaping (UnretainedObject, Input) -> Output) {
self.unretainedObject = unretainedObject
self.downstream = downstream
self.resultSelector = resultSelector
}

func receive(subscription: Subscription) {
if unretainedObject == nil { return }
downstream.receive(subscription: subscription)
}

func receive(_ input: Input) -> Subscribers.Demand {
guard let unretainedObject = unretainedObject else { return .none }
return downstream.receive(resultSelector(unretainedObject, input))
}

func receive(completion: Subscribers.Completion<Failure>) {
if unretainedObject == nil {
return downstream.receive(completion: .finished)
}
downstream.receive(completion: completion)
}
}
}
#endif
140 changes: 140 additions & 0 deletions Tests/WithUnretainedTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//
// WithUnretainedTests.swift
// CombineExtTests
//
// Created by Robert on 02/09/2021.
//

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

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final class WithUnretainedTests: XCTestCase {
fileprivate var testClass: TestClass!
var subscription: AnyCancellable?
var values: [String] = []

enum WithUnretainedTestsError: Swift.Error {
case someError
}

override func setUp() {
super.setUp()

testClass = TestClass()
values = []
}

override func tearDown() {
subscription?.cancel()
subscription = nil
}

func testObjectAttached() {
let testClassId = testClass.id
var completed = false

let correctValues = [
"\(testClassId), 1",
"\(testClassId), 2",
"\(testClassId), 3",
"\(testClassId), 5",
"\(testClassId), 8"
]

let inputArr = [1, 2, 3, 5, 8]

subscription = Publishers.Sequence<[Int], WithUnretainedTestsError>(sequence: inputArr)
.withUnretained(self.testClass)
.map { "\($0.id), \($1)" }
.sink(receiveCompletion: { _ in completed = true },
receiveValue: { self.values.append($0) })

XCTAssertEqual(values, correctValues)
XCTAssertTrue(completed)
}

func testObjectDeallocatesWithEmptyPublisher() {
subscription = Empty<Int, WithUnretainedTestsError>()
.withUnretained(self.testClass)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })

// Confirm the object can be deallocated
XCTAssertTrue(testClass != nil)
testClass = nil
XCTAssertTrue(testClass == nil)
}

func testObjectDeallocates() {
let inputArr = [1, 2, 3, 5, 8]

subscription = Publishers.Sequence<[Int], WithUnretainedTestsError>(sequence: inputArr)
.withUnretained(self.testClass)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })

// Confirm the object can be deallocated
XCTAssertTrue(testClass != nil)
testClass = nil
XCTAssertTrue(testClass == nil)
}

func testObjectDeallocatesSequenceCompletes() {
let testClassId = testClass.id
var completed = false

let correctValues = [
"\(testClassId), 1",
"\(testClassId), 2",
"\(testClassId), 3"
]

let inputArr = [1, 2, 3]
subscription = Publishers.Sequence<[Int], WithUnretainedTestsError>(sequence: inputArr)
.withUnretained(self.testClass)
.handleEvents(receiveOutput: { _, value in
// Release the object in the middle of the sequence
// to confirm it properly terminates the sequence
if value == 3 {
self.testClass = nil
}
})
.map { "\($0.id), \($1)" }
.sink(receiveCompletion: { _ in completed = true },
receiveValue: { self.values.append($0) })

XCTAssertEqual(values, correctValues)
XCTAssertTrue(completed)
}

func testResultsSelector() {
let testClassId = testClass.id
var completed = false

let inputArr = [(1, "a"), (2, "b"), (3, "c"), (5, "d"), (8, "e")]

let correctValues = [
"\(testClassId), 1, a",
"\(testClassId), 2, b",
"\(testClassId), 3, c",
"\(testClassId), 5, d",
"\(testClassId), 8, e"
]

subscription = Publishers.Sequence<[(Int, String)], WithUnretainedTestsError>(sequence: inputArr)
.withUnretained(self.testClass) { ($0, $1.0, $1.1) }
.map { "\($0.id), \($1), \($2)" }
.sink(receiveCompletion: { _ in completed = true },
receiveValue: { self.values.append($0) })

XCTAssertEqual(values, correctValues)
XCTAssertTrue(completed)
}
}

private class TestClass {
let id: String = UUID().uuidString
}
#endif

0 comments on commit 689f88a

Please sign in to comment.