Skip to content
This repository was archived by the owner on May 26, 2020. It is now read-only.

Commit 1022ff4

Browse files
andersiodmcrodrigues
authored andcommitted
Align the semantic of ReactiveArray.producer with properties. (#14)
1 parent f1f3b6f commit 1022ff4

File tree

2 files changed

+118
-71
lines changed

2 files changed

+118
-71
lines changed

Sources/ReactiveArray.swift

Lines changed: 88 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,17 @@ import ReactiveSwift
33
import Result
44

55
public final class ReactiveArray<Element> {
6-
76
public typealias Snapshot = ContiguousArray<Element>
87
public typealias Change = Delta<Snapshot, IndexSet>
98

10-
public let signal: Signal<Change, NoError>
11-
12-
fileprivate var elements: ContiguousArray<Element>
9+
fileprivate let storage: Storage<ContiguousArray<Element>>
1310

11+
public let signal: Signal<Change, NoError>
1412
fileprivate let innerObserver: Observer<Change, NoError>
1513

1614
public init(_ elements: [Element]) {
17-
self.elements = ContiguousArray(elements)
18-
1915
(signal, innerObserver) = Signal<Change, NoError>.pipe()
16+
storage = Storage(ContiguousArray(elements))
2017
}
2118

2219
public convenience init() {
@@ -32,21 +29,22 @@ public final class ReactiveArray<Element> {
3229
extension ReactiveArray {
3330

3431
public var producer: SignalProducer<Change, NoError> {
35-
return SignalProducer<Change, NSError>.attempt { [weak self] in
36-
guard let `self` = self
37-
else { return .failure(NSError(domain: "org.RACCommunity.ReactiveCollections", code: 0, userInfo: nil)) }
38-
39-
return .success(
40-
Delta(
41-
previous: [],
42-
current: self.elements,
43-
inserts: IndexSet(integersIn: self.indices),
44-
deletes: .empty,
45-
updates: .empty
46-
)
47-
)}
48-
.flatMapError { _ in .empty }
49-
.concat(SignalProducer(signal))
32+
return SignalProducer { [weak self, storage] observer, disposable in
33+
storage.modify { elements in
34+
let delta = Delta(previous: [],
35+
current: elements,
36+
inserts: IndexSet(integersIn: elements.indices),
37+
deletes: .empty,
38+
updates: .empty)
39+
observer.send(value: delta)
40+
41+
if let strongSelf = self {
42+
disposable += strongSelf.signal.observe(observer)
43+
} else {
44+
observer.sendCompleted()
45+
}
46+
}
47+
}
5048
}
5149
}
5250

@@ -64,16 +62,16 @@ extension ReactiveArray: ExpressibleByArrayLiteral {
6462
extension ReactiveArray: MutableCollection {
6563

6664
public var startIndex: Int {
67-
return elements.startIndex
65+
return storage.elements.startIndex
6866
}
6967

7068
public var endIndex: Int {
71-
return elements.endIndex
69+
return storage.elements.endIndex
7270
}
7371

7472
public subscript(position: Int) -> Element {
7573
get {
76-
return elements[position]
74+
return storage.elements[position]
7775
}
7876
set {
7977
replaceSubrange(position..<index(after: position), with: CollectionOfOne(newValue))
@@ -82,7 +80,7 @@ extension ReactiveArray: MutableCollection {
8280

8381
public subscript(bounds: Range<Int>) -> ArraySlice<Element> {
8482
get {
85-
return elements[bounds]
83+
return storage.elements[bounds]
8684
}
8785
set {
8886
replaceSubrange(bounds, with: newValue)
@@ -124,29 +122,29 @@ extension ReactiveArray: RangeReplaceableCollection {
124122

125123
public func remove(at position: Int) -> Element {
126124
precondition(!isEmpty, "can't remove from an empty array")
127-
let result = self[position]
128-
removeSubrange(position..<index(after: position))
129-
return result
125+
126+
return storage.modify { elements in
127+
let value = elements[position]
128+
elements.remove(at: position)
129+
return value
130+
}
130131
}
131132

132133
public func removeAll(keepingCapacity keepCapacity: Bool = false) {
133134
if keepCapacity {
134135
removeSubrange(indices)
135136
} else {
136-
137-
let previous = elements
138-
139-
elements.removeAll()
140-
141-
innerObserver.send(value:
142-
Delta(
143-
previous: previous,
144-
current: elements,
145-
inserts: .empty,
146-
deletes: IndexSet(integersIn: previous.indices),
147-
updates: .empty
148-
)
149-
)
137+
storage.modify { elements in
138+
let previous = storage.elements
139+
elements.removeAll()
140+
141+
let delta = Delta(previous: previous,
142+
current: elements,
143+
inserts: .empty,
144+
deletes: IndexSet(integersIn: previous.indices),
145+
updates: .empty)
146+
innerObserver.send(value: delta)
147+
}
150148
}
151149
}
152150

@@ -201,27 +199,59 @@ extension ReactiveArray: RangeReplaceableCollection {
201199
}
202200

203201
public func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C: Collection, C.Iterator.Element == Element {
202+
storage.modify { elements in
203+
let previous = elements
204204

205-
let previous = elements
206-
207-
elements.replaceSubrange(subrange, with: newElements)
205+
elements.replaceSubrange(subrange, with: newElements)
208206

209-
let inserts = IndexSet(integersIn: subrange.lowerBound..<subrange.lowerBound.advanced(by: newElements.underestimatedCount))
210-
let deletes = IndexSet(integersIn: subrange)
211-
let updates = inserts.intersection(deletes)
207+
let insertsUpperBound = subrange.lowerBound.advanced(by: Int(newElements.distance(from: newElements.startIndex, to: newElements.endIndex).toIntMax()))
208+
let inserts = IndexSet(integersIn: subrange.lowerBound ..< insertsUpperBound)
209+
let deletes = IndexSet(integersIn: subrange)
210+
let updates = inserts.intersection(deletes)
212211

213-
innerObserver.send(value:
214-
Delta(
215-
previous: previous,
216-
current: elements,
217-
inserts: inserts.subtracting(updates),
218-
deletes: deletes.subtracting(updates),
219-
updates: updates
220-
)
221-
)
212+
let delta = Delta(previous: previous,
213+
current: storage.elements,
214+
inserts: inserts.subtracting(updates),
215+
deletes: deletes.subtracting(updates),
216+
updates: updates)
217+
innerObserver.send(value: delta)
218+
}
222219
}
223220

224221
public func reserveCapacity(_ n: Int) {
225-
elements.reserveCapacity(n)
222+
storage.modify { $0.reserveCapacity(n) }
223+
}
224+
}
225+
226+
private final class Storage<Elements> {
227+
private var _elements: Elements
228+
229+
var elements: Elements {
230+
get { return _elements }
231+
set {
232+
writeLock.lock()
233+
_elements = newValue
234+
writeLock.unlock()
235+
}
236+
}
237+
238+
/// A lock to protect mutations and their subsequent event emission. Reads do
239+
/// not need to be protected, since the copy-on-write already guards reads.
240+
/// The producer, however, has to acquire the lock to block new mutations
241+
/// before it observes the delta signal.
242+
fileprivate let writeLock: NSLock
243+
244+
init(_ elements: Elements) {
245+
self._elements = elements
246+
247+
writeLock = NSLock()
248+
writeLock.name = "org.RACCommunity.ReactiveCollections.ReactiveArray.writeLock"
249+
}
250+
251+
func modify<Result>(_ action: (inout Elements) throws -> Result) rethrows -> Result {
252+
writeLock.lock()
253+
let returnValue = try action(&_elements)
254+
writeLock.unlock()
255+
return returnValue
226256
}
227257
}

Tests/ReactiveCollectionsTests/ReactiveArrayTests.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -638,25 +638,41 @@ class ReactiveArrayTests: XCTestCase {
638638
zip(changes, expectedChanges).forEach { XCTAssertEqual($0, $1) }
639639
}
640640

641-
func test_producer_not_retaining_array() {
641+
func test_producer_should_not_retain_the_array() {
642+
var array = ReactiveArray([1, 2, 3]) as Optional
643+
weak var weakArray = array
642644

643-
let completedExpectation = expectation(description: "Completed expectation")
645+
withExtendedLifetime(array!.producer) {
646+
array = nil
647+
XCTAssertNil(weakArray)
648+
}
649+
}
644650

651+
func test_producer_should_send_last_snapshot_even_if_array_has_deinitialized() {
645652
var array = ReactiveArray([1, 2, 3]) as Optional
646-
647653
let producer = array!.producer
648-
649654
array = nil
650655

651-
producer
652-
.on(completed: {
653-
completedExpectation.fulfill()
654-
}, value: { _ in
655-
XCTAssertFalse(false, "Producer should not send any values") }
656-
)
657-
.start()
656+
var latestSnapshot: ContiguousArray<Int>?
657+
var completed = false
658+
var hasUnanticipatedEvents = false
658659

659-
waitForExpectations(timeout: 1.0, handler: nil)
660+
producer.start { event in
661+
switch event {
662+
case let .value(delta):
663+
latestSnapshot = delta.current
664+
665+
case .completed:
666+
completed = true
667+
668+
case .interrupted, .failed:
669+
hasUnanticipatedEvents = true
670+
}
671+
}
672+
673+
XCTAssertEqual(latestSnapshot ?? [], [1, 2, 3])
674+
XCTAssertTrue(completed)
675+
XCTAssertFalse(hasUnanticipatedEvents)
660676
}
661677
}
662678

@@ -696,7 +712,8 @@ extension ReactiveArrayTests {
696712
("test_remove_subrange", test_remove_subrange),
697713
("test_producer", test_producer),
698714
("test_producer_with_up_to_date_changes", test_producer_with_up_to_date_changes),
699-
("test_producer_not_retaining_array", test_producer_not_retaining_array)
715+
("test_producer_should_not_retain_the_array", test_producer_should_not_retain_the_array),
716+
("test_producer_should_send_last_snapshot_even_if_array_has_deinitialized", test_producer_should_send_last_snapshot_even_if_array_has_deinitialized)
700717
]
701718
}
702719
}

0 commit comments

Comments
 (0)