7
7
8
8
import SwiftUI
9
9
10
- #if os(macOS)
11
- import AppKit
12
- typealias PlatformColor = NSColor
13
- #else
14
- import UIKit
15
- typealias PlatformColor = UIColor
16
- #endif
17
-
10
+ /// A simple and cross-platform SFSymbol picker for SwiftUI.
18
11
public struct SymbolPicker : View {
19
12
20
13
// MARK: - Static consts
@@ -25,59 +18,62 @@ public struct SymbolPicker: View {
25
18
26
19
private static var gridDimension : CGFloat {
27
20
#if os(iOS)
28
- return 64
21
+ return 64
29
22
#elseif os(tvOS)
30
- return 128
23
+ return 128
31
24
#elseif os(macOS)
32
- return 30
25
+ return 48
33
26
#else
34
- return 48
27
+ return 48
35
28
#endif
36
29
}
37
30
38
31
private static var symbolSize : CGFloat {
39
32
#if os(iOS)
40
- return 24
33
+ return 24
41
34
#elseif os(tvOS)
42
- return 48
35
+ return 48
43
36
#elseif os(macOS)
44
- return 14
37
+ return 24
45
38
#else
46
- return 24
39
+ return 24
47
40
#endif
48
41
}
49
42
50
43
private static var symbolCornerRadius : CGFloat {
51
44
#if os(iOS)
52
- return 8
45
+ return 8
53
46
#elseif os(tvOS)
54
- return 12
47
+ return 12
55
48
#elseif os(macOS)
56
- return 4
49
+ return 8
57
50
#else
58
- return 8
51
+ return 8
59
52
#endif
60
53
}
61
54
62
- private static var systemGray5 : Color {
63
- dynamicColor (
64
- light: . init( red: 0.9 , green: 0.9 , blue: 0.92 , alpha: 1.0 ) ,
65
- dark: . init( red: 0.17 , green: 0.17 , blue: 0.18 , alpha: 1.0 )
66
- )
55
+ private static var unselectedItemBackgroundColor : Color {
56
+ #if os(iOS)
57
+ return Color ( UIColor . systemBackground)
58
+ #else
59
+ return . clear
60
+ #endif
67
61
}
68
62
69
- private static var systemBackground : Color {
70
- dynamicColor (
71
- light: . init( red: 1 , green: 1 , blue: 1 , alpha: 1.0 ) ,
72
- dark: . init( red: 0 , green: 0 , blue: 0 , alpha: 1.0 )
73
- )
63
+ private static var selectedItemBackgroundColor : Color {
64
+ #if os(tvOS)
65
+ return Color . gray. opacity ( 0.3 )
66
+ #else
67
+ return Color . accentColor
68
+ #endif
74
69
}
75
70
76
- private static var secondarySystemBackground : Color {
77
- dynamicColor (
78
- light: . init( red: 0.95 , green: 0.95 , blue: 1 , alpha: 1.0 ) ,
79
- dark: . init( red: 0 , green: 0 , blue: 0 , alpha: 1.0 )
80
- )
71
+ private static var backgroundColor : Color {
72
+ #if os(iOS)
73
+ return Color ( UIColor . systemGroupedBackground)
74
+ #else
75
+ return . clear
76
+ #endif
81
77
}
82
78
83
79
// MARK: - Properties
@@ -88,6 +84,9 @@ public struct SymbolPicker: View {
88
84
89
85
// MARK: - Public Init
90
86
87
+ /// Initializes `SymbolPicker` with a string binding that captures the raw value of
88
+ /// user-selected SFSymbol.
89
+ /// - Parameter symbol: String binding to store user selection.
91
90
public init ( symbol: Binding < String > ) {
92
91
_symbol = symbol
93
92
}
@@ -97,32 +96,59 @@ public struct SymbolPicker: View {
97
96
@ViewBuilder
98
97
private var searchableSymbolGrid : some View {
99
98
#if os(iOS)
100
- if #available( iOS 15 . 0 , * ) {
99
+ if #available( iOS 15 . 0 , * ) {
100
+ symbolGrid
101
+ . searchable ( text: $searchText, placement: . navigationBarDrawer( displayMode: . always) )
102
+ } else {
103
+ VStack {
104
+ TextField ( LocalizedString ( " search_placeholder " ) , text: $searchText)
105
+ . padding ( 8 )
106
+ . padding ( . horizontal, 8 )
107
+ . background ( Color ( UIColor . systemGray5) )
108
+ . cornerRadius ( 8.0 )
109
+ . padding ( . horizontal, 16.0 )
110
+ . autocapitalization ( . none)
111
+ . disableAutocorrection ( true )
101
112
symbolGrid
102
- . searchable ( text: $searchText, placement: . navigationBarDrawer( displayMode: . always) )
103
- } else {
104
- VStack {
105
- TextField ( LocalizedString ( " search_placeholder " ) , text: $searchText)
106
- . padding ( 8 )
107
- . padding ( . horizontal, 8 )
108
- . background ( Self . systemGray5)
109
- . cornerRadius ( 8.0 )
110
- . padding ( . horizontal, 16.0 )
111
- . autocapitalization ( . none)
112
- . disableAutocorrection ( true )
113
- symbolGrid
114
- . padding ( )
115
- }
113
+ . padding ( )
116
114
}
115
+ }
117
116
#elseif os(tvOS)
117
+ VStack {
118
+ TextField ( LocalizedString ( " search_placeholder " ) , text: $searchText)
119
+ . padding ( . horizontal, 8 )
120
+ . autocapitalization ( . none)
121
+ . disableAutocorrection ( true )
118
122
symbolGrid
119
- . searchable ( text: $searchText, placement: . automatic)
123
+ }
124
+
125
+ /// `searchable` is crashing on tvOS 16. What the hell aPPLE?
126
+ ///
127
+ /// symbolGrid
128
+ /// .searchable(text: $searchText, placement: .automatic)
120
129
#elseif os(macOS)
121
- VStack ( spacing: 10 ) {
130
+ VStack ( spacing: 0 ) {
131
+ HStack {
122
132
TextField ( LocalizedString ( " search_placeholder " ) , text: $searchText)
133
+ . textFieldStyle ( . plain)
134
+ . font ( . system( size: 18.0 ) )
123
135
. disableAutocorrection ( true )
124
- symbolGrid
136
+
137
+ Button {
138
+ presentationMode. wrappedValue. dismiss ( )
139
+ } label: {
140
+ Image ( systemName: " xmark.circle.fill " )
141
+ . resizable ( )
142
+ . frame ( width: 16.0 , height: 16.0 )
143
+ }
144
+ . buttonStyle ( . borderless)
125
145
}
146
+ . padding ( )
147
+
148
+ Divider ( )
149
+
150
+ symbolGrid
151
+ }
126
152
#else
127
153
symbolGrid
128
154
. searchable ( text: $searchText, placement: . automatic)
@@ -133,110 +159,67 @@ public struct SymbolPicker: View {
133
159
ScrollView {
134
160
LazyVGrid ( columns: [ GridItem ( . adaptive( minimum: Self . gridDimension, maximum: Self . gridDimension) ) ] ) {
135
161
ForEach ( Self . symbols. filter { searchText. isEmpty ? true : $0. localizedCaseInsensitiveContains ( searchText) } , id: \. self) { thisSymbol in
136
- Button ( action : {
162
+ Button {
137
163
symbol = thisSymbol
138
-
139
- // Dismiss sheet. macOS will have done button
140
- #if !os(macOS)
141
164
presentationMode. wrappedValue. dismiss ( )
142
- #endif
143
- } ) {
165
+ } label: {
144
166
if thisSymbol == symbol {
145
167
Image ( systemName: thisSymbol)
146
168
. font ( . system( size: Self . symbolSize) )
169
+ #if os(tvOS)
170
+ . frame( minWidth: Self . gridDimension, minHeight: Self . gridDimension)
171
+ #else
147
172
. frame( maxWidth: . infinity, minHeight: Self . gridDimension)
148
- #if !os(tvOS)
149
- . background( Color . accentColor)
150
- #else
151
- . background( Color . gray. opacity ( 0.3 ) )
152
- #endif
173
+ #endif
174
+ . background( Self . selectedItemBackgroundColor)
153
175
. cornerRadius ( Self . symbolCornerRadius)
154
176
. foregroundColor ( . white)
155
177
} else {
156
178
Image ( systemName: thisSymbol)
157
179
. font ( . system( size: Self . symbolSize) )
158
180
. frame ( maxWidth: . infinity, minHeight: Self . gridDimension)
159
- . background ( Self . systemBackground )
181
+ . background ( Self . unselectedItemBackgroundColor )
160
182
. cornerRadius ( Self . symbolCornerRadius)
161
183
. foregroundColor ( . primary)
162
184
}
163
185
}
164
- . buttonStyle ( PlainButtonStyle ( ) )
186
+ . buttonStyle ( . plain)
187
+ #if os(iOS)
188
+ . hoverEffect( . lift)
189
+ #endif
165
190
}
166
191
}
167
192
}
168
193
}
169
194
170
195
public var body : some View {
171
196
#if !os(macOS)
172
- NavigationView {
173
- ZStack {
174
- Self . secondarySystemBackground. edgesIgnoringSafeArea ( . all)
175
- searchableSymbolGrid
176
- }
197
+ NavigationView {
198
+ ZStack {
177
199
#if os(iOS)
178
- . navigationBarTitleDisplayMode ( . inline )
200
+ Self . backgroundColor . edgesIgnoringSafeArea ( . all )
179
201
#endif
180
- . toolbar {
181
- ToolbarItem ( placement: . cancellationAction) {
182
- Button ( LocalizedString ( " cancel " ) ) {
183
- presentationMode. wrappedValue. dismiss ( )
184
- }
185
- }
186
- }
187
- }
188
- . navigationViewStyle ( StackNavigationViewStyle ( ) )
189
- #else
190
- VStack ( alignment: . leading, spacing: 10 ) {
191
- Text ( LocalizedString ( " sf_symbol_picker " ) )
192
- . font ( . headline)
193
- Divider ( )
194
202
searchableSymbolGrid
195
- . frame ( maxWidth: . infinity, maxHeight: . infinity)
196
- Divider ( )
197
- HStack {
198
- Button {
199
- symbol = " "
203
+ }
204
+ #if os(iOS)
205
+ . navigationBarTitleDisplayMode( . inline)
206
+ #endif
207
+ #if !os(tvOS)
208
+ /// tvOS can use back button on remote
209
+ . toolbar {
210
+ ToolbarItem ( placement: . cancellationAction) {
211
+ Button ( LocalizedString ( " cancel " ) ) {
200
212
presentationMode. wrappedValue. dismiss ( )
201
- } label: {
202
- Text ( LocalizedString ( " cancel " ) )
203
- }
204
- . keyboardShortcut ( . cancelAction)
205
- Spacer ( )
206
- Button {
207
- presentationMode. wrappedValue. dismiss ( )
208
- } label: {
209
- Text ( LocalizedString ( " done " ) )
210
213
}
211
214
}
212
215
}
213
- . padding ( )
214
- . frame ( width: 520 , height: 300 , alignment: . center)
215
- #endif
216
- }
217
-
218
- // MARK: - Private helpers
219
-
220
- private static func dynamicColor( light: PlatformColor , dark: PlatformColor ) -> Color {
221
- #if os(iOS)
222
- let color = PlatformColor { $0. userInterfaceStyle == . dark ? dark : light }
223
- if #available( iOS 15 . 0 , * ) {
224
- return Color ( uiColor: color)
225
- } else {
226
- return Color ( color)
227
- }
228
- #elseif os(tvOS)
229
- let color = PlatformColor { $0. userInterfaceStyle == . dark ? dark : light }
230
- return Color ( uiColor: color)
231
- #elseif os(macOS)
232
- let color = PlatformColor ( name: nil ) { $0. name == . darkAqua ? dark : light }
233
- if #available( macOS 12 . 0 , * ) {
234
- return Color ( nsColor: color)
235
- } else {
236
- return Color ( color)
237
- }
216
+ #endif
217
+ }
218
+ . navigationViewStyle ( . stack)
238
219
#else
239
- return Color ( uiColor: dark)
220
+ searchableSymbolGrid
221
+ . frame ( width: 540 , height: 320 , alignment: . center)
222
+ . background ( . regularMaterial)
240
223
#endif
241
224
}
242
225
0 commit comments