美文网首页SwiftUI
SwiftUI 轻松入门之登录界面

SwiftUI 轻松入门之登录界面

作者: Codepgq | 来源:发表于2020-03-28 20:38 被阅读0次

    前言

    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要强大了,但是要看你为苹果充值了多少

    开始干活

    看完本期内容你将会了解

    • 如何跳转页面
    • 如何处理输入事件
    • @ViewBuilder
    • 如何桥接UIKit
    • 熟悉几个常用的View

    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部分了,这部分全是新内容!!!!

    下面挨个解释一下啥意思

    • VStack

      垂直方向的Stack,上面的代码又是一种简写形式,他的功能就是在垂直方向,可以让你放入至多10个子View,未简写方式如下

              VStack(alignment: .leading, spacing: 10) {
                  Text("xxxx")
              }
      

      默认的alignment.center

      默认的spacingnil

    • HStack

      VStack类似,只不过一个是垂直方向,一个是水平方向

    • ZStack

      ps: 这个虽然没有用到,但是顺带一起提了

      上面的VStackHStack都是沿着一个方向进行布局,如果我们想要进行叠加布局怎么办???ZStack就是干着活的。上面的三个Stack除了布局方式不一样,其他的都一样。

    • Image

      这个用来显示一张图片,内部不多,具体可以自行点击进去查看,需要说明的是,系统为我们提供了一堆内置的图片,使用Image(systemName: "xxx")进行调用,如果不知道名字怎么办!!!!

      福利地址 下载完成之后就可以查看了

    • TextField

      文本输入框,没啥好讲的,但是要吐槽一下,现在的TextField并不好用!!!!,能用的功能不多,要想做更多的事情,还是需要使用UITextField,这个也是后续会聊到的内容,如何桥接UITextFieldSwiftUI

    • Divider

      分割线

    • Spacer

      空白填充,如果不使用这个,那么我们的UI会是居中对齐的,如果我们想要填充对齐到某一个方向,就可以使用他

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

    • padding

      边距,如果你没有指定方向,默认就是四周,指定了一个之后,其他的就会失效,意思就是你指定了.top,如果此时你不指定左右下三个方向,那么他们是一点间距都没有的

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

    image-20200328100855116.png

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

    • 密码的可见/隐藏
    • 登录按钮的实现

    密码的可见和隐藏

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

    • SecureField

      这个View输入的内容是不可见的(也就是一堆小圆点)

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

    所以我们需要加入一个新的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的属性

    • frame

      设置大小和对齐方式

    • background

      背景,这里使用的是协议进行的约束,也就是你只要遵从了该协议就行,Color就遵循了

    • cornerRadius

      圆角

    • disabled

      是否是非激活状态

    效果图 loginAccount.gif

    4、编写手机号登录界面

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

    • 键盘无法自动消失
    • 没有限制TextField的最大输入长度

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

    桥接UITextFieldSwiftUI

    新建一个文件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修饰的

    最后我们补全在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

    希望对你有所收获

    相关文章

      网友评论

        本文标题:SwiftUI 轻松入门之登录界面

        本文链接:https://www.haomeiwen.com/subject/qgnsuhtx.html