前言
SwiftUI
出來也有段時(shí)間了裳扯,關(guān)于SwiftUI
更多的信息請(qǐng)看這里奢米,那么蘋果為什么要推出SwiftUI
呢油湖?很多小伙伴會(huì)有疑問检激,有的公司可能還在用著OC
進(jìn)行的開發(fā)肴捉,還有些小伙伴可能連Swift
都不是很了解,這怎么就又出來一個(gè)SwiftUI
叔收。
回想一下我們?cè)偈褂?code>OC或者Swift
進(jìn)行UI
開發(fā)的時(shí)候齿穗,假設(shè)我們要顯示一個(gè)Label
到屏幕中,我們要進(jìn)行哪些操作呢饺律?下面代碼用Swift
舉例:
...
void viewDidload() {
super.viewDidload()
let label = UILabel()
label.text = "你好窃页,Swift"
view.addSubview(label)
}
...
emmmm,這一切看起來都沒有問題复濒,先聲明label
脖卖,然后為label
設(shè)置文字,最后在把他添加到View
中巧颈。但是時(shí)代在進(jìn)步吶畦木,看看隔壁的Flutter
,人家要顯示一行文本到屏幕上面是怎么操作的砸泛?
...
@override
Widget build(BuildContext context) {
return Text('Welcome to Flutter');
}
...
去掉申明部分十籍,別人一行代碼就搞定了蛆封,明顯比你優(yōu)秀啊,而且人家的閱讀性絲毫不比你弱勾栗,你怎么辦~
這個(gè)時(shí)候蘋果就在想了:“這個(gè)小伙子輕輕松松就可以把代碼運(yùn)行在多平臺(tái)上惨篱,那開發(fā)者不是就更愿意用這個(gè)編寫么?不行围俘,老子要反擊6噬摺!楷拳!”
所以SwiftUI
就出來了绣夺,然后就實(shí)現(xiàn)了聲明式或者函數(shù)式
的方式來進(jìn)行界面開發(fā),由于是自家平臺(tái)欢揖,要做到一份代碼陶耍,多端通用自然也要提上日程,畢竟人是越來越懶了她混,能點(diǎn)頭就搞定的烈钞,絕不開口說話。
我們看看SwiftUI
如何實(shí)現(xiàn)顯示文本:
...
var body: some View {
Text("你好坤按,Swift")
}
...
現(xiàn)在看起來和Flutter
旗鼓相當(dāng)了不是嗎毯欣?SwiftUI
充分利用了Swift
的特性,可以省略分號(hào)臭脓,在某些情況下可以省略return
酗钞,美滋滋~~
必看
本文默認(rèn)你有Swift
基礎(chǔ),如果沒有請(qǐng)自行了解来累,至少熟悉基本語法砚作,不然有些省略寫法你看你會(huì)很暈
如果你之前連官方的Demo
都沒有看過,又沒有網(wǎng)頁嘹锁、Flutter
葫录、小程序等開發(fā)經(jīng)驗(yàn),那么你暫時(shí)可以記住一句話领猾,什么都是View
米同,你所看到的都是View
組成。
Xcode
版本:11.4
macOS
系統(tǒng)版本:10.15.3
(你可以不是10.15
以上的摔竿,但是如果要運(yùn)行macOS版本面粮,系統(tǒng)要求必須要10.15
以上,最新版的Xcode
也要10.15.2
以上拯坟,所以升級(jí)吧5稹!S艏尽)
新建工程
新建之后我們可以看到如下文件
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)
}
可以看到這里和我們之前的工程不一樣了冷溃,之前那個(gè)Window
的屬性字段不見了,取而代之的是直接返回了UISceneConfiguration
梦裂,在參數(shù)中我們可以看到有一個(gè)Default Configuration
的字符串似枕,這個(gè)字符串在我們的info.plist
中可以查看到
這個(gè)是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()
}
}
看到這個(gè)代碼凿歼,大家應(yīng)該都很熟悉了,這里和之前的創(chuàng)建方式基本類似了冗恨,這里我們看到答憔,他的rootviewController
是通過一個(gè)UIHostingController
包裝起來的,里面的rootView就是我們的ContentView
掀抹,所以程序運(yùn)行之后虐拓,我們看到的就是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
了傲武,然后又一個(gè)關(guān)鍵字some
蓉驹,這個(gè)是在之前的語法中沒有的,也是在SwiftUI
中加入的揪利,你應(yīng)該還記得上面提到的态兴,你看到的都是View
public protocol View : _View {
associatedtype Body : View
var body: Self.Body { get }
}
可以看到,SwiftUI
中的View
是一個(gè)協(xié)議疟位,但是View
使用了associatedtype
來修飾瞻润,他不能直接作為類型使用,他只能約束類型甜刻。所以就有關(guān)鍵字some
沒它之前我要顯示Label敢订,要這樣子寫
var body: Text {
Text("test")
}
要顯示圖片要這樣子寫:
var body: Image {
Image("abc.png")
}
要根據(jù)不同的類型指定,這是一個(gè)很痛苦的事情罢吃,本來就是聲明式UI
楚午,你還要我每個(gè)都指定一下,豈不是很麻煩尿招。有了some
只有矾柜,就美滋滋了,不管你顯示什么就谜,只要你遵循了View
協(xié)議就成
var body: some View {
Image("abc.png")
}
var body: some View {
Text("label")
}
some
怎么實(shí)現(xiàn)的怪蔑??丧荐?缆瓣?答案在這里
OK
,到這里為止,我們看完了第一個(gè)結(jié)構(gòu)體虹统,但是下面還有一個(gè)ContentView_Previews
弓坞,這個(gè)家伙又是來干什么的呢隧甚??渡冻?戚扳?
可以看到自動(dòng)生成的代碼后面攜帶了_Previews
,字面上的意思就是預(yù)覽W逦恰C苯琛!超歌,嗯他就是用來預(yù)覽的砍艾,畢竟隔壁的Flutter
早就實(shí)現(xiàn)了,你作為后面出來小伙子巍举,不能比前輩還少功能吧
如何開啟預(yù)覽脆荷??禀综?
然后點(diǎn)擊resume
(在右上角)简烘,等待一會(huì)兒就可以了,至于預(yù)覽顯示的速度(看你電腦設(shè)備定枷,我反正是放棄了)孤澎。
友情提示(按下command
然后點(diǎn)擊文字,有驚喜哦)
這個(gè)就比隔壁的Flutter
要強(qiáng)大了欠窒,但是要看你為蘋果充值了多少
開始干活
看完本期內(nèi)容你將會(huì)了解
- 如何跳轉(zhuǎn)頁面
- 如何處理輸入事件
@ViewBuilder
- 如何橋接
UIKit
- 熟悉幾個(gè)常用的
View
1覆旭、新建兩個(gè)文件
LoginAccountView
和LoginPhoneView
,新建的時(shí)候岖妄,記得要選擇SwiftUI
2型将、修改ContentView
剛才我們建立了兩個(gè)View
,現(xiàn)在我們要通過一個(gè)列表顯示兩個(gè)選項(xiàng)荐虐,當(dāng)我們點(diǎn)擊的時(shí)候跳轉(zhuǎn)過去
NavigationView
字面上上的意思七兜,學(xué)過iOS
開發(fā)的都知道宜猜,導(dǎo)航欄`View末荐。
你可以把
NavigationView
看做是有導(dǎo)航欄的controller
我們要用列表展示兩種登錄方式然后你想列表欺税,列表不就是List
么~~蛛芥,對(duì)就是這么簡單
List
展示一組列表,你可以把他看成是UITableView
有了List
藻丢,我們需要一些Item
病蛉,同時(shí)我們點(diǎn)擊他的時(shí)候扛施,需要他跳轉(zhuǎn)到二級(jí)頁面汽烦,跳轉(zhuǎn)到二級(jí)頁面也可以裂解為連接到下一級(jí)頁面涛菠,所以這個(gè)關(guān)鍵字就是NavigationLink
NavigationLink
擁有跳轉(zhuǎn)到另外一個(gè)View
的能力,之前提到過什么都是View組成,所以下一級(jí)頁面也是一個(gè)View
俗冻。他有三個(gè)參數(shù):
- 一個(gè)是
destination
:表示連接的View
礁叔;- 第二個(gè)是:
isActive
,用于表示是否已經(jīng)激活下一個(gè)View
了(或者說下一個(gè)View
是不是已經(jīng)顯示了)言疗; 可忽略的參數(shù)- 最后一個(gè)是
label
:需要返回View
的closure
最后我們?cè)诮o這個(gè)導(dǎo)航欄設(shè)置一個(gè)標(biāo)題
.navigationBarTitle(
Text("登錄Demo"),
displayMode: .large
)
在SwiftUI
中晴圾,默認(rèn)的displayMode
是large
效果颂砸,具體啥樣子噪奄,參考設(shè)置主頁
large 和手機(jī)設(shè)置效果一樣
inline,傳統(tǒng)樣式
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("使用手機(jī)號(hào)驗(yàn)證碼登錄")
}
}
.navigationBarTitle(Text("登錄Demo"), displayMode: .large)
}
}
}
然后運(yùn)行起來勤篮,你就可以看到一個(gè)有兩個(gè)列表項(xiàng)的視圖,點(diǎn)擊某一項(xiàng)的時(shí)候色罚,可以進(jìn)行調(diào)整到對(duì)應(yīng)的View
中
3碰缔、開始編寫賬號(hào)密碼登錄頁面
先把下面的代碼替換原來的實(shí)現(xiàn)
@State var account: String = ""
@State var password: String = ""
var body: some View {
VStack {
HStack {
Image(systemName: "person")
TextField("請(qǐng)輸入賬號(hào)", text: $account, onCommit: {
})
}
Divider()
HStack {
Image(systemName: "lock")
TextField("請(qǐng)輸入密碼", text: $password, onCommit: {
})
}
Divider()
Spacer()
}
.padding(.top, 100)
.padding(.leading)
.padding(.trailing)
}
首先來了一個(gè)之前沒見過的修飾符@State
,對(duì)于沒見過的內(nèi)容戳护,一律command+點(diǎn)擊金抡,進(jìn)入內(nèi)部文檔查看一下他的意思:
@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
修飾梗肝,那你暫時(shí)可以理解為使用了@State修飾的屬性,我們就可以控制的讀寫铺董。
然后我們看到使用這個(gè)屬性的時(shí)候是這樣子的$account
巫击,這個(gè)在之前的Swift
也是沒有出現(xiàn)過的。其實(shí)這個(gè)就是配套@State
使用的精续,如果對(duì)方需要的參數(shù)是Binding<T>
坝锰,那么你就使用這個(gè)就好了。
@State
和$value
是一種縮寫的方式重付,他們本來長這個(gè)樣子
@State private var a: Int = 0
priavte var a = State(initialValue: 0)
$a
a.binding
關(guān)于更多的這方面信息顷级,請(qǐng)查看
接下來就是body
部分了,這部分全是新內(nèi)容H返妗9薄!森爽!
下面挨個(gè)解釋一下啥意思
-
VStack
垂直方向的Stack恨豁,上面的代碼又是一種簡寫形式,他的功能就是在垂直方向爬迟,可以讓你放入至多10個(gè)子View橘蜜,未簡寫方式如下
VStack(alignment: .leading, spacing: 10) { Text("xxxx") }
默認(rèn)的
alignment
是.center
默認(rèn)的
spacing
是nil
-
HStack
和
VStack
類似,只不過一個(gè)是垂直方向,一個(gè)是水平方向 -
ZStack
ps: 這個(gè)雖然沒有用到计福,但是順帶一起提了
上面的
VStack
和HStack
都是沿著一個(gè)方向進(jìn)行布局跌捆,如果我們想要進(jìn)行疊加布局怎么辦?象颖?佩厚?ZStack
就是干著活的。上面的三個(gè)Stack
除了布局方式不一樣说订,其他的都一樣抄瓦。 -
Image
這個(gè)用來顯示一張圖片,內(nèi)部不多陶冷,具體可以自行點(diǎn)擊進(jìn)去查看钙姊,需要說明的是,系統(tǒng)為我們提供了一堆內(nèi)置的圖片埂伦,使用
Image(systemName: "xxx")
進(jìn)行調(diào)用煞额,如果不知道名字怎么辦!U疵铡2不佟!福利地址 下載完成之后就可以查看了
-
TextField
文本輸入框基跑,沒啥好講的婚温,但是要吐槽一下,現(xiàn)在的TextField并不好用IАg哉佟!逆日!嵌巷,能用的功能不多,要想做更多的事情室抽,還是需要使用
UITextField
搪哪,這個(gè)也是后續(xù)會(huì)聊到的內(nèi)容,如何橋接UITextField
到SwiftUI
-
Divider
分割線
-
Spacer
空白填充坪圾,如果不使用這個(gè)晓折,那么我們的
UI
會(huì)是居中對(duì)齊的,如果我們想要填充對(duì)齊到某一個(gè)方向兽泄,就可以使用他
然后就是用到View的幾個(gè)屬性的
-
padding
邊距漓概,如果你沒有指定方向,默認(rèn)就是四周病梢,指定了一個(gè)之后胃珍,其他的就會(huì)失效梁肿,意思就是你指定了
.top
,如果此時(shí)你不指定左右下三個(gè)方向,那么他們是一點(diǎn)間距都沒有的
OK到這里觅彰,我們就把上面的View的部分全部講完了吩蔑,你先運(yùn)行也會(huì)看到這樣子的UI
接下來我們?cè)诨ㄒ稽c(diǎn)時(shí)間,把他完善一下
- 密碼的可見/隱藏
- 登錄按鈕的實(shí)現(xiàn)
密碼的可見和隱藏
在Swift中我們使用的是一個(gè)屬性就可以控制了填抬,很抱歉烛芬,在SwiftUI中并沒有這樣子的屬性可以給到我們,所以他提供了另外一個(gè)輸入框飒责,專門給我們使用
-
SecureField
這個(gè)
View
輸入的內(nèi)容是不可見的(也就是一堆小圓點(diǎn))
一般來說赘娄,密碼是否可見,我們會(huì)有一個(gè)按鈕去顯示控制
所以我們需要加入一個(gè)新的View
读拆, Button
SwiftUI
為我們提供了好幾種Button
擅憔,目前我們只需要使用一種就好了鸵闪,有興趣的可以去官網(wǎng)自行查看檐晕。
在第二個(gè)HStack
中我們新增一個(gè)Button
,并新增一個(gè)屬性,用來控制是否可以顯示按鈕
var showPwd = false
...HStack
Button(action: {
self.showPwd.toggle()
}) {
Image(systemName: self.showPwd ?
"eye" : "eye.slash")
}
然后就給你報(bào)錯(cuò)了蚌讼,這是因?yàn)槟銢]給showPwd
這個(gè)屬性添加 @State
辟灰,加上之后就沒事了。
現(xiàn)在按鈕是可以點(diǎn)擊了篡石,圖片也在切換了芥喇,但是密碼還是公開的,接下來我們就把這部分實(shí)現(xiàn)
把TextField的代碼修改為如下代碼
Image(systemName: "lock")
if showPwd {
TextField("請(qǐng)輸入密碼", text: $password, onCommit: {
})
} else {
SecureField("請(qǐng)輸入密碼", text: $password, onCommit: {
})
}
再次運(yùn)行之后凰萨,就可以愉快的切換了
登錄按鈕的實(shí)現(xiàn)
在Devider
和Spacer
之間插入一個(gè)Button
继控,同時(shí)添加一個(gè)屬性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)
這里我們使用了幾個(gè)View的屬性
-
frame
設(shè)置大小和對(duì)齊方式
-
background
背景,這里使用的是協(xié)議進(jìn)行的約束胖眷,也就是你只要遵從了該協(xié)議就行武通,
Color
就遵循了 -
cornerRadius
圓角
-
disabled
是否是非激活狀態(tài)
4、編寫手機(jī)號(hào)登錄界面
再開始之前珊搀,指出我們上面的登錄界面的一些體驗(yàn)不友好的地方
- 鍵盤無法自動(dòng)消失
- 沒有限制TextField的最大輸入長度
接下來的代碼中冶忱,我們就要優(yōu)化這個(gè)問題
橋接UITextField
到SwiftUI
新建一個(gè)文件PQTextField
繼承協(xié)議UIViewRepresentable
,這個(gè)協(xié)議就是用來橋接的境析,其他的暫時(shí)不管囚枪。
你只要記得三個(gè)重要的方法
-
makeUIView
創(chuàng)建橋接的UIKit
-
updateUIView
更新他
-
makeCoordinator
UIKit代理的實(shí)現(xiàn)者
然后我們參考上面的TextView
,我們要做一個(gè)體驗(yàn)和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?
/// 配置時(shí)使用
var onConfig: PQTextFieldClosure?
func makeUIView(context: Context) -> UITextField {
}
func updateUIView(_ tf: UITextField, context: Context) {
}
func makeCoordinator() -> Coordinator {
}
}
然后我們依次把空白的地方補(bǔ)全
首先是makeUIView
劳淆,這里需要我們返回一個(gè)UIKit
的視圖
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
return textField
}
然后分析我們要實(shí)現(xiàn)的功能链沼,監(jiān)聽UITextField輸入情況,這里要設(shè)置他的代理沛鸵;設(shè)置的他的初始值括勺,比如placeholder
,
創(chuàng)建代理類
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
半毛錢關(guān)系都沒有朝刊,具體做的事情就是監(jiān)聽代理耀里,然后通過closure
回調(diào)出去
實(shí)現(xiàn)makeCoordinator
方法
func makeCoordinator() -> Coordinator {
Coordinator(self, onEditing: onEditing, onCommit: onCommit)
}
然后在makeUIView
中補(bǔ)全代碼
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
}
實(shí)現(xiàn)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?
/// 配置時(shí)使用
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)
}
}
}
有了上面的基礎(chǔ),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: "請(qǐng)輸入號(hào)碼", maxLength: 11,text: phoneNumber, onEditing: { tf in
}, onCommit: { tf in
})
.frame(height: 40)
}
if !isPhoneNum {
Text("手機(jī)號(hào)碼應(yīng)該是11位數(shù)字")
.font(.caption)
.foregroundColor(.red)
}
Divider()
}
VStack {
HStack {
PQTextField(placeholder: "請(qǐng)輸入驗(yàn)證碼", maxLength: 4, text: code, onEditing: { tf in
}, onCommit: { tf in
})
.frame(height: 40)
Button(action: {
// get code
}, label: {
Text((countDown == 60) ? "獲取驗(yàn)證碼" : "請(qǐng)\(countDown)s之后重試")
}).disabled(countDown != 60 || phoneNumber.count != 11)
}
if !isCode {
Text("請(qǐng)輸入正確的驗(yàn)證碼(4位數(shù)字)")
.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() {
}
}
首先我們創(chuàng)建了幾個(gè)屬性
- phoneNumber 保存手機(jī)使用
- code 驗(yàn)證碼
- phoneNumIsEdit 是否開始輸入手機(jī)號(hào)了
- codeIsEdit 是否開始輸入驗(yàn)證碼了
- timer 倒計(jì)時(shí)的時(shí)候使用
- countDown 倒計(jì)時(shí)的時(shí)間
- isPhoneNum 判斷是不是手機(jī)號(hào)拾氓,這里只做了非常簡單的判斷
- isCode 判斷是不是驗(yàn)證碼冯挎,這里也是非常簡單的判斷
- isCanLogin 是否可以登錄了(控制按鈕是否可以點(diǎn)擊)
接下來的視圖部分和之前大體相同,這部分的代碼帶過
最后我們看到我們又使用了兩個(gè)新的方法
-
onAppear
這個(gè)會(huì)在視圖加載的時(shí)候調(diào)用
-
onDisappear
這個(gè)會(huì)在視圖消失的時(shí)候調(diào)用
那么在這里做啥子呢咙鞍?房官,沒錯(cuò),就是用來場(chǎng)景定時(shí)器的
我們?nèi)?shí)現(xiàn)兩個(gè)定時(shí)器方法
創(chuàng)建定時(shí)器
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
})
// 先不觸發(fā)定時(shí)器
timer?.fireDate = .distantFuture
}
}
創(chuàng)建定時(shí)器续滋,這里一定要注意的是翰守,一定要做好判斷,不能重復(fù)創(chuàng)建定時(shí)器疲酌,否則會(huì)有多少個(gè)定時(shí)器同時(shí)在跑蜡峰,尤其是當(dāng)前界面進(jìn)入下級(jí)頁面的時(shí)候
銷毀定時(shí)器
private func invalidate() {
timer?.invalidate()
}
為什么創(chuàng)建的時(shí)候做了判斷,但是銷毀的時(shí)候卻沒有處理呢朗恳?湿颅??
如果你足夠細(xì)心粥诫,那你一定看到了countDown
是用@State
修飾的
最后我們補(bǔ)全在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: "請(qǐng)輸入號(hào)碼", 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("手機(jī)號(hào)碼應(yīng)該是11位數(shù)字")
.font(.caption)
.foregroundColor(.red)
}
Divider()
}
VStack {
HStack {
PQTextField(placeholder: "請(qǐng)輸入驗(yàn)證碼", 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) ? "獲取驗(yàn)證碼" : "請(qǐng)\(countDown)s之后重試")
}).disabled(countDown != 60 || phoneNumber.count != 11)
}
if !isCode {
Text("請(qǐng)輸入正確的驗(yàn)證碼(4位數(shù)字)")
.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
})
// 先不觸發(fā)定時(shí)器
timer?.fireDate = .distantFuture
}
}
private func invalidate() {
timer?.invalidate()
}
}
最終我們的兩個(gè)小Demo
就完成了。
第二個(gè)Demo
基于第一個(gè)怀浆,如果你第二個(gè)沒懂谊囚,你看你需要再去看看第一個(gè)Demo
實(shí)現(xiàn)點(diǎn)擊空白處隱藏鍵盤
新建文件DismissKeyboard.swift
首先分析一下功能,點(diǎn)擊空白處执赡,空白處的View
是Spacer
镰踏,Spacer
又遵循View
協(xié)議,那我們可以為View
擴(kuò)展一個(gè)隱藏鍵盤的方法
import SwiftUI
extension View {
func endEditing() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}
}
這里不建議使用keywindow
的方法去做了
然后為了方便其他的View
使用搀玖,自定義了一個(gè)struct
遵從ViewModifier
協(xié)議
struct DismissKeyboard: ViewModifier {
func body(content: Content) -> some View {
content.onTapGesture {
content.endEditing()
}
}
}
如何使用呢余境??灌诅?
Text("xxxx")
.modifier(DismissKeyboard())
其實(shí)ViewModifier
的妙用有很多芳来,這里只是舉了一個(gè)例子,比如我們要為某一個(gè)視圖設(shè)置獨(dú)特的樣式猜拾,我們就可以新建一個(gè)文件
即舌,然后編寫樣式,之后只要需要用到這個(gè)樣式的挎袜,就可以用類似上面的調(diào)用方法顽聂。
題外話: 那除了使用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()
}
}
}
他們兩個(gè)的區(qū)別,我個(gè)人認(rèn)為一個(gè)像繼承紊搪,一個(gè)像協(xié)議蜜葱。扯遠(yuǎn)了~~~
最后我們新建一個(gè)自己的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
替換成為我們自己創(chuàng)建的DismissKeyboardSpacer
,再去運(yùn)行一下看下效果
到這里我們的入門教程之登陸界面就完了RG6凇!
回顧一下我們學(xué)到了哪些東西V臀啊=伊邸!
首先視圖方面
HStack梆奈、VStack野崇、ZStack、List亩钟、Button乓梨、Text、TextFiled径荔、Divider督禽、Spacer、NavigationView总处、NavigationLink
然后方法方面
frame、padding睛蛛、rotationEffect鹦马、font、foregroundColor忆肾、background荸频、disabled、cornerRadius客冈、onAppear旭从、onDisappear
還了解了定時(shí)器的創(chuàng)建,UIKit的橋接场仲、@ViewBuilder和悦、ViewModifier、@State渠缕、Binding
希望對(duì)你有所收獲