Home

Awesome

Data driven settings screens

<a href="https://www.youtube.com/watch?v=MTY9m2--tiA&feature=youtu.be"><img align="right" src="Assets/demo.gif"></a>

Create your Swift structure and let the UI be generated for you automatically

struct Calculation: AutomaticSettings {
        var text = ""
        var mode = Mode.linearRegression
    }

    // sourcery: injectFooter
    struct Smoothing: AutomaticSettings {
        var dayPeriod = 7
        var algorithm = Algorithm.movingAverage

        struct Grouped: AutomaticSettings {
            // sourcery: range = 1...4
            var level: Float = 1

            var prettyCool = true
        }

        var grouped: Grouped = .init()
    }


Watch overview demo on YouTube

Almost every app contain some kind of settings screens, most apps usually contain debug settings along user-facing ones, the implementation of those screens in UIKit could really get complicated, the emergence of SwiftUI had simplified this a lot, but I believe this could be streamlined even more by leveraging data driven approach and Sourcery.

That's where AutomaticSettings fits in.

Settings are:

How does it work?

You have a master structure for your settings e.g.

struct BetaSettings: AutomaticSettings {
    struct Calculation: AutomaticSettings {
        var mode = .linearRegression
        var averageMethod = .weightedAverage
        var timePeriod = 7
    }

    var calculation: Calculation = .init()
}

Sourcery analyzes your structure and generates functions for each settings and sections, you simply provide a content function to your main view:

extension BetaSettingsView {
    var content: some View {
        calculationLink()
        otherSectionLink()
        Button("Some action that doesn't have data backing") {
            // ...
        }
    }
}

If you modify your data structure Sourcery will update the proper functions and your UI will reflect the current state of data.

Tweaking Variable behaviour

Variables can be annotated using // sourcery: annotation with the following options:

Supported types

We automatically support UI for the following types:

For supporting custom enums you can either leverage the enums that implement CaseIterable, Hashable, RawRepresentable, RawValue == String, or you can add support for a custom type that conforms to SettingsDisplayable. For complex types you can also implement a custom setting DSL function for your type.

Adding new sections

When you add a new section and mark it with AutoSettings, you will need to include it in the BetaSettingsView+Content.swift content function by calling either:

You can alter them with footerView / headerView. Keep in mind that if you use injectFooter / injectHeader then you can't provide a footer nor header for links as they will auto-call those functions, so you use one or the other approach.

advertisingLink(footerView: {
                    Button("Report Ad") {
                        //...
                    }
})

Tweaking section behaviour

Sections can be annotated using // sourcery: annotation with the following options:

Thus allowing you to add headers / footer by simply adding correctly named functions (don't fret as compiler will tell you if you named it wrong) in an extension of BetaSetingsView or one of the sections subviews.

The template currently supports nesting up to 2 levels, meaning you can have BetaSettings.Section.SubSection but if needed it could be easily extended to deeper nesting levels.

Installation

Few steps are required:

  1. Your project needs Sourcery
  2. Copy the Sourcery template to your project's templates and configure it.
  3. Add this library code to your project, you can use Swift Package Manager / CocoaPods or just copy the few files this project contains.
  4. Add view implementation for your settings screen (this is to allow you to customize it further)

If anything is unclear refer to the example app. The repo also contains commits that highlight specific parts of the feature set.

Configuring the template

The template requires few parameters in your sourcery.yml file, add them in args: section like this:

args:
  settingsView: BetaSettingsView
  settingsStructure: BetaSettings
  settingsExternalData: BetaSettingsExternalData
  settingsImports:
    - FrameworkMySettingsNeed
    - MyCustomFrameworkName
    - OtherInternalFramework

Sample view implementation

struct BetaSettingsView: View, AutomaticSettingsViewDSL {
    private enum Subscreen: Swift.Identifiable {
        case review

        var id: String {
            switch self {
            case .review:
                return "review"
            }
        }
    }

    @ObservedObject
    var viewModel: AutomaticSettingsViewModel<BetaSettings, BetaSettingsExternalData>

    @State(initialValue: nil)
    private var subscreen: Subscreen?

    var body: some View {
        NavigationView {
            content
                .sheet(item: $subscreen, content: { subscreen in
                    Group {
                        switch subscreen {
                        case .review:
                            reviewScreen
                        }
                    }
                })
                .navigationBarTitle("Beta Settings")
                .navigationBarItems(
                    leading: Button("Cancel") {
                        viewModel.cancel()
                    },
                    trailing: Group {
                        if viewModel.applicableChanges.isEmpty {
                            EmptyView()
                        } else {
                            Button("Review") {
                                subscreen = .review
                            }
                        }
                    }
                )
        }
    }

    var reviewScreen: some View {
        NavigationView {
            Form {
                if let changes = viewModel.applicableChanges, !changes.isEmpty {
                    ForEach(changes) { change in
                        VStack {
                            HStack {
                                Text(change.name)
                                Spacer()
                                if change.requiresRestart {
                                    Image(systemName: "goforward")
                                        .renderingMode(.template)
                                        .foregroundColor(.red)
                                }
                            }
                            HStack {
                                Spacer()
                                Text(change.change)
                            }
                        }
                    }
                }
            }
            .navigationBarTitle("Review changes")
            .navigationBarItems(
                leading: Button("Cancel") {
                    subscreen = nil
                },
                trailing: Button("Save\(viewModel.needsRestart ? " & Restart" : "")") {
                    subscreen = nil
                    viewModel.saveChanges()
                }
            )
        }
    }
}

extension BetaSettingsView {
    var content: some View {
        // put your actual settings content here
    }
}