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 {