swiftui+combine:为啥滚动 LazyVGrid 时 isFavoriteO 改变了?
Posted
技术标签:
【中文标题】swiftui+combine:为啥滚动 LazyVGrid 时 isFavoriteO 改变了?【英文标题】:swiftui+combine: why isFavoriteO changed when scroll the LazyVGrid?swiftui+combine:为什么滚动 LazyVGrid 时 isFavoriteO 改变了? 【发布时间】:2021-03-29 07:51:05 【问题描述】:我有一个 LazyVGrid,每个项目都有最喜欢的按钮。并使用combine去抖用户输入($isFavoriteI),当isFavoriteO改变时,再修改items。
它工作正常,但是当我滚动列表时,日志将打印:“X,isFavorite changed as false/true)”,什么原因 isFavoriteO 改变了,为什么?因为列表中的项目重用?如何避免?
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
import SwiftUI
import Combine
struct Item
var index: Int
var favorite: Bool
var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
// Item(index: 8, favorite: true),
// Item(index: 9, favorite: false),
// Item(index: 10, favorite: true),
// Item(index: 11, favorite: false),
// Item(index: 12, favorite: true),
// Item(index: 13, favorite: false),
// Item(index: 14, favorite: true),
// Item(index: 15, favorite: false),
// Item(index: 16, favorite: true),
// Item(index: 17, favorite: false),
// Item(index: 18, favorite: true),
// Item(index: 19, favorite: false),
]
struct ViewModelInListTestView: View
var body: some View
ScrollView(showsIndicators: false)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4)
ForEach(items, id: \.index) item in
ItemView(item: item)
.navigationTitle("ViewModel In List")
struct ItemView: View
let item: Item
@ObservedObject var viewModel: ViewModel
init(item: Item)
print("ItemView.init, \(item.index)")
self.item = item
self.viewModel = ViewModel(item: item)
var body: some View
HStack
Text("index \(item.index)")
Spacer()
Image(systemName: viewModel.isFavoriteI ? "heart.fill" : "heart")
.foregroundColor(viewModel.isFavoriteI ? .red : .white)
.padding()
.onTapGesture onFavoriteTapped()
.onChange(of: viewModel.isFavoriteO) isFavorite in
setFavorite(isFavorite)
.frame(width: 200, height: 150)
.background(Color.gray)
func onFavoriteTapped()
viewModel.isFavoriteI.toggle()
func setFavorite(_ isFavorite: Bool)
print("index \(item.index), isFavorite changed as \(isFavorite)")
items[item.index].favorite = isFavorite
class ViewModel: ObservableObject
@Published var isFavoriteI: Bool = false
@Published var isFavoriteO: Bool = false
private var subscriptions: Set<AnyCancellable> = []
init(item: Item)
print("ViewModel.init, \(item.index)")
let isFavorite = item.favorite
isFavoriteI = isFavorite; isFavoriteO = isFavorite
$isFavoriteI
.print("index \(item.index) isFavoriteI:")
.dropFirst()
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.eraseToAnyPublisher()
.print("index \(item.index) isFavoriteO:")
.receive(on: DispatchQueue.main)
.assign(to: \.isFavoriteO, on: self)
.store(in: &subscriptions)
更新@ 4.15 根据@Cenk Bilgen,我重新编写了代码,但奇怪的事情发生了。如果添加 removeDuplicates,print("set favorite as (favorite)") 将不会出现。为什么?
import SwiftUI
import Combine
struct Item: Identifiable
var index: Int
var favorite: Bool
var id: Int index
class Model: ObservableObject
@Published var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
]
struct ViewModelInListTestView: View
@StateObject var model = Model()
var body: some View
print("ViewModelInListTestView refreshing"); return
ScrollView(showsIndicators: false)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4)
ForEach(model.items.indices) index in
ItemView(item: model.items[index])
.environmentObject(model)
.navigationTitle("ViewModel In List")
struct ItemView: View
@EnvironmentObject var model: Model
let item: Item
@State private var updateFavourite = PassthroughSubject<Bool, Never>()
@State private var favorite: Bool = false
init(item: Item)
self.item = item
self._favorite = State(initialValue: item.favorite)
var body: some View
print("ItemView \(item.index) refreshing"); return
HStack
Text("index \(item.index)")
Spacer()
Image(systemName: favorite ? "heart.fill" : "heart")
.foregroundColor(favorite ? .red : .white)
.padding()
.onTapGesture
favorite.toggle()
updateFavourite.send(favorite)
.onReceive(
updateFavourite
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
// .removeDuplicates() <------ HERE
// .eraseToAnyPublisher()
) favorite in
print("set favorite as \(favorite)")
model.items[item.index].favorite = favorite
.frame(width: 200, height: 150)
.background(Color.gray)
【问题讨论】:
每次初始化 ItemView 时(通常是每次重绘),您都在重新创建 viewModel。我认为您可以通过将.onTapGesture
处理程序更改为具有去抖动逻辑的发布者来完全删除它并澄清代码。
ItemView.init, 7 ViewModel.init, 7 index 7 isFavoriteO:: receive subscription: (RemoveDuplicates) index 7 isFavoriteO:: request unlimited index 7 isFavoriteI:: receive subscription: (PublishedSubject) index 7 isFavoriteI:: request unlimited index 7 isFavoriteI:: receive value: (false) index 0, isFavorite changed as false index 7, isFavorite changed as true
根据此日志,viewmodel 从未初始化过重新绘制 ItemView。
查看代码,您在视图初始化程序的第三行调用self.viewModel = ViewModel(item: item)
,并且每当需要重绘视图时都会调用该初始化程序(在 SwiftUI 重绘的情况下意味着结构是重新创建)。我的建议是在 init 之外对其进行初始化,例如将其设为 @StateObject 或者我认为您可以重构以在点击手势本身中移动去抖点击的责任。
您可以尝试运行我的代码,跟踪日志。这是不容易的。我认为这里甚至是苹果的问题。
你说得对,这并不容易。我在下面添加了一个答案,这不是很好,我不确定这正是您想要的去抖动行为,但可能会有所帮助。
【参考方案1】:
struct Item: Identifiable
var index: Int
var favorite: Bool
var id: Int index
class Model: ObservableObject
@Published var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
]
struct SimplerView: View
@StateObject var model = Model()
var body: some View
ScrollView(showsIndicators: false)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4)
ForEach(items.indices) index in
ItemView(item: $model.items[index])
.environmentObject(model)
.navigationTitle("ViewModel In List")
struct ItemView: View
@EnvironmentObject var model: Model
@Binding var item: Item
@State private var updateFavourite = PassthroughSubject<Bool, Never>()
var body: some View
HStack
Text("index \(item.index)")
Spacer()
Image(systemName: item.favorite ? "heart.fill" : "heart")
.foregroundColor(item.favorite ? .red : .white)
.padding()
.onTapGesture
updateFavourite.send(item.favorite)
.onReceive(updateFavourite
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)) value in
item.favorite = !value
.frame(width: 200, height: 150)
.background(Color.gray)
【讨论】:
奇怪的事情:如果添加 removeDuplicates 则无法得到响应,如下所示:.onReceive(updateFavourite.debounce(for: .seconds(0.5), scheduler: RunLoop.main).removeDuplicates())
去抖动后添加打印时会发现“receive cancel”而不是“receive value: (false)”。
请在原帖末尾查看我的更新代码,谢谢。【参考方案2】:
我不太明白 isFavoriteI 和 isFavoriteO 是如何工作的,但是你 可以试试这个:
在ItemView中删除
.onChange(of: viewModel.isFavoriteO) isFavorite in
setFavorite(isFavorite)
并改变:
func onFavoriteTapped()
viewModel.isFavoriteI.toggle()
print("\(item.index), isFavorite changed as \(viewModel.isFavoriteI)")
items[item.index].favorite = viewModel.isFavoriteI
【讨论】:
viewModel 中的 isFavoriteI(nput) 和 isFavoriteO(utput) 用于消除用户输入的抖动。当我快速多次点击收藏按钮时,isFavoriteI每次都会更新,而isFavoriteO只在最后一次点击时更新。 我确信单元可重复使用会导致此问题。 LazyVGrid 和 LazyVStack 会导致这种情况,但 VStack 不会。我认为当 ItemView 被重用时,这意味着将另一个 Item 数据集设置为旧的相同 ItemView。此产品不会触发 ItemView init()(print("ItemView.init, (item.index)") not present) 或 $isFavoriteI(print("index (item.index) isFavoriteI:") not present),但会触发.onChange(of: viewModel.isFavoriteO)(print("index (item.index), isFavorite changed as (isFavorite)") present)。是有线的,如何避免?【参考方案3】:这样做的好处是在down代码中,SwiftUI会停止不必要的渲染,如果需要它会渲染!
你遇到了一些问题,你应该为你的项目使用 id,而且在这种情况下 combine 不能很好地工作,所以使用更好更简单的方法:
import SwiftUI
struct ContentView: View
@StateObject var itemModel: ItemModel = sharedItemModel
var body: some View
ScrollView
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4)
ForEach(Array(itemModel.items.enumerated()), id:\.element.id) (offset, element) in
ItemView(index: offset, favorite: element.favorite)
Button("append new random element") itemModel.items.append(Item(favorite: Bool.random()))
.padding()
struct ItemView: View
let index: Int
let favorite: Bool
init(index: Int, favorite: Bool)
self.index = index
self.favorite = favorite
var body: some View
print("rendering item: " + index.description)
return HStack
Text("index " + index.description)
.bold()
.padding()
Spacer()
Image(systemName: favorite ? "heart.fill" : "heart")
.foregroundColor(Color.red)
.padding()
.onTapGesture sharedItemModel.items[index].favorite.toggle()
.frame(width: 200, height: 150)
.background(Color.gray)
.cornerRadius(10.0)
struct Item: Identifiable
let id: UUID = UUID()
var favorite: Bool
class ItemModel: ObservableObject
@Published var items: [Item] = [Item]()
let sharedItemModel: ItemModel = ItemModel()
【讨论】:
1. ForEach(items, id: \.index), 使用的 id。 2.结合不工作?我认为这是错误的方式。 combine 用于消除用户输入的抖动。 3.你的代码不能解决我的问题。 你误会了我的话!这意味着使用 combine 来解决您的问题不是正确的举动!但我们可以有不同的信念,没问题 如何在不合并的情况下消除用户输入的抖动?有什么建议吗?以上是关于swiftui+combine:为啥滚动 LazyVGrid 时 isFavoriteO 改变了?的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI 中的一排 Picker ***,为啥拖动滚动区域从屏幕上的 Picker 控件偏移?
使用 Combine 和 SwiftUI 显示变化值的最简洁方式是啥?