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:
- In Xcode, select File | New | Workspace... from the menu:
Figure 2.10 – Xcode – a new project
- 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
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.
- Create a new project from the Xcode menu by selecting File | New | Project...:
Figure 2.12 – A new project
- From the template selector, select Framework:
Figure 2.13 – A new project framework
- Name the project
AppleInc
:
Figure 2.14 – Naming the project
- 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
- 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
- Give the playground a name, and add it to your workspace:
Figure 2.17 – A new project
- Press the run button on the Xcode toolbar to build the AppleInc module:
Figure 2.18 – The Xcode toolbar
- 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:
- Select the
AppleInc
group in the file navigator, and create a new file by selecting File | New | File... from the menu. Let’s call itSecretProductDepartment
. - In this new file, create a
SecretProductDepartment
class using theprivate
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 } }
- Now, let’s look at the
fileprivate
access control. Structures marked asfileprivate
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 forAppleStore
andFactory
using thefileprivate
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.
- Create a new file called
Apple
, and then create a class for Apple that uses thepublic
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 asopen
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 theopen
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.