โจSwatch
Last updated
Last updated
ๆฏไธๅผต่ฒ็ฅจ้ฝๆ่ชๅทฑ้ ่จญ็ๅฏฌๅบฆ๏ผไฝๅฆๆ่ฒ็ฅจๅบไธ็ๆๅญ่ถ ้่ฒ็ฅจๅฏฌๅบฆๆ๏ผ่ฒ็ฅจๆ่ชๅๅปถ้ทๅฏฌๅบฆ๏ผ่ฎๆ่ทๆๅญไธๆจฃ้ทใ
โฌ๏ธ ้่ฆ๏ผ ViewPreference, Color+SystemColors
struct Swatch: View {
let color: Color // swatch color
let preferedWidth: CGFloat // prefered swatch width
// โญ state var without initial value
@State private var width: CGFloat // swatch width
var body: some View {
VStack(alignment: .leading, spacing: 4) {
color
// โญ swatch width is flexible (height is fixed)
.frame(width: width, height: preferedWidth)
Text(".\(color.name)") // ๐ Color extension
.fixedSize() // โญ text no wrap
.actOnSelfWidth { // ๐ ViewPreference
// โญ if text width > prefered width, change swatch watch
if $0 > preferedWidth { width = $0 }
}
}
.padding(4)
.border(Color.label3)
}
}
extension Swatch {
// ---------------------------
// โจ init State var โจ
// ---------------------------
init(_ color: Color, preferedWidth width: CGFloat = 60) {
self.color = color
self.preferedWidth = width
// โญ state var initialized in an init
// swatch width = prefered width (at first)
_width = State(initialValue: width)
}
}
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 20){
swatchPad("Label Colors", Color.labelColors)
swatchPad("Fill Colors", Color.fillColors)
swatchPad("Content Backgrounds", Color.backgrounds)
swatchPad("Grouped Content Backgrounds", Color.groupedContentBackgrounds)
}
}
}
extension ContentView {
// a row of Swatches
func swatchPad(
_ title : String,
_ colors: [Color],
spacing : CGFloat? = nil
) -> some View
{
VStack(alignment: .leading) {
Text(title).font(.title)
HStack(spacing: spacing) {
ForEach(colors, id: \.self){
Swatch($0) // ๐ฆ Swatch
}
}
}
}
}
initializes a ๏ผ State variable in an initializer.
Frame - related concept
ViewPreference - required custom type
requires Color+SystemColors.
SwiftUI @State var initialization issue - how to init a State var?
๐ ViewPreference
/*
History:
2021.??.?? - first version
2022.01.18 + reportWidth, actOnWidth, actOnSelfWidth, ViewPreference.Width
*/
import SwiftUI
/// ๐
ฟ๏ธ HasDefaultValue
public protocol HasDefaultValue {
static var defaultValue: Self { get }
}
// ------------------------------------------
// `HasDefaultValue` conforming types
// ------------------------------------------
/// ๐ CGSize
extension CGSize: HasDefaultValue {
public static var defaultValue: Self { .zero }
}
/// ๐ CGRect
extension CGRect: HasDefaultValue {
public static var defaultValue: Self { .zero }
}
/// ๐ CGFloat
extension CGFloat: HasDefaultValue {
public static var defaultValue: Self { .zero }
}
// -----------------------
// ViewPreference
// -----------------------
/// ๐ธ ViewPreference
public enum ViewPreference {
// ๅฐๆ้ bounds anchor ็่ณ่จๆพๅฐ view extension methods ็ๅๆธไธญ๏ผ
// ็ก้่ฎ method name ไธ่ฆๅคช้ทใ
public enum BoundsAnchorType {
case first
case last
}
/// โญ ViewPreference type aliases
public typealias Size = ViewPreference.First<CGSize>
public typealias Width = ViewPreference.First<CGFloat>
/// anchor preferences
public typealias FirstBoundsAnchor = ViewPreference.FirstNonNil<Anchor<CGRect>>
public typealias LastBoundsAnchor = ViewPreference.LastNonNil<Anchor<CGRect>>
/// ๐ธ ViewPreference.First
public enum First<T: HasDefaultValue>: PreferenceKey {
public typealias Value = T
public static var defaultValue: Value { Value.defaultValue }
public static func reduce(value: inout Value, nextValue: () -> Value) {
// โญ ignore all values other than the first
}
}
/// ๐ธ ViewPreference.FirstNonNil<T>
public enum FirstNonNil<T>: PreferenceKey {
public typealias Value = T?
public static var defaultValue: Value { nil }
public static func reduce(value: inout Value, nextValue: () -> Value) {
value = value ?? nextValue() // nil or first non-nil value
}
}
/// ๐ธ ViewPreference.LastNonNil<T>
public enum LastNonNil<T>: PreferenceKey {
public typealias Value = T?
public static var defaultValue: Value { nil }
public static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue() ?? value // nil or last non-nil value
}
}
}
// -----------------------
// View extensions
// -----------------------
/// ๐ CGRect + ViewPreference
extension View {
/// โญ usage: `view.actOnSize { size in ... }`
public func actOnSelfSize(
action: @escaping (CGSize) -> Void
) -> some View
{
// ไธ่ฌ็ view ไธฆไธ็ฅ้่ชๅทฑ็ size ่ณ่จ๏ผไฝไฝฟ็จ๏ผ
// `.background(GeometryReader{ geo in Color.clear })`
// ๅฐฑๅฏไปฅๅจไธๅฝฑ้ฟ่ชๅทฑ็ไฝๅฑ่ๅค่งไธ๏ผ้้ `geo` (GeometryProxy) ็ฉไปถ
// ๅพ็ฅ่ชๅทฑ็ size (geo.size)๏ผ็ถๅพ็จ .preference() ๅๅ ฑไธๅฑคใ
self.background( GeometryReader { geo in
Color.clear
// โญ ไธๅฑคๅๅ ฑ่ชๅทฑ็ size
.preference(key: ViewPreference.Size.self, value: geo.size)
})
// โญ ไธๅฑคๅพ็ฅ size๏ผไธฆ็จ `action` ่็ใ
.onPreferenceChange(ViewPreference.Size.self, perform: action)
}
/// โญ usage: `view.actOnSelfWidth { width in ... }`
public func actOnSelfWidth(
action: @escaping (CGFloat) -> Void
) -> some View
{
self.background( GeometryReader { geo in
Color.clear
// โญ ไธๅฑคๅๅ ฑ่ชๅทฑ็ width
.preference(key: ViewPreference.Width.self, value: geo.size.width)
})
// โญ ไธๅฑคๅพ็ฅ width๏ผไธฆ็จ `action` ่็ใ
.onPreferenceChange(ViewPreference.Width.self, perform: action)
}
// โญ local abbreviations
typealias FBA = ViewPreference.FirstBoundsAnchor
typealias LBA = ViewPreference.LastBoundsAnchor
typealias Width = ViewPreference.Width
// ---------------------------------
// ไธๅฑคๅๅ ฑ่ณๆ็ใๆนๆณๅฝๅๅๅใ๏ผ
// 1. ๅฆๆๅชๆถ้ๅ
ถไธญไธๅ๏ผ็จ๏ผreport...()
// 2. ๅฆๆๆถ้ๅพๅคๅ๏ผๅฐฑ็จ๏ผcollect...()
// ----------------------------------
/// โญ report width
@ViewBuilder public func reportWidth() -> some View {
self.background( GeometryReader { geo in
Color.clear
// โญ ไธๅฑคๅๅ ฑ่ชๅทฑ็ width
.preference(key: ViewPreference.Width.self, value: geo.size.width)
})
}
/// โญ act on width reported
/// usage: `view.actOnWidth { width in ... }`
@ViewBuilder public func actOnWidth(
action: @escaping (CGFloat) -> Void
) -> some View
{
onPreferenceChange(Width.self) { action($0) }
}
/// โญ report bounds anchor (with anchor type)
@ViewBuilder public func report(
bounds anchorType: ViewPreference.BoundsAnchorType
) -> some View {
switch anchorType {
case .first:
// โญ ๅๅ ฑ anchor preference
anchorPreference(key: FBA.self, value: .bounds) { $0 }
// โฐโ 1 โโโฏ โฐโโ2โโโฏ โฐโ3โโโฏ
// 1. ่ฆๅๅ ฑ็ anchor preference (K = FirstBoundsAnchor)
// 2. ่ฆๅๅ ฑ็ anchor ็จฎ้ก (.bounds)
// 3. ๅฐ Anchor<A> ่ฝ็บ่ฆๅๅ ฑ็่ณๆ๏ผ @escaping (Anchor<A>) -> K.Value
// ๅฆๆ Anchor<A> ๆฌ่บซๅฐฑๆฏ่ฆๅๅ ฑ็่ณๆ (K.Value)๏ผ็ดๆฅไฝฟ็จ `$0` ๅฐฑๅฏไปฅใ
case .last:
anchorPreference(key: LBA.self, value: .bounds) { $0 }
}
}
/// โญ report bounds anchor
public func reportBounds() -> some View {
// โญ ๅๅ ฑ anchor preference
anchorPreference(key: FBA.self, value: .bounds) { $0 }
// โฐโ 1 โโโฏ โฐโโ2โโโฏ โฐโ3โโโฏ
// 1. ่ฆๅๅ ฑ็ anchor preference (K = FirstBoundsAnchor)
// 2. ่ฆๅๅ ฑ็ anchor ็จฎ้ก (.bounds)
// 3. ๅฐ Anchor<A> ่ฝ็บ่ฆๅๅ ฑ็่ณๆ๏ผ @escaping (Anchor<A>) -> K.Value
// ๅฆๆ Anchor<A> ๆฌ่บซๅฐฑๆฏ่ฆๅๅ ฑ็่ณๆ (K.Value)๏ผ็ดๆฅไฝฟ็จ `$0` ๅฐฑๅฏไปฅใ
}
/// โญ background by reported bounds (with anchor type)
@ViewBuilder public func background<V: View>(
bounds anchorType: ViewPreference.BoundsAnchorType,
@ViewBuilder content: @escaping (CGRect) -> V
) -> some View
{
switch anchorType {
case .first:
backgroundPreferenceValue(FBA.self) { anchor in
GeometryReader { geo in
if let anchor = anchor { content(geo[anchor]) }
}
}
case .last:
backgroundPreferenceValue(LBA.self) { anchor in
GeometryReader { geo in
if let anchor = anchor { content(geo[anchor]) }
}
}
}
}
/// โญ background by reported bounds
public func backgroundByBounds<V: View>(
@ViewBuilder content: @escaping (CGRect) -> V
) -> some View
{
backgroundPreferenceValue(FBA.self) { anchor in
GeometryReader { geo in
if let anchor = anchor { content(geo[anchor]) }
}
}
}
/// โญ act on bounds reported
@ViewBuilder public func actOnBounds(
_ anchorType: ViewPreference.BoundsAnchorType,
action: @escaping (Anchor<CGRect>) -> Void
) -> some View
{
switch anchorType {
case .first:
onPreferenceChange(FBA.self) { anchor in
if let anchor = anchor { action(anchor) }
}
case .last:
onPreferenceChange(LBA.self) { anchor in
if let anchor = anchor { action(anchor) }
}
}
}
}