Skip to content

Commit

Permalink
refine remove symbol UI
Browse files Browse the repository at this point in the history
  • Loading branch information
xnth97 committed Aug 12, 2023
1 parent 882dba0 commit 34f118e
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 69 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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+

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"cancel" = "Cancel";
"sf_symbol_picker" = "Select a symbol";
"done" = "Done";
"none" = "None";
"remove_symbol" = "Remove Symbol";
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
"cancel" = "キャンセル";
"sf_symbol_picker" = "シンボルを選択";
"done" = "完了";
"remove_symbol" = "シンボルを削除";
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
"cancel" = "取消";
"sf_symbol_picker" = "选择符号";
"done" = "确定";
"remove_symbol" = "移除符号";
140 changes: 77 additions & 63 deletions Sources/SymbolPicker/SymbolPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,54 +79,43 @@ 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

/// Initializes `SymbolPicker` with a string binding that captures the raw value of
/// 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

@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)
Expand All @@ -149,7 +138,7 @@ public struct SymbolPicker: View {
.disableAutocorrection(true)

Button {
presentationMode.wrappedValue.dismiss()
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.resizable()
Expand All @@ -162,6 +151,16 @@ public struct SymbolPicker: View {
Divider()

symbolGrid

if canDeleteIcon {
Divider()
HStack {
Spacer()
deleteButton
.padding(.horizontal)
.padding(.vertical, 8.0)
}
}
}
#else
symbolGrid
Expand All @@ -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)
Expand Down Expand Up @@ -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))
}
}

Expand All @@ -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)
Expand All @@ -256,7 +266,7 @@ public struct SymbolPicker: View {
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(LocalizedString("cancel")) {
presentationMode.wrappedValue.dismiss()
dismiss()
}
}
}
Expand All @@ -265,19 +275,23 @@ 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 {
NSLocalizedString(key, bundle: .module, comment: "")
}

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 {
Expand Down

0 comments on commit 34f118e

Please sign in to comment.