• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 7/4/19

Protect your classes and objects

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

In this chapter, we will refer to one of the OOP principles - encapsulation - which is hiding the implementation from outsiders and exposing only what’s required to efficiently utilize an object.

Access control

We are going adopt the idea of access control by implementing restricted access to a class, module or file.

We know what class is and we know what file is. We’ve been working with them!

Module here sounds like a new term.

A module is a bundle of related source files associated with a name, like a framework. It could be like Foundation and UIKit.

Also know that those frameworks implement much more functionality than we, as developers, can utilize directly. That’s done by restricting access to the details of implementation, also known as implementing  access control.

Control levels

There 4 levels of control available to us:

  • Open: you can access open classes and class members from any source file. Subclassing and overriding don't have restrictions.

  • Public: is similar to Open; however, subclassing and overriding restrictions apply.

  • Internal: these elements are accessible from all files of a module in which our declaration is located and not accessible from other modules.

  • Private to file: those elements are accessible only within that file.

  • Private: private elements are only accessible in the context in which they are defined. For example, if a method is private, it can only be used inside the class in which it is located.

By default, all items are marked as Internal; this makes them accessible within a module.

We can get the responsibility of careful utilization of object's components off developers' shoulders by applying control levels to the components of our classes: properties and methods.

To do that, we need to use one of the 4 keywords for each control level respectively:

  • open

  • public

  • internal

  • fileprivate

  • and private 

And place a suitable keyword in front of the related declaration:

class Unicorn {
    // properties
    private var height = 170
    public var power = Double.infinity
    
    // methods
    private func sleep() {
    }
    
    public func run() {
    }
}

Then, if we try to access private members from outside of the class, we get errors:

var unicorn = Unicorn()

print(unicorn.power) // Ok
unicorn.height = 180 // Error
unicorn.sleep() // Error
unicorn.run() // Ok

The control levels can be assigned to class elements as well as classes:

public class PublicClass {
    
}

private class PrivateClass {
    
}

Readability

In addition to security, specifying control levels for class members provides for better readability. When a developer is readying a source file, it's always clear which elements can be used externally.

Hierarchy of control

An element may have the same or more restrictive control level then its containing element:

public class PublicClass {
    
    public var publicProperty = true
    
    internal var internalProperty = 0
    
    fileprivate let fileprivateProperty = "Hello!"
    
    private func privateMethod() {
        
    }
}

In the example above, the class is declared as public, so that all elements of that class may be of the same or lesser level of exposure. In this case, it includes all levels.

If we declare a class as fileprivate, its elements can only be fileprivate and private:

fileprivate class FileprivateClass {
    
    var defaultProperty = true // automatically assigned fileprivate
    
    public var publicProperty = true // automatically downgraded to fileprivate

    internal var internalProperty = 0 // automatically downgraded to fileprivate
    
    fileprivate let fileprivateProperty = "Hello!"
    
    private func privateMethod() {
        
    }
}

In the example above, we've added a property without an explicit access control keywords. In this scenario, by default, it assumes the level of the containing element - the class - fileprivate

When declaring a variable of a class, if control level of the declaration context is higher than the one of the class, the variable must also have an explicit control level. Let's declare a FileprivateClass variable in the context of our playground file:

var a = FileprivateClass() // Error
fileprivate var b = FileprivateClass() // Ok
private var c = FileprivateClass() // Ok

As we can observe, if the default access level of variable's context is higher than a class we are assigning to it, we must explicitly specify the variable's level as the same or lower than the one of the class.

Improving NeXTDestination

Let's look at our code and identify which elements in our classes need not be accessible outside the class. Here's our current state of code:

import Foundation

class Entertainment {
    //properties
    static let generalOptions = Entertainment.generateGeneralOptions()
    var name: String
    var cost: Double
    var location: String
    
