Skip to content

Commit

Permalink
Debuggable Enum parameters (#46)
Browse files Browse the repository at this point in the history
Adds a general solution for debuggable enum parameters.

Requires the enum conforms to `CaseIterable, CustomDebugStringConvertible, Hashable`.
Uses `debugDescription` for the string displayed in the debug view picker.

Shows a picker for selecting enum options in the debug menu.

![Kapture 2022-03-07 at 10 24 41](https://user-images.githubusercontent.com/609274/157094905-806e76ff-ef02-47bf-914d-3b6a724a52a4.gif)
  • Loading branch information
Malcolm Jarvis authored Mar 8, 2022
1 parent e193122 commit ed68c81
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
D40AF62927D682B7006A5763 /* CustomSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40AF62827D682B7006A5763 /* CustomSegmentedControl.swift */; };
D40AF62A27D682B7006A5763 /* CustomSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40AF62827D682B7006A5763 /* CustomSegmentedControl.swift */; };
D4E48B5827C702A800A8D8F0 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E48B4927C702A600A8D8F0 /* ExampleApp.swift */; };
D4E48B5927C702A800A8D8F0 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E48B4927C702A600A8D8F0 /* ExampleApp.swift */; };
D4E48B5A27C702A800A8D8F0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E48B4A27C702A600A8D8F0 /* ContentView.swift */; };
Expand All @@ -26,6 +28,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
D40AF62827D682B7006A5763 /* CustomSegmentedControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSegmentedControl.swift; sourceTree = "<group>"; };
D42B337A27BD7E710071C18E /* CustomDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDatePicker.swift; sourceTree = "<group>"; };
D45C4ED327B1CE5000BA8515 /* */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ""; sourceTree = "<group>"; };
D4B3610827B18839001F5E20 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -86,6 +89,7 @@
D4B3610827B18839001F5E20 /* CustomButton.swift */,
D4C4975227B451950061244C /* CustomToggle.swift */,
D42B337A27BD7E710071C18E /* CustomDatePicker.swift */,
D40AF62827D682B7006A5763 /* CustomSegmentedControl.swift */,
D4E48B4927C702A600A8D8F0 /* ExampleApp.swift */,
D4E48B4A27C702A600A8D8F0 /* ContentView.swift */,
D4E48B4B27C702A800A8D8F0 /* Assets.xcassets */,
Expand Down Expand Up @@ -253,6 +257,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D40AF62927D682B7006A5763 /* CustomSegmentedControl.swift in Sources */,
D4E48B6927C7039400A8D8F0 /* CustomButton.swift in Sources */,
D4E48B5A27C702A800A8D8F0 /* ContentView.swift in Sources */,
D4E48B6B27C7039400A8D8F0 /* CustomDatePicker.swift in Sources */,
Expand All @@ -266,6 +271,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D40AF62A27D682B7006A5763 /* CustomSegmentedControl.swift in Sources */,
D4E48B6C27C7039400A8D8F0 /* CustomButton.swift in Sources */,
D4E48B5B27C702A800A8D8F0 /* ContentView.swift in Sources */,
D4E48B6E27C7039400A8D8F0 /* CustomDatePicker.swift in Sources */,
Expand Down
38 changes: 38 additions & 0 deletions Example/CustomSegmentedControl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Exhibition
import SwiftUI

struct CustomSegmentedControl: View {
let title: String
@Binding var selection: Option

var body: some View {
Picker(title, selection: $selection) {
ForEach(Option.allCases, id: \.rawValue) { option in
Text(option.rawValue).tag(option)
}
}
.pickerStyle(SegmentedPickerStyle())
}

enum Option: String, CaseIterable, CustomDebugStringConvertible {
case first = "first"
case second = "second"
case third = "third"

var debugDescription: String {
rawValue
}
}
}

struct CustomSegmentedControl_Previews: ExhibitProvider, PreviewProvider {
static var exhibit: Exhibit = Exhibit(
name: "CustomSegmentedControl",
section: "Pickers"
) { context in
CustomSegmentedControl(
title: context.parameter(name: "title", defaultValue: "Title"),
selection: context.parameter(name: "selection", defaultValue: .first)
)
}
}
3 changes: 3 additions & 0 deletions Example/Exhibition.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import Exhibition
import SwiftUI

public struct Exhibition: View {
public init() {}

public var body: some View {
NavigationView {
ExhibitListView(
exhibits: [
CustomButton_Previews.anyExhibit,
CustomDatePicker_Previews.anyExhibit,
CustomSegmentedControl_Previews.anyExhibit,
CustomToggle_Previews.anyExhibit,
]
)
Expand Down
1 change: 1 addition & 0 deletions Sources/Exhibition/ExhibitListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public struct ExhibitListView: View {
.parameterView(IntParameterView.self)
.parameterView(DateParameterView.self)
.parameterView(ClosureParameterView.self)
.parameterView(EnumParameterView.self)
}

private var searchResults: [AnyExhibit] {
Expand Down
68 changes: 68 additions & 0 deletions Sources/Exhibition/ParameterViews/EnumParameterView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import SwiftUI

public extension Context {
/// Helper enum to avoid repetition of protocols below
typealias EnumType = CaseIterable & CustomDebugStringConvertible & Hashable

/// Create a constant parameter for a selectable Enum
/// - Parameters:
/// - name: The debug description name for the parameter.
/// - defaultValue: The initial case for the enum.
/// - Returns: The currently selected case of the enum
func parameter<E: EnumType>(name: String, defaultValue: E) -> E {
let parameter: EnumParameter = parameter(
name: name,
defaultValue: EnumParameter(value: defaultValue)
)

return parameter.current as! E
}

/// Create a binding parameter for a selectable enum
/// - Parameters:
/// - name: The debug description name for the parameter.
/// - defaultValue: The initial case for the enum.
/// - Returns: A binding for the currently selected case of the enum
func parameter<E: EnumType>(name: String, defaultValue: E) -> Binding<E> {
let parameter: Binding<EnumParameter> = parameter(
name: name,
defaultValue: EnumParameter(value: defaultValue)
)

return Binding(
get: { parameter.wrappedValue.current as! E },
set: { parameter.wrappedValue.current = $0 }
)
}
}

/// A parameter representing a selectable Enum
struct EnumParameter {
var current: AnyHashable
let cases: [String: AnyHashable]

init<E: Context.EnumType>(value: E) {
self.current = value
self.cases = Dictionary(uniqueKeysWithValues: E.allCases.map {
($0.debugDescription, $0)
})
}

func value<E>() -> E? {
return current as? E
}
}

/// Debug parameter row for `EnumParameter`
struct EnumParameterView: ParameterView {
let key: String
@Binding var value: EnumParameter

public var body: some View {
Picker(key, selection: $value.current) {
ForEach(value.cases.sorted(by: keyAscending), id: \.0) { (key, value) in
Text(key).tag(value)
}
}
}
}

0 comments on commit ed68c81

Please sign in to comment.