Home

Awesome

Prototype (WIP)

Prototype is a work-in-progress project that generates SwiftUI Forms and Views for data structures and classes. It's designed to complement SwiftData Models seamlessly.

Overview

SwiftUI has transformed UI development in Swift, but rapid prototyping by creating views for data models still involves some boilerplate work to be coded. Prototype aims to eliminate this boilerplate by providing a convenient macro to auto-generate SwiftUI code for your data.

Key Features

Example

Here's a quick example of Prototype in action:

Source:

@Prototype(style: .labeled, kinds: .form, .view)
struct Author {
    let name: String
}

Macro Expansion:

struct AuthorView: View {
    public let model: Author

    public init(model: Author) {
        self.model = model
    }

    public var body: some View {
        LabeledContent("AuthorView.name.label") {
            LabeledContent("AuthorView.name", value: model.name)
        }
    }
}

struct AuthorForm: View {
    @Binding public var model: Author
    private let footer: AnyView?
    private let numberFormatter: NumberFormatter

    public init(model: Binding<Author>, numberFormatter: NumberFormatter = .init()) {
        self._model = model
        self.footer = nil
        self.numberFormatter = numberFormatter
    }

    public init<Footer>(model: Binding<Author>, numberFormatter: NumberFormatter = .init(), @ViewBuilder footer: () -> Footer) where Footer: View {
        self._model = model
        self.footer = AnyView(erasing: footer())
        self.numberFormatter = numberFormatter
    }

    public var body: some View {
        Form {
            LabeledContent("AuthorForm.name.label") {
                TextField("AuthorForm.name", text: .constant(model.name))
            }

            if let footer {
                footer
            }
        }
    }
}

Source:

@Prototype(style: .inline, kinds: .form, .view)
struct Article {
    var title: String
    var content: String
    @Field(.secure) var password: String
    
    @Section("metadata")
    
    @Field(.readonly) var isPublished: Bool
    @Field(.hidden) let views: Int
    let author: Author
}

Macro Expansion:

struct ArticleForm: View {
    @Binding public var model: Article
    private let footer: AnyView?
    private let numberFormatter: NumberFormatter

    public init(model: Binding<Article>, numberFormatter: NumberFormatter = .init()) {
        self._model = model
        self.footer = nil
        self.numberFormatter = numberFormatter
    }

    public init<Footer>(model: Binding<Article>, numberFormatter: NumberFormatter = .init(), @ViewBuilder footer: () -> Footer) where Footer: View {
        self._model = model
        self.footer = AnyView(erasing: footer())
        self.numberFormatter = numberFormatter
    }

    public var body: some View {
        Form {
            TextField("ArticleForm.title", text: $model.title)
            TextField("ArticleForm.content", text: $model.content)
            SecureField("ArticleForm.password", text: $model.password)
            Section(header: Text("ArticleForm.metadata")) {
                Toggle("ArticleForm.isPublished", isOn: .constant(model.isPublished))
                AuthorForm(model: .constant(model.author))
            }

            if let footer {
                footer
            }
        }
    }
}

struct ArticleView: View {
    public let model: Article

    public init(model: Article) {
        self.model = model
    }

    public var body: some View {
        LabeledContent("ArticleView.title", value: model.title)
        LabeledContent("ArticleView.content", value: model.content)
        LabeledContent("ArticleView.password", value: "********")
        GroupBox("ArticleView.metadata") {
            LabeledContent("ArticleView.isPublished") {
                Text(model.isPublished.description)
            }
            AuthorView(model: model.author)
        }
    }
}

Source:

@Prototype(style: .inline, kinds: .settings)
struct General {
    var boolValue: Bool = false
    var intValue: Int = 0
    var doubleValue: Double = 0
    var stringValue: String = ""
    var optionalBoolValue: Bool?
    var optionalIntValue: Int?
    var optionalDoubleValue: Double?
    var optionalStringValue: String?
}

