SwiftUI 学习笔记(六):常见的 View 和 Modifier 解析(五)

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") //此时返回的按钮是一个箭头
            })
    }
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注