Book Image

Swift Cookbook. - Second Edition

By : Keith Moon, Chris Barker
Book Image

Swift Cookbook. - Second Edition

By: Keith Moon, Chris Barker

Overview of this book

Swift is an exciting, multi-platform, general-purpose programming language, and with this book, you'll explore the features of its latest version, Swift 5.3. The book begins with an introduction to the basic building blocks of Swift 5.3, its syntax, and the functionalities of Swift constructs. You’ll then discover how Swift Playgrounds provide an ideal platform to write, execute, and debug your Swift code. As you advance through the chapters, the book will show you how to bundle variables into tuples or sets, order your data with an array, store key-value pairs with dictionaries, and use property observers. You’ll also get to grips with the decision-making and control structures in Swift, examine advanced features such as generics and operators, and explore functionalities outside of the standard library. Once you’ve learned how to build iOS applications using UIKit, you'll find out how to use Swift for server-side programming, run Swift on Linux, and investigate Vapor. Finally, you'll discover some of the newest features of Swift 5.3 using SwiftUI and Combine to build adaptive and reactive applications, and find out how to use Swift to build and integrate machine learning models along with Apple’s Vision Framework. By the end of this Swift book, you'll have discovered solutions to boost your productivity while developing code using Swift 5.3.
Table of Contents (14 chapters)
12
About Packt

Controlling access with access control

Swift provides fine-grained access control, allowing you to specify the visibility that your code has to other areas of code. This enables you to be deliberate about the interface you provide to other parts of the system, thus encapsulating implementation logic and helping separate the areas of concern.

Swift has five access levels:

  • Private: Only accessible within the existing scope (defined by curly brackets) or extensions in the same file.
  • File private: Accessible to anything in the same file, but nothing outside the file.
  • Internal: Accessible to anything in the same module, but nothing outside the module.
  • Public: Accessible both inside and outside the module, but cannot be subclassed or overwritten outside of the defining module.
  • Open: Accessible everywhere, with no restrictions in terms of its use, and can therefore be subclassed and overwritten.

These can be applied to types, properties, and functions.

Getting ready

To explore each of these access levels, we need to step outside our playground comfort zone and create a module. To have something that will hold our module and a playground that can use it, we will need to create an Xcode workspace:

  1. In Xcode, select File | New | Workspace... from the menu:
Figure 2.9 – Xcode – New project
  1. Give your workspace a name, such as AccessControl, and choose a save location. You will now see an empty workspace:
Figure 2.10 – Xcode – New project structure

In this workspace, we need to create a module. To illustrate the access controls that are available, let's have our module represent something that tightly controls which information it exposes, and which information it keeps hidden. One thing that fits this definition is Apple; that is, the company.

  1. Create a new project from the Xcode menu by selecting File | New | Project...:
Figure 2.11 – New project
  1. From the template selector, select Framework:
Figure 2.12 – New project framework
  1. Name the project AppleInc:
Figure 2.13 – Naming the project
  1. Choose a location. Then, at the bottom of the window, ensure that Add to: has been set to the workspace we just created:
Figure 2.14 – New project workspace group
  1. Now that we have a module, let's set up a playground to use it in. From the Xcode menu, select File | New | Playground...:
Figure 2.15 – New playground
  1. Give the playground a name and save it to a location:
Figure 2.16 – New project
  1. This playground will not be added to the workspace automatically; you will need to locate the playground you just created and drag it into the file explorer pane on the left-hand side of your workspace.
  2. Press the run button on the Xcode toolbar to build the AppleInc module:
Figure 2.17 – Xcode toolbar
  1. Select the playground from the file navigator and add an import statement to the top of the file:
import AppleInc 

We are now ready to look into the different access controls that are available.

How to do it...

Let's investigate the most restrictive of the access controls: private. Structures marked as private are only visible within the scope of the type they have been defined in, as well as any extensions of that type that are located in the same file. We know that Apple has super-secret areas where it works on its new products, so let's create one:

  1. Select the AppleInc group in the file navigator and create a new file by selecting File | New | File... from the menu. Let's call it SecretProductDepartment.
  2. In this new file, create a SecretProductDepartment class using the private access control:
class SecretProductDepartment { 

private var secretCodeWord = "Titan"
private var secretProducts = ["Apple Glasses",
"Apple Car",
"Apple Brain Implant"]

func nextProduct(codeWord: String) -> String? {
let codeCorrect = codeWord == secretCodeWord
return codeCorrect ? secretProducts.first : nil
}
}

Next, let's look at the fileprivate access control. Structures marked as fileprivate are only visible within the file that they are defined in, so a collection of related structures defined in the same file will be visible to each other, but anything outside the file will not see these structures.

