Table in SwiftUI (Episode III° – The Split)

When you use the app on an iPad, you often see a list on the left and some details on the right. In this post, we will explore how to implement this layout.

Our starting point is the code from the first post in this series, which you can find in ContentView.swift.

The first change we’ll make is to create our TableView struct::

struct TableView: View {
    @State var beers: [Beer] = []
    @State var page = 0
    @State private var sortOrder = [KeyPathComparator(\Beer.name, order: .reverse)]
    @State private var searchText = ""
    @State private var beerSelected: Set<Beer.ID> = Set<Beer.ID>()
    var body: some View {
        Table(beersSearched, selection: $beerSelected, sortOrder: $sortOrder) {
            TableColumn("Name", value: \.name)
            TableColumn("First Brew", value: \.first_brewed)
            TableColumn("ABV") { beer in
                Text("\(beer.abv)")
            }
            TableColumn("Image") { beer in
                AsyncImage(url: URL(string: beer.image_url)) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    ProgressView()
                }
                .frame(width: 100, height: 100)
            }
        }.onAppear {
            Task {
                await getBeers()
            }
        }.onChange(of: sortOrder) {
            beers.sort(using: sortOrder)
        }.searchable(text: $searchText, prompt: Text("Search by name"))
    }
    
    var beersSearched: [Beer] {
        if searchText.count == 0 {
            return self.beers
        } else {
            return self.beers.filter({(beer: Beer) -> Bool in
                return beer.name.lowercased().contains(searchText.lowercased())
            })
        }
    }
    
    func getBeers() async {
        do {
            page += 1
            let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=30")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let beersLoaded = try JSONDecoder().decode([Beer].self, from: data)
            beers = beersLoaded + beers
            beers.sort(using: sortOrder)
        } catch {
            print("Some error")
        }
    }
}

This approach allows us to reuse the code as follows:

struct ContentView: View {
    var body: some View {
        NavigationSplitView(sidebar: {
            TableView()
        }, detail: {
            TableView()
        })
    }
}

With the NavigationSplitView, there is an element on the left (the sidebar) and the rest of the space is dedicated to the detail view. If we insert a table in the sidebar, only the first column will be visible:

With this code, the same view is presented in both the sidebar and the details section. As an exercise, you can try to create your own custom detail view.

Here is one possible solution:

struct TableView: View {
    @State var beers: [Beer] = []
    @State var page = 0
    @State private var sortOrder = [KeyPathComparator(\Beer.name, order: .reverse)]
    @State private var searchText = ""
    @Binding var beerSelected: Beer?
    @State var idSelected: Beer.ID?
    
    var body: some View {
        Table(beersSearched, selection: $idSelected, sortOrder: $sortOrder) {
            TableColumn("Name", value: \.name)
            TableColumn("First Brew", value: \.first_brewed)
            TableColumn("ABV") { beer in
                Text("\(beer.abv)")
            }
            TableColumn("Image") { beer in
                AsyncImage(url: URL(string: beer.image_url)) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    ProgressView()
                }
                .frame(width: 100, height: 100)
            }
        }.onAppear {
            Task {
                await getBeers()
            }
        }.onChange(of: sortOrder) {
            beers.sort(using: sortOrder)
        }.searchable(text: $searchText, prompt: Text("Search by name"))
        .onChange(of: idSelected) { // <-- New
            beerSelected = beers.first(where: {(beer) in beer.id == idSelected!})
        }
    }
    
    // the rest of the code is equal
}

I made a few minor adjustments:

  • The beerSelected variable is now an optional of type Beer? This variable will hold the selected beer and is bound to the variable passed from ContentView.
  • idSelected contains the ID of the selected beer.
  • I added a new onChange method, which searches for the selected beer by its ID. I’m aware it’s not the best approach, but for this purpose, it’s sufficient.

The ContentView is now updated as follows:

struct ContentView: View {
    @State private var beerSelected: Beer?

    var body: some View {
        NavigationSplitView(sidebar: {
            TableView(beerSelected: $beerSelected)
        }, detail: {
            if let selected = beerSelected {
                VStack {
                    AsyncImage(url: URL(string: selected.image_url)) { image in
                        image.resizable().scaledToFit()
                    } placeholder: {
                        ProgressView()
                    }
                    .frame(width: 200, height: 200)
                    Text("\(selected.name)")
                }
            } else {
                Text("Please Select")
            }
        })
    }
}

In the ContentView, we declare beerSelected which is passed to the TableView. When it’s not null, we display the name and the image of the beer.

One last thing, is possible change the width of the column, using the property width.

With this post, the series is complete. Stay tuned for new posts.

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.

share this post with friends

Picture of Nicola De filippo

Nicola De filippo

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve

Leave a comment

Your email address will not be published. Required fields are marked *

Who I am

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve.

Follow Me Here

Get The Latest Updates

Periodically receive my super contents on coding and programming

join the family;)

Recent Posts