-
-
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.
@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.
https://gist.github.com/KazaiMazai/d9458293c0ef2006bb39958dff624f08
As well as the updated post with some comments:
https://kazaimazai.com/taking-uicollectionview-to-swiftui/