解析SwiftUI佈局細節(一)

zxRisingSun發表於2020-12-17

 

前言


 

      在前面的文章中談了談對SwiftUI的基本的認識,以及用我們最常見的TB+NA的方式搭建了一個很基本的場景來幫助認識了一下SwiftUI,具體的文章可以在SwiftUI分類部分查詢,這篇我準備在寫UI的時候從SwiftUI角度我們具體的應該怎樣去做,或者說是用SwiftUI我們該從什麼角度去解析一個頁面。以及對SwiftUI裡面的其中一些細節知識做一下分析總結。

      以前我們用UIKit寫一個列表頁的時候我們的步驟可能是下面這樣的:

      1、建立檢視控制器

      2、大概解析一下UI,該建立頭部的建立頭部檢視,該寫CollectionViewCell或者TableViewCell的我們會做一個基本的分類,規劃一下我們需要幾個型別的Cell等等

      3、把它們進行一個組裝,處理相應的各種代理或者事件回撥等等

      4、處理資料和檢視進行資料對接

      可能我們大部分都是這樣的一個基本的流程,當然還有些涉及到複雜點的業務我們會從單元測試開始等等的會有些許差異,但SwiftUI的重點是對UI的處理,所以我們的重點就單純說說UI部分,那大家可以這樣想,我們用SwiftUI做的時候該怎樣去開始呢,用SwiftUI做的時候流程還會和我們使用UIKit處理的時候還一樣嗎?在實現的細節方面又會有哪些差距呢?帶著這樣一個小小的思考我們進行下面的總結。

 

SwiftUI我們怎麼做以及細節分析


 

      前面文章我有提過一點就是View,SwiftUI最大的區別除了宣告式的UI之外我自己覺得最大的需要我們理解的點就是View,所有的你能看到的基本單位都成了View,沒有了控制器這個概念,這點需要我們轉過這個彎,不然容易繞進去。

      我們從一個具體的實際頁面開始梳理一下用SwiftUI實際寫UI的時候一些基本的知識,就如我們Demo中的我的頁面舉例:

      我們首先得認識一下它倆:VStack (豎直)  HStack  (橫向)

      它們倆我最能接受的方式就是把他們理解成容器(受Cocos影響),一個縱向 (vertical) 容器,一個橫向(horizontal)容器,它們前面的V和H也就是這兩單詞的首字母,提醒一下你要是記不住的話可以記這一點。H(heng) 剩下的V就是縱向的,所有的iOS方向屬性幾乎都是這樣,加深記憶的一個方式而已,但能保證你以後絕不會再搞混淆! 當然這個橫向和縱向也是相對你手機螢幕的是豎直還是水平的,不是絕對的,這個理解一下也容易!由於這兩裡面的東西幾乎都是一樣的,我們就針對一個VStack進行具體的分析,先看看它的原始碼:

