Treehouse Intermediate Course - Part 15 (Delegation 2)

So there just wasn't a chance yesterday to code! But I'm back today. I was thinking more about creating my own little programme to simulate F1 races and still love that. It could be extended to cricket - a ball by ball approach! But that's something for another time. For now, I'm going to complete this section on delegation; once the race horse example is done, THEN I'm going to play around with the code to see how the F1 sim could work! Cool.

Start Time - 08:58

Implementing A Delegate

So we need to get any object to adhere to a particular contract, to loosely communicate with race...sounds complex but we're going to find out more!

Right so this is the example. This is the delegate -

protocol HorseRaceDelegate {
    
    func race(_ race: Race, didStartAt time: Date)
    func addLapLeader(forLap lap: Int, atTime time: Date)
    func race(_ race: Race, didEndAt time: Date, withWinner winner: Horse)

}

Delegates are used with the creation of a protocol. 

*Paused at 09:04 (6 minutes so far)

*Resumed at 09:27 

So just to update, the tracker and broadcaster stored properties as well as the use of them within the other classes etc. That gets rid of the errors. 

Now we are creating a weak (so there isn't a reference cycle for this - we'll come to this more later apparently!) var for delegate, which follows the protocol of the above. 

We've just added :class to the protocol so that it is 'class bound'. We know that SOMEONE will be acting as the delegate. Rather than calling tracker/broadcaster, we call the delegate object. 
A key thing here is we don't know who the delegate is. But it doesn't matter. We made it an optional so if there is no delegate, it will fail without any problems. 

So we are now making Tracker conform to HorseRaceDelegate. An error will come up as we need to provide implementation of the three methods that are in the HRD Protocol. OK that makes sense.

So now we have these methods within the Tracker class, which conforms to the HRD Protocol!

 func race(_ race: Race, didStartAt time: Date) {
         stats.updateValue(time, forKey: Keys.raceStartTime)
    }
    
    func addLapLeader(_ horse: Horse, forLap lap: Int, atTime time: Date) {
        let lapLead = "Horse: \(horse.name), time: \(time)"
        let lapLeadKey = "\(Keys.lapLeader) \(lap)"
        
        stats.updateValue(lapLead, forKey: lapLeadKey)
    }
    
    func race(_ race: Race, didEndAt time: Date, withWinner winner: Horse) {
        stats.updateValue(winner.name, forKey: Keys.winner)
        stats.updateValue(time, forKey: Keys.raceEndTime)
    }


This last part is important for what to code - 

let race = Race(laps: 1, participants: participants)
let tracker = Tracker()

race.delegate = tracker

race.start()

It's just about making sense even though it's complex! 

Acting As A Delegate

A good point is that we've got the same outcome as before. But it's better long term apparently. OK something isn't right with the code - I'm not getting the end of race update info.... Yes gone over and no joy! Will come back to that later....

Yes! Got it! Just missed the print summary function....all that diagnosing for nothing! Anyway, a good time to pause!

Paused at 09:55 (34 minutes so far today)

Continued at 13:25

Back! Working outside so lighting a bit of an issue. All good. Let's continue with the last video in this section.

Having objects with single responsibilities is supposed to be good long-term. So that's why delegation is so useful. 

Now we have RaceBroadcaster class conforming to the HRD protocol. 

OK a much needed analogy of what a delegate is and why we need it! 

CEO of company. Lots of important tasks but these involve other side tasks. You could do it all yourself but it gets in the way of the main job! So tasks need an assistant. 

So you make a list of requirements - think of these as the PROTOCOL. A list of requirements. Now anyone who applies to be your assistant needs those requirements!That's the same as conforming to the protocol. The assistant can now take care of the delegated tasks. Let's say in a few months the assistant quits. Anyone who meets the requirements can apply. The new assistant specialises in a  different subset but still gets the job done. 

So a good point here. ANY object can be the delegate. It doesn't matter what class etc. as long as they contain the key functions or variables that are required in the protocol. OK, that does make sense. 

Delegate properties needed to be weak. Class bound. 

*Paused for approx 1 hour or so

Apparently some of Apple's template code comes like this. Making the delegate properties as weak seems confusing and unclear. 

OK summary - 

1. Create a protocol that defines the set of requirements that the delegate must adhere to.
2. Add a delegate property to the class. The type must be the protocol just defined! 
3. We make this an optional variable property. 
4. It needs to be marked weak due to the reference cycle. 
5. We need to make it class-bound too as only classes can have weak relationships.
6. Objects need to conform to the delegate protocol and provide implementations for the requirements.
7. It is convention to add the word delegate to the class name that we are acting as a delegate of to create the protocol name. E.g. HorseRaceDelegate.


OK! So I've completed this stage on delegates. It's making more and more sense. The next bit is about location manager within an Xcode project. So now is the perfect time to step up and invest some time into creating F1 race simulations! A key point here is that I don't know how well these will actually work. I also do not have to FINISH anything right now! By that, I mean that I can just play around with some of the numbers and codes and come back to it, when I come up with solutions to inevitable problems. 

So, using something from Ray Wenderlich which has stuck - in a good way - here is my 'to do list...'

  1. Copy all of the code over to a new playground. I don't want to be editing/amending certain bits and keeping others. Also the horse racing sim works, so let's keep that!
  2. Change over all of the key class, function and other names so that they are F1 relevant. At this point I will have the exact same sim but with F1 names!
  3. Try out several different sims with a variety of drivers!
  4. Start to develop the 'Driver' class, so there are more variables other than average speed. 
  5. For the 'Race' class, try out other variables e.g. length of lap, number of corners...anything else that comes to mind!
  6. Develop some 'events', which can take place during the race, then get reported. 


