SwiftUI 列表重复 ID 值

jon*_*ham 7 google-cloud-firestore swiftui combine swiftui-list

我正在开发一个 SwiftUI 项目,该项目使用合并从 Firebase Firestore 中提取数据。每个用户都可以在应用程序中创建“优惠”。为了在他们的帐户页面上列出他们的报价,我使用 onAppear 将 currentUserUid 传递到我的视图模型,以便我可以使用 currentUserUid 过滤数据库结果。OfferHistoryView 如下。当视图第一次出现时,这非常有效。我的问题是当我从 OfferDetailView 返回时,我收到以下消息。

ForEach<Array, String, NavigationLink<OfferRowView, ModifiedContent<OfferDetailView, _EnvironmentKeyWritingModifier<Optional>>>>:ID 在集合中出现多次,这将给出未定义的结果!

虽然这不会使应用程序崩溃,但并不理想。我尝试过,每次加载视图时以及每次调用组合时都从集合中删除所有项目,但这并不能解决问题。我还添加了打印语句来尝试捕获重复项,但我从未看到重复项。您可以在下面看到我的打印语句和其余相应文件。任何帮助,将不胜感激。

OfferViewHistory - 消息的来源。

struct OfferHistoryView: View {
    let db = Firestore.firestore()
    
    @EnvironmentObject var authSession: AuthSession
    @EnvironmentObject var offerHistoryViewModel: OfferHistoryViewModel
    
    var body: some View {
        
        return VStack {
            List {
                ForEach(self.offerHistoryViewModel.offerRowViewModels, id: \.id) { offerRowViewModel in
                    NavigationLink(destination: OfferDetailView(offerDetailViewModel: OfferDetailViewModel(offer: offerRowViewModel.offer, listing: offerRowViewModel.listing ?? testListing1))
                                    .environmentObject(authSession)
                    ) {
                        OfferRowView(offerRowViewModel: offerRowViewModel)
                    }
                } // ForEach
            } // List
            .navigationBarTitle("Offer History")
        } // VStack
        .onAppear(perform: {
            for offerRowViewModel in self.offerHistoryViewModel.offerRowViewModels {
                print("Before startCombine: \(offerRowViewModel.id)")
            }
            self.offerHistoryViewModel.startCombine(currentUserUid: self.authSession.currentUserUid)
            for offerRowViewModel in self.offerHistoryViewModel.offerRowViewModels {
                print("After startCombine: \(offerRowViewModel.id)")
            }
        })
    } // View
}
Run Code Online (Sandbox Code Playgroud)

OfferHistoryViewModel - 调用组合的地方。

class OfferHistoryViewModel: ObservableObject {
    var offerRepository: OfferRepository

    // Published Properties
    @Published var offerRowViewModels = [OfferRowViewModel]()
    
    // Combine Cancellable
    private var cancellables = Set<AnyCancellable>()
        
    // Intitalizer
    init(offerRepository: OfferRepository) {
        self.offerRepository = offerRepository
    }
    
    // Starting Combine - Filter results for offers created by the current user only.
    func startCombine(currentUserUid: String) {
        for offerRowViewModel in self.offerRowViewModels {
            print("Before startCombine func: \(offerRowViewModel.id)")
        }
        offerRepository
            .$offers
            .receive(on: RunLoop.main)
            .map { offers in
                offers
                    .filter { offer in
                        (currentUserUid != "" ? offer.userId == currentUserUid : false)
                    }
                    .map { offer in
                        OfferRowViewModel(offer: offer, listingRepository: ListingRepository())
                    }
            }
            .assign(to: \.offerRowViewModels, on: self)
            .store(in: &cancellables)
        
        for offerRowViewModel in self.offerRowViewModels {
            print("After startCombine func: \(offerRowViewModel.id)")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

报价行视图

struct OfferRowView: View {
    @ObservedObject var offerRowViewModel: OfferRowViewModel
    
    var body: some View {
        // Convenience variable for accessing the offer & listing.
        let offer = offerRowViewModel.offer
        let listing = offerRowViewModel.listing
        
        return VStack {
            Text(offer.id ?? "ID")
            Text(listing?.id ?? "ID")
            } // VStack
    } // View
}
Run Code Online (Sandbox Code Playgroud)

报价行视图模型

class OfferRowViewModel: ObservableObject, Identifiable {
    // Properties
    var id: String = ""
    var listingRepository: ListingRepository
    
    // Published Properties
    @Published var offer: Offer
    @Published var listing: Listing?
    
    // Combine Cancellable
    private var cancellables = Set<AnyCancellable>()
        
    // Initializer
    init(offer: Offer, listingRepository: ListingRepository) {
        self.offer = offer
        self.listingRepository = listingRepository
        self.startCombine()
    }
    
    // Starting Combine
    func startCombine() {
        // Get Offer
        $offer
            .receive(on: RunLoop.main)
            .compactMap { offer in
                offer.id
            }
            .assign(to: \.id, on: self)
            .store(in: &cancellables)
        
        // Get Connected Listing
        listingRepository
            .$listings
            .receive(on: RunLoop.main)
            .map { listings in
                listings
                    .first(where: { $0.id == self.offer.listingId})
            }
            .assign(to: \.listing, on: self)
            .store(in: &cancellables)
    }
}
Run Code Online (Sandbox Code Playgroud)

asy*_*ait 2

问题是 OfferRowViewModel 声明一个id属性,最初将其设置为"",然后使用合并发布者将其更新为“真实”id 作为基础属性。

当您遵循 时Identifiable,使用计算属性通常会非常方便。这将始终给出正确的 id 并且永远不会不同步:

var id: String { offer.id }
Run Code Online (Sandbox Code Playgroud)

示例中还有另一个问题值得解决。OfferRowViewModel 代表了一个监视列表并对其进行过滤的发布者。但正如所写,这意味着每个 OfferRowViewModel 都会迭代每个列表,或者换句话说,每次报价或列表更改时都会执行 O(n * m) 操作。这在实践中可能没问题,但如果有大量优惠或大量列表,或者它们经常更改,则可能会产生性能问题。联合起来确实很强大,但它的缺点之一是它会让我们更难看到这样的效率问题。

通过用简单的模型类型替换 OfferRowViewModel 可以使此代码变得更简单:

struct OfferAndListing: Identifiable {
    var offer: Offer
    var listing: Listing

    var id: String { offer.id }
}
Run Code Online (Sandbox Code Playgroud)

那么您提供报价的发布商可能看起来更像这样:

Publishers.CombineLatest(offerRepository.$offers, listingRepository.$listings)
    .receive(on: RunLoop.main)
    .map { (offers, listings) in
                offers
                    .filter { $0.userId == currentUserUid }
                    .map { OfferAndListing(
                        offer: $0,
                        listing: listings.first(where: { })
                    )}
    }
    .sink { [weak self] in self?.offersAndListings = $0 }
    .store(in: &cancellables)
Run Code Online (Sandbox Code Playgroud)

这还没有提高效率,但它提供了一个地方来查看工作正在哪里完成和优化,使用更容易推理的单个发布者链,并简化了模型类型,因此它不需要了解任何信息合并、数据存储库等。