Skip to content

Instantly share code, notes, and snippets.

@cjnevin
Last active November 4, 2022 05:22
Show Gist options
  • Save cjnevin/b10022e54279bd9e93ab9208ffca3af3 to your computer and use it in GitHub Desktop.
Save cjnevin/b10022e54279bd9e93ab9208ffca3af3 to your computer and use it in GitHub Desktop.
import UIKit
struct FormEmptyError: Error {}
struct FormCompleteError: Error {}
protocol FormProtocol {
var isValid: Bool { get }
var elements: [any FormElementProtocol] { get }
mutating func update()
}
protocol FormElementProtocol {
associatedtype WrappedValue
var id: String { get }
var wrappedValue: WrappedValue { get set }
var isValid: Bool { get }
var isVisible: Bool { get set }
func setValue(_ value: Any?)
}
@propertyWrapper
struct FormElement<T>: FormElementProtocol {
class Storage {
var value: T
init(value: T) {
self.value = value
}
}
private let storage: Storage
let id: String
var wrappedValue: T {
get { storage.value }
set { storage.value = newValue }
}
var isVisible: Bool
let requirement: (T) -> Bool
init(
_ value: T,
id: String,
isVisible: Bool = true,
requirement: @escaping (T) -> Bool = { _ in true }
) {
self.id = id
self.storage = .init(value: value)
self.isVisible = isVisible
self.requirement = requirement
}
var isValid: Bool {
isVisible ? requirement(wrappedValue) : true
}
func setValue(_ value: Any?) {
if let newValue = value as? T {
storage.value = newValue
} else {
assertionFailure("Value was incorrect type: \(value), expected: \(T.self)")
}
}
}
struct FormWizard<Form: FormProtocol> {
var form: Form
var currentElement: any FormElementProtocol
init(_ form: Form) {
var mutable = form
mutable.update()
self.form = mutable
currentElement = mutable.elements[0]
}
var currentIndex: Int? {
form.elements.firstIndex { $0.id == currentElement.id }
}
mutating func next() throws {
guard let index = currentIndex else { throw FormEmptyError() }
let nextIndex = form.elements.index(after: index)
guard form.elements.indices.contains(nextIndex) else {
throw FormCompleteError()
}
guard form.elements[nextIndex].isVisible else {
currentElement = form.elements[nextIndex]
print("skipped \(currentElement.id)")
return try next()
}
currentElement = form.elements[nextIndex]
}
mutating func setValue(_ newValue: Any?) {
currentElement.setValue(newValue)
form.update()
}
}
struct SmartForm: FormProtocol {
@FormElement(nil, id: "choice", requirement: notEmpty)
var choice: String?
var choiceId: String { _choice.id }
@FormElement([], id: "choices", requirement: count(1...3))
var choices: [String]
var choicesId: String { _choices.id }
@FormElement(nil, id: "text", requirement: notEmpty)
var text: String?
var textId: String { _text.id }
mutating func update() {
_text.isVisible = choices.contains("Other")
if !_text.isVisible {
text = nil
}
}
var elements: [any FormElementProtocol] {
[_choice, _choices, _text]
}
var isValid: Bool {
_choice.isValid && _choices.isValid && _text.isValid
}
}
private func count(_ range: ClosedRange<Int>) -> ([String]) -> Bool {
{ elements in range ~= elements.count }
}
private func notEmpty(_ string: String?) -> Bool {
string != nil && string?.isEmpty == false
}
protocol FormWorkerDelegate: AnyObject {
associatedtype Form: FormProtocol
func setAssociatedValues(for id: String, values: Any)
func setFormValidity(_ isValid: Bool)
func showElement(_ element: any FormElementProtocol)
func didFinish(form: Form)
}
class FormWorker<Form: FormProtocol, Delegate: FormWorkerDelegate> where Delegate.Form == Form {
weak var delegate: Delegate?
private var wizard: FormWizard<Form>
init(_ form: Form) {
wizard = .init(form)
}
func start(at index: Int) {
wizard.currentElement = wizard.form.elements[index]
start()
}
func start() {
showCurrentElement()
}
func next() {
do {
print(wizard.currentElement.id, "->", "next")
try wizard.next()
showCurrentElement()
} catch {
delegate?.didFinish(form: wizard.form)
}
}
func setCurrentValue(_ value: Any?) {
wizard.setValue(value)
print(wizard.currentElement.id, "->", "set value", value, "[ valid = \(wizard.currentElement.isValid) ]")
delegate?.setFormValidity(wizard.form.isValid)
}
func fetchChoiceItems() {
delegate?.setAssociatedValues(for: "choice", values: ["Single", "One"])
}
func fetchChoicesItems() {
delegate?.setAssociatedValues(for: "choices", values: ["Multi", "Many", "Other"])
}
private func showCurrentElement() {
delegate?.showElement(wizard.currentElement)
switch wizard.currentElement.id {
case "choice": fetchChoiceItems()
case "choices": fetchChoicesItems()
default: break
}
}
}
let worker = FormWorker<SmartForm, SmartFormInteractor>(SmartForm())
let interactor = SmartFormInteractor()
worker.delegate = interactor
protocol SmartFormInteractorDelegate: AnyObject {
func setFormIsIncomplete(_ isIncomplete: Bool)
func showChoices(_ choices: Choices)
func showText(_ text: String?)
func didFinishForm()
}
class SmartFormPresenter: SmartFormInteractorDelegate {
func setFormIsIncomplete(_ isIncomplete: Bool) {
print("[presenter] form \(isIncomplete ? "incomplete" : "complete")")
}
func showChoices(_ choices: Choices) {
print("[presenter] show choices")
}
func showText(_ text: String?) {
print("[presenter] show text")
}
func didFinishForm() {
print("[presenter] did finish")
}
}
let presenter = SmartFormPresenter()
interactor.delegate = presenter
class SmartFormInteractor: FormWorkerDelegate {
weak var delegate: SmartFormInteractorDelegate?
var associatedValues: [String: Any] = [:]
var currentElement: (any FormElementProtocol)?
func setAssociatedValues(for id: String, values: Any) {
associatedValues[id] = values
if let element = currentElement, id == element.id {
showElement(element)
}
}
func setFormValidity(_ isValid: Bool) {
print("form [ valid = \(isValid) ]")
delegate?.setFormIsIncomplete(!isValid)
}
func showElement(_ element: any FormElementProtocol) {
switch (
element.id,
element.wrappedValue,
associatedValues[element.id]
) {
case (
"choice",
let value as String?,
let choices as [String]?
):
if let choices = choices {
delegate?.showChoices(Choices(
selected: value,
all: choices
))
}
print("choice", "<-", value, "from", choices ?? "loading...", "[ valid = \(element.isValid) ]")
case (
"choices",
let values as [String],
let choices as [String]?
):
if let choices = choices {
delegate?.showChoices(Choices(
selected: values,
all: choices
))
}
print("choices", "<-", values, "from", choices ?? "loading...", "[ valid = \(element.isValid) ]")
case (
"text",
let value as String?,
.none
):
print("text", "<-", value, "[ valid = \(element.isValid) ]")
delegate?.showText(value)
default:
break
}
currentElement = element
}
func didFinish(form: SmartForm) {
print("finished")
dump(form)
delegate?.didFinishForm()
}
}
worker.start()
worker.setCurrentValue("Single")
worker.next()
worker.setCurrentValue(["Many", "Other"])
worker.next()
worker.setCurrentValue("Description")
worker.next()
worker.start(at: 0)
worker.setCurrentValue("")
worker.setCurrentValue("One")
worker.start(at: 1)
worker.setCurrentValue(["Many"])
worker.next()
struct Choices {
struct Choice {
let text: String
let selected: Bool
}
let multiSelect: Bool
let items: [Choice]
init(selected: String?, all: [String]) {
multiSelect = false
items = all.map {
.init(text: $0, selected: $0 == selected)
}
}
init(selected: [String], all: [String]) {
multiSelect = true
items = all.map {
.init(text: $0, selected: selected.contains($0))
}
}
}
@cjnevin
Copy link
Author

