iOS Bite: defer

By Jason Connery

May 17, 2024 at 10:15

ios-bites

defer

defer is a way to define code statements to execute at the end of scope.

Typically, this would be for cleanup tasks.

It’s ideal for situations where there’s more than one way to exit the current scope - more than one return, a throw, etc.

I feel like I don’t often see it , and sometimes forget to use it myself - despite defer existing since the early days.

Example - there’s a bug here

func loadContent() async {
    //Show loading indicator and clear old data
    isFetching = true
    items = []
    do {
        //Update items and turn off loading data
        items = try await fetchData()
        isFetching = false
    }
    catch {
        //TODO: handle error
        print("Loading error: \(error)")
    }
}

Note, we don’t reset isFetching in the catch.

We could add the missing reset to the catch

func loadContent() async {
    //Show loading indicator and clear old data
    isFetching = true
    items = []
    do {
        //Update items and turn off loading data
        items = try await fetchData()
        isFetching = false
    }
    catch {
        //TODO: handle error
        print("Loading error: \(error)")
        isFetching = false
    }
}

But if our function has additional exit points, the issue returns

do {
    //Update items and turn off loading data
    let fetchedItems = try await fetchData()

    guard !Task.isCancelled else {
        //forgot it here!
        return
    }

    if fetchedItems.isEmpty {
        //and here
        throw PlaygroundError.emptyItems
    }
    else {
        items = fetchedItems
        isFetching = false
    }
}
catch {
    ...

The defer block

defer {
    isFetching = false
}

Deter blocks can be anywhere in the scope, but typically close to related statements.

You can have multiple defer statements if you need to clean up or reset multiple things.

It’s async compatible - doing an await within a scope doesn’t cause that scope to end.

defer blocks allow for a neater solution

func loadContent() async {
    //Show loading indicator and clear old data
    isFetching = true
    items = []

    defer {
        isFetching = false
    }

    do {
        //Update items
        let fetchedItems = try await fetchData()
        items = fetchedItems
        // no need to reset isFetching here
    }
    catch {
        print("Loading error: \(error)")
        // no need to reset it here
    }
}

Other solutions to this specific problem

This example is overly simplistic, and could be solved by restructuring. This particular example of a loading state only while fetching could also be neatly solved with Alex’s Loadable enum, which makes storing the data or the loading state mutually exclusive.

defer could also be used to close anything session or context based, delete temporary files, any item its important to set something to false or call a close, save, finish type method when done with something.

Where doesn’t defer help?

Any code structure where scope ends “early” in your flow, or has multiple scopes, for example delegate callbacks and closures.

This might explain defer not being that common, but with async await having existed for many years now, those patterns are less and less common.

Here is an example of defer that wouldn’t work with a closure based fetch:

func loadContent() async {
    //Show loading indicator and clear old data
    isFetching = true
    items = []

    defer {
        isFetching = false
    }

    fetchDataOutdated(completion: { result in
        switch result {
        case .success(let data):
            self.items = data
        case .failure(let error):
            print("fetch error: \(error)")
        }
    })
    //execution hits here, ending scope before escaping closure is called
}

Last updated: May 17, 2024 at 10:15