When you buy an iPhone from the Apple Store, it's not made in-store; it's made in a factory that the public doesn't have access to. So, let's model this using fileprivate.

  1. Create a new file called AppleStore. Then, create structures for AppleStore and Factory using the fileprivate access control:
public enum DeviceModel {
case iPhone12
case iPhone12Mini
case iPhone12Pro
case iPhone12ProMax
}

public class AppleiPhone {

public let model: DeviceModel

fileprivate init(model: DeviceModel) {
self.model = model
}
}

fileprivate class Factory {
func makeiPhone(ofModel model: DeviceModel) -> AppleiPhone {
return AppleiPhone(model: model)
}
}

public class AppleStore {

private var factory = Factory()

public func selliPhone(ofModel model: DeviceModel)
-> AppleiPhone {
return factory.makeiPhone(ofModel: model)
}
}

To investigate the public access control, we will be defining something that is visible outside the defining module but cannot be subclassed or overridden. Apple itself is the perfect candidate to model this behavior as certain parts of it are visible to the public. However, it closely guards its image and brand, so subclassing Apple to alter and customize it will not be allowed.

  1. Create a new file called Apple and create a class for Apple that uses the public access control:
public class Person { 

public let name: String

public init(name: String) {
self.name = name
}
}

public class Apple {

public private(set) var ceo: Person
private var employees = [Person]()
public let store = AppleStore()
private let secretDepartment = SecretProductDepartment()

public init() {
ceo = Person(name: "Tim Cook")
employees.append(ceo)
}

public func newEmployee(person: Person) {
employees.append(person)
}

func weeklyProductMeeting() {

var superSecretProduct =
secretDepartment.nextProduct(codeWord: "Not sure...
Abracadabra?") // nil

// Try again
superSecretProduct =
secretDepartment.nextProduct(givenCodeWord: "Titan")
print(superSecretProduct as Any) // "Apple Glasses"
}
}

Lastly, we have the open access control. Structures defined as open are available outside the module and can be subclassed and overridden without restriction. To explain this last control, we want to model something that exists within Apple's domain but is completely open and free from restrictions. So, for this, we can use the Swift language itself!

Swift has been open sourced by Apple, so while they maintain the project, the source code is fully available for others to take, modify, and improve.

