SwiftUI學(xué)習(xí)(4)-Hello World!

新建一個(gè)SwiftUI的項(xiàng)目

圖1
圖2

項(xiàng)目結(jié)構(gòu)

我們發(fā)現(xiàn)圖2中,項(xiàng)目結(jié)構(gòu)變得非常的簡單,只有兩個(gè)文件#AppName#App.swiftContentView.swift

我們先看一下相對(duì)簡單的ContentView.swift這個(gè)文件。
代碼并不多,創(chuàng)建一個(gè)結(jié)構(gòu)體遵循了View協(xié)議,重寫了body變量的get方法這時(shí)候我們發(fā)現(xiàn)一個(gè)比較讓人疑惑的事情,這個(gè)View類型用一個(gè)some來修飾。

some關(guān)鍵字是什么?
被some關(guān)鍵字修飾的類型被成為不透明類型(opaque return types)。
它的語義可以簡單的理解為“返回符合這個(gè)協(xié)議的具體類型,但不指明具體類型”。
它相當(dāng)于一個(gè)反向范型。通常的范型,我們是需要使用協(xié)議本身,并不關(guān)心子類是什么,需要被調(diào)用者指定。而反向范型正好相反,反范型函數(shù)需要使用協(xié)議的子類來處理,但是返回一個(gè)遵循協(xié)議的不透明類型。

范型:具體類型由調(diào)用者指定
some:具體類型由被調(diào)用者指定

//Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
//代碼提示以上錯(cuò)誤,無法編譯通過
//(1)
func someView() -> View {
    Text("Hello World")
}
//(2)
func someView2() -> Text {
    Text("Hello World")
}
//(3)
func someView3() -> some View {
    Text("Hello World")
}

函數(shù)(1)無法編譯通過,因?yàn)閂iew協(xié)議內(nèi)部定義了Self范型,我們可以看下View協(xié)議的定義。

public protocol View {
    associatedtype Body : View
    @ViewBuilder var body: Self.Body { get }
}

這里使用了Self范型,以及Body范型。Swift語法中,這不符合語法規(guī)范。因?yàn)榫幾g器只能知道View的接口情況,而并不知道associatedtype的具體情況。

函數(shù)(2)雖然可以編譯通過,但是我們可能用任意視圖去渲染,如果每次視圖不一樣,都要改方法的返回值,那就太麻煩了。some關(guān)鍵字就解決了這個(gè)問題。它可以有被調(diào)用者指定具體類型,而不用透明出來。

接下來我們看一下有一個(gè)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變量。這個(gè)類對(duì)應(yīng)右手邊Canvas窗口。可以進(jìn)行實(shí)時(shí)預(yù)覽。previews可以返回多個(gè)View,每個(gè)View都會(huì)生成一個(gè)預(yù)覽窗口。我們可以結(jié)合不同的參數(shù),測(cè)試各種case。

不同參數(shù)的測(cè)試

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中繪制了一個(gè)Text,通過init傳入的參數(shù)展示內(nèi)容。Text被設(shè)定為最多展示兩行。
預(yù)覽窗口分別顯示了一行文本,兩行文本,三行文本展示的樣式。

4.jpg
5.jpg
6.jpg

不同機(jī)型的測(cè)試

View協(xié)議定義了previewDevice的方法,可以傳入一個(gè)PreviewDevice對(duì)象,測(cè)試不同機(jī)型的展示

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ù)不同機(jī)型預(yù)覽視圖


8.jpg
9.jpg
10.jpg
11.jpg

如果是支持多平臺(tái)的項(xiàng)目,還可以根據(jù)不同平臺(tái)預(yù)覽 ,重寫PreviewProvider的platform方法

static var platform: PreviewPlatform? { get } 

PreviewPlatfrom是一個(gè)枚舉,定義如下

public enum PreviewPlatform {
    case iOS
    case macOS
    case tvOS
    case watchOS
}

預(yù)覽代碼對(duì)原有的業(yè)務(wù)邏輯無入侵,而且可以事實(shí)預(yù)覽非常方便,調(diào)試階段可以把各種UI的case寫入預(yù)覽類里面,每個(gè)View創(chuàng)建時(shí),默認(rèn)都會(huì)創(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)
    }  

