在 SwiftUI 中创建多步骤视图

利用 iOS 16 .onPush 过渡构建可自定义的多用途步骤视图

在本文中,我们将介绍如何创建可自定义的多步骤视图,该视图可用于项目中的不同位置,以便在多个屏幕中进行 SwiftUI 导航。它可以是多才多艺的。您可以使用它向用户展示注册帐户、订购包裹、交付过程的步骤,甚至将其用于评级系统,正如我在下面的示例部分中所做的那样。

本文将依赖于将步骤作为枚举。在展示如何构建它之前,我们将介绍构建多步骤视图所需的内容。

CaseIterable 扩展

我们需要在枚举中获取下一个和上一个案例,以根据当前视图更改我们的视图。我们可以通过使用 all-cases 数组来做到这一点。接下来,我们可以创建一个数组来存储所有案例的所有原始值。这rawValues将是我们添加到视图中的额外内容。在示例中,我尝试将图标和标题作为额外内容。

extension CaseIterable where Self: Equatable & RawRepresentable {
    var allCases: AllCases { Self.allCases }
    func next() -> Self {
        let index = allCases.firstIndex(of: self)!
        let next = allCases.index(after: index)
        guard next != allCases.endIndex else { return allCases[index] }
        return allCases[next]
    }
    
    func previous() -> Self {
        let index = allCases.firstIndex(of: self)!
        let previous = allCases.index(index, offsetBy: -1)
        guard previous >= allCases.startIndex else { return allCases[index] }
        return allCases[previous]
    }
    
    static var allValues: [RawValue] {
        return allCases.map { $0.rawValue }
    }
}

额外步骤内容

为额外的内容准备一个视图。正如我上面提到的,在下面的示例中,附加内容是图标或文本;因此,我们正在发送字符串并检查它们是否可以是图像。如果没有,则将它们显示为文本。您可以使用不同的Image初始化程序。对于本教程,我将只使用系统符号。因此,我使用的init是系统名称。

struct ExtraStepContent: View {
    let index : Int
    let color : Color
    let extraContent : [String]
    let extraContentSize : CGSize?
    var body: some View {
        ZStack {
            if let extra = extraContent[index] {
                if UIImage(systemName: extra) != nil {
                    Image(systemName: extra)
                } else {
                    Text(extra)
                }
            }
        }
        .foregroundColor(color)
        .frame(width: extraContentSize?.width, height: extraContentSize?.height)
    }
}

额外内容位置

为额外的内容位置选项创建一个枚举。我在下面有三个选项,但您可以根据需要添加额外的选项。

enum ExtraContentPosition {
    case above
    case inline
    case onTop
}

多步骤视图

最后,我们正在创建我们的多步骤视图。我们需要这个视图来接受代表我们项目中某些步骤的任何枚举。我们可以通过使我们所有的枚举都符合CaseIterable协议来实现这一点,然后将它们传递给我们的多步骤视图。

我们还将一些观点作为内容传递。您可以传递的内容示例是表示步骤的矩形或圆形。在视图主体中,我们将遍历这些步骤并为它们提供所有相同的传递内容。

接下来,您可以添加额外的内容。为此,我们可以传递附加内容的位置(aboveinlineonTop),然后添加 if 或 switch 语句来调整额外视图的位置。if 语句的行数将更少,如下所示

如果您希望每一步都有一个点击操作,您可以将一个函数传递给视图,如下所示。将轻敲步骤的索引传递给操作函数以执行基于步骤的操作。

struct MultiStepsView <T, Content: View> : View where T: Swift.CaseIterable {
    @Binding var steps: [T]
    let extraContent : [String]?
    let extraContentPosition : ExtraContentPosition?
    let extraContentSize : CGSize?
    let action : (Int) -> Void
    @ViewBuilder let content: () -> Content
    
    @State var numberOfSteps : Int = 0
    @State var widthOfLastItem = 0.0
    @State var images : [UIImage] = []
    
    @ViewBuilder
    var body: some View {
        VStack {
            HStack {
                ForEach(0..<numberOfSteps, id: \.self) { index in
                    ZStack {
                        HStack(spacing: 0) {
                            VStack {
                                if let extraContent = extraContent, extraContentPosition == .above {
                                    ExtraStepContent(index: index, color: index < steps.count ? .accentColor :.gray, extraContent: extraContent, extraContentSize: extraContentSize)
                                }
                                content().foregroundColor(index < steps.count ? .accentColor :.gray)
                            }
                            if let extraContent = extraContent, extraContentPosition == .inline {
                                ExtraStepContent(index: index, color: index < steps.count ? .accentColor :.gray, extraContent: extraContent, extraContentSize: extraContentSize)
                            }
                        }
                    }.overlay {
                        if let extraContent = extraContent, extraContentPosition == .onTop , index < steps.count {
                            ExtraStepContent(index: index, color: .accentColor, extraContent: extraContent, extraContentSize: extraContentSize)
                        }
                    }
                    .onTapGesture {
                        action(index)
                    }
                }
            }
        }.onAppear() {
            numberOfSteps = type(of: steps).Element.self.allCases.count
        }
    }
}

示例1:送外卖

这是 CaseIterable我们所有步骤的枚举:


enum FoodDeliveryState : String, CaseIterable  {
    case orderReceived = "checkmark.circle"
    case preparingOrder = "takeoutbag.and.cup.and.straw"
    case onItsWay = "bicycle"
    case delivered = "house"
}

所有步骤的简单通用详细视图如下所示:

struct FoodDeliveryDetailed: View {
    let icon : String
    let color : Color
    let text : String
    var body: some View {
        VStack {
            Spacer()

            ZStack {
                Circle().fill(.ultraThinMaterial.opacity(0.3))
                    .frame(width: 260)
                    .offset(x: -10, y: -10)
                Circle().fill(.ultraThinMaterial.opacity(0.5))
                    .frame(width: 260)
                Circle().fill(.ultraThinMaterial.opacity(0.3))
                    .frame(width: 230)
                    .offset(x: 20, y: 20)
                
                Image(systemName: icon)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 160)
                    .foregroundColor(color.opacity(0.6))
            }
            Spacer()
            
            Text("YOUR ORDER STATUS:")
            
                        Text(text)
                            .font(.title)
                            .padding()
                            .background(Color.black.opacity(0.1))
                            .border(.white)
            Spacer()

        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .padding(24)
        .foregroundColor(.white)
    }
}

Multi-StepsView 父视图

import SwiftUI

struct FoodDelivery: View {
    @State var foodDeliveryStatus : [FoodDeliveryState] = [.orderReceived]
    @State var currentStatus : FoodDeliveryState = .orderReceived
    
    @State var currentColor : Color = .teal
    @State var accentColor : Color = .teal

    
    var body: some View {
        VStack {
            MultiStepsView(steps: $foodDeliveryStatus, extraContent: FoodDeliveryState.allValues, extraContentPosition : .inline, extraContentSize: nil, action: {_ in }) {
                HStack(spacing: 6) {
                    ForEach(0..<4) { _ in
                        Circle()
                            .frame(width: 4)
                    }
            }
                .frame(height: 20, alignment: .bottom)
                .padding(.horizontal, 8)
            }
            .accentColor(accentColor)
            .font(.title)
            .bold()
            .padding(4)
            
            Spacer()
            
            switch currentStatus {
            case .orderReceived:
                FoodDeliveryDetailed(icon: currentStatus.rawValue, color: .teal, text: "Order Received")
                    .transition(.push(from: .trailing))
                    .onAppear() {currentColor = .teal; accentColor = .green}

            case .preparingOrder:
                FoodDeliveryDetailed(icon: currentStatus.rawValue, color: .cyan, text: "Preparing Order")
                    .transition(.push(from: .trailing))
                    .onAppear() {currentColor = .cyan; accentColor = .yellow}

            case .onItsWay:
                FoodDeliveryDetailed(icon: currentStatus.rawValue, color: .blue, text: "On its way!")
                    .transition(.push(from: .trailing))
                    .onAppear() {currentColor = .blue; accentColor = .orange}

            case .delivered:
                FoodDeliveryDetailed(icon: currentStatus.rawValue, color: .mint, text: "Delivered")
                    .transition(.push(from: .trailing))
                    .onAppear() {currentColor = .mint; accentColor = .secondary}
            }
            
            Spacer()
        }
        .animation(.linear, value: currentStatus)
        .onTapGesture {
            currentStatus = currentStatus.next()
            foodDeliveryStatus.append(currentStatus)
        }
        .onLongPressGesture {
            foodDeliveryStatus.removeAll()
            currentStatus = .orderReceived
            foodDeliveryStatus.append(currentStatus)
        }
        .background(currentColor.opacity(1))
    }
}

