This story is an extension of the previous stories of Data Binding with KeyPath Part1 and Part2.
The two stories are all about one-way and two-way data binding with KeyPath. And this story will continually demonstrate some applications of KeyPath on validation, calculation, and collection and will show you how awesome it is.
KeyPath in Validation
If you are familiar with the following code of guards, you will find it’s very annoying to write these duplicate code like me.
guard let name = nameField.text?.trimmingCharacters(in: .whitespaces), name.isUserName else { nameField.becomeFirstResponder()
}guard let email = emailField.text?.trimmingCharacters(in: .whitespaces), email.isEmail else { emailField.becomeFirstResponder()
}guard let password = passwordField.text?.trimmingCharacters(in: .whitespaces), password.isPassword else { passwordField.becomeFirstResponder()
}
If we have bound the mode property with these views, how about to validate the model property rather than retrieving values from views again.
public protocol KPValidation {}extension KPValidation {
public func validate(_ rules: ((Self) -> Bool)...) -> Bool {
for rule in rules where !rule(self) {
return false
}
return true
}
}extension Model: KPValidation {}
Just with the above few lines, the model will get the ability of validation with pure KeyPath.
let user = viewModel.modelguard user.validate(
\.email.isSome,
\.email!.isEmpty.not,
\.email!.isEmail,
{ $0.email.hasSuffix("@gmail.com") }) else { emailField.becomeFirstResponder()
return
}
Now it becomes more expressive, and the bonus is that all Bool property (including computed) from Foundation framework can be used as validation rule, like String.isEmpty showed in the example, you can also extend them to get more rules if necessary as following code.
public extension Optional {
var isSome: Bool {
return self != nil
}
}public extension Bool {
var not: Bool {
return !self
}
}public extension Int {
var largerThan5: Bool {
return self > 5
}
}
In the above email validation rules, you may notice that KeyPaths are mixed with closure. Yes, KeyPath<Root, Bool> is working with the closure type (Root)->Bool smoothly. KeyPath has transformed into Closure automatically when typecasting.
There are many Bool types in the Foundation framework, that means they are validation rules by handy, here are some extreme examples.
\String.first?.isNumber\DateComponents.isValidDate\[Int].isEmpty
For complicated validation rule, you can just implement it with closure.
KeyPath in Calculation with operators
An operator is a special symbol or phrase that you use to check, change, or combine values. For example, the addition operator (+) adds two numbers, as in let i = 1 + 2, and the logical AND operator (&&) combines two Boolean values, as in if enteredDoorCode && passedRetinaScan.
Take + as an example, we can easily let KeyPath take part in math addition calculation with extensions.
extension KeyPath where Value: AdditiveArithmetic {
public static func + (_ lhs: KeyPath, _ rhs: Value) -> (Root) -> (Value) { { $0[keyPath: lhs] + rhs }
}
...
}public func + <Root, Value: AdditiveArithmetic> (_ lhs: @escaping (Root) -> Value, _ rhs: @escaping (Root) -> Value) -> (Root) -> (Value) { { lhs($0) + rhs($0) }}
And if we continually extend KeyPath to other Swift operators, we could fully embrace KeyPath’s calculation ability.
let closure1: (User) -> Bool = \User.password.count >= 8let closure2: (User) -> Bool = \User.password.count <= 20let closure3: (User) -> Bool = closure1 && closure2
let closure4: (User) -> Int = \User.followingCount + 1let closure5: (User) -> Int = (\User.int1 + 12) * (\User.int2 - 68)
With the help of operators, a KeyPath is transformed into a calculation which can be calculated later.
KeyPath in Collection
One of Swift 5.2’s new features is the ability to use KeyPaths as functions. KeyPath is coordinated with closures when meet collection operators, such as forEach, map, filter. And if we continually extend it to more collection operators: sum, sort(by:), min, max etc, we could also make KeyPath fully embrace functional programming.
extension Sequence { public func sorted<Value: Comparable>(by keyPath: KeyPath<Element, Value>) -> [Element] { return self.sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
}
}
And an alternative sort of a list could be like this:
let users = [user1, user2, user3]let sorted = users.sorted(by: \User.followers.count)
For a complex scenario to find a sorted list of the first follower of admins, and require the admin’s follower number less than 5.
users.filter(\.isAdmin)
.first(\.followers.count < 5 )
.sorted(by: \.followers.count)
.compactMap(\.followers.first)
Swift KeyPath is awesome in many fields: data binding, validation, calculation, collection etc. And there are still many other fields we can leverage KeyPath in our daily programming. So I start this project, it’s all about KeyPath applications. Enjoy Swift 🤟 and until then.