Skip to content

Commit d707d6e

Browse files
Merge branch 'master' into master
2 parents 0aa5f69 + 33071e5 commit d707d6e

14 files changed

+1165
-1158
lines changed

Sources/CombineDataSources/BatchesDataSource/BatchesDataSource.swift

Lines changed: 195 additions & 195 deletions
Large diffs are not rendered by default.

Sources/CombineDataSources/CollectionView/UICollectionView+Subscribers.swift

Lines changed: 76 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -7,81 +7,81 @@ import UIKit
77
import Combine
88

99
extension UICollectionView {
10-
11-
/// A collection view specific `Subscriber` that receives `[[Element]]` input and updates a sectioned collection view.
12-
/// - Parameter cellIdentifier: The Cell ID to use for dequeueing table cells.
13-
/// - Parameter cellType: The required cell type for table rows.
14-
/// - Parameter cellConfig: A closure that receives an initialized cell and a collection element
15-
/// and configures the cell for displaying in its containing table view.
16-
public func sectionsSubscriber<CellType, Items>(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping CollectionViewItemsController<Items>.CellConfig<Items.Element.Element, CellType>)
17-
-> AnySubscriber<Items, Never> where CellType: UICollectionViewCell,
18-
Items: RandomAccessCollection,
19-
Items.Element: RandomAccessCollection,
20-
Items.Element: Equatable {
21-
22-
return sectionsSubscriber(.init(cellIdentifier: cellIdentifier, cellType: cellType, cellConfig: cellConfig))
23-
}
24-
25-
/// A table view specific `Subscriber` that receives `[[Element]]` input and updates a sectioned table view.
26-
/// - Parameter source: A configured `CollectionViewItemsController<Items>` instance.
27-
public func sectionsSubscriber<Items>(_ source: CollectionViewItemsController<Items>)
28-
-> AnySubscriber<Items, Never> where
29-
Items: RandomAccessCollection,
30-
Items.Element: RandomAccessCollection,
31-
Items.Element: Equatable {
32-
33-
source.collectionView = self
34-
dataSource = source
35-
36-
return AnySubscriber<Items, Never>(receiveSubscription: { subscription in
37-
subscription.request(.unlimited)
38-
}, receiveValue: { [weak self] items -> Subscribers.Demand in
39-
guard let self = self else { return .none }
40-
41-
if self.dataSource == nil {
42-
self.dataSource = source
43-
}
44-
45-
source.updateCollection(items)
46-
return .unlimited
47-
}) { _ in }
48-
}
49-
50-
/// A table view specific `Subscriber` that receives `[Element]` input and updates a single section table view.
51-
/// - Parameter cellIdentifier: The Cell ID to use for dequeueing table cells.
52-
/// - Parameter cellType: The required cell type for table rows.
53-
/// - Parameter cellConfig: A closure that receives an initialized cell and a collection element
54-
/// and configures the cell for displaying in its containing table view.
55-
public func itemsSubscriber<CellType, Items>(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping CollectionViewItemsController<[Items]>.CellConfig<Items.Element, CellType>)
56-
-> AnySubscriber<Items, Never> where CellType: UICollectionViewCell,
57-
Items: RandomAccessCollection,
58-
Items: Equatable {
59-
60-
return itemsSubscriber(.init(cellIdentifier: cellIdentifier, cellType: cellType, cellConfig: cellConfig))
61-
}
62-
63-
/// A table view specific `Subscriber` that receives `[Element]` input and updates a single section table view.
64-
/// - Parameter source: A configured `CollectionViewItemsController<Items>` instance.
65-
public func itemsSubscriber<Items>(_ source: CollectionViewItemsController<[Items]>)
66-
-> AnySubscriber<Items, Never> where
67-
Items: RandomAccessCollection,
68-
Items: Equatable {
69-
70-
source.collectionView = self
71-
dataSource = source
72-
73-
return AnySubscriber<Items, Never>(receiveSubscription: { subscription in
74-
subscription.request(.unlimited)
75-
}, receiveValue: { [weak self] items -> Subscribers.Demand in
76-
guard let self = self else { return .none }
77-
78-
if self.dataSource == nil {
79-
self.dataSource = source
80-
}
81-
82-
source.updateCollection([items])
83-
return .unlimited
84-
}) { _ in }
85-
}
10+
11+
/// A collection view specific `Subscriber` that receives `[[Element]]` input and updates a sectioned collection view.
12+
/// - Parameter cellIdentifier: The Cell ID to use for dequeueing table cells.
13+
/// - Parameter cellType: The required cell type for table rows.
14+
/// - Parameter cellConfig: A closure that receives an initialized cell and a collection element
15+
/// and configures the cell for displaying in its containing table view.
16+
public func sectionsSubscriber<CellType, Items>(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping CollectionViewItemsController<Items>.CellConfig<Items.Element.Element, CellType>)
17+
-> AnySubscriber<Items, Never> where CellType: UICollectionViewCell,
18+
Items: RandomAccessCollection,
19+
Items.Element: RandomAccessCollection,
20+
Items.Element: Equatable {
21+
22+
return sectionsSubscriber(.init(cellIdentifier: cellIdentifier, cellType: cellType, cellConfig: cellConfig))
23+
}
24+
25+
/// A table view specific `Subscriber` that receives `[[Element]]` input and updates a sectioned table view.
26+
/// - Parameter source: A configured `CollectionViewItemsController<Items>` instance.
27+
public func sectionsSubscriber<Items>(_ source: CollectionViewItemsController<Items>)
28+
-> AnySubscriber<Items, Never> where
29+
Items: RandomAccessCollection,
30+
Items.Element: RandomAccessCollection,
31+
Items.Element: Equatable {
32+
33+
source.collectionView = self
34+
dataSource = source
35+
36+
return AnySubscriber<Items, Never>(receiveSubscription: { subscription in
37+
subscription.request(.unlimited)
38+
}, receiveValue: { [weak self] items -> Subscribers.Demand in
39+
guard let self = self else { return .none }
40+
41+
if self.dataSource == nil {
42+
self.dataSource = source
43+
}
44+
45+
source.updateCollection(items)
46+
return .unlimited
47+
}) { _ in }
48+
}
49+
50+
/// A table view specific `Subscriber` that receives `[Element]` input and updates a single section table view.
51+
/// - Parameter cellIdentifier: The Cell ID to use for dequeueing table cells.
52+
/// - Parameter cellType: The required cell type for table rows.
53+
/// - Parameter cellConfig: A closure that receives an initialized cell and a collection element
54+
/// and configures the cell for displaying in its containing table view.
55+
public func itemsSubscriber<CellType, Items>(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping CollectionViewItemsController<[Items]>.CellConfig<Items.Element, CellType>)
56+
-> AnySubscriber<Items, Never> where CellType: UICollectionViewCell,
57+
Items: RandomAccessCollection,
58+
Items: Equatable {
59+
60+
return itemsSubscriber(.init(cellIdentifier: cellIdentifier, cellType: cellType, cellConfig: cellConfig))
61+
}
62+
63+
/// A table view specific `Subscriber` that receives `[Element]` input and updates a single section table view.
64+
/// - Parameter source: A configured `CollectionViewItemsController<Items>` instance.
65+
public func itemsSubscriber<Items>(_ source: CollectionViewItemsController<[Items]>)
66+
-> AnySubscriber<Items, Never> where
67+
Items: RandomAccessCollection,
68+
Items: Equatable {
69+
70+
source.collectionView = self
71+
dataSource = source
72+
73+
return AnySubscriber<Items, Never>(receiveSubscription: { subscription in
74+
subscription.request(.unlimited)
75+
}, receiveValue: { [weak self] items -> Subscribers.Demand in
76+
guard let self = self else { return .none }
77+
78+
if self.dataSource == nil {
79+
self.dataSource = source
80+
}
81+
82+
source.updateCollection([items])
83+
return .unlimited
84+
}) { _ in }
85+
}
8686
}
8787

Sources/CombineDataSources/TableView/TableViewBatchesController.swift

Lines changed: 109 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -7,121 +7,121 @@ import UIKit
77
import Combine
88

99
public class TableViewBatchesController<Element: Hashable> {
10-
// Input
11-
public let reload = PassthroughSubject<Void, Never>()
12-
public let loadNext = PassthroughSubject<Void, Never>()
13-
14-
// Output
15-
public let loadError = CurrentValueSubject<Error?, Never>(nil)
16-
17-
// Private user interface
18-
private let tableView: UITableView
19-
private var batchesDataSource: BatchesDataSource<Element>!
20-
private var spin: UIActivityIndicatorView = {
21-
let spin = UIActivityIndicatorView(style: .large)
22-
spin.tintColor = .systemGray
23-
spin.startAnimating()
24-
spin.alpha = 0
25-
return spin
26-
}()
27-
28-
private var itemsController: TableViewItemsController<[[Element]]>!
29-
private var subscriptions = [AnyCancellable]()
30-
31-
public convenience init(tableView: UITableView, itemsController: TableViewItemsController<[[Element]]>, initialToken: Data?, loadItemsWithToken: @escaping (Data?) -> AnyPublisher<BatchesDataSource<Element>.LoadResult, Error>) {
32-
self.init(tableView: tableView)
33-
34-
// Create a token-based batched data source.
35-
batchesDataSource = BatchesDataSource<Element>(
36-
input: BatchesInput(reload: reload.eraseToAnyPublisher(), loadNext: loadNext.eraseToAnyPublisher()),
37-
initialToken: initialToken,
38-
loadItemsWithToken: loadItemsWithToken
39-
)
40-
41-
self.itemsController = itemsController
42-
43-
bind()
44-
}
45-
46-
public convenience init(tableView: UITableView, itemsController: TableViewItemsController<[[Element]]>, loadPage: @escaping (Int) -> AnyPublisher<BatchesDataSource<Element>.LoadResult, Error>) {
47-
self.init(tableView: tableView)
48-
49-
// Create a paged data source.
50-
self.batchesDataSource = BatchesDataSource<Element>(
51-
input: BatchesInput(reload: reload.eraseToAnyPublisher(), loadNext: loadNext.eraseToAnyPublisher()),
52-
loadPage: loadPage
53-
)
10+
// Input
11+
public let reload = PassthroughSubject<Void, Never>()
12+
public let loadNext = PassthroughSubject<Void, Never>()
5413

55-
self.itemsController = itemsController
14+
// Output
15+
public let loadError = CurrentValueSubject<Error?, Never>(nil)
5616

57-
bind()
58-
}
59-
60-
private init(tableView: UITableView) {
61-
self.tableView = tableView
62-
63-
// Add bottom offset.
64-
var newInsets = tableView.contentInset
65-
newInsets.bottom += 60
66-
tableView.contentInset = newInsets
67-
68-
// Add spinner.
69-
tableView.addSubview(spin)
70-
}
71-
72-
private func bind() {
73-
// Display items in table view.
74-
batchesDataSource.output.$items
75-
.receive(on: DispatchQueue.main)
76-
.bind(subscriber: tableView.rowsSubscriber(itemsController))
77-
.store(in: &subscriptions)
17+
// Private user interface
18+
private let tableView: UITableView
19+
private var batchesDataSource: BatchesDataSource<Element>!
20+
private var spin: UIActivityIndicatorView = {
21+
let spin = UIActivityIndicatorView(style: .large)
22+
spin.tintColor = .systemGray
23+
spin.startAnimating()
24+
spin.alpha = 0
25+
return spin
26+
}()
7827

79-
// Show/hide spinner.
80-
batchesDataSource.output.$isLoading
81-
.receive(on: DispatchQueue.main)
82-
.sink { [weak self] isLoading in
83-
guard let self = self else { return }
84-
if isLoading {
85-
self.spin.center = CGPoint(x: self.tableView.frame.width/2, y: self.tableView.contentSize.height + 30)
86-
self.spin.alpha = 1
87-
self.tableView.scrollRectToVisible(CGRect(x: 0, y: self.tableView.contentOffset.y + self.tableView.frame.height, width: 10, height: 10), animated: true)
88-
} else {
89-
self.spin.alpha = 0
90-
}
91-
}
92-
.store(in: &subscriptions)
28+
private var itemsController: TableViewItemsController<[[Element]]>!
29+
private var subscriptions = [AnyCancellable]()
9330

94-
// Bind errors.
95-
batchesDataSource.output.$error
96-
.subscribe(loadError)
97-
.store(in: &subscriptions)
31+
public convenience init(tableView: UITableView, itemsController: TableViewItemsController<[[Element]]>, initialToken: Data?, loadItemsWithToken: @escaping (Data?) -> AnyPublisher<BatchesDataSource<Element>.LoadResult, Error>) {
32+
self.init(tableView: tableView)
33+
34+
// Create a token-based batched data source.
35+
batchesDataSource = BatchesDataSource<Element>(
36+
input: BatchesInput(reload: reload.eraseToAnyPublisher(), loadNext: loadNext.eraseToAnyPublisher()),
37+
initialToken: initialToken,
38+
loadItemsWithToken: loadItemsWithToken
39+
)
40+
41+
self.itemsController = itemsController
42+
43+
bind()
44+
}
9845

99-
// Observe for table dragging.
100-
let didDrag = Publishers.CombineLatest(Just(tableView), tableView.publisher(for: \.contentOffset))
101-
.map { $0.0.isDragging }
102-
.scan((from: false, to: false)) { result, value -> (from: Bool, to: Bool) in
103-
return (from: result.to, to: value)
104-
}
105-
.filter { tuple -> Bool in
106-
tuple == (from: true, to: false)
107-
}
108-
109-
// Observe table offset and trigger loading next page at bottom
110-
Publishers.CombineLatest(Just(tableView), didDrag)
111-
.map { $0.0 }
112-
.filter { table -> Bool in
113-
return isAtBottom(of: table)
114-
}
115-
.sink { [weak self] _ in
116-
self?.loadNext.send()
117-
}
118-
.store(in: &subscriptions)
119-
}
46+
public convenience init(tableView: UITableView, itemsController: TableViewItemsController<[[Element]]>, loadPage: @escaping (Int) -> AnyPublisher<BatchesDataSource<Element>.LoadResult, Error>) {
47+
self.init(tableView: tableView)
48+
49+
// Create a paged data source.
50+
self.batchesDataSource = BatchesDataSource<Element>(
51+
input: BatchesInput(reload: reload.eraseToAnyPublisher(), loadNext: loadNext.eraseToAnyPublisher()),
52+
loadPage: loadPage
53+
)
54+
55+
self.itemsController = itemsController
56+
57+
bind()
58+
}
59+
60+
private init(tableView: UITableView) {
61+
self.tableView = tableView
62+
63+
// Add bottom offset.
64+
var newInsets = tableView.contentInset
65+
newInsets.bottom += 60
66+
tableView.contentInset = newInsets
67+
68+
// Add spinner.
69+
tableView.addSubview(spin)
70+
}
71+
72+
private func bind() {
73+
// Display items in table view.
74+
batchesDataSource.output.$items
75+
.receive(on: DispatchQueue.main)
76+
.bind(subscriber: tableView.rowsSubscriber(itemsController))
77+
.store(in: &subscriptions)
78+
79+
// Show/hide spinner.
80+
batchesDataSource.output.$isLoading
81+
.receive(on: DispatchQueue.main)
82+
.sink { [weak self] isLoading in
83+
guard let self = self else { return }
84+
if isLoading {
85+
self.spin.center = CGPoint(x: self.tableView.frame.width/2, y: self.tableView.contentSize.height + 30)
86+
self.spin.alpha = 1
87+
self.tableView.scrollRectToVisible(CGRect(x: 0, y: self.tableView.contentOffset.y + self.tableView.frame.height, width: 10, height: 10), animated: true)
88+
} else {
89+
self.spin.alpha = 0
90+
}
91+
}
92+
.store(in: &subscriptions)
93+
94+
// Bind errors.
95+
batchesDataSource.output.$error
96+
.subscribe(loadError)
97+
.store(in: &subscriptions)
98+
99+
// Observe for table dragging.
100+
let didDrag = Publishers.CombineLatest(Just(tableView), tableView.publisher(for: \.contentOffset))
101+
.map { $0.0.isDragging }
102+
.scan((from: false, to: false)) { result, value -> (from: Bool, to: Bool) in
103+
return (from: result.to, to: value)
104+
}
105+
.filter { tuple -> Bool in
106+
tuple == (from: true, to: false)
107+
}
108+
109+
// Observe table offset and trigger loading next page at bottom
110+
Publishers.CombineLatest(Just(tableView), didDrag)
111+
.map { $0.0 }
112+
.filter { table -> Bool in
113+
return isAtBottom(of: table)
114+
}
115+
.sink { [weak self] _ in
116+
self?.loadNext.send()
117+
}
118+
.store(in: &subscriptions)
119+
}
120120
}
121121

122122
fileprivate func isAtBottom(of tableView: UITableView) -> Bool {
123-
let height = tableView.frame.size.height
124-
let contentYoffset = tableView.contentOffset.y
125-
let distanceFromBottom = tableView.contentSize.height - contentYoffset
126-
return distanceFromBottom <= height
123+
let height = tableView.frame.size.height
124+
let contentYoffset = tableView.contentOffset.y
125+
let distanceFromBottom = tableView.contentSize.height - contentYoffset
126+
return distanceFromBottom <= height
127127
}

0 commit comments

Comments
 (0)