利用 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
协议来实现这一点,然后将它们传递给我们的多步骤视图。
我们还将一些观点作为内容传递。您可以传递的内容示例是表示步骤的矩形或圆形。在视图主体中,我们将遍历这些步骤并为它们提供所有相同的传递内容。
接下来,您可以添加额外的内容。为此,我们可以传递附加内容的位置(above
、inline
或onTop
),然后添加 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")
}
}
}
}