解析SwiftUI布局細節(一)入門


 

前言


 

      在前面的文章中談了談對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

      項目地址

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM