# GridLayout

{% hint style="info" %}
協助 🌅 [](<> "mention") 計算**最佳的佈局方式**：

當收到來自<mark style="color:red;">**母視件**</mark> (通常是 [GeometryReader](../../../../measure/geometryreader)) 所提議的佈局大小 (<mark style="color:red;">**proposed size**</mark>) 時，<mark style="color:purple;">**GridLayout**</mark> 會根據使用者所希望的<mark style="color:orange;">**格子比例**</mark> (<mark style="color:purple;">**cellAspectRatio**</mark>)，自動計算出最佳的<mark style="color:orange;">**行數**</mark> (<mark style="color:purple;">**cols**</mark>) 與<mark style="color:orange;">**列數**</mark> (<mark style="color:purple;">**rows**</mark>)，並且可以提供<mark style="color:orange;">**格子的大小**</mark> (<mark style="color:purple;">**cellSize**</mark>) 與<mark style="color:orange;">**每個格子的中心點**</mark>**位置** (<mark style="color:purple;">**centerOfCell(at: index)**</mark>)。
{% endhint %}

{% tabs %}
{% tab title="📦 GridLayout" %}
```swift
import SwiftUI
import Extensions   // for 🌀CGSize+aspectRatio, 🌀Int+cgfloat

// 📦 GridLayout
public struct GridLayout {
    
    let size: CGSize                 // size proposed by parent view
    private(set) var rows: Int = 0   // best layout in rows x cols
    private(set) var cols: Int = 0   // (to be calculated in init)
    
    public init(
        itemCount n: Int,               // number of items
        in     size: CGSize,            // proposed size
        nearAspectRatio r: CGFloat = 1  // desired cell aspect ratio
    ) {
        
        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 desiredAspectRatio
        var bestLayout: (rows: Int, cols: Int) = (1, n)
        var smallestVariance: CGFloat?

        // start to find
        for rows in 1...n {
            
            // calculate how many columns we need
            let cols = (n / rows) + (n % rows > 0 ? 1 : 0)
            
            // cellAspectRatio = (width/cols) : (height/rows)
            //                 = size.aspectRatio * rows/cols
            // -----------------------------------------------------------
            // ⭐️ because rows is strictly increasing, cols is decreasing,
            //    cellAspectRatio will be strictly increasing.
            // -----------------------------------------------------------
            // - size.aspectRatio: 🌀CGSize + aspectRatio
            // - row.cgfloat     : 🌀Int + cgfloat
            let cellAspectRatio = size.aspectRatio * (rows.cgfloat/cols.cgfloat)
            
            // current variance with desired aspect ratio
            let variance = self.variance(cellAspectRatio, r)
            
            // if variance is not set, or 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  // break for loop
            }
        }
        
        // record best layout found
        rows = bestLayout.rows
        cols = bestLayout.cols
    }
    
    /* ------- Public Methods ------- */
    
    // cell size
    public var cellSize: CGSize {
        (rows == 0 || cols == 0) ? .zero :
            CGSize(
                width : size.width  / CGFloat(cols),
                height: size.height / CGFloat(rows)
            )
    }
    
    // center point of cell at index
    public func centerOfCell(at index: Int) -> CGPoint {
        CGPoint(
            x: (CGFloat(index % cols) + 0.5) * cellSize.width,
            y: (CGFloat(index / cols) + 0.5) * cellSize.height
        )
    }
    
    /* ------- Helper Methods ------- */
    
    // 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="⬇️ 應用" %}
* [](<> "mention") - help calculate best layout.
{% endtab %}

{% tab title="👥  相關" %}
* helps [](<> "mention") to calculate the best layout.
* [Emoji Memory Game 📚](../../../../../../master/todo/tutorials/emoji-game)
* [SwiftUI's Grid Views](https://www.objc.io/blog/2020/11/23/grid-layout) - objc.io
* [grids](../../../../view/grids "mention")
{% endtab %}
{% endtabs %}
