The Basics of Swift Object-Oriented Programming

Swift provides extensive support for developing object-oriented applications. The subject area of object-oriented programming is, however, large. It is not an exaggeration to state that entire books have been dedicated to the subject. As such, a detailed overview of object-oriented software development is beyond the scope of this book. Instead, we will introduce the basic concepts involved in object-oriented programming and then move on to explain the concept as it relates to Swift application development. Once again, while we strive to provide the basic information you need in this chapter, we recommend reading a copy of Apple’s The Swift Programming Language book for more extensive coverage of this subject area.

What is an Instance?

Objects (also referred to as class instances) are self-contained modules of functionality that can be easily used and re-used as the building blocks for a software application.

Instances consist of data variables (called properties) and functions (called methods) that can be accessed and called on the instance to perform tasks and are collectively referred to as class members.

What is a Class?

Much as a blueprint or architect’s drawing defines what an item or a building will look like once it has been constructed, a class defines what an instance will look like when it is created. It defines, for example, what the methods will do and what the properties will be.

Declaring a Swift Class

Before an instance can be created, we first need to define the class ‘blueprint’ for the instance. In this chapter we will create a bank account class to demonstrate the basic concepts of Swift object-oriented programming.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

In declaring a new Swift class we specify an optional parent class from which the new class is derived and also define the properties and methods that the class will contain. The basic syntax for a new class is as follows:

class NewClassNameParentClass {
   // Properties
   // Instance Methods
   // Type methods
}Code language: Swift (swift)

The Properties section of the declaration defines the variables and constants that are to be contained within the class. These are declared in the same way that any other variable or constant would be declared in Swift.

The Instance methods and Type methods sections define the methods that are available to be called on the class and instances of the class. These are essentially functions specific to the class that perform a particular operation when called upon and will be described in greater detail later in this chapter.

To create an example outline for our BankAccount class, we would use the following:

class BankAccount {
 
}Code language: Swift (swift)

Now that we have the outline syntax for our class, the next step is to add some instance properties to it.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

When naming classes, note that the convention is for the first character of each word to be declared in uppercase (a concept referred to as UpperCamelCase). This contrasts with property and function names where lower case is used for the first character (referred to as lowerCamelCase).

Adding Instance Properties to a Class

A key goal of object-oriented programming is a concept referred to as data encapsulation. The idea behind data encapsulation is that data should be stored within classes and accessed only through methods defined in that class. Data encapsulated in a class are referred to as properties or instance variables.

Instances of our BankAccount class will be required to store some data, specifically a bank account number and the balance currently held within the account. Properties are declared like any other variables and constants are declared in Swift. We can, therefore, add these variables as follows:

class BankAccount {
    var accountBalance: Float = 0
    var accountNumber: Int = 0
}Code language: Swift (swift)

Having defined our properties, we can now move on to defining the methods of the class that will allow us to work with our properties while staying true to the data encapsulation model.

Defining Methods

The methods of a class are essentially code routines that can be called upon to perform specific tasks within the context of that class.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Methods come in two different forms, type methods and instance methods. Type methods operate at the level of the class, such as creating a new instance of a class. Instance methods, on the other hand, operate only on the instances of a class (for example performing an arithmetic operation on two property variables and returning the result).

Instance methods are declared within the opening and closing braces of the class to which they belong and are declared using the standard Swift function declaration syntax.

Type methods are declared in the same way as instance methods with the exception that the declaration is preceded by the class keyword.

For example, the declaration of a method to display the account balance in our example might read as follows:

class BankAccount {
 
    var accountBalance: Float = 0
    var accountNumber: Int = 0
 
    func displayBalance()
    {
       print("Number \(accountNumber)")
       print("Current balance is \(accountBalance)")
    }
}Code language: Swift (swift)

The method is an instance method so it is not preceded by the class keyword.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

When designing the BankAccount class it might be useful to be able to call a type method on the class itself to identify the maximum allowable balance that can be stored by the class. This would enable an application to identify whether the BankAccount class is suitable for storing details of a new customer without having to go through the process of first creating a class instance. This method will be named getMaxBalance and is implemented as follows:

class BankAccount {
 
    var accountBalance: Float = 0
    var accountNumber: Int = 0
 
    func displayBalance()
    {
       print("Number \(accountNumber)")
       print("Current balance is \(accountBalance)")
    }
 
    class func getMaxBalance() -> Float {
        return 100000.00
    }
}Code language: Swift (swift)

