diff --git a/ExampleApp/ExampleApp/AdvancedFeatures/CommandsExampleViewController.swift b/ExampleApp/ExampleApp/AdvancedFeatures/CommandsExampleViewController.swift index 86cc37d7..eb5b7e48 100644 --- a/ExampleApp/ExampleApp/AdvancedFeatures/CommandsExampleViewController.swift +++ b/ExampleApp/ExampleApp/AdvancedFeatures/CommandsExampleViewController.swift @@ -487,12 +487,15 @@ extension CommandsExampleViewController: TableViewDelegate { viewport.size.width -= (offsetX * 2) viewport.size.height -= (offsetY * 2) - Utility.drawRect(rect: CGRect(origin: CGPoint(x: offsetX, y: offsetY), size: viewport.size), color: .red, in: editor) return viewport } var containerScrollView: UIScrollView? { - editor.scrollView + nil + } + + var resolvedViewportBorderDisplay: ViewportBorderDisplay { + .visible(color: .red, borderWidth: 1) } func tableView(_ tableView: TableView, didReceiveKey key: EditorKey, at range: NSRange, in cell: TableCell) { } diff --git a/Proton/Sources/Swift/Helpers/Utility.swift b/Proton/Sources/Swift/Helpers/Utility.swift index eb666c5b..38b8c326 100644 --- a/Proton/Sources/Swift/Helpers/Utility.swift +++ b/Proton/Sources/Swift/Helpers/Utility.swift @@ -25,18 +25,18 @@ import UIKit public class Utility { private init() { } - public static func drawRect(rect: CGRect, color: UIColor, in view: UIView, name: String = "rect_layer") { + public static func drawRect(rect: CGRect, color: UIColor, borderWidth: CGFloat = 1.0, in view: UIView, name: String = "rect_layer") { let path = UIBezierPath(rect: rect).cgPath - drawPath(path: path, color: color, in: view, name: name) + drawPath(path: path, color: color, borderWidth: borderWidth, in: view, name: name) } - public static func drawPath(path: CGPath, color: UIColor, in view: UIView, name: String = "path_layer") { + public static func drawPath(path: CGPath, color: UIColor, borderWidth: CGFloat = 1.0, in view: UIView, name: String = "path_layer") { let existingLayer = view.layer.sublayers?.first(where: { $0.name == name}) as? CAShapeLayer let shapeLayer = existingLayer ?? CAShapeLayer() shapeLayer.path = path shapeLayer.strokeColor = color.cgColor shapeLayer.fillColor = UIColor.clear.cgColor - shapeLayer.lineWidth = 1.0 + shapeLayer.lineWidth = borderWidth shapeLayer.name = name if existingLayer == nil { diff --git a/Proton/Sources/Swift/Table/TableView.swift b/Proton/Sources/Swift/Table/TableView.swift index 3bfb8022..1bc3d2db 100644 --- a/Proton/Sources/Swift/Table/TableView.swift +++ b/Proton/Sources/Swift/Table/TableView.swift @@ -38,12 +38,24 @@ public protocol TableCellLifeCycleObserver: AnyObject { func tableView(_ tableView: TableView, didRemoveCellFromViewport cell: TableCell) } + +public enum ViewportBorderDisplay { + case hidden + case visible(color: UIColor, borderWidth: CGFloat) +} + /// An object capable of handing `TableView` events public protocol TableViewDelegate: AnyObject { var containerScrollView: UIScrollView? { get } var viewport: CGRect? { get } + /// Governs whether resolved viewport is displayed + /// - Note: This may be used for debugging purposes. + /// - 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 + /// check for DEBUG flags and would be displayed based on value provided. + var resolvedViewportBorderDisplay: ViewportBorderDisplay { get } + /// Invoked when `EditorView` within the cell receives focus /// - Parameters: /// - tableView: TableView containing cell @@ -145,6 +157,10 @@ public protocol TableViewDelegate: AnyObject { func tableView(_ tableView: TableView, didRemoveCellFromViewport cell: TableCell) } +public extension TableViewDelegate { + var resolvedViewportBorderDisplay: ViewportBorderDisplay { .hidden } +} + /// A view that provides a tabular structure where each cell is an `EditorView`. /// Since the cells contains an `EditorView` in itself, it is capable of hosting any attachment that `EditorView` can host /// including another `TableView` as an attachment. @@ -165,6 +181,12 @@ public class TableView: UIView { private let repository = TableCellRepository() + private var _containerScrollView: UIScrollView? { + didSet { + _containerScrollView != nil ? setupScrollObserver() : removeScrollObserver() + } + } + private lazy var columnRightBorderView: UIView = { makeSelectionBorderView() }() @@ -345,6 +367,40 @@ public class TableView: UIView { } } + public override func didMoveToWindow() { + guard window != nil else { + removeScrollObserver() + return + } + + // Only try to auto resolve container scrollview, if not already provided by the delegate + guard self.containerScrollView == nil else { return } + + // If table has the Editor which is scrollable, use that as container for viewport + let containerEditorView = self.containerAttachment?.containerEditorView + if let scrollView = containerEditorView?.scrollView, scrollView.isScrollEnabled { + _containerScrollView = scrollView + return + } + + // Else, find the next available scrollview up the hierarchy + var currentView: UIView? = containerEditorView + while let view = currentView, !(view is UIScrollView) { + currentView = view.superview + } + + if let scrollView = currentView as? UIScrollView { + _containerScrollView = scrollView + } + + // If there's still none, default to container editor scrollview + // This would typically be the case where the Editor starts off as non-scrollable but becomes scrollable + // as the content overflows in which case this should resolve correctly. + if _containerScrollView == nil { + _containerScrollView = containerEditorView?.scrollView + } + } + /// 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 /// - Parameters: /// - cell: Cell to lock on @@ -399,7 +455,7 @@ public class TableView: UIView { } private func setupScrollObserver() { - observation = delegate?.containerScrollView?.observe(\.bounds, options: [.new, .old]) { [weak self] container, change in + observation = containerScrollView?.observe(\.bounds, options: [.new, .old]) { [weak self] container, change in self?.viewportChanged() } } @@ -450,7 +506,7 @@ public class TableView: UIView { // ensure editor is not hidden e.g. inside an Expand in collapsed state attachmentContentView.attachment?.containerEditorView?.isHidden == false, tableView.bounds != .zero, - let containerScrollView = delegate?.containerScrollView, + let containerScrollView = self.containerScrollView, let rootEditorView = containerAttachment?.containerEditorView?.rootEditor else { cellsInViewport = [] return @@ -472,8 +528,10 @@ public class TableView: UIView { // Convert the visible rectangle back to the nestedView's coordinate space let visibleRectOfNestedView = rootEditorView.convert(visibleRectOfNestedViewInScrollView, from: containerScrollView) - // Uncomment following line to show the resolved viewport -// Utility.drawRect(rect: visibleRectOfNestedView, color: .red, in: rootEditorView, name: "viewport") + if let viewportBorder = delegate?.resolvedViewportBorderDisplay, + case let ViewportBorderDisplay.visible(color, borderWidth) = viewportBorder { + Utility.drawRect(rect: visibleRectOfNestedView, color: color, borderWidth: borderWidth, in: rootEditorView, name: "viewport") + } let adjustedViewport = visibleRectOfNestedView.offsetBy(dx: tableView.bounds.minX, dy: tableView.bounds.minY) @@ -834,7 +892,7 @@ extension TableView: UIScrollViewDelegate { extension TableView: TableContentViewDelegate { var containerScrollView: UIScrollView? { - delegate?.containerScrollView + delegate?.containerScrollView ?? _containerScrollView } var viewport: CGRect? { diff --git a/Proton/Tests/Table/TableViewTests.swift b/Proton/Tests/Table/TableViewTests.swift index c824ac03..6802921d 100644 --- a/Proton/Tests/Table/TableViewTests.swift +++ b/Proton/Tests/Table/TableViewTests.swift @@ -52,6 +52,32 @@ class TableViewTests: XCTestCase { window.rootViewController = viewController } + func testResolvesContainerScrollView() throws { + delegate.containerScrollView = nil + + let viewport = CGRect(x: 0, y: 100, width: 350, height: 200) + delegate.viewport = viewport + + let attachment = AttachmentGenerator.makeTableViewAttachment( + id: 1, + numRows: 20, + numColumns: 5, + initialRowHeight: 100 + ) + let tableView = attachment.view + + tableView.delegate = delegate + + editor.replaceCharacters(in: .zero, with: "Some text in editor") + editor.insertAttachment(in: editor.textEndRange, attachment: attachment) + editor.replaceCharacters(in: editor.textEndRange, with: "Text after grid") + + viewController.render() + + XCTAssertNil(delegate.containerScrollView) + XCTAssertEqual(attachment.view.containerScrollView, editor.scrollView) + } + func testReusesTextFromPreRenderedCells() throws { delegate.containerScrollView = editor.scrollView