  1. Create a new file called SwiftLanguage and create a class for the Swift language that uses the open access control:
open class SwiftLanguage { 

open func versionNumber() -> Float {
return 5.1
}

open func supportedPlatforms() -> [String] {
return ["iOS", "macOS", "tvOS", "watchOS", "Linux"]
}
}

We now have a module that uses Swift's access controls to provide interfaces that match our model and provide the appropriate visibility.

How it works...

Let's examine our SecretProductDepartment class to see how its visibility matches our model:

class SecretProductDepartment { 

private var secretCodeWord = "Titan"
private var secretProducts = ["Apple Glasses",
"Apple Car",
"Apple Brain Implant"]

func nextProduct(codeWord: String) -> String? {
let codeCorrect = codeWord == secretCodeWord
return codeCorrect ? secretProducts.first : nil
}
}

The SecretProductDepartment class is declared without an access control keyword, and when no access control is specified, the default control of internal is applied. Since we want the secret product department to be visible within Apple, but not from outside Apple, this is the correct access control to use.

The two properties of the secretCodeWord and secretProducts classes are marked as private, thus hiding their values and existence from anything outside the SecretProductDepartment class. To see this restriction in action, add the following to the same file, but outside the class:

let insecureCodeWord = SecretProductDepartment().secretCodeWord 

When you try to build the module, you are told that secretCodeWord can't be accessed due to the private protection level.

While these properties are not directly accessible, we can provide an interface that allows the information to be provided in a controlled way. This is what the nextProduct method provides:

func nextProduct(codeWord: String) -> String? { 
let codeCorrect = codeWord == secretCodeWord
return codeCorrect ? secretProducts.first : nil
}

If the correct codeword is passed, it will provide the name of the next product from the secret department, but the details of all other products, and the codeword itself, will be hidden. Since this method doesn't have a specified access control, it is set to the default of internal.

It's not possible for contents within a structure to have a more permissive access control than the structure itself. For instance, we can't define the nextProduct method as being public because this is more permissive than the class it is defined in, which is only internal.
Thinking about it, this is the obvious outcome as you cannot create an instance of an internal class outside of the defining module, so how can you possibly call a method on a class instance that you can't even create?

Next, let's look at the AppleStore.swift file we created. The purpose here is to provide people outside of Apple with the ability to purchase an iPhone through the Apple Store, but to restrict the creation of iPhones to just the factories where they are built, and then restrict access to those factories to just the Apple Store:

public enum DeviceModel {
case iPhone12
case iPhone12Mini
case iPhone12Pro
case iPhone12ProMax
}

public class AppleiPhone {

public let model: DeviceModel

fileprivate init(model: DeviceModel) {
self.model = model
}
}

public class AppleStore {

private var factory = Factory()

public func selliPhone(ofModel model: DeviceModel)
-> AppleiPhone {
return factory.makeiPhone(ofModel: model)
}
}

Since we want to be able to sell iPhones outside of the AppleInc module, the DeviceModel enum and the AppleiPhone and AppleStore classes are all declared as public. This has the benefit of making them available outside the module but preventing them from being subclassed or modified. Given how Apple protects the look and feel of their phones and stores, this seems correct for this model.

The Apple Store needs to get their iPhones from somewhere; that is, from the factory:

fileprivate class Factory { 

func makeiPhone(ofModel model: DeviceModel) -> AppleiPhone {
return AppleiPhone(model: model)
}
}

By making the Factory class fileprivate, it is only visible within this file, which is perfect because we only want the Apple Store to be able to use the factory to create iPhones.

We have also restricted the iPhone's initialization method so that it can only be accessed from structures in this file:

fileprivate init(model: DeviceModel) 

The resulting iPhone is public, but only structures within this file can create iPhone class objects in the first place. In this case, this is done by the factory.

Next, let's look at the Apple.swift file:

public class Person { 

public let name: String

public init(name: String) {
self.name = name
}
}

public class Apple {

public private(set) var ceo: Person
private var employees = [Person]()
public let store = AppleStore()
private let secretDepartment = SecretProductDepartment()

public init() {
ceo = Person(name: "Tim Cook")
employees.append(ceo)
}

public func newEmployee(person: Person) {
employees.append(person)
}

func weeklyProductMeeting() {

var superSecretProduct =
secretDepartment.nextProduct(givenCodeWord: "Not sure...
Abracadabra?") // nil

// Try again
superSecretProduct =
secretDepartment.nextProduct(givenCodeWord: "Titan")
print(superSecretProduct) // "Apple Glasses"
}
}

The preceding code made both the Person and Apple classes public, along with the newEmployee method. This allows new employees to join the company. The CEO, however, is defined as both public and private:

public private(set) var ceo: Person 

We can define a separate, more restrictive, access control for setting a property than the one that was set for getting it. This has the effect of making it a read-only property from outside the defining structure. This provides the access we require since we want the CEO to be visible outside of the AppleInc module, but we want to only be able to change the CEO from within Apple.

The final access control is open. We applied this to the SwiftLanguage class:

open class SwiftLanguage { 

open func versionNumber() -> Float {
return 5.0
}

open func supportedPlatforms() -> [String] {
return ["iOS", "macOSX", "tvOS", "watchOS", "Linux"]
}
}

By declaring the class and methods as open, we are allowing them to be subclassed, overridden, and modified by anyone, including those outside the AppleInc module. With the Swift language being fully open source, this matches what we are trying to achieve.

There's more...

With our module fully defined, let's see how things look from outside the module. We need to build the module to make it available to the playground. Select the playground; it should contain a statement that imports the AppleInc module:

import AppleInc 

First, let's look at the most accessible class that we created; that is, SwiftLanguage. Let's subclass the SwiftLanguage class and override its behavior:

class WinSwift: SwiftLanguage { 

override func versionNumber() -> Float {
return 5.3
}

override func supportedPlatforms() -> [String] {

var supported = super.supportedPlatforms()
supported.append("Windows")

return supported
}
}

Since SwiftLanguage is open, we can subclass it to add more supported platforms and increase its version number.

Next, let's create an instance of the Apple class and see how we can interact with it:

let apple = Apple() 

let keith = Person(name: "Keith Moon")
apple.newEmployee(person: keith)

print("Current CEO: \(apple.ceo.name)")
let craig = Person(name: "Craig Federighi")
apple.ceo = craig // Doesn't compile

We can create Person and provide it to Apple as a new employee since the Person class and the newEmployee method are declared as public. We can retrieve information about the CEO, but we aren't able to set a new CEO as we defined the property as private (set).

Another one of the public interfaces selliPhone provided by the module allows us to buy an iPhone from the Apple Store:

// Buy new iPhone  
let boughtiPhone = apple.store.selliPhone(ofModel: .iPhone12Pro)
// This works

// Try and create your own iPhone
let buildAniPhone = AppleiPhone(model: .iPhone12Pro)
// Doesn't compile

We can retrieve a new iPhone from the Apple Store because we declared the selliPhone method as public. However, we can't create a new iPhone directly since the iPhone's init method is declared as fileprivate.

See also

Further information about access control can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/access-control.