โจMyGrid
Last updated
Last updated
โญ๏ธ ๆณจๆ๏ผ๐ MyGrid
<
Item, ItemView
>
ๆฏไธๅ Generic Typesโ๏ธ
ไฝฟ็จ generic type ็ nested type ่ฆๅฐๅฟโ๏ธ
๐ MyGrid (line: 44)๏ผๅจ MyGrid ๅ ง้จไฝฟ็จ Layout ๅบๆฌไธๆฒไป้บผๅ้ก
๐๏ธ TestMyGrid (line: 52)๏ผๅจ MyGrid ๅค้ขไฝฟ็จ Layout ๅฐฑ่ฆๅฐๅฟไบ๏ผๅฆๆ่ฉฒ่กๅฏซๆไธ้ข้ๆจฃ๏ผ
let layout = MyGrid.Layout( ... )
้ฃๅฐฑๆๅบ็พไธๅๅ้ก๏ผ
ๆฌไพ็ฑ Grid ๆน็ทจ่ไพใ
similar to Grids layout.
MyGrid.Layout
is nested types in Generic Types.
code could cause โ๏ธ Error: Generic parameter '...' could not be inferred.
to nest or not to nestโ - question about nested types.
revised to show the problem with .readSize(). ๐
โฌ๏ธ ้่ฆ๏ผ arr.index(of:)
// 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
}
โฌ๏ธ ้่ฆ๏ผ Vector2D, floating +โโจรท int
// 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))
}
}
โฌ๏ธ ้่ฆ๏ผ SlidersForSize
// 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)
}
}