Declaring and Initializing a Class Instance

So far, all we have done is define the blueprint for our class. To do anything with this class, we need to create instances of it. The first step in this process is to declare a variable to store a reference to the instance when it is created. We do this as follows:

var account1: BankAccount = BankAccount()Code language: Swift (swift)

When executed, an instance of our BankAccount class will have been created and will be accessible via the account1 variable.

Initializing and De-initializing a Class Instance

A class will often need to perform some initialization tasks at the point of creation. These tasks can be implemented by placing an init method within the class. In the case of the BankAccount class, it would be useful to be able to initialize the account number and balance properties with values when a new class instance is created. To achieve this, the init method could be written in the class as follows:

class BankAccount {
 
    var accountBalance: Float = 0
    var accountNumber: Int = 0
 
    init(number: Int, balance: Float)
    {
        accountNumber = number
        accountBalance = balance
    }
 
    func displayBalance()
    {
       print("Number \(accountNumber)")
       print("Current balance is \(accountBalance)")
    }
}Code language: Swift (swift)

When creating an instance of the class, it will now be necessary to provide initialization values for the account number and balance properties as follows:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

var account1 = BankAccount(number: 12312312, balance: 400.54)Code language: Swift (swift)

Conversely, any cleanup tasks that need to be performed before a class instance is destroyed by the Swift runtime system can be performed by implementing the de-initializer within the class definition:

class BankAccount {
 
    var accountBalance: Float = 0
    var accountNumber: Int = 0
 
    init(number: Int, balance: Float)
    {
        accountNumber = number
        accountBalance = balance
    }
 
    deinit {
        // Perform any necessary clean up here
    }
 
    func displayBalance()
    {
       print("Number \(accountNumber)")
       print("Current balance is \(accountBalance)")
    }
}Code language: Swift (swift)

Calling Methods and Accessing Properties

Now is probably a good time to recap what we have done so far in this chapter. We have now created a new Swift class named BankAccount. Within this new class we declared some properties to contain the bank account number and current balance together with an initializer and a method to display the current balance information. In the preceding section we covered the steps necessary to create and initialize an instance of our new class. The next step is to learn how to call the instance methods and access the properties we built into our class. This is most easily achieved using dot notation.

Dot notation involves accessing an instance variable or calling an instance method by specifying a class instance followed by a dot followed in turn by the name of the property or method:

classInstance.propertyName
classInstance.instanceMethod()Code language: Swift (swift)

For example, to get the current value of our accountBalance instance variable:

var balance1 = account1.accountBalanceCode language: Swift (swift)

Dot notation can also be used to set values of instance properties:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

account1.accountBalance = 6789.98Code language: Swift (swift)

The same technique is used to call methods on a class instance. For example, to call the displayBalance method on an instance of the BankAccount class:

account1.displayBalance()Code language: Swift (swift)

Type methods are also called using dot notation, though they must be called on the class type instead of a class instance:

ClassName.typeMethod()Code language: Swift (swift)

For example, to call the previously declared getMaxBalance type method, the BankAccount class is referenced:

var maxAllowed = BankAccount.getMaxBalance()Code language: Swift (swift)

Stored and Computed Properties

Class properties in Swift fall into two categories referred to as stored properties and computed properties. Stored properties are those values that are contained within a constant or variable. Both the account name and number properties in the BankAccount example are stored properties.

A computed property, on the other hand, is a value that is derived based on some form of calculation or logic at the point at which the property is set or retrieved. Computed properties are implemented by creating getter and optional corresponding setter methods containing the code to perform the computation. Consider, for example, that the BankAccount class might need an additional property to contain the current balance less any recent banking fees. Rather than use a stored property, it makes more sense to use a computed property that calculates this value on request. The modified BankAccount class might now read as follows:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

class BankAccount {
 
    var accountBalance: Float = 0
    var accountNumber: Int = 0;
    let fees: Float = 25.00
 
    var balanceLessFees: Float {
        get {
            return accountBalance - fees
        }
    }
 
    init(number: Int, balance: Float)
    {
        accountNumber = number
        accountBalance = balance
    }
.
.
.
}Code language: Swift (swift)

The above code adds a getter that returns a computed property based on the current balance minus a fee amount. An optional setter could also be declared in much the same way to set the balance value less fees:

var balanceLessFees: Float {
    get {
        return accountBalance - fees
    }
 
    set(newBalance)
    {
        accountBalance = newBalance - fees
    }
}Code language: Swift (swift)