示例2:采购步骤

CaseIterable枚举

enum OrderState : String, CaseIterable  {
    case shoppingCart = "cart"
    case payment = "creditcard.fill"
    case shippingLocation = "mappin.and.ellipse"
    case complete = "shippingbox.fill"
}

Multi-StepsView 父视图

import SwiftUI

struct PurchasingSteps: View {
    @State var orderStatus : [OrderState] = [.shoppingCart]
    @State var currentStatus : OrderState = .shoppingCart
    
    var body: some View {
        ZStack {
            VStack {
                // MULTI-STEPS View
                MultiStepsView(steps: $orderStatus, extraContent: OrderState.allValues, extraContentPosition : .above, extraContentSize: CGSize(width: 30, height: 30), action: {_ in }) {
                    RoundedRectangle(cornerRadius: 5).frame(height: 10)
                }
                .padding()
                .font(.title2)
                
                Spacer()
                 // STEP VIEW - CONDITIONAL
                
                switch currentStatus {
                case .shoppingCart:
                    Text("SHOPPING CART ITEMS")
                case .payment:
                    Text("PAYMENT FORM")
                case .shippingLocation:
                    Text("LOCATION FORM")
                case .complete:
                    Text("COMPLETE")
                }
                
                
                
                Spacer()
                
                // BOTTOM BUTTONS
                HStack {
                    
                    Button("Back") {
                        guard orderStatus.count > 1 else { return }
                        orderStatus.removeAll(where: {$0 == currentStatus})
                        currentStatus = currentStatus.previous()
                    }
                    .frame(maxWidth: .infinity)
                    .background(RoundedRectangle(cornerRadius: 10).fill(.blue))
                    
                    
                    Button("Next") {
                        if !orderStatus.contains(currentStatus.next()) {
                            currentStatus = currentStatus.next()
                            orderStatus.append(currentStatus)
                        }
                    }
                    .frame(maxWidth: .infinity)
                    .background(RoundedRectangle(cornerRadius: 10).fill(.blue))
                    
                }.buttonStyle(.borderedProminent)
                    .padding(.horizontal)
                
            }
        }
    }
}

示例3:星级

CaseIterable 枚举

enum Rating : Int, CaseIterable  {
    case oneStar = 1
    case twoStars = 2
    case threeStars = 3
    case fourStars = 4
    case fiveStars = 5
}

Multi-StepsView 父视图

struct StarsRating: View {
    @State var rating : [Rating] = []
    func addStars (index : Int) {
        var currentRating : Rating = .oneStar
        rating.removeAll()
        for _ in 0 ... index {
            rating.append(currentRating)
            currentRating = currentRating.next()
        }
    }
    
    var body: some View {
        ZStack {
            Color.blue.opacity(0.2).ignoresSafeArea()
            VStack {
                MultiStepsView(steps: $rating, extraContent: ["star.fill","star.fill","star.fill","star.fill","star.fill"], extraContentPosition: .onTop, extraContentSize: CGSize(width: 39, height: 30), action: addStars) {
                    Image(systemName: "star")
                        .frame(width: 30, height: 30)
                }
                .accentColor(.yellow)
                .font(.title2)
                
                Text(rating.isEmpty ? "How much would you rate this?": "\(rating.last!.rawValue) stars")
            }
        }
    }
}
庄朋龙
庄朋龙

一个爱生活的技术菜鸟

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注