Skip to content

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

License

mrylmz/Prototype

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

  • Rapid prototyping: Prototype offers a simple macro for effortlessly generating SwiftUI views from your data structures and classes.

  • SwiftData Models Integration: Prototype works seamlessly with SwiftData Models, making it easy to create SwiftUI representations for your data.

  • Customization: While Prototype streamlines the process, you can still customize the generated SwiftUI code to match your design needs.

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.

About

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

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages