如何在 SwiftUI 中强制重新创建视图?

时间:2021-06-05 15:33:05

标签: swiftui

我创建了一个获取并显示数据列表的视图。工具栏中有一个上下文菜单,用户可以在其中更改数据类别。此上下文菜单位于列表之外。

我想要做的是当用户选择一个类别时,列表应该从后端重新获取数据并重新绘制整个视图。

我制作了一个 BaseListView,它可以在我的应用程序的各种屏幕中重复使用,而且由于 loadData 在 BaseListView 中,我不知道如何调用它来重新加载数据。

我这样做是出于好意吗?有没有办法强制 SwiftUI 重新创建整个视图,以便 BaseListView 在第一次创建时加载数据并呈现子视图?

struct ProductListView: View {
    var body: some View {
        BaseListView(
            rowView: { ProductRowView(product: $0, searchText: $1)},
            destView: { ProductDetailsView(product: $0) },
            dataProvider: {(pageIndex, searchText, complete) in
                return fetchProducts(pageIndex, searchText, complete)
            })
            .hideKeyboardOnDrag()
            .toolbar {
                ProductCategories()
            }
            .onReceive(self.userSettings.$selectedCategory) { category in
               //TODO: Here I need to reload data & recreate entire of view.
            }
            .navigationTitle("Products")
    }
}
extension ProductListView{
    private func fetchProducts(_ pageIndex: Int,_ searchText: String, _ complete: @escaping ([Product], Bool) -> Void) -> AnyCancellable {
        let accountId = Defaults.selectedAccountId ?? ""
        let pageSize = 20
        let query = AllProductsQuery(id: accountId,
                                   pageIndex: pageIndex,
                                   pageSize: pageSize,
                                   search: searchText)
        return Network.shared.client.fetchPublisher(query: query)
            .sink{ completion in
                switch completion {
                case .failure(let error):
                    print(error)
                case .finished:
                    print("Success")
                }
            } receiveValue: { response in
                if let data = response.data?.getAllProducts{
                    let canLoadMore = (data.count ?? 0) > pageSize * pageIndex
                    let rows = data.rows
                    complete(rows, canLoadMore)
                }
            }
    }
}

ProductCategory 是一个单独的视图:

struct ProductCategories: View {
    @EnvironmentObject var userSettings: UserSettings
    var categories = ["F&B", "Beauty", "Auto"]
    var body: some View{
        Menu {
            ForEach(categories,id: \.self){ item in
                Button(item, action: {
                    userSettings.selectedCategory = item
                    Defaults.selectedCategory = item
                })
            }
        }
        label: {
            Text(self.userSettings.selectedCategory ?? "All")
                .regularText()
                .autocapitalization(.words)
                .frame(maxWidth: .infinity)
            
        }.onAppear {
            userSettings.selectedCategory = Defaults.selectedCategory
        }
    }
}

由于我的应用程序具有具有相同行为(分页、搜索等)的各种列表视图,因此我创建了这样的 BaseListView:

struct BaseListView<RowData: StringComparable & Identifiable, RowView: View, Target: View>: View {
    enum ListState {
        case loading
        case loadingMore
        case loaded
        case error(Error)
    }
    
    typealias DataCallback = ([RowData],_ canLoadMore: Bool) -> Void
    
    @State var rows: [RowData] = Array()
    @State var state: ListState = .loading
    @State var searchText: String = ""
    @State var pageIndex = 1
    @State var canLoadMore = true
    @State var cancellableSet = Set<AnyCancellable>()
    @ObservedObject var searchBar = SearchBar()
    @State var isLoading = false
    
    let rowView: (RowData, String) -> RowView
    let destView: (RowData) -> Target
    let dataProvider: (_ page: Int,_ search: String, _  complete: @escaping DataCallback) -> AnyCancellable
    var searchable: Bool?

    var body: some View {
        HStack{
            content
        }
        .if(searchable != false){view in
            view.add(searchBar)
        }
        .hideKeyboardOnDrag()
        .onAppear(){
            print("On appear")
            searchBar.$text
                .debounce(for: 0.8, scheduler: RunLoop.main)
                .removeDuplicates()
                .sink { text in
                    print("Search bar updated")
                    self.state = .loading
                    self.pageIndex = 1
                    self.searchText = text
                    self.rows.removeAll()
                    self.loadData()
                }.store(in: &cancellableSet)
        }
    }
    
    private var content: some View{
        switch state {
        case .loading:
            return Spinner(isAnimating: true, style: .large).eraseToAnyView()
        case .error(let error):
            print(error)
            return Text("Unable to load data").eraseToAnyView()
        case .loaded, .loadingMore:
            return
                ScrollView{
                    list(of: rows)
                }
                .eraseToAnyView()
        }
    }
    
    private func list(of data: [RowData])-> some View{
        LazyVStack{
            let filteredData = rows.filter({
                searchText.isEmpty || $0.contains(string: searchText)
            })
            
            ForEach(filteredData){ dataItem in
                VStack{
                    //Row content:
                    if let target = destView(dataItem), !(target is EmptyView){
                        NavigationLink(destination: target){
                            row(dataItem)
                        }
                    }else{
                        row(dataItem)
                    }
                    
                    //LoadingMore indicator
                    if case ListState.loadingMore = self.state{
                        if self.rows.isLastItem(dataItem){
                            Seperator(color: .gray)
                            LoadingView(withText: "Loading...")
                        }
                    }
                }
            }
        }
    }
    
    private func row(_ dataItem: RowData) -> some View{
        rowView(dataItem, searchText).onAppear(){
            //Check if need to load next page of data
            if rows.isLastItem(dataItem) && canLoadMore && !isLoading{
                isLoading = true
                state = .loadingMore
                pageIndex += 1
                print("Load page \(pageIndex)")
                loadData()
            }
        }.padding(.horizontal)
    }
    
    private func loadData(){
        dataProvider(pageIndex, searchText){ newData, canLoadMore in
            self.state = .loaded
            rows.append(contentsOf: newData)
            self.canLoadMore = canLoadMore
            isLoading = false
        }
        .store(in: &cancellableSet)
    }
}

1 个答案:

答案 0 :(得分:1)

在您的 BaseListView 中,您应该有一个 onChange 修饰符来捕捉对 userSettings.$selectedCategory 的更改并在那里调用 loadData。

如果您无权访问 BaseListView 中的 userSettings,请将其作为 Binding 或 @EnvironmentObject 传入。