美文网首页SwiftUI
SwiftUI - 常用控件

SwiftUI - 常用控件

作者: 西西的一天 | 来源:发表于2019-12-07 23:02 被阅读0次

    这一节里,我们一起来通过完成一个表单,了解一下SwiftUI中的一些常用控件。其中,涉及的知识点:

    1. TextField
    2. UI控件的一些重构小技巧
    3. 通过依赖注入来实现keyboard事件监听
    4. Button
    5. Toggle
    6. 如何设置一个背景图片

    功能很简单,效果图如下所示,这里的UI布局细节不是重点,有需要时,可通过Modifier在进行调整。源码:Github

    效果图

    TextField

    使用方式很简单,通过最基础的构造函数便可把它放在视图中

    @State var name: String = ""
    ...
    TextField("Name", text: $name)
    ...
    

    其中第一个参数就是UIKit中的的hint,第二个参数就是和当前视图绑定的一个state变量,当用户输入参数时,该state变量也会跟着改变。系统还提供了其它的一些构造函数,我们进入TextField的定义来看一看:

    extension TextField where Label == Text {
        public init(_ titleKey: LocalizedStringKey, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {})
    
        public init<S>(_ title: S, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) where S : StringProtocol
    
        public init<T>(_ titleKey: LocalizedStringKey, value: Binding<T>, formatter: Formatter, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {})
    
        public init<S, T>(_ title: S, value: Binding<T>, formatter: Formatter, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) where S : StringProtocol
    }
    

    其实就是一些函数重载,第一个参数有两种:其一是直接hardcode,另一种是用来支持多语言的。其余的参数我们分别来看一看:

    1. onEditingChanged:需要的是一个回调函数,该回调函数的入参是一个Bool值。当该TextField获得或失去焦点时,会触发函数的调用,而这个Bool值在获得焦点时为true,失去焦点时为false。
    2. onCommit:是在用户点击键盘的Done时被触发。
    3. formatter:需要传入的是一个Formatter对象,用来格式化显示的内容,比如货币,数字这种。
      可以看到这几个入参都是用来替换UIKit中UITextFieldDelegate

    那么,除了这些通过构造函数可以完成的,其余的一些属性都是通过Modifier来实现的,比如键盘的类型通过.keyboardType(UIKeyboardType.emailAddress)设置。style则通过.textFieldStyle(RoundedBorderTextFieldStyle())设置,SwiftUI内置了4中TextFiled的style,分别为no styleDefaultTextFieldStylePlainTextFieldStyleRoundedBorderTextFieldStyle,试一下发现,前三种是没有差别的。通常都不设置样式,而通过一些通用的Modifier来自定义样式,比如示例图中的样式,是通过如下代码进行设置的:

    TextField("Name", text: $userManager.profile.name)
      .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
      .background(Color.white)
      .cornerRadius(self.cornerRadius)
      .overlay(
        RoundedRectangle(cornerRadius: self.cornerRadius)
          .stroke(lineWidth: 2)
          .foregroundColor(.blue)
        )
      .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2)
    

    UI控件的一些重构小技巧

    看到这个,我们会想到,如果需要有另一个输入框,比如密码输入框,如何来复用这些样式呢?第一个想到的一定是抽取一个UI组件,比如叫MyInputField,再把需要的参数传递进去。

    struct MyInputField: View {
      TextField("Name", text: $userManager.profile.name)
        .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
        .background(Color.white)
        .cornerRadius(self.cornerRadius)
        .overlay(
          RoundedRectangle(cornerRadius: self.cornerRadius)
            .stroke(lineWidth: 2)
            .foregroundColor(.blue)
          )
        .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2)
      }
    }
    

    但发现,这样会有一些问题,因为SwiftUI中的密码输入框,和UIKit的中的有所不同,它是单独的一个组件叫做SecureField,而我们想要的只是样式。
    这里就引入了我们的第二中抽取方式,创建自定义的一个Modifier。

    struct BorderedViewModifier: ViewModifier {
        private let cornerRadius: CGFloat = 8
        func body(content: Content) -> some View {
            content
                .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
                .background(Color.white)
                .cornerRadius(self.cornerRadius)
                .overlay(
                    RoundedRectangle(cornerRadius: self.cornerRadius)
                        .stroke(lineWidth: 2)
                        .foregroundColor(.blue)
                )
                .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2)
        }
    }
    
    extension View {
        func bordered() -> some View {
            ModifiedContent(content: self, modifier: BorderedViewModifier())
        }
    }
    

    这里分了两步,第一步构造一个自定义的ViewModifier,实现它的一个方法,在此方法中,把想要复用的样式写进入,想要用这个样式只需要:
    ModifiedContent(content: someView, modifier: BorderedViewModifier())
    再进一步,可以写一个View的extension,定义如上的一个方法bordered()。这样要使用这个样式就更方便了

     TextField("Name", text: $userManager.profile.name).bordered()
    

    通过依赖注入来实现keyboard事件监听

    谈到了输入框,就必然会想到keyboard,iOS不像Android的那样,系统会自动将输入框上移,避免被挡住。iOS就需要自己动手了。这里推荐的一个方式是通过@ObservedObject进行依赖注入一个keyboard的处理类,实现方式和UIKit也是一样的,监听系统的一个Notification:

    import UIKit
    
    final class KeyboardFollower: ObservableObject {
        
        @Published var keyboardHeight: CGFloat = 0
        
        func subscribe() {
            NotificationCenter.default.addObserver(self, selector: #selector(keyboardVisibilityChanged), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
        }
        
        func unsubscribe() {
            NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
        }
        
        @objc func keyboardVisibilityChanged(_ notification: Notification) {
            guard let userInfo = notification.userInfo else { return }
            guard let keyboardBeginFrame = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { return }
            guard let keyboardEndFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
            let visible = keyboardBeginFrame.minY > keyboardEndFrame.minY
            keyboardHeight = visible ? keyboardEndFrame.height : 0
        }
    }
    

    使用起来需要在该View初始化时出入一个KeyboardFollower的实例

    struct RegisterView: View {
        @ObservedObject var keyboardHandler: KeyboardFollower
        ...
    }
    

    然后监听这个keyboardHandler中的keyboardHeight变更,注意这里有有两个方法.onAppear.onDisappear,它们和原先的ViewController的生命周期方法是相同的,在这个两个方法中进行监听的开启和关闭,在通过padding来改变TextField的位置。

    struct RegisterView: View {
        @ObservedObject var keyboardHandler: KeyboardFollower
        @EnvironmentObject var userManager: UserManager
    
        var body: some View {
            ZStack {
                ...
                VStack {
                    WelcomeMessageView()
                    TextField("Name", text: $userManager.profile.name)
                        .bordered()
                        .padding([.leading, .trailing])
                    ...
                }
                .padding(.bottom, keyboardHandler.keyboardHeight)
                .onAppear { self.keyboardHandler.subscribe() }
                .onDisappear { self.keyboardHandler.unsubscribe() }
            }
        }
    }
    

    Button

    button相对来说就简单很多,但有一点是它强大之处。在UIKit时,要自定义一个Button是比较麻烦的,尤其是在有图片,文字的布局时。SwiftUI对此做了优化,我们先来看一下它的构造函数:

    @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
    public struct Button<Label> : View where Label : View {
    
        /// Creates an instance for triggering `action`.
        ///
        /// - Parameters:
        ///     - action: The action to perform when `self` is triggered.
        ///     - label: A view that describes the effect of calling `action`.
        public init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
    
        /// Declares the content and behavior of this view.
        public var body: some View { get }
    
        /// The type of view representing the body of this view.
        ///
        /// When you create a custom view, Swift infers this type from your
        /// implementation of the required `body` property.
        public typealias Body = some View
    }
    

    它的构造函数的一个参数是一个block,没什么好说的,就是点击事件,第二个参数也是一个block,返回值是Label,而这个Label是View的子类,也就是说,可以根据需要,写任意一个View给它。这一点,真的是可圈可点。

    struct SubmitButton: View {
        let action: () -> Void;
        var body: some View {
            Button(action: self.action) {
                HStack {
                    Image(systemName: "checkmark")
                        .resizable()
                        .frame(width: 16, height: 16, alignment: .center)
                    Text("OK")
                        .font(.body)
                        .bold()
              }
            }
            .bordered()
        }
    }
    

    Toggle

    看过Button之后,Toggle就简单很多了,和button一样,它的第二个参数也是可以传入任意View的

    Toggle(isOn: $userManager.settings.rememberUser) {
      Text("Remember me")
      .font(.subheadline)
      .multilineTextAlignment(.center)
      .foregroundColor(.gray)
    }.padding(.trailing)
    

    如何设置一个背景图片

    这个应该算是一个题外话了,在设置背景图片时,真的是被坑到了。设置背景一个有两种方式,其一是使用ZStack,让图片位于最顶部;其二,是给某个Stack设置background的Modifier,比如HStack { ... }.background(some image or color)。它们有什么区别呢?
    使用background modifier时,背景图片的大小是由该Stack的大小决定的,而使用ZStack的方式,图片的大小就比较自由,但当图片的大小超过屏幕时,比如宽度超过屏幕宽度,在这个ZStack的其他UI组件的默认宽度就是那个图片的宽度。
    当需求是如上方图片那样时,就只能使用ZStack了,图片的宽度超过了屏幕宽度,那么就需要把多出去的部分砍掉,折腾了很久后,找到了一个Modifier:.frame(minWidth: 0, maxWidth: .infinity),这样就完成了需求。

    WelcomeBackgroundImage

    struct WelcomeBackgroundImage: View {
        var body: some View {
            Image("swift_world")
                .resizable()
                .scaledToFill()
                .frame(minWidth: 0, maxWidth: .infinity)
                .edgesIgnoringSafeArea(.all)
                .saturation(0.5)
                .blur(radius: 5)
                .opacity(0.08)
        }
    }
    

    RegisterView

    import SwiftUI
    
    struct RegisterView: View {
        @ObservedObject var keyboardHandler: KeyboardFollower
        var body: some View {
            ZStack {
                WelcomeBackgroundImage()
                VStack {
                    ...
                }
                .padding(.bottom, keyboardHandler.keyboardHeight)
                .onAppear { self.keyboardHandler.subscribe() }
                .onDisappear { self.keyboardHandler.unsubscribe() }
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:SwiftUI - 常用控件

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