• 20 hours
  • Medium

Free online content available in this course.

Videos available in this course

Certificate of achievement available at the end this course

Got it!

Optimize the object initialization

Log in or subscribe for free to enjoy all this course has to offer!

We are already familiar with the initialization process. And we know that an object must be initialized before it can be utilized.

Now we have a requirement for an object initialization itself: All the stored variables must be initialized by the end of object initialization!

In this chapter we will learn various aspects that surround this requirement.

We know a number of ways to initialize variables during the declaration:

  • assign a value

  • mark it as optional and leave it as nil

  • assign a default type value by using a type initializer

Designated and Convenience Initializers

Other then having to initialize all the values at declaration, we can create initializing methods - initializers!

There are 2 types of initializers:

  • Designated initializers

  • Convenience initializers 

Designated initializers are the primary initializers.  They cover options necessary to complete the initialization process. Unless we initialize all the properties at declaration, a class must have at least one designated initializer.

Earlier in this course we learned that we need to use keyword  init to declare initializers. This applies to both types of initializers: designated and convenience. 

Let's look back at out Rect class example. If we decide not to assign values at declaration to the length and width properties of our Rect object, we have to have a designated initializer that takes 2 parameters: length and width and assigns those values to the corresponding object variables:

class Rect {
    var length: Double
    var width: Double

    init(length: Double, width: Double) {
        self.length = length
        self.width = width
    }
}

The convenience initializers, on the other hand, are not mandatory. They are essentially helpers that make developers’ lives easier and cover common cases. 

A declaration of such an initializer requires using the keyword   convenience   placed right before the initializer's declaration.

So we could implement a basic init() method with no parameters as a convenience method. Within it, call our designated method (which requires 2 parameters) by providing some default values to it:

convenience init() {
    self.init(length: 2.0, width: 6.0)
}

That's quite convenient, isn't it?

Now let's complicate things a bit and look at a couple more requirements:

  • If a class is a subclass (has a parent class), its designated initializers have to call one of the parent's designated initializers (NOT convenience). A parent initializer has to be called at the end of a child's initialization.

  • A convenience method has to call one of the designated initializers of a class (of itself).    

To demonstrate how this works, let's alter our Rect class and make it a subclass of a Shape class:

class Shape {
    var positionX: Double
    var positionY: Double
    
    init(positionX: Double, positionY: Double) {
        self.positionX = positionX
        self.positionY = positionY
    }
    
    convenience init() {
        self.init(positionX: 0.0, positionY: 0.0)
    }
}

class Rect: Shape {
    var length: Double
    var width: Double

    init(length: Double, width: Double) {
        self.length = length
        self.width = width
        // calling a DESIGNATED parent initializer
        // calling a parent initializer at THE END of current initialization - when all local variables are initialized
        super.init(positionX: 0.0, positionY: 0.0)
        // Here we could do something else that' i's not related to initialization. But why would we? - This is an initializer after all...
    }

    convenience init() {
        self.init(length: 2.0, width: 6.0)
    }
}

Let's see how we fulfill the requirements in Rect class:

  • our designated initializer is calling a parent's designated initializer at the end of the initialization.

  • our convenience initializer calls its own designated initializer. 

Improving NeXTDestination

It's reasonable to assume that a choice of destinations and entertainment options plays a big role in our travel experience. We need to include some appealing options in our initial data. We can do that by conveniently wrapping this function into convenience initializers.

Start with the Destination class and create a convenience initializer that would generate an array of entertainment options for the current destination from a pool of the general options. Then, pass it to the designated initializer with the rest of parameters: name and cost.

To make it more interesting, let's do the following:

  • Each destination will offer a different number of options: use a random number between 1 and the number of elements in the general array. Then randomly pick that number of elements from the general array.

  • Assume that we visit each place only once

Implementation tip: for generating random numbers starting with 1, use this expression:

