Weekly Swift articles, podcasts and tips by John Sundell.

Access control

Published on 11 Jan 2020

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:

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.)