什么是 SwiftUI?[1]
官方的定義非常明確:
SwiftUI is a user interface toolkit that lets us design apps in a declarative way.
SwiftUI 就是?種描述式的構(gòu)建 UI 的?式。
簡介[2]
蘋果在 2019 WWDC 推出新一代聲明式布局框架-SwiftUI 缤灵,該框架可用于 watchOS跨跨、tvOS址愿、macOS、iOS 等个盆,蘋果的任意平臺都可以使用矾芙,達(dá)到跨平臺的實現(xiàn)舍沙。
在 SwiftUI 出現(xiàn)之前近上,蘋果不同的設(shè)備之間的開發(fā)框架并不互通剔宪,macOS 開發(fā)用的 AppKit,iOS 開發(fā)用的 UIKit壹无,WatchOS 開發(fā)用的堆疊葱绒,每個都不一樣,不能達(dá)到互通互用斗锭,可復(fù)用性差地淀。
之前
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
}
現(xiàn)在
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ModelData())
}
}
分成兩個部分:
- struct ContentView 定義的是視圖結(jié)構(gòu)。
- struct ContentView_Previews 是預(yù)覽視圖聲明岖是。
我們主要關(guān)注第一部分:struct ContentView
關(guān)鍵字 some 帮毁,其實就是一個opaque(不透明)類型实苞,在返回類型前面添加這個關(guān)鍵字,代表你和編譯器都確定這個函數(shù)總會返回一個特定的具體類型-只是你不知道是哪一種
SwiftUI 的編輯器是雙向交互的:
- 左邊代碼編輯器的改動會立即反應(yīng)到右邊的預(yù)覽視圖烈疚。
- 右邊的預(yù)覽視圖的編輯也會同步到左邊的代碼視圖黔牵。
優(yōu)點
- 使用 SwiftUI,系統(tǒng)會默認(rèn)支持白天和黑夜模式的自動切換
- 實時刷新預(yù)覽
- 各種尺寸的屏幕間自動適配
- 高效:更少的代碼爷肝,更快的交付
SwiftUI 1.0 基本沒有公司敢用在正式上線的APP 上猾浦,API 在 Beta 版本之間各種廢棄,UI 樣式經(jīng)常不兼容灯抛,大列表性能差
缺點
- iOS 14 才可放心的使用金赦,
- 要解決的是如何部署到低版本操作系統(tǒng)上?
SwiftUI的基本組件[3]
名稱 | 含義 |
---|---|
Text | 用來顯示文本的組件对嚼,類似UIKit中的UILabel |
Image | 用來展示圖片的組件夹抗,類似UIKit中的UIImageView |
Button | 用來展示圖片的組件,類似UIKit中的UIButton |
List | 用來展示列表的組件纵竖,類似UIKit中的UITableView |
ScrollView | 用來支持滑動的組件兔朦,類似UIKit中的UIScrollView |
Spacer | 一個靈活的空間,用來填充空白的組件 |
Divider | 一條分割線磨确,用來劃分區(qū)域的組件 |
VStack | 將子視圖按“豎直方向”排列布局沽甥。(Vertical stack) |
HStack | 將子視圖按“水平方向”排列布局。(Horizontal stack) |
ZStack | 將子視圖按“兩軸方向均對齊”布局(居中乏奥,有重疊效果) |
基本組件:
- Text:用來顯示文本的組件
Text("Hello, we are QiShare!").foregroundColor(.blue).font(.system(size: 32.0))
- Image:用來展示圖片的組件
Image.init(systemName: "star.fill").foregroundColor(.yellow)
- Button:用于可點擊的按鈕組件
Button(action: { self.showingProfile.toggle() }) {
Image(systemName: "paperplane.fill")
.imageScale(.large)
.accessibility(label: Text("Right"))
.padding()
}
- List:用來展示列表的組件
List(0..<5){_ in
NavigationLink.init(destination: VStack(alignment:.center){
Image.init(systemName: "\(item+1).square.fill").foregroundColor(.green)
Text("詳情界面\(item + 1)").font(.system(size: 16))
}) {
//ListRow
}
布局組件:
VStack摆舟、HStack、ZStack
功能組件:
- NavigationView:負(fù)責(zé)App中導(dǎo)航功能的組件邓了,類似UIKit中的UINavigationView
- NavigationLink:負(fù)責(zé)App頁面 跳轉(zhuǎn) 的組件恨诱,類似于UINavigationView中的 push與pop 功能
NavigationView {
List(0..<5){_ in
NavigationLink.init(destination: VStack(alignment:.center){
Image.init(systemName: "\(item+1).square.fill").foregroundColor(.green)
Text("詳情界面\(item + 1)").font(.system(size: 16))
}) {
//ListRow
}
}
.navigationBarTitle("導(dǎo)航\(item)",displayMode: .inline)
- TabView:負(fù)責(zé)App中的標(biāo)簽頁功能的組件,類似UIKit中的UITabBarController
TabView {
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)
UI布局的基本法則
在SwiftUI中骗炉,不能給子視圖強制規(guī)定一個尺寸
父view為子view提供一個建議的size
子view根據(jù)自身的特性照宝,返回一個size
父view根據(jù)子view返回的size為其進(jìn)行布局
struct ContentView: View {
var body: some View {
Text("Hello, world")
.border(Color.green)
}
}
- ContentView是Text的父view,為Text提供一個建議的size(全屏尺寸)
- 然后Text根據(jù)自身的特性句葵,返回了它實際需要的size
(注意:Text的特性是盡可能的只使用必要的空間厕鹃,也就是說能夠剛好展示完整文本的空間) - 然后ContentView根據(jù)Text返回的size,在其內(nèi)部對Text進(jìn)行布局乍丈,在SwiftUI中剂碴,容器默認(rèn)的布局方式為居中對齊。
Frame[4]
frame 在UIKit中是一種絕對布局轻专,它的位置是相對于父view左上角的絕對坐標(biāo)忆矛。但SwiftUI中frame的概念卻完全不同
在SwiftUI中,frame是一個modifier(修飾符的意思)请垛,并不是真的修改了view催训。實際上會創(chuàng)建一個新的view
舉個例子
struct ContentView: View {
var body: some View {
Text("Hello, world")
.background(Color.green)
.frame(width: 200, height: 50)
}
}
在上邊的代碼中洽议,.background并不會直接去修改原來的Text,而是在Text圖層的下方新建了一個新的view
為什么會這樣呢漫拭?
根據(jù)布局的3法則考慮這個問題
在考慮布局的時候绞铃,是自下而上的!!!
- 我們先考慮ContentVIew,它的父view給他的建議尺寸為整個屏幕的大小
- ContentVIew去詢問它的child嫂侍,它的child為下邊的那個frame儿捧,返回了width200, height50挑宠, 因此frame告訴ContentView它需要的size為width200菲盾, height50,因此最終ContentView的size為width200各淀, height50
- background是個一個透明的view懒鉴,它的父控件frame,給的建議尺寸是width200碎浇, height50临谱。它又去詢問其child,text返回的是只需要容納文本的size奴璃,因此text的size并不會是width: 200, height: 50
所以要想達(dá)到理想效果悉默,需要修改一下上邊的代碼,調(diào)整frame和background的順序就能實現(xiàn)
struct ContentView: View {
var body: some View {
Text("Hello, world")
.frame(width: 200, height: 50)
.background(Color.green)
}
}
數(shù)據(jù)處理的基本原則
- Data Access as a Dependency 數(shù)據(jù)訪問依賴
SwiftUI中的界面是嚴(yán)格數(shù)據(jù)驅(qū)動的:運行時界面的修改苟穆,只能通過修改數(shù)據(jù)來間接完成抄课,而不是直接對界面進(jìn)行修改操作。不回再像 傳統(tǒng)命令式編程 MVC 模式下那樣雳旅,ViewController 承載各種 UIVew控件跟磨,開發(fā)者需要手動處理 UIView 和 數(shù)據(jù)之間的依賴關(guān)系。當(dāng)數(shù)據(jù)產(chǎn)生變化時攒盈,要不停的同步數(shù)據(jù)和視圖之間的狀態(tài)變化抵拘。
SwiftUI是一切皆 View,所以可以把 View 切分成各種細(xì)粒度的組件型豁,然后通過組合的方式拼裝成最終的界面僵蛛,這種視圖的拼裝方式大大提高了界面開發(fā)的靈活性和復(fù)用性,視圖組件化并任意組合的方式是 SwiftUI 官方非常鼓勵的做法
- A Single Source Of Truth 單一數(shù)據(jù)源
在 SwiftUI 中偷遗,不同視圖間如果要訪問同樣的數(shù)據(jù)墩瞳,不需要各自持有數(shù)據(jù)驼壶,直接共用一個數(shù)據(jù)源即可氏豌。這樣的好處是無需手動處理視圖和數(shù)據(jù)的同步,當(dāng)數(shù)據(jù)源發(fā)生變化時會自動更新與該數(shù)據(jù)有依賴關(guān)系的視圖
從上圖可以看出SwiftUI 的數(shù)據(jù)流轉(zhuǎn)過程:
- 用戶對界面進(jìn)行操作热凹,產(chǎn)生一個操作行為 action
- 該行為觸發(fā)數(shù)據(jù)狀態(tài)的改變
- 數(shù)據(jù)狀態(tài)的變化會觸發(fā)視圖重繪
- SwiftUI 內(nèi)部按需更新視圖,最終再次呈現(xiàn)給用戶,等待下次界面操作
數(shù)據(jù)流工具[5]
通過它們建立數(shù)據(jù)和視圖的依賴關(guān)系
- Property
- @State
- @Binding
- ObservableObject
- @EnvironmentObject
- Property:
開發(fā)中最常見的空盼,它就是一個簡單的屬性爹梁,沒什么特別。ChildView 需要 Parent View 給它傳一個字符串坦胶,并且 ChildView 不對這個字符串進(jìn)行修改,所以直接定義一個 Property,在使用的時候突诬,直接讓 Parent View 告訴它就好了。
struct ContentView : View {
var body: some View {
ChildView(text: "Demo")
}
}
struct ChildView: View {
let text: String
var body: some View {
Text(text)
}
}
- @State:
- 基于值類型的狀態(tài)管理芜繁,這些值通常是字符串旺隙、數(shù)字、布爾等常量值
- 只能在當(dāng)前 View 的 body 內(nèi)修改骏令,所以它的使用場景是只影響當(dāng)前 View 內(nèi)部的變化
- 當(dāng)被@State包裝的屬性改變蔬捷,SwiftUI 內(nèi)部會自動重新計算和繪制 View的body部分
- 被@State包裝的變量一定要用private修飾,并且這個變量只能在當(dāng)前view以及其子View的body中使用榔袋,不讓外部使用周拐。如果想讓外部使用,則應(yīng)該使用@ObservedObject和@EnvironmentObject
struct PlayerView : View {
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
}
}
}
- @Binding
傳統(tǒng)的命令式編程中最復(fù)雜的部分莫過于狀態(tài)管理凰兑,尤其是多數(shù)據(jù)同步妥粟。
一個數(shù)據(jù)存在于不同的 UI 中,某個數(shù)據(jù)改變就要同步到不同的UI 中吏够。當(dāng)這樣需要同步的數(shù)據(jù)變的很多罕容,再加上一些其他的異步的操作和邏輯處理,會使代碼變得臃腫稿饰、可讀性下降锦秒,并且伴隨著而來的就是各種 Bug,SwiftUI 的解決辦法就是使用 @Binding
使用@state包裝的屬性只在它所屬view的內(nèi)部使用喉镰,那么當(dāng)它的子視圖要訪問這個屬性的時候就要用到@binding了
@Binding主要有下面幾個作用
- 在不持有數(shù)據(jù)源的情況下旅择,任意讀取
- 從 @State 中獲取數(shù)據(jù),并保持同步
- 對包裝的值采用傳址而不是傳值
struct ContentView: View {
// 用@State修飾需要改變的變量
@State private var count: Int = 0
var body: some View {
VStack {
Text("\(count)").foregroundColor(.orange).font(.largeTitle).padding()
// $訪問傳遞給另外一個UI
CountButton(count: $count)
}
}
}
struct CountButton : View {
// 用@State修飾侣姆,綁定count的值
@Binding var count: Int
var body: some View {
Button(action: {
// 此處修改數(shù)據(jù)會同步到上面的UI
self.count = self.count + 1
}) { Text("改變Count")
}
}
}
- ObservableObject
它的原理和RxSwift發(fā)布者和訂閱者的模式類似
ObservableObject 是個協(xié)議生真,必須要類去實現(xiàn)該協(xié)議,適用于多個 UI 之間的同步數(shù)據(jù)
在應(yīng)用開發(fā)過程中捺宗,很多數(shù)據(jù)其實并不是在 View 內(nèi)部產(chǎn)生的柱蟀,這些數(shù)據(jù)可能是一些本地存儲的數(shù)據(jù),也可能是網(wǎng)絡(luò)請求的模型數(shù)據(jù)蚜厉,這些數(shù)據(jù)默認(rèn)是與 SwiftUI 沒有依賴關(guān)系的长已,要想建立依賴關(guān)系就要用 ObservableObject,與之配合的是還有@ObservedObject和@Published兩個修飾符
@Published 修飾的屬性一旦發(fā)生了變化,會自動觸發(fā) ObservableObject 的objectWillChange 的 send方法术瓮,刷新頁面康聂。這一步是系統(tǒng)幫我們默認(rèn)實現(xiàn)的
ObservedObject:被觀察的對象 ,告訴SwiftUI胞四,這個對象是可以被觀察的恬汁,里面含有被@Published包裝了的屬性
@ObservedObject包裝的對象,必須遵循ObservableObject協(xié)議辜伟。也就是說必須是class對象氓侧,不能是struct。
@ObservedObject允許外部進(jìn)行訪問和修改
class UserSettings: ObservableObject {
// 有可能會有多個視圖使用导狡,所以屬性未聲明為私有
@Published var score = 123
}
struct ContentView: View {
@ObservedObject var settings = UserSettings()
var body: some View {
VStack {
Text("人氣值: \(settings.score)").font(.title).padding()
Button(action: {
self.settings.score += 1
}) {
Text("增加人氣")
}
}
}
}
有這樣一個場景甘苍,A->B->C->D->E->F,A界面的數(shù)據(jù)要傳遞給F界面烘豌,假如使用@ObservedObject包裝载庭,需要一層一層傳遞。再有反向傳值的話就更復(fù)雜廊佩,且容易出錯囚聚。而使用@EnvironmentObject則不需要,直接在F界面标锄,通過SwiftUI環(huán)境直接取出來就行顽铸。
- @EnvironmentObject 包裝的屬性是全局的,整個app都可以訪問
- 主要是為了解決跨組件數(shù)據(jù)傳遞的問題料皇。
- 組件層級嵌套太深谓松,就會出現(xiàn)數(shù)據(jù)逐層傳遞的問題,@EnvironmentObject可以幫助組件快速訪問全局?jǐn)?shù)據(jù)践剂,避免不必要的組件數(shù)據(jù)傳遞問題鬼譬。
- 使用基本與@ObservedObject一樣,但@EnvironmentObject突出強調(diào)此數(shù)據(jù)將由某個外部實體提供逊脯,所以不需要在具體使用的地方初始化优质,而是由外部統(tǒng)一提供。
- 使用@EnvironmentObject军洼,SwiftUI 將立即在環(huán)境中搜索正確類型的對象巩螃。如果找不到這樣的對象,則應(yīng)用程序?qū)⒘⒓幢罎⒇罢砸?慎用
class UserSettings: ObservableObject {
@Published var score = 123
}
struct ContentView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
NavigationView{
VStack {
// 顯示score
Text("人氣值: \(settings.score)").font(.title).padding()
// 改變score
Button(action: {
self.settings.score += 1
}) {
Text("增加人氣")
}
// 跳轉(zhuǎn)下一個界面
NavigationLink(destination: DetailView()) {
Text("下一個界面")
}
}
}
}
}
struct DetailView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
VStack {
Text("人氣值: \(settings.score)").font(.title).padding()
Button(action: {
self.settings.score += 1
}) {
Text("增加人氣")
}
}
}
}
// 需要注意此時需要修改SceneDelegate避乏,傳入environmentObject
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserSettings()))
- Property、 @State甘桑、 @Binding 一般修飾的都是 View 內(nèi)部的數(shù)據(jù)拍皮。
- @ObservedObject歹叮、 @EnvironmentObject 一般修飾的都是 View 外部的數(shù)據(jù):
- 網(wǎng)絡(luò)或本地存儲的數(shù)據(jù)
- 界面之間互相傳遞的數(shù)據(jù)
總結(jié):
- View與View間的公用數(shù)據(jù)使用@State + @Binding。
- 多個View與Class間的公用數(shù)據(jù):對View用@ObservedObject春缕,讓Class滿足ObservableObject協(xié)議盗胀。
- 父View與子View對Class間的公用數(shù)據(jù):父View用@ObservedObject艘蹋,子View用@EnvironmentObject锄贼,Class滿足ObservableObject協(xié)議
與UIKit彼此相容
由于SwiftUI 是一個新發(fā)布的框架,UI 組件并不齊全女阀,當(dāng) SwiftUI 中并沒有提供類似的功能時宅荤,可以把 UIKit 中已有的部分進(jìn)行封裝后,提供給 SwiftUI 使用浸策。不過需要遵循UIViewRepresentable協(xié)議冯键。UIViewRepresentable協(xié)議是SwiftUI框架中提供的用于將UIView轉(zhuǎn)換成SwiftUI中View的協(xié)議
當(dāng)然,也可以在已有的項目中庸汗,僅用 SwiftUI 制作一部分的 UI 界面惫确。
UIViewRepresentable協(xié)議
protocol UIViewRepresentable : View
associatedtype UIViewType : UIView
/// 返回想要封裝的 UIView 類型 和 實例
func makeUIView(context: Self.Context) !" Self.UIViewType
/// UIViewRepresentable 中的某個屬性發(fā)生變化,SwiftUI 要求更新該 UIKit 部件時被調(diào)用
func updateUIView(
_ uiView: Self.UIViewType,
context: Self.Context
)
}
舉個栗子
struct SearchBar : UIViewRepresentable {
@Binding var text : String
class Cordinator : NSObject, UISearchBarDelegate {
@Binding var text : String
init(text : Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Cordinator {
return Cordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
學(xué)習(xí)資料
- 斯坦福公開課 CS193P·2020 年春:該課程強推蚯舱,我當(dāng)年學(xué)習(xí) OC 看的就是它改化,現(xiàn)在到SwiftUI了還是先看這個,系統(tǒng)且細(xì)致枉昏,結(jié)合案例和編程過程中的小技巧介紹陈肛,是很好的入門課程。
- 蘋果官方 SwiftUI 課程:打開Xcode兄裂,照著官方的教學(xué)句旱,從頭到尾學(xué)著做一遍應(yīng)用。
- Hacking with swift:這是國外一個程序員用業(yè)余時間搭建的分享網(wǎng)站晰奖,有大量的文章可以閱讀谈撒,還有推薦初學(xué)者跟著做的「100 Days of SwiftUI」課程。
- 蘋果官方文檔:雖然很多文檔缺乏工程細(xì)節(jié)匾南,但是文檔涉及很多概念性的內(nèi)容港华,你可以知道官方是怎么思考的,并且有很多具體的機制參數(shù)
- SwiftUI的 View 如何布局午衰?