[SwiftUI 100 天] Cupcake Corner - part3 禁用 modifier

483 阅读6分钟
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

表单验证和禁用

SwiftUI 的Form视图以一种非常便捷的方式存储用户输入的内容,但是有时候更进一步是重要的 —— 检查输入内容以确保它们有效,然后再继续操作。

好吧,我们有一个专门用于这个目的的 modifier:disabled()。这需要检查一个条件,如果条件为true,则它附着的任何内容都将不响应用户的输入 —— 按钮不能点击,滑块不能拖动,等等。你可以在这里使用简单的属性,但是任何条件都可以:读取一个计算属性,调用方法,等等。

为了演示这一点,下面是一个接收用户名和电子邮件地址的表单:

struct ContentView: View {
    @State private var username = ""
    @State private var email = ""

    var body: some View {
        Form {
            Section {
                TextField("Username", text: $username)
                TextField("Email", text: $email)
            }

            Section {
                Button("Create account") {
                    print("Creating account…")
                }
            }

        }
    }
}

在这个实例中,除非两个字段都填写,否则我们不希望用户创建帐户。因此,我们可以通过像下面这样添加disabled()modifier,来禁用包含“创建账户”按钮的表单段。

Section {
    Button("Create account") {
        print("Creating account…")
    }
}
.disabled(username.isEmpty || email.isEmpty)

它的意思是“如果username或者email是空的,则禁用这个段”,这正是我们想要的。

你可能会发现,把你的条件提取为单独的计算属性是值得的,比如这样:

var disableForm: Bool {
    username.count < 5 || email.count < 5
}
现在,你可以你的 modifier 中引用它:
.disabled(disableForm)

不管你是如何做的,我都希望你尝试运行应用,看看 SwiftUI 如何处理禁用的按钮 —— 当我们没有通过测试时,按钮的文本变为灰色,只要测试通过,按钮就会亮起蓝色。


译自 www.hackingwithswift.com/books/ios-s…

获取基本的订单详情

这个项目的第一步是创建一个用户下单界面,接收订单的基本信息:他们想要多少个纸杯蛋糕,哪些款式,是否需要任何特殊定制。

在进入UI之前,我们需要先定义数据模型。之前,我们曾经在简单值类型上使用@State,在引用类型上使用@ObservedObject,并且研究了如何让一个ObservableObject类包含结构体,以便我们同时获得两者的好处。

在这里,我们将采用不同的解决方案:我们要用一个单一的类来存储所有的数据,这些数据会在不同屏之间传递。也就是说,我们应用里的所有屏共享相同的数据,你会看到,这种方式非常好用。

就现在来说,这个类还不需要很多属性:

  • 蛋糕的类型,加上所有可能选项的静态数组。
  • 用户想要订的蛋糕数量。
  • 用户是否有特殊要求,它会在我们的 UI 中显示或者隐藏额外的选项。
  • 用户是否要在蛋糕上加些糖霜。
  • 用户是否要在蛋糕上加些巧克力米。

上面的每一个属性都要在自己改变时更新 UI,这意味着我们需要@Published来标记它们,并且让整个类遵循ObservableObject

因此,请创建一个新的 Swift 文件,取名 Order.swift,去掉 Foundation 的导入,然后添加下面的代码:

class Order: ObservableObject {
    static let types = ["香草", "草莓", "巧克力", "彩虹"]

    @Published var type = 0
    @Published var quantity = 3

    @Published var specialRequestEnabled = false
    @Published var extraFrosting = false
    @Published var addSprinkles = false
}

现在,我们可以通过添加下面的属性在ContentView中创建单一实例:

@ObservedObject var order = Order()

这是创建订单的唯一地方 —— 我们应用里的其他屏都会被传入这个属性,以便大家使用相同的数据。

我们将分三部分构建这一屏的 UI,首先是纸杯蛋糕的类型和数量。第一部分将显示一个选择器,用户可以从香草,草莓,巧克力和彩虹蛋糕中进行选择,然后是一个分档器,可以在 3 到 20 之间选择数量。所有这些内容会被包进一个表单,而表单本身处于导航视图中,以便我们可以设置标题。

