Skip to content

Commit 09106c1

Browse files
committed
Added capability to auto resolve container scrollview for table viewport calculations
1 parent e374630 commit 09106c1

File tree

4 files changed

+99
-11
lines changed

4 files changed

+99
-11
lines changed

Diff for: ExampleApp/ExampleApp/AdvancedFeatures/CommandsExampleViewController.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -487,12 +487,15 @@ extension CommandsExampleViewController: TableViewDelegate {
487487
viewport.size.width -= (offsetX * 2)
488488
viewport.size.height -= (offsetY * 2)
489489

490-
Utility.drawRect(rect: CGRect(origin: CGPoint(x: offsetX, y: offsetY), size: viewport.size), color: .red, in: editor)
491490
return viewport
492491
}
493492

494493
var containerScrollView: UIScrollView? {
495-
editor.scrollView
494+
nil
495+
}
496+
497+
var resolvedViewportBorderDisplay: ViewportBorderDisplay {
498+
.visible(color: .red, borderWidth: 1)
496499
}
497500

498501
func tableView(_ tableView: TableView, didReceiveKey key: EditorKey, at range: NSRange, in cell: TableCell) { }

Diff for: Proton/Sources/Swift/Helpers/Utility.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,18 @@ import UIKit
2525
public class Utility {
2626
private init() { }
2727

28-
public static func drawRect(rect: CGRect, color: UIColor, in view: UIView, name: String = "rect_layer") {
28+
public static func drawRect(rect: CGRect, color: UIColor, borderWidth: CGFloat = 1.0, in view: UIView, name: String = "rect_layer") {
2929
let path = UIBezierPath(rect: rect).cgPath
30-
drawPath(path: path, color: color, in: view, name: name)
30+
drawPath(path: path, color: color, borderWidth: borderWidth, in: view, name: name)
3131
}
3232

33-
public static func drawPath(path: CGPath, color: UIColor, in view: UIView, name: String = "path_layer") {
33+
public static func drawPath(path: CGPath, color: UIColor, borderWidth: CGFloat = 1.0, in view: UIView, name: String = "path_layer") {
3434
let existingLayer = view.layer.sublayers?.first(where: { $0.name == name}) as? CAShapeLayer
3535
let shapeLayer = existingLayer ?? CAShapeLayer()
3636
shapeLayer.path = path
3737
shapeLayer.strokeColor = color.cgColor
3838
shapeLayer.fillColor = UIColor.clear.cgColor
39-
shapeLayer.lineWidth = 1.0
39+
shapeLayer.lineWidth = borderWidth
4040
shapeLayer.name = name
4141

4242
if existingLayer == nil {

Diff for: Proton/Sources/Swift/Table/TableView.swift

+63-5
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,24 @@ public protocol TableCellLifeCycleObserver: AnyObject {
3838
func tableView(_ tableView: TableView, didRemoveCellFromViewport cell: TableCell)
3939
}
4040

41+
42+
public enum ViewportBorderDisplay {
43+
case hidden
44+
case visible(color: UIColor, borderWidth: CGFloat)
45+
}
46+
4147
/// An object capable of handing `TableView` events
4248
public protocol TableViewDelegate: AnyObject {
4349
var containerScrollView: UIScrollView? { get }
4450

4551
var viewport: CGRect? { get }
4652

53+
/// Governs whether resolved viewport is displayed
54+
/// - Note: This may be used for debugging purposes.
55+
/// - Important: It is responsibility of consumer of the API to ensure that this is not displayed in app if not intended to. i.e. display of viewport does not
56+
/// check for DEBUG flags and would be displayed based on value provided.
57+
var resolvedViewportBorderDisplay: ViewportBorderDisplay { get }
58+
4759
/// Invoked when `EditorView` within the cell receives focus
4860
/// - Parameters:
4961
/// - tableView: TableView containing cell
@@ -145,6 +157,10 @@ public protocol TableViewDelegate: AnyObject {
145157
func tableView(_ tableView: TableView, didRemoveCellFromViewport cell: TableCell)
146158
}
147159

160+
public extension TableViewDelegate {
161+
var resolvedViewportBorderDisplay: ViewportBorderDisplay { .hidden }
162+
}
163+
148164
/// A view that provides a tabular structure where each cell is an `EditorView`.
149165
/// Since the cells contains an `EditorView` in itself, it is capable of hosting any attachment that `EditorView` can host
150166
/// including another `TableView` as an attachment.
@@ -165,6 +181,12 @@ public class TableView: UIView {
165181

166182
private let repository = TableCellRepository()
167183

184+
private var _containerScrollView: UIScrollView? {
185+
didSet {
186+
_containerScrollView != nil ? setupScrollObserver() : removeScrollObserver()
187+
}
188+
}
189+
168190
private lazy var columnRightBorderView: UIView = {
169191
makeSelectionBorderView()
170192
}()
@@ -345,6 +367,40 @@ public class TableView: UIView {
345367
}
346368
}
347369

370+
public override func didMoveToWindow() {
371+
guard window != nil else {
372+
removeScrollObserver()
373+
return
374+
}
375+
376+
// Only try to auto resolve container scrollview, if not already provided by the delegate
377+
guard self.containerScrollView == nil else { return }
378+
379+
// If table has the Editor which is scrollable, use that as container for viewport
380+
let containerEditorView = self.containerAttachment?.containerEditorView
381+
if let scrollView = containerEditorView?.scrollView, scrollView.isScrollEnabled {
382+
_containerScrollView = scrollView
383+
return
384+
}
385+
386+
// Else, find the next available scrollview up the hierarchy
387+
var currentView: UIView? = containerEditorView
388+
while let view = currentView, !(view is UIScrollView) {
389+
currentView = view.superview
390+
}
391+
392+
if let scrollView = currentView as? UIScrollView {
393+
_containerScrollView = scrollView
394+
}
395+
396+
// If there's still none, default to container editor scrollview
397+
// This would typically be the case where the Editor starts off as non-scrollable but becomes scrollable
398+
// as the content overflows in which case this should resolve correctly.
399+
if _containerScrollView == nil {
400+
_containerScrollView = containerEditorView?.scrollView
401+
}
402+
}
403+
348404
/// Maintains the scroll lock on the cell passed in if the original rect ends up moving as a result of cells getting rendered above this rect position
349405
/// - Parameters:
350406
/// - cell: Cell to lock on
@@ -399,7 +455,7 @@ public class TableView: UIView {
399455
}
400456

401457
private func setupScrollObserver() {
402-
observation = delegate?.containerScrollView?.observe(\.bounds, options: [.new, .old]) { [weak self] container, change in
458+
observation = containerScrollView?.observe(\.bounds, options: [.new, .old]) { [weak self] container, change in
403459
self?.viewportChanged()
404460
}
405461
}
@@ -450,7 +506,7 @@ public class TableView: UIView {
450506
// ensure editor is not hidden e.g. inside an Expand in collapsed state
451507
attachmentContentView.attachment?.containerEditorView?.isHidden == false,
452508
tableView.bounds != .zero,
453-
let containerScrollView = delegate?.containerScrollView,
509+
let containerScrollView = self.containerScrollView,
454510
let rootEditorView = containerAttachment?.containerEditorView?.rootEditor else {
455511
cellsInViewport = []
456512
return
@@ -472,8 +528,10 @@ public class TableView: UIView {
472528
// Convert the visible rectangle back to the nestedView's coordinate space
473529
let visibleRectOfNestedView = rootEditorView.convert(visibleRectOfNestedViewInScrollView, from: containerScrollView)
474530

475-
// Uncomment following line to show the resolved viewport
476-
// Utility.drawRect(rect: visibleRectOfNestedView, color: .red, in: rootEditorView, name: "viewport")
531+
if let viewportBorder = delegate?.resolvedViewportBorderDisplay,
532+
case let ViewportBorderDisplay.visible(color, borderWidth) = viewportBorder {
533+
Utility.drawRect(rect: visibleRectOfNestedView, color: color, borderWidth: borderWidth, in: rootEditorView, name: "viewport")
534+
}
477535

478536
let adjustedViewport = visibleRectOfNestedView.offsetBy(dx: tableView.bounds.minX, dy: tableView.bounds.minY)
479537

@@ -834,7 +892,7 @@ extension TableView: UIScrollViewDelegate {
834892

835893
extension TableView: TableContentViewDelegate {
836894
var containerScrollView: UIScrollView? {
837-
delegate?.containerScrollView
895+
delegate?.containerScrollView ?? _containerScrollView
838896
}
839897

840898
var viewport: CGRect? {

Diff for: Proton/Tests/Table/TableViewTests.swift

+27
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,33 @@ class TableViewTests: XCTestCase {
5252
window.rootViewController = viewController
5353
}
5454

55+
func testResovlesContainerScrollView() throws {
56+
delegate.containerScrollView = nil
57+
58+
let viewport = CGRect(x: 0, y: 100, width: 350, height: 200)
59+
delegate.viewport = viewport
60+
61+
let attachment = AttachmentGenerator.makeTableViewAttachment(
62+
id: 1,
63+
numRows: 20,
64+
numColumns: 5,
65+
initialRowHeight: 100
66+
)
67+
let tableView = attachment.view
68+
69+
tableView.delegate = delegate
70+
71+
editor.replaceCharacters(in: .zero, with: "Some text in editor")
72+
editor.insertAttachment(in: editor.textEndRange, attachment: attachment)
73+
editor.replaceCharacters(in: editor.textEndRange, with: "Text after grid")
74+
75+
viewController.render()
76+
77+
XCTAssertNil(delegate.containerScrollView)
78+
XCTAssertEqual(attachment.view.containerScrollView, editor.scrollView)
79+
80+
}
81+
5582
func testReusesTextFromPreRenderedCells() throws {
5683
delegate.containerScrollView = editor.scrollView
5784

0 commit comments

Comments
 (0)