美文网首页SwiftUI
SwiftUI:四种方式自定义TextField

SwiftUI:四种方式自定义TextField

作者: 猪猪行天下 | 来源:发表于2021-07-23 09:15 被阅读0次

    TextFieldStyle

    在考虑自定义之前,我们应该考虑SwiftUI提供什么。TextField有自己的风格,给我们提供了一些选项:

    • DefaultTextFieldStyle
    • PlainTextFieldStyle
    • RoundedBorderTextFieldStyle
    defaultstyles.png
    VStack {
      Section(header: Text("DefaultTextFieldStyle").font(.headline)) {
        TextField("Placeholder", text: .constant(""))
        TextField("Placeholder", text: $text)
      }
      .textFieldStyle(DefaultTextFieldStyle())
    
      Section(header: Text("PlainTextFieldStyle").font(.headline)) {
        TextField("Placeholder", text: .constant(""))
        TextField("Placeholder", text: $text)
      }
      .textFieldStyle(PlainTextFieldStyle())
    
      Section(header: Text("RoundedBorderTextFieldStyle").font(.headline)) {
        TextField("Placeholder", text: .constant(""))
        TextField("Placeholder", text: $text)
      }
      .textFieldStyle(RoundedBorderTextFieldStyle())
    }
    

    DefaultTextFieldStyleTextField的默认样式,在iOS中,这匹配了PlainTextFieldStyle

    PlainTextFieldStyleRoundedBorderTextFieldStyle区别似乎只是一个圆角和边框,然而一个RoundedBorderTextFieldStyleTextField还带有一个白色/黑色背景(取决于环境外观),而TextField PlainTextFieldStyle是透明的:

    defaultstylesBackground.png
    VStack {
      Section(header: Text("DefaultTextFieldStyle").font(.headline)) {
        TextField("Placeholder", text: .constant(""))
        TextField("Placeholder", text: $text)
      }
      .textFieldStyle(DefaultTextFieldStyle())
    
      Section(header: Text("PlainTextFieldStyle").font(.headline)) {
        TextField("Placeholder", text: .constant(""))
        TextField("Placeholder", text: $text)
      }
      .textFieldStyle(PlainTextFieldStyle())
    
      Section(header: Text("RoundedBorderTextFieldStyle").font(.headline)) {
        TextField("Placeholder", text: .constant(""))
        TextField("Placeholder", text: $text)
      }
      .textFieldStyle(RoundedBorderTextFieldStyle())
    }
    .background(Color.yellow)
    

    这是系统的方式,下面让我们说说自定义的方式

    方式1: swiftUI方式

    没有公共的API来创建新的TextField的样式的,推荐的方式就是对TextField进行一次包装:

    public struct FSTextField: View {
      var titleKey: LocalizedStringKey
      @Binding var text: String
    
      /// Whether the user is focused on this `TextField`.
      @State private var isEditing: Bool = false
    
      public init(_ titleKey: LocalizedStringKey, text: Binding<String>) {
        self.titleKey = titleKey
        self._text = text
      }
    
      public var body: some View {
        TextField(titleKey, text: $text, onEditingChanged: { isEditing = $0 })
          // Make sure no other style is mistakenly applied.
          .textFieldStyle(PlainTextFieldStyle())
          // Text alignment.
          .multilineTextAlignment(.leading)
          // Cursor color.
          .accentColor(.pink)
          // Text color.
          .foregroundColor(.blue)
          // Text/placeholder font.
          .font(.title.weight(.semibold))
          // TextField spacing.
          .padding(.vertical, 12)
          .padding(.horizontal, 16)
          // TextField border.
          .background(border)
      }
    
      var border: some View {
        RoundedRectangle(cornerRadius: 16)
          .strokeBorder(
            LinearGradient(
              gradient: .init(
                colors: [
                  Color(red: 163 / 255.0, green: 243 / 255.0, blue: 7 / 255.0),
                  Color(red: 226 / 255.0, green: 247 / 255.0, blue: 5 / 255.0)
                ]
              ),
              startPoint: .topLeading,
              endPoint: .bottomTrailing
            ),
            lineWidth: isEditing ? 4 : 2
          )
      }
    }
    
    customSwiftUI.gif

    这是可以真正的自定义一个TextField。没有办法改变占位符文本的颜色,或者设置不同文本的字体的大小:我们可以通过使用外部文本甚至在跟踪TextField状态时应用掩码来绕过一些限制,但是我们会很快遇到其他的困境,例如键盘操作相关的一些内容。

    在ios15之后,你可以通过使用FocusState属性包装器消除SwiftUI上的键盘。

    @FocusState private var textFieldFocused: Bool
    
    VStack {
        if showName {
            Text("Your name is \(name)")
        }
        TextField("Name", text: $name)
            .submitLabel(.next)
            .focused($textFieldFocused)
    
        Button("Submit") {
            showName = true
            textFieldFocused = false
        }
    }.padding()
    
    

    方式2: 桥接UIKit方式

    TextField不能满足我们的需求时,我们可以回到UIKit的UITextField.这需要创建一个UIViewRepresentable:

    struct UIKitTextField: UIViewRepresentable {
      var titleKey: String
      @Binding var text: String
    
      public init(_ titleKey: String, text: Binding<String>) {
        self.titleKey = titleKey
        self._text = text
      }
    
      func makeUIView(context: Context) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.delegate = context.coordinator
        textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textField.placeholder = NSLocalizedString(titleKey, comment: "")
    
        return textField
      }
    
      func updateUIView(_ uiView: UITextField, context: Context) {
        if text != uiView.text {
            uiView.text = text
        }
      }
    
      func makeCoordinator() -> Coordinator {
        Coordinator(self)
      }
    
      final class Coordinator: NSObject, UITextFieldDelegate {
        var parent: UIKitTextField
    
        init(_ textField: UIKitTextField) {
          self.parent = textField
        }
    
        func textFieldDidChangeSelection(_ textField: UITextField) {
          guard textField.markedTextRange == nil, parent.text != textField.text else {
            return
          }
          parent.text = textField.text ?? ""
        }
    
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
          textField.resignFirstResponder()
          return true
        }
      }
    }
    

    与SwiftUI的TextField使用比较:

    struct ContentView: View {
      @State var text = ""
    
      var body: some View {
        VStack {
          TextField("Type something... (SwiftUI)", text: $text)
          UIKitTextField("Type something... (UIKit)", text: $text)
        }
      }
    }
    
    uikitswiftui.gif

    一旦我们有了这个基本TextField文本框,我们可以继续获取所有需要的UIKit功能,例如,改变占位符的文本颜色现在需要在UIKitTextFieldmakeUIView(context:)方法中添加以下代码:

    textField.attributedPlaceholder = NSAttributedString(
      string: NSLocalizedString(titleKey, comment: ""),
      attributes: [.foregroundColor: UIColor.red]
    )
    
    red.png

    有了UIKit,我们可以做更多的事情,而不仅仅是简单的定制。例如,我们可以将日期/选择器和键盘类型与我们的TextField文本字段关联起来,这两种类型在SwiftUI中都不支持。更重要的是,我们可以使任何文本字段成为第一响应者。

    对于一个高级的TextField UIViewRepresentable示例,我建议查看SwiftUIX's CocoaTextField

    方式3: Introspect

    尽管SwiftUI APIs 与UIKit非常不同,但通常UIKit仍然在幕后使用。在iOS 14中,TextField的底层仍然是使用的UITextField:记住这一点,我们可以遍历TextField的UIKit层次结构,并寻找相关的UITextField

    SwiftUI库Introspect所要做的就是,允许我们接触到与SwiftUI视图对应的UIKit视图,从而让我们解锁UIKit的性能和管理,而无需创建我们自己的UIViewRepresentable:

    import Introspect
    
    struct ContentView: View {
      @State var text = ""
    
      var body: some View {
        TextField("Type something...", text: $text)
          .introspectTextField { textField in
            // this method will be called with our view's UITextField (if found)
            ...
          }
      }
    }
    

    例如,SwiftUI没有办法将工具栏与给定的文本字段关联起来,我们可以使用Introspect来修补它:

    struct ContentView: View {
      @State var text = ""
    
      var body: some View {
        TextField("Type something...", text: $text)
          .introspectTextField(customize: addToolbar)
      }
    
      func addToolbar(to textField: UITextField) {
        let toolBar = UIToolbar(
          frame: CGRect(
            origin: .zero,
            size: CGSize(width: textField.frame.size.width, height: 44)
          )
        )
        let flexButton = UIBarButtonItem(
          barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
          target: nil,
          action: nil
        )
        let doneButton = UIBarButtonItem(
          title: "Done",
          style: .done,
          target: self,
          action: #selector(textField.didTapDoneButton(_:))
        )
        toolBar.setItems([flexButton, doneButton], animated: true)
        textField.inputAccessoryView = toolBar
      }
    }
    
    extension  UITextField {
      @objc func didTapDoneButton(_ button: UIBarButtonItem) -> Void {
        resignFirstResponder()
      }
    }
    

    超过20行添加一个Done按钮!

    toolbar.gif

    虽然这种方法现在很有效,但不能保证在未来的iOS版本中也能有效,因为我们依赖于SwiftUI的私有实现细节。
    使用Introspect是安全的:当SwiftUI的TextField将不再使用UITextField时,我们的自定义方法(addToolbar(to)在上面的例子)将不会被调用。

    方式4: TextFieldStyle

    在文章的开头提到了SwiftUI不允许我们创建自己的TextFieldStyle
    在Xcode 12.5中,这是完整的TextFieldStyle声明:

    /// A specification for the appearance and interaction of a text field.
    @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
    public protocol TextFieldStyle {
    
    }
    

    然而,它实际上可以通过一个“hidden”_body方法来创建我们自己的样式,因此我们可以这样考虑实际的TextFieldStyle声明如下:

    public protocol TextFieldStyle {
      associatedtype _Body: View
      @ViewBuilder func _body(configuration: TextField<Self._Label>) -> Self._Body
      typealias _Label = _TextFieldStyleLabel
    }
    

    这让创建我们自己的样式成为可能:

    struct FSTextFieldStyle: TextFieldStyle {
      func _body(configuration: TextField<_Label>) -> some View {
         //
      }
    }
    

    下面是我们如何用一个新的FSTextFieldStyle来替换之前的FSTextField声明:

    struct ContentView: View {
      @State var text = ""
    
      /// Whether the user is focused on this `TextField`.
      @State private var isEditing: Bool = false
    
      var body: some View {
        TextField("Type something...", text: $text, onEditingChanged: { isEditing = $0 })
          .textFieldStyle(FSTextFieldStyle(isEditing: isEditing))
      }
    }
    
    struct FSTextFieldStyle: TextFieldStyle {
      /// Whether the user is focused on this `TextField`.
      var isEditing: Bool
    
      func _body(configuration: TextField<_Label>) -> some View {
        configuration
          .textFieldStyle(PlainTextFieldStyle())
          .multilineTextAlignment(.leading)
          .accentColor(.pink)
          .foregroundColor(.blue)
          .font(.title.weight(.semibold))
          .padding(.vertical, 12)
          .padding(.horizontal, 16)
          .background(border)
      }
    
      var border: some View {
        RoundedRectangle(cornerRadius: 16)
          .strokeBorder(
            LinearGradient(
              gradient: .init(
                colors: [
                  Color(red: 163 / 255.0, green: 243 / 255.0, blue: 7 / 255.0),
                  Color(red: 226 / 255.0, green: 247 / 255.0, blue: 5 / 255.0)
                ]
              ),
              startPoint: .topLeading,
              endPoint: .bottomTrailing
            ),
            lineWidth: isEditing ? 4 : 2
          )
      }
    }
    
    customSwiftUI.gif

    不幸的是,这种方法使用了私有API,使用起来不安全:希望我们很快就能得到一个正式的API。

    相关文章

      网友评论

        本文标题:SwiftUI:四种方式自定义TextField

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