前言
SwiftUI
出来也有段时间了,关于
SwiftUI
更多的信息请看
这里
,那么苹果为什么要推出
SwiftUI
呢?很多小伙伴会有疑问,有的公司可能还在用着
OC
进行的开发,还有些小伙伴可能连
Swift
都不是很了解,这怎么就又出来一个
SwiftUI
。
回想一下我们再使用
OC
或者
Swift
进行
UI
开发的时候,假设我们要显示一个
Label
到屏幕中,我们要进行哪些操作呢?下面代码用
Swift
举例:
emmmm,这一切看起来都没有问题,先声明
label
,然后为
label
设置文字,最后在把他添加到
View
中。但是时代在进步呐,看看隔壁的
Flutter
,人家要显示一行文本到屏幕上面是怎么操作的?
去掉申明部分,别人一行代码就搞定了,明显比你优秀啊,而且人家的阅读性丝毫不比你弱,你怎么办~
这个时候苹果就在想了:“这个小伙子轻轻松松就可以把代码运行在多平台上,那开发者不是就更愿意用这个编写么?不行,老子要反击!!!”
所以
SwiftUI
就出来了,然后就实现了
声明式或者函数式
的方式来进行界面开发,由于是自家平台,要做到一份代码,多端通用自然也要提上日程,毕竟人是越来越懒了,能点头就搞定的,绝不开口说话。
我们看看
SwiftUI
如何实现显示文本:
现在看起来和
Flutter
旗鼓相当了不是吗?
SwiftUI
充分利用了
Swift
的特性,可以省略分号,在某些情况下可以省略
return
,美滋滋~~
本文默认你有
Swift
基础,如果没有请自行了解,至少熟悉基本语法,不然有些省略写法你看你会很晕
如果你之前连官方的
Demo
都没有看过,又没有网页、
Flutter
、小程序等开发经验,那么你暂时可以记住一句话,
什么都是
View
,你所看到的都是
View
组成。
Xcode
版本:
11.4
macOS
系统版本:
10.15.3
(你可以不是
10.15
以上的,但是如果要运行macOS版本,系统要求必须要
10.15
以上,最新版的
Xcode
也要
10.15.2
以上,所以升级吧!!!)
AppDeleagte
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
可以看到这里和我们之前的工程不一样了,之前那个Window的属性字段不见了,取而代之的是直接返回了UISceneConfiguration,在参数中我们可以看到有一个Default Configuration的字符串,这个字符串在我们的info.plist中可以查看到
这个是iOS13新加入的,通过Scene管理App的生命周期,所以SceneDelegate接管了他
SceneDelegate
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
看到这个代码,大家应该都很熟悉了,这里和之前的创建方式基本类似了,这里我们看到,他的rootviewController是通过一个UIHostingController包装起来的,里面的rootView就是我们的ContentView,所以程序运行之后,我们看到的就是ContentView
ContentView
终于到今天的主角了~~~
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
这里的代码就是新鲜热乎的(如果你没看过SwiftUI的话)
这里我们看到ContentView是用Struct修饰的,不在是class了,然后又一个关键字some,这个是在之前的语法中没有的,也是在SwiftUI中加入的,你应该还记得上面提到的,你看到的都是View
public protocol View : _View {
associatedtype Body : View
var body: Self.Body { get }
可以看到,SwiftUI中的View是一个协议,但是View使用了associatedtype来修饰,他不能直接作为类型使用,他只能约束类型。所以就有关键字some
没它之前我要显示Label,要这样子写
var body: Text {
Text("test")
要显示图片要这样子写:
var body: Image {
Image("abc.png")
要根据不同的类型指定,这是一个很痛苦的事情,本来就是声明式UI,你还要我每个都指定一下,岂不是很麻烦。有了some只有,就美滋滋了,不管你显示什么,只要你遵循了View协议就成
var body: some View {
Image("abc.png")
var body: some View {
Text("label")
some怎么实现的????答案在这里
OK,到这里为止,我们看完了第一个结构体,但是下面还有一个ContentView_Previews,这个家伙又是来干什么的呢????
可以看到自动生成的代码后面携带了_Previews,字面上的意思就是预览!!!,嗯他就是用来预览的,毕竟隔壁的Flutter早就实现了,你作为后面出来小伙子,不能比前辈还少功能吧
如何开启预览???
LoginAccountView和LoginPhoneView,新建的时候,记得要选择SwiftUI
2、修改ContentView
刚才我们建立了两个View,现在我们要通过一个列表显示两个选项,当我们点击的时候跳转过去
NavigationView 字面上上的意思,学过iOS开发的都知道,导航栏`View。
你可以把NavigationView看做是有导航栏的controller
我们要用列表展示两种登录方式然后你想列表,列表不就是List么~~,对就是这么简单
List展示一组列表,你可以把他看成是UITableView
有了List,我们需要一些Item,同时我们点击他的时候,需要他跳转到二级页面,跳转到二级页面也可以裂解为连接到下一级页面,所以这个关键字就是NavigationLink
NavigationLink拥有跳转到另外一个View的能力,之前提到过什么都是View组成,所以下一级页面也是一个View。
他有三个参数:
一个是destination:表示连接的View;
第二个是:isActive,用于表示是否已经激活下一个View了(或者说下一个View是不是已经显示了); 可忽略的参数
最后一个是label:需要返回View的closure
最后我们在给这个导航栏设置一个标题
.navigationBarTitle(
Text("登录Demo"),
displayMode: .large
在SwiftUI中,默认的displayMode是large效果,具体啥样子,参考设置主页
large 和手机设置效果一样
inline,传统样式
automatic 支持large就使用large,否则就使用inline
最后我们的ContentView代码是这样子的
struct ContentView: View {
@State private var loginAccountIsActive: Bool = false
@State private var loginPhoneIsActive: Bool = false
var body: some View {
NavigationView {
List {
NavigationLink(
destination: LoginAccountView(),
isActive: $loginAccountIsActive) {
Text("使用账户密码登录")
NavigationLink(
destination: LoginPhoneView(),
isActive: $loginPhoneIsActive) {
Text("使用手机号验证码登录")
.navigationBarTitle(Text("登录Demo"), displayMode: .large)
首先来了一个之前没见过的修饰符@State,对于没见过的内容,一律command+点击,进入内部文档查看一下他的意思:
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
/// Initialize with the provided initial value.
public init(wrappedValue value: Value)
/// Initialize with the provided initial value.
public init(initialValue value: Value)
/// The current state value.
public var wrappedValue: Value { get nonmutating set }
/// Produces the binding referencing this state value
public var projectedValue: Binding<Value> { get }
我们都知道,如果要在Struct中修改属性,就要添加mutating修饰,那你暂时可以理解为使用了@State修饰的属性,我们就可以控制的读写。
然后我们看到使用这个属性的时候是这样子的$account,这个在之前的Swift也是没有出现过的。其实这个就是配套@State使用的,如果对方需要的参数是Binding<T>,那么你就使用这个就好了。
@State和$value是一种缩写的方式,他们本来长这个样子
@State private var a: Int = 0
priavte var a = State(initialValue: 0)
a.binding
关于更多的这方面信息,请查看
接下来就是body部分了,这部分全是新内容!!!!
下面挨个解释一下啥意思
VStack
垂直方向的Stack,上面的代码又是一种简写形式,他的功能就是在垂直方向,可以让你放入至多10个子View,未简写方式如下
VStack(alignment: .leading, spacing: 10) {
Text("xxxx")
默认的alignment是.center
默认的spacing是nil
HStack
和VStack类似,只不过一个是垂直方向,一个是水平方向
ZStack
ps: 这个虽然没有用到,但是顺带一起提了
上面的VStack和HStack都是沿着一个方向进行布局,如果我们想要进行叠加布局怎么办???ZStack就是干着活的。上面的三个Stack除了布局方式不一样,其他的都一样。
Image
这个用来显示一张图片,内部不多,具体可以自行点击进去查看,需要说明的是,系统为我们提供了一堆内置的图片,使用Image(systemName: "xxx")进行调用,如果不知道名字怎么办!!!!
福利地址 下载完成之后就可以查看了
TextField
文本输入框,没啥好讲的,但是要吐槽一下,现在的TextField并不好用!!!!,能用的功能不多,要想做更多的事情,还是需要使用UITextField,这个也是后续会聊到的内容,如何桥接UITextField到SwiftUI
Divider
Spacer
空白填充,如果不使用这个,那么我们的UI会是居中对齐的,如果我们想要填充对齐到某一个方向,就可以使用他
然后就是用到View的几个属性的
padding
边距,如果你没有指定方向,默认就是四周,指定了一个之后,其他的就会失效,意思就是你指定了.top,如果此时你不指定左右下三个方向,那么他们是一点间距都没有的
OK到这里,我们就把上面的View的部分全部讲完了,你先运行也会看到这样子的UI
一般来说,密码是否可见,我们会有一个按钮去显示控制
所以我们需要加入一个新的View, Button
SwiftUI为我们提供了好几种Button,目前我们只需要使用一种就好了,有兴趣的可以去官网自行查看。
在第二个HStack中我们新增一个Button,并新增一个属性,用来控制是否可以显示按钮
var showPwd = false
...HStack
Button(action: {
self.showPwd.toggle()
Image(systemName: self.showPwd ?
"eye" : "eye.slash")
然后就给你报错了,这是因为你没给showPwd这个属性添加 @State,加上之后就没事了。
现在按钮是可以点击了,图片也在切换了,但是密码还是公开的,接下来我们就把这部分实现
把TextField的代码修改为如下代码
Image(systemName: "lock")
if showPwd {
TextField("请输入密码", text: $password, onCommit: {
} else {
SecureField("请输入密码", text: $password, onCommit: {
再次运行之后,就可以愉快的切换了
登录按钮的实现
在Devider和Spacer之间插入一个Button,同时添加一个属性isCanLogin
var isCanLogin: Bool {
account.count > 0 &&
password.count > 0
Button(action: {
print("login action")
Text("Login")
.foregroundColor(.white)
.frame(width: 100, height: 45, alignment: .center)
.background(isCanLogin ? Color.blue: Color.gray)
.cornerRadius(10)
.disabled(!isCanLogin)
这里我们使用了几个View的属性
frame
设置大小和对齐方式
background
背景,这里使用的是协议进行的约束,也就是你只要遵从了该协议就行,Color就遵循了
cornerRadius
disabled
是否是非激活状态
桥接UITextField到SwiftUI
新建一个文件PQTextField继承协议UIViewRepresentable,这个协议就是用来桥接的,其他的暂时不管。
你只要记得三个重要的方法
makeUIView
创建桥接的UIKit
updateUIView
makeCoordinator
UIKit代理的实现者
然后我们参考上面的TextView,我们要做一个体验和TextField基本一致的View出来
struct PQTextField: UIViewRepresentable {
typealias PQTextFieldClosure = (UITextField) -> Void
/// placeholder
var placeholder: String? = nil
/// max can input length
var maxLength: Int? = nil
/// default text
var text: String? = nil
/// onEditing
var onEditing: PQTextFieldClosure?
/// onCommit
var onCommit: PQTextFieldClosure?
/// 配置时使用
var onConfig: PQTextFieldClosure?
func makeUIView(context: Context) -> UITextField {
func updateUIView(_ tf: UITextField, context: Context) {
func makeCoordinator() -> Coordinator {
然后我们依次把空白的地方补全
首先是makeUIView,这里需要我们返回一个UIKit的视图
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
return textField
然后分析我们要实现的功能,监听UITextField输入情况,这里要设置他的代理;设置的他的初始值,比如placeholder,
创建代理类
class Coordinator: NSObject, UITextFieldDelegate {
let textField: PQTextField
var onEditing: PQTextFieldClosure?
var onCommit: PQTextFieldClosure?
init(_ tf: PQTextField, onEditing: PQTextFieldClosure?, onCommit: PQTextFieldClosure?) {
self.textField = tf
self.onEditing = onEditing
self.onCommit = onCommit
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
onEditing?(textField)
var length = range.location + 1
if string == "", textField.text?.count ?? 0 == range.location + range.length { // 表示是删除
length -= 1
if length >= self.textField.maxLength ?? -1 {
onCommit?(textField)
if let maxLength = self.textField.maxLength, string != "" {
let value = (textField.text?.count ?? 0) < maxLength
return value
return true
func textFieldDidEndEditing(_ textField: UITextField) {
onCommit?(textField)
onCommit = nil
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
onCommit?(textField)
onCommit = nil
return true
@objc
func textChange(textField: UITextField) {
onEditing?(textField)
代理类里面的代码就是Swift的部分,和SwiftUI半毛钱关系都没有,具体做的事情就是监听代理,然后通过closure回调出去
实现makeCoordinator方法
func makeCoordinator() -> Coordinator {
Coordinator(self, onEditing: onEditing, onCommit: onCommit)
然后在makeUIView中补全代码
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeholder
textField.addTarget(context.coordinator, action: #selector(context.coordinator.textChange(textField:)), for: .editingChanged)
textField.text = text
onConfig?(textField)
return textField
实现updateUIView
func updateUIView(_ tf: UITextField, context: Context) {
tf.placeholder = placeholder
tf.text = text
最后完整的代码如下
struct PQTextField: UIViewRepresentable {
typealias PQTextFieldClosure = (UITextField) -> Void
/// placeholder
var placeholder: String? = nil
/// max can input length
var maxLength: Int? = nil
/// default text
var text: String? = nil
/// onEditing
var onEditing: PQTextFieldClosure?
/// onCommit
var onCommit: PQTextFieldClosure?
/// 配置时使用
var onConfig: PQTextFieldClosure?
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeholder
textField.addTarget(context.coordinator, action: #selector(context.coordinator.textChange(textField:)), for: .editingChanged)
textField.text = text
onConfig?(textField)
return textField
func updateUIView(_ tf: UITextField, context: Context) {
tf.placeholder = placeholder
tf.text = text
func makeCoordinator() -> Coordinator {
Coordinator(self, onEditing: onEditing, onCommit: onCommit)
class Coordinator: NSObject, UITextFieldDelegate {
let textField: PQTextField
var onEditing: PQTextFieldClosure?
var onCommit: PQTextFieldClosure?
init(_ tf: PQTextField, onEditing: PQTextFieldClosure?, onCommit: PQTextFieldClosure?) {
self.textField = tf
self.onEditing = onEditing
self.onCommit = onCommit
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
onEditing?(textField)
var length = range.location + 1
if string == "", textField.text?.count ?? 0 == range.location + range.length { // 表示是删除
length -= 1
if length >= self.textField.maxLength ?? -1 {
onCommit?(textField)
if let maxLength = self.textField.maxLength, string != "" {
let value = (textField.text?.count ?? 0) < maxLength
return value
return true
func textFieldDidEndEditing(_ textField: UITextField) {
onCommit?(textField)
onCommit = nil
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
onCommit?(textField)
onCommit = nil
return true
@objc
func textChange(textField: UITextField) {
onEditing?(textField)
有了上面的基础,View搭建这块我们就手到擒来了
struct LoginPhoneView: View {
@State private var phoneNumber: String = ""
@State private var code: String = ""
@State private var phoneNumIsEdit = false
@State private var codeIsEdit = false
@State private var timer: Timer?
@State private var countDown = 60
var isPhoneNum: Bool {
if accountIsEdit {
return phoneNumber.count == 11
return true
var isCode: Bool {
if codeIsEdit {
return code.count == 4
return true
var isCanLogin: Bool {
isPhoneNum && isCode
var body: some View {
VStack {
VStack {
HStack {
Image(systemName: "phone.down.circle")
.rotationEffect(Angle(degrees: 90))
PQTextField(placeholder: "请输入号码", maxLength: 11,text: phoneNumber, onEditing: { tf in
}, onCommit: { tf in
.frame(height: 40)
if !isPhoneNum {
Text("手机号码应该是11位数字")
.font(.caption)
.foregroundColor(.red)
Divider()
VStack {
HStack {
PQTextField(placeholder: "请输入验证码", maxLength: 4, text: code, onEditing: { tf in
}, onCommit: { tf in
.frame(height: 40)
Button(action: {
// get code
}, label: {
Text((countDown == 60) ? "获取验证码" : "请\(countDown)s之后重试")
}).disabled(countDown != 60 || phoneNumber.count != 11)
if !isCode {
Text("请输入正确的验证码(4位数字)")
.font(.caption)
.foregroundColor(.red)
.frame(alignment: .top)
Divider()
Button(action: {
print("login action", self.phoneNumber, self.code)
Text("Login")
.foregroundColor(.white)
}.frame(width: 100, height: 45, alignment: .center)
.background(isCanLogin ? Color.blue: Color.gray)
.cornerRadius(10)
.disabled(!isCanLogin)
Spacer()
.onAppear {
self.createTimer()
.onDisappear {
self.invalidate()
.padding()
private func createTimer() {
private func invalidate() {
首先我们创建了几个属性
phoneNumber 保存手机使用
code 验证码
phoneNumIsEdit 是否开始输入手机号了
codeIsEdit 是否开始输入验证码了
timer 倒计时的时候使用
countDown 倒计时的时间
isPhoneNum 判断是不是手机号,这里只做了非常简单的判断
isCode 判断是不是验证码,这里也是非常简单的判断
isCanLogin 是否可以登录了(控制按钮是否可以点击)
接下来的视图部分和之前大体相同,这部分的代码带过
最后我们看到我们又使用了两个新的方法
onAppear
这个会在视图加载的时候调用
onDisappear
这个会在视图消失的时候调用
那么在这里做啥子呢?,没错,就是用来场景定时器的
我们去实现两个定时器方法
创建定时器
private func createTimer() {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (t) in
if self.countDown < 0 {
self.countDown = 0
t.invalidate()
self.countDown -= 1
// 先不触发定时器
timer?.fireDate = .distantFuture
创建定时器,这里一定要注意的是,一定要做好判断,不能重复创建定时器,否则会有多少个定时器同时在跑,尤其是当前界面进入下级页面的时候
销毁定时器
private func invalidate() {
timer?.invalidate()
为什么创建的时候做了判断,但是销毁的时候却没有处理呢???
如果你足够细心,那你一定看到了countDown是用@State修饰的
最后我们补全在PQTextField的Closure的代码之后,完整的代码如下
struct LoginPhoneView: View {
@State private var phoneNumber: String = ""
@State private var code: String = ""
@State private var phoneNumIsEdit = false
@State private var codeIsEdit = false
@State private var timer: Timer?
@State private var countDown = 60
var isPhoneNum: Bool {
if phoneNumIsEdit {
return phoneNumber.count == 11
return true
var isCode: Bool {
if codeIsEdit {
return code.count == 4
return true
var isCanLogin: Bool {
isPhoneNum && isCode
var body: some View {
VStack {
VStack {
HStack {
Image(systemName: "phone.down.circle")
.rotationEffect(Angle(degrees: 90))
PQTextField(placeholder: "请输入号码", maxLength: 11,text: phoneNumber, onEditing: { tf in
self.phoneNumIsEdit = true
self.phoneNumber = tf.text ?? ""
}, onCommit: { tf in
self.phoneNumIsEdit = false
self.phoneNumber = tf.text ?? ""
.frame(height: 40)
if !isPhoneNum {
Text("手机号码应该是11位数字")
.font(.caption)
.foregroundColor(.red)
Divider()
VStack {
HStack {
PQTextField(placeholder: "请输入验证码", maxLength: 4, text: code, onEditing: { tf in
self.codeIsEdit = true
self.code = tf.text ?? ""
}, onCommit: { tf in
self.codeIsEdit = false
self.code = tf.text ?? ""
.frame(height: 40)
Button(action: {
// get code
}, label: {
Text((countDown == 60) ? "获取验证码" : "请\(countDown)s之后重试")
}).disabled(countDown != 60 || phoneNumber.count != 11)
if !isCode {
Text("请输入正确的验证码(4位数字)")
.font(.caption)
.foregroundColor(.red)
.frame(alignment: .top)
Divider()
Button(action: {
print("login action", self.phoneNumber, self.code)
Text("Login")
.foregroundColor(.white)
}.frame(width: 100, height: 45, alignment: .center)
.background(isCanLogin ? Color.blue: Color.gray)
.cornerRadius(10)
.disabled(!isCanLogin)
Spacer()
.onAppear {
self.createTimer()
.onDisappear {
self.invalidate()
.padding()
private func createTimer() {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (t) in
if self.countDown < 0 {
self.countDown = 0
t.invalidate()
self.countDown -= 1
// 先不触发定时器
timer?.fireDate = .distantFuture
private func invalidate() {
timer?.invalidate()
最终我们的两个小Demo就完成了。
第二个Demo基于第一个,如果你第二个没懂,你看你需要再去看看第一个Demo
实现点击空白处隐藏键盘
新建文件DismissKeyboard.swift
首先分析一下功能,点击空白处,空白处的View是Spacer,Spacer又遵循View协议,那我们可以为View扩展一个隐藏键盘的方法
import SwiftUI
extension View {
func endEditing() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
这里不建议使用keywindow的方法去做了
然后为了方便其他的View使用,自定义了一个struct遵从ViewModifier协议
struct DismissKeyboard: ViewModifier {
func body(content: Content) -> some View {
content.onTapGesture {
content.endEditing()
如何使用呢???
Text("xxxx")
.modifier(DismissKeyboard())
其实ViewModifier的妙用有很多,这里只是举了一个例子,比如我们要为某一个视图设置独特的样式,我们就可以新建一个文件,然后编写样式,之后只要需要用到这个样式的,就可以用类似上面的调用方法。
题外话: 那除了使用ViewModifier之外呢,我们还可以使用@ViewBuilder去做
struct DismissKeyboardBuilder<Content: View>: View {
let content: Content
init(@ViewBuilder _ content: () -> Content) {
self.content = content()
var body: some View {
content.onTapGesture {
self.content.endEditing()
他们两个的区别,我个人认为一个像继承,一个像协议。扯远了~~~
最后我们新建一个自己的Spacer
public struct DismissKeyboardSpacer: View {
public private(set) var minLength: CGFloat? = nil
public init(minLength: CGFloat? = nil) {
self.minLength = minLength
public var body: some View {
ZStack {
Color.black.opacity(0.001)
.modifier(DismissKeyboard())
Spacer(minLength: minLength)
.frame(height: minLength)
然后把LoginPhoneView里面的Spacer替换成为我们自己创建的DismissKeyboardSpacer,再去运行一下看下效果
首先视图方面
HStack、VStack、ZStack、List、Button、Text、TextFiled、Divider、Spacer、NavigationView、NavigationLink
然后方法方面