這是一個(gè)簡單的視圖布局,我們可以看到它特別像Android里面的布局文件xml。但由于它確實(shí)也是源代碼,所以在代碼風(fēng)格上尤為特殊,要避免過于混亂導(dǎo)致的維護(hù)困難。xml是通過節(jié)點(diǎn)對(duì)齊的方式書寫,SwiftUI也有自己的書寫樣式。上面的代碼我給Text設(shè)定了很多屬性。我們可以看到它是以鏈?zhǔn)椒绞秸{(diào)用的。也有一些容器組件。自動(dòng)生成的代碼樣式就是如上面所以,不同屬性之間通過【換行】+【Tab縮進(jìn)】方式排列,容易組件于基本組件之間也用【Tab縮進(jìn)】方式排列。同一層級(jí)多個(gè)組件對(duì)齊。這可能是蘋果官方推薦的一種代碼樣式,建議所有人都按照這種風(fēng)格書寫,由于屬性太多導(dǎo)致很多行,同一層級(jí)的多個(gè)組件之間建議增加空行。

@ViewBuilder
在上述代碼我們看到SwiftUI的布局代碼跟Xml或者json有點(diǎn)類似。VStack是個(gè)節(jié)點(diǎn)里面有HStack和Text節(jié)點(diǎn),HStack又有兩個(gè)Text。這其中每個(gè)節(jié)點(diǎn)都可以作為一個(gè)完整獨(dú)立的視圖。包括單純的Text里面的一串屬性,多一個(gè),少一個(gè)也并不影響其完整性,其最終結(jié)果都返回一個(gè)View。而@ViewBuilder,可以理解為一種語法糖或者宏,其有點(diǎn)類似于閉包,把一段布局代碼封裝成一個(gè)特殊的對(duì)象,這個(gè)對(duì)象作為參數(shù)和返回值傳遞到各種地方進(jìn)行復(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可以把我們既定的樣式進(jìn)行封裝,把我們的樣式封裝成一種容器類的視圖。這樣我們就可以復(fù)用我們定義的一些通用的樣式,并且以一種裝飾器的方式構(gòu)建頁面。代碼變得簡潔易懂,復(fù)用性強(qiáng)。

接下來我們看另外一個(gè)文件##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這個(gè)寫法。

這個(gè)寫法有點(diǎn)像Java的注解。實(shí)際上有點(diǎn)類似,它定義了程序的入口點(diǎn)。它后面必須跟一個(gè)struct,后面的類必須提供一個(gè)static void main方法,否則會(huì)報(bào)錯(cuò),說實(shí)話者跟J2SE的main函數(shù)有點(diǎn)像。App啟動(dòng)后會(huì)調(diào)用這個(gè)main方法。

假如你嘗試再搞一個(gè)遵循App的struct,前面加上@main,程序完全可以正常運(yùn)行,而你原來的App struct就沒有用了。我也嘗試自己隨便寫一個(gè)struct,不遵循App定義,也實(shí)現(xiàn)了static void main方法,程序編譯通過,并且也可以啟動(dòng),只不過后面直接崩潰了。

再說一下App,App是一個(gè)協(xié)議(protocol),其定義如下。

public protocol App {
    associatedtype Body : Scene
    @SceneBuilder var body: Self.Body { get }
    init()
}

顯而易見,Scene也是一個(gè)協(xié)議,App有一個(gè)body屬性遵循Scene協(xié)議。每個(gè)Scene有一個(gè)包含一個(gè)視圖層級(jí)樹的根視圖,并由系統(tǒng)管理其生命周期。SwiftUI提供了一些具體的場(chǎng)景類型來處理常見的一些場(chǎng)景。例如文檔和設(shè)置,你也可以自定義一些Scene。

同樣顯而易見,從代碼我們也知道,WindowGroup是實(shí)現(xiàn)了Scene的一個(gè)具體struct。WindowGroup是作為視圖層級(jí)的一個(gè)容器,我們可以簡單看下WindowGroup的定義。

public struct WindowGroup<Content> : Scene where Content : View {
      public init(@ViewBuilder content: () -> Content)
}

容器通常有一個(gè)Content范型,其實(shí)現(xiàn)View協(xié)議,并且再初始化的時(shí)候傳入一個(gè)閉包,閉包返回一個(gè)View。WindowGroup是一個(gè)既有的,SwiftUI提供的Scene。Content就是WindowGroup的根視圖。
如果我們想要自定義Scene,可以進(jìn)行如下代碼。

struct MyScene: Scene {
    var body: some Scene {
        WindowGroup {
            MyRootView()
        }
    }
}

系統(tǒng)根據(jù)程序狀態(tài),以適合平臺(tái)特性的方式顯示W(wǎng)indowGroup的視圖。例如,系統(tǒng)允許用戶在macOS和iPadOS等平臺(tái)上創(chuàng)建或刪除包含MyRootView的窗口。在其他平臺(tái)上,就會(huì)全屏展示。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容