cjnevin commented Nov 3, 2022

choice <- nil from loading... [ valid = false ]
[presenter] show choices
choice <- nil from ["Single", "One"] [ valid = false ]
choice -> set value Optional("Single") [ valid = true ]
form [ valid = false ]
[presenter] form incomplete
choice -> next
choices <- [] from loading... [ valid = false ]
[presenter] show choices
choices <- [] from ["Multi", "Many", "Other"] [ valid = false ]
choices -> set value Optional(["Many", "Other"]) [ valid = true ]
form [ valid = false ]
[presenter] form incomplete
choices -> next
text <- nil [ valid = false ]
[presenter] show text
text -> set value Optional("Description") [ valid = true ]
form [ valid = true ]
[presenter] form complete
text -> next
finished
▿ __lldb_expr_1.SmartForm
  ▿ _choice: __lldb_expr_1.FormElement<Swift.Optional<Swift.String>>
    ▿ storage: __lldb_expr_1.FormElement<Swift.Optional<Swift.String>>.Storage #0
      ▿ value: Optional("Single")
        - some: "Single"
    - id: "choice"
    - isVisible: true
    - requirement: (Function)
  ▿ _choices: __lldb_expr_1.FormElement<Swift.Array<Swift.String>>
    ▿ storage: __lldb_expr_1.FormElement<Swift.Array<Swift.String>>.Storage #1
      ▿ value: 2 elements
        - "Many"
        - "Other"
    - id: "choices"
    - isVisible: true
    - requirement: (Function)
  ▿ _text: __lldb_expr_1.FormElement<Swift.Optional<Swift.String>>
    ▿ storage: __lldb_expr_1.FormElement<Swift.Optional<Swift.String>>.Storage #2
      ▿ value: Optional("Description")
        - some: "Description"
    - id: "text"
    - isVisible: true
    - requirement: (Function)
