Skip to content

Commit 80837f4

Browse files
committed
subscribe() is out, bind() is in
1 parent 500fe19 commit 80837f4

File tree

9 files changed

+86
-49
lines changed

9 files changed

+86
-49
lines changed

Assets/slack.png

2.01 KB
Loading

Example/Example/Base.lproj/Main.storyboard

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14854.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="cqX-1O-xhM">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="cqX-1O-xhM">
33
<device id="retina6_1" orientation="portrait" appearance="light"/>
44
<dependencies>
5-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14806.4"/>
5+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14819.2"/>
66
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
77
<capability name="collection view cell content view" minToolsVersion="11.0"/>
88
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -25,7 +25,7 @@
2525
<rect key="frame" x="0.0" y="0.0" width="342.5" height="50"/>
2626
<autoresizingMask key="autoresizingMask"/>
2727
<subviews>
28-
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Plain list" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="dE9-Yq-PRN">
28+
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Static plain list" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="dE9-Yq-PRN">
2929
<rect key="frame" x="20" y="0.0" width="314.5" height="50"/>
3030
<autoresizingMask key="autoresizingMask"/>
3131
<fontDescription key="fontDescription" type="system" pointSize="17"/>
@@ -284,12 +284,12 @@
284284
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Status" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2nN-q1-dXy">
285285
<rect key="frame" x="121" y="67.5" width="72" height="31.5"/>
286286
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
287-
<color key="textColor" systemColor="linkColor" red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
287+
<color key="textColor" systemColor="linkColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
288288
<nil key="highlightedColor"/>
289289
</label>
290290
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fbr-Gc-rOl">
291291
<rect key="frame" x="37" y="135" width="240" height="46"/>
292-
<color key="backgroundColor" systemColor="linkColor" red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
292+
<color key="backgroundColor" systemColor="linkColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
293293
<constraints>
294294
<constraint firstAttribute="height" constant="46" id="6r8-B6-yiH"/>
295295
<constraint firstAttribute="width" constant="240" id="kUK-LS-l4G"/>
@@ -310,7 +310,7 @@
310310
</button>
311311
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eGM-4g-fqO">
312312
<rect key="frame" x="37" y="217" width="240" height="46"/>
313-
<color key="backgroundColor" systemColor="systemGrayColor" red="0.55686274509803924" green="0.55686274509803924" blue="0.57647058823529407" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
313+
<color key="backgroundColor" systemColor="systemGrayColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
314314
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
315315
<state key="normal" title="Reload">
316316
<color key="titleColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
@@ -399,7 +399,7 @@
399399
<nil key="highlightedColor"/>
400400
</label>
401401
</subviews>
402-
<color key="backgroundColor" systemColor="tertiarySystemGroupedBackgroundColor" red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
402+
<color key="backgroundColor" systemColor="tertiarySystemGroupedBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
403403
<constraints>
404404
<constraint firstItem="b97-Y7-dI9" firstAttribute="top" secondItem="Elw-G5-4Vf" secondAttribute="bottom" constant="8" id="3dE-Sl-j8Z"/>
405405
<constraint firstAttribute="bottom" secondItem="b97-Y7-dI9" secondAttribute="bottom" constant="7" id="5gd-yF-ve0"/>
@@ -461,7 +461,7 @@
461461
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l3U-52-thG">
462462
<rect key="frame" x="20" y="21" width="334" height="21"/>
463463
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
464-
<color key="textColor" systemColor="linkColor" red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
464+
<color key="textColor" systemColor="linkColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
465465
<nil key="highlightedColor"/>
466466
</label>
467467
</subviews>

