16

I found SwiftUI Text views to be extremely easy to create Labels with custom designs. So I wanted to use it as a view to a regular UIKit UICollectionViewCell.

This is my code so far (you can copy and paste inside Xcode 11).

import SwiftUI
import UIKit

struct ContentView: View {
    var body: some View {
        CollectionComponent()
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

struct CollectionComponent : UIViewRepresentable {
    func makeCoordinator() -> CollectionComponent.Coordinator {
        Coordinator(data: [])
    }

    class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
        var data: [String] = []

        init(data: [String]) {

            for index in (0...1000) {
                self.data.append("\(index)")
            }
        }

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            data.count
        }

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! GenericCell
            cell.customView.rootView = AnyView(
                Text(data[indexPath.item]).font(Font.title).border(Color.red)
            )
            return cell
        }
    }


    func makeUIView(context: Context) -> UICollectionView {
        let layout = UICollectionViewFlowLayout()
        layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        layout.scrollDirection = .vertical
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.dataSource = context.coordinator
        cv.delegate = context.coordinator
        cv.register(GenericCell.self, forCellWithReuseIdentifier: "cell")

        cv.backgroundColor = .white
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 0
        return cv
    }
    func updateUIView(_ uiView: UICollectionView, context: Context) {

    }
}


open class GenericCell: UICollectionViewCell {
    public var customView = UIHostingController(rootView: AnyView(Text("")))
    public override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }
    private func configure() {
        contentView.addSubview(customView.view)
        customView.view.preservesSuperviewLayoutMargins = false
        customView.view.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            customView.view.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
            customView.view.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
            customView.view.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
            customView.view.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor),
        ])
    }
}

The first screen is good.

enter image description here

But as i scroll past the end of visible screen it looks like

enter image description here

Is there something i am doing wrong with autoresizing my cells? Or is this just more SwiftUI bugs?

[Edit] I have accepted a SwiftUI answer, but If any one can provide me a fix to work with UIKit as asked in this question, I will accept.

swift nub
  • 2,747
  • 3
  • 17
  • 39
  • instead of using UIcollectionview you could use a list within a list to get the same effect – yawnobleix Aug 08 '19 at 07:12
  • @yawnobleix how? – swift nub Aug 08 '19 at 11:02
  • 1
    I will try to post it when I am at home tonight – yawnobleix Aug 08 '19 at 11:48
  • added an answer – yawnobleix Aug 11 '19 at 16:22
  • 1
    Could you possibly give us how you accomplish this using `UICollectionView` and `UICollectionViewCell`? This would help point to the specific issue - which may be that "for now" you cannot do this in `SwiftUI`. For instance, is this an issue related to the auto-layout constraints in either component? Is this related to trying to make a "lazy" or "reusable cell" version of a collection view in SwiftUI, which doesn't exist? Or is this a Dynamic Type issue? In other words (and forgive me with the play on words) - how would you handle this in a `UIKit` only app? –  Sep 21 '19 at 15:22
  • @dfd If i had to do this with UIKit, it would simply be a collectionView on Storyboard. With a registered cell. There isn't any different (except less code because of using a storyboard) – swift nub Sep 21 '19 at 16:28

4 Answers4

0

I made some modifications and It works but I do not think this is the best practice.

import SwiftUI
import UIKit

struct ContentView: View {
    var body: some View {
        CollectionComponent()
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

struct CollectionComponent : UIViewRepresentable {
    func makeCoordinator() -> CollectionComponent.Coordinator {
        Coordinator(data: [])
    }

    class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
        var data: [String] = []

        init(data: [String]) {

            for index in (0...1000) {
                self.data.append("\(index)")
            }
        }

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            data.count
        }
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            return CGSize(width: collectionView.frame.width/2.5, height: collectionView.frame.width/2)
        }

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! GenericCell

            cell.customView?.rootView = Text(data[indexPath.item])

            return cell
        }
    }


    func makeUIView(context: Context) -> UICollectionView {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        let cvs = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cvs.dataSource = context.coordinator
        cvs.delegate = context.coordinator
        cvs.register(GenericCell.self, forCellWithReuseIdentifier: "cell")

        cvs.backgroundColor = .white
        return cvs
    }
    func updateUIView(_ uiView: UICollectionView, context: Context) {

    }
}


public class GenericCell: UICollectionViewCell {

