防止每次@State更改时重排/重画

时间:2020-07-07 06:27:59

标签: swiftui combine

didSelectItemAt导致UI在每次更改lastSelectedIndex的值时重排/重绘,从而导致性能问题。我不确定我是否已正确使用@State来将价值从孩子传播到父母。

P.S。我需要使用UICollectionView而不是swiftui ListScrollView

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()
    }
}

2 个答案:

答案 0 :(得分:2)

快速修复

您可以为此目的使用ObservableObject

class Model: ObservableObject {
    @Published var index: Int
    init(index: Int) { self.index = index }
}

为您的文本字段创建一个容器视图,该容器视图将在模型更改时更新:

struct IndexPreviewer: View {
    @ObservedObject var model: Model
    var body: Text { Text("Current Selected Index \(model.index)") }
}

然后在您的ContentView中包含此模型和观察者:

struct ContentView: View {
    private let model = Model(index: -1)
    var body: some View {
        VStack {
            IndexPreviewer(model: model)
            CustomCollectionView(lastSelectedIndex: index)
        }
    }
    var index: Binding<Int> {
        Binding {model.index} set: {model.index = $0}
    }
}

说明

问题在于,一旦更新@State属性,将重新评估包含视图的body。因此,您无法在包含集合视图的视图上创建@State属性,因为每次您选择其他单元格时,都会向您的容器发送一条消息,容器将重新评估包含集合视图的主体。因此,收集视图将刷新并重新加载您的数据,就像Asperi在其答案中所写。

那您该怎么解决呢?从容器视图中删除状态属性包装器。因为当您更新lastSelectedIndex时,不应再次呈现容器视图(ContentView)。但是您的“文本”视图应该被更新。因此,您应该将文本视图包装在遵守选择索引的单独视图中。 这是ObservableObject发挥作用的地方。这是一个可以自行存储状态数据的类,而不是直接存储在视图的属性中的类。

您可能会问,为什么在模型更改时IndexPreviewer更新而ContentView没有更新?那是因为@ObservedObject属性包装器。当关联的ObservableObject更改时,将其添加到视图中将刷新视图。这就是为什么我们不在ContentView中包含@ObservedObject,而在IndexPreviewer中包含它。

如何/在哪里存储模型?

为简单起见,我将model作为常量属性添加到ContentView。但是,当ContentView不是您的SwiftUI层次结构的根视图时,这不是一个好主意。 假设您的内容视图还从其父视图接收了Bool值:

struct Wrapper: View {
    @State var toggle = false
    var body: some View {
        VStack {
            Toggle("toggle", isOn: $toggle)
            ContentView(toggle: toggle)
        }
    }
}
struct ContentView: View {
    let toggle: Bool
    private let model = Model(index: -1)
    ...
}

当您在iOS 13或14上运行该代码并尝试单击collection view单元格,然后更改切换时,您会看到更改切换时所选索引将重置为-1。为什么会这样?

当您单击切换开关时,@State var toggle将发生变化,并且由于它使用@State属性包装器,因此将重新计算视图的body。因此,将构建另一个ContentView,并随之创建一个新的Model对象。

有两种方法可以防止这种情况的发生。一种方法是在层次结构中向上移动模型。但这会在视图层次结构的顶部创建混乱的根视图。在某些情况下,最好将瞬态UI状态保留在所包含的UI组件本地。这可以通过未记录的技巧来实现,该技巧将@State用于模型对象。就像我说的那样,目前(2020年7月)尚未公开,但是使用@State包装的属性将在UI更新期间保持其值。

长话短说:您可能应该使用以下方式存储模型:

struct ContentView: View {
    @State private var model = Model(index: -1)

答案 1 :(得分:1)

@State的功能是导致依赖视图刷新。如果发生可表示的变化的依赖状态调用updateUIView,则当您将reloadData放入其中时-它会重新加载:

func updateUIView(_ uiView: UICollectionView, context: Context) {
    // Either remove reload from here (ig. make it once in makeUIView to load
    // content, or make reload here only conditionally depending on some parameter
    // which really needs collection to be reloaded
    // uiView.reloadData()
}