    static func generateGeneralOptions() -> [Entertainment] {
        var options = [Entertainment]()
        
        // activities
        let activity = Activity(name: "Horseback riding", cost: 60)
        activity.difficulty = .intermediate
        activity.equipmentRequired = "A helmet"
        activity.weatherConditions = "No heavy rain"
        options.append(activity)
        
        // arts
        let arts = Arts(name: "MoMA. Items: Is Fashion Modern?", cost: 12)
        arts.genre = .modern
        arts.teaser = "Items: Is Fashion Modern? explores the present, past—and sometimes the future—of 111 items of clothing and accessories that have had a strong impact on the world in the 20th and 21st centuries—and continue to hold currency today. "
        arts.ageLimit = 0
        options.append(arts)
        
        // gastronomy
        let gastronomy = Gastronomy(name: "Horseback riding", cost: 60)
        gastronomy.cuisine = "Italian"
        gastronomy.dressCode = .casual
        gastronomy.minNumberOfPeople = 4
        options.append(gastronomy)
        
        return options
    }
    
    //initializer
    init(name: String, cost: Double) {
        self.name = name
        self.cost = cost
        location = ""
    }
    
    func tellStory() -> String {
        return "It was fun to experience \(name)"
    }
}

enum Difficulty: String {
    case hard = "Hard"
    case intermediate = "Intermediate"
    case easy = "Easy"
}

enum Genre: String {
    case classical = "Classical"
    case modern = "Modern"
    case historical = "Historical"
}

enum DressCode: String {
    case streetwear = "Streewear"
    case casual = "Casual"
    case formal = "Formal"
}

class Activity: Entertainment {
    var weatherConditions = ""
    var equipmentRequired = ""
    var difficulty: Difficulty = .intermediate
    
    override func tellStory() -> String{
        // intro - print using the parent implementation
        super.tellStory()
        
        var story = " "
        
        // mention about weather requirements
        story += "Oh, the weather requirement: \(weatherConditions). "
        
        // tell about equipment requirements
        story += "And the equipment: \(equipmentRequired). "
        
        // finish off with the financial impact:)
        if cost <= 30 {
            story += "All that for ONLY $\(cost). "
        }
        else if cost >= 100 {
            story += "It was costly - $\(cost) - but definitely worth it. "
        }
        else {
            story += "It was afforable at a price of $\(cost). "
        }
        
        // share the full story
        return story
    }
}

class Arts: Entertainment {
    var genre: Genre = .modern
    var teaser = ""
    var ageLimit = 0
    
    override func tellStory() -> String{
        // intro
        var story = "\(name) was an interesting experience! "
        
        // talk about genre
        story += " It was \(genre.rawValue) - "
        switch genre {
        case .modern:
            story += " my cup of tea! "
        default:
            story += " I'm not a big fan, but it was educational! "
        }
        
        story += teaser
        
        // mention age restrictions
        if ageLimit > 0 {
            story += "You must be at least \(ageLimit) years of age to participate. "
        }
        else {
            story += "It's suitable for everyone. "
        }
        
        // finish off with the financial impact:)
        story += "It cost me $\(cost). "
        
        // share the full story
        return story
    }
}

class Gastronomy: Entertainment {
    var dressCode: DressCode = .casual
    var cuisine = ""
    var minNumberOfPeople = 1
    
    override func tellStory() -> String{
        // intro
        var story = "\(name) was outstanding! "
        
        // tell about cuisine
        story += "\(cuisine) cuisine was delicious. "
        
        // mention booking requirements
        if minNumberOfPeople > 1 {
            story += "I could go by myself - loved the food! "
        }
        else {
            story += "The place required minimum \(minNumberOfPeople) for booking. We all loved the food! "
        }
        
        // share the dress code info
        story += "There was a \(dressCode.rawValue) - "
        switch dressCode {
        case .streetwear:
            story += " no need change before going! "
        case .casual:
            story += " nothing fancy, very simple! "
        case .formal:
            story += " - a great opportunity to dress up! "
        }
        
        // finish off with the financial impact:)
        story += "All that for $\(cost). "
        
        // share the full story
        return story
    }
}

class Destination {
    // properties
    var name: String
    var cost: Double
    var entertainmentOptions: [Entertainment]
    var accomplishments: [Entertainment]
    
