-
-
Save KazaiMazai/ca9e18f76e02ff9d17c99846ab8cea1c to your computer and use it in GitHub Desktop.
| import UIKit | |
| import SwiftUI | |
| public protocol SectionProtocol: Hashable { | |
| associatedtype Item: Hashable | |
| var items: [Item] { get } | |
| } | |
| extension CollectionView { | |
| public typealias DataSource = UICollectionViewDiffableDataSource<Section, Item> | |
| public typealias SnapShot = NSDiffableDataSourceSnapshot<Section, Item> | |
| public typealias CellProvider = DataSource.CellProvider | |
| public typealias SupplementaryViewProvider = | |
| (SnapShot, UICollectionView, String, IndexPath) -> UICollectionReusableView? | |
| public typealias CollectionViewProvider = () -> UICollectionView | |
| public typealias CollectionViewUpdateCompleteHandler = (UICollectionView) -> Void | |
| } | |
| public struct CollectionView<Section, Item> | |
| where | |
| Section: SectionProtocol, | |
| Section.Item == Item { | |
| private let collectionViewProvider: CollectionViewProvider | |
| private let cellProvider: CellProvider | |
| private let supplementaryViewProvider: SupplementaryViewProvider? | |
| private let updateCompleteHandler: CollectionViewUpdateCompleteHandler? | |
| private var animateCollectionUpdates: Bool = true | |
| private let sections: [Section] | |
| public init(sections: [Section], | |
| collectionViewProvider: @escaping CollectionViewProvider, | |
| cellProvider: @escaping CellProvider, | |
| supplementaryViewProvider: SupplementaryViewProvider? = nil, | |
| updateCompleteHandler: CollectionViewUpdateCompleteHandler? = nil) { | |
| self.collectionViewProvider = collectionViewProvider | |
| self.cellProvider = cellProvider | |
| self.sections = sections | |
| self.supplementaryViewProvider = supplementaryViewProvider | |
| self.updateCompleteHandler = updateCompleteHandler | |
| } | |
| } | |
| extension CollectionView: UIViewRepresentable { | |
| public func makeCoordinator() -> Coordinator { | |
| Coordinator() | |
| } | |
| public func makeUIView(context: UIViewRepresentableContext<CollectionView>) -> UICollectionView { | |
| let collectionView = collectionViewProvider() | |
| let datasource = DataSource(collectionView: collectionView, | |
| cellProvider: cellProvider) | |
| datasource.setSupplementaryViewProvider(with: supplementaryViewProvider) | |
| context.coordinator.datasource = datasource | |
| return collectionView | |
| } | |
| public func updateUIView(_ uiView: UICollectionView, | |
| context: UIViewRepresentableContext<CollectionView>) { | |
| context.coordinator.applySnapshotInBackground(sections: sections, | |
| animated: animateCollectionUpdates) { | |
| updateCompleteHandler?(uiView) | |
| } | |
| } | |
| } | |
| extension UICollectionViewDiffableDataSource where SectionIdentifierType: SectionProtocol, | |
| SectionIdentifierType.Item == ItemIdentifierType { | |
| func setSupplementaryViewProvider( | |
| with provider: CollectionView<SectionIdentifierType, ItemIdentifierType>.SupplementaryViewProvider?) { | |
| guard let provider = provider else { | |
| supplementaryViewProvider = nil | |
| return | |
| } | |
| supplementaryViewProvider = { [weak self] (collecion, kind, idx) in | |
| guard let self = self else { | |
| return nil | |
| } | |
| return provider(self.snapshot(), collecion, kind, idx) | |
| } | |
| } | |
| } | |
| public extension CollectionView { | |
| func animatingDifferences(_ animated: Bool) -> Self { | |
| var selfCopy = self | |
| selfCopy.animateCollectionUpdates = animated | |
| return selfCopy | |
| } | |
| } |
| import UIKit | |
| import SwiftUI | |
| extension CollectionView { | |
| public class Coordinator: NSObject { | |
| var datasource: DataSource? | |
| private let updateQueue = DispatchQueue( | |
| label: "CollectionView.Coordinator.Update.Queue", | |
| qos: .userInteractive) | |
| func applySnapshotInBackground(sections: [Section], | |
| animated: Bool, | |
| complete: @escaping () -> Void) { | |
| updateQueue.async { [weak self] in | |
| guard let self = self else { | |
| DispatchQueue.main.async { | |
| complete() | |
| } | |
| return | |
| } | |
| self.applySnapshot(sections: sections, animated: animated) { | |
| DispatchQueue.main.async { | |
| complete() | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| extension CollectionView.Coordinator { | |
| private func applySnapshot(sections: [Section], | |
| animated: Bool, | |
| complete: @escaping () -> Void) { | |
| guard let datasource = self.datasource else { | |
| complete() | |
| return | |
| } | |
| var snapshot = CollectionView.SnapShot() | |
| snapshot.appendSections(sections) | |
| sections.forEach { | |
| snapshot.appendItems($0.items, toSection: $0) | |
| } | |
| datasource.apply(snapshot, animatingDifferences: animated) { | |
| complete() | |
| } | |
| } | |
| } |
| public protocol ReusableView { | |
| } | |
| public extension ReusableView { | |
| static var reuseIdentifier: String { | |
| "\(self)" | |
| } | |
| } | |
| public class HostingCollectionViewCell<Content: View>: UICollectionViewCell | |
| where Content: ReusableView { | |
| private var hostingController: UIHostingController<Content>? | |
| override init(frame: CGRect) { | |
| super.init(frame: frame) | |
| } | |
| required init?(coder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| public static var reuseIdentifier: String { | |
| String(describing: Content.reuseIdentifier) | |
| } | |
| public func set(rootView: Content) { | |
| guard let host = hostingController else { | |
| let host = UIHostingController(rootView: rootView) | |
| let parentController = resolveParentViewController() | |
| parentController?.addChild(host) | |
| addSubview(host.view) | |
| host.view.translatesAutoresizingMaskIntoConstraints = false | |
| NSLayoutConstraint.activate([ | |
| leadingAnchor.constraint(equalTo: host.view.leadingAnchor), | |
| trailingAnchor.constraint(equalTo: host.view.trailingAnchor), | |
| topAnchor.constraint(equalTo: host.view.topAnchor), | |
| bottomAnchor.constraint(equalTo: host.view.bottomAnchor) | |
| ]) | |
| parentController.map { host.didMove(toParent: $0) } | |
| return | |
| } | |
| host.rootView = rootView | |
| host.view.invalidateIntrinsicContentSize() | |
| } | |
| deinit { | |
| hostingController?.willMove(toParent: nil) | |
| hostingController?.view.removeFromSuperview() | |
| hostingController?.removeFromParent() | |
| hostingController = nil | |
| } | |
| } | |
| fileprivate extension UIView { | |
| func resolveParentViewController() -> UIViewController? { | |
| var parentResponder: UIResponder? = self | |
| while parentResponder != nil { | |
| parentResponder = parentResponder?.next | |
| if let viewController = parentResponder as? UIViewController { | |
| return viewController | |
| } | |
| } | |
| return nil | |
| } | |
| } |
@KazaiMazai - I have been using your updated implementation of CollectionView and I am running into some issues. 1) The functions CollectionView.animateDifferences(...), .onUpdate(...), and .collectionViewDelegate(...) need to return selfCopy rather than self. 2) The UICollectionViewDelegate is held as a weak reference and is therefore nil upon returning from CollectionView.makeUIView(...). I have worked around these issues by modifying your code specifically for my use (not a general solution). If you would like to resolve the issues, I would be happy to give you feedback on the fix. Thanks for your efforts.
@jafienberg
Hey! Just noticed you reply that I missed a year ago. Thanks for pointing it out. Fixed the issues in the latest revision.
@KazaiMazai - I have been using your updated implementation of CollectionView and I am running into some issues. 1) The functions CollectionView.animateDifferences(...), .onUpdate(...), and .collectionViewDelegate(...) need to return selfCopy rather than self. 2) The UICollectionViewDelegate is held as a weak reference and is therefore nil upon returning from CollectionView.makeUIView(...). I have worked around these issues by modifying your code specifically for my use (not a general solution). If you would like to resolve the issues, I would be happy to give you feedback on the fix. Thanks for your efforts.