Swift (part 2)

Estimated time

1/2 day

Object oriented programming features

Swift supports most Object Oriented Programming features:

  • Classes that can be instantiated into objects.
    • Constructors and destructors are called initializers and deinitializers respectively.
  • Encapsulation and 4 access levelsopen in new window that range from private to public
  • Simple inheritance of classes. Multiple inheritance of classes and is not supported.
  • Inheritance allows one class to use the characteristics of another.
  • Method overriding and polymorphismopen in new window and access control.
  • Overloading of operators and functions, compositionopen in new window.
  • Static methods and properties are supported.
  • Generic types are supported
  • Protocols which are the equivalent of Interfaces.
    • Classes and structs can conform to multiple protocols.
    • Protocols can have associated typesopen in new window which is similar to generic types.
    • They are used a lot by swift developers to the point that there is a programming technique called Protocol oriented programming.

Here are some additional features:

  • Extensions allow to add functions and conform to additional protocols outside of the original class, struct or protocol declaration. This has many uses that simplify our code and here are some examples.
    • They can add methods to classes from any library that we can use.
    • They can define default implementations in protocols.
  • Abstract classes are not available

this codeopen in new window illustrates some of the above features.

In additions to classes, structs in swift are powerful and provide similar features than classes with some exceptions.

Structs

In Swift, structs have many similar features with classes. They support properties, methods, subscriptsopen in new window, initializers, extensions and conforming to protocols. The features that are only available in classes are as followsopen in new window:

  • Inheritance.
  • Type casting (enables you to check and interpret the type of a class instance at runtime).
  • Deinitializers.
  • Reference counting allows more than one reference to a class instance (similar to pointers but much less complex to use).

this codeopen in new window sample shows how to use structs with protocols.

Opaque types

This feature seems advanced to understand but since it's used a lot in SwiftUI, let's explore a simple explanation and we'll provide some links to study it further.

In a base level, opaque types allow to return Protocols while keeping the concrete type information known by the compiler. It is enabled by prefixing the type with the some keyword.

Opaque types allow to keep the benefits of abstracting the code on a developer level while maintaining the performance and optimization benefits of concrete typing. In addition to that, they allow the compiler to better handle some cases such as Self or associated type requirements. Please note that explaining all the features that opaque types bring to the code is an advanced topic. For more information and details, please read the articles mentioned in the Sources and more reading section.

For this training, we'll assume that opaque help types the compiler perform better optimizations with protocols, are used in many places in SwiftUI and allow to improve our code in some cases. We'll show below a simple use case where we can define a method that returns an Equatable.

// Source: https://www.educative.io/answers/what-is-opaque-type-in-swift

// create a function that returns some Equatable
// The compiler fails is the return type is just "Equatable"
func makeInteger() -> some Equatable{
  Int.random(in: 0...10)
}

let firstInteger = makeInteger()
let secondInteger = makeInteger()

// this returns "false" because they are of the same concrete type else, Xcode will scream at us.
print(firstInteger == secondInteger)

func makeString() -> some Equatable{
  "A String"
}
let firstString = makeString()

// Compiler error because the concrete type is not the same.
print(firstInteger == firstString)

As of Swift 5.1 opaque types are only available for return valuesopen in new window. As of Swift 5.7 opaque arguments have been implementedopen in new window

Use structs by default

As surprising as it seems, Apple recommends using structs by default instead of classesopen in new window. More precisely, when we want to add a new data type, we should not assume that it should be a class by default and check if a structure is more relevant. Apple provides the following recommendations:

  • Use structures by default.
  • Use classes when you need Objective-C interoperability.
  • Use classes when you need to control the identity of the data you’re modeling.
  • Use structures along with protocols to adopt behavior by sharing implementations.

We note that structures are the default choice mostly because they are value types. This makes the code more predictable because changes cannot come from a parent call. Another advantage of structs is that they are more friendly with functional programming. We'll talk about functional programming in the next section.

Functional programming features