var pool = Entertainment.generalOptions
let numberOfOptions = Int(arc4random_uniform(UInt32(pool.count))) + 1

By incrementing the result of the random function we guarantee it starting from 1, not 0.

Try to do it on your own:

// No scrolling yet!





















import Foundation

    /* ... */
    
class Destination {
    /* ... */
    
    convenience init(name: String, cost: Double) {
        var options = [Entertainment]()
        var pool = Entertainment.generalOptions
        let numberOfOptions = Int(arc4random_uniform(UInt32(pool.count))) + 1
        
        if pool.count > 0 {
            for _ in 0 ..< numberOfOptions {
                let randomIndex = Int(arc4random_uniform(UInt32(pool.count)))
                options.append(pool[randomIndex])
                pool.remove(at: randomIndex)
            }
        }
        self.init(name: name, cost: cost, entertainmentOptions: options)
    }
    
    /* ... */
}

Great! Now let's do the same for Adventure class. Except, we don't have general options for destinations :euh:.

Well, come up with 5 or more on your own - the more the merrier! And create a convenience initializer. This time we don't need a random selection, just use them all:

// No scrolling yet!
























class Adventure {
    /* ... */
    convenience init() {
        var options = [
            Destination(name: "New York City", cost: 800.00),
            Destination(name: "Buenos Aires", cost: 1200.00),
            Destination(name: "Dubai", cost: 1800.00),
            Destination(name: "Tokyo", cost: 2000.00),
            Destination(name: "Paris", cost: 900.00),
            Destination(name: "Rome", cost: 1050.00),
            Destination(name: "Lisbon", cost: 600.00),
                       ]
        self.init(destinations: options)
    }
    /* ... */
}

Good progress! We can do a bit more - alter the implementation of pickDestination() and pickEntertainment() methods.  

Picking options

Let's start with pickEntertainment() method. Here are the requirements:

  • Pick a new entertainment option that fits within the remaining budget, and use the entertainmentOptions property to choose from them.

  • Assume we want to ensure that each option only once.

  • Return nil if one of the following criteria is met:

    • we already experienced everything available for current destination

    • there's nothing available to fit our remaining budget

Implementation tip: To ensure we pick each option only once, we can use a  compare   method available for arrays:

accomplishments.contains(where: {$0.name == nextEntertainment.name})

This command will tell us whether we've already done a given option.

As usual, try on your own:

// No scrolling yet!
























class Destination {
    /* ... */
    func pickEntertainment(availableBudget: Double) -> Entertainment? {
        
        var pool = entertainmentOptions
        
        if pool.count == 0 {
            return nil
        }
        
        while true { // going through all the elements randomly to see if anything fits.
            let randomIndex = Int(arc4random_uniform(UInt32(pool.count)))
            let nextEntertainment = pool[randomIndex]

            if nextEntertainment.cost <= availableBudget && !accomplishments.contains(where: {$0.name == nextEntertainment.name}) {
                return nextEntertainment // return once found a suitable element
            }

            pool.remove(at: randomIndex)
            if pool.count == 0 {
                return nil // returning once nothing left to check
            }
        }
    }
    /* ... */
}

Let's see what's done here:

  • Using an infinite while loop to do the work until we are satisfied with the result.

  • Copying an available pool of options

  • Extracting a random element from the pool and checking if it meets our criteria

    • returning the element if it's suitable 

    • and continuing to explore other elements until all of them are checked

And now, complete a similar implementation for pickDestination() method. Here're the requirements:

  • Pick a new destination that fits within the remaining budget.

  • Assume we want to visit each destination only once.

  • Return nil if one of the following criteria is met:

    • we already visited max destinations

    • there's nothing available to fit our remaining budget

Try on your own:

// No scrolling yet!
























class Adventure {
    /* ... */
    func pickDestination() -> Destination? {
        var pool = destinations
        
        if pool.count == 0 || placesVisited.count >= Adventure.maxDestinations{
            return nil
        }
        
        while true { // going through all the elements randomly to see if anything fits.
            
            let randomIndex = Int(arc4random_uniform(UInt32(pool.count)))
            let nextDestination = pool[randomIndex]
            
            if nextDestination.cost <= amountRemaining && !placesVisited.contains(where: {$0.name == nextDestination.name}) {
                return nextDestination // return once found a suitable element
            }
            
            pool.remove(at: randomIndex)
            if pool.count == 0 {
                return nil // returning once nothing left to check
            }
        }
    }
    /* ... */
}

 This implementation is very similar to the pickEntertainment method.

Simulating the journey

Ready for the final round in this chapter :pirate:?

And finally, this is a good time to add a couple more methods to actually simulate our journey.

Let's start with the Destination class and add a   haveFun()  method to simulate collecting accomplishments. It needs to consider the currently available budget and then report to the Adventure instance the total cost of accomplishments, so that it can be adjusted for the next destination. Here's the declaration:

func haveFun(availableBudget: Double) -> Double {
    var amountSpent = 0.0
    /* ... */
    return amountSpent
}

Next, we need to implement the rest of the logic in this method.

Implementation tips:

  • Before starting the simulation, make sure to clear accomplishment properties.

  • Loop through picking a new entertainment until no more new entertainments can be selected, you can use this condition: 

    while let nextAccomplishment = pickEntertainment(availableBudget: budget) {
    }
  • Collect suitable entertainment elements in the accomplishments property

  • Remember to adjust the available budget each time you add a new accomplishment

  • Collect the total amount spent on accomplishments to return it.

Your turn!

// No scrolling yet!

























class Destination {
    /* ... */
    func haveFun(availableBudget: Double) -> Double {
        accomplishments.removeAll()
        var amountSpent = 0.0
        var budget = availableBudget
        
        while let nextAccomplishment = pickEntertainment(availableBudget: budget) {
            accomplishments.append(nextAccomplishment)
            amountSpent += nextAccomplishment.cost
            budget -= nextAccomplishment.cost
        }
        return amountSpent
    }
    /* ... */
}

Let's review what's done here:

  • Cleared the accomplishments property (we could also assign a brand new empty array to the variable instead).

  • Declared variables to keep track of current expenses (will increase with each accomplishment) and adjust budget (will decrease with each accomplishment).

  • Looped through suitable entertainment options until no more were left, then added them to the accomplishments and adjusted the finances.

  • Returned the total cost of all accomplishments.

And now a sibling method for the Adventure class: add the letsGo() method to simulate collecting visited places. It also needs to consider currently available budget which is available within the Adventure instance, so we don't need a parameter for that and we don't need to return anything!

Implementation tips:

  • Before starting the simulation, make sure to clear related properties: placesVisited, amountSpent.

  • Loop through picking a new destination until no more new destinations can be selected

  • Collect suitable destinations in the placesVisited property

  • Remember to adjust the available budget each time you add a new accomplishment

Alright, the playground is all yours!

// No scrolling yet!

























class Adventure {
    /* ... */
    func letsGo() {
        placesVisited.removeAll()
        amountSpent = 0.0
        while let nextDestination = pickDestination() {
            placesVisited.append(nextDestination)
            amountSpent += nextDestination.cost
            amountSpent += nextDestination.haveFun(availableBudget: amountRemaining)
        }
    }
    /* ... */
}

Our code is now almost complete - there's just a tiny bit more left to do :pirate:!

Let's Recap!

  • At the end of the initialization, all stored properties must be initialized.

  • There are two types of initializers:

    • Designated: primary initializers that have to provide functionality to initialize all stored properties.

    • Convenience: optional initializers are helpers that cover common cases for initialization.

  • If a class is a subclass, its designated initializers must use a designated initializer of a parent class. A parent initializer must be called at the end of initialization of a subclass.

  • A convenience initializer must use a designated initializer of the same class.

Example of certificate of achievement
Example of certificate of achievement