🕵️♂️ An elegant SwiftUI Form builder to create a searchable Settings and DebugMenu screens for iOS.
What one man can invent Settings UI, another can discover its field.
– Sherlock Forms
An elegant SwiftUI Form builder to create a searchable Settings and DebugMenu screens for iOS.
(Supports from iOS 14, except .searchable
works from iOS 15)
Normal | Searching | Context Menu |
---|---|---|
UserDefaults | App Info | Device Info |
---|---|---|
This repository consists of 3 modules:
SherlockForms
: SwiftUI Form builder to enhance cell findability using iOS 15 .searchable
.
.searchable
, including Text, Button, Toggle, Picker, NavigationLink, etc.SherlockDebugForms
: Useful app/device info-views and helper methods, specifically for debugging purpose.
SherlockHUD
: Standalone, simple-to-use Notification View (Toast) UI used in SherlockForms
SherlockForms
& SherlockDebugForms
From SherlockForms-Gallery app:
import SwiftUI
import SherlockDebugForms
/// NOTE: Each view that owns `SherlockForm` needs to conform to `SherlockView` protocol.
@MainActor
struct RootView: View, SherlockView
{
/// NOTE:
/// `searchText` is required for `SherlockView` protocol.
/// This is the only requirement to define as `@State`, and pass it to `SherlockForm`.
@State public var searchText: String = ""
@AppStorage("username")
private var username: String = "John Appleseed"
@AppStorage("language")
private var languageSelection: Int = 0
@AppStorage("status")
private var status = Constant.Status.online
... // Many more @AppStorage properties...
var body: some View
{
// NOTE:
// `SherlockForm` and `xxxCell` are where all the search magic is happening!
// Just treat `SherlockForm` as a normal `Form`, and use `Section` and plain SwiftUI views accordingly.
SherlockForm(searchText: $searchText) {
// Simple form cells.
Section {
textCell(title: "User", value: username)
arrayPickerCell(title: "Language", selection: $languageSelection, values: Constant.languages)
casePickerCell(title: "Status", selection: $status)
toggleCell(title: "Low Power Mode", isOn: $isLowPowerOn)
sliderCell(
title: "Speed",
value: $speed,
in: 0.5 ... 2.0,
step: 0.1,
maxFractionDigits: 1,
valueString: { "x\($0)" },
sliderLabel: { EmptyView() },
minimumValueLabel: { Image(systemName: "tortoise") },
maximumValueLabel: { Image(systemName: "hare") },
onEditingChanged: { print("onEditingChanged", $0) }
)
stepperCell(
title: "Font Size",
value: $fontSize,
in: 8 ... 24,
step: 1,
maxFractionDigits: 0,
valueString: { "\($0) pt" }
)
}
// Navigation Link Cell (`navigationLinkCell`)
Section {
navigationLinkCell(
title: "UserDefaults",
destination: { UserDefaultsListView() }
)
navigationLinkCell(
title: "App Info",
destination: { AppInfoView() }
)
navigationLinkCell(
title: "Device Info",
destination: { DeviceInfoView() }
)
navigationLinkCell(title: "Custom Page", destination: {
CustomView()
})
}
// Buttons
Section {
buttonCell(
title: "Reset UserDefaults",
action: {
Helper.deleteUserDefaults()
showHUD(.init(message: "Finished resetting UserDefaults"))
}
)
buttonDialogCell(
title: "Delete All Contents",
dialogTitle: nil,
dialogButtons: [
.init(title: "Delete All Contents", role: .destructive) {
try await deleteAllContents()
showHUD(.init(message: "Finished deleting all contents"))
},
.init(title: "Cancel", role: .cancel) {
print("Cancelled")
}
]
)
}
}
.navigationTitle("Settings")
// NOTE:
// Use `formCopyable` here to allow ALL `xxxCell`s to be copyable.
.formCopyable(true)
}
}
To get started:
protocol SherlockView
@State var searchText: String
to your viewbody
, use SherlockForm
(just like normal Form
), and use various built-in form components:
textCell
textFieldCell
textEditorCell
buttonCell
buttonDialogCell
(iOS 15)navigationLinkCell
toggleCell
arrayPickerCell
casePickerCell
datePickerCell
sliderCell
stepperCell
simpleList
nestedList
ContainerCell
)
hstackCell
vstackCell
.formCellCopyable(true)
to each cell or entire form..enableSherlockHUD(true)
to topmost view hierarchy to enable HUDTo customize cell’s internal content view rather than cell itself,
use .formCellContentModifier
which may solve some troubles (e.g. context menu) when customizing cells.
SherlockHUD
import SwiftUI
import SherlockHUD
@main
struct MyApp: App
{
var body: some Scene
{
WindowGroup {
NavigationView {
RootView()
}
.enableSherlockHUD(true) // Set at the topmost view!
}
}
}
@MainActor
struct RootView: View
{
/// Attaching `.enableSherlockHUD(true)` to topmost view will allow using `showHUD`.
@Environment(\.showHUD)
private var showHUD: (HUDMessage) -> Void
var body: some View
{
VStack(spacing: 16) {
Button("Tap") {
showHUD(HUDMessage(message: "Hello SherlockForms!", duration: 2, alignment: .top))
// alignment = top / center / bottom (default)
// Can also attach custom view e.g. ProgressView. See also `HUDMessage.loading`.
}
}
.font(.largeTitle)
}
}
See SherlockHUD-Demo app for more information.