    // initializer
    init(name: String, cost: Double, entertainmentOptions: [Entertainment]) {
        self.name = name
        self.cost = cost
        self.entertainmentOptions = entertainmentOptions
        accomplishments = []
    }
    
    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)
    }
    
    // methods
    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
    }
    
    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
            }
        }
    }
    
    // tell story
    func tellStory() -> String {
        
        // create a variable to collect the sum of accomplishments cost
        var accomplishmentsCost = 0.0
        
        // intro
        var story = "It was great to visit \(name) \n"
        
        // summary
        if accomplishments.count > 0 {
            story += "While there I was lucky to have an opportunity to do \(accomplishments.count) things: \n"
        }
        else {
            story += "Sadly I wasn't able to do anythng there:( \n"
        }
        
        for entertainment in accomplishments {
            
            // add individual story
            story += entertainment.tellStory()
            
            // highlight sublass specific info
            if let activity = entertainment as? Activity {
                story += "I liked that it was \(activity.difficulty) \n"
            }
            else if let arts = entertainment as? Arts {
                story += "I liked that it was of \(arts.genre) genre \n"
            }
            else if let gastronomy = entertainment as? Gastronomy {
                story += "I liked that it was of \(gastronomy.cuisine) cuisine \n"
            }
            
            // include the cost in total
            accomplishmentsCost += entertainment.cost
        }
        
        // financial impact
        story += "The visit without entertainment cost me $\(cost). \n"
        story += "The total cost including entertainment amounted to $\(accomplishmentsCost + cost). \n"
        
        return story
    }
}

class Adventure {
    // static properties
    static let maxDestinations = 1
    static let totalBudget = 10000.0
    static let returnCost = 800.0
    
    // properties
    var amountSpent: Double
    var destinations: [Destination]
    var placesVisited: [Destination]
    
    var amountRemaining: Double {
        return Adventure.totalBudget - Adventure.returnCost - amountSpent
    }
    
    // initializer
    init(destinations: [Destination]) {
        self.destinations = destinations
        amountSpent = 0.0
        placesVisited = []
    }
    
    convenience init() {
        let 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)
    }
    
    // methods
    func letsGo() {
        placesVisited.removeAll()
        amountSpent = 0.0
        while let nextDestination = pickDestination() {
            placesVisited.append(nextDestination)
            amountSpent += nextDestination.cost
            amountSpent += nextDestination.haveFun(availableBudget: amountRemaining)
        }
    }
    
    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
            }
        }
    }
    
    func tellStory() -> String {
        return ""
    }
}

Identify methods that need a different control level and make alterations to the code accordingly.

And do it by yourself first, of course :p:

// No scrolling yet!

























// private methods
class Entertainment {
    /* ... */
    private static func generateGeneralOptions() -> [Entertainment] {...}
    /* ... */
}

class Destination {
    /* ... */
    private func pickEntertainment(availableBudget: Double) -> Entertainment? {...}
    /* ... */
}

class Adventure {
    /* ... */
    private func pickDestination() -> Destination? {...}
    /* ... */
}

This is great, except we are missing one grand component: the actual implementation of the tellStory method for the Adventure class.

Here are the requirements:

  • Make an introduction

  • Loop  through the visited destinations and collect the stories

  • Make a conclusion

Your turn!

// No scrolling yet!
























class Adventure {
    /* ... */
    func tellStory() -> String {
        // intro
        var story = "My Grand Adventure - OOP in Swift! \n\n"
        story += "I've visited \(placesVisited.count) destinations! \n\n"
        
        // stories for destinations
        for destination in placesVisited {
            story += destination.tellStory() + "\n"
        }
        
        // conclusion
        story += "It was a fun adventure!"
        
        return story
    }
    /* ... */
}

And finally, we can enjoy the results:

var adventure = Adventure()
adventure.letsGo()
print(adventure.tellStory())

To sum up, heres the complete code of the NeXTDestination project:

import Foundation

class Entertainment {
    //properties
    static let generalOptions = Entertainment.generateGeneralOptions()
    var name: String
    var cost: Double
    var location: String
    
