GameplayKit Validators

GameplayKit introduced a load of really useful data structures and algorithms when iOS 9 was released and I've barely scratched the surface of them.

I'm not a game developer so I thought nothing of them but recently I've been going there first when trying to implement various systems. Previously this was using the GKStateMachine to manage which part of the app the user was in.

Recently another use has come up and it all started with this post from Scott Robbins about Composite Validators it's a great post and I've found it very useful. However, for my specific need it didn't feel flexible enough. My validator needed to validate more than just a String and ideally I'd like something that can validate anything.

At the same time I saw this series of tweets from Rob Napier.

and I felt it was about time to break out GameplayKit again.

GKRuleSystem and GKRule

The GKRuleSystem works together with GKRule to take a complex set of rules and states and makes evaluating them a modular thing that you can then inspect to find out various facts about your system.

The example shown by Apple is for use in a SpriteKit entity and allows the entity to determine what it should be doing based on a series of rules and states. For instance, if the entity can't see the player it should be searching for the player. If it can see the player but is out of range it should be chasing. If it is within range it should be attacking etc...

Anyway, this seemed like a good option for my validator.

Facts and Rules

I created a Fact object that would encapsulate my rule sets...

class Fact<T: Error>: NSObject {
    let error: T
    let salience: Int
    let evaluationBlock: (_ system: GKRuleSystem) -> (Bool)
    init(error: T, salience: Int = 0, evaluationBlock: @escaping (_ system: GKRuleSystem) -> (Bool)) {
        self.error = error
        self.evaluationBlock = evaluationBlock
        self.salience = salience
    }
}

and a FactBasedRule that can take a Fact and update the GKRuleSet...

class FactBasedRule<T: Error>: GKRule {
    let fact: Fact<T>

    init(fact: Fact<T>) {
        self.fact = fact
        super.init()
        salience = fact.salience
    }

    override func evaluatePredicate(in system: GKRuleSystem) -> Bool {
        return fact.evaluationBlock(system)
    }

    override func performAction(in system: GKRuleSystem) {
        system.assertFact(fact)
    }
}

And some errors to return to let me know what went wrong...

enum PasswordError: Error {
    case passwordsDontMatch
    case notLongEnough
    case uppercaseMissing
    case lowerCaseMissing
    case noNumbers
    case spacesNotAllowed
}

Validators

Now I was able to create the logic that would do the validation. So to validate that the password has an uppercase letter I was able to use the following Fact...

let uppercaseLetterFact = Fact(error: PasswordError.uppercaseMissing) {
    system in
    guard let password = system.state["password"] as? String,
        password.rangeOfCharacter(from: .uppercaseLetters) != nil else {
            return false
    }
    return true
}

And other similar Facts for the other rules such as one to check that the password and "confirmed" password are the same...

let passwordsMatchFact = Fact(error: PasswordError.passwordsDontMatch, salience: 1) {
    system in
    guard let password = system.state["password"] as? String,
        let confirmedPass = system.state["confirmedPassword"] as? String,
        password == confirmedPass else {
            return false
    }
    return true
}

And the method to validate them and return the first error...

func validatePasswords(password: String, confirmedPassword: String) -> ValidationResult<PasswordError> {
    // create the facts
    let facts = [lengthFact, passwordsMatchFact, uppercaseLetterFact, lowercaseLetterFact, numberFact, noWhiteSpaceFact]
    let ruleSystem = GKRuleSystem()
    ruleSystem.add(facts.map(FactBasedRule.init))
    
    // add the state to the rule system
    ruleSystem.state["password"] = password
    ruleSystem.state["confirmedPassword"] = confirmedPassword
    
    // evaluate
    ruleSystem.evaluate()
    
    // extract the errors from the facts created by the evaluation
    let error = facts.filter { ruleSystem.grade(forFact: $0) == 0 }
                     .sorted { $0.salience > $1.salience }
                     .map { $0.error }
                     .first
        
    if let error = error {
        return .invalid(error: error)
    } else {
        return .valid
    }
}

This seems to work really well 🙂

PasswordValidator().validatePasswords(password: "Hell  ld1", confirmedPassword: "Hell  ld1")

This evaluates to PasswordError.noSpacesAllowed which is exactly what it should evaluate to. When everything is correct the returned error is nil.

Conclusion

So this is what I came up with after a couple of attempts and it seems to work quite well. Is it the best way to approach this? Probably not but for me it was a bit of fun and I ended up with something I might actually use.

I don't get the benefit of combining multiple validation systems into one like Scott does with the email and password validators (I really like that in the composite pattern). But it would be fairly straight forward to create a struct that has two rule systems. Or even one that uses a single rule system with different agendas to validate email/password.

What you do get from this is that the state held by the rule system can contain anything (literally [String: Any]). So you can validate multiple items and items of different types etc...

Anyway, I love exploring off the beaten track in iOS and it can show up some really nice systems.

You can get all my code at the gist I created here.

Update

I have updated the code slightly to return a Result enum much like Scott did in his code. When I went to use the validator I realised that having an enum with valid/invalid is much better than having an optional error.

I have amended the gist also.

Leave a Reply

Your email address will not be published. Required fields are marked *