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 控件偏移?

为啥 SwiftUI 不能正确显示两个视图?

使用 Combine 和 SwiftUI 显示变化值的最简洁方式是啥?

SwiftUI+Combine - 动态订阅发布者的字典

SwiftUI + Combine:如何将数据分配给带有动画的模型

SwiftUI/Combine 发布者是双向的吗?