didSelectItemAt
导致UI在每次更改lastSelectedIndex
的值时重排/重绘,从而导致性能问题。我不确定我是否已正确使用@State
来将价值从孩子传播到父母。
P.S。我需要使用UICollectionView而不是swiftui List
或ScrollView
。
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()
}
}
答案 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()
}