The new setter takes as a parameter a floating-point value from which it deducts the fee value before assigning the result to the current balance property. Although these are computed properties, they are accessed in the same way as stored properties using dot-notation. The following code gets the current balance less the fees value before setting the property to a new value:

var balance1 = account1.balanceLessFees
account1.balanceLessFees = 12123.12Code language: Swift (swift)

Lazy Stored Properties

There are several different ways in which a property can be initialized, the most basic being direct assignment as follows:

var myProperty = 10Code language: Swift (swift)

Alternatively, a property may be assigned a value within the initializer:

class MyClass {
  let title: String
 
  init(title: String) {
    self.title = title
  }
}Code language: Swift (swift)

For more complex requirements, a property may be initialized using a closure:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

class MyClass {
    
    var myProperty: String = {
        var result = resourceIntensiveTask()
        result = processData(data: result)
        return result
    }()
.
.
}Code language: Swift (swift)

Particularly in the case of a complex closure, there is the potential for the initialization to be resource intensive and time consuming. When declared in this way, the initialization will be performed every time an instance of the class is created, regardless of when (or even if) the property is actually used within the code of the app. Also, situations may arise where the value assigned to the property may not be known until a later stage in the execution process, for example after data has been retrieved from a database or user input has been obtained from the user. A far more efficient solution in such situations would be for the initialization to take place only when the property is first accessed. Fortunately, this can be achieved by declaring the property as lazy as follows:

class MyClass {
    
    lazy var myProperty: String = {
        var result = resourceIntensiveTask()
        result = processData(data: result)
        return result
    }()
.
.
}Code language: Swift (swift)

When a property is declared as being lazy, it is only initialized when it is first accessed, allowing any resource intensive activities to be deferred until the property is needed and any initialization on which the property is dependent to be completed. Note that lazy properties must be declared as variables (var).

Using self in Swift

Programmers familiar with other object-oriented programming languages may be in the habit of prefixing references to properties and methods with self to indicate that the method or property belongs to the current class instance. The Swift programming language also provides the self property type for this purpose and it is, therefore, perfectly valid to write code that reads as follows:

class MyClass {
    var myNumber = 1
 
    func addTen() {
        self.myNumber += 10
    }
}Code language: Swift (swift)

In this context, the self prefix indicates to the compiler that the code refers to a property named myNumber which belongs to the MyClass class instance. When programming in Swift, however, it is no longer necessary to use self in most situations since this is now assumed to be the default for references to properties and methods. To quote Apple’s Swift Programming Language guide, “in practice you don’t need to write self in your code very often”. The function from the above example, therefore, can also be written as follows with the self reference omitted:

func addTen() {
    myNumber += 10
}Code language: Swift (swift)

In most cases, use of self is optional in Swift. That being said, one situation where it is still necessary to use self is when referencing a property or method from within a closure expression. The use of self, for example, is mandatory in the following closure expression:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

document?.openWithCompletionHandler({(success: Bool) -> Void in
    if success {
        self.ubiquityURL = resultURL
    }
})Code language: Swift (swift)

It is also necessary to use self to resolve ambiguity such as when a function parameter has the same name as a class property. In the following code, for example, the first print statement will output the value passed through to the function via the myNumber parameter while the second print statement outputs the number assigned to the myNumber class property (in this case 10):

class MyClass {
 
    var myNumber = 10 // class property
 
    func addTen(myNumber: Int) {
        print(myNumber) // Output the function parameter value
        print(self.myNumber) // Output the class property value
    }
}Code language: Swift (swift)

Whether or not to use self in most other situations is largely a matter of programmer preference. Those who prefer to use self when referencing properties and methods can continue to do so in Swift. Code that is written without use of the self property type (where doing so is not mandatory) is, however, just as valid when programming in Swift.

Understanding Swift Protocols

By default, there are no specific rules to which a Swift class must conform as long as the class is syntactically correct. In some situations, however, a class will need to meet certain criteria in order to work with other classes. This is particularly common when writing classes that need to work with the various frameworks that comprise the iOS SDK. A set of rules that define the minimum requirements which a class must meet is referred to as a Protocol. A protocol is declared using the protocol keyword and simply defines the methods and properties that a class must contain in order to be in conformance. When a class adopts a protocol, but does not meet all of the protocol requirements, errors will be reported stating that the class fails to conform to the protocol.

