SwiftUI 輕松入門之登錄界面

前言

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酗钞,美滋滋~~

本文Demo地址

必看

本文默認(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艏尽)

新建工程

image-20200327205509861.png

新建之后我們可以看到如下文件

目錄

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中可以查看到

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ù)覽脆荷??禀综?

previews

然后點(diǎn)擊resume(在右上角)简烘,等待一會(huì)兒就可以了,至于預(yù)覽顯示的速度(看你電腦設(shè)備定枷,我反正是放棄了)孤澎。

image-20200327212631982.png

友情提示(按下command然后點(diǎn)擊文字,有驚喜哦)

屬性

這個(gè)就比隔壁的Flutter要強(qiáng)大了欠窒,但是要看你為蘋果充值了多少

開始干活

看完本期內(nèi)容你將會(huì)了解

  • 如何跳轉(zhuǎn)頁面
  • 如何處理輸入事件
  • @ViewBuilder
  • 如何橋接UIKit
  • 熟悉幾個(gè)常用的View

1覆旭、新建兩個(gè)文件

LoginAccountViewLoginPhoneView,新建的時(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:需要返回Viewclosure

最后我們?cè)诮o這個(gè)導(dǎo)航欄設(shè)置一個(gè)標(biāo)題

.navigationBarTitle(
    Text("登錄Demo"), 
    displayMode: .large
)

SwiftUI中晴圾,默認(rèn)的displayModelarge效果颂砸,具體啥樣子噪奄,參考設(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)
        }
    }
}
loginDemo.gif

然后運(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)的spacingnil

  • HStack

    VStack類似,只不過一個(gè)是垂直方向,一個(gè)是水平方向

  • ZStack

    ps: 這個(gè)雖然沒有用到计福,但是順帶一起提了

    上面的VStackHStack都是沿著一個(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)容,如何橋接UITextFieldSwiftUI

  • 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

image-20200328100855116.png

接下來我們?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)

DeviderSpacer之間插入一個(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)

效果圖
loginAccount.gif

4、編寫手機(jī)號(hào)登錄界面

再開始之前珊搀,指出我們上面的登錄界面的一些體驗(yàn)不友好的地方

  • 鍵盤無法自動(dòng)消失
  • 沒有限制TextField的最大輸入長度

接下來的代碼中冶忱,我們就要優(yōu)化這個(gè)問題

橋接UITextFieldSwiftUI

新建一個(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ǔ)全在PQTextFieldClosure的代碼之后油航,完整的代碼如下

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

loginPhone.gif
實(shí)現(xiàn)點(diǎn)擊空白處隱藏鍵盤

新建文件DismissKeyboard.swift

首先分析一下功能,點(diǎn)擊空白處执赡,空白處的ViewSpacer镰踏,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)行一下看下效果

loginPhone.gif

到這里我們的入門教程之登陸界面就完了RG6凇!

回顧一下我們學(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ì)你有所收獲

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸽素,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子亦鳞,更是在濱河造成了極大的恐慌馍忽,老刑警劉巖棒坏,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異遭笋,居然都是意外死亡坝冕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門瓦呼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來徽诲,“玉大人,你說我怎么就攤上這事吵血』烟妫” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蹋辅,是天一觀的道長钱贯。 經(jīng)常有香客問我,道長侦另,這世上最難降的妖魔是什么秩命? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮褒傅,結(jié)果婚禮上弃锐,老公的妹妹穿的比我還像新娘。我一直安慰自己殿托,他們只是感情好霹菊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著支竹,像睡著了一般旋廷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上礼搁,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天饶碘,我揣著相機(jī)與錄音,去河邊找鬼馒吴。 笑死扎运,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的饮戳。 我是一名探鬼主播豪治,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼莹捡!你這毒婦竟也來了鬼吵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤篮赢,失蹤者是張志新(化名)和其女友劉穎齿椅,沒想到半個(gè)月后琉挖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涣脚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年示辈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遣蚀。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡矾麻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出芭梯,到底是詐尸還是另有隱情险耀,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布玖喘,位于F島的核電站甩牺,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏累奈。R本人自食惡果不足惜贬派,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望澎媒。 院中可真熱鬧搞乏,春花似錦、人聲如沸戒努。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柏卤。三九已至冬三,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缘缚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國打工敌蚜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留桥滨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓弛车,卻偏偏與公主長得像齐媒,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纷跛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容