SwiftUI - 常用控件

這一節(jié)里,我們一起來通過完成一個表單,了解一下SwiftUI中的一些常用控件。其中,涉及的知識點:

  1. TextField
  2. UI控件的一些重構(gòu)小技巧
  3. 通過依賴注入來實現(xiàn)keyboard事件監(jiān)聽
  4. Button
  5. Toggle
  6. 如何設(shè)置一個背景圖片

功能很簡單,效果圖如下所示,這里的UI布局細(xì)節(jié)不是重點,有需要時,可通過Modifier在進(jìn)行調(diào)整。源碼:Github。

效果圖

TextField

使用方式很簡單,通過最基礎(chǔ)的構(gòu)造函數(shù)便可把它放在視圖中

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

其中第一個參數(shù)就是UIKit中的的hint,第二個參數(shù)就是和當(dāng)前視圖綁定的一個state變量,當(dāng)用戶輸入?yún)?shù)時,該state變量也會跟著改變。系統(tǒng)還提供了其它的一些構(gòu)造函數(shù),我們進(jìn)入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
}

其實就是一些函數(shù)重載,第一個參數(shù)有兩種:其一是直接hardcode,另一種是用來支持多語言的。其余的參數(shù)我們分別來看一看:

  1. onEditingChanged:需要的是一個回調(diào)函數(shù),該回調(diào)函數(shù)的入?yún)⑹且粋€Bool值。當(dāng)該TextField獲得或失去焦點時,會觸發(fā)函數(shù)的調(diào)用,而這個Bool值在獲得焦點時為true,失去焦點時為false。
  2. onCommit:是在用戶點擊鍵盤的Done時被觸發(fā)。
  3. formatter:需要傳入的是一個Formatter對象,用來格式化顯示的內(nèi)容,比如貨幣,數(shù)字這種。
    可以看到這幾個入?yún)⒍际怯脕硖鎿QUIKit中UITextFieldDelegate

那么,除了這些通過構(gòu)造函數(shù)可以完成的,其余的一些屬性都是通過Modifier來實現(xiàn)的,比如鍵盤的類型通過.keyboardType(UIKeyboardType.emailAddress)設(shè)置。style則通過.textFieldStyle(RoundedBorderTextFieldStyle())設(shè)置,SwiftUI內(nèi)置了4中TextFiled的style,分別為no styleDefaultTextFieldStyle,PlainTextFieldStyleRoundedBorderTextFieldStyle,試一下發(fā)現(xiàn),前三種是沒有差別的。通常都不設(shè)置樣式,而通過一些通用的Modifier來自定義樣式,比如示例圖中的樣式,是通過如下代碼進(jìn)行設(shè)置的:

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控件的一些重構(gòu)小技巧

看到這個,我們會想到,如果需要有另一個輸入框,比如密碼輸入框,如何來復(fù)用這些樣式呢?第一個想到的一定是抽取一個UI組件,比如叫MyInputField,再把需要的參數(shù)傳遞進(jìn)去。

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

但發(fā)現(xiàn),這樣會有一些問題,因為SwiftUI中的密碼輸入框,和UIKit的中的有所不同,它是單獨的一個組件叫做SecureField,而我們想要的只是樣式。
這里就引入了我們的第二中抽取方式,創(chuàng)建自定義的一個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())
    }
}

這里分了兩步,第一步構(gòu)造一個自定義的ViewModifier,實現(xiàn)它的一個方法,在此方法中,把想要復(fù)用的樣式寫進(jìn)入,想要用這個樣式只需要:
ModifiedContent(content: someView, modifier: BorderedViewModifier())
再進(jìn)一步,可以寫一個View的extension,定義如上的一個方法bordered()。這樣要使用這個樣式就更方便了

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

通過依賴注入來實現(xiàn)keyboard事件監(jiān)聽

談到了輸入框,就必然會想到keyboard,iOS不像Android的那樣,系統(tǒng)會自動將輸入框上移,避免被擋住。iOS就需要自己動手了。這里推薦的一個方式是通過@ObservedObject進(jìn)行依賴注入一個keyboard的處理類,實現(xiàn)方式和UIKit也是一樣的,監(jiān)聽系統(tǒng)的一個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
    ...
}

然后監(jiān)聽這個keyboardHandler中的keyboardHeight變更,注意這里有有兩個方法.onAppear,.onDisappear,它們和原先的ViewController的生命周期方法是相同的,在這個兩個方法中進(jìn)行監(jiān)聽的開啟和關(guān)閉,在通過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對此做了優(yōu)化,我們先來看一下它的構(gòu)造函數(shù):

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

它的構(gòu)造函數(shù)的一個參數(shù)是一個block,沒什么好說的,就是點擊事件,第二個參數(shù)也是一個block,返回值是Label,而這個Label是View的子類,也就是說,可以根據(jù)需要,寫任意一個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一樣,它的第二個參數(shù)也是可以傳入任意View的

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

如何設(shè)置一個背景圖片

這個應(yīng)該算是一個題外話了,在設(shè)置背景圖片時,真的是被坑到了。設(shè)置背景一個有兩種方式,其一是使用ZStack,讓圖片位于最頂部;其二,是給某個Stack設(shè)置background的Modifier,比如HStack { ... }.background(some image or color)。它們有什么區(qū)別呢?
使用background modifier時,背景圖片的大小是由該Stack的大小決定的,而使用ZStack的方式,圖片的大小就比較自由,但當(dāng)圖片的大小超過屏幕時,比如寬度超過屏幕寬度,在這個ZStack的其他UI組件的默認(rèn)寬度就是那個圖片的寬度。
當(dāng)需求是如上方圖片那樣時,就只能使用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() }
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 開始之前:請確保你的系統(tǒng)版本為macOS 10.15及以上版本且已經(jīng)安裝了Xcode 11。這種組合使您可以在Xc...
    Augs閱讀 3,124評論 0 7
  • 本文的主角[SwiftUI-CSS](https://github.com/hite/SwiftUI-CSS) 是...
    hite和落雁閱讀 2,999評論 0 2
  • 回答第二個問題: 1、每天堅持(雖然每次老師講課的時間都是上午8:00,那時正是我在上班路上的時間),我也要盡快在...
    金海錦閱讀 139評論 0 0
  • 尋著淡淡的花香, 發(fā)現(xiàn)不知不覺 又已經(jīng)到了梔子花掛滿枝頭的季節(jié)。。。。 小院的花爭相斗艷 無奈確深鎖閨苑 是錯過了...
    alice_99e9閱讀 296評論 0 3
  • 《覺》 江東圍城知魚美; 山南連郭覺筍香。
    自命飛皇Yoes閱讀 311評論 0 1

友情鏈接更多精彩內(nèi)容