-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy path09-CustomComponents.swift
110 lines (97 loc) · 2.75 KB
/
09-CustomComponents.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import SwiftUI
import SwiftUINavigation
private let readMe = """
This case study demonstrates how to enhance an existing SwiftUI component so that it can be \
driven off of optional and enum state.
The BottomMenuModifier component in this is file is primarily powered by a simple boolean \
binding, which means its content cannot be dynamic based off of the source of truth that drives \
its presentation, and it cannot make mutations to the source of truth.
However, by leveraging the binding transformations that come with this library we can extend the \
bottom menu component with additional APIs that allow presentation and dismissal to be powered \
by optionals and enums.
"""
struct CustomComponents: View {
@State var count: Int?
var body: some View {
Form {
Section {
Text(readMe)
}
Button("Show bottom menu") {
withAnimation {
count = 0
}
}
if let count = count, count > 0 {
Text("Current count: \(count)")
.transition(.opacity)
}
}
.bottomMenu(item: $count) { $count in
Stepper("Number: \(count)", value: $count.animation())
}
.navigationTitle("Custom components")
}
}
private struct BottomMenuModifier<BottomMenuContent>: ViewModifier
where BottomMenuContent: View {
@Binding var isActive: Bool
let content: () -> BottomMenuContent
func body(content: Content) -> some View {
content.overlay(
ZStack(alignment: .bottom) {
if isActive {
Rectangle()
.fill(Color.black.opacity(0.4))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
withAnimation {
isActive = false
}
}
.zIndex(1)
.transition(.opacity)
self.content()
.padding()
.background(Color.white)
.cornerRadius(10)
.frame(maxWidth: .infinity)
.padding(24)
.padding(.bottom)
.zIndex(2)
.transition(.move(edge: .bottom))
}
}
.ignoresSafeArea()
)
}
}
extension View {
fileprivate func bottomMenu<Content>(
isActive: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content
) -> some View
where Content: View {
modifier(
BottomMenuModifier(
isActive: isActive,
content: content
)
)
}
fileprivate func bottomMenu<Item, Content>(
item: Binding<Item?>,
@ViewBuilder content: @escaping (Binding<Item>) -> Content
) -> some View
where Content: View {
modifier(
BottomMenuModifier(
isActive: Binding(item),
content: { Binding(unwrapping: item).map(content) }
)
)
}
}
#Preview {
CustomComponents()
}