SwiftUI 簡明教程之文字與圖片

Bruce2077發表於2021-04-16

本文為 Eul 樣章,如果您喜歡,請移步 AppStore/Eul 檢視更多內容。

Eul 是一款 SwiftUI & Combine 教程類 App(iOS、macOS),以文章(文字、圖片、程式碼)配合真機示例(Xcode 12+、iOS 14+,macOS 11+)的形式呈現給讀者。筆者意在儘可能使用簡潔明瞭的語言闡述 SwiftUI & Combine 相關的知識,使讀者能快速掌握並在 iOS 開發中實踐。

Text

本地化字串

SwiftUI 中涉及到字串的地方,基本都支援普通的字串和本地化字串。Text 的初始化方法也不例外:

/// 普通字串
init<S>(_ content: S) where S : StringProtocol

/// 本地化字串
init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

我們先建立多語言檔案,分別寫入中英文的 Stay Hungry, Stay Foolish! 文字,通過列舉去獲取對應的 LocalizedStringKey,然後就可以使用 Text(LocalizeKey.Hungry) 方便地展示本地化字串了。

enum LocalizeKey {
  static let kHungry: LocalizedStringKey = "Hungry"
}

struct LocalizableView: View {
  var body: some View {
    Text(LocalizeKey.kHungry)
  }
}

// "Hungry" = "Stay Hungry, Stay Foolish!";
// "Hungry" = "求知若飢,虛心若愚!";

富文字

Text 實現了操作符過載,我們可以直接用 + 來拼接不同樣式的文字。

struct RichTextView: View {
  private let text: Text =
    Text("Stay ").foregroundColor(.blue).font(.title).italic() +
    Text("Hungry, ").font(.headline) +
    Text("Stay ").foregroundColor(.red).font(.title) +
    Text("Foolish!").font(.headline).underline()
  
  var body: some View {
    text
  }
}

另外,Text 本身遵循 Equatable 協議,我們還可以直接使用 ==!= 來對兩個 Text 進行判等。

日期

Text 甚至可以直接展示日期,現在建立一個倒數計時控制元件只需要一行程式碼就可以實現!

Text 的初始化方法有如下幾種:

/** 以下日期均指當地日期 */

/// 使用指定樣式展示日期
public init(_ date: Date, style: Text.DateStyle)

/// 展示日期範圍
public init(_ dates: ClosedRange<Date>)

/// 展示日期間隔
public init(_ interval: DateInterval)

DateStyle 有如下列舉值:

public struct DateStyle {
    /// 時間,比如:11:23PM
    public static let time: Text.DateStyle
  
    /// 日期,比如:June 3, 2019
    public static let date: Text.DateStyle
  
    /// 相對現在的時間,比如:2 hours, 23 minutes
    public static let relative: Text.DateStyle
 	
    /// 與現在的時間差,比如:-3 months,+2 hours
    public static let offset: Text.DateStyle
  
    /// 倒數計時,比如:36:59:01
    public static let timer: Text.DateStyle
}

下面我們通過程式碼展示其用法:

struct DateView: View {
  private var future: Date { now.addingTimeInterval(3600) }
  private var now: Date { Date() }
  
  var body: some View {
    VStack(alignment: .leading, spacing: 10) {
      row(style: ".date") { Text(now, style: .date) }
      row(style: ".offset") { Text(future, style: .offset) }
      row(style: ".relative") { Text(future, style: .relative) }
      row(style: ".time") { Text(future, style: .time) }
      row(style: ".timer") { Text(future, style: .timer) }
      row(style: "Range") { Text(now...future) }
      row(style: "Interval") { Text(DateInterval(start: now, end: future)) }
    }
  }
  
  func row<Content: View>(style: String, @ViewBuilder content: () -> Content) -> some View {
    VStack {
      HStack {
        content()
        Spacer()
        Text(style).foregroundColor(.secondary)
      }
      
      Divider ()
    }
  }
}

先簡述一下 @ViewBuilder 的作用:它可以用來修飾閉包引數,並從中構建檢視。

.offset.relative.timer 展示的時間都是根據秒數變化的,其它樣式的日期則是靜態的。

Label

構建方法

Label 是一個相當強大的控制元件,可以快速生成圖片和文字的組合,預設佈局是左圖右文,也支援自定義配置。

它有如下初始化方法:

init<S>(S, image: String)

init<S>(S, systemImage: String)

init(LocalizedStringKey, image: String)

init(LocalizedStringKey, systemImage: String)

// Title: View, icon: View
init(title: () -> Title, icon: () -> Icon)

我們試著用以上方法構建不同的檢視,程式碼和介面如下:

Label("Swift", systemImage: "swift")
  .foregroundColor(.orange)

Label(
  title: {
    Text("Apple")
  },icon: {
    Image(systemName: "applelogo")
  }
)
.foregroundColor(.blue)

Label(
  title: {
    Image(systemName: "gift.fill")
      .renderingMode(.original)
  },icon: {
    Text("Gift")
  }
)
.foregroundColor(.red)
.labelStyle(TitleOnlyLabelStyle())

LabelStyle 有如下三種樣式:

  • DefaultLabelStyle // Title + Icon
  • IconOnlyLabelStyle // 只顯示 Icon
  • TitleOnlyLabelStyle // 只顯示 Title

自定義樣式

上面的構建方法中,其實還有一種是未曾提及的:

init(LabelStyleConfiguration)

LabelStyleConfiguration 是一個結構體型別,包含 IconTitle

我們可以通過這個初始化方法,給系統提供的樣式新增自定義的樣式。

比如我們需要給 Label 加上陰影,可以先建立一個遵循 LabelStyle 協議的 ShadowLabelStyle,然後使用該樣式。下面程式碼中的 Configuration 實際上就是 LabelStyleConfiguration ,只不過系統通過 typealias Configuration = LabelStyleConfiguration 改頭換面了而已。

Label("Apple", systemImage: "applelogo")
        .labelStyle(ShadowLabelStyle())

struct ShadowLabelStyle: LabelStyle {
  func makeBody(configuration: Configuration) -> some View {
    Label(configuration)
      .shadow(color: Color.black.opacity(0.5), radius: 5, x: 0, y: 5)
  }
}

上面的樣式有一定的侷限性,如果我們需要一個垂直佈局或是左右對齊的樣式呢?實現的原理是一樣的,程式碼如下:

struct VerticalLabelStyle: LabelStyle {
  func makeBody(configuration: Configuration) -> some View {
    VStack(alignment: .center, spacing: 10) {
      configuration.icon
      configuration.title
    }
  }
}

struct LeftRightLabelStyle: LabelStyle {
  func makeBody(configuration: Configuration) -> some View {
    HStack(alignment: .center, spacing: 10) {
      configuration.icon
      Spacer()
      configuration.title
    }
  }
}

TextField

TextField 有如下的三種構建方式:

  1. 普通的初始化方法
  2. 在方法 1 的基礎上新增了監聽功能,可以監聽編輯狀態、Return 鍵的按下動作
  3. 在方法 2 的基礎上新增了格式轉換功能

前兩種方法比較簡單,這裡說一下方法 3 的細節。如下樣例是將輸入的文字轉換成數字,當我們正在輸入的時候,格式轉換功能是不生效的,只有當編輯結束的時候,才會去執行轉換,如果轉換成功,會更新繫結的值(s3),如果轉換失敗,不會更新 s3。

@State private var s1 = ""
@State private var s2 = ""
@State private var s3 = 0
@State private var pwd = ""

GroupBox(label: Text(s1)) {
  /// 1
  TextField("TextField", text: $s1)
}

GroupBox(label: Text(s2)) {
  /// 2
  TextField("Observe TextField", text: $s2) { (isEditing) in
    print(isEditing)
  } onCommit: {
    print("Return")
  }
  .textFieldStyle(RoundedBorderTextFieldStyle())
}

GroupBox(label: Text(String(s3))) {
  /// 3
  TextField("Formatter TextField", value: $s3, formatter: NumberFormatter()) {
    (isEditing) in
    print(isEditing)
  } onCommit: {
    print("Return")
  }
  .textFieldStyle(RoundedBorderTextFieldStyle())
  .keyboardType(.numbersAndPunctuation)
}

GroupBox(label: Text("密碼輸入: \(pwd)")) {
  /// 密碼輸入
  SecureField("Password", text: $pwd)
}

TextEditor

TextEditor 的使用比較簡單,如下程式碼我們就可以輕鬆建立一個文字輸入框:

@State private var text = "Stay Hungry, Stay Foolish!"

TextEditor(text: $text)
  .frame(height: 150)
  .lineSpacing(10.0)                  // 行距
  .multilineTextAlignment(.center)    // 對齊方式
  .overlay(
    RoundedRectangle(cornerRadius: 5)
      .stroke(Color.blue, lineWidth: 1)
  )

系統並沒有提供 placeholder 這樣的特性,不過我們可以輕鬆實現這個功能。

// 新增 placeholder
ZStack(alignment: .topLeading) {
  TextEditor(text: $text2)
    .frame(height: 150)
    .border(Color.blue)
  
  if text2.isEmpty {
    Text("Type something")
      .foregroundColor(Color(UIColor.placeholderText))
      .padding(8)
  }
}

Image

Image 用來展示圖片,它可以載入資源包中的圖片檔案和系統內建的圖示(SF Symbols)。

以下是幾點提示:

  • 圖片預設具有伸展特性,.resizable() 可使圖片不超出螢幕或指定區域
  • 如果要展示 SF Symbols 內建圖示自帶的顏色,可以用 .renderingMode(.original). 來渲染。
  • Text 可以直接插入圖片,支援字串插值和拼接兩種方式

本文為 Eul 樣章,如果您喜歡,請移步 AppStore/Eul 檢視更多內容。

相關文章