Access Control
The concept of access control enables us to restrict how types, functions and other declarations can be accessed by other code. Swift offers five different levels of access control, and making full use of them can be crucial in order to write programs that have clearly separated concerns and a robust structure.
When we define any new type, property, or function in Swift, it’ll have the internal
access level by default. That means that it’ll be visible to all other code that lives within the same module — such as an app, a system extension, a framework, or a Swift package.
As an example, let’s say that we’re building a shopping app, and that we’ve defined a class called PriceCalculator
that lets us calculate the total price for an array of products:
class PriceCalculator {
func calculatePrice(for products: [Product]) -> Int {
// The reduce function enables us to reduce a collection,
// in this case an array of products, into a single value:
products.reduce(into: 0) { totalPrice, product in
totalPrice += product.price
}
}
}
As we’re currently not specifying any explicit access level, our PriceCalculator
class (and its calculatePrice
method) will be accessible from anywhere within our app. However, if we’re looking to share our new class with other modules (we might, for instance, implement it within a framework that we share between our main app and an extension, or a companion Apple Watch app), then we’ll need to make it public
in order for it to be visible within those external contexts:
public class PriceCalculator {
public func calculatePrice(for products: [Product]) -> Int {
products.reduce(into: 0) { totalPrice, product in
totalPrice += product.price
}
}
}
However, the above change is not quite enough. While we’re now able to find our class outside of the module that it’s defined in, we can’t create any instances of it — since its (implicit) initializer is, just like any other code, internal
by default. To fix that, let’s define a public
initializer, which we’ll leave empty since there’s no actual work to be done within it:
public class PriceCalculator {
public init() {}
...
}
We’re now able to find, initialize, and call our PriceCalculator
both inside and outside of its module — fantastic. But let’s now say that we’re also looking to subclass it in order to modify it, or to add new functionality to it. While that’s currently possible within its own module, it’s again something that’s prevented outside of it.
To change that, we’ll have to use Swift’s currently most open level of access control, which is appropriately named open
:
open class PriceCalculator {
...
}
With the above change in place, we can now create custom subclasses of PriceCalculator
anywhere — which can have new initializers, new properties, and new methods. Here’s how we might use that to implement a DiscountedPriceCalculator
, which lets us apply a given discount
to all price calculations:
class DiscountedPriceCalculator: PriceCalculator {
let discount: Int
init(discount: Int) {
self.discount = discount
super.init()
}
func calculateDiscountedPrice(for products: [Product]) -> Int {
let price = calculatePrice(for: products)
return price - discount
}
}
Above we’re defining a brand new price calculation method, but it would arguably be much more appropriate to override and modify the existing calculatePrice
method that we inherited from our base class instead. That way, there would be no confusion around which method to call, and we could keep our two classes consistent.
To be able to do that, we again have to mark the original declaration — this time our calculatePrice
method declaration — as open
:
open class PriceCalculator {
public init() {}
open func calculatePrice(for products: [Product]) -> Int {
...
}
}
With the above in place, we can now freely override calculatePrice
, rather than having to create a separate method:
class DiscountedPriceCalculator: PriceCalculator {
let discount: Int
init(discount: Int) {
self.discount = discount
super.init()
}
override func calculatePrice(for products: [Product]) -> Int {
let price = super.calculatePrice(for: products)
return price - discount
}
}
So that’s internal
, public
and open
— which are used to gradually open a declaration up for public use and modification. But we can of course also go the other way, and hide parts of our code from being discovered and used. At first, it may seem questionable what the value is in doing that, but it can really help us make our API much more narrow and focused — which in turn can make it easier to understand, test, and use.
So let’s now go all the way to the other side of the access level spectrum, and take a look at the most restrictive level — private
. Any type, property or method that’s marked as private
will only be visible within its own type (which also includes extensions on that type defined within the same file).
Anything that should be considered a private implementation detail of a given type should arguably be marked as private
. For example, our price calculator’s discount
property from before was really only meant to be used within its own class — so let’s go ahead and make that property private:
class DiscountedPriceCalculator: PriceCalculator {
private let discount: Int
...
}
Our previous implementation will continue to work exactly the same way as before, since discount
will remain entirely visible within our DiscountedPriceCalculator
class. However, if we wanted to slightly extend that visibility to also include other types defined within the same file, we’d have to use fileprivate
— which does exactly what it sounds like, it keeps a declaration private within the file that it’s defined in:
class DiscountedPriceCalculator: PriceCalculator {
fileprivate let discount: Int
...
}
With the above change in place, we can now access our discount
property from related code defined in the same file — such as this extension on UIAlertController
which lets us easily show a price description for an array of products within an alert:
extension UIAlertController {
func showPriceDescription(
for products: [Product],
calculator: DiscountedPriceCalculator
) {
let price = calculator.calculatePrice(for: products)
// We can now access 'discount' even outside of the type
// that it's declared in, thanks to 'fileprivate':
message = """
Your \(products.count) product(s) will cost \(price).
Including a discount of \(calculator.discount).
"""
}
}
When it comes to free functions, types and extensions, private
and fileprivate
act exactly the same. They’re only different when applied to declarations that are defined within a type.
So, to sum up, these are the five levels of access control that Swift currently offers:
private
keeps a property or function private within its enclosing type, including any extensions on that type that are defined within the same file. When applied to a top-level type, function or extension, it acts the same way asfileprivate
.fileprivate
makes a declaration visible within the entire file that it’s defined in, while hiding it from all other code.internal
is the default access level, and makes a declaration visible within the whole module that it’s defined in.public
reveals a function, type, extension or property outside of its module.open
enables a class to be subclassed, and a function or property to be overridden, outside of its module.
In general, it’s often best to start out with the most restrictive level of access that a given declaration can practically have, and then open things up later if needed. That way we’re limiting the avenues for interaction between our various types and functions, which may at first seem like a bad thing, but is often truly essential in order to build maintainable and well-structured systems.
Thanks for reading! 🚀
(Note that this article didn’t go into mutation-specific access modifiers, such as private(set)
. Those will be covered by another Basics article in the future.)