OK, so that's plenty. For now, I'm just going to focus on 1. to 3. Just for now!

Right, steps 1 and 2 done!

import UIKit
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true


class Driver {
    let name: String
    let maxSpeed: Double
    var distanceTraveled = 0.0
    var currentLap = 1
    
    init(name: String, maxSpeed: Double) {
        self.maxSpeed = maxSpeed
        self.name = name
    }
    
    var currentSpeed: Double {
        let random = Double(arc4random())
        return random.truncatingRemainder(dividingBy: maxSpeed - 13) + 13
    }
}


protocol F1RaceDelegate: class {
    
    func race(_ race: Race, didStartAt time: Date)
    func addLapLeader(_ driver: Driver, forLap lap: Int, atTime time: Date)
    func race(_ race: Race, didEndAt time: Date, withWinner winner: Driver)
}

class Tracker: F1RaceDelegate {
    
    struct Keys {
        static let raceStartTime = "raceStartTime"
        static let lapLeader = "leaderForLap"
        static let raceEndTime = "raceEndTime"
        static let winner = "winner"
    }
    
    var stats = [String: Any]()
    
    
    func race(_ race: Race, didStartAt time: Date) {
        stats.updateValue(time, forKey: Keys.raceStartTime)
    }
    
    func addLapLeader(_ driver: Driver, forLap lap: Int, atTime time: Date) {
        let lapLead = "Driver: \(driver.name), time: \(time)"
        let lapLeadKey = "\(Keys.lapLeader) \(lap)"
        
        stats.updateValue(lapLead, forKey: lapLeadKey)
    }
    
    func race(_ race: Race, didEndAt time: Date, withWinner winner: Driver) {
        stats.updateValue(winner.name, forKey: Keys.winner)
        stats.updateValue(time, forKey: Keys.raceEndTime)
        printRaceSummary()
    }
    
    func printRaceSummary() {
        print("***********")
        
        let raceStartTime = stats[Keys.raceStartTime]!
        print("Race start time: \(raceStartTime)")
        
        for (key, value) in stats where key.contains(Keys.lapLeader) {
            print("\(key): \(value)")
        }
        
        let raceEndTime = stats[Keys.raceEndTime]!
        print("Race end time: \(raceEndTime)")
        
        let winner = stats[Keys.winner]!
        print("Winner: \(winner)")
        
        print("***********")
    }
}


//class RaceBroadcaster: F1RaceDelegate {
//
//    func race(_ race: Race, didStartAt time: Date) {
//
//    }
//
//    func addLapLeader(_ driver: Driver, forLap lap: Int, atTime time: Date) {
//
//    }
//
//    func race(_ race: Race, didEndAt time: Date, withWinner winner: Driver) {
//
//    }
//
//}

class Race {
    let laps: Int
    let lapLength: Double = 300
    let participants: [Driver]
    
    weak var delegate: F1RaceDelegate?
    
    
    lazy var timer: Timer = Timer(timeInterval: 1, repeats: true) { timer in
        self.updateProgress()
    }
    
    init(laps: Int, participants: [Driver]) {
        self.laps = laps
        self.participants = participants
    }
    
    func start() {
        // RunLoop.main.add(timer, forMode: .RunLoop.Mode.default)
        RunLoop.main.add(timer , forMode: .default)
        delegate?.race(self, didStartAt: Date())
        print("Race in progress...")
    } // END func start()
    
    func updateProgress() {
        print("....")
        for driver in participants {
            driver.distanceTraveled += driver.currentSpeed
            
            if driver.distanceTraveled >= lapLength {
                driver.distanceTraveled = 0
                
                delegate?.addLapLeader(driver, forLap: driver.currentLap, atTime: Date())
                
                driver.currentLap += 1
                
                if driver.currentLap >= laps + 1 {
                    delegate?.race(self, didEndAt: Date(), withWinner: driver)
                    stop()
                    break
                }
            }
        }
    }
    
    func stop() {
        print("Race complete!")
        timer.invalidate()
        
        
    }
}



let hamilton = Driver(name: "Lewis Hamilton", maxSpeed: 20)
let verstappen = Driver(name: "Max Verstappen", maxSpeed: 19)
let bottas = Driver(name: "Valterri Bottas", maxSpeed: 15)
let vettel = Driver(name: "Sebastian Vettel", maxSpeed: 14)
let leclerc = Driver(name: "Charles Leclerc", maxSpeed: 14)

let participants = [hamilton, verstappen, bottas, vettel, leclerc]

let race = Race(laps: 1, participants: participants)
let tracker = Tracker()
//let broadcaster = RaceBroadcaster()

race.delegate = tracker

race.start()


Again, this is not my code! I've just adapted it very slightly so it applies to F1. The key thing here is trying to understand the various machinations at work, which I can only do by playing around with the code. Let's just go through it all...

OK! So I've also added in a team enum. Now you can specify the team the driver drives for, and that is also added to the protocol so that the end race result reflects that! A small detail but fun to add. 

Finish Time - 15:26 (Total time today 1 hour 30 - approx)

Right, that's a good time to stop for now. I have so many ideas about where to continue this, but am not sure where or how to start! The key thing is, again, to play around and get an idea of what controls what. How a line of code affects the outcome. What it all means! But this is good...I have so much to play around with here, I'm looking forward to trying out some more!





Comments

Popular posts from this blog

*Xcode Project Entry 2* F1 Quiz - part 1

Angela Yu Course Part 10 (up to lesson 112)

Angela Yu Xcode 12 Course - Part 7 (lectures 74 to 79)