所以这个类别树视图支持来自 API 的数据,这些数据没有关于类别级别深度的限制或规则。所以基本上父类别可以有很多子级别。要在 SwiftUI View 中实现此实现,我们可以使用递归视图方法。但是如何递归调用视图呢?
下面是 CategoryTreeView 的实现:
struct ProductCategoryTreeView: View {
var selectionType: ProductCategoryBottomSheetSelectionType
var selectedCategories: [ProductCategoryModel]?
let categories: [ProductCategoryModel]
let ids: [String]
let onClick: (ProductCategoryModel, [String]) -> Void
init(selectionType: ProductCategoryBottomSheetSelectionType,
selectedCategories: [ProductCategoryModel]?,
categories: [ProductCategoryModel],
ids: [String] = [],
onClick: @escaping (ProductCategoryModel, [String]) -> Void) {
self.selectionType = selectionType
self.selectedCategories = selectedCategories
self.categories = categories
self.ids = ids
self.onClick = onClick
}
var body: some View {
OptionalLazyVStack(alignment: .center, spacing: 0) {
ForEach(categories) { category in
let categoryIds = ids + [category.id]
VStack(alignment: .center, spacing: 0) {
CategoryItemView(
selectionType: selectionType,
selectedCategories: selectedCategories,
category: category,
ids: categoryIds,
onClick: onClick
).listRowInsets(EdgeInsets())
Rectangle()
.fill(ColorManager.Primary.grayLight1.color)
.frame(height: 1)
}
if let childCategories = category.childCategories,
category.isExpanded {
Self(selectionType: selectionType,
selectedCategories: selectedCategories,
categories: childCategories,
ids: categoryIds,
onClick: onClick)
}
}
}.padding(.leading, getLeadingPadding())
.animation(.none)
}
private func getLeadingPadding() -> CGFloat {
switch selectionType {
case .single:
return ids.count == 0 ? CGFloat(ids.count * 16) : CGFloat(ids.count) * 10
case .multiple:
return ids.count == 0 ? CGFloat(ids.count * 32) : CGFloat(ids.count) * 16
}
}
}
这是 CategoryItemView:
private struct CategoryItemView: View {
var selectionType: ProductCategoryBottomSheetSelectionType
var selectedCategories: [ProductCategoryModel]?
let category: ProductCategoryModel
let ids: [String]
let onClick: (ProductCategoryModel, [String]) -> Void
@State private var isLoading: Bool = false
var body: some View {
HStack(alignment: .center, spacing: 12) {
switch selectionType {
case .single:
singleView
case .multiple:
multipleView
}
}.contentShape(Rectangle())
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.if(isHighlight(category)) {
$0.background(
RoundedRectangle(cornerRadius: 8)
.foregroundColor(ColorManager.Primary.blueLight2.color)
)
}.onTapGesture {
isLoading = true
onClick(category, ids)
}.padding(.vertical, 4)
}
}
private extension CategoryItemView {
@ViewBuilder var singleView: some View {
Text(getCategoryName(category))
.typography(type: .body2())
Spacer()
if isLoading(category) {
ActivityIndicatorView(isAnimating: .constant(true), style: .medium)
} else {
categoryItemArrowView(category)
}
}
@ViewBuilder var multipleView: some View {
if isLoading(category) {
ActivityIndicatorView(isAnimating: .constant(true), style: .medium)
} else {
categoryItemArrowView(category)
}
Text(getCategoryName(category))
.typography(type: .body2())
Spacer()
checkboxView
.isHidden(isHideCheckboxView(category), remove: true)
}
@ViewBuilder var checkboxView: some View {
BSCCheckbox(label: "",
isDisabled: false,
state: Binding.init(get: { getCategoryCheckboxState(category) },
set: { _ in
onClick(category, ids)
}))
}
}
// MARK: View
private extension CategoryItemView {
@ViewBuilder func categoryItemArrowView(_ category: ProductCategoryModel) -> some View {
if let childCount = category.childCount, childCount > 0 {
Group {
R.image.down.image
.resizable()
.rotationEffect(category.isExpanded ? Angle(degrees: -180) : Angle(degrees: 0))
}.frame(width: 18, height: 18)
.if(selectionType == .single) {
$0.foregroundColor(ColorManager.Primary.blue.color)
}.if(selectionType == .multiple) {
$0.foregroundColor(ColorManager.Text.blackLow.color)
}
}
}
}
// MARK: Handler
private extension CategoryItemView {
func getCategoryName(_ category: ProductCategoryModel) -> String {
var name = category.name ?? ""
if LanguageManager.getCurrentLanguage() == LanguageOption.en {
name = category.nameEnglish ?? name
}
return name
}
func getCategoryCheckboxState(_ category: ProductCategoryModel) -> BSCCheckboxState {
return selectedCategories?.contains(where: { $0.id == category.id }) ?? false ? .selected : .unselected
}
func isLoading(_ category: ProductCategoryModel) -> Bool {
guard let isEmpty = category.childCategories?.isEmpty, !isEmpty else {
return false
}
return (category.childCategories?.isEmpty ?? true) && isLoading
}
func isHighlight(_ category: ProductCategoryModel) -> Bool {
return selectedCategories?.first(where: { $0.id == category.id }) != nil
}
func isHideCheckboxView(_ category: ProductCategoryModel) -> Bool {
if let childCount = category.childCount, childCount == 0 {
return false
} else {
return true
}
}
}
在这里我们可以看到类别树的主视图是循环 CategoryItemView 的 foreach,我们在其中维护 categoryIds 以确定用户选择哪个类别。此处使用的 CategoryModel 代表我们从 API 获得的数据,因此您可以根据您的数据使用自己的 CategoryModel。
主要主题是递归视图,我们可以使用“Self”将其称为自己的自我。因此,请确保您遵循每个递归规则,即您应该有一个基本案例来防止永远递归运行。这可能会使您的模拟器或设备冻结 😂 我们不希望这样。
您可以根据您的应用需求进行修改,只需了解 SwiftUI 中递归视图的基本概念即可。