Skip to content

Commit

Permalink
Created DebugToggle property wrapper (#10)
Browse files Browse the repository at this point in the history
* [1] Defined basic structure for Debug menu

- Created DebugMenuStore as global data source
- Defined Toggle, Button, Submenu actions

* - Made shared instance of DebugMenuStore public
- Public init for DebugToggleAction

* - DebugMenuView init made public

* - Added support for submenu action
- Navigation titles
- Clean up protocol
- Move structs to their own files

* - Created BaseDebugDataSource for convenience

* - Updated example project

* - Addressed PR comments

* - Fix example app

* - Introduced DebugAction protocol
- Removed DebugActionType enum
- Removed Binding to datasource for submenu action

* - Fixed example project

* - Removed debug menu store so app side can implement single global store

* - Fix example app

* - UserDefault property wrapper implemented
- DebugToggleAction now just takes a Binding

* WIP reset to default

* - Added common options flag to data source
- Added protocol for DebugResettable and button to reset values to default

* - Added defaultValue to DebugResettable

* - New init for DebugToggleRow that takes in UserDefault property wrapper

* - Debug Toggle Property Wrapper

* - Updated example app

* - Reset to defaults moved to data source method
  • Loading branch information
AZielinsky95 authored Mar 1, 2022
1 parent 9501e2c commit 33cf23f
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 35 deletions.
4 changes: 4 additions & 0 deletions DebugMenu/DebugMenu/BaseDebugDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ open class BaseDebugDataSource: DebugMenuDataSource {
public func addActions(_ actions: [DebugAction]) {
self.actions.append(contentsOf: actions)
}

open var includeCommonOptions: Bool {
false
}
}
12 changes: 11 additions & 1 deletion DebugMenu/DebugMenu/DebugMenuDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@

import Foundation

public protocol DebugMenuDataSource: AnyObject {
public protocol DebugMenuDataSource: ObservableObject {
var navigationTitle: String { get }
var actions: [DebugAction] { get }
func addAction(_ action: DebugAction)
func addActions(_ actions: [DebugAction])
func resetToDefaults()
var includeCommonOptions: Bool { get }
}

extension DebugMenuDataSource {
public func resetToDefaults() {
for action in self.actions {
(action as? DebugResettable)?.resetToDefault()
}
}
}
24 changes: 21 additions & 3 deletions DebugMenu/DebugMenu/DebugMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import SwiftUI
import Combine

public struct DebugMenuView: View {
public struct DebugMenuView<DataSource>: View where DataSource: DebugMenuDataSource {

private var dataSource: DebugMenuDataSource
@ObservedObject private var dataSource: DataSource

public init(dataSource: DebugMenuDataSource) {
public init(dataSource: DataSource) {
self.dataSource = dataSource
}

Expand All @@ -24,13 +24,31 @@ public struct DebugMenuView: View {
ForEach(0..<options.count) { index in
options[index]
}
if dataSource.includeCommonOptions {
commonOptions()
}
}
.navigationBarTitle(Text(dataSource.navigationTitle), displayMode: .inline)

} else {
Text("No debug options set!")
.font(.system(size: 20, weight: .semibold))
.navigationBarTitle(Text(dataSource.navigationTitle), displayMode: .inline)
}
}
}

@ViewBuilder
func commonOptions() -> some View {
Section(header: Text("Common")) {
DebugButtonAction(title: "Reset To Default Values", action: {
dataSource.resetToDefaults()
}).asAnyView
}
}
}

public protocol DebugResettable {
var defaultValue: Bool { get }
func resetToDefault()
}
4 changes: 2 additions & 2 deletions DebugMenu/DebugMenu/DebugSubmenuButtonRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import SwiftUI

public struct DebugSubmenuAction: DebugAction {
let title: String
let dataSource: DebugMenuDataSource
let dataSource: BaseDebugDataSource

public var asAnyView: AnyView {
AnyView(DebugSubmenuButtonRow(action: self))
}

public init(title: String, dataSource: DebugMenuDataSource) {
public init(title: String, dataSource: BaseDebugDataSource) {
self.title = title
self.dataSource = dataSource
}
Expand Down
102 changes: 102 additions & 0 deletions DebugMenu/DebugMenu/DebugToggle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// DebugToggle.swift
//
//
// Created by Alejandro Zielinsky on 2021-12-02.
//

import SwiftUI
import Combine

@propertyWrapper
public struct DebugToggle<Value> {

public let displayTitle: String
public let defaultValue: Value
public let storage: UserDefaults
public let key: String?

private var value: Value

@available(*, unavailable)
public var wrappedValue: Value {
get { fatalError("only works on instance properties of classes") }
set { fatalError("only works on instance properties of classes") }
}

public var projectedValue: Self {
self
}

public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
) -> Value {
get {
let propertyWrapper = object[keyPath: storageKeyPath]
if let key = propertyWrapper.key {
return propertyWrapper.storage.object(forKey: key) as? Value ?? propertyWrapper.defaultValue
} else {
return propertyWrapper.value
}
}
set {
(object.objectWillChange as? ObservableObjectPublisher)?.send()
let propertyWrapper = object[keyPath: storageKeyPath]
if let key = propertyWrapper.key {
if let optional = newValue as? AnyOptional, optional.isNil {
propertyWrapper.storage.removeObject(forKey: key)
} else {
propertyWrapper.storage.set(newValue, forKey: key)
}
} else {
object[keyPath: storageKeyPath].value = newValue
}
}
}

public init(wrappedValue: Value,
key: String,
storage: UserDefaults = .standard) {
self.displayTitle = key.camelCaseToWords().replacingOccurrences(of: "Key", with: "")
self.defaultValue = wrappedValue
self.key = key
self.storage = storage
self.value = wrappedValue
}

public init(wrappedValue: Value,
title: String,
key: String? = nil,
storage: UserDefaults = .standard) {
self.displayTitle = title
self.defaultValue = wrappedValue
self.storage = storage
self.key = key
self.value = wrappedValue
}
}

public extension DebugToggle where Value: ExpressibleByNilLiteral {
init(title: String) {
self.init(wrappedValue: nil, title: title)
}
}

private protocol AnyOptional {
var isNil: Bool { get }
}

private extension String {
func camelCaseToWords() -> String {
return unicodeScalars.reduce("") {
if CharacterSet.uppercaseLetters.contains($1) {
return ($0.capitalized + " " + String($1))
}
else {
return $0.capitalized + String($1)
}
}
}
}
32 changes: 14 additions & 18 deletions DebugMenu/DebugMenu/DebugToggleRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,37 @@
//

import SwiftUI
import Combine

public protocol DebugAction {
var asAnyView: AnyView { get }
}

public struct DebugToggleAction: DebugAction {
public struct DebugToggleAction: DebugAction, DebugResettable {

let displayTitle: String
let key: String
let onToggleComplete: ((Bool) -> Void)?
let userDefaults: UserDefaults
let toggle: Binding<Bool>
public private(set) var defaultValue: Bool

public var asAnyView: AnyView {
AnyView(DebugToggleRow(action: self))
}

public init(title: String, userDefaultsKey: String? = nil, onToggleComplete: ((Bool) -> Void)? = nil, userDefaults: UserDefaults = .standard) {
public init(title: String, toggle: Binding<Bool>) {
self.displayTitle = title
self.key = userDefaultsKey ?? title.trimmingCharacters(in: .whitespaces).lowercased()
self.onToggleComplete = onToggleComplete
self.userDefaults = userDefaults
self.toggle = toggle
self.defaultValue = toggle.wrappedValue
}

public func resetToDefault() {
toggle.wrappedValue = defaultValue
}
}

public struct DebugToggleRow: View {
struct DebugToggleRow: View {
let action: DebugToggleAction

public var body: some View {
Toggle(action.displayTitle, isOn: binding(for: action.key, onToggleComplete: action.onToggleComplete))
}

func binding(for key: String, onToggleComplete: ((Bool) -> Void)? = nil) -> Binding<Bool> {
Binding { action.userDefaults.bool(forKey: key) } set: { value in
action.userDefaults.set(value, forKey: key)
onToggleComplete?(value)
}
var body: some View {
Toggle(action.displayTitle, isOn: action.toggle)
}
}
2 changes: 1 addition & 1 deletion DebugMenu/DebugMenuExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "file:///Users/alejandrozielinsky/iOSProjects/debug-menu-ios";
requirement = {
branch = "az/password-protection";
branch = "az/userdefault-property-wrapper";
kind = branch;
};
};
Expand Down
39 changes: 29 additions & 10 deletions DebugMenu/DebugMenuExample/DebugMenuStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,40 @@

import Foundation
import DebugMenu
import SwiftUI
import Combine

public class DebugMenuStore: BaseDebugDataSource {


@DebugToggle(key: "debugForceFooKey") var debugForceFoo = false
@DebugToggle(title: "Show All Foos", key: "showFoosKey") var showAllFoos = false
@DebugToggle(title: "In Memory Flag") var inMemoryFlag = false

public static let shared = DebugMenuStore()

init() {
let testToggle = DebugToggleAction(title: "Test Toggle", userDefaultsKey: "testKey")
let anotherToggle = DebugToggleAction(title: "Another Toggle",
userDefaultsKey: "secondKey",
onToggleComplete: { value in print("Toggled! \(value)")})
super.init()
buildDebugMenu()
}

func buildDebugMenu() {
let testButton = DebugButtonAction(title: "Test Button", action: { print("Button Tapped") })
let testSubmenu = DebugSubmenuAction(title: "Test submenu", dataSource: TestDataSource())
super.init(actions: [testToggle,
anotherToggle,
testButton,
testSubmenu])

let debugForceFooAction = DebugToggleAction(title: $debugForceFoo.displayTitle, toggle: Binding(get: { self.debugForceFoo }, set: { self.debugForceFoo = $0 }))
let showFoosAction = DebugToggleAction(title: $showAllFoos.displayTitle, toggle: Binding(get: { self.showAllFoos }, set: { self.showAllFoos = $0 }))
let inMemoryAction = DebugToggleAction(title: $inMemoryFlag.displayTitle, toggle: Binding(get: { self.inMemoryFlag }, set: { self.inMemoryFlag = $0 }))

self.addActions([
testButton,
testSubmenu,
debugForceFooAction,
showFoosAction,
inMemoryAction
])
}

public override var includeCommonOptions: Bool {
true
}
}

0 comments on commit 33cf23f

Please sign in to comment.