admaDIC App Development & IT Solutions

SwiftUI Table on iOS

by Annett Schwarze | 2025-11-28

SwiftUI offers a powerful Table view on macOS, while it is more limited on iOS. In this example the Table is used on iOS with special considerations for small and large screens.

For instance on small screens only the first table column is shown. Therefore the information of the other columns is included in the first column for these cases. As the table header is not clickable for sorting, the sorting feature is implemented without using Table's built-in sorting features.

        
import SwiftUI

struct ContentView: View {
    struct Game: Identifiable, Hashable, Codable {
        enum Console: String, CaseIterable, Codable, Hashable, Identifiable, Comparable {
            static func < (lhs: Game.Console, rhs: Game.Console) -> Bool {
                lhs.rawValue < rhs.rawValue
            }
            case ps5 = "PS5"
            case xbox = "Xbox"
            case nintendoSwitch = "Nintendo Switch"
            var id: String { rawValue }
        }

        enum Owner: String, CaseIterable, Codable, Hashable, Identifiable, Comparable {
            static func < (lhs: Game.Owner, rhs: Game.Owner) -> Bool {
                lhs.rawValue < rhs.rawValue
            }
            case mother = "Mother"
            case father = "Father"
            case child1 = "Child 1"
            case child2 = "Child 2"
            var id: String { rawValue }
        }

        let id: UUID
        let title: String
        let console: Console
        let owner: Owner

        init(id: UUID = UUID(), title: String, console: Console, owner: Owner) {
            self.id = id
            self.title = title
            self.console = console
            self.owner = owner
        }
    }

    @State private var games: [Game] = [
        Game(title: "Mario Party", console: .ps5, owner: .mother),
        Game(title: "Kirby", console: .xbox, owner: .father),
        Game(title: "Zelda", console: .nintendoSwitch, owner: .child1),
        Game(title: "Pikmin", console: .ps5, owner: .child2)
    ]

    @State private var selectedConsole: Game.Console? = nil
    @State private var selectedOwner: Game.Owner? = nil
    @State private var sortBy: SortBy? = nil
    @State private var searchText: String = ""

    @State private var sortOrder: [KeyPathComparator < Game > ] = [
        KeyPathComparator(\Game.title),
        KeyPathComparator(\Game.console),
        KeyPathComparator(\Game.owner),
    ]

    enum SortBy: String, CaseIterable, Codable, Hashable, Identifiable {
        case title = "Title"
        case console = "Console"
        case owner = "Owner"
        var id: String { rawValue }
    }

    private var filteredAndSortedGames: [Game] {
        var result = games
        if let selectedConsole { result = result.filter { $0.console == selectedConsole } }
        if let selectedOwner { result = result.filter { $0.owner == selectedOwner } }
        if !searchText.isEmpty {
            let q = searchText.lowercased()
            result = result.filter { $0.title.lowercased().contains(q) }
        }
        if let sortBy {
            switch sortBy {
            case .title:
                result.sort { lhs, rhs in
                lhs.title < rhs.title
            }
            case .console:
                result.sort { $0.console.rawValue < $1.console.rawValue }
            case .owner:
                result.sort { $0.owner.rawValue < $1.owner.rawValue }
            }
        }
        return result
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("GameTracker").font(.largeTitle).bold()
                .padding(.leading, 20)

            controlBar

            Group {
                if UIDevice.current.userInterfaceIdiom == .phone {
                    Table(filteredAndSortedGames, sortOrder: $sortOrder) {
                        TableColumn("Game") { game in
                            VStack(alignment: .leading, spacing: 4) {
                                Text(game.title)
                                    .font(.body)

                                Text(game.console.rawValue)
                                    .foregroundStyle(.secondary)
                                    .font(.caption)
                                Text(game.owner.rawValue)
                                    .foregroundStyle(.secondary)
                                    .font(.caption)
                            }
                        }
                    }
                } else {
                    Table(filteredAndSortedGames,  sortOrder: $sortOrder) {
                        TableColumn("Title") { game in
                            Text(game.title)
                        }
                        TableColumn("Console") { game in
                            Text(game.console.rawValue)
                        }
                        TableColumn("Owner") { game in
                            Text(game.owner.rawValue)
                        }
                    }
                }
            }
        }
        .padding()
        .searchable(text: $searchText, placement: .automatic, prompt: "Search titles")
    }

    private var controlBar: some View {
        AdaptiveStack(hSpacing: 50, vSpacing: 20, alignment: .leading) {
            VStack(alignment: .leading) {
                Text("Sort by").font(.title2)
                Menu {
                    Button("Unsorted") { sortBy = nil }
                    Divider()
                    ForEach(SortBy.allCases) { sortKey in
                        Button(sortKey.rawValue) { sortBy = sortKey }
                    }
                } label: {
                    Label(sortBy?.rawValue ?? "Unsorted", systemImage: "arrow.up.arrow.down")
                }
            }

            VStack(alignment: .leading) {
                Text("Filter").font(.title2)
                HStack(spacing: 20) {
                    // Console filter
                    Menu {
                        Button("All Consoles") { selectedConsole = nil }
                        Divider()
                        ForEach(Game.Console.allCases) { console in
                            Button(console.rawValue) { selectedConsole = console }
                        }
                    } label: {
                        Label(selectedConsole?.rawValue ?? "All Consoles", systemImage: "gamecontroller")
                    }

                    // Owner filter
                    Menu {
                        Button("All Owners") { selectedOwner = nil }
                        Divider()
                        ForEach(Game.Owner.allCases) { owner in
                            Button(owner.rawValue) { selectedOwner = owner }
                        }
                    } label: {
                        Label(selectedOwner?.rawValue ?? "All Owners", systemImage: "person")
                    }
                }
            }
        }
        .padding(.bottom, 20)
        .padding(.leading, 20)
    }

    struct AdaptiveStack < Content: View > : View {
        @Environment(\.horizontalSizeClass) private var hSize
        @Environment(\.verticalSizeClass) private var vSize

        let hSpacing: CGFloat
        let vSpacing: CGFloat
        let alignment: HorizontalAlignment
        @ViewBuilder var content: () -> Content

        private var isIPhonePortrait: Bool {
            UIDevice.current.userInterfaceIdiom == .phone && hSize == .compact && vSize == .regular
        }

        var body: some View {
            Group {
                if isIPhonePortrait {
                    VStack(alignment: alignment, spacing: vSpacing) {
                        content()
                    }
                } else {
                    HStack(spacing: hSpacing) {
                        content()
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView()
}
    
SwiftUI Table on iOS

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners
Last Change: Fri Nov 28 08:18:54 2025 GMT