SwiftUI

SwiftUI 轻松入门之登录界面

2020-03-28  本文已影响0人  Codepgq

前言

SwiftUI出来也有段时间了,关于SwiftUI更多的信息请看这里,那么苹果为什么要推出SwiftUI呢?很多小伙伴会有疑问,有的公司可能还在用着OC进行的开发,还有些小伙伴可能连Swift都不是很了解,这怎么就又出来一个SwiftUI

回想一下我们再使用OC或者Swift进行UI开发的时候,假设我们要显示一个Label到屏幕中,我们要进行哪些操作呢?下面代码用Swift举例:

...
void viewDidload() {
    super.viewDidload()
    
    let label = UILabel()
    label.text = "你好,Swift"
    view.addSubview(label)
}
...

emmmm,这一切看起来都没有问题,先声明label,然后为label设置文字,最后在把他添加到View中。但是时代在进步呐,看看隔壁的Flutter,人家要显示一行文本到屏幕上面是怎么操作的?

...
  @override
  Widget build(BuildContext context) {
      return Text('Welcome to Flutter');
    }
...

去掉申明部分,别人一行代码就搞定了,明显比你优秀啊,而且人家的阅读性丝毫不比你弱,你怎么办~

这个时候苹果就在想了:“这个小伙子轻轻松松就可以把代码运行在多平台上,那开发者不是就更愿意用这个编写么?不行,老子要反击!!!”

所以SwiftUI就出来了,然后就实现了声明式或者函数式的方式来进行界面开发,由于是自家平台,要做到一份代码,多端通用自然也要提上日程,毕竟人是越来越懒了,能点头就搞定的,绝不开口说话。

我们看看SwiftUI如何实现显示文本:

...
var body: some View {
    Text("你好,Swift")
}
...

现在看起来和Flutter旗鼓相当了不是吗?SwiftUI充分利用了Swift的特性,可以省略分号,在某些情况下可以省略return,美滋滋~~

本文Demo地址

必看

本文默认你有Swift基础,如果没有请自行了解,至少熟悉基本语法,不然有些省略写法你看你会很晕

如果你之前连官方的Demo都没有看过,又没有网页、Flutter、小程序等开发经验,那么你暂时可以记住一句话,什么都是View,你所看到的都是View组成。

Xcode版本:11.4

macOS系统版本:10.15.3(你可以不是10.15以上的,但是如果要运行macOS版本,系统要求必须要10.15以上,最新版的Xcode也要10.15.2以上,所以升级吧!!!)

新建工程

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)
    }

可以看到这里和我们之前的工程不一样了,之前那个Window的属性字段不见了,取而代之的是直接返回了UISceneConfiguration,在参数中我们可以看到有一个Default Configuration的字符串,这个字符串在我们的info.plist中可以查看到

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早就实现了,你作为后面出来小伙子,不能比前辈还少功能吧

如何开启预览???

previews

然后点击resume(在右上角),等待一会儿就可以了,至于预览显示的速度(看你电脑设备,我反正是放弃了)。

image-20200327212631982.png

友情提示(按下command然后点击文字,有惊喜哦)

属性

这个就比隔壁的Flutter要强大了,但是要看你为苹果充值了多少

开始干活

看完本期内容你将会了解

1、新建两个文件

LoginAccountViewLoginPhoneView,新建的时候,记得要选择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:需要返回Viewclosure

最后我们在给这个导航栏设置一个标题

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

SwiftUI中,默认的displayModelarge效果,具体啥样子,参考设置主页

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)
        }
    }
}
loginDemo.gif

然后运行起来,你就可以看到一个有两个列表项的视图,点击某一项的时候,可以进行调整到对应的View

3、开始编写账号密码登录页面

先把下面的代码替换原来的实现

    @State var account: String = ""
    @State var password: String = ""
    var body: some View {
        VStack {
            HStack {
                Image(systemName: "person")
                TextField("请输入账号", text: $account, onCommit: {
                    
                })
            }
            Divider()
            HStack {
                Image(systemName: "lock")
                TextField("请输入密码", text: $password, onCommit: {
                    
                })
            }
            Divider()
            Spacer()
        }
        .padding(.top, 100)
        .padding(.leading)
        .padding(.trailing)
    }

首先来了一个之前没见过的修饰符@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
a.binding

关于更多的这方面信息,请查看

接下来就是body部分了,这部分全是新内容!!!!

下面挨个解释一下啥意思

然后就是用到View的几个属性的

OK到这里,我们就把上面的View的部分全部讲完了,你先运行也会看到这样子的UI

image-20200328100855116.png

接下来我们在花一点时间,把他完善一下

密码的可见和隐藏

在Swift中我们使用的是一个属性就可以控制了,很抱歉,在SwiftUI中并没有这样子的属性可以给到我们,所以他提供了另外一个输入框,专门给我们使用

一般来说,密码是否可见,我们会有一个按钮去显示控制

所以我们需要加入一个新的ViewButton

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: {
        
    })
}

再次运行之后,就可以愉快的切换了

登录按钮的实现

DeviderSpacer之间插入一个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的属性

效果图 loginAccount.gif

4、编写手机号登录界面

再开始之前,指出我们上面的登录界面的一些体验不友好的地方

接下来的代码中,我们就要优化这个问题

桥接UITextFieldSwiftUI

新建一个文件PQTextField继承协议UIViewRepresentable,这个协议就是用来桥接的,其他的暂时不管。

你只要记得三个重要的方法

然后我们参考上面的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 是否可以登录了(控制按钮是否可以点击)

接下来的视图部分和之前大体相同,这部分的代码带过

最后我们看到我们又使用了两个新的方法

那么在这里做啥子呢?,没错,就是用来场景定时器的

我们去实现两个定时器方法

创建定时器

    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修饰的

最后我们补全在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: "请输入号码", 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

loginPhone.gif
实现点击空白处隐藏键盘

新建文件DismissKeyboard.swift

首先分析一下功能,点击空白处,空白处的ViewSpacerSpacer又遵循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,再去运行一下看下效果

loginPhone.gif

到这里我们的入门教程之登陆界面就完了!!!

回顾一下我们学到了哪些东西!!!

首先视图方面

HStack、VStack、ZStack、List、Button、Text、TextFiled、Divider、Spacer、NavigationView、NavigationLink

然后方法方面

frame、padding、rotationEffect、font、foregroundColor、background、disabled、cornerRadius、onAppear、onDisappear

还了解了定时器的创建,UIKit的桥接、@ViewBuilder、ViewModifier、@State、Binding

希望对你有所收获

上一篇下一篇

猜你喜欢

热点阅读