👔ViewPreference
💾 程式:https://github.com/lochiwei/ViewPreferences/tree/main
/*
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) }
}
}
}
}
used by view.actOnSelfWidth().
History
2021.??.?? - first version
2022.01.18 + reportWidth, actOnWidth, actOnSelfWidth, ViewPreference.Width
💾 程式:https://github.com/lochiwei/ViewPreferences/tree/main
// Swift Package import link:
// https://github.com/lochiwei/ViewPreferences.git
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 }
}
// -----------------------
// 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 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
}
}
}
/// 🌀 CGRect + ViewPreference
extension View {
/// ⭐ usage: `view.actOnSelfSize { 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)
}
// ⭐ local abbreviations
typealias FBA = ViewPreference.FirstBoundsAnchor
typealias LBA = ViewPreference.LastBoundsAnchor
// ---------------------------------
// 下層回報資料的「方法命名原則」:
// 1. 如果只收集其中一個,用:report...()
// 2. 如果收集很多個,就用:collect...()
// ----------------------------------
/// ⭐ 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) }
}
}
}
}
Last updated
Was this helpful?