🖼️Swatch_0
Last updated
Was this helpful?
Last updated
Was this helpful?
swift⟩ custom ⟩ extension ⟩ Color ⟩ system ⟩ Swatch
每一張色票都有自己預設的寬度,但如果色票底下的文字超過色票寬度時,色票會自動延長寬度,變成跟文字一樣長。
⬆️ 需要: ViewPreference, system colors
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
}
}
}
}
}
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) }
}
}
}
}