I have a SwiftUI View that displays a UIViewControllerRepresentable which is wrapped in a NavigationView. I've gotten it all working to what I want though now I want to be able to push from the collection view another View when the user taps in a cell. The issue I am facing is that the Coordinator does not have access to the navigation controller. Is there a better implementation to this?
This is the initial SwiftUI View
import SwiftUI
struct ExploreView: View {
#ObservedObject var exploreViewVM = ExploreViewViewModel()
var body: some View {
NavigationView {
CollectionView(products: exploreViewVM.products)
}
}
}
struct ExploreView_Previews: PreviewProvider {
static var previews: some View {
ExploreView()
}
}
This is the collectionView wrapper. As you can see in the didSelectItemAt function, I am trying to wrap the view within a HostingController. I know it does not make any sense to instantiate the navigation controller with the root of hosting controller and then push to it. Though I am not sure how to go about it and would appreciate any help.
import SwiftUI
import SDWebImage
struct CollectionView: UIViewControllerRepresentable {
var products: [Product]
let cellId = "cellId"
typealias UIViewControllerType = UICollectionViewController
func makeUIViewController(context: Context) -> UICollectionViewController {
let collectionView = UICollectionViewController(collectionViewLayout: createLayout())
collectionView.collectionView.backgroundColor = .white
collectionView.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
collectionView.collectionView.dataSource = context.coordinator
collectionView.collectionView.delegate = context.coordinator
return collectionView
}
func createLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { sectionNumber, env in
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(0.3333), heightDimension: .fractionalWidth(0.3333)))
item.contentInsets.trailing = 1
item.contentInsets.bottom = 1
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(500)), subitems: [item])
group.contentInsets.leading = 1
let section = NSCollectionLayoutSection(group: group)
return section
}
}
func updateUIViewController(_ uiViewController: UICollectionViewController, context: Context) {
}
class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return parent.products.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: parent.cellId, for: indexPath)
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
imageView.sd_setImage(with: URL(string: parent.products[indexPath.item].imageUrl)) { (image, _, _, _) in
imageView.image = image
}
cell.backgroundView = imageView
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let controller = UIHostingController(rootView: ProductView(product: parent.products[indexPath.item]))
let navigation = UINavigationController(rootViewController: controller)
navigation.pushViewController(controller, animated: true)
}
let parent: CollectionView
init(_ parent: CollectionView) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
Related
In our app we have an infinite (vertical) scroll where we need pagination (snapping to cells - of different heights), as this seemed to be very tedious to implement with SwiftUI, we used this gist that wraps a UICollectionView with a UIViewControllerRepresentable. Since the scroll is infite, further data is appended to the initial data once the user reaches almost the end.
This all works well, except: when the data array gets updated, the scrolling animation (either the snapping or normal scroll) stops at once, without finishing the animation. This also happens when updating the data using withAnimation:
withAnimation {
// update data
}
My suspicion is that this is due to the way the UICollectionView is wrapped since when using the normal ScrollView of SwiftUI it all works fine.
How can I insert data to the UICollectionView without breaking the scrolling animation? Or is there a better way to wrap it than with the gist linked?
P.S. using some complex SwiftUI solution is also not ideal since we need a custom Scroll to refresh functionality, which would make things even more complex in SwiftUI but has been implemented very easily with the Representable.
MRE (source of wrapper: this gist):
import SwiftUI
import UIKit
struct Model: Identifiable {
let id = UUID().uuidString
}
class PostService: ObservableObject {
#Published var posts: [Model] = []
init() {
self.posts = [Model(), Model(), Model()]
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
print("adding data")
self.addData()
}
}
func addData() {
Task { // this is because in the real example there are async calls in here...
let newData = Model()
DispatchQueue.main.async {
withAnimation {
self.pooledPosts.append(newData)
}
}
}
}
}
struct ContentView: View {
#StateObject var postService: PostService
var body: some View {
CollectionView(
collection: postService.posts,
scrollDirection: .vertical,
contentSize: .crossAxisFilled(mainAxisLength: UIScreen.main.bounds.height - 100),
itemSpacing: .init(mainAxisSpacing: 0, crossAxisSpacing: 0),
rawCustomize: { collectionView in
collectionView.showsVerticalScrollIndicator = false
//collectionView.isPagingEnabled = true
// no matter what this is set to the issue is present
},
contentForData: MyCustomCell.init)
}
}
struct MyCustomCell : View {
let data: Model
var body: some View {
ZStack(alignment: .center) {
Text(self.data.id)
.font(.system(size: 24))
.foregroundColor(Color(hex: 0x000000))
.fontWeight(.black)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue.cornerRadius(14))
}
}
struct CollectionView
<Collections, CellContent>
: UIViewControllerRepresentable
where
Collections : RandomAccessCollection,
Collections.Index == Int,
Collections.Element : RandomAccessCollection,
Collections.Element.Index == Int,
Collections.Element.Element : Identifiable,
CellContent : View
{
typealias Row = Collections.Element
typealias Data = Row.Element
typealias ContentForData = (Data) -> CellContent
typealias ScrollDirection = UICollectionView.ScrollDirection
typealias SizeForData = (Data) -> CGSize
typealias CustomSizeForData = (UICollectionView, UICollectionViewLayout, Data) -> CGSize
typealias RawCustomize = (UICollectionView) -> Void
enum ContentSize {
case fixed(CGSize)
case variable(SizeForData)
case crossAxisFilled(mainAxisLength: CGFloat)
case custom(CustomSizeForData)
}
struct ItemSpacing : Hashable {
var mainAxisSpacing: CGFloat
var crossAxisSpacing: CGFloat
}
fileprivate let collections: Collections
fileprivate let contentForData: ContentForData
fileprivate let scrollDirection: ScrollDirection
fileprivate let contentSize: ContentSize
fileprivate let itemSpacing: ItemSpacing
fileprivate let rawCustomize: RawCustomize?
init(
collections: Collections,
scrollDirection: ScrollDirection = .vertical,
contentSize: ContentSize,
itemSpacing: ItemSpacing = ItemSpacing(mainAxisSpacing: 0, crossAxisSpacing: 0),
rawCustomize: RawCustomize? = nil,
contentForData: #escaping ContentForData)
{
self.collections = collections
self.scrollDirection = scrollDirection
self.contentSize = contentSize
self.itemSpacing = itemSpacing
self.rawCustomize = rawCustomize
self.contentForData = contentForData
}
func makeCoordinator() -> Coordinator {
return Coordinator(view: self)
}
func makeUIViewController(context: Context) -> ViewController {
let coordinator = context.coordinator
let viewController = ViewController(coordinator: coordinator, scrollDirection: self.scrollDirection)
coordinator.viewController = viewController
self.rawCustomize?(viewController.collectionView)
return viewController
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
// TODO: Obviously we can be efficient about what needs to be updated here
context.coordinator.view = self
uiViewController.layout.scrollDirection = self.scrollDirection
self.rawCustomize?(uiViewController.collectionView)
uiViewController.collectionView.reloadData()
}
}
extension CollectionView {
/*
Convenience init for a single-section CollectionView
*/
init<Collection>(
collection: Collection,
scrollDirection: ScrollDirection = .vertical,
contentSize: ContentSize,
itemSpacing: ItemSpacing = ItemSpacing(mainAxisSpacing: 0, crossAxisSpacing: 0),
rawCustomize: RawCustomize? = nil,
contentForData: #escaping ContentForData) where Collections == [Collection]
{
self.init(
collections: [collection],
scrollDirection: scrollDirection,
contentSize: contentSize,
itemSpacing: itemSpacing,
rawCustomize: rawCustomize,
contentForData: contentForData)
}
}
extension CollectionView {
fileprivate static var cellReuseIdentifier: String {
return "HostedCollectionViewCell"
}
}
extension CollectionView {
final class ViewController : UIViewController {
fileprivate let layout: UICollectionViewFlowLayout
fileprivate let collectionView: UICollectionView
init(coordinator: Coordinator, scrollDirection: ScrollDirection) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = scrollDirection
self.layout = layout
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = nil
collectionView.register(HostedCollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier)
collectionView.dataSource = coordinator
collectionView.delegate = coordinator
self.collectionView = collectionView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("In no way is this class related to an interface builder file.")
}
override func loadView() {
self.view = self.collectionView
}
}
}
extension CollectionView {
final class Coordinator : NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
fileprivate var view: CollectionView
fileprivate var viewController: ViewController?
init(view: CollectionView) {
self.view = view
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return self.view.collections.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.view.collections[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellReuseIdentifier, for: indexPath) as! HostedCollectionViewCell
let data = self.view.collections[indexPath.section][indexPath.item]
let content = self.view.contentForData(data)
cell.provide(content)
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let cell = cell as! HostedCollectionViewCell
cell.attach(to: self.viewController!)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let cell = cell as! HostedCollectionViewCell
cell.detach()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
switch self.view.contentSize {
case .fixed(let size):
return size
case .variable(let sizeForData):
let data = self.view.collections[indexPath.section][indexPath.item]
return sizeForData(data)
case .crossAxisFilled(let mainAxisLength):
switch self.view.scrollDirection {
case .horizontal:
return CGSize(width: mainAxisLength, height: collectionView.bounds.height)
case .vertical:
fallthrough
#unknown default:
return CGSize(width: collectionView.bounds.width, height: mainAxisLength)
}
case .custom(let customSizeForData):
let data = self.view.collections[indexPath.section][indexPath.item]
return customSizeForData(collectionView, collectionViewLayout, data)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return self.view.itemSpacing.mainAxisSpacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return self.view.itemSpacing.crossAxisSpacing
}
}
}
private extension CollectionView {
final class HostedCollectionViewCell : UICollectionViewCell {
var viewController: UIHostingController<CellContent>?
func provide(_ content: CellContent) {
if let viewController = self.viewController {
viewController.rootView = content
} else {
let hostingController = UIHostingController(rootView: content)
hostingController.view.backgroundColor = nil
self.viewController = hostingController
}
}
func attach(to parentController: UIViewController) {
let hostedController = self.viewController!
let hostedView = hostedController.view!
let contentView = self.contentView
parentController.addChild(hostedController)
hostedView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(hostedView)
hostedView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
hostedView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
hostedView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
hostedView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
hostedController.didMove(toParent: parentController)
}
func detach() {
let hostedController = self.viewController!
guard hostedController.parent != nil else { return }
let hostedView = hostedController.view!
hostedController.willMove(toParent: nil)
hostedView.removeFromSuperview()
hostedController.removeFromParent()
}
}
}
So I am calling this into a colleciton view. This example is for SwiftUI but I am creationg a collection view with a list layout.
Example: https://hygraph.com/blog/swift-with-hygraph
I have done all the other setup which you can find in the gist but I keep getting these errors:
[Common] Snapshot request 0x60000348db30 complete with error: <NSError: 0x600003489020; domain: FBSSceneSnapshotErrorDomain; code: 4; reason: "an unrelated condition or state was not satisfied">
My Gist: https://github.com/ImranRazak1/HygraphSwift
View Controller
import UIKit
class ViewController: UIViewController {
enum Section {
case main
}
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section,Product>?
var products: [Product] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
navigationItem.title = "Products"
view.backgroundColor = .white
//Uncomment when needed
configureHierarchy()
configureDataSource()
collectionView.register(ListCell.self, forCellWithReuseIdentifier: "ListCell")
}
func loadProducts() async {
self.products = await APIService().listProducts()
}
func configure<T: SelfConfiguringCell>(with product: Product, for indexPath: IndexPath) -> T {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ListCell", for: indexPath) as? T else {
fatalError("Unable to dequeue Cell")
}
cell.configure(with: product)
return cell
}
}
extension ViewController {
private func createLayout() -> UICollectionViewLayout {
let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
return UICollectionViewCompositionalLayout.list(using: config)
}
}
extension ViewController {
private func configureHierarchy() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
collectionView.delegate = self
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Product> { (cell, indexPath, product) in
var content = cell.defaultContentConfiguration()
content.text = "\(product.name)"
cell.contentConfiguration = content
}
dataSource = UICollectionViewDiffableDataSource<Section, Product>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Product) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
}
//inital data
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
snapshot.appendSections([.main])
//Problem loading this information
snapshot.appendItems(products, toSection: .main)
dataSource?.apply(snapshot, animatingDifferences: false)
Task {
do {
await self.loadProducts()
snapshot.appendItems(products, toSection: .main)
await dataSource?.apply(snapshot, animatingDifferences: false)
}
}
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
}
}
Best,
Imran
I am trying to use UITableView in a SwiftUI app
struct UIList: UIViewRepresentable {
var rows: [String]
func makeUIView(context: Context) -> UITableView {
let collectionView = UITableView(frame: .zero, style: .plain)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
return collectionView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var rows: [String]
init(rows: [String]) {
self.rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) //as! AlbumPrivateCell
let view = Text(rows[indexPath.row]).frame(height: 50).background(Color.blue)// UIFactory(appComponent:
let controller = UIHostingController(rootView: view)
let tableCellViewContent = controller.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
return tableViewCell
}
}
}
When I scroll the table quickly the cells content get a random padding at top and bottom of each cell, any idea why this happens ?
PS: I know I could use List , I try to use UITableView because I have to add multiple swipe actions, but List only allows one swipe action (delete)
I assume this is due to corrupted reused table view cells... (and probably lost hosting controllers, because there where created on stack and not stored anywhere)
Please find below corrected a bit your code with mentioned fixes. Tested & worked with Xcode 11.2 / iOS 13.2.
Here is code (with some comments inline):
class HostingCell: UITableViewCell { // just to hold hosting controller
var host: UIHostingController<AnyView>?
}
struct UIList: UIViewRepresentable {
var rows: [String]
func makeUIView(context: Context) -> UITableView {
let collectionView = UITableView(frame: .zero, style: .plain)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(HostingCell.self, forCellReuseIdentifier: "Cell")
return collectionView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var rows: [String]
init(rows: [String]) {
self.rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
let view = Text(rows[indexPath.row])
.frame(height: 50).background(Color.blue)
// create & setup hosting controller only once
if tableViewCell.host == nil {
let controller = UIHostingController(rootView: AnyView(view))
tableViewCell.host = controller
let tableCellViewContent = controller.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
} else {
// reused cell, so just set other SwiftUI root view
tableViewCell.host?.rootView = AnyView(view)
}
tableViewCell.setNeedsLayout()
return tableViewCell
}
}
}
Added demo code for just UIList itself - works fine with pro models as well.
struct TestUIList: View {
var body: some View {
UIList(rows: generateRows())
}
func generateRows() -> [String] {
(0..<100).reduce([]) { $0 + ["Row \($1)"] }
}
}
Update on July 8 2022 - Apple appears to have fixed the two finger scrolling bug, although the interaction is still a bit buggy.
Collection view + compositional layout + diffable data source + drag and drop does not seem to work together. This is on a completely vanilla example modeled after this (which works fine.)
Dragging an item with one finger works until you use a second finger to simultaneously scroll, at which point it crashes 100% of the time. I would love for this to be my problem and not an Apple oversight.
I tried using a flow layout and the bug disappears. Also it persists even if I don't use the list configuration of compositional layout, so that's not it.
Any ideas? Potential workarounds? Is this a known issue?
(The sample code below should run as-is on a blank project with a storyboard containing one view controller pointing to the view controller class.)
import UIKit
struct VideoGame: Hashable {
let id = UUID()
let name: String
}
extension VideoGame {
static var data = [VideoGame(name: "Mass Effect"),
VideoGame(name: "Mass Effect 2"),
VideoGame(name: "Mass Effect 3"),
VideoGame(name: "ME: Andromeda"),
VideoGame(name: "ME: Remaster")]
}
class CollectionViewDataSource: UICollectionViewDiffableDataSource<Int, VideoGame> {
// 1
override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
guard let fromGame = itemIdentifier(for: sourceIndexPath),
sourceIndexPath != destinationIndexPath else { return }
var snap = snapshot()
snap.deleteItems([fromGame])
if let toGame = itemIdentifier(for: destinationIndexPath) {
let isAfter = destinationIndexPath.row > sourceIndexPath.row
if isAfter {
snap.insertItems([fromGame], afterItem: toGame)
} else {
snap.insertItems([fromGame], beforeItem: toGame)
}
} else {
snap.appendItems([fromGame], toSection: sourceIndexPath.section)
}
apply(snap, animatingDifferences: false)
}
}
class DragDropCollectionViewController: UIViewController {
var videogames: [VideoGame] = VideoGame.data
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewCompositionalLayout.list(using: UICollectionLayoutListConfiguration(appearance: .insetGrouped)))
lazy var dataSource: CollectionViewDataSource = {
let dataSource = CollectionViewDataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, model) -> UICollectionViewListCell in
return collectionView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: model)
})
return dataSource
}()
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, VideoGame> { (cell, indexPath, model) in
var configuration = cell.defaultContentConfiguration()
configuration.text = model.name
cell.contentConfiguration = configuration
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
var snapshot = dataSource.snapshot()
snapshot.appendSections([0])
snapshot.appendItems(videogames, toSection: 0)
dataSource.applySnapshotUsingReloadData(snapshot)
}
}
extension DragDropCollectionViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return []
}
let itemProvider = NSItemProvider(object: item.id.uuidString as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
}
// 4
extension DragDropCollectionViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
//Not needed
}
}
If you download the modern Collectionviews project from Apple, there is one that shows compositional layout, diffable datasource and reordering. However this is only for their new list cells, not a reg CollectionView cell.
You can find it here:
Modern CollectionViews
I am trying to use UITableView in a SwiftUI app
struct UIList: UIViewRepresentable {
var rows: [String]
func makeUIView(context: Context) -> UITableView {
let collectionView = UITableView(frame: .zero, style: .plain)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
return collectionView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var rows: [String]
init(rows: [String]) {
self.rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) //as! AlbumPrivateCell
let view = Text(rows[indexPath.row]).frame(height: 50).background(Color.blue)// UIFactory(appComponent:
let controller = UIHostingController(rootView: view)
let tableCellViewContent = controller.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
return tableViewCell
}
}
}
When I scroll the table quickly the cells content get a random padding at top and bottom of each cell, any idea why this happens ?
PS: I know I could use List , I try to use UITableView because I have to add multiple swipe actions, but List only allows one swipe action (delete)
I assume this is due to corrupted reused table view cells... (and probably lost hosting controllers, because there where created on stack and not stored anywhere)
Please find below corrected a bit your code with mentioned fixes. Tested & worked with Xcode 11.2 / iOS 13.2.
Here is code (with some comments inline):
class HostingCell: UITableViewCell { // just to hold hosting controller
var host: UIHostingController<AnyView>?
}
struct UIList: UIViewRepresentable {
var rows: [String]
func makeUIView(context: Context) -> UITableView {
let collectionView = UITableView(frame: .zero, style: .plain)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(HostingCell.self, forCellReuseIdentifier: "Cell")
return collectionView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var rows: [String]
init(rows: [String]) {
self.rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
let view = Text(rows[indexPath.row])
.frame(height: 50).background(Color.blue)
// create & setup hosting controller only once
if tableViewCell.host == nil {
let controller = UIHostingController(rootView: AnyView(view))
tableViewCell.host = controller
let tableCellViewContent = controller.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
} else {
// reused cell, so just set other SwiftUI root view
tableViewCell.host?.rootView = AnyView(view)
}
tableViewCell.setNeedsLayout()
return tableViewCell
}
}
}
Added demo code for just UIList itself - works fine with pro models as well.
struct TestUIList: View {
var body: some View {
UIList(rows: generateRows())
}
func generateRows() -> [String] {
(0..<100).reduce([]) { $0 + ["Row \($1)"] }
}
}