Framework Access Levels in Swift - A Curious Journey

Why we have public and open keywords after all?

Subscribe to my newsletter and never miss my upcoming articles

Hello fellow developers, Leo here.

Today will be a quick topic about framework building and the difference between public and open access control levels.

This masterpiece was painted by Jacques Fouquier, Jacques Fouquières or Jacob Focquier (c. 1590/91 – 1655) was a Flemish landscape painter and was chosen because a castle has various access layers. Got it? The Hortus Palatinus, or Garden of the Palatinate, was a Baroque garden attached to Heidelberg Castle, Germany. The garden itself was commissioned by Frederick V, Elector Palatine in 1614 for his new wife, Elizabeth Stuart, and became famous across Europe during the 17th century. At the time it was known as the 'Eighth Wonder of the World', and has since been termed 'Germany's greatest Renaissance garden.'

Let's go!

Problem

You want to share your code outside the framework but you are getting errors like: No such "class/struc/func/etc".

First of all we need a little of background knowledge about access control in Swift. Swift provides default access control levels, this means that you don't need to specify explicitly access control levels in a single-target app.

Swift's access control model has two concepts: modules and source files.

A module is everything that is a single unit of code distribution, this means every time you can build something in Xcode, it is a module. A module can be an app bundle or even a framework, Xcode will both as modules. Modules can be imported by other modules with the Swift's import keyword.

A source file is literally the ".swift" file. You can create and define as much as you want structs/classes/enums in a single source file and all will share the same source file.

That said we can now explore the five Swift's access levels.

Access levels

There are five access levels: open , public, internal, fileprivate and private. Open access is the highest (least restrictive) access level and private access is the lowest (most restrictive) access level.

Private and File Private

Private access restricts the use of the entity in the enclosing declaration. For example if you declare a "private let name: String" inside a struct Person, that property name will only be accessible from inside the Person struct. Setting properties and func private or fileprivate conforms to Open/Close principle of SOLID as you can check in the example below:

class Person1 {
    private let name: String = ""
}

class Person2: Person1 {
    init() {
        print(name)
    }
}

This will result in:

Screen Shot 2021-04-08 at 09.03.22.png

Like many other languages, Swift allows a class to override methods and properties declared in its superclasses. This means that the program has to determine at runtime which method or property is being referred to and then perform an indirect call or indirect access.

This technique, called dynamic dispatch, increases language expressivity at the cost of a constant amount of runtime overhead for each indirect usage

Do you remember that we said earlier that Swift access control has two contexts? It's now that we will apply that because fileprivate access control level is all about the file that it's declared. You define something file private like the example :

fileprivate let nameList: [String]

This means that nameList array will be available only in the file its belong. If any other ".swift" file try to use nameList it won't be possible because nameList don't exist outside the file scope. Very neat isn't it?

That's the reason why Xcode refactor tool always mark your extracted methods are marked with file private. If the method was used by any other members (class/structs/enums) in the same file, your refactor will still work! Very good shortcut for developers.

Wrap-up: private is the most restrictive access level giving access only to the enclosing type and fileprivate the second most restrictive giving access to anyone in the same file, not necessarily the same type.

Internal

Internal is the default access level. Everything you declare that you don't explicitly declare a access level, compiler will add an internal to it. Transforming automagically this:

let nameList: [String]

To this:

internal let nameList: [String]

The default access level is important because it's declare that everything inside that module context ( remember the contexts?) can use/see the declaration. This means that if you have a framework and an app, and you don't want that the framework could inspect inside your app, this feature comes by default. And that's great because in the next sections we'll will explore the least restrictive access levels and why they are important.

Wrap-up :

All entities in your code (with a few specific exceptions, as described later in this chapter) have a default access level of internal if you don’t specify an explicit access level yourself.

Public and Open

Public and Open are special cases used only if you want to your declaration be accessible outside the module context. For example, you are creating a framework and need to expose the main APIs/Entities to anyone who imports it. Like the struct below:

public struct HttpsHeader {
    let autentication: String

    func printAllHeader() {
        print("allHeader")
    }
}

Image the code above in your framework. Outside the framework anyone can see this Struct, but they can create this? This is a tricky question and we need some initializer knowledge to answer that.

A default initializer has the same access level as the type it initializes, unless that type is defined as public. This is still valid for memberwise initializer of structs too, so to the code above be used outside the framework we need to add:

public struct HttpsHeader {
    let autentication: String

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

    func printAllHeader() {
        print("allHeader")
    }
}

Now you can initialize the HttpsHeader struct outside the framework. As you might've be thinking right now the func printAllHeader are not visible outside the framework.

It's important you notice that public and Open are both OPT-IN access levels. This is brilliantly designed by the Swift developers because this behavior will assure that everything you are exposing outside the module it's wanted. You can't unintentionally expose some internal function or properties. This is amazing and a very good language design choice.

Finally the open access control level. The open keyword is used only by class or class members. And that differs from public access because any class marked as open allows the code outside the module to subclass and override it's features. It's important to be very cautious using open keyword because if your framework depends of some feature implemented of that class, if anyone subclass/override it, will the framework be able to work properly? These questions aren't arisen with the public keyword because any class market with public access level can't be subclassed.

Principles Of Access Control Levels

As the docs says:

No entity can be defined in terms of another entity that has a lower (more restrictive) access level.

This is the most important part of understanding all the Swift's access control. This drives all the compiler decisions and you should take that in to account when choosing you entities/properties access control level. You doesn't need to know everything about Swift by heart, because it's impossible and it's not productive but I'm sure that if really understand this principle your coding way will be way smoooooother.

This principle leads to a various curious situations.

Curious situations

For functions this code won't compile:

public struct HttpsHeader {
    let autentication: String

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

    func printAllHeader(privateClass: PrivateClass) { // This won't compile
        print()
    }

    private class PrivateClass {

    }

}

Resulting: Screen Shot 2021-04-09 at 09.01.18.png

This means that the function can't have a access level least restrictive that it parameters. And the result type is included in that rule too:

public struct HttpsHeader {
    let autentication: String

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

    func printAllHeader() -> PrivateClass { // This won't compile too with the same problem above
        return PrivateClass()
    }

    private class PrivateClass {

    }

}

To compile you need to add private to the func declaration too.

The tuple follows this principle too and this shouldn't compile:

struct HttpsHeader {
    public let autentication: String
    let httpsTuple: (PublicClass, FilePrivateClass)

    fileprivate class FilePrivateClass {}
    public class PublicClass {}
}

You have to give the httpsTuple property a fileprivate keyword to work. Tuple is a special case that it doesn't have a access level it is automatically attributed to it in compile time following the same principle rule. The tuple final access level will be equal the most restrictive access level among all the tuple types.

Talking about extensions, you set a extension with a explicit control access level and this leads to a new default access level for all properties, functions and members inside that extension.

you can mark an extension with an explicit access-level modifier (for example, private) to set a new default access level for all members defined within the extension. This new default can still be overridden within the extension for individual type members.

Conclusion

Today we study a little bit of access levels and some curiosities about them. Also we discover the guiding principle of all Swift's language access controls decision and how can you reason about the levels on your own code. It's possible that if you only work with single-target app, you never come across public, open or even internal because you ever needed them. Because of SOLID principles it's more common use the private and fileprivate ones.

That's all my people, I hope you liked this as I liked writing this. If you want to support this blog you can Buy Me a Coffee or just leave a comment saying hello.

Thanks for the reading and... That's all folks.

credits: image

No Comments Yet