Skip to content

Commit

Permalink
Dynamic colours (#5)
Browse files Browse the repository at this point in the history
* Add support for iOS 13 dynamic colours
  • Loading branch information
dhardiman authored Jul 18, 2019
1 parent 880ef2b commit 36b2196
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 10 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ The "key" will be used as a static property name in a `class` so should have a f
- `Double`: A double value
- `Bool`: A boolean value
- `Colour`: A colour in hex format, will be output as a `UIColor`.
- `DynamicColour`: A pair of colours, in hex format, that will be output as an iOS 13-compatible dynamic colour.
- `DynamicColourReference`: A pair of colours, as properties on the current configuration or somewhere else, that will be output as an iOS 13-compatible dynamic colour.
- `Image`: The name of an image. Will be converted to `UIImage(named: "the value")!`.
- `Regex`: A regular expression pattern. Will be converted to `try! NSRegularExpression(patthen: "the value", options: [])`
- `EncryptionKey`: A key to use to encrypt sensitive info.
Expand Down Expand Up @@ -224,6 +226,67 @@ If you find yourself repeating override patterns, for example `(PROD|STAGING)` y
}
```

### DynamicColour and DynamicColourReference
To support iOS 13's dark mode, it is possible to output colours as dynamic. For example:

```
"background": {
"type": "DynamicColour",
"defaultValue": {
"light": "#FF",
"dark": "#00"
}
}
```

will output:

```
@nonobjc static var background: UIColor {
if #available(iOS 13, *) {
return UIColor(dynamicProvider: {
if $0.userInterfaceStyle == .dark {
return UIColor(white: 0.0 / 255.0, alpha: 1.0)
} else {
return UIColor(white: 255.0 / 255.0, alpha: 1.0)
}
})
} else {
return UIColor(white: 255.0 / 255.0, alpha: 1.0)
}
}
```

Similarly, it is possible to use references to another colour, so:

```
"background": {
"type": "DynamicColourReference",
"defaultValue": {
"light": "UIColor.white",
"dark": "UIColor.black"
}
}
```

will output:

```
@nonobjc static var background: UIColor {
if #available(iOS 13, *) {
return UIColor(dynamicProvider: {
if $0.userInterfaceStyle == .dark {
return UIColor.black
} else {
return UIColor.white
}
})
} else {
return UIColor.white
}
}
```

## Writing your own schemas
Just add a new class or struct to the project and implement `Template`. Add your new parser to the `templates` array in main.swift. Your template should inspect a `template` dictionary in any config and decide whether it can parse it. Either using a `name` item, or through other means. Ensure `ConfigurationFile` is the last item in that array. As the default schema parser it claims to be able to parse all files.

Expand Down
2 changes: 2 additions & 0 deletions Sources/Config/ConfigurationFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func parseNextProperty(properties: [String: Property], pair: (key: String, value
return properties
}
copy[pair.key] = ReferenceProperty(key: pair.key, dict: dict, typeName: referenceType.typeName)
case .dynamicColour, .dynamicColourReference:
copy[pair.key] = ConfigurationProperty<[String: String]>(key: pair.key, typeHint: typeHintValue, dict: dict, patterns: patterns)
}
} else {
if let customType = customTypes.first(where: { $0.typeName == typeHintValue }) {
Expand Down
43 changes: 35 additions & 8 deletions Sources/Config/ConfigurationProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ struct ConfigurationProperty<T>: Property, AssociatedPropertyKeyProviding {
}

func value(for scheme: String) -> T {

if let override = overrides.first(where: { item in
if associatedProperty != nil {
return item.key == scheme
Expand All @@ -99,13 +98,11 @@ struct ConfigurationProperty<T>: Property, AssociatedPropertyKeyProviding {
template += "\(String.indent(for: indentWidth))/// \(description)\n"
}
if requiresNonObjCDeclarations {
template += """
\(String.indent(for: indentWidth))@nonobjc\(isPublic ? " public" : "") static var {key}: {typeName} {
\(String.indent(for: indentWidth + 1))return {value}
\(String.indent(for: indentWidth))}
"""
template += computedProperty(nonObjc: true, indentWidth: indentWidth, isPublic: isPublic, outputProvidesReturn: type?.valueProvidesReturn ?? false)
} else {
template += "\(String.indent(for: indentWidth))\(isPublic ? "public " : "")static let {key}: {typeName} = {value}"
template += (type?.computedProperty ?? false) ?
computedProperty(nonObjc: false, indentWidth: indentWidth, isPublic: isPublic, outputProvidesReturn: type?.valueProvidesReturn ?? false) :
staticProperty(indentWidth: indentWidth, isPublic: isPublic)
}
let propertyValue = value(for: scheme)
let outputValue: String
Expand All @@ -116,7 +113,17 @@ struct ConfigurationProperty<T>: Property, AssociatedPropertyKeyProviding {
}
return template.replacingOccurrences(of: "{key}", with: key)
.replacingOccurrences(of: "{typeName}", with: typeName)
.replacingOccurrences(of: "{value}", with: outputValue)
.replacingOccurrences(of: "{value}", with: reindent(value: outputValue, to: indentWidth))
}

private func reindent(value: String, to indentWidth: Int) -> String {
let split = value.split(separator: "\n")
guard split.count > 1 else { return value }
return split.enumerated().map {
guard $0.offset > 0 else { return String($0.element) }
return String.indent(for: indentWidth + 1) + $0.element
}
.joined(separator: "\n")
}

func keyValue(for scheme: String) -> String {
Expand All @@ -126,4 +133,24 @@ struct ConfigurationProperty<T>: Property, AssociatedPropertyKeyProviding {
}
return value
}

private func computedProperty(nonObjc: Bool, indentWidth: Int, isPublic: Bool, outputProvidesReturn: Bool) -> String {
let modifiers = [
nonObjc ? "@nonobjc" : nil,
isPublic ? "public" : nil,
"static",
"var"
]
.compactMap { $0 }
.joined(separator: " ")
return """
\(String.indent(for: indentWidth))\(modifiers) {key}: {typeName} {
\(String.indent(for: indentWidth + 1))\(outputProvidesReturn ? "" : "return "){value}
\(String.indent(for: indentWidth))}
"""
}

private func staticProperty(indentWidth: Int, isPublic: Bool) -> String {
return "\(String.indent(for: indentWidth))\(isPublic ? "public " : "")static let {key}: {typeName} = {value}"
}
}
59 changes: 57 additions & 2 deletions Sources/Config/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,16 @@ enum PropertyType: String {
case reference = "Reference"
case image = "Image"
case regex = "Regex"
case dynamicColour = "DynamicColour"
case dynamicColourReference = "DynamicColourReference"

var typeName: String {
switch self {
case .encrypted, .encryptionKey:
return "[UInt8]"
case .dictionary:
return "[String: Any]"
case .colour:
case .colour, .dynamicColour, .dynamicColourReference:
return "UIColor"
case .image:
return "UIImage"
Expand All @@ -86,6 +88,24 @@ enum PropertyType: String {
}
}

var computedProperty: Bool {
switch self {
case .dynamicColour, .dynamicColourReference:
return true
default:
return false
}
}

var valueProvidesReturn: Bool {
switch self {
case .dynamicColour, .dynamicColourReference:
return true
default:
return false
}
}

func valueDeclaration(for value: Any, iv: IV, key: String?) -> String {
let stringValueAllowingOptional = { (optional: Bool) -> String in
if let string = value as? String {
Expand Down Expand Up @@ -124,8 +144,12 @@ enum PropertyType: String {
if let int = value as? Int {
return "\(int)"
} else {
fallthrough
return "\(value)"
}
case .dynamicColour:
return dynamicColourValue(for: value as? [String: String])
case .dynamicColourReference:
return dynamicColourReferenceValue(for: value as? [String: String])
default:
return "\(value)"
}
Expand All @@ -142,4 +166,35 @@ enum PropertyType: String {
return "UIColor(red: \(CGFloat((rgbValue & 0xFF0000) >> 16)) / 255.0, green: \(CGFloat((rgbValue & 0x00FF00) >> 8)) / 255.0, blue: \(CGFloat(rgbValue & 0x0000FF)) / 255.0, alpha: 1.0)"
}
}

private func dynamicColourOutput(from value: [String: String]?, transform: ((String) -> String)? = nil) -> String {
guard let value = value, let light = value["light"], let dark = value["dark"] else {
return "Invalid dictionary. Should have a 'light' and a 'dark' value"
}
return dynamicColourOutputFor(light: transform?(light) ?? light, dark: transform?(dark) ?? dark)
}

private func dynamicColourOutputFor(light: String, dark: String) -> String {
return """
if #available(iOS 13, *) {
return UIColor(dynamicProvider: {
if $0.userInterfaceStyle == .dark {
return \(dark)
} else {
return \(light)
}
})
} else {
return \(light)
}
"""
}

private func dynamicColourValue(for value: [String: String]?) -> String {
return dynamicColourOutput(from: value) { self.colourValue(for: $0) }
}

private func dynamicColourReferenceValue(for value: [String: String]?) -> String {
return dynamicColourOutput(from: value, transform: nil)
}
}
52 changes: 52 additions & 0 deletions Tests/ConfigTests/ConfigurationPropertyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,58 @@ class ConfigurationPropertyTests: XCTestCase {
expect(actualValue).to(equal(expectedValue))
}

func testItCanWriteADynamicColourProperty() throws {
let colourProperty = ConfigurationProperty<[String:String]>(key: "test", typeHint: "DynamicColour", dict: [
"defaultValue": [
"light": "#00",
"dark": "#FF"
]
])
let expectedValue = """
static var test: UIColor {
if #available(iOS 13, *) {
return UIColor(dynamicProvider: {
if $0.userInterfaceStyle == .dark {
return UIColor(white: 255.0 / 255.0, alpha: 1.0)
} else {
return UIColor(white: 0.0 / 255.0, alpha: 1.0)
}
})
} else {
return UIColor(white: 0.0 / 255.0, alpha: 1.0)
}
}
"""
let actualValue = try whenTheDeclarationIsWritten(for: colourProperty)
expect(actualValue).to(equal(expectedValue))
}

func testItCanWriteADynamicReferenceColourProperty() throws {
let colourProperty = ConfigurationProperty<[String:String]>(key: "test", typeHint: "DynamicColourReference", dict: [
"defaultValue": [
"light": "green",
"dark": "blue"
]
])
let expectedValue = """
static var test: UIColor {
if #available(iOS 13, *) {
return UIColor(dynamicProvider: {
if $0.userInterfaceStyle == .dark {
return blue
} else {
return green
}
})
} else {
return green
}
}
"""
let actualValue = try whenTheDeclarationIsWritten(for: colourProperty)
expect(actualValue).to(equal(expectedValue))
}

func testItCanWriteAnImageProperty() throws {
let imageProperty = ConfigurationProperty<String>(key: "test", typeHint: "Image", dict: [
"defaultValue": "image-name",
Expand Down

0 comments on commit 36b2196

Please sign in to comment.