把下面的代码放进ContentViewbody属性:

NavigationView {
    Form {
        Section {
            Picker("Select your cake type", selection: $order.type) {
                ForEach(0..<Order.types.count) {
                    Text(Order.types[$0])
                }
            }

            Stepper(value: $order.quantity, in: 3...20) {
                Text("Number of cakes: \(order.quantity)")
            }
        }
    }
    .navigationBarTitle("Cupcake Corner")
}

在我们添加第二部分和第三部分之前,我想要你编译并运行代码,然后尝试一下。我们的两个表单应该可以正常工作,但你可能 —— 只是可能 —— 在 Xcode 中看到一条常规的烦人的消息,有关ForEach:“count (4) != its initial count (1)”。

我之所以说“可能”,是因为这感觉就像是另一个 SwiftUI 的 bug:即使我们的ForEach正在使用固定的数据(即Order.types数组中的项目数),SwiftUI 似乎也很难处理它。如果你没有看到这个错误,就不必担心,但假如你的确看到了它,我想向你展示如何解决它:我们需要给它一个显式的标识符。

把你的ForEach修改成这样:

ForEach(0..<Order.types.count, id: \.self) {

这样就完全清除掉问题了,不过老实说,我寄希望于这个错误会在以后的 SwiftUI 更新中消失。

我们表单的第二部分将包含三个分别绑定到specialRequestEnabledextraFrostingaddSprinkles的开关。但是,只有第一个开关启用的前提下,第二个和第三个开关才应该可见,因此我们要把它们包装到一个条件中。

现在,添加第二部分:

Section {
    Toggle(isOn: $order.specialRequestEnabled.animation()) {
        Text("Any special requests?")
    }

    if order.specialRequestEnabled {
        Toggle(isOn: $order.extraFrosting) {
            Text("Add extra frosting")
        }

        Toggle(isOn: $order.addSprinkles) {
            Text("Add extra sprinkles")
        }
    }
}

继续再次运行应用,然后尝试一下 —— 注意刚才我绑定第一个开关时附着了一个animation()modifier,以便第二个和第三个开关可以平滑地滑入和滑出。

但是,还有另一个 bug,这一次它是我们自己制造的:如果我们启用特殊要求,然后启用“额外的糖霜”和“额外的巧克力米”中的一个或者两个,然后再禁用特殊要求,则我们之前的特殊要求选项会保持激活。也就是说,如果我们重新启用特殊要求,那么之前的特殊要求选项还处于激活状态。

这种问题不难解决 —— 如果你对代码里的每一层都做了编程:你的服务端,你的数据库等都在specialRequestEnabled被设置为false的时候忽略extraFrostingaddSprinkles。但是,一个更好的(也更安全的) 的方案是确保specialRequestEnabled被设置为false时,extraFrostingaddSprinkles也都被设置为false

我们可以通过添加一个didSet属性观察者给specialRequestEnabled来实现这个方案。添加下面的代码:

@Published var specialRequestEnabled = false {
    didSet {
        if specialRequestEnabled == false {
            extraFrosting = false
            addSprinkles = false
        }
    }
}

我们的第三部分最简单,因为它只是一个指向下一屏的NavigationLink。尽管我们还没有第二屏,可以快速添加一个:创建一个新的 SwiftUI 视图,取名AddressView,然后给它一个orderObservedObject属性,像这样:

struct AddressView: View {
    @ObservedObject var order: Order

    var body: some View {
        Text("Hello World")
    }
}

struct AddressView_Previews: PreviewProvider {
    static var previews: some View {
        AddressView(order: Order())
    }
}

我们很快会让这个新视图更加有用,不过目前我们可以先回到 ContentView.swift ,为表单添加最后一部分。这将创建一个指向AddressViewNavigationLink,传入当前的订单对象。

添加最后一部分:

Section {
    NavigationLink(destination: AddressView(order: order)) {
        Text("Delivery details")
    }
}

这样就完成了我们的第一屏。在我们继续之前最后尝试一下 —— 你应该能够选择蛋糕类型,选择数量,以及切换所有的开关。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~