# 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-and-subtypes](../../../../../swift/type/category/generic-types/generics-and-subtypes "mention")❗️
{% 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-less-than-c-greater-than](../../cannot-convert-value-of-type-self-to-expected-argument-type-binding-less-than-c-greater-than "mention")
{% endhint %}
{% endtab %}

{% tab title="👥 相關" %}
* 本例由 [grid](grid "mention") 改編而來。
* similar to [..](.. "mention") layout.
* `MyGrid.Layout` is [nested-types](../../../../../swift/type/category/nested-types "mention") in [generics-and-subtypes](../../../../../swift/type/category/generic-types/generics-and-subtypes "mention").
* code could cause  ⛔️ <mark style="color:red;">Error</mark>:  [generic-parameter-...-could-not-be-inferred.](mygrid/generic-parameter-...-could-not-be-inferred. "mention")
* [to-nest-or-not-to-nest](../../../../../swift/type/category/nested-types/to-nest-or-not-to-nest "mention") - question about [nested-types](../../../../../swift/type/category/nested-types "mention").
* revised to show the [problem-with-.readsize](problem-with-.readsize "mention").  🐞
{% endtab %}
{% endtabs %}

## Files

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

````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](../../../../../custom/package/geometrykit/vector2d "mention"), [floating-+-int](../../../../../swift/type/category/basic/numbers/floating-point/floating-+-int "mention")

````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](../../../../../custom/control/slidersforsize "mention")

```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 %}
