From 9ae86e2277e5c8a8b8491a14b5311e5c5d05b524 Mon Sep 17 00:00:00 2001 From: Mohamed Afifi Date: Thu, 18 Jan 2024 22:39:48 -0500 Subject: [PATCH] Support using collection view safe area insets as cell layout margins --- UI/NoorUI/Pager/PageViewController.swift | 2 +- .../CollectionView/CollectionView.swift | 11 ++++ .../CollectionViewController.swift | 50 ++++++++++++++--- .../HostingCollectionViewCell.swift | 32 +++++++++++ .../Miscellaneous/DisableSafeAreaInsets.swift | 54 ------------------- 5 files changed, 88 insertions(+), 61 deletions(-) delete mode 100644 UI/UIx/SwiftUI/Miscellaneous/DisableSafeAreaInsets.swift diff --git a/UI/NoorUI/Pager/PageViewController.swift b/UI/NoorUI/Pager/PageViewController.swift index 47963b17..887dc198 100644 --- a/UI/NoorUI/Pager/PageViewController.swift +++ b/UI/NoorUI/Pager/PageViewController.swift @@ -247,7 +247,7 @@ extension _PageViewController { self.element = element super.init(rootView: rootView) view.backgroundColor = .clear - _disableSafeAreaInsets() + _disableSafeArea = true } @available(*, unavailable) diff --git a/UI/UIx/SwiftUI/CollectionView/CollectionView.swift b/UI/UIx/SwiftUI/CollectionView/CollectionView.swift index ce1154f1..dc5f8ca9 100644 --- a/UI/UIx/SwiftUI/CollectionView/CollectionView.swift +++ b/UI/UIx/SwiftUI/CollectionView/CollectionView.swift @@ -37,6 +37,7 @@ public struct CollectionView< configure: configure, content: content, isPagingEnabled: isPagingEnabled, + usesCollectionViewSafeAreaForCellLayoutMargins: usesCollectionViewSafeAreaForCellLayoutMargins, scrollAnchorId: scrollAnchorId, scrollAnchor: scrollAnchor ) @@ -64,6 +65,12 @@ public struct CollectionView< } } + public func usesCollectionViewSafeAreaForCellLayoutMargins(_ flag: Bool) -> Self { + mutateSelf { + $0.usesCollectionViewSafeAreaForCellLayoutMargins = flag + } + } + // MARK: Private private let layout: UICollectionViewLayout @@ -72,6 +79,7 @@ public struct CollectionView< private var configure: ((UICollectionView) -> Void)? private var isPagingEnabled: Bool = false + private var usesCollectionViewSafeAreaForCellLayoutMargins: Bool = false private var scrollAnchorId: Binding? private var scrollAnchor: ScrollAnchor = .center @@ -92,6 +100,7 @@ private struct CollectionViewBody< let content: (SectionId, Item) -> ItemContent let isPagingEnabled: Bool + let usesCollectionViewSafeAreaForCellLayoutMargins: Bool let scrollAnchorId: Binding? let scrollAnchor: ScrollAnchor @@ -123,6 +132,8 @@ private struct CollectionViewBody< viewController.dataSource?.sections = sections + viewController.usesCollectionViewSafeAreaForCellLayoutMargins = usesCollectionViewSafeAreaForCellLayoutMargins + viewController.scroller.isPagingEnabled = isPagingEnabled viewController.scroller.onScrollAnchorIdUpdated = { scrollAnchorId?.wrappedValue = $0 diff --git a/UI/UIx/SwiftUI/CollectionView/CollectionViewController.swift b/UI/UIx/SwiftUI/CollectionView/CollectionViewController.swift index de24a82a..8ae00fcf 100644 --- a/UI/UIx/SwiftUI/CollectionView/CollectionViewController.swift +++ b/UI/UIx/SwiftUI/CollectionView/CollectionViewController.swift @@ -39,12 +39,29 @@ final class CollectionViewController< let collectionView: UICollectionView lazy var scroller = CollectionViewScroller(collectionView: collectionView) + var usesCollectionViewSafeAreaForCellLayoutMargins = true { + didSet { + if usesCollectionViewSafeAreaForCellLayoutMargins != oldValue { + updateLayoutMarginsForVisibleCells() + } + } + } + var dataSource: CollectionViewDataSource? { didSet { scroller.dataSource = dataSource } } + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + updateLayoutMarginsForVisibleCells() + } + + override func loadView() { + view = collectionView + } + override func viewIsAppearing(_ animated: Bool) { super.viewIsAppearing(animated) scroller.scrollToInitialItemIfNeeded() @@ -63,7 +80,15 @@ final class CollectionViewController< // MARK: - Cell Lifecycle func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - (cell as? CellType)?.cellWillDisplay(animated: false) + guard let typedCell = cell as? CellType else { + return + } + typedCell.updateLayoutMargins( + usesCollectionViewSafeAreaForCellLayoutMargins: usesCollectionViewSafeAreaForCellLayoutMargins, + collectionViewSafeAreaInsets: view.safeAreaInsets + ) + + typedCell.cellWillDisplay(animated: false) } func collectionView( @@ -71,7 +96,10 @@ final class CollectionViewController< didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath ) { - (cell as? CellType)?.cellDidEndDisplaying(animated: false) + guard let typedCell = cell as? CellType else { + return + } + typedCell.cellDidEndDisplaying(animated: false) } // MARK: - Paging @@ -104,10 +132,6 @@ final class CollectionViewController< scroller.endInteractiveScrolling() } - override func loadView() { - view = collectionView - } - // MARK: Private private func setUpDataSource(content: @escaping (SectionId, Item) -> ItemContent) { @@ -129,7 +153,21 @@ final class CollectionViewController< let cell = collectionView.dequeueReusableCell(CellType.self, for: indexPath) cell.configure(content: content(section.id, item), dataId: itemId) + cell.updateLayoutMargins( + usesCollectionViewSafeAreaForCellLayoutMargins: usesCollectionViewSafeAreaForCellLayoutMargins, + collectionViewSafeAreaInsets: view.safeAreaInsets + ) + return cell } } + + private func updateLayoutMarginsForVisibleCells() { + for cell in collectionView.visibleCells { + (cell as? CellType)?.updateLayoutMargins( + usesCollectionViewSafeAreaForCellLayoutMargins: usesCollectionViewSafeAreaForCellLayoutMargins, + collectionViewSafeAreaInsets: view.safeAreaInsets + ) + } + } } diff --git a/UI/UIx/SwiftUI/CollectionView/HostingCollectionViewCell.swift b/UI/UIx/SwiftUI/CollectionView/HostingCollectionViewCell.swift index f4779ceb..2147f579 100644 --- a/UI/UIx/SwiftUI/CollectionView/HostingCollectionViewCell.swift +++ b/UI/UIx/SwiftUI/CollectionView/HostingCollectionViewCell.swift @@ -10,6 +10,18 @@ import SwiftUI final class HostingCollectionViewCell: UICollectionViewCell { // MARK: Internal + override var safeAreaInsets: UIEdgeInsets { + .zero + } + + func updateLayoutMargins(usesCollectionViewSafeAreaForCellLayoutMargins: Bool, collectionViewSafeAreaInsets: UIEdgeInsets) { + if usesCollectionViewSafeAreaForCellLayoutMargins { + cellLayoutMargins = .useCollectionViewSafeArea(collectionViewSafeAreaInsets) + } else { + cellLayoutMargins = .zero + } + } + func configure(content: Content, dataId: AnyHashable) { self.dataId = dataId let content = EpoxySwiftUIHostingView.Content(rootView: content, dataID: dataId) @@ -34,6 +46,12 @@ final class HostingCollectionViewCell: UICollectionViewCell { private var dataId: AnyHashable? private var hostingView: EpoxySwiftUIHostingView? + private var cellLayoutMargins: CollectionViewCellLayoutMargins = .zero { + didSet { + updateLayoutMarginsIfNeeded() + } + } + private func setViewIfNeeded(view: EpoxySwiftUIHostingView) { guard hostingView == nil else { return @@ -52,4 +70,18 @@ final class HostingCollectionViewCell: UICollectionViewCell { view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) } + + private func updateLayoutMarginsIfNeeded() { + switch cellLayoutMargins { + case .zero: + hostingView?.layoutMargins = .zero + case .useCollectionViewSafeArea(let insets): + hostingView?.layoutMargins = insets + } + } +} + +private enum CollectionViewCellLayoutMargins { + case zero + case useCollectionViewSafeArea(_ safeArea: UIEdgeInsets) } diff --git a/UI/UIx/SwiftUI/Miscellaneous/DisableSafeAreaInsets.swift b/UI/UIx/SwiftUI/Miscellaneous/DisableSafeAreaInsets.swift deleted file mode 100644 index fda003a8..00000000 --- a/UI/UIx/SwiftUI/Miscellaneous/DisableSafeAreaInsets.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// DisableSafeAreaInsets.swift -// -// -// Created by Mohamed Afifi on 2023-12-25. -// - -import SwiftUI - -// From:https://github.com/SwiftUIX/SwiftUIX/blob/2bf8eda3acad39b0419e4053d321059030cfa04b/Sources/SwiftUIX/Intramodular/Bridging/CocoaHostingController.swift#L263C17-L263C39 - -extension UIHostingController { - /// https://twitter.com/b3ll/status/1193747288302075906 - public func _disableSafeAreaInsets() { - guard let viewClass = object_getClass(view), !String(cString: class_getName(viewClass)).hasSuffix("_SwiftUIX_patched") else { - return - } - - let className = String(cString: class_getName(viewClass)).appending("_SwiftUIX_patched") - - if let viewSubclass = NSClassFromString(className) { - object_setClass(view, viewSubclass) - } else { - className.withCString { className in - guard let subclass = objc_allocateClassPair(viewClass, className, 0) else { - return - } - - if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) { - let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in - .zero - } - - class_addMethod(subclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method)) - } - - if let method2 = class_getInstanceMethod(viewClass, #selector(getter: UIView.safeAreaLayoutGuide)) { - let safeAreaLayoutGuide: @convention(block) (AnyObject) -> UILayoutGuide? = { (_: AnyObject!) -> UILayoutGuide? in - nil - } - - class_replaceMethod(viewClass, #selector(getter: UIView.safeAreaLayoutGuide), imp_implementationWithBlock(safeAreaLayoutGuide), method_getTypeEncoding(method2)) - } - - objc_registerClassPair(subclass) - object_setClass(view, subclass) - } - - view.setNeedsDisplay() - view.setNeedsLayout() - view.layoutIfNeeded() - } - } -}