我创建了一个获取并显示数据列表的视图。工具栏中有一个上下文菜单,用户可以在其中更改数据类别。此上下文菜单位于列表之外。
我想要做的是当用户选择一个类别时,列表应该从后端重新获取数据并重新绘制整个视图。
我制作了一个 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)
}
}
答案 0 :(得分:1)
在您的 BaseListView 中,您应该有一个 onChange 修饰符来捕捉对 userSettings.$selectedCategory 的更改并在那里调用 loadData。
如果您无权访问 BaseListView 中的 userSettings,请将其作为 Binding 或 @EnvironmentObject 传入。