Functional programming revolves around three main conceptsopen in new window: pure functions, immutable objects and declarative programming.

Pure functions are functions that do not have side effects and will thus return always the same output given the same input. Swift allows to create pure functions but does not provide compile time guarantees that a function is pure.

Immutable objects can be created using classes or structs with constant properties (declared with let). As mentioned above, structs are recommended by default and here are other good reasonsopen in new window. One of the most notable ones is that since structs are passed around by value, thus they help us avoiding side effects.

Declarative programming can be easily explained as a way of programming that is centered around telling what to do and not how to do itopen in new window. This allows to obtain a clearer and more maintainable code than traditional imperative programming. For example, when we want to filter a table, a for loop is not declarative (we say imperative in this case) while the WHERE SQL syntax is considered declarative. Declarative programing is possible in Swift through chaining functions and passing functions as arguments. Indeed, as we have seen earlier, Swift has 1st class support for functions. In addition to that, we can find declarative APIs in the standard Swift library and in Swift UI. The latter will be explored in a different chapter. For now, let's illustrate with this codeopen in new window how to process a list of strings using only declarative APIs provided by Swift.

Swift has many more features and provides a rich standard library. We'll explore them as needed in the next sections. For now, let's create some UIs in the next chapter.

Structured Concurrency

  • Swift supports writing concurrent code in a structured way.
  • Concurrency means that we execute multiple tasks at the same time. For example, update the UI while the app performs an HTTP request to the server.
  • In Swift, we can create concurrent Tasks with the Task, TaskGroup types.
  • Without Structured concurrency, we would use complex concepts to such as callbacks which make code less readable (have you lived the callback hellopen in new window ?).
  • We say that a code is structuredopen in new window when we use the well-know control flow structures :if/then/else, loops, functions, lexical scopes for variables.
    • Structured concurrency means that we write concurrent code using the usual control flow structures (as opposed to callback-based concurrent code)
    • In Swift is possible through the async and await keywords.
    • When we await a Task, the control flow will continue when it end without blocking the Task or TaskGroup on which it is launched.
    • A function that has uses the await keyword must be declared as async
  • To summarize async and await + Task and TaskGroup = Structured Concurrency
  • Continuations allow to convert callback code into async/await

This swift script shows a sample of using Task + async/awaitopen in new window

This swift script shows a sample of using TaskGoup + async/awaitopen in new window

This swift script shows a sample of TaskGoup cancellationopen in new window

This swift script shows how to convert callbacks into async/awaitopen in new window

Structured Concurrency in Playground Book

// Reference: https://stackoverflow.com/a/24066317
import PlaygroundSupport

//Playground does not stop at the end of the code
PlaygroundPage.current.needsIndefiniteExecution = true

func sampleFunc() async {
  print("sampleFunc")
  try? await Task.sleep(until: .now + .seconds(2))
}

Task {
    await sampleFunc()
    print("done")
    // End the playground
    PlaygroundPage.current.finishExecution()
}

Generics

  • Generics allow to pass a type as a parameter to a class, struct, enum or function.
  • A type parameter can be declares with <T> where T is the type parameter.
  • Examples
    • func printArray<T>(array: [T]) { for item in array { print(item) } }
  • Swift can infer the type of the parameter if it is not provided and if it's not ambiguous.

this codeopen in new window illustrates some of the above features.

Key-paths

  • Key-pathsopen in new window allow to refer to properties of a type.
  • They are created with the \.propertyName syntax.
  • They are often used to sort, filter, group and map collections and in SwiftUI to bind properties to UI elements.

this codeopen in new window illustrates some of the above features.

Exercises

Exercise 1

Please click on this link to view the exerciseopen in new window

Please open to see the solution(s)

Solutionopen in new window

Exercise 2

Please click on this link to view the exerciseopen in new window

Please open to see the solution(s)

Solutionopen in new window

Exercise 3

Please click on this link to view the exerciseopen in new window

Please open to see the solution(s)

Solution 1open in new window

Solution 2 with resultsopen in new window

Sources and more reading