UIViewRepresentable Rows In List Disappearing in SwiftUI

Viewed 550

I have this List:

struct SignInList: View {
    @ObservedObject var model:SignInModel
    
    var body: some View {
        List(model.currentSignIns) { signIn in
            SignInRow(description: signIn)
        }
    }
}

With this list row:

struct SignInRow: View, Identifiable {
    let id = UUID()
    let description: SignInDescription
    
    var body: some View {
        VStack {
            ButtonView(view: description.button)
        }
    }
}

// Map a UIView to a View. Will be using this to hold a UIView based sign-in button
// https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
struct ButtonView: UIViewRepresentable {
    let view: UIView
    
    func makeUIView(context: Context) -> UIView {
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
    }
}

but when I scroll the list, the cells of the list disappear:

Cells Disappearing

If I change the list cells to purely SwiftUI, e.g.,

struct SignInRow: View, Identifiable {
    let id = UUID()
    let description: SignInDescription
    
    var body: some View {
        VStack {
            Rectangle()
        }
    }
}

I do not get this effect. The cells do not disappear when scrolling.

I have tried some ideas on SO (e.g., SwiftUI and ScrollView: Views Disappear After Device Rotation and Content in scrollview as a list item disappears when scrolling (swiftui), why?) but no real improvement so far. (When I embed a ForEach in a ScrollView, I get different problems-- only one of my two rows shows up and the buttons are no longer tappable).

2 Answers

I think @Asperi is on the right track with List's being the issue. I'm switching away from using them. Here's what's working for me now:

struct SignInList: View {
    @ObservedObject var model:SignInModel
    
    var body: some View {
        ScrollView() {
            ForEach(model.currentSignIns) { signIn in
                SignInRow(description: signIn)
                    .padding(.horizontal, 20)
            }
        }
    }
}

struct SignInRow: View, Identifiable {
    let id = UUID()
    let description: SignInDescription
    
    var body: some View {
        VStack {
            ButtonView(view: description.button)
        }.frame(height: 40)
    }
}

I assume the issue is because List reuses rows, so it removes your externally injected UIView from superview, but afterwards do not insert it again (it is not the way representable works), but just updates.

In general the pattern is that you should provide data, but view should be created in makeView and updated in updateView.

Here is simplified demo on some replicated from your code. Tested with Xcode 12.1 / iOS 14.1.

demo

class SignInModel: ObservableObject {
    @Published var currentSignIns = (1..<100).map { "\($0)" }
}

struct SignInList: View {
    @ObservedObject var model = SignInModel()
    
    var body: some View {
        List(model.currentSignIns) { signIn in
            SignInRow(description: signIn)
        }
    }
}

struct SignInRow: View, Identifiable {
    let id = UUID()
    let description: String
    
    var body: some View {
        VStack {
            ButtonView(title: description)
        }
    }
}

struct ButtonView: UIViewRepresentable {
    let title: String
    
    func makeUIView(context: Context) -> UIView {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        return button
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
    }
}
Related