Macro Expansion:

struct GeneralSettingsView: View {
    @AppStorage("General.boolValue") private var boolValue: Bool = false
    @AppStorage("General.intValue") private var intValue: Int = 0
    @AppStorage("General.doubleValue") private var doubleValue: Double = 0
    @AppStorage("General.stringValue") private var stringValue: String = ""
    @AppStorage("General.optionalBoolValue") private var optionalBoolValue: Bool?
    @AppStorage("General.optionalIntValue") private var optionalIntValue: Int?
    @AppStorage("General.optionalDoubleValue") private var optionalDoubleValue: Double?
    @AppStorage("General.optionalStringValue") private var optionalStringValue: String?
    private var optionalBoolValueBinding: Binding<Bool> {
        Binding(
            get: {
                optionalBoolValue ?? false
            },
            set: {
                optionalBoolValue = $0
            }
        )
    }
    private var optionalIntValueBinding: Binding<Int> {
        Binding(
            get: {
                optionalIntValue ?? 0
            },
            set: {
                optionalIntValue = $0
            }
        )
    }
    private var optionalDoubleValueBinding: Binding<Double> {
        Binding(
            get: {
                optionalDoubleValue ?? 0
            },
            set: {
                optionalDoubleValue = $0
            }
        )
    }
    private var optionalStringValueBinding: Binding<String> {
        Binding(
            get: {
                optionalStringValue ?? ""
            },
            set: {
                optionalStringValue = $0
            }
        )
    }
    private let footer: AnyView?
    private let numberFormatter: NumberFormatter

    public init<Footer>(numberFormatter: NumberFormatter = .init(), @ViewBuilder footer: () -> Footer) where Footer: View {
        self.footer = AnyView(erasing: footer())
        self.numberFormatter = numberFormatter
    }

    public var body: some View {
        Form {
            Toggle("GeneralSettingsView.boolValue", isOn: $boolValue)
            TextField("GeneralSettingsView.intValue", value: $intValue, formatter: numberFormatter)
            TextField("GeneralSettingsView.doubleValue", value: $doubleValue, formatter: numberFormatter)
            TextField("GeneralSettingsView.stringValue", text: $stringValue)
            Toggle("GeneralSettingsView.optionalBoolValue", isOn: optionalBoolValueBinding)
            TextField("GeneralSettingsView.optionalIntValue", value: optionalIntValueBinding, formatter: numberFormatter)
            TextField("GeneralSettingsView.optionalDoubleValue", value: optionalDoubleValueBinding, formatter: numberFormatter)
            TextField("GeneralSettingsView.optionalStringValue", text: optionalStringValueBinding)

            if let footer {
                footer
            }
        }
    }
}

Source:

@Prototype(style: .inline, kinds: .view, .form)
@Model
final class Item {
    @Format(using: Date.FormatStyle(date: .numeric, time: .standard))
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

Macro Expansion:

struct ItemView: View {
    public let model: Item

    public init(model: Item) {
        self.model = model
    }

    public var body: some View {
        LabeledContent("ItemView.timestamp", value: model.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
    }
}

struct ItemForm: View {
    @Binding public var model: Item
    private let footer: AnyView?
    private let numberFormatter: NumberFormatter

    public init(model: Binding<Item>, numberFormatter: NumberFormatter = .init()) {
        self._model = model
        self.footer = nil
        self.numberFormatter = numberFormatter
    }

    public init<Footer>(model: Binding<Item>, numberFormatter: NumberFormatter = .init(), @ViewBuilder footer: () -> Footer) where Footer: View {
        self._model = model
        self.footer = AnyView(erasing: footer())
        self.numberFormatter = numberFormatter
    }

    public var body: some View {
        Form {
            DatePicker("ItemForm.timestamp", selection: $model.timestamp)

            if let footer {
                footer
            }
        }
    }
}

License

Prototype is under the MIT License. Refer to LICENSE for details.