    public var textView = Text("")
    public var customView: UIHostingController<Text>?
    public override init(frame: CGRect) {
        super.init(frame: .zero)


        customView = UIHostingController(rootView: textView)
        customView!.view.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(customView!.view)

        customView!.view.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        customView!.view.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
        customView!.view.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
        customView!.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }
    public required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
utgoer
  • 29
  • 5
-2

Using just SwiftUI, you can put together a view that reflows Text views like you are wanting. The code is on GitHub, but is pretty long, so I won't post it here. You can find my explanation of what is going on in this answer. Here's a demo:

enter image description here

John M.
  • 8,892
  • 4
  • 31
  • 42
  • it would be nice if that GitHub is arranged into a project. – swift nub Sep 28 '19 at 02:04
  • 4
    Why is everyone posting answer with SwithUi scroll view when the question clearly states that the want to use UIKit component Collection view inside a SwiftUI framework? The correct answer should show to use collection View inside SwiftUi using UIViewRepresentable. Even I want to achieve the same - USE COLLECTION VIEW CUMPOLSORY... – Tejas Oct 01 '19 at 08:02
  • the effect the poster wanted can be achieved with using just SwiftUi, who says that this approach is wrong? – yawnobleix Oct 01 '19 at 09:14
  • @yawnobleix Please read the question and title. It says about using UICollectionView – Tejas Oct 02 '19 at 03:20
  • @Tejas The reason I added this answer (although it only uses SwiftUI) is that the poster upvoted and commented favorably on another SwiftUI answer. He noted that what he wanted was a reflowing layout, which the first SwiftUI answer did not provide. I assume then that he (and possibly others) might have decided to use UICollectionView to wrap SwiftUI views so they could get reflow. If this is the case, then a pure SwiftUI solution, which avoids the clipping issues in his UICollectionView workaround, would be of interest. – John M. Oct 02 '19 at 15:05
  • @JohnM. My only point is to stick to the requirement when you post the answer. Otherwise, it would be very misleading for a person who is looking for a solution and finds something different. Anyways, Could you please post the solution using CollectionView? – Tejas Oct 03 '19 at 00:54
  • 1
    @Tejas you are correct. I am still looking for a UIKit fix. If any one provides it, i will accept it. I have also edited the question above to reflect what i just said here. – swift nub Oct 06 '19 at 12:05
-2

This is a pure SwiftUI solution. It will wrap what ever view you give it and give you the effect you want. Let me know if it works for you.

struct WrappedGridView: View {
    let views: [WrappedGridViewHolder]
    var showsIndicators = false
    var completion:(Int)->Void = {x in}
    var body: some View {
        GeometryReader { geometry in
            ScrollView(showsIndicators: showsIndicators) {
                self.generateContent(in: geometry)
            }
        }
    }
    
    init(views: [AnyView], showsIndicators: Bool = false, completion: @escaping (Int)->Void = {val in}) {
        self.showsIndicators = showsIndicators
        self.views = views.map { WrappedGridViewHolder(view: $0) }
        self.completion = completion
    }

    private func generateContent(in g: GeometryProxy) -> some View {
        var width = CGFloat.zero
        var height = CGFloat.zero

        return
            ZStack(alignment: .topLeading) {
                    ForEach(views) { item in
                        item
                            .padding(4)
                            .alignmentGuide(.leading) { d in
                                if (abs(width - d.width) > g.size.width) {
                                    width = 0
                                    height -= d.height
                                }
                                let result = width
                                if item == self.views.last {
                                    width = 0
                                } else {
                                    width -= d.width
                                }
                                return result
                            }
                            .alignmentGuide(.top) { d in
                                let result = height
                                if item == self.views.last {
                                    height = 0
                                }
                                return result
                            }
                            .onTapGesture {
                                tapped(value: item)
                            }
                    }
            }
            .background(
                GeometryReader { r in
                    Color
                        .clear
                        .preference(key: SizePreferenceKey.self, value: r.size)
                }
            )
    }
    
    func tapped(value: WrappedGridViewHolder) {
        guard let index = views.firstIndex(of: value) else { assert(false, "This should never happen"); return }
        completion(index)
    }
}

struct SizePreferenceKey: PreferenceKey {
    typealias Value = CGSize
    static var defaultValue: Value = .zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        
    }
}

extension WrappedGridView {
    struct WrappedGridViewHolder: View, Identifiable, Equatable {
        let id = UUID().uuidString
        let view: AnyView
        var body: some View {
            view
        }
        
        static func == (lhs: WrappedGridViewHolder, rhs: WrappedGridViewHolder) -> Bool { lhs.id == rhs.id }
    }
}
Just a coder
  • 15,480
  • 16
  • 85
  • 138
  • 4
    This is not an answer to the question "How to set a SwiftUI view as a cell to a CollectionView" – Andrzej Polis Nov 19 '20 at 07:57
  • 1
    the poster needed it to work with collectionview because it was getting truncated. I merely showed that collection view wasnt needed. Maybe ask the original poster to change the question? – Just a coder Nov 19 '20 at 14:11
-3

Here is a solution using just purely swifUI, note this doesn't have the benefits which come with CollectionView (ie controlling elements which are off of the screen)

struct doubleList: View {
    var body: some View {
        VStack{
            ForEach(1 ..< 10) {
                index in
                HStack{
                ForEach(1 ..< 10) {
                    index2 in
                    Text(String(index) + String(index2))
                        .frame(width: 35.0)
                    }
                }
            }
        }
    }
}

This will give a result looking like this

Double list SwiftUI

yawnobleix
  • 1,204
  • 10
  • 21
  • the problem here is that the structure is rigid. In this case the data in numbers. In the real code, the data is Words. What is needed isn't a rigid 10x10 row/height. What is needed is that when a word reaches the end, it should wrap and go to the next line. I have not found a way to do this yet. (i did give you +1 though) – swift nub Sep 19 '19 at 12:29
  • 1
    so you want multiple words of various length to be displayed in a box on the screen? it would be helpful in the future if you include more details in your question – yawnobleix Sep 24 '19 at 13:21
  • Why is everyone posting answer with SwithUi scroll view when the question clearly states that the want to use UIKit component Collection view inside a SwiftUI framework? The correct answer should show to use collection View inside SwiftUi using UIViewRepresentable. – Tejas Oct 01 '19 at 08:01