Prevent reflow/redraw every time @State is changed

didSelectItemAt causes UI to reflow/redraw every time value for lastSelectedIndex is changed, causing performance issue. I'm not sure if I have used @State properly to propagate value from child to parent.

P.S. I need to use UICollectionView for a reason instead of swiftui List or ScrollView.


Code Block
import Foundation
import SwiftUI
struct ContentView: View {
@State var lastSelectedIndex : Int = -1
var body: some View {
ZStack {
CustomCollectionView(lastSelectedIndex: $lastSelectedIndex)
Text("Current Selected Index \(lastSelectedIndex)")
}
}
}
struct CustomCollectionView: UIViewRepresentable {
@Binding var lastSelectedIndex : Int
func makeUIView(context: Context) -> UICollectionView {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: 400, height: 300)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.reuseId)
collectionView.delegate = context.coordinator
collectionView.dataSource = context.coordinator
collectionView.backgroundColor = .systemBackground
collectionView.isDirectionalLockEnabled = true
collectionView.backgroundColor = UIColor.black
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.alwaysBounceVertical = false
return collectionView
}
func updateUIView(_ uiView: UICollectionView, context: Context) {
uiView.reloadData()
}
func makeCoordinator() -> CustomCoordinator {
CustomCoordinator(self)
}
}
class CustomCoordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
let parent:CustomCollectionView
init(_ parent:CustomCollectionView) {
self.parent = parent
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.reuseId, for: indexPath) as! CustomCollectionViewCell
cell.backgroundColor = UIColor.red
cell.label.text = "Current Index is \(indexPath.row)"
NSLog("Called for Index \(indexPath.row)")
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
parent.lastSelectedIndex = indexPath.row
}
}
class CustomCollectionViewCell: UICollectionViewCell {
static let reuseId = "customCell"
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.numberOfLines = 0
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Accepted Reply

When you have a UIViewRepresentable, the method func updateUIView(_ uiView: UICollectionView, context: Context) is called every single time a @State/@Binding variable is changed. Currently your logic in that method is to call uiView.reloadData() every time. You could update the logic to only reload the data once, or only load it if certain conditions are met. For example:

Code Block swift
struct CustomCollectionView: UIViewRepresentable {
@Binding var lastSelectedIndex : Int
private var didLoadData = false
// ...
func updateUIView(_ uiView: UICollectionView, context: Context) {
if didLoadData { return }
uiView.reloadData()
didLoadData = true
}
// ...
}

  • This does not works as self is immutable

Add a Comment

Replies

When you have a UIViewRepresentable, the method func updateUIView(_ uiView: UICollectionView, context: Context) is called every single time a @State/@Binding variable is changed. Currently your logic in that method is to call uiView.reloadData() every time. You could update the logic to only reload the data once, or only load it if certain conditions are met. For example:

Code Block swift
struct CustomCollectionView: UIViewRepresentable {
@Binding var lastSelectedIndex : Int
private var didLoadData = false
// ...
func updateUIView(_ uiView: UICollectionView, context: Context) {
if didLoadData { return }
uiView.reloadData()
didLoadData = true
}
// ...
}

  • This does not works as self is immutable

Add a Comment
Answer for the same post from StackOverflow