SwiftUI 创建递归类别树视图

在这里插入图片描述

所以这个类别树视图支持来自 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 中递归视图的基本概念即可。

发表回复