โจselected button with underline
Last updated
Last updated
SwiftUI โฉ Data Flow โฉ View Preferences โฉ selected button with underline
ๆฌไพไฝฟ็จ Anchor Preferences ่ Matched Geometry Effect ไพ้ๅฐๅๆจฃ็ๆๆใ
โญ๏ธ ๅพๆญคไพๅฏ็ๅบ๏ผ
"source view" ๆๆฏ matched geometry effect ็ใ็งปๅ็ฎๆจใ๏ผๅ
ถไป non-source view (isSource
:
false
) ้ฝๆ็งปๅๅฐ source view ๆๅจ็ไฝ็ฝฎ (frame)ใ
ๆญคไพ็ non-source view ้ฝๆฏ overlay๏ผๅฏไปฅ็ๅบๅฎๅ็ใๅๅฑค้ไฟใ๏ผ
็ฌฌไธๅ button (World) ๆไธๅ overlay๏ผๆไปฅๅนพไน็ไธๅฐใ
็ฌฌไบๅ button (Alarm) ๆๅ ฉๅ overlay๏ผๆไปฅๅฏไปฅ็ๅฐไธไบ๏ผ่ไธ็ฌฌไธๅ button ็ overlay ไฝๅจ็ฌฌไบๅ button ไนไธใ
็ฌฌไธๅ button (Bedtime) ๅชๆไธๅ overlay๏ผๆไปฅๅฏไปฅๅพๆธ ๆฅ็ๅบไพใ
โฌ๏ธ ้่ฆ๏ผ Text+, StackForEach
// 2022.02.24
// 2022.02.25 (r) + different colors/heights to reveal non-source views.
import SwiftUI
import PlaygroundSupport
import CustomViews
PlaygroundPage.current.setLiveView(ContentView())
// ----------------------------------------
// ๐ธ Bounds (Anchor PreferenceKey)
// ----------------------------------------
private enum Bounds: PreferenceKey {
public typealias Value = Anchor<CGRect>?
public static var defaultValue: Value { nil }
public static func reduce(value: inout Value, nextValue: () -> Value) {
value = value ?? nextValue() // โญ๏ธ first non-nil (if any)
}
}
// ๐ ็ตฆ่จญๅฎ่่ชฟ็จ PreferenceKey ็ๆนๆณ็จ็น็ๅๅญ๏ผ้
ๅ็จๅผ็ขผ็็ฎ็๏ผ
// ๅฏๆ้ซ็จๅผ็ขผ็ๅฏ่ฎๆงใ
private extension View {
/// ๐ธ set selected bounds for views
func setSelectedBounds(
at index: Int,
selected: @escaping (Int) -> Bool
) -> some View
{
anchorPreference(key: Bounds.self, value: .bounds) {
// โญ๏ธ tranform `anchor` to nil if not `selected`
anchor in selected(index) ? anchor : nil
}
}
/// ๐ธ underline the selected bounds
func underlineSelectedBounds() -> some View {
backgroundPreferenceValue(Bounds.self) { anchor in
GeometryReader { proxy in
let bounds = proxy[anchor!] // โญ๏ธ force-unwrap is OKโ๏ธ
Color.green
.frame(width: bounds.width, height: 1)
.offset(x: bounds.minX, y: bounds.height)
}
}
}
}
// -------------------
// ContentView
// -------------------
struct ContentView: View {
// โญ๏ธ current index
@State private var selected = 0
// โญ๏ธ for matched geometry effect
@Namespace private var ns
var body: some View {
VStack {
Text("**Anchor Preference**")
.font(.title3)
.foregroundColor(.secondary)
buttons1
.padding()
.border(.white.opacity(0.3))
Text("**Matched Geometry Effect**")
.font(.title3)
.foregroundColor(.secondary)
buttons2
.padding()
.border(.white.opacity(0.3))
}
}
}
extension ContentView {
/// tabs
var tabs: [Text] {
[
label("World", "globe.asia.australia", .blue),
label("Alarm", "alarm", .pink),
label("Bedtime", "bed.double.fill", .orange),
]
}
/// button for tab i
func button(_ i: Int) -> some View {
Button {
withAnimation {
self.selected = i // โญ๏ธ set current index on click
}
} label: {
self.tabs[i]
}
.foregroundColor(.white)
}
/// custom label
func label(_ text: String, _ symbol: String, _ color: Color) -> Text {
Text(symbol: symbol).foregroundColor(color) + Text(text)
}
// -------------------------
// anchor preference
// -------------------------
var buttons1: some View {
HStackForEach(tabs.indices) { i in
button(i)
// โญ๏ธ set selected bounds
.setSelectedBounds(at: i) { self.selected == $0 }
}
// โญ๏ธ underline "the selected bounds"
.underlineSelectedBounds()
}
// -------------------------------
// matched geometry effect
// -------------------------------
var buttons2: some View {
HStackForEach(tabs.indices) { i in
button(i)
// โญ๏ธ source view
// โ "source view" is the "target" for matched geometry effectโ
// โ๏ธtotally confusedโ๏ธ
.matchedGeometryEffect(id: i, in: ns)
.overlay {
colors[i]
.opacity(0.9)
.frame(height: 10.0 * (3-i), alignment: .bottom)
.shadow(radius: 4)
// โญ๏ธ non-source view
.matchedGeometryEffect(id: selected, in: ns, isSource: false)
}
}
}
var colors: [Color] {
[.red, .green, .blue]
}
}
Thinking in SwiftUI, Ch.5 - Layout, Anchors (p.100)
example of Anchor Preferences.
can be done by Matched Geometry Effect.
dynamic underline - use ๏ผ State var.
2022.02.24
import SwiftUI
import PlaygroundSupport
import CustomViews
PlaygroundPage.current.setLiveView(ContentView())
/// ๐ธ Bounds (Anchor PreferenceKey)
private enum Bounds: PreferenceKey {
public typealias Value = Anchor<CGRect>?
public static var defaultValue: Value { nil }
public static func reduce(value: inout Value, nextValue: () -> Value) {
value = value ?? nextValue() // โญ๏ธ first non-nil (if any)
}
}
struct ContentView: View {
// โญ๏ธ current index
@State private var selected = 0
var body: some View {
HStackForEach(tabs.indices) { i in
Button {
self.selected = i // โญ๏ธ set current index on click
} label: {
self.tabs[i]
}
.foregroundColor(.white)
// โญ๏ธ set anchor preference (.bounds) for each button
.anchorPreference(key: Bounds.self, value: .bounds) {
// โญ๏ธ tranform `anchor` to nil if not `selected`
anchor in self.selected == i ? anchor : nil
}
}
// โญ๏ธ set background based on "the selected bounds"
.backgroundPreferenceValue(Bounds.self) { anchor in
GeometryReader { proxy in
let bounds = proxy[anchor!] // โญ๏ธ force unwrapโ๏ธ
Color.indigo
.frame(width: bounds.width, height: 4)
.offset(x: bounds.minX, y: bounds.height - 2)
.animation(.default)
}
}
.padding()
.border(.white.opacity(0.3))
}
}
extension ContentView {
var tabs: [Text] {
[ Text("World Clock"), Text("Alarm"), Text("Bedtime") ]
}
}