Consider the following protocol declaration. Any classes that adopt this protocol must include both a readable String value called name and a method named buildMessage() which accepts no parameters and returns a String value:

protocol MessageBuilder {
 
    var name: String { get }
    func buildMessage() -> String
}Code language: Swift (swift)

Below, a class has been declared which adopts the MessageBuilder protocol:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

class MyClass: MessageBuilder {
    
}Code language: Swift (swift)

Unfortunately, as currently implemented, MyClass will generate a compilation error because it contains neither the name variable nor the buildMessage() method as required by the protocol it has adopted. To conform to the protocol, the class would need to meet both requirements, for example:

class MyClass: MessageBuilder {
    
    var name: String
    
    init(name: String) {
      self.name = name
    }
    
    func buildMessage() -> String {
        "Hello " + name
    }
}Code language: Swift (swift)

Opaque Return Types

Now that protocols have been explained, it is a good time to introduce the concept of opaque return types. As we have seen in previous chapters, if a function returns a result, the type of that result must be included in the function declaration. The following function, for example, is configured to return an Int result:

func doubleFunc1 (value: Int) -> Int {
    return value * 2
}Code language: Swift (swift)

Instead of specifying a specific return type (also referred to as a concrete type), opaque return types allow a function to return any type as long as it conforms to a specified protocol. Opaque return types are declared by preceding the protocol name with the some keyword. The following changes to the doubleFunc1() function, for example, declare that a result will be returned of any type that conforms to the Equatable protocol:

func doubleFunc1(value: Int) -> some Equatable {
    value * 2
}Code language: Swift (swift)

To conform to the Equatable protocol, which is a standard protocol provided with Swift, a type must allow the underlying values to be compared for equality. Opaque return types can, however, be used for any protocol, including those you create yourself.

Given that both the Int and String concrete types are in conformance with the Equatable protocol, it is possible to also create a function that returns a String result:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

func doubleFunc2(value: String) -> some Equatable {
    value + value
}Code language: Swift (swift)

Although these two methods return entirely different concrete types, the only thing known about these types is that they conform to the Equatable protocol. We therefore know the capabilities of the type, but not the actual type.

In fact, we only know the concrete type returned in these examples because we have access to the source code of the functions. If these functions resided in a library or API framework for which the source is not available to us, we would not know the exact type being returned. This is intentional and designed to hide the underlying return type used within public APIs. By masking the concrete return type, programmers will not come to rely on a function returning a specific concrete type or risk accessing internal objects which were not intended to be accessed. This also has the benefit that the developer of the API can make changes to the underlying implementation (including returning a different protocol compliant type) without having to worry about breaking dependencies in any code that uses the API.

This raises the question of what happens when an incorrect assumption is made when working with the opaque return type. Consider, for example, that the assumption could be made that the results from the doubleFunc1() and doubleFunc2() functions can be compared for equality:

let intOne = doubleFunc1(value: 10)
let stringOne = doubleFunc2(value: "Hello")
 
 
if (intOne == stringOne) {
    print("They match")
}Code language: Swift (swift)

Working on the premise that we do not have access to the source code for these two functions there is no way to know whether the above code is valid. Fortunately, although we, as programmers, have no way of knowing the concrete type returned by the functions, the Swift compiler has access to this hidden information. The above code will, therefore, generate the following syntax error long before we get to the point of trying to execute invalid code:

Binary operator '==' cannot be applied to operands of type 'some
 Equatable' (result of 'doubleFunc1(value:)') and 'some Equatable'
(result of 'doubleFunc2(value:)')Code language: plaintext (plaintext)

Opaque return types are a fundamental foundation of the implementation of the SwiftUI APIs and are used widely when developing apps in SwiftUI (the some keyword will appear frequently in SwiftUI View declarations). SwiftUI advocates the creation of apps by composing together small, reusable building blocks and refactoring large view declarations into collections of small, lightweight subviews. Each of these building blocks will typically conform to the View protocol. By declaring these building blocks as returning opaque types that conform to the View protocol, these building blocks become remarkably flexible and interchangeable, resulting in code that is cleaner and easier to reuse and maintain.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Summary

Object-oriented programming languages such as Swift encourage the creation of classes to promote code reuse and the encapsulation of data within class instances. This chapter has covered the basic concepts of classes and instances within Swift together with an overview of stored and computed properties and both instance and type methods. The chapter also introduced the concept of protocols which serve as templates to which classes must conform and explained how they form the basis of opaque return types.


Categories