Skip to content

Commit 27091f8

Browse files
committed
v1.4
1 parent 6ce1613 commit 27091f8

File tree

5 files changed

+118
-133
lines changed

5 files changed

+118
-133
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ let package = Package(
99
platforms: [
1010
.iOS(.v14),
1111
.macOS(.v12),
12-
.tvOS(.v15),
12+
.tvOS(.v14),
1313
.watchOS(.v8),
1414
],
1515
products: [

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ A simple and cross-platform SFSymbol picker for SwiftUI
77

88
## Features
99

10-
SymbolPicker provides a simple and cross-platform interface for picking a SFSymbol with search functionality that is backported to iOS 14. SymbolPicker is implemented with SwiftUI and supports iOS, macOS, tvOS and watchOS platforms.
10+
SymbolPicker provides a simple and cross-platform interface for picking a SFSymbol with search functionality that is backported to iOS and tvOS 14. SymbolPicker is implemented with SwiftUI and supports iOS, macOS, tvOS and watchOS platforms.
1111

1212
![](/Screenshots/demo.png)
1313

1414
## Usage
1515

1616
### Requirements
1717

18-
* iOS 14.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+
18+
* iOS 14.0+ / macOS 12.0+ / tvOS 14.0+ / watchOS 8.0+
1919
* Xcode 13.0+
2020
* Swift 5.0+
2121

@@ -25,7 +25,7 @@ SymbolPicker is available as a Swift Package. Add this repo to your project thro
2525

2626
```swift
2727
dependencies: [
28-
.package(url: "https://github.com/xnth97/SymbolPicker.git", .upToNextMajor(from: "1.1.0"))
28+
.package(url: "https://github.com/xnth97/SymbolPicker.git", .upToNextMajor(from: "1.4.0"))
2929
]
3030
```
3131

@@ -42,9 +42,9 @@ struct ContentView: View {
4242
@State private var icon = "pencil"
4343

4444
var body: some View {
45-
Button(action: {
45+
Button {
4646
iconPickerPresented = true
47-
}) {
47+
} label: {
4848
HStack {
4949
Image(systemName: icon)
5050
Text(icon)
@@ -61,6 +61,7 @@ struct ContentView: View {
6161

6262
- [ ] Categories support
6363
- [x] Multiplatform support
64+
- [x] Platform availability support
6465
- [ ] Inline UI
6566
- [ ] Codegen from latest SF Symbols
6667

Screenshots/demo.png

-128 KB
Loading

Sources/SymbolPicker/SymbolPicker.swift

Lines changed: 109 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,7 @@
77

88
import SwiftUI
99

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.
1811
public struct SymbolPicker: View {
1912

2013
// MARK: - Static consts
@@ -25,59 +18,62 @@ public struct SymbolPicker: View {
2518

2619
private static var gridDimension: CGFloat {
2720
#if os(iOS)
28-
return 64
21+
return 64
2922
#elseif os(tvOS)
30-
return 128
23+
return 128
3124
#elseif os(macOS)
32-
return 30
25+
return 48
3326
#else
34-
return 48
27+
return 48
3528
#endif
3629
}
3730

3831
private static var symbolSize: CGFloat {
3932
#if os(iOS)
40-
return 24
33+
return 24
4134
#elseif os(tvOS)
42-
return 48
35+
return 48
4336
#elseif os(macOS)
44-
return 14
37+
return 24
4538
#else
46-
return 24
39+
return 24
4740
#endif
4841
}
4942

5043
private static var symbolCornerRadius: CGFloat {
5144
#if os(iOS)
52-
return 8
45+
return 8
5346
#elseif os(tvOS)
54-
return 12
47+
return 12
5548
#elseif os(macOS)
56-
return 4
49+
return 8
5750
#else
58-
return 8
51+
return 8
5952
#endif
6053
}
6154

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
6761
}
6862

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
7469
}
7570

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
8177
}
8278

8379
// MARK: - Properties
@@ -88,6 +84,9 @@ public struct SymbolPicker: View {
8884

8985
// MARK: - Public Init
9086

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.
9190
public init(symbol: Binding<String>) {
9291
_symbol = symbol
9392
}
@@ -97,32 +96,59 @@ public struct SymbolPicker: View {
9796
@ViewBuilder
9897
private var searchableSymbolGrid: some View {
9998
#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)
101112
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()
116114
}
115+
}
117116
#elseif os(tvOS)
117+
VStack {
118+
TextField(LocalizedString("search_placeholder"), text: $searchText)
119+
.padding(.horizontal, 8)
120+
.autocapitalization(.none)
121+
.disableAutocorrection(true)
118122
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)
120129
#elseif os(macOS)
121-
VStack(spacing: 10) {
130+
VStack(spacing: 0) {
131+
HStack {
122132
TextField(LocalizedString("search_placeholder"), text: $searchText)
133+
.textFieldStyle(.plain)
134+
.font(.system(size: 18.0))
123135
.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)
125145
}
146+
.padding()
147+
148+
Divider()
149+
150+
symbolGrid
151+
}
126152
#else
127153
symbolGrid
128154
.searchable(text: $searchText, placement: .automatic)
@@ -133,110 +159,67 @@ public struct SymbolPicker: View {
133159
ScrollView {
134160
LazyVGrid(columns: [GridItem(.adaptive(minimum: Self.gridDimension, maximum: Self.gridDimension))]) {
135161
ForEach(Self.symbols.filter { searchText.isEmpty ? true : $0.localizedCaseInsensitiveContains(searchText) }, id: \.self) { thisSymbol in
136-
Button(action: {
162+
Button {
137163
symbol = thisSymbol
138-
139-
// Dismiss sheet. macOS will have done button
140-
#if !os(macOS)
141164
presentationMode.wrappedValue.dismiss()
142-
#endif
143-
}) {
165+
} label: {
144166
if thisSymbol == symbol {
145167
Image(systemName: thisSymbol)
146168
.font(.system(size: Self.symbolSize))
169+
#if os(tvOS)
170+
.frame(minWidth: Self.gridDimension, minHeight: Self.gridDimension)
171+
#else
147172
.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)
153175
.cornerRadius(Self.symbolCornerRadius)
154176
.foregroundColor(.white)
155177
} else {
156178
Image(systemName: thisSymbol)
157179
.font(.system(size: Self.symbolSize))
158180
.frame(maxWidth: .infinity, minHeight: Self.gridDimension)
159-
.background(Self.systemBackground)
181+
.background(Self.unselectedItemBackgroundColor)
160182
.cornerRadius(Self.symbolCornerRadius)
161183
.foregroundColor(.primary)
162184
}
163185
}
164-
.buttonStyle(PlainButtonStyle())
186+
.buttonStyle(.plain)
187+
#if os(iOS)
188+
.hoverEffect(.lift)
189+
#endif
165190
}
166191
}
167192
}
168193
}
169194

170195
public var body: some View {
171196
#if !os(macOS)
172-
NavigationView {
173-
ZStack {
174-
Self.secondarySystemBackground.edgesIgnoringSafeArea(.all)
175-
searchableSymbolGrid
176-
}
197+
NavigationView {
198+
ZStack {
177199
#if os(iOS)
178-
.navigationBarTitleDisplayMode(.inline)
200+
Self.backgroundColor.edgesIgnoringSafeArea(.all)
179201
#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()
194202
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")) {
200212
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"))
210213
}
211214
}
212215
}
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)
238219
#else
239-
return Color(uiColor: dark)
220+
searchableSymbolGrid
221+
.frame(width: 540, height: 320, alignment: .center)
222+
.background(.regularMaterial)
240223
#endif
241224
}
242225

0 commit comments

Comments
 (0)