From 34f118e2a4712e3ceef05481b9f15ab1e890bc37 Mon Sep 17 00:00:00 2001 From: Yubo Qin <xnth97@live.com> Date: Fri, 11 Aug 2023 19:24:48 -0700 Subject: [PATCH] refine remove symbol UI --- Package.swift | 4 +- README.md | 6 +- .../Resources/en.lproj/Localizable.strings | 2 +- .../Resources/ja.lproj/Localizable.strings | 1 + .../Resources/zh_CN.lproj/Localizable.strings | 1 + Sources/SymbolPicker/SymbolPicker.swift | 140 ++++++++++-------- 6 files changed, 85 insertions(+), 69 deletions(-) diff --git a/Package.swift b/Package.swift index 183a2f2..1c7c1ed 100644 --- a/Package.swift +++ b/Package.swift @@ -7,9 +7,9 @@ let package = Package( name: "SymbolPicker", defaultLocalization: "en", platforms: [ - .iOS(.v14), + .iOS(.v15), .macOS(.v12), - .tvOS(.v14), + .tvOS(.v15), .watchOS(.v8), ], products: [ diff --git a/README.md b/README.md index 4698f7d..2014307 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A simple and cross-platform SFSymbol picker for SwiftUI ## Features -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, watchOS and visionOS platforms. +SymbolPicker provides a simple and cross-platform interface for picking a SFSymbol. SymbolPicker is implemented with SwiftUI and supports iOS, macOS, tvOS, watchOS and visionOS platforms. ![](/Screenshots/demo.png) @@ -21,7 +21,7 @@ Please use [xrOS branch](https://github.com/xnth97/SymbolPicker/tree/xrOS) for v ### Requirements -* iOS 14.0+ / macOS 12.0+ / tvOS 14.0+ / watchOS 8.0+ / visionOS 1.0+ +* iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+ / visionOS 1.0+ * Xcode 13.0+ * Swift 5.0+ @@ -68,8 +68,8 @@ struct ContentView: View { - [ ] Categories support - [x] Multiplatform support - [x] Platform availability support -- [ ] Inline UI - [ ] Codegen from latest SF Symbols +- [x] Nullable symbol ## License diff --git a/Sources/SymbolPicker/Resources/en.lproj/Localizable.strings b/Sources/SymbolPicker/Resources/en.lproj/Localizable.strings index c114459..ce2dfe3 100644 --- a/Sources/SymbolPicker/Resources/en.lproj/Localizable.strings +++ b/Sources/SymbolPicker/Resources/en.lproj/Localizable.strings @@ -2,4 +2,4 @@ "cancel" = "Cancel"; "sf_symbol_picker" = "Select a symbol"; "done" = "Done"; -"none" = "None"; +"remove_symbol" = "Remove Symbol"; diff --git a/Sources/SymbolPicker/Resources/ja.lproj/Localizable.strings b/Sources/SymbolPicker/Resources/ja.lproj/Localizable.strings index d956f2d..aa67cff 100644 --- a/Sources/SymbolPicker/Resources/ja.lproj/Localizable.strings +++ b/Sources/SymbolPicker/Resources/ja.lproj/Localizable.strings @@ -2,3 +2,4 @@ "cancel" = "キャンセル"; "sf_symbol_picker" = "シンボルを選択"; "done" = "完了"; +"remove_symbol" = "シンボルを削除"; diff --git a/Sources/SymbolPicker/Resources/zh_CN.lproj/Localizable.strings b/Sources/SymbolPicker/Resources/zh_CN.lproj/Localizable.strings index cfee937..d682e48 100644 --- a/Sources/SymbolPicker/Resources/zh_CN.lproj/Localizable.strings +++ b/Sources/SymbolPicker/Resources/zh_CN.lproj/Localizable.strings @@ -2,3 +2,4 @@ "cancel" = "取消"; "sf_symbol_picker" = "选择符号"; "done" = "确定"; +"remove_symbol" = "移除符号"; diff --git a/Sources/SymbolPicker/SymbolPicker.swift b/Sources/SymbolPicker/SymbolPicker.swift index 26ee4d7..47c01d3 100644 --- a/Sources/SymbolPicker/SymbolPicker.swift +++ b/Sources/SymbolPicker/SymbolPicker.swift @@ -79,9 +79,10 @@ public struct SymbolPicker: View { // MARK: - Properties @Binding public var symbol: String? - private let canBeNone: Bool @State private var searchText = "" - @Environment(\.presentationMode) private var presentationMode + @Environment(\.dismiss) private var dismiss + + private let nullable: Bool // MARK: - Public Init @@ -89,20 +90,23 @@ public struct SymbolPicker: View { /// user-selected SFSymbol. /// - Parameter symbol: String binding to store user selection. public init(symbol: Binding<String>) { - _symbol = Binding(get: { + _symbol = Binding { return symbol.wrappedValue - }, set: { newValue in - /// As the `canBeNone` is set to `false`, this can not be `nil` + } set: { newValue in + /// As the `nullable` is set to `false`, this can not be `nil` if let newValue { symbol.wrappedValue = newValue } - }) - canBeNone = false + } + nullable = false } - + + /// Initializes `SymbolPicker` with a nullable string binding that captures the raw value of + /// user-selected SFSymbol. `nil` if no symbol is selected. + /// - Parameter symbol: Optional string binding to store user selection. public init(symbol: Binding<String?>) { _symbol = symbol - canBeNone = true + nullable = true } // MARK: - View Components @@ -110,23 +114,8 @@ public struct SymbolPicker: View { @ViewBuilder private var searchableSymbolGrid: some View { #if os(iOS) - if #available(iOS 15.0, *) { - symbolGrid - .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) - } else { - VStack { - TextField(LocalizedString("search_placeholder"), text: $searchText) - .padding(8) - .padding(.horizontal, 8) - .background(Color(UIColor.systemGray5)) - .cornerRadius(8.0) - .padding(.horizontal, 16.0) - .autocapitalization(.none) - .disableAutocorrection(true) - symbolGrid - .padding(.top) - } - } + symbolGrid + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) #elseif os(tvOS) VStack { TextField(LocalizedString("search_placeholder"), text: $searchText) @@ -149,7 +138,7 @@ public struct SymbolPicker: View { .disableAutocorrection(true) Button { - presentationMode.wrappedValue.dismiss() + dismiss() } label: { Image(systemName: "xmark.circle.fill") .resizable() @@ -162,6 +151,16 @@ public struct SymbolPicker: View { Divider() symbolGrid + + if canDeleteIcon { + Divider() + HStack { + Spacer() + deleteButton + .padding(.horizontal) + .padding(.vertical, 8.0) + } + } } #else symbolGrid @@ -171,43 +170,17 @@ public struct SymbolPicker: View { private var symbolGrid: some View { ScrollView { + #if os(tvOS) || os(watchOS) + if canDeleteIcon { + deleteButton + } + #endif + LazyVGrid(columns: [GridItem(.adaptive(minimum: Self.gridDimension, maximum: Self.gridDimension))]) { - // The `None` option - if canBeNone { - Button { - symbol = nil - presentationMode.wrappedValue.dismiss() - } label: { - if symbol == nil { - Text(LocalizedString("none")) - .font(.headline) -#if os(tvOS) - .frame(minWidth: Self.gridDimension, minHeight: Self.gridDimension) -#else - .frame(maxWidth: .infinity, minHeight: Self.gridDimension) -#endif - .background(Self.selectedItemBackgroundColor) - .cornerRadius(Self.symbolCornerRadius) - .foregroundColor(.white) - } else { - Text(LocalizedString("none")) - .font(.headline) - .frame(maxWidth: .infinity, minHeight: Self.gridDimension) - .background(Self.unselectedItemBackgroundColor) - .cornerRadius(Self.symbolCornerRadius) - .foregroundColor(.primary) - } - } - .buttonStyle(.plain) - #if os(iOS) - .hoverEffect(.lift) - #endif - } - // The actual symbols ForEach(Self.symbols.filter { searchText.isEmpty ? true : $0.localizedCaseInsensitiveContains(searchText) }, id: \.self) { thisSymbol in Button { symbol = thisSymbol - presentationMode.wrappedValue.dismiss() + dismiss() } label: { if thisSymbol == symbol { Image(systemName: thisSymbol) @@ -236,6 +209,31 @@ public struct SymbolPicker: View { } } .padding(.horizontal) + + #if os(iOS) + /// Avoid last row being hidden. + if canDeleteIcon { + Spacer() + .frame(height: Self.gridDimension * 2) + } + #endif + } + } + + private var deleteButton: some View { + Button(role: .destructive) { + symbol = nil + dismiss() + } label: { + Label(LocalizedString("remove_symbol"), systemImage: "trash") + #if !os(tvOS) && !os(macOS) + .frame(maxWidth: .infinity) + #endif + #if !os(watchOS) + .padding(.vertical, 12.0) + #endif + .background(Self.unselectedItemBackgroundColor) + .clipShape(RoundedRectangle(cornerRadius: 12.0, style: .continuous)) } } @@ -247,6 +245,18 @@ public struct SymbolPicker: View { Self.backgroundColor.edgesIgnoringSafeArea(.all) #endif searchableSymbolGrid + + #if os(iOS) + if canDeleteIcon { + VStack { + Spacer() + + deleteButton + .padding() + .background(.regularMaterial) + } + } + #endif } #if os(iOS) .navigationBarTitleDisplayMode(.inline) @@ -256,7 +266,7 @@ public struct SymbolPicker: View { .toolbar { ToolbarItem(placement: .cancellationAction) { Button(LocalizedString("cancel")) { - presentationMode.wrappedValue.dismiss() + dismiss() } } } @@ -265,11 +275,15 @@ public struct SymbolPicker: View { .navigationViewStyle(.stack) #else searchableSymbolGrid - .frame(width: 540, height: 320, alignment: .center) + .frame(width: 540, height: 340, alignment: .center) .background(.regularMaterial) #endif } + private var canDeleteIcon: Bool { + nullable && symbol != nil + } + } private func LocalizedString(_ key: String) -> String { @@ -277,7 +291,7 @@ private func LocalizedString(_ key: String) -> String { } struct SymbolPicker_Previews: PreviewProvider { - @State static var symbol: String = "square.and.arrow.up" + @State static var symbol: String? = "square.and.arrow.up" static var previews: some View { Group {