swiftui+combine:为什么滚动 LazyVGrid 时 isFavoriteO 发生了变化?

swiftui+combine: why isFavoriteO changed when scroll the LazyVGrid?

我有一个 LazyVGrid,每个项目都有收藏按钮。并使用 combine 去抖动用户输入($isFavoriteI),当 isFavoriteO 更改时,然后修改项目。

它工作正常,但是当我滚动列表时,日志将打印:“X,isFavorite 更改为 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,我 re-write 代码,但奇怪的事情发生了。 print("set favorite as (favorite)") 如果添加 removeDuplicates 将不会出现。为什么?


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

我不太明白 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
}

最好的做法是在向下代码中这样做,SwiftUI 会停止不必要的渲染,如果需要它会渲染!

你遇到了一些问题,你应该为你的项目使用 id,而且在这种情况下 combine 不能很好地工作,所以在 down 中使用更好更简单的方法:


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

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