Skip to content

Commit

Permalink
Merge pull request #81 from iZettle/with-weak
Browse files Browse the repository at this point in the history
[Discussion] with(weak:) convenience helper for breaking retain cycles
  • Loading branch information
mansbernhardt authored Apr 29, 2019
2 parents 6a8ec12 + 2df5034 commit e7305c0
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 3 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion Flow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
F688B941205FB78A00BA5A70 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
F688B942205FB78A00BA5A70 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = "<group>"; };
F688B943205FB78A00BA5A70 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
F688B944205FB78B00BA5A70 /* FlowFramework.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = FlowFramework.podspec; sourceTree = "<group>"; };
F688B944205FB78B00BA5A70 /* FlowFramework.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = FlowFramework.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
F68EF3501FD58FBD0001129C /* Delegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Flow/Delegate.swift; sourceTree = "<group>"; };
F68EF3521FD58FC70001129C /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Event.swift; path = Flow/Event.swift; sourceTree = "<group>"; };
F68EF3541FD58FD20001129C /* UIView+Signal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+Signal.swift"; path = "Flow/UIView+Signal.swift"; sourceTree = "<group>"; };
Expand Down
2 changes: 1 addition & 1 deletion Flow/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.5.2</string>
<string>1.8.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
143 changes: 143 additions & 0 deletions Flow/Signal+Combiners.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,149 @@ 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 breaking 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<T: AnyObject>(weak object: T) -> CoreSignal<Kind.DropWrite, (Value, T)> {
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<T: AnyObject, A, B>(weak object: T) -> CoreSignal<Kind.DropWrite.DropWrite, (A, B, T)> 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<T: AnyObject, A, B, C>(weak object: T) -> CoreSignal<Kind.DropWrite.DropWrite, (A, B, C, T)> 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<T: AnyObject, A, B, C, D>(_ object: T) -> CoreSignal<Kind.DropWrite.DropWrite, (A, B, C, D, T)> 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<T: AnyObject, A, B, C, D, E>(_ object: T) -> CoreSignal<Kind.DropWrite.DropWrite, (A, B, C, D, E, T)> 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<T: AnyObject, A, B, C, D, E, F>(_ object: T) -> CoreSignal<Kind.DropWrite.DropWrite, (A, B, C, D, E, F, T)> 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<T: AnyObject, A, B, C, D, E, F, G>(_ object: T) -> CoreSignal<Kind.DropWrite.DropWrite, (A, B, C, D, E, F, G, T)> 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<T: AnyObject, A, B, C, D, E, F, G, H>(_ object: T) -> CoreSignal<Kind.DropWrite.DropWrite, (A, B, C, D, E, F, G, H, T)> 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<T: AnyObject, A, B, C, D, E, F, G, H, I>(_ object: T) -> CoreSignal<Kind.DropWrite.DropWrite, (A, B, C, D, E, F, G, H, I, T)> 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.
///
/// 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<T: AnyObject>(weak object: T) -> CoreSignal<Kind.DropWrite.DropWrite, T> {
return with(weak: object).map { $1 }
}
}

/// Returns a new signal merging the values emitted from `signals`
Expand Down
21 changes: 21 additions & 0 deletions Flow/Signal+Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion FlowFramework.podspec
Original file line number Diff line number Diff line change
@@ -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
Expand Down
116 changes: 116 additions & 0 deletions FlowTests/SignalProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit e7305c0

Please sign in to comment.