最前面想說的話
轉(zhuǎn)眼已經(jīng)是2020年10月了,2020已經(jīng)過去大半了,上半年的計劃已經(jīng)完成傅是,所以下半年的學(xué)習(xí)應(yīng)該提上日程了,利用兩周時間每周一三學(xué)習(xí)周六整理歸納學(xué)習(xí)的東西發(fā)表一篇文章蕾羊,二四鍛煉喧笔,周五周日休息
SwiftUI 學(xué)習(xí)的想法
其實本來不是不打算先學(xué)習(xí)SwiftUI的,但是因為現(xiàn)在iOS14推出了小組件功能必須要用SwiftUI實現(xiàn)龟再,所以提前學(xué)習(xí)一下是很有必要的书闸。預(yù)計發(fā)表三篇關(guān)于SwiftUI的日志,分別整理SwiftUI的基礎(chǔ)布局和SwiftUI里的響應(yīng)式布局利凑,最后結(jié)合一個實際項目輸出自己的心得
這篇文章參考的書籍是《SwiftUI和Combine編程》這本書第一二章浆劲,僅為自己學(xué)習(xí)歸納使用。
你好哀澈,SwiftUI
創(chuàng)建第一個項目
“選擇 iOS tab 下的 Single View App 模板牌借,Xcode 將為我們創(chuàng)建一個單頁的 iOS app「畎矗”“接下來膨报,將項目命名為 Calculator,并且選擇 “Use SwiftUI” 作為用戶界面的構(gòu)建方式适荣,讓 Xcode 使用 SwiftUI 來創(chuàng)建第一個畫面丙躏。“點擊 Next 并選擇合適的位置后束凑,我們可以得到一個使用 SwiftUI 作為界面開發(fā)方式的新項目晒旅。在項目導(dǎo)航中找到 ContentView.swift,它的內(nèi)容如下:
import SwiftUI
struct ContentView : View {
var body: some View {
Text("Hello World")
}
}
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
上面代碼中 ContentView 所定義的是實際的 UI汪诉,ContentView_Previews 則是一個滿足了 PreviewProvider 的 dummy 界面废恋。如果你的操作系統(tǒng)是 macOS 10.15 或以上的話,你可以使用編輯器右上角的切換編輯器視圖按鈕扒寄,選中 “Editor and Canvas” (快捷鍵 Option + Command + 回車)鱼鼓,并單擊右上角的 Resume 按鈕。Xcode 預(yù)覽界面將會讀取 ContentView_Previews 的內(nèi)容该编,并渲染在編輯器右側(cè)的實時預(yù)覽欄中迄本。它就是 ContentView_Previews 的 previews 屬性所返回的 View,也就是 ContentView 的內(nèi)容课竣。默認情況下嘉赎,Xcode 會使用你當前選取的 iOS 模擬器作為預(yù)覽尺寸置媳,為了能讓你得到的內(nèi)容和書中一致,建議你在這個例子中選擇 iPhone XR 模擬器作為目標設(shè)備公条。
使用 Modifier 描述 Text 及 Button
除了改變 Text 的文本內(nèi)容外拇囊,我們會通過在 Text 聲明之后使用方法調(diào)用的方式,來定義文本樣式靶橱。讓我們從單個的計算器按鈕入手寥袭,比如加號鍵。將 ContentView 的 body 部分替換為以下內(nèi)容:
var body: some View {
Text("+")
.font(.title) /// 1
.foregroundColor(.white) /// 2
.padding() /// 3
.background(Color.orange) /// 4
}
font关霸,foregroundColor传黄,padding 和 background 各自定義了 Text 的一項屬性:
font, foregroundColor, background 不用多說,padding將把當前的 View 包裹在一個新的 View 里队寇,并在四周填充空白部分尝江。在上面例子中,我們沒有給定參數(shù)英上,這會在文本的四周都填上系統(tǒng)默認尺寸的空白炭序。我們可以為 padding 指定需要填充的方向以及大小,比如:.padding(.top, 16) 將在上方填充 16 point 的空白苍日,.padding(.horizontal, 8) 在水平方向 (也即 [.leading, .trailing]) 填充 8 point
上面的四種方法被成為View的Modifier,modifier 一般來說對順序不敏感惭聂,對布局也不關(guān)心,它們更像是針對對象 View 本身的屬性的修改相恃。而與之相反辜纲,封裝類的 modifier 的順序十分重要。大致來說拦耐,view modifier 分為兩種類別:
像是 font耕腾,foregroundColor 這樣定義在具體類型 (比如例中的 Text) 上,然后返回同樣類型 (Text) 的原地 modifier杀糯。
像是 padding扫俺,background 這樣定義在 View extension 中,將原來的 View 進行包裝并返回新的 View 的封裝類 modifier固翰。
基本布局
Stack 容器
橫向排列的布局空間叫HStack狼纬,豎向排列的布局控件叫VStack, 疊加排列的布局控件叫ZStack,除了ZStack骂际,H/V Stack 類似UIStackView的橫向和縱向
struct CalculatorButton : View {
let fontSize: CGFloat = 38
let title: String
let size: CGSize
let backgroundColorName: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: fontSize))
.foregroundColor(.white)
.frame(width: size.width, height: size.height)
.background(Color(backgroundColorName))
.cornerRadius(size.width / 2)
}
}
}
這樣一來疗琉,ContentView 的 body 里可以簡化為:
var body: some View {
HStack {
CalculatorButton(
title: "1",
size: CGSize(width: 88, height: 88),
backgroundColorName: "digitBackground")
{
print("Button: 1")
}
CalculatorButton(
title: "2",
size: CGSize(width: 88, height: 88),
backgroundColorName: "digitBackground")
{
print("Button: 2")
}
CalculatorButton(
title: "3",
size: CGSize(width: 88, height: 88),
backgroundColorName: "digitBackground")
{
print("Button: 3")
}
CalculatorButton(
title: "+",
size: CGSize(width: 88, height: 88),
backgroundColorName: "operatorBackground")
{
print("Button: +")
}
}
}
ForEach
在項目中新建一個 Swift 文件,將它命名為 CalculatorButtonItem.swift歉铝,添加如下代碼:
enum CalculatorButtonItem {
enum Op: String {
case plus = "+"
case minus = "-"
case divide = "÷"
case multiply = "×"
case equal = "="
}
enum Command: String {
case clear = "AC"
case flip = "+/-"
case percent = "%"
}
case digit(Int)
case dot
case op(Op)
case command(Command)
}
我們可以粗略地把計算器上的按鈕分為四大類:代表從 0 至 9 的數(shù)字 digit盈简,小數(shù)點 dot,加減乘除等號這樣的操作 (Op),以及清空柠贤、符號翻轉(zhuǎn)等這類命令 (Command)香浩。接下來,可以在 extension 里追加定義必要的外觀:
extension CalculatorButtonItem {
var title: String {
switch self {
case .digit(let value): return String(value)
case .dot: return "."
case .op(let op): return op.rawValue
case .command(let command): return command.rawValue
}
}
var size: CGSize {
CGSize(width: 88, height: 88)
}
var backgroundColorName: String {
switch self {
case .digit, .dot: return "digitBackground"
case .op: return "operatorBackground"
case .command: return "commandBackground"
}
}
}
///@propertyWrapper
struct RowView: View {
let row: [CalculatorButtonItem]
@Binding var brain: JiSuanQiState
var body: some View {
HStack {
ForEach(row, id: \.self) { item in
BtnView(
content: item.title,
backgroundcolorName: item.backgroundColorName,
size: item.size,
action: {
debugPrint("輸出按鈕:\(item.title)")
self.brain = self.brain.apply(item: item)
})
}
}
}
}
struct JiSuanqiButtonPad: View {
let pad: [[CalculatorButtonItem]] = [
[.command(.clear), .command(.flip),
.command(.percent), .op(.divide)],
[.digit(7), .digit(8), .digit(9), .op(.multiply)],
[.digit(4), .digit(5), .digit(6), .op(.minus)],
[.digit(1), .digit(2), .digit(3), .op(.plus)],
[.digit(0), .dot, .op(.equal)]
]
@Binding var brain: JiSuanQiState
var body: some View {
VStack(spacing: 8) {
ForEach(pad①, id: \.self②) { row in
RowView(row: row, brain: self.$brain)③
}
}
}
}
我們需要對此進行一些解釋种吸。ForEach 是 SwiftUI 中一個用來列舉元素弃衍,并生成對應(yīng) View collection 的類型呀非。它接受一個數(shù)組坚俗,且數(shù)組中的元素需要滿足 Identifiable 協(xié)議。如果數(shù)組元素不滿足 Identifiable岸裙,我們可以使用 ForEach(_:id:) 來通過某個支持 Hashable 的 key path 獲取一個等效的元素是 Identifiable 的數(shù)組猖败。在我們的例子中,數(shù)組 row 中的元素類型 CalculatorButtonItem 是不遵守 Identifiable 的降允。為了解決這個問題恩闻,我們可以為 CalculatorButtonItem 加上 Hashable,這樣就可以直接用 ForEach(row, id: .self) 的方式轉(zhuǎn)換為可以接受的類型了剧董。在 CalculatorButtonItem.swift 文件最后幢尚,加上一行:
extension CalculatorButtonItem: Hashable {}
使用 frame 或 Spacer 占滿屏幕
frame: 中的 minWidth 和 maxWidth 為 Text 的寬度定義了范圍,這會“提示” Text 不必遵守內(nèi)容尺寸翅楼,而是去適應(yīng)容器的尺寸尉剩。將 .infinity 傳遞給 maxWidth,表示不對最大寬度進行限制毅臊,這種情況下 Text 會盡可能占據(jù)它的容器的寬度理茎,變?yōu)槿翆挕rame 還提供了一個 alignment 參數(shù)管嬉,讓我們可以設(shè)定其對齊
Spacer: SwiftUI 允許我們定義可伸縮的空白:Spacer皂林,它會嘗試將可占據(jù)的空間全部填滿。在我們的 body 中蚯撩,可以加入一個 Spacer 來把 VStack 的上半部分全部填滿
VStack(spacing: 12) {
Spacer()
Text("0")
.font(.system(size: 76))
.minimumScaleFactor(0.5)
.padding(.trailing, 24)
.lineLimit(1)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing)
CalculatorButtonPad()
.padding(.bottom)
}
至此一個完整的計算器頁面就算完成了