SwiftUI offers a lot of “novel” API design ideas and ways of using Swift that can be borrowed and applied in reverse to normal Swift code. The PreferenceKey approach is one of them: it provides a pattern for child views to send custom values up to the parent view in a type-safe manner, using the protocol approach. I’ll talk more about PreferenceKey if I have the opportunity, but this design pattern has nothing to do with the UI, and we can use it to improve API design in Swift in general. The original | address
In this article, we’ll look at how to do it. The relevant code for this article can be found here. You can copy this code to Playground to execute and see the results.
The traffic lights
Take a traffic light as an example.
The TrafficLight type, which is of Model type, defines.stop,.proceed, and.caution states, representing stop, proceed, and caution states respectively (which, of course, would be ‘red, green, and yellow’ in plain English, but a Model should not be colored, View hierarchy dependent). It also holds a state to indicate the current state and sends this state through onStateChanged at setup time:
public class TrafficLight { public enum State { case stop case proceed case caution } public private(set) var state: State = .stop { didSet { onStateChanged? (state) } } public var onStateChanged: ((State) -> Void)? }Copy the code
The rest of the logic is irrelevant to this topic, but it’s also relatively simple. Keep reading if you’re interested. But this does not affect the understanding of this article.
Other parts of TrafficLight
Using the traffic light in the ViewController is also simple. Inside onStateChanged we set the color of our View according to the red, green and yellow colors:
light = TrafficLight() light.onStateChanged = { [weak self] state in guard let self = self else { return } let color: UIColor switch state { case .proceed: color = .green case .caution: color = .yellow case .stop: Color =. Red} UIView. The animate (withDuration: 0.25) {self. The backgroundColor = color}} light. The start ()Copy the code
This way, the View’s color can be changed as TrafficLight changes:
The green signal
The world is big, and some places (like Japan) tend to use cyan, or really turquoise, to mean “passable.” Sometimes this is the result of technological limitations or advances.
The green light was traditionally green in colour (hence its name) though modern LED green lights are turquoise.
— Traffic Light at Wikipedia
Suppose we wanted TrafficLight to support green lights in cyan, one of the simplest ways we could think of would be to provide an option for “green light color” in TrafficLight:
public class TrafficLight {
public enum GreenLightColor {
case green
case turquoise
}
public var preferredGreenLightColor: GreenLightColor = .green
//...
}
Copy the code
Then use the corresponding color in the ViewController:
extension TrafficLight.GreenLightColor { var color: UIColor { switch self { case .green: return .green case .turquoise: Return UIColor(Red: 0.25, Green: 0.88, blue: 0.82, alpha: 1.00)}}} light. PreferredGreenLightColor =. Turquoise light. OnStateChanged = {[weak self. weak light] state in guard let self = self, let light = light else { return } // ... // case .proceed: color = .green case .proceed: color = light.preferredGreenLightColor.color }Copy the code
This certainly solves the problem, but there are pitfalls. First, an additional storage property, preferredGreenLightColor, needs to be added to TrafficLight, which increases the memory overhead used by the TrafficLight example. In the example above, the additional GreenLightColor attribute would incur 8 bytes of overhead per instance. This overhead would be a shame if we had to deal with many TrafficLight instances simultaneously and only a few of them needed.turquoise.
Strictly, in case the TrafficLight. GreenLightColor enumeration actually require only 1 byte. However, the minimum unit of memory allocation in 64-bit systems is 8 bytes.
This can be even more expensive if the attributes you want to add are not simple enUms, as in the example, but more complex types with multiple attributes.
In addition, if we need to add other attributes, it is easy to think of adding more storage attributes to TrafficLight. This is a very unextendable method, and we can’t add storage attributes in extension:
Var TrafficLight {enum A {case A} var TrafficLight: A = .a // Extensions must not contain stored properties }Copy the code
You need to modify TrafficLight’s source code to add this option, and you also need to set appropriate initial values for the added properties, or provide additional init methods. Adding options like this would not be possible without directly modifying TrafficLight’s source code (for example, if the type was someone else’s code, or packaged into the framework).
Option Pattern
An Option Pattern can be used to solve this problem. In TrafficLight, instead of providing a dedicated preferredGreenLightColor, we define a generic Options dictionary to put the desired option values into. To restrict the values that can be put into the dictionary, create a new TrafficLightOption protocol:
Public protocol TrafficLightOption {associatedType Value static var defaultValue: Value {get}} public protocol TrafficLightOption {associatedType Value static var defaultValue: Value {get}}Copy the code
In TrafficLight, add the following options properties and subscripts:
public class TrafficLight {
// ...
// 1
private var options = [ObjectIdentifier: Any]()
public subscript<T: TrafficLightOption>(option type: T.Type) -> T.Value {
get {
// 2
options[ObjectIdentifier(type)] as? T.Value
?? type.defaultValue
}
set {
options[ObjectIdentifier(type)] = newValue
}
}
// ...
}
Copy the code
- Only meet
Hashable
Type, can be asoptions
Dictionary keys.ObjectIdentifier
Given a type or class instance, you can generate a value that uniquely represents that type and instance. It is very suitable for use asoptions
The key. - Through the key in the
options
Looks for the value set in. If not, return the default valuetype.defaultValue
.
Now, the TrafficLight. GreenLightColor extend, make it meet the TrafficLightOption. If TrafficLight is already packaged as a framework, we can even pull this code out of TrafficLight’s target:
extension TrafficLight {
public enum GreenLightColor: TrafficLightOption {
case green
case turquoise
public static let defaultValue: GreenLightColor = .green
}
}
Copy the code
We declare defaultValue as GreenLightColor, so trafficlightoption. Value will also be inferred by the compiler to be GreenLightColor.
Finally, provide a setter and getter for this option:
extension TrafficLight {
public var preferredGreenLightColor: TrafficLight.GreenLightColor {
get { self[option: GreenLightColor.self] }
set { self[option: GreenLightColor.self] = newValue }
}
}
Copy the code
You can now use this option as before by setting the preferredGreenLightColor directly on light, and it is no longer TrafficLight’s stored property. As long as it’s not set up, there’s no overhead.
light.preferredGreenLightColor = .turquoise
Copy the code
With TrafficLightOption, when we want to add options to TrafficLight, we now do not need to change the code of the type itself, we just need to declare a new type that meets TrafficLightOption and then implement the appropriate calculated properties for it. This greatly increases the extensibility of the original type.
conclusion
Option Pattern is a swiftui-inspired Pattern that helps provide a type-safe way to add “storage” to existing types without adding storage attributes.
This pattern is ideal for adding functionality to existing types from the outside, or for transforming the way types are used from the bottom up. This technology can have a beneficial impact on Swift development and API design updates. In turn, understanding this pattern will help you understand many of the concepts in SwiftUI, such as PreferenceKey and alignmentGuide.
recommended
IOS development technical materials