๐Ÿ“ฆGridLayout

ๅ”ๅŠฉ ๐ŸŒ… Grid ่จˆ็ฎ—ๆœ€ไฝณ็š„ไฝˆๅฑ€ๆ–นๅผ๏ผš

็•ถๆ”ถๅˆฐไพ†่‡ชๆฏ่ฆ–ไปถ (้€šๅธธๆ˜ฏ GeometryReader) ๆ‰€ๆ่ญฐ็š„ไฝˆๅฑ€ๅคงๅฐ (proposed size) ๆ™‚๏ผŒGridLayout ๆœƒๆ นๆ“šไฝฟ็”จ่€…ๆ‰€ๅธŒๆœ›็š„ๆ ผๅญๆฏ”ไพ‹ (cellAspectRatio)๏ผŒ่‡ชๅ‹•่จˆ็ฎ—ๅ‡บๆœ€ไฝณ็š„่กŒๆ•ธ (cols) ่ˆ‡ๅˆ—ๆ•ธ (rows)๏ผŒไธฆไธ”ๅฏไปฅๆไพ›ๆ ผๅญ็š„ๅคงๅฐ (cellSize) ่ˆ‡ๆฏๅ€‹ๆ ผๅญ็š„ไธญๅฟƒ้ปžไฝ็ฝฎ (centerOfCell(at: index))ใ€‚

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))
    }
    
}

Last updated