• Stars
    star
    300
  • Rank 134,125 (Top 3 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 3 years ago
  • Updated about 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Data driven settings UI generation.

Data driven settings screens

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:

  • Codable
  • based on pure swift value types
  • UI is generated by Sourcery but developers retain the ability to customize it
  • All changes are tracked and can be reviewed before applying them (like a transaction)
  • Settings can be marked as requiring a restart or triggering side-effects

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:

  • skipSetting: will not generate UI for the given variable
  • requiresRestart: if a variable changes it will force an app restart in the review screen
  • sideEffect: allows you to add side-effects that should happen when variable changes (e.g. call external controller or some function)
  • range: if a variable is a Float/Double it allows you specify the range of allowed values e.g. range = 0.0...1.0
Supported types

We automatically support UI for the following types:

  • String
  • Bool
  • Int
  • Float
  • Enums
  • SettingsDisplayable

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:

  • fooLink() - will inject the section as a navigation link (only available for top level sections)
  • FooView() - will create a view containing all the settings for the given section

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:

  • injectFooter: automatically injects sub-section footer via function you define named sectionNameFooter
  • injectHeader: automatically injects sub-section header via function you define named sectionNameHeader

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
    }
}

More Repositories

1

Sourcery

Meta-programming for Swift, stop writing boilerplate code.
Swift
7,544
star
2

LifetimeTracker

Find retain cycles / memory leaks sooner.
Swift
3,052
star
3

Playgrounds

Better playgrounds that work both for Objective-C and Swift
Objective-C
2,632
star
4

Bootstrap

iOS project bootstrap aimed at high quality coding.
Objective-C
2,047
star
5

Inject

Hot Reloading for Swift applications!
Swift
1,914
star
6

Swift-Macros

A curated list of awesome Swift Macros
Swift
1,863
star
7

LineDrawing

Beatiful and fast smooth line drawing algorithm for iOS - as seen in Foldify.
Objective-C
1,287
star
8

Difference

Simple way to identify what is different between 2 instances of any type. Must have for TDD.
Swift
1,213
star
9

PropertyMapper

Property mapping for Objective-C iOS apps.
Objective-C
1,126
star
10

KZFileWatchers

A micro-framework for observing file changes, both local and remote. Helpful in building developer tools.
Swift
1,074
star
11

LinkedConsole

Clickable links in your Xcode console, so you never wonder which class logged the message.
Swift
934
star
12

Traits

Modify your native iOS app in real time.
Swift
905
star
13

IconOverlaying

Build informations on top of your app icon.
Shell
650
star
14

crafter

Crafter - Xcode project configuration CLI made easy.
Ruby
547
star
15

Strongify

Strongify is a 1-file µframework providing a nicer API for avoiding weak-strong dance.
Swift
444
star
16

KZNodes

Have you ever wonder how you could make Origami like editor in 1h ?
Objective-C
335
star
17

SFObservers

NSNotificationCenter and KVO auto removal of observers.
Objective-C
309
star
18

CCNode-SFGestureRecognizers

Adding UIGestureRecognizers to cocos2d, painless.
Objective-C
202
star
19

DetailsMatter

Objective-C
198
star
20

Pinch-to-reveal

Pinch to reveal animation transition built with Layer masking, as seen in boeing app for iPad.
Objective-C
193
star
21

ViewModelOwners

Protocols that help make your MVVM setup more consistent
Swift
143
star
22

KZAsserts

Asserts on roids, test all your assumptions with ease.
Objective-C
100
star
23

SFContainerViewController

UIViewControllers containment predating Apple implementation. Works in both 4.x and 5.x iOS, no memory or hierarchy issues.
Objective-C
82
star
24

OhSnap

Reproduce bugs your user saw by capturing and replaying data snapshots with ease.
Swift
81
star
25

Versionable

Migration for `Codable` objects.
Swift
79
star
26

Swift-Observable

Native KVO like behaviour build in Swift.
Swift
64
star
27

BehavioursExample

Objective-C
58
star
28

SourceryWorkshops

Swift
45
star
29

Learn-iOS-GameDev-Level-0

Teeter clone accompanying tutorial at http://merowing.info/2013/04/learn-ios-game-dev-level-0/
Objective-C
24
star
30

XibReferencing

Simple category and sample showing how you can reference one Xib view from another
Objective-C
20
star
31

NSObject-SFExecuteOnDealloc

A simple category on NSObject that allows you to execute block when object is deallocated
Objective-C
18
star
32

KZImageSplitView

Objective-C
17
star
33

jenkins_jobs_to_statusboard

Ruby script that generates html table for embedding in StatusBoard by Panic http://panic.com/statusboard/
Ruby
15
star
34

SourceryPro-Feedback

Repository for discussing https://merowing.info/sourcery-pro/
12
star
35

krzysztofzablocki

2
star
36

krzysztofzablocki.github.io

Blog
HTML
2
star
37

starter-hugo-academic

Jupyter Notebook
1
star