Skip to content

Commit 59176ae

Browse files
Add BBUIScrollViewRepresentable
1 parent 1e82a7b commit 59176ae

File tree

3 files changed

+204
-57
lines changed

3 files changed

+204
-57
lines changed

BBSwiftUIKit/BBSwiftUIKit/BBScrollView.swift

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,71 +8,95 @@
88

99
import SwiftUI
1010

11+
public protocol BBUIScrollViewRepresentable {
12+
var contentOffset: CGPoint { get set }
13+
var contentOffsetToScrollAnimated: CGPoint? { get set }
14+
var isPagingEnabled: Bool { get set }
15+
var bounces: Bool { get set }
16+
var alwaysBounceVertical: Bool { get set }
17+
var alwaysBounceHorizontal: Bool { get set }
18+
var showsVerticalScrollIndicator: Bool { get set }
19+
var showsHorizontalScrollIndicator: Bool { get set }
20+
21+
func updateScrollView(_ scrollView: UIScrollView)
22+
}
23+
1124
public extension CGPoint {
1225
static let bb_invalidContentOffset = CGPoint(x: CGFloat.greatestFiniteMagnitude, y: CGFloat.greatestFiniteMagnitude)
1326
}
1427

15-
public extension BBScrollView {
16-
func bb_contentOffset(_ contentOffset: Binding<CGPoint>) -> BBScrollView {
28+
public extension BBUIScrollViewRepresentable {
29+
func bb_isPagingEnabled(_ isPagingEnabled: Bool) -> Self {
1730
var view = self
18-
view._contentOffset = contentOffset
31+
view.isPagingEnabled = isPagingEnabled
1932
return view
2033
}
2134

22-
func bb_contentOffsetToScrollAnimated(_ contentOffsetToScrollAnimated: Binding<CGPoint?>) -> BBScrollView {
35+
func bb_bounces(_ bounces: Bool) -> Self {
2336
var view = self
24-
view._contentOffsetToScrollAnimated = contentOffsetToScrollAnimated
37+
view.bounces = bounces
2538
return view
2639
}
2740

28-
func bb_isPagingEnabled(_ isPagingEnabled: Bool) -> BBScrollView {
41+
func bb_alwaysBounceVertical(_ alwaysBounceVertical: Bool) -> Self {
2942
var view = self
30-
view.isPagingEnabled = isPagingEnabled
43+
view.alwaysBounceVertical = alwaysBounceVertical
3144
return view
3245
}
3346

34-
func bb_bounces(_ bounces: Bool) -> BBScrollView {
47+
func bb_alwaysBounceHorizontal(_ alwaysBounceHorizontal: Bool) -> Self {
3548
var view = self
36-
view.bounces = bounces
49+
view.alwaysBounceHorizontal = alwaysBounceHorizontal
3750
return view
3851
}
3952

40-
func bb_alwaysBounceVertical(_ alwaysBounceVertical: Bool) -> BBScrollView {
53+
func bb_showsVerticalScrollIndicator(_ showsVerticalScrollIndicator: Bool) -> Self {
4154
var view = self
42-
view.alwaysBounceVertical = alwaysBounceVertical
55+
view.showsVerticalScrollIndicator = showsVerticalScrollIndicator
4356
return view
4457
}
4558

46-
func bb_alwaysBounceHorizontal(_ alwaysBounceHorizontal: Bool) -> BBScrollView {
59+
func bb_showsHorizontalScrollIndicator(_ showsHorizontalScrollIndicator: Bool) -> Self {
4760
var view = self
48-
view.alwaysBounceHorizontal = alwaysBounceHorizontal
61+
view.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator
4962
return view
5063
}
5164

52-
func bb_showsVerticalScrollIndicator(_ showsVerticalScrollIndicator: Bool) -> BBScrollView {
65+
func updateScrollView(_ scrollView: UIScrollView) {
66+
scrollView.isPagingEnabled = isPagingEnabled
67+
scrollView.bounces = bounces
68+
scrollView.alwaysBounceVertical = alwaysBounceVertical
69+
scrollView.alwaysBounceHorizontal = alwaysBounceHorizontal
70+
scrollView.showsVerticalScrollIndicator = showsVerticalScrollIndicator
71+
scrollView.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator
72+
}
73+
}
74+
75+
public extension BBScrollView {
76+
func bb_contentOffset(_ contentOffset: Binding<CGPoint>) -> Self {
5377
var view = self
54-
view.showsVerticalScrollIndicator = showsVerticalScrollIndicator
78+
view._contentOffset = contentOffset
5579
return view
5680
}
5781

58-
func bb_showsHorizontalScrollIndicator(_ showsHorizontalScrollIndicator: Bool) -> BBScrollView {
82+
func bb_contentOffsetToScrollAnimated(_ contentOffsetToScrollAnimated: Binding<CGPoint?>) -> Self {
5983
var view = self
60-
view.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator
84+
view._contentOffsetToScrollAnimated = contentOffsetToScrollAnimated
6185
return view
6286
}
6387
}
6488

65-
public struct BBScrollView<Content: View>: UIViewRepresentable {
66-
let axis: Axis.Set
67-
@Binding var contentOffset: CGPoint
68-
@Binding var contentOffsetToScrollAnimated: CGPoint?
69-
var isPagingEnabled: Bool
70-
var bounces: Bool
71-
var alwaysBounceVertical: Bool
72-
var alwaysBounceHorizontal: Bool
73-
var showsVerticalScrollIndicator: Bool
74-
var showsHorizontalScrollIndicator: Bool
75-
let content: () -> Content
89+
public struct BBScrollView<Content: View>: UIViewRepresentable, BBUIScrollViewRepresentable {
90+
public let axis: Axis.Set
91+
@Binding public var contentOffset: CGPoint
92+
@Binding public var contentOffsetToScrollAnimated: CGPoint?
93+
public var isPagingEnabled: Bool
94+
public var bounces: Bool
95+
public var alwaysBounceVertical: Bool
96+
public var alwaysBounceHorizontal: Bool
97+
public var showsVerticalScrollIndicator: Bool
98+
public var showsHorizontalScrollIndicator: Bool
99+
public let content: () -> Content
76100

77101
public init(_ axis: Axis.Set,
78102
contentOffset: Binding<CGPoint> = .constant(.bb_invalidContentOffset),
@@ -135,12 +159,8 @@ public struct BBScrollView<Content: View>: UIViewRepresentable {
135159
} else if contentOffset != .bb_invalidContentOffset {
136160
scrollView.contentOffset = contentOffset
137161
}
138-
scrollView.isPagingEnabled = isPagingEnabled
139-
scrollView.bounces = bounces
140-
scrollView.alwaysBounceVertical = alwaysBounceVertical
141-
scrollView.alwaysBounceHorizontal = alwaysBounceHorizontal
142-
scrollView.showsVerticalScrollIndicator = showsVerticalScrollIndicator
143-
scrollView.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator
162+
163+
updateScrollView(scrollView)
144164

145165
let host = context.coordinator.host!
146166
host.rootView = content()

BBSwiftUIKit/BBSwiftUIKit/BBTableView.swift

Lines changed: 118 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,86 @@
88

99
import SwiftUI
1010

11-
public struct BBTableView<Data, Content>: UIViewControllerRepresentable where Data : RandomAccessCollection, Content : View, Data.Element : Equatable {
11+
public extension BBTableView {
12+
func bb_reloadData(_ reloadData: Binding<Bool>) -> Self {
13+
var view = self
14+
view._reloadData = reloadData
15+
return view
16+
}
17+
18+
func bb_reloadRows(_ reloadRows: Binding<[Int]>) -> Self {
19+
var view = self
20+
view._reloadRows = reloadRows
21+
return view
22+
}
23+
24+
func bb_contentOffset(_ contentOffset: Binding<CGPoint>) -> Self {
25+
var view = self
26+
view._contentOffset = contentOffset
27+
return view
28+
}
29+
30+
func bb_contentOffsetToScrollAnimated(_ contentOffsetToScrollAnimated: Binding<CGPoint?>) -> Self {
31+
var view = self
32+
view._contentOffsetToScrollAnimated = contentOffsetToScrollAnimated
33+
return view
34+
}
35+
}
36+
37+
public struct BBTableView<Data, Content>: UIViewControllerRepresentable, BBUIScrollViewRepresentable where Data : RandomAccessCollection, Content : View, Data.Element : Equatable {
1238
let data: Data
1339
let content: (Data.Element) -> Content
40+
41+
@Binding public var reloadData: Bool
42+
@Binding public var reloadRows: [Int]
43+
@Binding public var contentOffset: CGPoint
44+
@Binding public var contentOffsetToScrollAnimated: CGPoint?
45+
public var isPagingEnabled: Bool
46+
public var bounces: Bool
47+
public var alwaysBounceVertical: Bool
48+
public var alwaysBounceHorizontal: Bool
49+
public var showsVerticalScrollIndicator: Bool
50+
public var showsHorizontalScrollIndicator: Bool
1451

15-
public init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content) {
52+
public init(_ data: Data,
53+
reloadData: Binding<Bool> = .constant(false),
54+
reloadRows: Binding<[Int]> = .constant([]),
55+
contentOffset: Binding<CGPoint> = .constant(.bb_invalidContentOffset),
56+
contentOffsetToScrollAnimated: Binding<CGPoint?> = .constant(nil),
57+
isPagingEnabled: Bool = false,
58+
bounces: Bool = true,
59+
alwaysBounceVertical: Bool = false,
60+
alwaysBounceHorizontal: Bool = false,
61+
showsVerticalScrollIndicator: Bool = true,
62+
showsHorizontalScrollIndicator: Bool = true,
63+
@ViewBuilder content: @escaping (Data.Element) -> Content)
64+
{
1665
self.data = data
1766
self.content = content
67+
self._reloadData = reloadData
68+
self._reloadRows = reloadRows
69+
self._contentOffset = contentOffset
70+
self._contentOffsetToScrollAnimated = contentOffsetToScrollAnimated
71+
self.isPagingEnabled = isPagingEnabled
72+
self.bounces = bounces
73+
self.alwaysBounceVertical = alwaysBounceVertical
74+
self.alwaysBounceHorizontal = alwaysBounceHorizontal
75+
self.showsVerticalScrollIndicator = showsVerticalScrollIndicator
76+
self.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator
1877
}
1978

2079
public func makeUIViewController(context: Context) -> UIViewController {
2180
_BBTableViewController(self)
2281
}
2382

24-
public func updateUIViewController(_ vc: UIViewController, context: Context) {
25-
(vc as! _BBTableViewController).update(self)
83+
public func updateUIViewController(_ viewController: UIViewController, context: Context) {
84+
let vc = viewController as! _BBTableViewController<Data, Content>
85+
updateScrollView(vc.tableView)
86+
vc.update(self)
2687
}
2788
}
2889

29-
private class _BBTableViewController<Data, Content>: UIViewController, UITableViewDataSource where Data: RandomAccessCollection, Content: View, Data.Element: Equatable {
90+
private class _BBTableViewController<Data, Content>: UIViewController, UITableViewDataSource, UITableViewDelegate where Data: RandomAccessCollection, Content: View, Data.Element: Equatable {
3091
var representable: BBTableView<Data, Content>
3192
var tableView: UITableView!
3293

@@ -50,6 +111,7 @@ private class _BBTableViewController<Data, Content>: UIViewController, UITableVi
50111
tableView.register(_BBTableViewHostCell<Content>.self, forCellReuseIdentifier: "cell")
51112
tableView.separatorStyle = .none
52113
tableView.dataSource = self
114+
tableView.delegate = self
53115
view.addSubview(tableView)
54116

55117
NSLayoutConstraint.activate([
@@ -61,26 +123,52 @@ private class _BBTableViewController<Data, Content>: UIViewController, UITableVi
61123
}
62124

63125
func update(_ newRepresentable: BBTableView<Data, Content>) {
64-
if tableView.window == nil { return }
65-
66-
var removals: [IndexPath] = []
67-
var insertions: [IndexPath] = []
68-
let diff = newRepresentable.data.difference(from: data)
69-
for step in diff {
70-
switch step {
71-
case let .remove(i, _, _): removals.append(IndexPath(row: i, section: 0))
72-
case let .insert(i, _, _): insertions.append(IndexPath(row: i, section: 0))
126+
if newRepresentable.reloadData {
127+
representable = newRepresentable
128+
tableView.reloadData()
129+
130+
DispatchQueue.main.async {
131+
self.representable.reloadData = false
132+
self.representable.reloadRows.removeAll()
133+
}
134+
} else {
135+
var removals: [IndexPath] = []
136+
var insertions: [IndexPath] = []
137+
let diff = newRepresentable.data.difference(from: data)
138+
for step in diff {
139+
switch step {
140+
case let .remove(i, _, _): removals.append(IndexPath(row: i, section: 0))
141+
case let .insert(i, _, _): insertions.append(IndexPath(row: i, section: 0))
142+
}
143+
}
144+
145+
representable = newRepresentable
146+
147+
if !removals.isEmpty || !insertions.isEmpty {
148+
tableView.performBatchUpdates({
149+
tableView.deleteRows(at: removals, with: .automatic)
150+
tableView.insertRows(at: insertions, with: .automatic)
151+
}, completion: nil)
152+
}
153+
154+
if !representable.reloadRows.isEmpty {
155+
tableView.reloadRows(at: representable.reloadRows.map { IndexPath(row: $0, section: 0) }, with: .automatic)
156+
157+
DispatchQueue.main.async {
158+
self.representable.reloadRows.removeAll()
159+
}
73160
}
74161
}
75162

76-
representable = newRepresentable
77-
78-
tableView.beginUpdates()
79-
if !removals.isEmpty { tableView.deleteRows(at: removals, with: .automatic) }
80-
if !insertions.isEmpty { tableView.insertRows(at: insertions, with: .automatic) }
81-
tableView.endUpdates()
82-
83-
if let visibleIndexPaths = tableView.indexPathsForVisibleRows { tableView.reloadRows(at: visibleIndexPaths, with: .automatic) }
163+
// TODO: Scroll to row
164+
if let contentOffset = representable.contentOffsetToScrollAnimated {
165+
tableView.setContentOffset(contentOffset, animated: true)
166+
DispatchQueue.main.async {
167+
self.representable.contentOffsetToScrollAnimated = nil
168+
}
169+
} else if representable.contentOffset != .bb_invalidContentOffset {
170+
tableView.contentOffset = representable.contentOffset
171+
}
84172
}
85173

86174
// MARK: UITableViewDataSource
@@ -94,6 +182,14 @@ private class _BBTableViewController<Data, Content>: UIViewController, UITableVi
94182
cell.update(view, parent: self)
95183
return cell
96184
}
185+
186+
// MARK: UITableViewDelegate
187+
188+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
189+
DispatchQueue.main.async {
190+
self.representable.contentOffset = scrollView.contentOffset
191+
}
192+
}
97193
}
98194

99195
private class _BBTableViewHostCell<Content: View>: UITableViewCell {

BBSwiftUIKitDemo/BBSwiftUIKitDemo/TableViewExample.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import BBSwiftUIKit
1212
struct TableViewExample: View {
1313
@State var list = 0..<100
1414
@State var updateHeight = false
15+
@State var reloadData = false
16+
@State var reloadRows: [Int] = []
17+
@State var contentOffset: CGPoint = .zero
18+
@State var contentOffsetToScrollAnimated: CGPoint? = nil
1519

1620
var body: some View {
1721
VStack {
@@ -30,14 +34,41 @@ struct TableViewExample: View {
3034
.background(Color.orange)
3135
}
3236
}
33-
Button("Update") {
37+
.bb_reloadData($reloadData)
38+
.bb_reloadRows($reloadRows)
39+
.bb_contentOffsetToScrollAnimated($contentOffsetToScrollAnimated)
40+
.bb_contentOffset($contentOffset)
41+
42+
Slider(value: $contentOffset.y, in: 0...1000)
43+
44+
Button("Scroll to y = 1000") {
45+
self.contentOffsetToScrollAnimated = CGPoint(x: 0, y: 1000)
46+
}
47+
.padding()
48+
49+
Button("Reload data") {
50+
if self.list.count > 50 {
51+
self.list = 0..<50
52+
} else {
53+
self.list = 0..<100
54+
}
55+
self.updateHeight.toggle()
56+
57+
self.reloadData = true
58+
}
59+
.padding()
60+
61+
Button("Reload rows") {
3462
if self.list.count > 50 {
3563
self.list = 0..<50
3664
} else {
3765
self.list = 0..<100
3866
}
3967
self.updateHeight.toggle()
68+
69+
self.reloadRows = (0..<10).map { $0 }
4070
}
71+
.padding()
4172
}
4273
}
4374
}

0 commit comments

Comments
 (0)