🕵️♂️ SherlockForms
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)
Overview
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
.- Various form cells to automagically interact with
.searchable
, including Text, Button, Toggle, Picker, NavigationLink, etc. - "Copy text" from context menu by long-press
- Various form cells to automagically interact with
SherlockDebugForms
: Useful app/device info-views and helper methods, specifically for debugging purpose.- App Info view
- Device Info view
- UserDefaults Editor
- TODO: File Browser
- TODO: Console Logger
SherlockHUD
: Standalone, simple-to-use Notification View (Toast) UI used inSherlockForms
Examples
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:
- Conform your Settings view to
protocol SherlockView
- Add
@State var searchText: String
to your view - Inside view's
body
, useSherlockForm
(just like normalForm
), and use various built-in form components:- Basic built-in cells
textCell
textFieldCell
textEditorCell
buttonCell
buttonDialogCell
(iOS 15)navigationLinkCell
toggleCell
arrayPickerCell
casePickerCell
datePickerCell
sliderCell
stepperCell
- List
simpleList
nestedList
- More customizable cells (part of
ContainerCell
)hstackCell
vstackCell
- Basic built-in cells
- (Optional) Attach
.formCellCopyable(true)
to each cell or entire form. - (Optional) Attach
.enableSherlockHUD(true)
to topmost view hierarchy to enable HUD
To 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.
Acknowledgement
- DebugMenu by @noppefoxwolf for various useful code in debugging
- swiftui-navigation by @pointfreeco for making smart state-binding techniques in SwiftUI navigation
- Custom HUDs in SwiftUI | FIVE STARS by Federico Zanetello for easy-to-learn SwiftUI HUD development
- @inamiy's Wife for dedicated support during this OSS development