0

I consider how to create SwitUI List that has as its row custom UIViews.

I create List:

List { 
   RowView()
}

RowView is UIViewRepresentable of UIRowView

struct RowView : UIViewRepresentable { 

   func makeUIView() -> UIRowView { ... }
}

UIRowView is custom view

UIRowView: UIView { ... } 

Currently first rows are displayed but they are usually not layout properly and while scrolling this views disappear instead of being recycled

UPDATE

Example 1

struct NoteView: UIViewRepresentable {

    // MARK: - Properties
    let note: Note
    let date = Date()

    func makeUIView(context: Context) -> UINoteView {

        let view = UINoteView()
        view.note = note

        return view
    }

    func updateUIView(_ uiView: UINoteView, context: Context) {

        uiView.note = note
        print("View bounds: \(uiView.bounds)")
    }
}

var body: some View {
        List {
            ForEach(Array(notes.enumerated()), id: \.1) { (i, note) in
                NoteView(note: note)
                .background(Color.green)
                .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
            }
        }.background(Color.red)
    }

Example 2 - Simplified

struct TestView : UIViewRepresentable {

    let text : String

    func makeUIView(context: Context) -> UILabel {
        UILabel()
    }

    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.text = text
    }
}

  var body: some View {

    List {
        ForEach(0..<30, id: \.self) { i in
            TestView(text: "\(i)")
        }
    }
}

Both seems to work incorrectly, as rows dissapears I had also issue with views not keeping padding and going outside of the screen if there was more content. Only several first rows (visible initially on screen layouts correctly) other disappears or jump somewhere.

enter image description hereenter image description here

UPDATE 2

Here is Autosizable UINoteView

class UINoteView: UIView {

    // MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupViews()
    }

    // MARK: - Properties
    var note: Note? {
        didSet {
            textView.attributedText = note?.content?.parsedHtmlAttributedString(textStyle: .html)

            noteFooterViewModel.note = note
        }
    }

    // MARK: - Views
    lazy var textView: UITextView = {
        let textView = UITextView()
        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.backgroundColor = UIColor.yellow
        textView.textContainer.lineBreakMode = .byWordWrapping
        textView.textContainerInset = .zero
        textView.textContainer.lineFragmentPadding = 0
        textView.isScrollEnabled = false
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true
        textView.isEditable = false
        textView.textContainer.maximumNumberOfLines = 0

        return textView
    }()

    lazy var label: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "TEST ROW \(note?.id ?? "")"
        return label
    }()

    lazy var vStack: UIStackView = {
        let stack = UIStackView(arrangedSubviews: [
            textView,
            noteFooter
        ])
        stack.axis = .vertical
        stack.alignment = .fill
        stack.distribution = .fill
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()

    var noteFooterViewModel = NoteFooterViewModel()

    var noteFooter: UIView {
        let footer = NoteFooter(viewModel: noteFooterViewModel)
        let hosting = UIHostingController(rootView: footer)
        hosting.view.translatesAutoresizingMaskIntoConstraints = false
        return hosting.view
    }

    private func setupViews() {

        self.backgroundColor = UIColor.green
        self.addSubview(vStack)

        NSLayoutConstraint.activate([
            vStack.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            vStack.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            vStack.topAnchor.constraint(equalTo: self.topAnchor),
            vStack.bottomAnchor.constraint(equalTo: self.bottomAnchor)
        ])
    }

}
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143

2 Answers2

1

UPDATED ANSWER try this:

struct TestView : UIViewRepresentable {

    let text : String

    var label : UILabel = UILabel()

    func makeUIView(context: Context) -> UILabel {

        return label
    }

    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.text = text
    }
}



struct ContentView : View {

    var body: some View {

        List (0..<30, id: \.self) { i in
            TestView(text: "\(i)").id(i)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Chris
  • 7,579
  • 3
  • 18
  • 38
  • I've added to examples simplified and more complex (also leaving just simple UILabel) in both cases I am losing cells while scrolling list, additionally this cells can jump outside of the screen. – Michał Ziobro Mar 24 '20 at 13:13
  • If I've added .id() like this NoteView(note: note).id(i) and place this view inside HStack it does not disappear now but it doesn't size vertically appropriately – Michał Ziobro Mar 24 '20 at 13:53
  • what do you mean with "doesn't size" ? just set frame(height:blabla) after your testview, and you can set whatever height you want – Chris Mar 24 '20 at 13:56
  • or just give us a new copyable example where we can see it....thanks – Chris Mar 24 '20 at 13:57
  • I would like it to autosize. I want to have custom view NoteView: UIViewRepresentable. it creates UINoteView: UIView. And there is mutliline label or multiline UITextView. And I would like cells to get its height automatically. I do not know size of this cells in advance – Michał Ziobro Mar 24 '20 at 13:58
  • ok...honestly, this is a second question ...in your first question. – Chris Mar 24 '20 at 14:00
  • I've pase my UINoteView. Only option I have now is to calculate number of lines of TextView and then set .frame() in advance – Michał Ziobro Mar 24 '20 at 14:00
  • Yes I can create list that displays UIKit UIView subclass but this views are collapsed they do not take size of its content – Michał Ziobro Mar 24 '20 at 14:02
  • I solved this issue it seems that appending .id([row-unique-identifier]) solves issue but it must be placed not on entire row view but on specific nested subview of row that wraps via UIViewRepresentable UIKit view. It solves then many bugs. I've tried previously placing this id on row's root view but it does not help – Michał Ziobro Mar 25 '20 at 09:57
0

I did like below. You can get the frame size even without UIViewRepresentable by getTextFrame(for note)

var body: some View {
       List {
            ForEach(Array(notes.enumerated()), id: \.1) { (i, note) in
                NoteView(note: note)
                 // add this
                 .frame(width: getTextFrame(for: note).width, height: getTextFrame(for: note).height)
                .background(Color.green)
                .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
            }
        }.background(Color.red)
    }

  func getTextFrame(for text: String, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> CGSize {
        let attributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.preferredFont(forTextStyle: .body)
        ]
        let attributedText = NSAttributedString(string: text, attributes: attributes)
        let width = maxWidth != nil ? min(maxWidth!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
        let height = maxHeight != nil ? min(maxHeight!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
        let constraintBox = CGSize(width: width, height: height)
        let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral
        print(rect.size)
        return rect.size
    }

struct NoteView: UIViewRepresentable {
    let note: String
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.delegate = context.coordinator
        textView.font = UIFont.preferredFont(forTextStyle: .body)
        textView.isEditable = false
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true
        textView.isScrollEnabled = false
        textView.backgroundColor = .clear
        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textView.textContainerInset = .zero
        textView.textContainer.lineFragmentPadding = 0
        textView.textContainer.lineBreakMode = .byWordWrapping
        textView.text = note
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
    }
}

user12208004
  • 1,704
  • 3
  • 15
  • 37