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

如何在SwiftUI中强制重新创建视图?,swiftui,Swiftui,我创建了一个视图,它获取并显示数据列表。工具栏中有一个上下文菜单,用户可以在其中更改数据类别。此关联菜单位于列表之外 我想做的是,当用户选择一个类别时,列表应该从后端重新提取数据并重新绘制整个视图 我制作了一个BaseListView,它可以在我的应用程序的各个屏幕中重用,而且由于loadData在BaseListView中,我不知道如何调用它来重新加载数据 我做这件事的时候很好吗?有没有办法强制SwiftUI重新创建整个视图,以便BaseListView在第一次创建时加载数据并呈现子视图 st

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

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

我制作了一个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)
    }
}
struct BaseListView:视图{
枚举列表状态{
装箱
案例加载更多
满箱
案例错误(错误)
}
typealias DataCallback=([RowData],uLoadMore:Bool)->Void
@状态变量行:[RowData]=数组()
@状态变量State:ListState=.loading
@状态变量searchText:String=“”
@状态变量pageIndex=1
@状态变量canLoadMore=true
@状态变量CancelableSet=Set()
@ObservedObject var searchBar=searchBar()
@状态变量isLoading=false
让rowView:(RowData,String)->rowView
让destView:(RowData)->目标
让dataProvider:(uPage:Int,search:String,complete:@escaping DataCallback)->anyCancelable
var可搜索:布尔?
var body:一些观点{
HStack{
内容
}
.if(searchable!=false){view in
view.add(搜索栏)
}
.hideKeyboardOnDrag()
.onAppear(){
打印(“出现时”)
搜索栏。$text
.debounce(对于:0.8,调度程序:RunLoop.main)
.removeDuplicates()
.sink{text in
打印(“搜索栏更新”)
self.state=.loading
self.pageIndex=1
self.searchText=文本
self.rows.removeAll()
self.loadData()
}.store(位于:&可取消集)
}
}
私有var内容:一些观点{
开关状态{
案例.加载:
返回微调器(isAnimating:true,样式:.large)。橡皮擦到任何视图()
案例错误(let error):
打印(错误)
返回文本(“无法加载数据”).橡皮擦到任何视图()
case.loaded、.loadingMore:
返回
滚动视图{
列表(共:行)
}
.eraseToAnyView()
}
}
私有函数列表(数据:[RowData])->某些视图{
懒散的{
让filteredData=rows.filter({
searchText.isEmpty | |$0.contains(字符串:searchText)
})
ForEach(filteredData){dataItem在
VStack{
//行内容:
如果let target=destView(数据项),!(目标为空视图){
导航链接(目的地:目标){
行(数据项)
}
}否则{
行(数据项)
}
//加载更多指示器
如果case ListState.loadingMore=self.state{
if self.rows.isLastItem(数据项){
分隔符(颜色:。灰色)
加载视图(带文本:“加载…”)
}
}
}
}
}
}
私有func行(dataItem:RowData)->某些视图{
行视图(dataItem,searchText).onAppear(){
//检查是否需要加载下一页数据
如果rows.isLastItem(dataItem)&&canLoadMore&&isLoading{
isLoading=true
状态=.loadingMore
页面索引+=1
打印(“加载页面\(页面索引)”)
loadData()
}
}.padding(.卧式)
}
私有函数loadData(){
dataProvider(pageIndex,searchText){newData,可以在中加载更多
self.state=.loaded
append(contentsOf:newData)
self.canLoadMore=canLoadMore
isLoading=错误
}
.store(位于:&可取消集)
}
}

在BaseListView中,您应该有一个onChange修饰符,用于捕获对用户设置的更改。$selectedCategory并在那里调用loadData


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

谢谢。您的意思是在BaseListView中定义一个Bool变量,然后在外部绑定一个变量,然后在需要重新加载数据时切换它吗?然后在加载数据后,我们需要将其切换回false?不,onChange应该注意userSettings.category的更改-当更改时,调用fetch再次获取相应的类别。这将触发列表的重新加载。您可以在要强制重新创建的视图上使用
id
。在这里找到一个演示。谢谢@Asperi。视图按预期重新创建,但是导航栏和搜索栏变得透明。我在这里贴了新问题,你能看一下吗?顺便说一句,请把你的答案复制到一个答案上,我会把它标记为正确答案。