id(_): Identifying SwiftUI Views

In this article, we will explore how SwiftUI uses the .id() method to identify a view. This is a method that has been the source of a lot of speculation. The question we all ask ourselves is: What is it good for? Well, we’ll see that the obvious answer is not necessarily the right answer.

What Is It Good For?

The name suggests the id() method is used to identify a view. Although not exactly false, there’s plenty to be said about that statement.

I’ll start by saying it took me a long time to decide to write about this method. Mostly, because until now, I couldn’t let go of the idea of what I thought the method should do.

The official Apple documentation has a single sentence to describe the .id() method:

Generates a uniquely identified view that can be inserted or removed.

Many of us thought. Ok, that’s wonderful. By assigning an id to a view, I can identify it and move it around as I please, making it jump from one container to another, or whatever I want. Well. Not so fast. If you try to do that, you’ll see that the id() modifier doesn’t seem to do anything. It’s just a waste of screen space.

So what’s the big reveal? Well, I’ll dare to say that the id of a view, identifies a view, but only partially. There are other internal considerations made by the framework, to determine if two views are the same. So if that is the case, then for the umpteenth time: what is .id() good for?

What I found, is that although we cannot use the id to determine if a view continues to be the same view as before, we can use it for the opposite. That is, we use id() to tell SwiftUI that a view is NO LONGER the same view it was.

But enough talking, let’s see some examples in action.

2020 Update: Apple slightly modified the documentation to make it less confusing, but it still remains a little vague.

Resetting State Values

In our first example, we are going to use the id() method, to trigger a reset of all the State values of a view. By changing the id value, all the State properties will revert back to their initial values:

struct ContentView: View {
    @State private var theId = 0
    
    var body: some View {
        VStack {
            ExampleView().id(theId)
            
            Button("Reset") { self.theId += 1 }
        }
    }
}

struct ExampleView: View {
    @State private var firstname = ""
    @State private var lastname = ""
    @State private var email = ""
    @State private var website = ""

    var body: some View {
        Form {
            TextField("Enter firstname", text: self.$firstname)
            TextField("Enter lastname", text: self.$lastname)
            TextField("Enter email address", text: self.$email)
            TextField("Enter website address", text: self.$website)
        }
    }
}

Technically speaking, we are not really resetting the State values. We are replacing the view with a new one, and because it is new, the State properties all have their initial values.

The fact that the view is completely replaced will become more evident with the following example.

Triggering Transitions

Now we are going to change the id of a view, to see how it triggers a transition to remove the old view, and another transition to insert the new one:

struct ContentView: View {
    @State private var theId = 0
    
    var body: some View {
        VStack(spacing: 20) {
            MyCircle()
                .transition(AnyTransition.opacity.combined(with: .slide))
                .id(theId)
            
            Text("id = \(theId)    ")

            Button("Increment Id") {
                withAnimation(.easeIn(duration: 2.0)) {
                    self.theId += 1
                }
            }
        }
    }
    
    struct MyCircle: View {
        private let color: Color = [.red, .green, .blue, .purple, .orange, .pink, .yellow].randomElement()!
        
        var body: some View {
            return Circle()
                .foregroundColor(color)
                .frame(width: 180, height: 180)
        }
    }
}

Improving the List View Performance

You probably noticed that when Lists start to have a few hundred rows, performance can become an issue if the backing array is modified. This is because, in its quest for being so “automatic”, SwiftUI tries to diff the before and after rows, detect the changes, and animate the modified rows into their new locations. This is a wonderful feature, but it can play against us when dealing with large data sets.

Some of those problems can be solved with the .id() method. By assigning an id to the List view, we can make sure we update that id whenever we update the array. This prevents SwiftUI from diffing the before and after, and instead, it creates a brand new List. With only 500 rows, the difference is already very noticeable:

Shuffle the array without using .id():

extension String {
    static func random(length: Int = 20) -> String {
        String((0..<length).map { _ in "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".randomElement()! })
    }
}

struct ContentView: View {
    @State private var array = (0..<500).map { _ in String.random() }
    
    var body: some View {
        VStack {
            List(array, id: \.self) { item in
                Text("\(item)")
            }

            Button("Shuffle") {
                self.array.shuffle()
            }
        }
    }
}

Shuffle the array using .id():

extension String {
    static func random(length: Int = 20) -> String {
        String((0..<length).map { _ in "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".randomElement()! })
    }
}

struct ContentView: View {
    @State private var theId = 0
    @State private var array = (0..<500).map { _ in String.random() }
    
    var body: some View {
        VStack {
            List(array, id: \.self) { item in
                Text("\(item)")
            }.id(theId)

            Button("Shuffle") {
                self.array.shuffle()
                self.theId += 1
            }
        }
    }
}

This trick may help us deal with long lists, but there are a couple of downsides, which depending on your actual needs, may become a show stopper, or not. You decide:

  • Changes to row locations will not be animated.
  • The scroll position will reset to the top.

These limitations are understandable, considering what we are doing. Unfortunately, since List does not provide (for the moment) a way to scroll to position, this may be a problem for certain use cases.

Summary

While .id() may not be all that we would expect, it still provides some useful applications. Using it to solve the performance problems in the List view can be a lifesaver, that is if you can spare the scroll limitation. In a way, it reminds me of the .reloadData() method of the UITableView. Please feel free to comment below, and follow me on twitter, if you would like to be notified when new articles come out. Until next time!

12 thoughts on “id(_): Identifying SwiftUI Views”

  1. Back in the mists of time when the project first started, the identifier was used to pair a (potentially copied, regenerated, etc.) View structure with the referenced on-screen resources (UIView, IOSurface, etc.). The identifier would be used to determine whether a given SwiftUI View should be treated as “create a new view object to put on the screen” or “find the corresponding pre-existing view object and update its state to match this.” It was *the* deep problem to solve in the whole effort—how to create & manage such identification implicitly.

    That may or may not be the case today, I’m not sure. However, you might be able to tell—drop a breakpoint on `-[UITableView initWithFrame:style:]` and see if a new instance is being created. It may be clever enough to just do a `-reloadData` call under the hood though (since that can effectively replace everything about the view).

    Reply
  2. There is also an accessibility(selectionIdentifier:). I wonder which is for getting a view in UI test, or if SwiftUI test is supported at all.

    Reply
  3. Very lovely presentation.
    I was wondering if you can assist with an issue I have with Arrays.
    I have seen a lot of examples of Arrays pre-populated, but not a single one which can potentially get its data from a CoreData Entity.
    I am using CoreData.
    Looping using ForEach or List only allows a Text View and does not allow me to append to an empty String Array.
    I need the array further process the Array for my App.
    Any Ideas?

    Very nice presentation.

    Kind Regards

    Reply
  4. Apple has changed its description to the `id()` method:
    “Binds a view’s identity to the given proxy value.”

    BTW, about the first heading “Was Is It Good For?”, it’s actually “What Is It Good For?”, right?

    Reply
  5. Fantastic ! I have spent ages looking at complex ways to get .onAppear() to execute when returning to “myView” where I compute different values each time it is displayed.

    After reading this article, I can achieve this by using:

    myView().id(Int.random(in: 1..<123))

    Many thanks !

    Reply
  6. Thanks for a great tip!
    I have two arrays which affect each other mutually when any one cell is updated (a grid of TextFields), including possibly lengthening or shortening the arrays. .id() was a great way of telling the parent view to repost when committing a change.
    It has (almost) fixed my problem. I say almost because I think the ForEach(1..<array.indices) that lays out the grid does not always notice the change in array size.

    Reply

Leave a Comment

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close