本文始發(fā)于我的博文手把手教你深入玩轉(zhuǎn)SwiftUI(1),現(xiàn)轉(zhuǎn)發(fā)至此。
Swift
是世界上最好的語言!
一、前言
作為一個使用 Swift 已經(jīng)六年的程序猿,SwiftUI 已經(jīng)發(fā)布一年多了,最近才終于有時間實踐了一番。決定寫寫這個系列,這是僅次于 Swift 的另一重要發(fā)布,下一代的客戶端 UI 開發(fā)技術(shù),剛好前段時間有個產(chǎn)品經(jīng)理的朋友說想學(xué)。
除了分享基礎(chǔ)知識,還會講解深層次的要點(diǎn)精髓。這才是學(xué)習(xí)的重點(diǎn)。
從蘋果的官方的教程開始。這教程非常給力,交互非常好,例子簡練精要,可以感受到SwiftUI
受到多大的重視。
本文中的例子來自文末附錄帶的蘋果官方 Demo
二、聲明式 UI 編程
相信很多寫過 React 的原生同學(xué)會有個比較深的感受,界面開發(fā)起來很方便,那是因為,人家已經(jīng)是“聲明式編程”了。iOS 還在用著 UIKit 和 Storyboard 繁瑣地寫著描述式 UI 和設(shè)置約束。
同時 React 的狀態(tài)管理和數(shù)據(jù)綁定也極大地刺激著我們。在Controller、View 和 Model 間來回轉(zhuǎn)悠的我們早已疲乏,項目越來越需要花精力重構(gòu),增大維護(hù)成本。雖然也有些辦法處理,但去不到根源。
不可否認(rèn) UIKit 曾經(jīng)作出巨大貢獻(xiàn),降低了我們的學(xué)習(xí)曲線,但終究是要被替代的,就像 Swift 替代 OC,只是時間問題。
SwiftUI
是新一代界面編程思想——聲明式編程,本質(zhì)是一套DSL
。網(wǎng)上有很多介紹,在此就不贅述了。
三、課程拆解
官方教程這一篇分六個 Section,主要講解幾個點(diǎn):
- 創(chuàng)建 SwiftUI 項目
- 使用 SwiftUI 預(yù)覽功能
- 自定義組件
- 使用 Text、Image、VStack、HStack、Spacer 等 UI 組件
- 使用其他庫的組件
下面主要從一個個文件的角度來拆解要點(diǎn)和各種新特性。
創(chuàng)建 SwiftUI 項目
- 打開 Xcode -> File -> New -> Project...
- 彈窗 App
-> Next
-> 填寫 Product Name(項目名),如 LandmarksApp
-> Interface 選擇 SwiftUI
-> Life Cycle 選擇 SwiftUI App
-> 選擇存放的目錄 Create
LandmarksApp.swift
import SwiftUI
@main
struct LandmarksApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
- SwiftUI
這里import
了一個新的庫SwiftUI
,包含以前常用的 UI 庫,并新定義了一些 UI 相關(guān)的類。其中最重要的是Combine
,后面會簡單說到。
import Combine
import CoreData
import CoreFoundation
import CoreGraphics
import Darwin
import DeveloperToolsSupport
import Foundation
import SwiftUI
import UIKit
import UniformTypeIdentifiers
import os.log
import os
import os.signpost
- View
SwiftUI 庫中定義了View
,可以看到是個協(xié)議,定義了屏幕上一個元素符合的條件。自定義新的 UI 類只要遵循這個協(xié)議,就可以被展示。這種設(shè)計比以前的UIView
作為 UI 父類更輕便。
而且它的body
屬性本質(zhì)也是View
,這種設(shè)計是SwiftUI
的精髓,下面會講到。
public protocol View {
/// 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.
associatedtype Body : View
/// The content and behavior of the view.
@ViewBuilder var body: Self.Body { get }
}
- 修飾詞
@main
這里提供了程序的啟動入口。
使用 SwiftUI 的應(yīng)用的生命周期需要符合App
協(xié)議,當(dāng)前是定義了一個結(jié)構(gòu)體類型,叫SwiftUIDemoApp
,遵循了App
協(xié)議。使用類遵循協(xié)議也可以,不一定要結(jié)構(gòu)體。
- 關(guān)鍵字
some
這里使用some
返回了一個Opaque Result Type(反向泛型類型,也稱不透明結(jié)果類型)。反向泛型類型的類型參數(shù),是由實現(xiàn)指定并隱藏起來的,一般的泛型是由使用者指定的。
struct ContentView: View {
var body: some View {
VStack {
Text("Hello Zack !")
Image("zack-avatar")
}
}
}
比如上面的代碼,實際上返回的類型是VStack<TupleView<(Text, Image)>>
,由系統(tǒng)自行推斷。我們可以任意組合 UI 組件內(nèi)部的實現(xiàn)(包括運(yùn)行時的變動),不用去指明具體類型,節(jié)省了非常多的工作量。
Swift 泛型系統(tǒng)長期存在一個問題是,帶有 associatedtype 的協(xié)議不能作為類型使用,只能作為類型約束使用,無法作為函數(shù)的返回參數(shù)。而Some
的設(shè)計,使這個問題得到解決。
因此可以看出,正是View
的遞歸設(shè)計和Some
的隱藏特性,支撐了SwiftUI
的聲明式以及高度可組合,這是它的精髓!
CircleImage.swift
import SwiftUI
struct CircleImage: View {
var body: some View {
Image("zackzheng")
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 7)
}
}
struct CircleImage_Previews: PreviewProvider {
static var previews: some View {
CircleImage()
}
}
- Image
使用Image
組件展示圖片,只需在Assets.xcassets
中添加素材,再用素材名稱初始化即可,比起以前方便一點(diǎn)。
- 鏈?zhǔn)秸{(diào)用
這里定義了一個結(jié)構(gòu)體CircleImage
。body
需要返回遵循View
協(xié)議的類型,因此可以推測.clipShape
、.overlay
、.shadow
幾個鏈?zhǔn)秸{(diào)用上的方法返回的都是View
,查看定義可驗證。
這里幾個方法從命名得知是形狀修剪、覆蓋邊框、陰影等,不需贅述。
鏈?zhǔn)秸{(diào)用
的設(shè)計也為聲明式提供了簡化和靈活度,不用如 React 一樣將所有屬性都配置。
- PreviewProvider
A type that produces view previews in Xcode.
這是用于實現(xiàn)SwiftUI
在 Xcode 中的實時預(yù)覽特性,在 Xcode 默認(rèn)是界面右側(cè)可以看到(點(diǎn)擊右側(cè)上方的 Resume,或者在菜單 Editor > Canvas 打開)。
Amazing!做過前端開發(fā)的原生同學(xué),都羨慕解釋型語言的這個特點(diǎn),雖然之前 Swift Playground 已經(jīng)可以實時運(yùn)行結(jié)果,但這次是 UI 啊!對標(biāo) Weex 或 React Native 的 Hot Reloading。
這里只是支持 Xcode 預(yù)覽,所以這部分代碼并不是必須的。
每個.swift
文件可以寫多個,預(yù)覽會縱向排列。另外previews
屬性實際上可以任意返回,即使不是你當(dāng)前新自定義的類。
系統(tǒng)需要升級到 11.x macOS Big Sur
使用搭載有 SwiftUI.framework 的 macOS 10.15 才能夠看到預(yù)覽
- 修改組件屬性
在預(yù)覽處處于 Preview 狀態(tài)下,command + 點(diǎn)擊組件,會彈出菜單,點(diǎn)擊“Show SwiftUI Inspector”,就可以對組件屬性進(jìn)行修改。
MapView.swift
import SwiftUI
import MapKit
struct MapView: View {
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868),
span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
)
var body: some View {
Map(coordinateRegion: $region)
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
這里介紹了引用其他庫的組件,MapKit 庫中的Map
組件。
@State
@State 是屬性修飾詞。
SwiftUI 將會把使用過
@State
修飾器的屬性存儲到一個特殊的內(nèi)存區(qū)域,并且這個區(qū)域和 View struct 是隔離的. 當(dāng)@State
裝飾過的屬性發(fā)生了變化,SwiftUI 會根據(jù)新的屬性值重新創(chuàng)建視圖。
當(dāng)我們需要屬性變化時,界面就自動刷新,則需要給屬性添加@State
修飾,類似于Vue和React中的State。以后不需要通過didSet
來實現(xiàn)類似功能。
- @Binding
在 Swift 中基礎(chǔ)屬性變量的傳遞形式是值類型傳遞,不是引用傳遞。通過在變量前面添加$
,可以變成引用傳遞,如代碼中的$region
。這樣當(dāng) region 變化時,由于@State
的關(guān)系,界面會重新渲染,Map 組件就會接收到屬性的新值。
所以這兩個裝飾器結(jié)合使用,達(dá)到通過屬性重新渲染子組件的目的。
監(jiān)聽和渲染底層是通過Combine
實現(xiàn),可以看到下面 View 的 extension,這里不展開講。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {
/// Adds an action to perform when this view detects data emitted by the
/// given publisher.
///
/// - Parameters:
/// - publisher: The publisher to subscribe to.
/// - action: The action to perform when an event is emitted by
/// `publisher`. The event emitted by publisher is passed as a
/// parameter to `action`.
///
/// - Returns: A view that triggers `action` when `publisher` emits an
/// event.
@inlinable public func onReceive<P>(_ publisher: P, perform action: @escaping (P.Output) -> Void) -> some View where P : Publisher, P.Failure == Never
}
ContentView.swift
struct ContentView: View {
var body: some View {
VStack {
MapView()
.ignoresSafeArea(edges: .top)
.frame(height: 300)
CircleImage()
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack {
Text("Joshua Tree National Park")
Spacer()
Text("California")
}
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("About Turtle Rock")
.font(.title2)
Text("Descriptive text goes here.")
}
.padding()
Spacer()
}
}
}
- VStack、HStack
這兩個組件其實就是UIStackView
的 SwiftUI 版本,VStack
是子組件縱向排列,HStack
是橫向排列,都可以分別設(shè)置兩個方向上的排列對齊方式。
相信用過 UIStackView 的同學(xué)都十分喜愛它,小編也用到愛不釋手,并且逐漸替代了很多 UI 布局場景,廣泛使用,放棄了麻煩的NSLayoutConstraint
。
它們的初始化方式很有意思,放幾個組件就可以了。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct VStack<Content> : View where Content : View {
/// Creates an instance with the given spacing and horizontal alignment.
///
/// - Parameters:
/// - alignment: The guide for aligning the subviews in this stack. This
/// guide has the same vertical screen coordinate for every child view.
/// - spacing: The distance between adjacent subviews, or `nil` if you
/// want the stack to choose a default distance for each pair of
/// subviews.
/// - content: A view builder that creates the content of this stack.
@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
/// 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 = Never
}
可以看到,實際上要求的是Content
類型。有興趣可以自行查閱Funtion builders了解,我就不說了。
從 Demo 也可以看出,這兩個組件會成為以后 SwiftUI 布局的基礎(chǔ)組件。配合 offset 和 padding 成為主要的布局配合。需要拋棄掉傳統(tǒng)的依賴NSLayoutConstraint
布局的慣性思維。所以小編適應(yīng)起來挺快的。
- .ignoresSafeArea(edges: .top)
處于頁面頂部安全區(qū)內(nèi)的組件,通過ignoresSafeArea
進(jìn)行適配,具體就不作介紹。
- offset、padding
這兩個方法可以改改 Demo ,就能看出區(qū)別了。
在 VStack、HStack 下,offset
相當(dāng)于改變bounds
,不會改變frame
。而padding
相反,會改變frame
,不會改變bounds
。
-
Bool
的toggle()
今天還看到Struct
類型Bool
有個mutating
方法toggle
,作用就是將true
變?yōu)?code>false,或者將false
變?yōu)?code>true。
mutating
大家應(yīng)該都很熟悉了,就是Struct
用于修改自身屬性的方法定義。所以這個方法很實用,比以前寫A = !A
感覺自然多了。
Swift 新手概念:生命周期、協(xié)議、結(jié)構(gòu)體、類、范型、import、Safe Area(安全區(qū)) 、bounds、frame、mutating
四、總結(jié)
至此,官方教程的第一章第一節(jié),我們講解完了。后面會繼續(xù)整理剩下的章節(jié)。
從上我們可以總結(jié)出幾點(diǎn):
- 協(xié)議在
SwiftUI
的整體設(shè)計上發(fā)揮了巨大的作用 -
SwiftUI
完美滿足 UI 快速實時預(yù)覽的需求 -
@Binding
和@State
實現(xiàn)了數(shù)據(jù)流層面的界面狀態(tài)綁定 -
some
實現(xiàn)的Opaque Result Types
解決了范型存在已久的問題,為更好的設(shè)計方式提供了途徑 -
VStack
、HStack
、offset
、padding
將構(gòu)成SwiftUI
布局的基礎(chǔ),替代繁瑣的NSLayoutConstraint
- 鏈?zhǔn)秸{(diào)用的設(shè)計簡化了聲明式的靈活實現(xiàn)
View
的遞歸設(shè)計和some
的隱藏特性,支撐了SwiftUI
的聲明式以及高度可組合特性,這是它的精髓- 聲明式和數(shù)據(jù)綁定的特性,極大地提高我們的效率,降低犯錯幾率、減少維護(hù)成本。
總而言之,SwiftUI
,非常優(yōu)秀!
五、附錄
SwiftUI Tutorials
SwiftUI Tutorials/Creating and Combining Views
官方 Demo —— Project files
-END-
歡迎到我的博客交流:https://zackzheng.info