From aee3507ef628cc135cc60ffd3b436347685bb2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=8Ans=20Bernhardt?= Date: Wed, 24 Apr 2019 11:18:55 +0200 Subject: [PATCH 1/3] - Added `deallocSignal(for:)` and `NSObject.deallocSignal` for listen on deallocation of objects. - Added signal transformation `with(weak:)` as a convenience helper for breaking retain cycles. --- Flow.xcodeproj/project.pbxproj | 2 +- Flow/Info.plist | 2 +- Flow/Signal+Combiners.swift | 131 ++++++++++++++++++++++++++++ Flow/Signal+Utilities.swift | 21 +++++ FlowFramework.podspec | 2 +- FlowTests/SignalProviderTests.swift | 116 ++++++++++++++++++++++++ 6 files changed, 271 insertions(+), 3 deletions(-) diff --git a/Flow.xcodeproj/project.pbxproj b/Flow.xcodeproj/project.pbxproj index ed01807..fea1120 100644 --- a/Flow.xcodeproj/project.pbxproj +++ b/Flow.xcodeproj/project.pbxproj @@ -116,7 +116,7 @@ F688B941205FB78A00BA5A70 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; F688B942205FB78A00BA5A70 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; F688B943205FB78A00BA5A70 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; - F688B944205FB78B00BA5A70 /* FlowFramework.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = FlowFramework.podspec; sourceTree = ""; }; + F688B944205FB78B00BA5A70 /* FlowFramework.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = FlowFramework.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; F68EF3501FD58FBD0001129C /* Delegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Flow/Delegate.swift; sourceTree = ""; }; F68EF3521FD58FC70001129C /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Event.swift; path = Flow/Event.swift; sourceTree = ""; }; F68EF3541FD58FD20001129C /* UIView+Signal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+Signal.swift"; path = "Flow/UIView+Signal.swift"; sourceTree = ""; }; diff --git a/Flow/Info.plist b/Flow/Info.plist index c1add73..9e0be35 100644 --- a/Flow/Info.plist +++ b/Flow/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.5.2 + 1.8.0 CFBundleSignature ???? CFBundleVersion diff --git a/Flow/Signal+Combiners.swift b/Flow/Signal+Combiners.swift index b101335..3e44afa 100644 --- a/Flow/Signal+Combiners.swift +++ b/Flow/Signal+Combiners.swift @@ -104,6 +104,137 @@ public extension SignalProvider { return state }) } + + /// Returns a new signal combining the latest value of `self` with the provided `object` up until `object` gets deallocated. + /// This is a convenience helper for break retain cycles in situations such as: + /// + /// class Class { + /// let bag = DisposeBag() + /// + /// func setupUsingWeakCapture() { + /// bag += someSignal.onValue { [weak self] value in + /// guard let `self` = self else { return } + /// self.handle(value) + /// } + /// + /// func setupWithWeak() { + /// bag += someSignal.with(weak: self).onValue { value, `self` in + /// self.handle(value) + /// } + /// } + /// + /// a)---------b------c-----d--| + /// | | | + /// +--------------------------+ + /// | with(weak: o) | + /// +--------------------------+ + /// | | | + /// (a,o))---(b,o)--(c,o)-(d,o)| + /// + func with(weak object: T) -> CoreSignal { + let signal = providedSignal + return CoreSignal(onEventType: { [weak object] callback in + let state = StateAndCallback(state: (), callback: callback) + + guard let object = object else { + return state + } + + state += deallocSignal(for: object).onValue { + state.dispose() + } + + state += signal.onEventType { [weak object] eventType in + guard let object = object else { + state.call(.event(.end)) + return + } + + switch eventType { + case .initial(nil): + state.call(.initial(nil)) + case .initial(let value?): + state.call(.initial((value, object))) + case .event(.value(let value)): + state.call(.event(.value((value, object)))) + case .event(.end(let error)): + state.call(.event(.end(error))) + } + } + + return state + }) + } + + /// Returns a new signal combining the latest tuple value of `self` with the provided `object` up until `object` gets deallocated. + /// + /// bag += combineLatest(a, b).with(weak: self).onValue { a, b, `self` in + /// self.handle(a, b) + /// } + /// + /// (a,1))-----(b,1)-----(b,2)--| + /// | | | + /// +---------------------------+ + /// | with(weak: o) | + /// +---------------------------+ + /// | | | + /// (a,1,o))--(b,1,o)--(b,2,o)--| + /// + /// - Note: See `with(weak:)` for more info. + func with(weak object: T) -> CoreSignal where Value == (A, B) { + return with(weak: object).map { ($0.0, $0.1, $1) } + } + + /// Returns a new signal combining the latest tuple value of `self` with the provided `object` up until `object` gets deallocated. + /// - Note: See `with(weak:)` for more info. + func with(weak object: T) -> CoreSignal where Value == (A, B, C) { + return with(weak: object).map { ($0.0, $0.1, $0.2, $1) } + } + + /// Returns a new signal combining the latest tuple value of `self` with the provided `object` up until `object` gets deallocated. + /// - Note: See `with(weak:)` for more info. + func with(_ object: T) -> CoreSignal where Value == (A, B, C, D) { + return with(weak: object).map { ($0.0, $0.1, $0.2, $0.3, $1) } + } + + /// Returns a new signal combining the latest tuple value of `self` with the provided `object` up until `object` gets deallocated. + /// - Note: See `with(weak:)` for more info. + func with(_ object: T) -> CoreSignal where Value == (A, B, C, D, E) { + return with(weak: object).map { ($0.0, $0.1, $0.2, $0.3, $0.4, $1) } + } + + /// Returns a new signal combining the latest tuple value of `self` with the provided `object` up until `object` gets deallocated. + /// - Note: See `with(weak:)` for more info. + func with(_ object: T) -> CoreSignal where Value == (A, B, C, D, E, F) { + return with(weak: object).map { ($0.0, $0.1, $0.2, $0.3, $0.4, $0.5, $1) } + } + + /// Returns a new signal combining the latest tuple value of `self` with the provided `object` up until `object` gets deallocated. + /// - Note: See `with(weak:)` for more info. + func with(_ object: T) -> CoreSignal where Value == (A, B, C, D, E, F, G) { + return with(weak: object).map { ($0.0, $0.1, $0.2, $0.3, $0.4, $0.5, $0.6, $1) } + } +} + +/// Returns a new signal signaling the provided `object` every time `self` signals, up until `object` gets deallocated. +/// +/// bag += button.with(weak: self).onValue { `self` in +/// self.handleButton() +/// } +/// +/// ())-----()-----()--| +/// | | | +/// +------------------+ +/// | with(weak: o) | +/// +------------------+ +/// | | | +/// o)------o------o---| +/// +/// - Note: See `with(weak:)` for more info. +public extension SignalProvider where Value == () { + func with(weak object: T) -> CoreSignal { + return with(weak: object).map { $1 } + } } /// Returns a new signal merging the values emitted from `signals` diff --git a/Flow/Signal+Utilities.swift b/Flow/Signal+Utilities.swift index dd24fb8..3aa69bd 100644 --- a/Flow/Signal+Utilities.swift +++ b/Flow/Signal+Utilities.swift @@ -31,3 +31,24 @@ public extension Sequence { }) } } + +/// Returns signal that will signal once `object` is deallocated. +public func deallocSignal(for object: AnyObject) -> Signal<()> { + let tracker = objc_getAssociatedObject(object, &trackerKey) as? DeallocTracker ?? DeallocTracker() + objc_setAssociatedObject(object, &trackerKey, tracker, .OBJC_ASSOCIATION_RETAIN) + return tracker.callbacker.providedSignal +} + +public extension NSObject { + /// Returns signal that will signal once `self` is deallocated. + var deallocSignal: Signal<()> { + return Flow.deallocSignal(for: self) + } +} + +private final class DeallocTracker { + let callbacker = Callbacker<()>() + deinit { callbacker.callAll(with: ()) } +} + +private var trackerKey = false diff --git a/FlowFramework.podspec b/FlowFramework.podspec index 97906d4..40282fe 100644 --- a/FlowFramework.podspec +++ b/FlowFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "FlowFramework" - s.version = "1.5.2" + s.version = "1.8.0" s.module_name = "Flow" s.summary = "Working with asynchronous flows" s.description = <<-DESC diff --git a/FlowTests/SignalProviderTests.swift b/FlowTests/SignalProviderTests.swift index f45251a..816c3c5 100644 --- a/FlowTests/SignalProviderTests.swift +++ b/FlowTests/SignalProviderTests.swift @@ -2218,6 +2218,122 @@ class SignalProviderTests: XCTestCase { XCTAssertEqual(p.value, 3) XCTAssertEqual(getCnt, 3) } + + func testDeallocSignal() { + runTest { bag in + class Class { } + let object = Class() + + let e = expectation(description: "dealloc") + bag += Flow.deallocSignal(for: object).onValue { + e.fulfill() + } + } + } + + func testSignalWeakly() { + class Class { } + var object: Class! = Class() + + let bag = DisposeBag() + let rw = ReadWriteSignal(0) + + var result = [Int]() + bag += rw.with(weak: object).atOnce().onValue { value, object in + result.append(value) + } + + XCTAssertEqual(result, [0]) + + rw.value = 1 + XCTAssertEqual(result, [0, 1]) + + object = nil + rw.value = 2 + XCTAssertEqual(result, [0, 1]) + } + + func testSignalWeaklyContainterRetainCycle() { + class Class { + let bag = DisposeBag() + var result = [Int]() + let rw = ReadWriteSignal(0) + + func setup() { + bag += rw.with(weak: self).atOnce().onValue { value, `self` in + self.result.append(value) + } + } + } + + var object: Class! = Class() + weak var weakObject = object + + object.setup() + + XCTAssertEqual(object.result, [0]) + XCTAssertNotNil(weakObject) + + object.rw.value = 1 + XCTAssertEqual(object.result, [0, 1]) + XCTAssertNotNil(weakObject) + + object = nil + XCTAssertNil(weakObject) + } + + func testWeaklyWithTuple() { + class Class { } + var object: Class! = Class() + + let bag = DisposeBag() + let rw1 = ReadWriteSignal(0) + let rw2 = ReadWriteSignal(0) + + var result = [Int]() + bag += combineLatest(rw1, rw2).with(weak: object).atOnce().onValue { val1, val2, object in + print(object, val1, val2) + result.append(val1) + } + + XCTAssertEqual(result, [0]) + + rw1.value = 1 + XCTAssertEqual(result, [0, 1]) + + object = nil + rw1.value = 2 + XCTAssertEqual(result, [0, 1]) + } + + func testDoubleWeakly() { + class Class { } + var object1: Class! = Class() + var object2: Class! = Class() + + let bag = DisposeBag() + let rw = ReadWriteSignal(0) + + var result = [Int]() + bag += rw.with(weak: object1).with(weak: object2).atOnce().onValue { value, obj1, obj2 in + XCTAssert(obj1 === object1) + XCTAssert(obj2 === object2) + result.append(value) + } + + XCTAssertEqual(result, [0]) + + rw.value = 1 + XCTAssertEqual(result, [0, 1]) + + object1 = nil + rw.value = 2 + XCTAssertEqual(result, [0, 1]) + + object2 = nil + rw.value = 3 + XCTAssertEqual(result, [0, 1]) + } } class SignalProviderStressTests: XCTestCase { From a38a958dc289d53aa3268a477af82e5388601ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=8Ans=20Bernhardt?= Date: Wed, 24 Apr 2019 11:19:43 +0200 Subject: [PATCH 2/3] - Added `deallocSignal(for:)` and `NSObject.deallocSignal` for listen on deallocation of objects. - Added signal transformation `with(weak:)` as a convenience helper for breaking retain cycles. Bump version to 1.8 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad992ae..d57a7b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 1.8 + +- Added `deallocSignal(for:)` and `NSObject.deallocSignal` for listen on deallocation of objects. +- Added signal transformation `with(weak:)` as a convenience helper for breaking retain cycles. + +# 1.7 + +- Migration to Swift 5. + # 1.6 - Addition: Make `Callbacker` conform to `SignalProvider`. From 2df5034d95d9ab2c915d2e4e8bdb6e282dd9cdc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=8Ans=20Bernhardt?= Date: Thu, 25 Apr 2019 11:32:45 +0200 Subject: [PATCH 3/3] Fixed spelling. --- Flow/Signal+Combiners.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Flow/Signal+Combiners.swift b/Flow/Signal+Combiners.swift index 3e44afa..86bd144 100644 --- a/Flow/Signal+Combiners.swift +++ b/Flow/Signal+Combiners.swift @@ -106,7 +106,7 @@ public extension SignalProvider { } /// Returns a new signal combining the latest value of `self` with the provided `object` up until `object` gets deallocated. - /// This is a convenience helper for break retain cycles in situations such as: + /// This is a convenience helper for breaking retain cycles in situations such as: /// /// class Class { /// let bag = DisposeBag() @@ -214,6 +214,18 @@ public extension SignalProvider { func with(_ object: T) -> CoreSignal where Value == (A, B, C, D, E, F, G) { return with(weak: object).map { ($0.0, $0.1, $0.2, $0.3, $0.4, $0.5, $0.6, $1) } } + + /// Returns a new signal combining the latest tuple value of `self` with the provided `object` up until `object` gets deallocated. + /// - Note: See `with(weak:)` for more info. + func with(_ object: T) -> CoreSignal where Value == (A, B, C, D, E, F, G, H) { + return with(weak: object).map { ($0.0, $0.1, $0.2, $0.3, $0.4, $0.5, $0.6, $0.7, $1) } + } + + /// Returns a new signal combining the latest tuple value of `self` with the provided `object` up until `object` gets deallocated. + /// - Note: See `with(weak:)` for more info. + func with(_ object: T) -> CoreSignal where Value == (A, B, C, D, E, F, G, H, I) { + return with(weak: object).map { ($0.0, $0.1, $0.2, $0.3, $0.4, $0.5, $0.6, $0.7, $0.8, $1) } + } } /// Returns a new signal signaling the provided `object` every time `self` signals, up until `object` gets deallocated.