Form
Form 是 SwiftUI 中新增的,用类似 UITableView 的风格创建一个表单,多用于 App 的设置界面。
struct ContentView: View { @State var enableLocation = false var body: some View { NavigationView { Form { Text("louyu") Toggle(isOn: $enableLocation) { Text("开启通知权限") } Button("确定") { } }.navigationBarTitle(Text("设置")) } } }
同样我们可以设置 Section 来实现分组效果。
struct ContentView: View { @State var enableLocation = false var body: some View { NavigationView { Form { Section(header: Text("第一部分header"), footer: Text("第一部分footer")) { Text("louyu") Toggle(isOn: $enableLocation) { Text("开启通知权限") } } Section(header: Text("第二部分header")) { Button("确定") { } } }.navigationBarTitle(Text("设置")) } } }
实现对 Form 输入内容的验证,可以通过设置 Section 的 disable 来解决。
struct ContentView: View { @State private var username = "" @State private var password = "" //验证条件 var validation: Bool { username.count < 6 || password.count < 6 } var body: some View { Form { Section { TextField("用户名", text: $username) SecureField("密码", text: $password) } Section { Button("登录") { } }.disabled(validation) // 如果不满足条件,按钮无法点击 } } }
Alert
在 UIKit 中,Alert 和 ActionSheet 为 UIAlertController 的两种显示模式;而 SwiftUI 将他们分成了两个单独的控件。
struct ContentView: View { //需要定义某种可绑定条件,以确定Alert是否可见 @State var isError: Bool = false var body: some View { Button("Alert") { self.isError.toggle() }.alert(isPresented: $isError, content: { Alert(title: Text("提示"), message: Text("错误"), dismissButton: .default(Text("OK"))) }) } }
和 UIKit不同的是,SwiftUI中 Alert弹出时需要绑定一个 Bool类型的数据,仅当它为 true时才会弹窗。
对于一个弹窗,我们往往会设置多个按钮,实现如下。
struct ContentView: View { @State var isError: Bool = false var body: some View { Button("Alert") { self.isError.toggle() }.alert(isPresented: $isError, content: { Alert(title: Text("提示"), message: Text("是否继续?"), primaryButton: .default(Text("确定"), action: { }), secondaryButton: .cancel(Text("取消")) ) }) } }
primaryButton和 secondaryButton必须成对出现。
ActionSheet
ActionSheet 的用法与 Alert 类似,这里只给出使用示例,其他不再赘述。
struct ContentView: View { @State var isSheet: Bool = false //计算属性 var actionSheet: ActionSheet { ActionSheet(title: Text("提示"), message: Text("获取照片"), buttons: [ .default(Text("拍照"), action: { }), .destructive(Text("相册"), action: { }), .cancel(Text("取消")) ] ) } var body: some View { Button("Action Sheet") { self.isSheet = true }.actionSheet(isPresented: $isSheet, content: { self.actionSheet }) } }
界面跳转
之前我们所实现的都是单个界面,在 SwiftUI 中若要实现多个界面跳转主要有两种方式—— Sheet 跳转和 NavigationLink 导航跳转。
Sheet 跳转
Sheet 跳转类似 UIKit 中在 controller 中使用 self.present。
struct ContentView: View { @State var isModal: Bool = false var modal: some View { Text("新界面") } var body: some View { Button("Modal") { self.isModal = true }.sheet(isPresented: $isModal, content: { //执行跳转 self.modal }) } }
iOS 13 开始,Sheet 方式弹出的界面并不是全屏显示,这种显示可以通过下拉的方式让界面 dismiss。若我们希望手动让界面 dismiss,可以按如下方式处理。
import SwiftUI struct ContentView: View { @State var isModal: Bool = false var body: some View { Button("跳转") { self.isModal = true }.sheet(isPresented: $isModal, content: { //跳转出来的界面 NewView() }) } } struct NewView: View { //通过presentationMode来dismiss @Environment(\.presentationMode) var presentationMode var body: some View { VStack { Text("新界面") Button("Dismiss") { //手动dismiss self.presentationMode.wrappedValue.dismiss() } } } }
NavigationLink 导航跳转
NavigationLink 导航跳转类似于 UIKit 中 self.navigationController.pushViewController。
struct ContentView: View { var body: some View { NavigationView { //借助NavigationLink通过按钮跳转 NavigationLink(destination: Text("下一个界面")) { Text("跳转") }.navigationBarTitle("导航栏") } } }
我们也可以和 Sheet 跳转一样,绑定一个 Bool 变量来控制跳转。
import SwiftUI struct ContentView: View { //声明isActive @State var isActive = false var body: some View { NavigationView { //NavigationLink在按下按钮时设置为true,在适当的地方设置为false即可返回 NavigationLink(destination: NextView(isActive: $isActive), isActive: $isActive) { Text("跳转") }.navigationBarTitle("导航栏") } } } struct NextView: View { @Binding var isActive: Bool var body: some View { Button("返回"){ //将isActive设为false self.isActive = false } } }
再举一个在 List 中点击 Row 跳转的例子。
import SwiftUI struct User: Identifiable { var id = UUID() var name: String } struct ContentView: View { //数据源 let users = [User(name:"ZhangSan"), User(name:"LiSi"), User(name:"WangWu")] var body: some View { //用数据源构造列表 NavigationView { List(users) { user in NavigationLink(destination: Text(user.name)) { Text(user.name) } }.navigationBarTitle("Users") } } }
NavigationLink 与 Button 共存
如果一个 List 和 Row 中既有 NavigationLink 又有 Button,且 Button 存在多个的情况下,如何分开处理他们的事件呢?
struct ContentView: View { var body: some View { NavigationView { List { ForEach(0..<10) { row in HStack { //按钮1 Button(action: { print("按钮1:\(row) Clicked") }) { Text("按钮1:\(row)") }.buttonStyle(BorderlessButtonStyle()) // 必须设置,否则无法分别响应 .padding() //按钮2 Button(action: { print("按钮2:\(row) Clicked") }) { Text("按钮2:\(row)") }.buttonStyle(BorderlessButtonStyle()) .padding() // NavigationLink NavigationLink(destination: Text("List Row\(row)") ) { Text("NavigationLink\(row)") } } } }.navigationBarTitle("List") } } }
跳转时执行额外操作
从前面的案例可以看出,通过 NavigationLink 可以很轻松地完成导航式界面跳转,但是如果跳转的同时还可以执行额外操作,目前只依靠 NavigationLink 是无法完成的,只能通过间接的方式实现。下面介绍两种常用的方式。
● NavigationLink 显示的内容为一个 EmptyView(空视图),用 Button 做真正的操作,适用于按钮点击式跳转。这里面还有个小细节,默认情况下,NavigationLink 是有个向右的小箭头,如果不显示箭头,可以里面包含一个 EmptyView。
struct ContentView: View { @State private var presentMe = false var body: some View { NavigationView { VStack { NavigationLink(destination: Text("Hello"), isActive: $presentMe) { EmptyView() } Button(action: { //执行一些额外操作 //最后再跳转 self.presentMe = true }, label: { Text("设置") }) Spacer() }.navigationBarTitle(Text("主界面")) } } }
● 通过 simultaneousGesture 创建一个单击手势,适用于列表点击式跳转。
import SwiftUI struct User: Identifiable { var id = UUID() var name: String } struct ContentView: View { let users = [User(name:"ZhangSan"), User(name:"LiSi"), User(name:"WangWu")] var body: some View { NavigationView { List(users) { user in NavigationLink(destination: Text(user.name)) { Text(user.name) }.simultaneousGesture(TapGesture().onEnded{ print("执行额外操作") }) }.navigationBarTitle("Users") } } }
界面跳转中的一些其它操作
● 当 Image 被嵌入到 NavigationLink 中时,默认会被蒙上蓝色,为了避免遮挡,除了可以设置图片的渲染模式来解决,也可以通过在 NavigationLink 上设置 .buttonStyle(PlainButtonStyle()) 解决。
struct ContentView: View { var body: some View { NavigationView { VStack { ForEach(1..<10) { index in NavigationLink(destination: Text("Hello")) { HStack { Image("img") .resizable() .scaledToFit() Text("\(index)") } }.buttonStyle(PlainButtonStyle()) } } } } }
● 用 NavigationLink 完成导航跳转以后,默认会是返回按钮显示为 Back。如果需要自定义导航返回按钮,需要在跳转后的 View 中做如下的处理:
① 通过 .navigationBarBackButtonHidden 隐藏原先的按钮。
② 通过 .navigationBarItems 添加一个 leading 按钮完成自定义,但是需要手动处理返回事件。
//设置跳转后的View struct DetailView: View { @Environment(\.presentationMode) var mode: Binding<PresentationMode> var body: some View { VStack { Text("zhangsan") } .navigationBarBackButtonHidden(true) //隐藏原先返回的按钮 .navigationBarItems(leading: Button(action: { self.mode.wrappedValue.dismiss() }) { Image(systemName: "arrow.left") //此时返回的按钮是一个箭头 }) } }