/// A view that arranges its children in a vertical line.
@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. It has
    ///     the same horizontal screen coordinate for all children.
    ///   - 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
}

 

      我們解釋一下它初始化的方法引數:

      1、首先我們要認識到VStack是一個結構體

      2、alignment: HorizontalAlignment 我們可以看到它有一個預設的居中對齊值,它控制的就是容器裡面的子檢視的對齊方式,這個可以自己體驗下。

      3、spacing: CGFloat? = nil 這是個可選型別的引數,它控制的是容器裡面子檢視之間的間距。

      4、@ViewBuilder content: () -> Content  這是一個很有意思的東西,很值得我們仔細的說說,因為我們在後面會經常使用到這個@ViewBuilder,要暫時不管它那這個引數就只剩下content: () -> Content部分,這個閉包相信都能理解,一個比較簡單的閉包,對Content 的約束都在宣告VStack的時候說的比較清楚。那他和普通的閉包區別也就在@ViewBuilder上,我們就把重點轉移到對@ViewBuilder的理解上了。

      下面是關於ViewBuilder的定義:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@_functionBuilder public struct ViewBuilder {

    /// Builds an empty view from a block containing no statements.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view through unmodified.
    ///
    /// An example of a single view written as a child view is
    /// `{ Text("Hello") }`.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}

      這裡面最值得注意點就是這個 @_functionBuilder 修飾符,_functionBuilder實質上能對函式進行一次處理,具體的我們可以看看下面的例子:

/// 用_functionBuilder修飾TestBuilder
/// 就像用_functionBuilder修飾了ViewBuilder一樣
/// 我們就用TestBuilder看看它的實際效果
@_functionBuilder struct TestBuilder {
    
    /// String... 引數 數量可變,你可以傳入任意數量的引數
    /// - Parameter items: items description
    /// - Returns: description
    static func buildBlock(_ items: String...) -> [String] {
        
        return items
    }
}

/// 然後我們有這樣一個方法
/// @TestBuilder模擬@ViewBuilder
/// - Parameter content: content description
func testBuilder(@TestBuilder _ content:() -> [String]){
        
    print(content())
}

/// 然後我們呼叫的時候
self.testBuilder {
     "1"
     "2"
     "3"
     "4"
}

      隨後的列印結果就是 ["1", "2", "3", "4"]

      那下面我們理解一下這個例子,在整個顯式的呼叫中,我們似乎是沒有用到buildBlock函式的,那要是我們在定義TestBuilder的時候要是不定義buildBlock是不是也可以,當然是不行的,這個在具體的例子中可以試試,在呼叫的時候就會報錯,告訴你沒有buildBlock函式,這個函式的具體的作用,我們在對它的註釋中能找到答案。

      Builds an empty view from a block containing no statements.

      可以簡單翻譯成-從不包含任何語句的塊中生成空檢視。那我們就明白了,它的作用感覺類似初始化的樣子,要沒有它就顯然是不行的。

      還有上面我們呼叫的時候為什麼要寫成列的形式,能不能寫成"1" "2" "3" "4" 這種形式呢?肯定是不行的,這個你也可以自己嘗試一下。

      我們要再往深入挖掘一下,因為後面還有個問題需要我們注意,在ViewBuilder的最後一個Extension中的buildBlock的程式碼是這樣的

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
}

     由於它裡面最多能接收10個View,所以在我們常見的Stack中也就最多能接收到是個子檢視,這點需要我們注意,不要到時候寫的超過十個瞭然後一頭霧水不知道是啥錯誤。接著我們肯定會疑惑,那就沒有辦法寫是個以上的子檢視了嗎?答案當然是不是,肯定可以,具體的可以通過Group或者ForEach來實現,我們就不在往下深究了,這個問題可以自己看看!

      不知道看到這大家對ViewBuilder應該有了一些認識了吧,我會在後面的參考文章中具體的在給幾個例子地址,大家可以再仔細的看看,我們就看我們Demo中的一個使用,他具體的一個場景是這樣的,在登入頁面,我想加一個點選除了輸入框之外收起鍵盤的操作,我們具體的實現方法其實就是在最底層新增了一個View,然後在它上面新增了點選的手勢,具體得我們看看程式碼:

/// 定義一個常見的背景View
struct Background<Content: View>: View {
    
    private var content: Content

    init(@ViewBuilder content: @escaping () -> Content) {
        
        self.content = content()
    }

    var body: some View {
         
        Color.white
        .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
        .overlay(content)
    }
}

///  UIApplication 的擴充套件
extension UIApplication {
    
    func endEditing() {
        
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

/// 具體的使用就是下面這樣,這樣就達到了我們的目的,中間的程式碼我隱藏起來了,程式碼在BaseLoginView中可以檢視到
///
var body: some View {
        
     Background {                
          /// 裡面具體的檢視內容
     }.onTapGesture {

          self.endEditing()
     }
}

      這樣我相信就基本把這個比較重要的@ViewBuilder給說清楚了,這個VStack或者HStack也就應該慢慢的再理解了。

      理解了之後我們也就能總結一下我們用SwiftUI寫UI時候的一個簡單邏輯

      1、建立好你需要的SwiftUI檔案

      2、規劃好你的檢視層級,比如說是不是巢狀的NavigationView裡面,然後開始規劃Stack,看具體的是需要規劃成幾個你需要的Stack

      3、再往下就是裡面具體的各種控制元件View了,我打算把他們放到下一篇再做一個具體的總結

      下一篇我們就說說SwiftUI關於View跳轉的方式,以及傳值注意點、View位置設定、大小縮放等等的屬性的使用

 

      參考文章:

      SwiftUI之ViewModifier詳解

      SwiftUI中的@ViewBuilder

      專案地址

 

相關文章