    private static func generateGeneralOptions() -> [Entertainment] {
        var options = [Entertainment]()
        
        // activities
        let activity = Activity(name: "Horseback riding", cost: 60)
        activity.difficulty = .intermediate
        activity.equipmentRequired = "A helmet"
        activity.weatherConditions = "No heavy rain"
        options.append(activity)
        
        // arts
        let arts = Arts(name: "MoMA. Items: Is Fashion Modern?", cost: 12)
        arts.genre = .modern
        arts.teaser = "Items: Is Fashion Modern? explores the present, past—and sometimes the future—of 111 items of clothing and accessories that have had a strong impact on the world in the 20th and 21st centuries—and continue to hold currency today. "
        arts.ageLimit = 0
        options.append(arts)
        
        // gastronomy
        let gastronomy = Gastronomy(name: "Horseback riding", cost: 60)
        gastronomy.cuisine = "Italian"
        gastronomy.dressCode = .casual
        gastronomy.minNumberOfPeople = 4
        options.append(gastronomy)
        
        return options
    }
    
    //initializer
    init(name: String, cost: Double) {
        self.name = name
        self.cost = cost
        location = ""
    }
    
    func tellStory() -> String {
        return "It was fun to experience \(name)"
    }
}

enum Difficulty: String {
    case hard = "Hard"
    case intermediate = "Intermediate"
    case easy = "Easy"
}

enum Genre: String {
    case classical = "Classical"
    case modern = "Modern"
    case historical = "Historical"
}

enum DressCode: String {
    case streetwear = "Streewear"
    case casual = "Casual"
    case formal = "Formal"
}

class Activity: Entertainment {
    var weatherConditions = ""
    var equipmentRequired = ""
    var difficulty: Difficulty = .intermediate
    
    override func tellStory() -> String{
        // intro - print using the parent implementation
        super.tellStory()
        
        var story = " "
        
        // mention about weather requirements
        story += "Oh, the weather requirement: \(weatherConditions). "
        
        // tell about equipment requirements
        story += "And the equipment: \(equipmentRequired). "
        
        // finish off with the financial impact:)
        if cost <= 30 {
            story += "All that for ONLY $\(cost). "
        }
        else if cost >= 100 {
            story += "It was costly - $\(cost) - but definitely worth it. "
        }
        else {
            story += "It was afforable at a price of $\(cost). "
        }
        
        // share the full story
        return story
    }
}

class Arts: Entertainment {
    var genre: Genre = .modern
    var teaser = ""
    var ageLimit = 0
    
    override func tellStory() -> String{
        // intro
        var story = "\(name) was an interesting experience! "
        
        // talk about genre
        story += " It was \(genre.rawValue) - "
        switch genre {
        case .modern:
            story += " my cup of tea! "
        default:
            story += " I'm not a big fan, but it was educational! "
        }
        
        story += teaser
        
        // mention age restrictions
        if ageLimit > 0 {
            story += "You must be at least \(ageLimit) years of age to participate. "
        }
        else {
            story += "It's suitable for everyone. "
        }
        
        // finish off with the financial impact:)
        story += "It cost me $\(cost). "
        
        // share the full story
        return story
    }
}

class Gastronomy: Entertainment {
    var dressCode: DressCode = .casual
    var cuisine = ""
    var minNumberOfPeople = 1
    
    override func tellStory() -> String{
        // intro
        var story = "\(name) was outstanding! "
        
        // tell about cuisine
        story += "\(cuisine) cuisine was delicious. "
        
        // mention booking requirements
        if minNumberOfPeople > 1 {
            story += "I could go by myself - loved the food! "
        }
        else {
            story += "The place required minimum \(minNumberOfPeople) for booking. We all loved the food! "
        }
        
        // share the dress code info
        story += "There was a \(dressCode.rawValue) - "
        switch dressCode {
        case .streetwear:
            story += " no need change before going! "
        case .casual:
            story += " nothing fancy, very simple! "
        case .formal:
            story += " - a great opportunity to dress up! "
        }
        
        // finish off with the financial impact:)
        story += "All that for $\(cost). "
        
        // share the full story
        return story
    }
}

class Destination {
    // properties
    var name: String
    var cost: Double
    var entertainmentOptions: [Entertainment]
    var accomplishments: [Entertainment]
    
