Last active
August 19, 2024 10:03
-
-
Save KazaiMazai/ca9e18f76e02ff9d17c99846ab8cea1c to your computer and use it in GitHub Desktop.
SwiftUI wrapper for UICollectionView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@jafienberg
Hey! Just noticed you reply that I missed a year ago. Thanks for pointing it out. Fixed the issues in the latest revision.