Skip to content

DirectToSwift/SwiftUIRules

Repository files navigation

SwiftUI Rules

Swift5.1 macOS iOS watchOS Travis

Going fully declarative: SwiftUI Rules.

SwiftUI Rules is covered in the companion AlwaysRightInstitute blog post: Dynamic Environments ¶ SwiftUI Rules.

Requirements

SwiftUI Rules requires an environment capable to run SwiftUI. That is: macOS Catalina, iOS 13 or watchOS 6. In combination w/ Xcode 11.

Note that you can run iOS 13/watchOS 6 apps on Mojave in the emulator, so that is fine as well.

Using the Package

You can either just drag the SwiftUIRules Xcode project into your own project, or you can use Swift Package Manager.

The package URL is: https://github.com/DirectToSwiftUI/SwiftUIRules.git .

Using SwiftUI Rules

SwiftUI Rules is covered in the companion AlwaysRightInstitute blog post: Dynamic Environments ¶ SwiftUI Rules.

Declaring Own Environment Keys

Let's say we want to add an own environment key called fancyColor.

First thing we need is an DynamicEnvironmentKey declaration:

struct FancyColorEnvironmentKey: DynamicEnvironmentKey {
  public static let defaultValue = Color.black
}

Most importantly this specifies the static Swift type of the environment key (Color) and it provides a default value. That value is used when the environment key is queried, but no value has been explicitly set by the user.

Second we need to declare a property on the DynamicEnvironmentPathes struct:

extension DynamicEnvironmentPathes {
  var fancyColor : Color {
    set { self[dynamic: FancyColorEnvironmentKey.self] = newValue }
    get { self[dynamic: FancyColorEnvironmentKey.self] }
  }
}

That's it. We can start using our new key.

Some View accessing our splendid new fancyColor using the @Environment property wrapper:

struct FancyText: View {
  
  @Environment(\.fancyColor) private var color
  
  var label : String
  
  var body: some View {
    Text(label)
      .foregroundColor(color) // boooring
  }
}

and a View providing it:

struct MyPage: View {
  
  var body: some View {
    VStack {
      Text("Hello")
      FancyText("World")
    }
    .environment(\.fancyColor, .red)
  }
}

Setting Up a Ruling Environment

We recommend creating a RuleModel.swift Swift file. Put all your rules in that central location:

// RuleModel.swift
import SwiftUIRules

let ruleModel : RuleModel = [
  \.priority == .low    => \.fancyColor <= .gray,
  \.priority == .high   => \.fancyColor <= .red,
  \.priority == .normal => \.fancyColor <= .black
]

You can hookup the rule system at any place in the SwiftUI View hierarchy, but we again recommend to do that at the very top. For example in a fresh application generated in Xcode, you could modify the generated ContentView like so:

struct ContentView: View {
  private let ruleContext = RuleContext(ruleModel: ruleModel)
  
  var body: some View {
    Group {
      // your views
    }
    .environment(\.ruleContext, ruleContext)
  }
}

Quite often some “root” properties need to be injected:

struct TodoList: View {
  let todos: [ Todo ]
  
  var body: someView {
    VStack {
      Text("Todos:")
      ForEach(todos) { todo in
        TodoView()
           // make todo available to the rule system
          .environment(\.todo, todo)
      }
    }
  }
}

TodoView and child views of that can now derive environment values of the todo key using the rule system.

Use Cases

Ha! Endless 🤓 It is quite different to “Think In Rules”™ (a.k.a. declaratively), but they allow you to compose your application in a highly decoupled and actually “declarative” ways.

It can be used low level, kinda like CSS. Consider dynamic environment keys a little like CSS classes. E.g. you could switch settings based on the platform:

[
  \.platform == "watch" => \.details <= "minimal",
  \.platform == "phone" => \.details <= "regular",
  \.platform == "mac" || \.platform == "pad" 
  => \.details <= "high"
]

But it can also be used at a very high level, for example in a workflow system:

[
  \.task.status == "done"    => \.view <= TaskFinishedView(),
  \.task.status == "done"    => \.actions <= [],
  \.task.status == "created" => \.view <= NewTaskView(),
  \.task.status == "created" => \.actions = [ .accept, .reject ]
]

struct TaskView: View {
  @Environment(\.view) var body // body derived from rules
}

Since SwiftUI Views are also just lightweight structs, you can build dynamic properties which carry them!

In any case: We are interested in any idea how to use it!

Limitations

Only DynamicEnvironmentKeys

Currently rules can only evaluate DynamicEnvironmentKeys, it doesn't take regular environment keys into account. That is, you can't drive for example the builtin SwiftUI lineLimit using the rulesystem.

[
  \.user.status == "VIP" => \.lineLimit <= 10,
  \.lineLimit <= 2
]

Does not work. This is currently made explicit by requiring keys which are used w/ the system to have the DynamicEnvironmentKey type. SO you can't accidentially run into this.

We may open it up to any EnvironmentKey, TBD.

No KeyPath'es in Assignments

Sometimes one might want this:

\.todos.count > 10 => \.person.status <= "VIP"

I.e. assign a value to a multi component keypath (\.person.status). That does not work.

SwiftUI Bugs

Sometimes SwiftUI “looses” its environment during navigation or in List's. watchOS and macOS seem to be particularily problematic, iOS less so. If that happens, one can pass on the ruleContext manually:

struct MyNavLink<Destination, Content>: View {
  @Environment(\.ruleContext) var ruleContext
  ...
  var body: someView {
    NavLink(destination: destination
      // Explicitly pass along:
      .environment(\.ruleContext, ruleContext)) 
  ...
}

Who

Brought to you by The Always Right Institute and ZeeZide. We like feedback, GitHub stars, cool contract work, presumably any form of praise you can think of.