SwiftUI View 的使用探討
SwiftUI 官方文件已經有相當完整的元件用法和清單,可以自行查閱,這裡先簡單列出一些常用的。其實跟 UIKit 一樣每個元件都有預設的實作和樣式,都是符合 Human Interface Guidelines,但是如果產品需求就是無法用原生元件達成,就需要自己建立客製化元件了。
User Interface Elements
最基礎使用者介面的單元,可以直接給予用戶訊息的元件,像是文字圖片等等。
- Text: 單行或多行的唯讀文字
1
Text("Hamlet").font(.title)
- Label: 帶有圖片的文字
1
Label("Lightning", systemImage: "bolt.fill")
- TextField: 一般輸入框
1
2
3
4TextField(
"User name (email address)",
text: $username
) - SecureField: 密碼輸入框
1
2
3
4
5
6SecureField(
"Password",
text: $password
) {
handleLogin(username: username, password: password)
} - TextEditor: 長文字編輯器
1
TextEditor(text: $fullText)
- Image: 圖片
1
2
3Image("avatar")
.resizable()
.aspectRatio(contentMode: .fit)
- Button: 按鈕
1
2
3Button(action: signIn) {
Text("Sign In")
} - Link: 超連結文字
1
Link("abc", destination: URL(string: "https://www.abc.com/")!)
- Menu: 選單
1
2
3
4
5
6
7Menu("Actions") {
Button("Copy", action: {})
Button("Delete", action: {})
Menu("More") {
Button("12345", action: {})
}
} - Slider: 滑動的控制元件
1
2
3
4
5
6
7Slider(
value: $speed,
in: 0...100,
onEditingChanged: { editing in
isEditing = editing
}
) - Stepper: 一加一減的控制元件
1
2
3
4
5
6
7Stepper {
Text("Value: \(value)")
} onIncrement: {
value += 1
} onDecrement: {
value -= 1
} - Toggle: 開關的的控制元件
1
2
3Toggle(isOn: $vibrateOnRing) {
Text("Vibrate on Ring")
}
- Shape: 基於 Path 繪畫出的元件,都是 Conform Shape,而 Shape Conform View
- 例如: Circle (圓形)、Rectangle (矩形)、RoundedRectangle (圓角矩形)
1
2
3
4
5
6
7
8
9
10
11
12
13Circle()
Rectangle()
.fill(.pink)
.frame(width: 100, height: 100, alignment: .center)
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
gradient: Gradient(colors: [.white, .yellow]),
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: 200, height: 200, alignment: .center)
- 例如: Circle (圓形)、Rectangle (矩形)、RoundedRectangle (圓角矩形)
- Canvas: 支援即時繪畫的功能
Layout Containers
以前在 UIKit 的時代 AutoLayout 的實踐方式有兩種,第一種是 StackView 自動帶有排版功能的元件、第二種是大家最熟悉的 Constraint。
演化到 SwiftUI 的時代,強化了第一種做法,且不再使用第二種做法,因此使用 Layout & Container 用來排列佈局所有元件,算是實作 UI 的關鍵,關係到不同元件之間的排列方式、間距、留白等等的問題。
選擇不同的容器來裝你的元件 - Picking Container Views for Your Content
- Stack 是堆疊元件用的容器,只是有方向上的不同,都是 Conform View。
- 例如: HStack (水平)、VStack (垂直)、ZStack (深度),下面有官方提供的範例,同時用到三種 Stack
- 注意: 一個 Stack 內部最多只能放 10 元件,不然會編譯錯誤!
- 注意: 需要使用 Spacer 或 Alignment (.leading .top …) 來實現靠左靠右、靠上靠下的效果!
1 | struct ProfileView: View { |
- Lazy 效果讓 View 在還沒出現在畫面上是不會建立該 View 的實體,例如: LazyHStack (水平)、LazyVStack (垂直)
- 當元件內容超過畫面的範圍,需要使用 ScrollView 把 Stack 包起來,來達到可拖拉的效果,同時也建議使用 LazyStack,可以節省資源
1
2
3
4
5
6
7ScrollView(.horizontal) {
LazyHStack {
ForEach(0...10, id: \.self) { i in
ProfileView()
}
}
}.frame(maxHeight: 300.0)
- Section 可來切分不同的區塊
- 如果想要固定 Header 改成
LazyVStack(spacing: 1, pinnedViews: [.sectionHeaders])
即可!1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55struct StackGroupView: View {
let sections = [
ColorData(color: .red, name: "Reds"),
ColorData(color: .green, name: "Greens"),
ColorData(color: .blue, name: "Blues")
]
var body: some View {
ScrollView {
LazyVStack(spacing: 1) {
ForEach(sections) { section in
Section(header: SectionHeaderView(colorData: section)) {
ForEach(section.variations) { variation in
section.color
.brightness(variation.brightness)
.frame(height: 20)
}
}
}
}
}
}
}
struct SectionHeaderView: View {
var colorData: ColorData
var body: some View {
HStack {
Text(colorData.name)
.font(.headline)
.foregroundColor(colorData.color)
Spacer()
}
.padding()
.background(Color.primary
.colorInvert()
.opacity(0.75))
}
}
struct ColorData: Identifiable {
let id = UUID()
let name: String
let color: Color
let variations: [ShadeData]
struct ShadeData: Identifiable {
let id = UUID()
var brightness: Double
}
init(color: Color, name: String) {
self.name = name
self.color = color
self.variations = stride(from: 0.0, to: 0.5, by: 0.1)
.map { ShadeData(brightness: $0) }
}
}
- 自動填滿成網格形式的排版
GridUnicodeView.swift 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32struct GridUnicodeView: View {
let rows: [GridItem] = Array(repeating: .init(.fixed(20)), count: 2)
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
var body: some View {
VStack(spacing: 10){
ScrollView(.horizontal) {
LazyHGrid(rows: rows, alignment: .top) {
ForEach((0...79), id: \.self) {
let codepoint = $0 + 0x1f600
let codepointString = String(format: "%02X", codepoint)
Text("\(codepointString)")
.font(.footnote)
let emoji = String(Character(UnicodeScalar(codepoint)!))
Text("\(emoji)")
.font(.largeTitle)
}
}
}.frame(height: 100)
ScrollView(.vertical){
LazyVGrid(columns: columns) {
ForEach((0...79), id: \.self) {
let codepoint = $0 + 0x1f600
let codepointString = String(format: "%02X", codepoint)
Text("\(codepointString)")
let emoji = String(Character(UnicodeScalar(codepoint)!))
Text("\(emoji)")
}
}.font(.largeTitle)
}
}
}
}GridProfileView.swift 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct GridProfileView: View {
var columes = [
GridItem(spacing: 0),
GridItem(spacing: 0),
GridItem(spacing: 0)
]
var body: some View {
ScrollView{
LazyVGrid(columns: columes, spacing: 0){
ForEach(0...10, id: \.self) { i in
ProfileView()
}
}
}
}
}
Collection Containers
專門處理集合資料的容器,
可以用更少的程式碼和設定完成清單的顯示,提供一些預設行為和UI(例如顯示更多、可選功能、排序等等),
如果想要客製化行為或是UI(例如分隔線),請使用上面提到的 LazyStack + ScrollView
。
- List: 清單
- Table: 可選可排序的表格
- Form: 對輸入或有控制功能的元件進行分類
- Group: 當作可將多個 View 拉成一群的功能(不會實際影響內部子 View 的排序方式和間距)
- GroupBox: 帶有文字標籤的分群
- ScrollView: 可滾動的元件
Presentation Containers
專門處理不同層級頁面的容器(ex.navigation、tab…)
- NavigationView: 就是 Navigation
1
2
3
4
5
6
7
8
9NavigationView {
List {
NavigationLink("Purple", destination: ColorDetail(color: .purple))
NavigationLink("Pink", destination: ColorDetail(color: .pink))
NavigationLink("Orange", destination: ColorDetail(color: .orange))
}
.navigationTitle("Colors")
Text("Select a Color") // A placeholder to show before selection.
} - OutlineGroup: 專門顯示大綱或樹狀資料夾層級的資料
- DisclosureGroup: 可用於顯示或隱藏Group
- TabView: 就是 Tab (僅支援 Text, Image, Text+Image),iOS15 有 badge 可用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18TabView {
Text("The First Tab")
.tabItem {
Image(systemName: "1.square.fill")
Text("First")
}
Text("Another Tab")
.tabItem {
Image(systemName: "2.square.fill")
Text("Second")
}
Text("The Last Tab")
.tabItem {
Image(systemName: "3.square.fill")
Text("Third")
}
}
.font(.headline) - Presentation Modifiers: 用來處理 alerts, popovers, sheets, dialogs
Debug
請善用 PreviewProvider
和 .border(Color.red)
就可以清楚地在預覽畫面上看到此 View 或 Container 的範圍。
Reuse View
如果用到相同樣式的元件(ex.按鈕、輸入框),重複設定很麻煩也可能設定缺漏,可以有下面兩種方式
- 解法: 可以建立客製化 View
- 宣告方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14struct CakeButton : View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: 16))
.foregroundColor(.white)
.frame(width: 120, height: 30)
.background(backgroundColor)
.cornerRadius(30/2)
}
}
} - 使用方式
1
2
3CakeButton(title: "123", backgroundColor: .gray){
print("CakeButton")
}
- 解法: 使用 ViewModifier 可以單純封裝設定樣式的部分,最後使用 .modifier 來執行
1
2
3public protocol ViewModifier {
func body(content: Self.Content) -> Self.Body
} - 宣告方式
1
2
3
4
5
6
7
8struct CakeButtonModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 12))
.foregroundColor(.white)
.frame(width: 40, height: 40)
}
} - 使用方式
1
2
3
4Button(action: { print("cake") }) {
Image(systemName: "cake")
.modifier(CakeButtonModifier())
}
Demo
希望這篇文章有幫助到您的開發之路!如果能給我一些按讚支持,我會非常感謝您的鼓勵!祝壞蟲遠離您!
評論