Top 7 Subtle Swift Features

Here, I collected Swift features that are less known and can be useful when you prepare for interviews or want to deepen your Swift knowledge.

Alex Dremov
Top 7 Subtle Swift Features

1. Keyword indirect

It’s used with enums only. As you know, enums are value type and stored on the stack. Therefore, the compiler needs to know how much memory each enum takes. As only one option is possible at any moment, the enum occupies the memory of the largest case plus some operational information.

// Just a general enum, nothing fancy
enum Foo {
    case bizz(String)
    case fizz(Int)
}

But what if we make enum dependant on itself?

// Infinite size??
enum Foo {
    case bizz(Foo)
    case fizz
}

This definition generates a compiler error.

😡
Recursive enum Foo is not marked indirect

The error makes sense: the compiler can’t calculate Foo size as it tends to infinity. Here comes the indirect keyword.

// Oh, fine
enum Foo {
    indirect case bizz(Foo)
    case fizz
}

Simple: it modifies the enum memory structure to solve the recursion problem. Detailed: .bizz(Foo) is no longer stored inline in memory. Actually, with the indirect modifier data is now stored behind a pointer (indirectly).

Problem solved! Also, we can modify the whole enum as indirect

// Every case is indirect now
indirect enum Foo {
    case bizz(Foo?)
    case fizz(Foo?)
}

2. Attribute @autoclosure

Swift’s @autoclosure attribute enables you to define an argument that automatically gets wrapped in a closure. It’s mostly used to defer the execution of an expression to when it’s actually needed.

func calculate(_ expression: @autoclosure () -> Int,
               zero: Bool) -> Int {
    guard !zero else {
        return 0
    }

    return expression()
}

Then, calculate can be called like this:

calculate(1 + 2, zero: false) // 3

calculate([Int](repeating: 5, count: 10000000).reduce(0, +),
                zero: false) // 50000000

calculate([Int](repeating: 5, count: 1000).reduce(0, +),
                zero: true) // 0

So, in this case, when zero: true, the call of calculate does not calculate the expression at all, improving code performance.

Subscribe and don't miss posts!

3. Lazy

A lazy stored property is a property whose initial value isn’t calculated until the first time it’s used. Lazy properties must always be declared as a variable. Note that if you use lazy in struct, then the function that uses it must be marked as mutating.

class Foo {
    lazy var bonk = DBConnection()
    
    func send() {
        bonk.sendMessage()
    }
}

We already covered @autoclosure which also can help to defer expression evaluation. That can be used with lazy! Consider this common case of dependency injection.

class Foo {
    let bonkProvider: () -> DBConnection
    lazy var bonk: DBConnection = bonkProvider()
    
    init(_ expression: @escaping @autoclosure () -> DBConnection) {
        self.bonkProvider = expression
    }
    
    func send() {
    	// Here bonkProvider() is called
        // only for the first call of send()
        bonk.sendMessage()
    }
}

4. Enums as namespaces

Swift does not have namespaces, which may be a problem in big projects. This is easily solved with enums.

enum API {}

extension API {
    static let token = "…"

    struct CatsCounter {
        …
    }
}

let a = API.CatsCounter()
print(API.token)

5. Dynamic member lookup

This section describes the @dynamicMemberLookup attribute. It can be used with structs and classes.

Just adding @dynamicMemberLookup to the definition generates an error

😡
@dynamicMemberLookup attribute requires Foo to have a subscript(dynamicMember:) method that accepts either ExpressibleByStringLiteral or a key path

Therefore, such subscript needs to be defined

@dynamicMemberLookup
class Foo {
    subscript(dynamicMember string: String) -> String {
        return string
    }
}

let a = Foo()
print(a.helloWorld)

In subscript you can implement much more complex logic to retrieve data. But you can see how this implementation is limited to strings only and not really safe. This can be modified with a key path.

class Bob {
    let age = 22
    let name = "Bob"
}

@dynamicMemberLookup
class Foo {
    let himself = Bob()
    
    subscript<T>(dynamicMember keyPath: KeyPath<Bob, T>) -> T {
        return himself[keyPath: keyPath]
    }
}

let a = Foo()
print(a.age)

Even though you know about this feature does not mean that it should be used everywhere. It’s up to you what is more readable and expressive: a.himself.age or a.age.


6. Dynamically callable

Also, a compiler feature that allows you to call objects. Can be applied to struct, enum, and class.

After adding the attribute, the error is generated:

😡
@dynamicCallable attribute requires RangeGenerator to have either a valid dynamicallyCall(withArguments:) method or dynamicallyCall(withKeywordArguments:) method

The method signature is similar to that of @dynamicMemberLookup.

@dynamicCallable
struct RangeGenerator {
    var range: Range<Int>
    
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> [Int] {
        if args.count > 1 || args.first?.key != "count" {
            fatalError("Unknown arguments \(args)")
        }
        let count = args.first!.value
        return (0..<count).map{ _ in Int.random(in: range) }
    }
}

let gen = RangeGenerator(range: 0..<100)
print(gen(count: 13))
// [2, 89, 4, 17, 65, 26, 73, 86, 93, 13, 25, 96, 96]

7. Inlining

Sometimes you want to give additional information about optimisations the compiler can use. Inlining code is one of the most important optimization features. So, how to use ‌@inlinable, @inline(__always), @usableFromInline?

The @inlinable attribute exports the body of a function as part of a module's interface, making it available to the optimizer when referenced from other modules.

As a result, @inlinable makes the implementation of the method public and able to be inlined into the caller. Secondly, it forces you to make everything it calls @usableFromInline.

@inline(__always) tells the compiler to ignore inlining heuristics and always (almost) inline the function.

A function that is @inline(__always), but not @inlinable, will not be available for inlining outside its module, because the function's code is not available.

💥
@inline(__always) can be beneficial for performance, but it can also have catastrophic effects on macro performance due to code size increase.
struct Foo {
    @inlinable
    @inline(__always)
    func simpleComputation(_ a: Int, _ b: Int) -> Int {
        duplicate(a) + duplicate(b)
    }
    
    @usableFromInline
    func duplicate(_ c: Int) -> Int {
        c * 2
    }
    
    func general() {
        print("Hello world")
    }
}

This has more effects on implementation, check the discussion on this forum if you want to understand this in-depth

References

  1. https://www.swiftbysundell.com/articles/using-autoclosure-when-designing-swift-apis/
  2. https://www.swiftbysundell.com/articles/powerful-ways-to-use-swift-enums/
  3. https://www.hackingwithswift.com/articles/134/how-to-use-dynamiccallable-in-swift
  4. https://www.hackingwithswift.com/example-code/language/what-are-lazy-variables
  5. https://forums.swift.org/t/who-benefits-from-the-indirect-keyword/20167
  6. https://www.tothenew.com/blog/recursive-enumerations-in-swift/
  7. https://www.avanderlee.com/swift/dynamic-member-lookup/

Share

Subscribe to Alex Dremov

Get the email newsletter and receive valuable tips to bump up your professional skills