Skip to content

Commit

Permalink
Added capability to auto resolve container scrollview for table viewp…
Browse files Browse the repository at this point in the history
…ort calculations
  • Loading branch information
rajdeep committed Oct 14, 2024
1 parent e374630 commit 42b28ef
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
8 changes: 4 additions & 4 deletions Proton/Sources/Swift/Helpers/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
68 changes: 63 additions & 5 deletions Proton/Sources/Swift/Table/TableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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()
}()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -834,7 +892,7 @@ extension TableView: UIScrollViewDelegate {

extension TableView: TableContentViewDelegate {
var containerScrollView: UIScrollView? {
delegate?.containerScrollView
delegate?.containerScrollView ?? _containerScrollView
}

var viewport: CGRect? {
Expand Down
26 changes: 26 additions & 0 deletions Proton/Tests/Table/TableViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 42b28ef

Please sign in to comment.