UICollectionView with SwiftUI cells creating memory issues

Viewed 20

I am making a swiftUI app. I am using UIKit's UICollectionView for some functionally and performance requirement. My cells are swiftUI views wrapped in UIHostingController. If I launch the app and start scrolling, Initially UICollectionView runs smooth but if I scroll further to the last element, My UICollectionView starts lagging. It is behaving the same every time I relaunch the app, Initially smooth and slow after few seconds of scrolling. If you check the debug navigator in Xcode the memory section keep on increasing as you scroll from top to bottom and bottom to top. I am not able to trace what is consuming so much memory.

I have created following demo to explain my problem. I was not able to achieve the same amount of lag as in the original app but if you keep scrolling you can see the memory level rising in debug navigator.

UICollectionView, UIViewRepresentable

struct UICollectionViewWrapper: UIViewRepresentable {
    
    var width: CGFloat
    var height: CGFloat
    
    @ObservedObject var viewModel: ViewModel
    
    func makeUIView(context: Context) -> UICollectionView {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .clear
        collectionView.dataSource = context.coordinator
        collectionView.delegate = context.coordinator
        collectionView.alwaysBounceVertical = true
        
        collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "collectionViewCell")
        
        return collectionView
    }
    
    func updateUIView(_ uiView: UICollectionView, context: Context) {
        //
    }
    
    class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
        var parent: UICollectionViewWrapper
        
        init(_ parent: UICollectionViewWrapper) {
            self.parent = parent
        }
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return parent.viewModel.songs.count
        }

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as! CollectionViewCell
            
            let song = parent.viewModel.songs[indexPath.row]
            cell.setUpCell(width: parent.width, height: parent.height, song: song)
        
            return cell
        }

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            return CGSize(width: parent.width, height: parent.height)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}

UICollectionViewCell

class CollectionViewCell: UICollectionViewCell {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setUpCell(width: CGFloat,
                   height: CGFloat,
                   song: Song
    ) {
        let host = UIHostingController(rootView: CoverView(song: song))
        let uiView = host.view!
        uiView.frame = CGRect(x: 0, y: 0, width: width, height: height)
        addSubview(uiView)
    }
    
}

ViewModel

class ViewModel: ObservableObject {
    @Published var songs = [Song]()
    
    func loadSongs() {
        for _ in 0..<100 {
            let song = Song(name: "Fix You",
                            artist: "ColdPlay",
                            album: "X&Y 2005",
                            lyrics: """
                                    When you try your best, but you don't succeed
                                    When you get what you want, but not what you need
                                    When you feel so tired, but you can't sleep
                                    Stuck in reverse
                                    And the tears come streaming down your face
                                    When you lose something you can't replace
                                    When you love someone, but it goes to waste
                                    Could it be worse?
                                    Lights will guide you home
                                    And ignite your bones
                                    And I will try to fix you
                                    """
            )
            songs.append(song)
        }
    }
}

SwiftUI Views

struct CoverView: View {
    var song: Song
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 15)
                .fill(.tint)
            
            VStack(alignment: .leading,  spacing: 10) {
                
                Text("\(song.artist) - \(song.name)")
                    .font(.system(size: 30, weight: .bold, design: .rounded))
                
                Text(song.album)
                    .font(.system(size: 22, weight: .medium, design: .rounded))
                Spacer()
                Text(song.lyrics)
                    .font(.system(size: 22, weight: .medium, design: .rounded))
            }
            .padding()
            .padding(.top)
            .foregroundColor(.white)
        }
    }
}

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        GeometryReader { geometry in
            UICollectionViewWrapper(width: geometry.size.width,
                                    height: geometry.size.height,
                                    viewModel: viewModel
            )
            .onAppear {
                viewModel.loadSongs()
            }
        }
    }
}

Can you help me solve this issue ? Thanks in advance !

UPDATE:

Made some changes in CollectionViewCell. Performance is improved but I can still see the memory spikes after scrolling for a while and performance goes down after that.

class CollectionViewCell: UICollectionViewCell {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private(set) var host: UIHostingController<CoverView>?
    
    func setUpCell(width: CGFloat,
                   height: CGFloat,
                   song: Song
    ) {
        if let host = host {
            host.rootView = CoverView(song: song)
            host.view.layoutIfNeeded()
        } else {
            let host = UIHostingController(rootView: CoverView(song: song), ignoreSafeArea: true)
            let uiView = host.view!
            uiView.frame = CGRect(x: 0, y: 0, width: width, height: height)
            addSubview(uiView)
            
            self.host = host
        }
    }
    
}
0 Answers
Related