Example/Example/GitHubSearchViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class GitHubSearchViewController: UIViewController, UISearchBarDelegate {
3030
.map { $0.items }
3131
.replaceError(with: [])
3232
.receive(on: DispatchQueue.main)
33-
.subscribe(retaining: tableView.rowsSubscriber(cellIdentifier: "Cell", cellType: UITableViewCell.self, cellConfig: { (cell, ip, repo) in
33+
.bind(subscriber: tableView.rowsSubscriber(cellIdentifier: "Cell", cellType: UITableViewCell.self, cellConfig: { (cell, ip, repo) in
3434
cell.textLabel!.text = repo.name
3535
cell.detailTextLabel!.text = repo.description
3636
}))

Example/Example/ViewController.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,16 @@ class ViewController: UIViewController {
5757
switch demo {
5858
case .plain:
5959
// A plain list with a single section -> Publisher<[Person], Never>
60-
data
61-
.map { $0[0] }
62-
.subscribe(retaining: tableView.rowsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
60+
first.publisher
61+
.bind(subscriber: tableView.rowsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
6362
cell.nameLabel.text = "\(indexPath.section+1).\(indexPath.row+1) \(model.name)"
6463
}))
6564
.store(in: &subscriptions)
6665

6766
case .multiple:
6867
// Table with sections -> Publisher<[[Person]], Never>
6968
data
70-
.subscribe(retaining: tableView.sectionsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
69+
.bind(subscriber: tableView.sectionsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
7170
cell.nameLabel.text = "\(indexPath.section+1).\(indexPath.row+1) \(model.name)"
7271
}))
7372
.store(in: &subscriptions)
@@ -80,7 +79,7 @@ class ViewController: UIViewController {
8079
return Section(header: "Header", items: persons, footer: "Footer")
8180
}
8281
}
83-
.subscribe(retaining: tableView.sectionsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
82+
.bind(subscriber: tableView.sectionsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
8483
cell.nameLabel.text = "\(indexPath.section+1).\(indexPath.row+1) \(model.name)"
8584
}))
8685
.store(in: &subscriptions)
@@ -93,7 +92,7 @@ class ViewController: UIViewController {
9392
controller.animated = false
9493

9594
data
96-
.subscribe(retaining: tableView.sectionsSubscriber(controller))
95+
.bind(subscriber: tableView.sectionsSubscriber(controller))
9796
.store(in: &subscriptions)
9897
}
9998

README.md

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**CombineDataSources** provides custom Combine subscribers that act as table and collection view controllers and bind a stream of element collections to table or collection sections with cells.
44

5-
⚠️⚠️⚠️ **Note**: The package is currently work in progress.
5+
⚠️⚠️⚠️ **Note** 🚨🚨🚨: The package is currently work in progress.
66

77
### Table of Contents
88

@@ -14,9 +14,7 @@
1414

1515
1.2 [Customize the table controller](#customize-the-table-controller)
1616

17-
1.3 [Subscribing a completing publisher](#subscribing-a-completing-publisher)
18-
19-
1.4 [List loaded in batches](#list-loaded-in-batches)
17+
1.3 [List loaded in batches](#list-loaded-in-batches)
2018

2119
2. [**Installation**](#installation)
2220

@@ -38,9 +36,10 @@ The repo contains a demo app in the *Example* sub-folder that demonstrates visua
3836
var data = PassthroughSubject<[Person], Never>()
3937

4038
data
41-
.subscribe(subscriber: tableView.rowsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
39+
.bind(subscriber: tableView.rowsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
4240
cell.nameLabel.text = model.name
4341
}))
42+
.store(in: &subscriptions)
4443
```
4544

4645
![Plain list updates with CombineDataSources](https://github.com/combineopensource/CombineDataSources/raw/master/Assets/plain-list.gif)
@@ -49,10 +48,11 @@ Respectively for a collection view:
4948

5049
```swift
5150
data
52-
.subscribe(collectionView.itemsSubscriber(cellIdentifier: "Cell", cellType: PersonCollectionCell.self, cellConfig: { cell, indexPath, model in
51+
.bind(subscriber: collectionView.itemsSubscriber(cellIdentifier: "Cell", cellType: PersonCollectionCell.self, cellConfig: { cell, indexPath, model in
5352
cell.nameLabel.text = model.name
5453
cell.imageURL = URL(string: "https://api.adorable.io/avatars/100/\(model.name)")!
5554
}))
55+
.store(in: &subscriptions)
5656
```
5757

5858
![Plain list updates for a collection view](https://github.com/combineopensource/CombineDataSources/raw/master/Assets/plain-collection.gif)
@@ -63,9 +63,10 @@ data
6363
var data = PassthroughSubject<[Section<Person>], Never>()
6464

6565
data
66-
.subscribe(subscriber: tableView.sectionsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
66+
.bind(subscriber: tableView.sectionsSubscriber(cellIdentifier: "Cell", cellType: PersonCell.self, cellConfig: { cell, indexPath, model in
6767
cell.nameLabel.text = model.name
6868
}))
69+
.store(in: &subscriptions)
6970
```
7071

7172
![Sectioned list updates with CombineDataSources](https://github.com/combineopensource/CombineDataSources/raw/master/Assets/sections-list.gif)
@@ -83,27 +84,10 @@ controller.animated = false
8384
// More custom controller configuration ...
8485

8586
data
86-
.subscribe(subscriber: tableView.sectionsSubscriber(controller))
87-
```
88-
89-
#### Subscribing a completing publisher
90-
91-
Sometimes you'll bind a publisher to your table or collection view and it will complete at a point. When you use `subscribe(_)` the completion event will release the CombineDataSource subscriber as well and that will likely render the table/collection empty.
92-
93-
In such case you can use the custom operator included in **CombineDataSources** `subscribe(retaining:)` that will give you an `AnyCancellable` to retain the subscriber, like so:
94-
95-
```swift
96-
var subscriptions = [AnyCancellable]()
97-
...
98-
Just([Person(name: "test"])
99-
.subscribe(retaining: tableView.rowsSubscriber(cellIdentifier: "Cell", cellType: UITableViewCell.self, cellConfig: { (cell, ip, person) in
100-
cell.textLabel!.text = person.name
101-
}))
87+
.subscribe(bind: tableView.sectionsSubscriber(controller))
10288
.store(in: &subscriptions)
10389
```
10490

105-
This will keep the subscriber and the data source alive until you cancel the subscription manually or it is released from memory.
106-
10791
#### List loaded in batches
10892

10993
A common pattern for list views is to load a very long list of elements in "batches" or "pages". (The distinction being that pages imply ordered, equal-length batches.)
@@ -186,6 +170,10 @@ Add the following dependency to your **Package.swift** file:
186170
187171
CombineOpenSource is available under the MIT license. See the LICENSE file for more info.
188172
173+
## Combine Open Source
174+
175+
Join ![Combine Slack channel](Assets/slack.png) [https://combineopensource.slack.com](https://combineopensource.slack.com) for Combine related talk.
176+
189177
## Credits
190178
191179
Created by Marin Todorov for [CombineOpenSource](https://github.com/combineopensource).

Sources/CombineDataSources/TableView/TableViewBatchesController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public class TableViewBatchesController<Element: Hashable> {
7373
// Display items in table view.
7474
batchesDataSource.output.$items
7575
.receive(on: DispatchQueue.main)
76-
.subscribe(retaining: tableView.rowsSubscriber(itemsController))
76+
.bind(subscriber: tableView.rowsSubscriber(itemsController))
7777
.store(in: &subscriptions)
7878

7979
// Show/hide spinner.

Sources/CombineDataSources/etc/Publisher+SubscribeRetaining.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
import Foundation
77
import Combine
88

9+
public typealias Binding = Subscriber
10+
911
public extension Publisher where Failure == Never {
10-
func subscribe<S: Subscriber>(retaining subscriber: S) -> AnyCancellable
11-
where S.Failure == Never, S.Input == Output {
12-
13-
sink(receiveCompletion: { (completion) in
14-
subscriber.receive(completion: completion)
15-
}) { (value) in
16-
_ = subscriber.receive(value)
17-
}
12+
func bind<B: Binding>(subscriber: B) -> AnyCancellable
13+
where B.Failure == Never, B.Input == Output {
14+
15+
handleEvents(receiveSubscription: { subscription in
16+
subscriber.receive(subscription: subscription)
17+
})
18+
.sink { value in
19+
_ = subscriber.receive(value)
20+
}
1821
}
1922
}

Tests/CombineDataSourcesTests/MemoryManagementTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import XCTest
22
import UIKit
3+
import Combine
34
@testable import CombineDataSources
45

56
final class MemoryManagementTests: XCTestCase {
@@ -23,4 +24,35 @@ final class MemoryManagementTests: XCTestCase {
2324
dataSource = nil
2425
XCTAssertNotNil(ctr!.dataSource)
2526
}
27+
28+
func testBind() {
29+
let expectation1 = expectation(description: "subscribed")
30+
let expectation2 = expectation(description: "value")
31+
32+
var subscriptions = [AnyCancellable]()
33+
var sub: AnySubscriber<String, Never>?
34+
35+
sub = AnySubscriber<String, Never>(
36+
receiveSubscription: { sub in
37+
expectation1.fulfill()
38+
},
39+
receiveValue: { value -> Subscribers.Demand in
40+
expectation2.fulfill()
41+
return .unlimited
42+
})
43+
{ (completion) in
44+
XCTFail("Binding sent completion event")
45+
}
46+
47+
DispatchQueue.main.async {
48+
let data = PassthroughSubject<String, Never>()
49+
data
50+
.bind(subscriber: sub!)
51+
.store(in: &subscriptions)
52+
53+
data.send("asdasd") // will be passed on
54+
data.send(completion: .finished) // will be filtered
55+
}
56+
wait(for: [expectation1, expectation2], timeout: 1)
57+
}
2658
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Marin Todorov on 8/30/19.
6+
//
7+
8+
import UIKit
9+
import Combine
10+
import XCTest
11+
12+
final class TableViewBatchesControllerTests: XCTestCase {
13+
// TODO: add
14+
15+
}

0 commit comments

Comments
 (0)