新建一個SwiftUI的項目
項目結(jié)構(gòu)
我們發(fā)現(xiàn)圖2中阴绢,項目結(jié)構(gòu)變得非常的簡單喘垂,只有兩個文件#AppName#App.swift和ContentView.swift
我們先看一下相對簡單的ContentView.swift這個文件。
代碼并不多侄泽,創(chuàng)建一個結(jié)構(gòu)體遵循了View協(xié)議礁芦,重寫了body變量的get方法這時候我們發(fā)現(xiàn)一個比較讓人疑惑的事情,這個View類型用一個some來修飾悼尾。
some關(guān)鍵字是什么柿扣?
被some關(guān)鍵字修飾的類型被成為不透明類型(opaque return types)。
它的語義可以簡單的理解為“返回符合這個協(xié)議的具體類型闺魏,但不指明具體類型”未状。
它相當(dāng)于一個反向范型。通常的范型析桥,我們是需要使用協(xié)議本身司草,并不關(guān)心子類是什么艰垂,需要被調(diào)用者指定。而反向范型正好相反埋虹,反范型函數(shù)需要使用協(xié)議的子類來處理猜憎,但是返回一個遵循協(xié)議的不透明類型。
范型:具體類型由調(diào)用者指定
some:具體類型由被調(diào)用者指定
//Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
//代碼提示以上錯誤搔课,無法編譯通過
//(1)
func someView() -> View {
Text("Hello World")
}
//(2)
func someView2() -> Text {
Text("Hello World")
}
//(3)
func someView3() -> some View {
Text("Hello World")
}
函數(shù)(1)無法編譯通過胰柑,因為View協(xié)議內(nèi)部定義了Self范型,我們可以看下View協(xié)議的定義。
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
這里使用了Self范型爬泥,以及Body范型柬讨。Swift語法中,這不符合語法規(guī)范急灭。因為編譯器只能知道View的接口情況姐浮,而并不知道associatedtype的具體情況谷遂。
函數(shù)(2)雖然可以編譯通過葬馋,但是我們可能用任意視圖去渲染,如果每次視圖不一樣肾扰,都要改方法的返回值畴嘶,那就太麻煩了。some關(guān)鍵字就解決了這個問題集晚。它可以有被調(diào)用者指定具體類型窗悯,而不用透明出來。
接下來我們看一下有一個struct叫ContentView_Previews它遵循了PreviewProvider協(xié)議偷拔。以下是PreviewProvider的定義
public protocol PreviewProvider : _PreviewProvider {
associatedtype Previews : View
@ViewBuilder static var previews: Self.Previews { get }
static var platform: PreviewPlatform? { get }
}
跟View的定義類似蒋院,只不過preivews是statics變量。這個類對應(yīng)右手邊Canvas窗口莲绰∑劬桑可以進行實時預(yù)覽。previews可以返回多個View蛤签,每個View都會生成一個預(yù)覽窗口辞友。我們可以結(jié)合不同的參數(shù),測試各種case震肮。
不同參數(shù)的測試
struct ContentView: View {
private var content:String = "test"
init(aContent: String) {
content = aContent
}
var body: some View {
Text(content)
.lineLimit(2)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(aContent: "Hello")
ContentView(aContent: "Hello\nHello")
ContentView(aContent: "Hello\nHello\nHello")
}
}
以上代碼在Content中繪制了一個Text称龙,通過init傳入的參數(shù)展示內(nèi)容。Text被設(shè)定為最多展示兩行戳晌。
預(yù)覽窗口分別顯示了一行文本鲫尊,兩行文本,三行文本展示的樣式沦偎。
不同機型的測試
View協(xié)議定義了previewDevice的方法疫向,可以傳入一個PreviewDevice對象竞帽,測試不同機型的展示
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(aContent: "Hello")
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
ContentView(aContent: "Hello")
.previewDevice(PreviewDevice(rawValue: "iPhone 8Plus"))
ContentView(aContent: "Hello")
.previewDevice(PreviewDevice(rawValue: "iPhone X"))
ContentView(aContent: "Hello")
.previewDevice(PreviewDevice(rawValue: "iPhone 11"))
}
}
以上代碼就可以根據(jù)不同機型預(yù)覽視圖
如果是支持多平臺的項目,還可以根據(jù)不同平臺預(yù)覽 鸿捧,重寫PreviewProvider的platform方法
static var platform: PreviewPlatform? { get }
PreviewPlatfrom是一個枚舉屹篓,定義如下
public enum PreviewPlatform {
case iOS
case macOS
case tvOS
case watchOS
}
預(yù)覽代碼對原有的業(yè)務(wù)邏輯無入侵,而且可以事實預(yù)覽非常方便匙奴,調(diào)試階段可以把各種UI的case寫入預(yù)覽類里面堆巧,每個View創(chuàng)建時,默認(rèn)都會創(chuàng)建預(yù)覽類泼菌。
是源代碼也是樣式表
var body: some View {
VStack {
HStack {
Text(content)
.lineLimit(2)
.background(Color.blue)
.foregroundColor(.green)
.lineSpacing(5)
Text("right")
.background(Color.red)
}
Text("bottom")
.font(.none)
}.colorScheme(.light)
}
這是一個簡單的視圖布局谍肤,我們可以看到它特別像Android里面的布局文件xml。但由于它確實也是源代碼哗伯,所以在代碼風(fēng)格上尤為特殊荒揣,要避免過于混亂導(dǎo)致的維護困難。xml是通過節(jié)點對齊的方式書寫焊刹,SwiftUI也有自己的書寫樣式系任。上面的代碼我給Text設(shè)定了很多屬性。我們可以看到它是以鏈?zhǔn)椒绞秸{(diào)用的虐块。也有一些容器組件俩滥。自動生成的代碼樣式就是如上面所以,不同屬性之間通過【換行】+【Tab縮進】方式排列贺奠,容易組件于基本組件之間也用【Tab縮進】方式排列霜旧。同一層級多個組件對齊。這可能是蘋果官方推薦的一種代碼樣式儡率,建議所有人都按照這種風(fēng)格書寫挂据,由于屬性太多導(dǎo)致很多行,同一層級的多個組件之間建議增加空行儿普。
@ViewBuilder
在上述代碼我們看到SwiftUI的布局代碼跟Xml或者json有點類似崎逃。VStack是個節(jié)點里面有HStack和Text節(jié)點,HStack又有兩個Text箕肃。這其中每個節(jié)點都可以作為一個完整獨立的視圖婚脱。包括單純的Text里面的一串屬性,多一個勺像,少一個也并不影響其完整性障贸,其最終結(jié)果都返回一個View。而@ViewBuilder吟宦,可以理解為一種語法糖或者宏篮洁,其有點類似于閉包,把一段布局代碼封裝成一個特殊的對象殃姓,這個對象作為參數(shù)和返回值傳遞到各種地方進行復(fù)用袁波。
struct RedBackGroundCornerRadius<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.background(Color.red)
.cornerRadius(5)
}
}
struct FontColorGreen<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content.foregroundColor(.green)
}
}
struct MyView: View {
var body: some View {
RedBackGroundCornerRadius {
FontColorGreen {
Text("Hello World!")
}
}
}
}
我們可以看到@ViewBuilder可以把我們既定的樣式進行封裝瓦阐,把我們的樣式封裝成一種容器類的視圖。這樣我們就可以復(fù)用我們定義的一些通用的樣式篷牌,并且以一種裝飾器的方式構(gòu)建頁面睡蟋。代碼變得簡潔易懂,復(fù)用性強枷颊。
接下來我們看另外一個文件##AppName##App.swift文件
import SwiftUI
@main
struct SwiftUIFirstAppApp: App {
var body: some Scene {
WindowGroup {
ContentView(aContent: "Hello World")
}
}
}
struct SwiftUIFirstAppApp_Previews: PreviewProvider {
static var previews: some View {
Text("Hello, World!")
}
}
全部的代碼也非常少戳杀。首先說一下@main這個寫法。
這個寫法有點像Java的注解夭苗。實際上有點類似信卡,它定義了程序的入口點。它后面必須跟一個struct题造,后面的類必須提供一個static void main方法傍菇,否則會報錯,說實話者跟J2SE的main函數(shù)有點像界赔。App啟動后會調(diào)用這個main方法丢习。
假如你嘗試再搞一個遵循App的struct,前面加上@main仔蝌,程序完全可以正常運行泛领,而你原來的App struct就沒有用了荒吏。我也嘗試自己隨便寫一個struct敛惊,不遵循App定義,也實現(xiàn)了static void main方法绰更,程序編譯通過瞧挤,并且也可以啟動,只不過后面直接崩潰了儡湾。
再說一下App特恬,App是一個協(xié)議(protocol),其定義如下。
public protocol App {
associatedtype Body : Scene
@SceneBuilder var body: Self.Body { get }
init()
}
顯而易見徐钠,Scene也是一個協(xié)議癌刽,App有一個body屬性遵循Scene協(xié)議。每個Scene有一個包含一個視圖層級樹的根視圖尝丐,并由系統(tǒng)管理其生命周期显拜。SwiftUI提供了一些具體的場景類型來處理常見的一些場景。例如文檔和設(shè)置爹袁,你也可以自定義一些Scene远荠。
同樣顯而易見,從代碼我們也知道失息,WindowGroup是實現(xiàn)了Scene的一個具體struct譬淳。WindowGroup是作為視圖層級的一個容器档址,我們可以簡單看下WindowGroup的定義。
public struct WindowGroup<Content> : Scene where Content : View {
public init(@ViewBuilder content: () -> Content)
}
容器通常有一個Content范型邻梆,其實現(xiàn)View協(xié)議守伸,并且再初始化的時候傳入一個閉包,閉包返回一個View浦妄。WindowGroup是一個既有的含友,SwiftUI提供的Scene。Content就是WindowGroup的根視圖校辩。
如果我們想要自定義Scene窘问,可以進行如下代碼。
struct MyScene: Scene {
var body: some Scene {
WindowGroup {
MyRootView()
}
}
}
系統(tǒng)根據(jù)程序狀態(tài)宜咒,以適合平臺特性的方式顯示W(wǎng)indowGroup的視圖惠赫。例如,系統(tǒng)允許用戶在macOS和iPadOS等平臺上創(chuàng)建或刪除包含MyRootView的窗口故黑。在其他平臺上儿咱,就會全屏展示。