iosswiftswiftuiipad

SwiftUI NavigationSplitView: Confused due to an official example by Apple


I'm tinkering with SwiftUI-NavigationSplitView. I started with this example from the official Developer-documentation.

Overview

The UI, I have made is very similar:

import SwiftUI

struct ContentView: View {
    @State var articles = [Article]()
    @State var selectedArticle: Article?
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selectedArticle) {
                ForEach(articles) { article in
                    // With attached .tag-modifier it works fine.
                    Text(article.name) // .tag(article)
                }
            }
        } detail: {
            VStack(alignment: .leading) {
                Text(selectedArticle?.name ?? "")
                    .font(.title2)
                    .bold()
                Text(selectedArticle?.desc ?? "")
                Spacer()
            }.padding()
        }
        .onChange(of: selectedArticle, {
            print(selectedArticle?.id)
            print(" ------------- ")
        })
        .onAppear() {
            for i in 1...6 {
                let article = Article(
                    name: "Name_\(i)",
                    desc: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor.\nIn enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus."
                )
                articles.append(article)
            }
        }
    }
}

#Preview {
    ContentView()
}

The Article-struct, I'm using:

struct Article: Identifiable, Hashable {
    let id = UUID()
    var name: String
    var desc: String
}

Previously it hadn't the .tag-modifier attached to the list-items.

It didn't work. The selectedArticle-variable never changed.

Finally I found a forum-post, where I saw the usage of .tag. That remind me, how it could work.

But I still don't understand the example of Apple.

How is it possible, that the Apple-example works? In case it really works. Can someone give some insights?

Has it to do with the usage of IDs?


Solution

  • Without an explicit tag, ForEach on a collection of Identifiable elements automatically tags each view with the elements' ids. It is as if you have wrote:

    ForEach(articles) { article in
        Text(article.name).tag(article.id)
    }
    

    The code in Apple's documentation uses the List initialiser that takes a collection directly, which implicitly adds tags the same way.

    As you may know, the selection of the List contains the tags of the list rows that are selected.

    In the documentation's code, the list is displaying some Employees, and they use a Set<Employee.ID> to record the list selection. This is because the automatically-added tags are of type Employee.ID.

    @State private var employeeIds: Set<Employee.ID> = []
    //                                  ^^^^^^^^^^^
    
    var body: some View {
        NavigationSplitView {
            List(model.employees, selection: $employeeIds) { employee in
                Text(employee.name)//.tag(employee.id)
            }
        } detail: {
            EmployeeDetails(for: employeeIds)
        }
    }
    

    In your code however, you have used a Article? to record the list selection.

    @State var selectedArticle: Article?
    

    This does not match the tags of your list rows. The list rows' automatically-added tags are of type Article.ID, aka UUID. This is why selectedArticle never changes - you did not select anything that has a tag of type Article.

    If you change selectedArticle to be of type Article.ID?, then it will work with the automatically-added tags.