SwifUI frame 的參數三兩事

我們都知道在 SwiftUI 中的元件可以透過 frame() 修飾器來調整大小,但這個修飾器有一堆的參數,從文件上很不容易知道這些參數到底在幹嘛,這篇文章就來詳細解析一番。

由元件大小來區分 SwiftUI 元件可以分成兩種類型,一種是有個性的元件,這種元件有他自己想要的大小,例如 Text 或是 Image,他們會忽略父元件給出的建議值,自己說了算。另外一種是隨波逐流型,也就是父元件有多大他就有多大,例如 Color 或是 Space。當然這些元件還要再細分成寬度或高度是不是依賴父元件的寬度或高度,但這裡先不要管這麼細。

接下來看 frame() 的參數。首先,列出 frame() 中跟大小有關的參數,如下,兩兩成對:

  • width / height
  • maxWidth / maxHeight
  • minWidth / minHeight
  • idealWidth / idealHeight

現在透過一個自訂的 Stack 來瞭解上面這幾個參數造成的效果。Layout 協定需要實作兩個函數,這篇文章只要關注第一個用來決定元件實際所需大小的函數,至於第二個函數是用來決定元件實際所在位置,之後文章再談。

struct MyStack: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        print(proposal) // 印出父元件給的建議大小
        let size = subviews.first?.sizeThatFits(proposal)
        print(size!) // 印出子元件自己說他要的大小
        
        return size! // 傳回最後決定的大小
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        // 這篇文章不討論此函數,之後再談
    }
}

現在我們利用上面的 MyStack 來排版一個簡單的畫面,如下。 先在 MyStack 中放一個 Color 元件,然後將 Color 元件透過 frame() 來指定成 100 x 100 大小,這個操作等同於 frame 是 Color 的父元件,也就是我們產生了一個固定大小 100 x 100 的黃色方格元件。

struct ContentView: View {
    var body: some View {
        MyStack {
            Color.yellow.frame(width: 100, height: 100)
        }
        .border(Color.black)
    }
}
// Print 的結果為
ProposedViewSize(width: Optional(393.0), height: Optional(759.0))
(100.0, 100.0)

接下來的流程是 ContentView 會先對 MyStack 給出一個建議大小,其實就是螢幕大小,例如 393 x 759,這是 iPhone 14 Pro 的大小。這個大小會傳到 MyStack 的 sizeThatFits() 函數去,從印出的 proposal 參數可以看到 393 x 759 這個數字。此函數中的 subviews.first 指的是 Color 這個元件的排版,因為 subviews 是一個陣列,所以也可以寫成 subviews[0]。呼叫 Color 排版的 sizeThatFits() 函數會得到 Color 自己要的排版大小,印出的結果會看到 100 x 100,這個數字就是 Color 後面的 frame(width: 100, height: 100) 造成的。最後原封不動的把 100 x 100 傳回去。請記得,實際上傳回去的大小應該是計算後的結果,也就是 MyStack 中如果包含了很多元件,這時傳回去的 size 應該是整體運算完的結果

參數 width / height

ContentView 對 MyStack 給出 393 x 759 的建議值,MyStack 對 Color 給出 20 x 20 建議值,因為 Color 後面 frame() 的關係,所以 Color 不管這個建議值,依然回傳 100 x 100,最後 MyStack 調整為 20 x 20,Color 還是 100 x 100。

MyStack {
    Color.yellow.frame(width: 100, height: 100)
}
.frame(width: 20, height: 20)
.border(Color.black)
// Print 的結果為
ProposedViewSize(width: Optional(20.0), height: Optional(20.0))
(100.0, 100.0)

參數 maxWidth / maxHeight

這組參數的意思是,如果設定的值比父元件給的建議值大,則以父元件的建議值作為子元件的建議值。以下面程式碼為例,MyStack 的 frame() 設定值超過了 ContentView 給的建議值,例如 1000 x 1000,這時 MyStack 收到的 proposal 值會是父元件的建議值,也就是 393 x 759。看黑邊框位置。

MyStack {
    Color.yellow.frame(width: 100, height: 100)
}
.frame(maxWidth: 1000, maxHeight: 1000)
.border(Color.black)
// Print 的結果為
ProposedViewSize(width: Optional(393.0), height: Optional(759.0))
(100.0, 100.0)

如果設定的值比父元件給的建議值小,例如 300 x 300,但大於子元件需要的值,目前是 100 x 100,這時 MyStack 收到的 proposal 就會是 300 x 300,並且傳給 Color,而 Color 不變應萬變都是回傳 100 x 100。

MyStack {
    Color.yellow.frame(width: 100, height: 100)
}
.frame(maxWidth: 300, maxHeight: 300)
.border(Color.black)
// Print 的結果為
ProposedViewSize(width: Optional(300.0), height: Optional(300.0))
(100.0, 100.0)

若設定的值比子元件的需求還要小,例如 20 x 20,此時 MyStack 收到的 proposal 值就是 20 x 20。

MyStack {
    Color.yellow.frame(width: 100, height: 100)
}
.frame(maxWidth: 20, maxHeight: 20)
.border(Color.black)
截圖-2023-09-12-下午12.18.46
// Print 的結果為
ProposedViewSize(width: Optional(20.0), height: Optional(20.0))
(100.0, 100.0)

參數 minWidth / minHeight

這組參數的意思是,若設定的值比子元件的需求還小時,則將子元件的需求值作為子元件的建議值。下面的範例是,如果 MyStack 設定的 frame 值比父元件給的建議值大,MyStack 收到的 proposal 以設定值為主。例如,下面程式碼會看不到黑邊框,因為跑到螢幕外面去了,得透過 Xcode 的 Debug View Hierarchy 工具才看得到。

MyStack {
    Color.yellow.frame(width: 100, height: 100)
}
.frame(minWidth: 1000, minHeight: 1000)
.border(Color.black)
// Print 的結果為
ProposedViewSize(width: Optional(1000.0), height: Optional(1000.0))
(100.0, 100.0)

若 MyStack 的設定值大小在父元件與子元件之間,這時雖然收到的 proposal 會是設定值,但是 sizeThatFits() 函數跑了不止一次,這代表決定全部子元件所需 size 過程需要多次計算,不是一次就可以搞定,而我們無法控制這個次數。

MyStack {
    Color.yellow.frame(width: 100, height: 100)
}
.frame(minWidth: 300, minHeight: 300)
截圖-2023-09-12-下午12.58.29
// Print 的結果為
ProposedViewSize(width: Optional(393.0), height: Optional(759.0))
(100.0, 100.0)
ProposedViewSize(width: Optional(300.0), height: Optional(300.0))
(100.0, 100.0)

如果 MyStack 給的 frame() 值低於 Color 元件,這時就會以 Color 元件為主,MyStack 的大小也會調整成跟子元件(也就是 Color)所需大小一樣。

MyStack {
    Color.yellow.frame(width: 100, height: 100)
}
.frame(minWidth: 20, minHeight: 20)
.border(Color.black)
// Print 的結果為
ProposedViewSize(width: Optional(393.0), height: Optional(759.0))
(100.0, 100.0)
ProposedViewSize(width: Optional(100.0), height: Optional(100.0))
(100.0, 100.0)

參數 idealWidth / idealHeight

理想值的意思是,不論 MyStack 的 frame 設定多少,最後 MyStack 的大小都會與子元件一樣,也就是 100 x 100。

MyStack {
    Color.yellow.frame(width: 100, height: 100)
}
.frame(idealWidth: CGFloat.random(in: 1...1000), idealHeight: CGFloat.random(in: 1...1000))
.border(Color.black)
截圖-2023-09-12-下午1.24.16
// Print 的結果為
ProposedViewSize(width: Optional(393.0), height: Optional(759.0))
(100.0, 100.0)
ProposedViewSize(width: Optional(100.0), height: Optional(100.0))
(100.0, 100.0)

最後,建議您將 Color 元件換成 Text 元件,然後將上述這些參數再跑一次看看,會發現 Text 元件有他自己要的大小(有個性的元件),不受父元件或是 frame() 的建議約束(僅受到寬度過小的約束)。

MyStack {
    Text("Hello, World!")
        .border(Color.red)
}
.frame(minWidth: 300, minHeight: 300)
.border(Color.black)
// Print 的結果為
ProposedViewSize(width: Optional(393.0), height: Optional(759.0))
(97.33333333333333, 20.333333333333332)
ProposedViewSize(width: Optional(300.0), height: Optional(300.0))
(97.33333333333333, 20.333333333333332)

應用

想想這個畫面要怎麼做出來?

答案是,下面兩種作法都可以。

VStack {
    Text("Hello, World!")
        .frame(maxWidth: .infinity)
        .background(.green)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.blue)
Text("Hello, World!")
    .frame(maxWidth: .infinity)
    .background(.green)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.blue)

另外一個例子,若要將文字放到父元件的左上角位置,下面這樣處理就可以了。

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
    }
}

One thought on “SwifUI frame 的參數三兩事

  1. Pingback: SwiftUI 的 frame 參數三兩事(二) – 研蘋果

發表迴響