> For the complete documentation index, see [llms.txt](https://lochiwei.gitbook.io/ios/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lochiwei.gitbook.io/ios/swiftui/view/layout/grids/examples/mygrid.md).

# MyGrid

{% embed url="<https://youtu.be/aj6cxsOW2X8>" %}

{% tabs %}
{% tab title="⭐️ 重點" %}
{% hint style="warning" %}
⭐️ 注意：👔 <mark style="color:purple;">`MyGrid`</mark>`<`<mark style="color:orange;">`Item, ItemView`</mark>`>` 是一個 [generics & subtypes](/ios/swift/type/category/generic-types/generics-and-subtypes.md)❗️
{% endhint %}

{% hint style="danger" %}
使用 generic type 的 nested type 要小心❗️

* 👔 **MyGrid** (line: <mark style="color:green;">**44**</mark>)：在 MyGrid 內部使用 Layout 基本上沒什麼問題
* 👁️ **TestMyGrid** (line: <mark style="color:red;">**52**</mark>)：在 MyGrid 外面使用 Layout 就要小心了，如果該行寫成下面這樣：

```swift
let layout = MyGrid.Layout( ... )
```

那就會出現下列問題：

* ⛔️ <mark style="color:red;">**Error**</mark>: [Cannot convert value of type 'Self' to expected argument type 'Binding\<C>'](/ios/swiftui/view/layout/cannot-convert-value-of-type-self-to-expected-argument-type-binding-less-than-c-greater-than.md)
  {% endhint %}
  {% endtab %}

{% tab title="👥 相關" %}

* 本例由 [Grid](/ios/swiftui/view/layout/grids/examples/grid.md) 改編而來。
* similar to [grids](/ios/swiftui/view/layout/grids.md) layout.
* `MyGrid.Layout` is [nested types](/ios/swift/type/category/nested-types.md) in [generics & subtypes](/ios/swift/type/category/generic-types/generics-and-subtypes.md).
* code could cause  ⛔️ <mark style="color:red;">Error</mark>:  [Generic parameter '...' could not be inferred.](/ios/swiftui/view/layout/grids/examples/mygrid/generic-parameter-...-could-not-be-inferred..md)
* [to nest or not to nest❓](/ios/swift/type/category/nested-types/to-nest-or-not-to-nest.md) - question about [nested types](/ios/swift/type/category/nested-types.md).
* revised to show the [problem with .readSize()](/ios/swiftui/view/layout/grids/examples/problem-with-.readsize.md).  🐞
  {% endtab %}
  {% endtabs %}

## Files

{% tabs %}
{% tab title="👔 MyGrid" %}
⬆️ 需要： [arr.index(of:)](/ios/swift/type/category/basic/array/.index-of.md)

````swift
// 2022.02.15

import SwiftUI

/// 👔 MyGrid<Item, ItemView>
/// ```
/// MyGrid(items: items) { item in 
///   /* build item view */ 
/// }
/// ```
struct MyGrid<Item: Identifiable, ItemView: View>: View {
    
    // 1. 使用者要提供：
    // • items          : 排版的項目
    // • cellAspectRatio: 希望的格子比例    (default = 1)
    // • viewForItem    : 繪製項目視圖的方法 (@ViewBuilder closure)
    let items: [Item]
    var cellAspectRatio: CGFloat = 1
    @ViewBuilder let viewForItem: (Item) -> ItemView
    
    // 2. 當 MyGrid 開始排版時：
    public var body: some View {
        // ⭐️ 2.1 MyGrid 會利用 GeometryReader 得到 proposed size，
        GeometryReader { geo in 
            // ⭐️ 2.2 然後用 `self.itemViews(in: size)` 繪製所有項目的視圖。
            self.itemViews(in: geo.size)
        }
    }
}

extension MyGrid {
    
    // 2.2
    @ViewBuilder func itemViews(in size: CGSize) -> some View {
        
        // ⭐️ 2.2.1: 委託 MyGrid.Layout 計算「最佳的排版方式」(layout)。
        //
        //    layout 擁有以下資訊：
        //      • .rows, .cols:「該切成幾行幾列」
        //      • .cellSize   :「格子比例」
        //      • .centerOfCell(at: i):「第幾個項目應該排在哪個位置」
        //
        // 👔 MyGrid<Item, ItemView>.Layout
        let layout = Layout(      
            cellAspectRatio, count: items.count, in: size
        )
        
        // ⭐️ ForEach
        ForEach(items) { item in 
            // ⭐️ 2.2.2: 委託 `self.view(for: item, in: layout)` 繪製每個項目的視圖。
            self.view(for: item, in: layout)
        }
    }
    
    // 2.2.2
    @ViewBuilder func view(for  item: Item, in layout: Layout) -> some View 
    {
        // ⭐️ 2.2.2.1: 計算項目編號，得知它是第幾個項目。
        let i = index(of: item)
        
        // ⭐️ 2.2.2.2: 計算項目的中心位置
        let center = layout.centerOfCell(at: i)  // 👔 MyGrid<Item, ItemView>.Layout
        
        // ⭐️ 2.2.2.3: 繪製項目視圖，並放在該放的位置。
        viewForItem(item)
            .frame(layout.cellSize)   // ⭐️ cell size
            // -------------------------------------------
            // ⚠️ 注意：
            //    一旦加上 .position 之後，.frame 會變成整個
            //    GeometryReader 的大小，不再是單一卡片的大小。
            // -------------------------------------------
            .position(center)         // ⭐️ put item view in position
    }
    
    /// 2.2.2.1: index of item
    func index(of item: Item) -> Int {
        items.index(of: item)!        // 🌀Array + .index(of:)
    }                                 //     where Element: Identifiable
}
````

{% endtab %}

{% tab title="👔 MyGrid.Layout" %}
⬆️ 需要： [Vector2D](/ios/custom/package/geometrykit/vector2d.md), [floating +−⨉÷ int](/ios/swift/type/category/basic/numbers/floating-point/floating-+-int.md)

````swift
// 2022.02.15
import SwiftUI

extension MyGrid {

    /// 👔 MyGrid<Item, ItemView>.Layout
    struct Layout {
        
        // ⭐ proposed size
        let size: CGSize
        
        // ⭐ best layout in rows x cols
        //   (to be calculated in `init`, with all available space taken)
        private(set) var rows: Int = 0   
        private(set) var cols: Int = 0
        
        // ⭐ init
        // 根據項目的數量(n)、所給的 proposed size(size)、與希望的 cell view 比例(r)，
        // 計算最佳的列數(rows)、行數(cols)，然後存回 GridLayout 的屬性。
        // 如果指定的行列數所切割出來的格子，其「「長寬比」最接近使用者指定的 
        // idealCellAspectRatio，則為最佳排版方式。
        
        /// ```
        /// GridLayout(itemCount: n, in: size, cellAspectRatio: r)
        /// ```
        public init(
            _ idealCellAspectRatio: CGFloat = 1,   // ideal cell aspect ratio
            count n: Int,           // item count
            in size: CGSize         // proposed size
        ) {
            
            self.size = size
            
            // if zero width or height or itemCount is not > 0,
            // do nothing.
            guard 
                size.width > 0, size.height > 0, n > 0 
            else { return }
            
            // ⭐ find the bestLayout
            // -----------------------
            //    which results in cells whose `aspectRatio`
            //    has the `smallestVariance` from `idealAspectRatio`
            var bestLayout: (rows: Int, cols: Int) = (1, n)    // start from 1 row
            var smallestVariance: CGFloat?
            
            // start to find best layout from 1 column
            for cols in 1...n {
                
                // ⭐ calculate how many columns we need
                let rows = (n / cols) + (n % cols > 0 ? 1 : 0)
                
                // cellAspectRatio = (cell width) / (cell height)
                //                 = (width/cols) / (height/rows)
                //                 = (width/height) / (cols/rows)
                //                 = size.aspectRatio * rows/cols
                // -----------------------------------------------------------
                // ⭐️ rows: ↘ (decreasing)
                //    cols: ↗︎ (strictly increasing) 
                //    ⇒ cellAspectRatio: ↘ (strictly decreasing)
                // -----------------------------------------------------------
                // 🅿️ Vector2D (🌀CGSize + .aspectRatio)
                // 🌀CGFloat (*) | (/) Int: custom operator
                let cellAspectRatio = size.aspectRatio * rows / cols
                
                // current variance with ideal aspect ratio
                let variance = self.variance(cellAspectRatio, idealCellAspectRatio)
                
                // ⭐️ if `smallestVariance` not set, or `variance` getting smaller,
                //    set new `smallestVariance` and new bestLayout
                if smallestVariance == nil || variance < smallestVariance! {
                    smallestVariance = variance
                    bestLayout = (rows: rows, cols: cols)
                } else { 
                    // ⭐️ `smallVariance` is set, and `variance` is getting larger,
                    //     break for loop
                    break  
                }
            }
            
            // save best layout found
            self.rows = bestLayout.rows
            self.cols = bestLayout.cols
        }
    }
}

extension MyGrid.Layout {
    
    /// ⭐️ cell size
    public var cellSize: CGSize {
        (rows == 0 || cols == 0) 
        ? .zero 
        // 🅿️ Vector2D (🌀CGSize(w,h)), CGFloat / Int
        : CGSize(size.width  / cols, size.height / rows)
    }
    
    /// ⭐️ center point of cell at index `i`
    public func centerOfCell(at i: Int) -> CGPoint {
        let rowIndex = i / cols
        let colIndex = i % cols
        return (rows == 0 || cols == 0) 
        ? .zero 
        // 🅿️ Vector2D, CGFloat + Int
        : cellSize.point ** [0.5 + colIndex, 0.5 + rowIndex]
    }
    
    /// ⭐️ variance between two aspect ratios
    func variance(_ r1: CGFloat, _ r2: CGFloat) -> CGFloat {
        precondition(r1 > 0 && r2 > 0)
        // ⭐️ 做對數計算，才不會造成 1:3 = 0.33 的比例
        //    比 2:1 = 2 的比例更接近 1:1 = 1
        return abs(log(r1) - log(r2))
    }
}
````

{% endtab %}

{% tab title="👁️ 預覽：TestMyGrid" %}
⬆️ 需要： [SlidersForSize](/ios/custom/control/slidersforsize.md)

```swift
// 2022.02.15

import SwiftUI

struct TestMyGrid: View {
    
    @State private var size = CGSize(300, 200)   // proposed size
    @State private var ratio: CGFloat = 1        // current cell aspect ratio
    @State private var rows = 0                  // current # of rows
    @State private var cols = 0                  // current # of cols
    
    let items = Array(1...10)
    var idealCellAspectRatio: CGFloat = 1
    
    var body: some View {
        VStack {
            ScrollView {
                myGrid.padding(40)
            }
            controls
        }
    }
}

extension TestMyGrid {
    var myGrid: some View {
        // 👔 MyGrid<Item, ItemView>
        MyGrid(                        
            items: items,  // ⭐️ Item == Int (require: Int: Identifiable)
            cellAspectRatio: idealCellAspectRatio
        ) 
        { i in             // ⭐️ viewForItem: (Item) -> ItemView
            Color.purple
                .border(.black)
                .overlay { Text("\(i)").bold().shadow(radius: 3) }
        }
        // ⭐️ read proposed size from parent (.frame() modifier)
        .readSize { size in

            // calc current layout
            // --------------------------------------------------------------------
            // ⭐️ 問題：
            //    這裏如果寫成：`let layout = MyGrid.Layout( ... )`，會產生錯誤：
            //    ⛔ Error: Generic parameter 'Item' could not be inferred.
            //    ⛔ Error: Generic parameter 'ItemView' could not be inferred.
            // ⭐️ 解決方案：
            //    "Explicitly specify the generic arguments to fix this issue."
            // ⭐️ 解藥：
            //    還好 Layout 的運作方式與 Item, ItemView 的型別無關，所以任意指定即可，
            //    例如：MyGrid<Int, Text>.Layout
            // --------------------------------------------------------------------
            let layout = MyGrid<Int, Text>.Layout(
                idealCellAspectRatio, count: items.count, in: size
            )
            
            rows = layout.rows
            cols = layout.cols
            ratio = layout.cellSize.aspectRatio
        }
        // ⭐️ parent's proposed size
        .frame(size)
        .dimension(.topLeading, arrow: .blue, label: .orange)
        .shadowedBorder()
        .padding(.bottom, 40)
    }
    
    /// view controls
    var controls: some View {
        VStack {
            Text("Cells try to maintain their aspect ratio.")
                .font(.title3)
            Group {
                Text("ideal cell aspect ratio = \(idealCellAspectRatio.decimalPlaces(2))")
                Text("current cell aspect ratio = \(ratio.decimalPlaces(2))")
                Text("rows: \(rows), cols: \(cols)")
            }
            .font(.caption).foregroundColor(.secondary)
            SlidersForSize($size)
        }
    }
}

struct TestMyGrid_Previews: PreviewProvider {
    static var previews: some View {
        TestMyGrid(idealCellAspectRatio: 4/3)
    }
}
```

{% endtab %}
{% endtabs %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lochiwei.gitbook.io/ios/swiftui/view/layout/grids/examples/mygrid.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