    // initializer
    init(name: String, cost: Double, entertainmentOptions: [Entertainment]) {
        self.name = name
        self.cost = cost
        self.entertainmentOptions = entertainmentOptions
        accomplishments = []
    }
    
    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)
    }
    
    // methods
    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
    }
    
    private 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
            }
        }
    }
    
    // tell story
    func tellStory() -> String {
        
        // create a variable to collect the sum of accomplishments cost
        var accomplishmentsCost = 0.0
        
        // intro
        var story = "It was great to visit \(name) \n"
        
        // summary
        if accomplishments.count > 0 {
            story += "While there I was lucky to have an opportunity to do \(accomplishments.count) things: \n"
        }
        else {
            story += "Sadly I wasn't able to do anythng there:( \n"
        }
        
        for entertainment in accomplishments {
            
            // add individual story
            story += entertainment.tellStory()
            
            // highlight sublass specific info
            if let activity = entertainment as? Activity {
                story += "I liked that it was \(activity.difficulty) \n"
            }
            else if let arts = entertainment as? Arts {
                story += "I liked that it was of \(arts.genre) genre \n"
            }
            else if let gastronomy = entertainment as? Gastronomy {
                story += "I liked that it was of \(gastronomy.cuisine) cuisine \n"
            }
            
            // include the cost in total
            accomplishmentsCost += entertainment.cost
        }
        
        // financial impact
        story += "The visit without entertainment cost me $\(cost). \n"
        story += "The total cost including entertainment amounted to $\(accomplishmentsCost + cost). \n"
        
        return story
    }
}

class Adventure {
    // static properties
    static let maxDestinations = 1
    static let totalBudget = 10000.0
    static let returnCost = 800.0
    
    // properties
    var amountSpent: Double
    var destinations: [Destination]
    var placesVisited: [Destination]
    
    var amountRemaining: Double {
        return Adventure.totalBudget - Adventure.returnCost - amountSpent
    }
    
    // initializer
    init(destinations: [Destination]) {
        self.destinations = destinations
        amountSpent = 0.0
        placesVisited = []
    }
    
    convenience init() {
        let 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)
    }
    
    // methods
    func letsGo() {
        placesVisited.removeAll()
        amountSpent = 0.0
        while let nextDestination = pickDestination() {
            placesVisited.append(nextDestination)
            amountSpent += nextDestination.cost
            amountSpent += nextDestination.haveFun(availableBudget: amountRemaining)
        }
    }
    
    private 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
            }
        }
    }
    
    func tellStory() -> String {
        
        var story = "My Grand Adventure - OOP in Swift! \n\n"
        
        story += "I've visited \(placesVisited.count) destinations! \n\n"
        
        for destination in placesVisited {
            story += destination.tellStory() + "\n"
        }
        return story
    }
}

// results
var adventure = Adventure()
adventure.letsGo()
print(adventure.tellStory())

Enjoy your reading!

Design vs. Implementation

As you noticed, we've added and altered quite a few elements comparing to the initial design:

Design vs. Implementation
Design vs. Implementation

Designing code prior to implementation as much as possible is extremely important in software development. However, I'd like to highlight two points:

  • It's not possible to account for absolutely everything during the design stage without spending unreasonable amount of resources. When it takes too long to design, requirements are likely to change, and it may become challenging to stay motivated without seeing any implementation results.

  • On the other hand, there's no point sticking to the original design if requirements have changed or a better solution's been found.

Let's Recap!

  • Access control limits access to elements of the code.

  • There are 4 levels of access control: public, internal, private to file, and private.

  • Assigned control levels provide better code readability.

  • An element may have the same or more restrictive access level then its containing element.

  • By default, all code elements have the Internal control level.

What's NeXT?

So, our current adventure is over but there’s no a such thing as the end of the road in Apple's universe!

In this course, you’ve learned various essential aspects of OOP in Swift, and that’s a great foundation!

Now you are ready to apply your knowledge as a base and learn more about native iOS development -- and, ultimately, come up with elegant solutions to greater challenges!

What's YOUR NeXT Destination?

I encourage you to continue your learning and take the next course that explores the building blocks of an app!

Example of certificate of achievement
Example of certificate of achievement