Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Swift Cookbook

You're reading from   Swift Cookbook Proven recipes for developing robust iOS applications with Swift 5.9

Arrow left icon
Product type Paperback
Published in Jun 2024
Publisher Packt
ISBN-13 9781803239583
Length 422 pages
Edition 3rd Edition
Languages
Tools
Arrow right icon
Authors (4):
Arrow left icon
Chris Barker Chris Barker
Author Profile Icon Chris Barker
Chris Barker
Daniel Bolella Daniel Bolella
Author Profile Icon Daniel Bolella
Daniel Bolella
Nathan Lawlor Nathan Lawlor
Author Profile Icon Nathan Lawlor
Nathan Lawlor
Keith Moon Keith Moon
Author Profile Icon Keith Moon
Keith Moon
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Preface 1. Chapter 1: Swift Fundamentals 2. Chapter 2: Mastering the Building Blocks FREE CHAPTER 3. Chapter 3: Data Wrangling with Swift 4. Chapter 4: Generics, Operators, and Nested Types 5. Chapter 5: Beyond the Standard Library 6. Chapter 6: Understanding Concurrency in Swift 7. Chapter 7: Building iOS Apps with UIKit 8. Chapter 8: Building iOS Apps with SwiftUI 9. Chapter 9: Getting to Grips with Combine 10. Chapter 10: Using CoreML and Vision in Swift 11. Chapter 11: Immersive Swift with ARKit and Augmented Reality 12. Chapter 12: Visualizing Data with Swift Charts 13. Index 14. Other Books You May Enjoy

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.10 – Xcode – a new project

Figure 2.10 – Xcode – a 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.11 – Xcode – a new project structure

Figure 2.11 – Xcode – a 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.12 – A new project

Figure 2.12 – A new project

  1. From the template selector, select Framework:
Figure 2.13 – A new project framework

Figure 2.13 – A new project framework

  1. Name the project AppleInc:
Figure 2.14 – Naming the project

Figure 2.14 – 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.15 – The new project workspace group

Figure 2.15 – The 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.16 – A new playground

Figure 2.16 – A new playground

  1. Give the playground a name, and add it to your workspace:
    Figure 2.17 – A new project

Figure 2.17 – A new project

  1. Press the run button on the Xcode toolbar to build the AppleInc module:
Figure 2.18 – The Xcode toolbar

Figure 2.18 – The 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
     }
    }
  3. Now, 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.

    Create a new file called AppleStore. Then, create structures for AppleStore and Factory using the fileprivate access control:

    public enum DeviceModel {
     case iPhone13
     case iPhone13Mini
     case iPhone13Pro
     case iPhone13ProMax
    }
    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 define 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.

  4. Create a new file called Apple, and then 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"
     }
    }
  5. 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.

    Create a new file called SwiftLanguage, and then 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 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 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.

Note

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?

Now, 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, restricting the creation of iPhones to just the factories where they are built, and then restricting access to those factories to just the Apple Store:

public enum DeviceModel {
 case iPhone13
 case iPhone13Mini
 case iPhone13Pro
 case iPhone13ProMax
}
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 AppleiPhone is public, but only structures within this file can create AppleiPhone class objects in the first place. In this case, this is done by the factory.

Now, 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 to set a property than the one that was set to get 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 allow 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.9
 }
 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.

Now, 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 provided by the module, selliPhone, allows us to buy an iPhone from the Apple Store:

// Buy new iPhone
let boughtiPhone = apple.store.selliPhone(ofModel: .iPhone13ProMax)
// This works
// Try and create your own iPhone
let buildAniPhone = AppleiPhone(model: .iPhone6S)
// 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.

You have been reading a chapter from
Swift Cookbook - Third Edition
Published in: Jun 2024
Publisher: Packt
ISBN-13: 9781803239583
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at AU $24.99/month. Cancel anytime