[presenter] did finish
[presenter] show choices
choice <- Optional("Single") from ["Single", "One"] [ valid = true ]
[presenter] show choices
choice <- Optional("Single") from ["Single", "One"] [ valid = true ]
choice -> set value Optional("") [ valid = false ]
form [ valid = false ]
[presenter] form incomplete
choice -> set value Optional("One") [ valid = true ]
form [ valid = true ]
[presenter] form complete
[presenter] show choices
choices <- ["Many", "Other"] from ["Multi", "Many", "Other"] [ valid = true ]
[presenter] show choices
choices <- ["Many", "Other"] from ["Multi", "Many", "Other"] [ valid = true ]
choices -> set value Optional(["Many"]) [ valid = true ]
form [ valid = true ]
[presenter] form complete
choices -> next
skipped text
finished
▿ __lldb_expr_1.SmartForm
  ▿ _choice: __lldb_expr_1.FormElement<Swift.Optional<Swift.String>>
    ▿ storage: __lldb_expr_1.FormElement<Swift.Optional<Swift.String>>.Storage #0
      ▿ value: Optional("One")
        - some: "One"
    - id: "choice"
    - isVisible: true
    - requirement: (Function)
  ▿ _choices: __lldb_expr_1.FormElement<Swift.Array<Swift.String>>
    ▿ storage: __lldb_expr_1.FormElement<Swift.Array<Swift.String>>.Storage #1
      ▿ value: 1 element
        - "Many"
    - id: "choices"
    - isVisible: true
    - requirement: (Function)
  ▿ _text: __lldb_expr_1.FormElement<Swift.Optional<Swift.String>>
    ▿ storage: __lldb_expr_1.FormElement<Swift.Optional<Swift.String>>.Storage #2
      - value: nil
    - id: "text"
    - isVisible: false
    - requirement: (Function)
[presenter] did finish

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment