2

I've run in to an odd problem with NavigationView on macCatalyst. Here below is a simple app with a sidebar and a detail view. Selecting an item on the sidebar shows a detail view with a scrollable list.

Everything works fine for the first NavigationLink, the detail view displays and is freely scrollable. However, if I select a list item which triggers a link to a second detail view, scrolling starts, then freezes. The app still works, only the detail view scrolling is locked up.

The same code works fine on an iPad without any freeze. If I build for macOS, the NavigationLink in the detail view is non-functional.

Are there any known workarounds ?

This is what it looks like, after clicking on LinkedView, a short scroll then the view freezes. It is still possible to click on the back button or another item on the sidebar, but the list view is blocked.

enter image description here

Here is the code: ContentView.swift

import SwiftUI

struct ContentView: View {

    var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]

    var body: some View {
        NavigationView {
            List() {
                ForEach(names.sorted(by: {$0.name < $1.name})) { item in
                    NavigationLink(destination: DetailListView(item: item)) {
                        Text(item.name)
                    }
                }
            }
            .listStyle(SidebarListStyle())

            Text("Detail view")
        }
    }
}

struct NamedItem: Identifiable {
    let name: String
    let id = UUID()
}

struct DetailListView: View {

    var item: NamedItem

    let sections = (0...4).map({NamedItem(name: "\($0)")})

    var body: some View {
        VStack {
            List {
                Text(item.name)
                NavigationLink(destination: DetailListView(item: NamedItem(name: "LinkedView"))) {
                    listItem("  LinkedView", "Item")
                        .foregroundColor(Color.blue)
                }

                ForEach(sections) { section in
                    sectionDetails(section)
                }
            }
        }
    }

    let info = (0...12).map({NamedItem(name: "\($0)")})

    func sectionDetails(_ section: NamedItem) -> some View {
        Section(header: Text("Section \(section.name)")) {
            Group {
                listItem("ID", "\(section.id)")
            }
            Text("")
            ForEach(info) { ch in
                listItem("Item \(ch.name)", "\(ch.id)")
            }
        }
    }

    func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
        HStack {
            Text(title)
                .frame(width: 200, alignment: .leading)
            Text(value)
                .padding(.leading, 10)
        }
    }

}

TestListApp.swift

import SwiftUI

@main
struct TestListApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Guy Brooker
  • 726
  • 7
  • 11

3 Answers3

2

I had this very same problem with Mac Catalyst app. On real device (iPhone 7 with iOS 14.4.2) there was no problem but with Mac Catalyst (MacBook Pro with Big Sur 11.2.3) the scrolling in the navigation view stuck very randomly as you explained. I figured out that the issue was with Macbook's trackpad and was related to scroll indicators because with external mouse the issue was absent. So the easiest solution to this problem is to hide vertical scroll indicators in navigation view. At least it worked for me. Below is some code from root view 'ContentView' how I did it. It's unfortunate to lose scroll indicators with big data but at least the scrolling works.

import SwiftUI

struct TestView: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: NewView()) {
                    Text("Navigation Link to new view")
                }
            }
            .onAppear {
                UITableView.appearance().showsVerticalScrollIndicator = false
            }   
        }
    }
}
Jyri
  • 51
  • 5
  • Well maybe it was too early to celebrate, the problem still occurs. But at least scrolling hasn't freezed using external mouse so there's surely something going on with the trackpad. Maybe something with multidirectional scrolling or something? – Jyri Apr 09 '21 at 06:12
  • didn't fix for me, and I am using Magic Mouse. Did anyone find a solution? – Mane Manero May 12 '21 at 12:13
  • Sorry, no, still having the same problem. The only workaround at the moment is to use external mouse with a scroll wheel. – Jyri Jun 19 '21 at 06:28
0

OK, so I managed to find a workaround, so thought I'd post this for help, until what seems to be a macCatalyst SwiftUI bug is fixed. I have posted a radar for the list freeze problem: FB8994665

The workaround is to use NavigationLink only to the first level of the series of pages which can be navigated (which gives me the sidebar and a toolbar), and from that point onwards use the NavigationStack package to mange links to other pages.

I ran in to a couple of other gotcha's with this arrangement.

Firstly the NavigationView toolbar loses its background when scrolling linked list views (unless the window is defocussed and refocussed), which seems to be another catalyst SwiftUI bug. I solved that by setting the toolbar background colour.

Second gotcha was that under macCatalyst the onTouch view modifier used in NavigationStack's PushView label did not work for most single clicks. It would only trigger consistently for double clicks. I fixed that by using a button to replace the label.

Here is the code, no more list freezes !

import SwiftUI
import NavigationStack

struct ContentView: View {

    var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]

    @State private var isSelected: UUID? = nil

    init() {
        // Ensure toolbar is allways opaque
        UINavigationBar.appearance().backgroundColor = UIColor.secondarySystemBackground
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(names.sorted(by: {$0.name < $1.name})) { item in
                    NavigationLink(destination: DetailStackView(item: item)) {
                        Text(item.name)
                    }
                }
            }
            .listStyle(SidebarListStyle())

            Text("Detail view")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .toolbar { Spacer() }
        }
    }
}

struct NamedItem: Identifiable {
    let name: String
    let id = UUID()
}

// Embed the list view in a NavigationStackView
struct DetailStackView: View {
    var item: NamedItem

    var body: some View {
        NavigationStackView {
            DetailListView(item: item)
        }
    }
}

struct DetailListView: View {

    var item: NamedItem

    let sections = (0...10).map({NamedItem(name: "\($0)")})

    var linked = NamedItem(name: "LinkedView")

    // Use a Navigation Stack instead of a NavigationLink
    @State private var isSelected: UUID? = nil
    @EnvironmentObject private var navigationStack: NavigationStack

    var body: some View {
        List {
            Text(item.name)
            PushView(destination: linkedDetailView,
                     tag: linked.id, selection: $isSelected) {
                listLinkedItem("  LinkedView", "Item")
            }

            ForEach(sections) { section in
                if section.name != "0" {
                    sectionDetails(section)
                }
            }
        }
        .navigationBarTitleDisplayMode(.inline)
        .navigationTitle(item.name)
    }

    // Ensure that the linked view has a toolbar button to return to this view
    var linkedDetailView: some View {
        DetailListView(item: linked)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button(action: {
                        self.navigationStack.pop()
                    }, label: {
                        Image(systemName: "chevron.left")
                    })
                }
            }
    }

    let info = (0...12).map({NamedItem(name: "\($0)")})

    func sectionDetails(_ section: NamedItem) -> some View {
        Section(header: Text("Section \(section.name)")) {
            Group {
                listItem("ID", "\(section.id)")
            }
            Text("")
            ForEach(info) { ch in
                listItem("Item \(ch.name)", "\(ch.id)")
            }
        }
    }

    // Use a button to select the linked view with a single click
    func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
        HStack {
            Button(title, action: {
                self.isSelected = linked.id
            })
            .foregroundColor(Color.blue)
            Text(value)
                .padding(.leading, 10)
        }
    }

    func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
        HStack {
            Text(title)
                .frame(width: 200, alignment: .leading)
            Text(value)
                .padding(.leading, 10)
        }
    }

}
Guy Brooker
  • 726
  • 7
  • 11
0

I have continued to experiment with NavigationStack and have made some modifications which will allow it to swap in and out List rows directly. This avoids the problems I was seeing with the NavigationBar background. The navigation bar is setup at the level above the NavigationStackView and changes to the title are passed via a PreferenceKey. The back button on the navigation bar hides if the stack is empty.

The following code makes use of PR#44 of swiftui-navigation-stack

import SwiftUI

struct ContentView: View {

    var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]

    @State private var isSelected: UUID? = nil

    var body: some View {
        NavigationView {
            List {
                ForEach(names.sorted(by: {$0.name < $1.name})) { item in
                    NavigationLink(destination: DetailStackView(item: item)) {
                        Text(item.name)
                    }
                }
            }
            .listStyle(SidebarListStyle())

            Text("Detail view")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .toolbar { Spacer() }
        }
    }
}

struct NamedItem: Identifiable {
    let name: String
    let depth: Int
    let id = UUID()
    init(name:String, depth: Int = 0) {
        self.name = name
        self.depth = depth
    }
    var linked: NamedItem {
        return NamedItem(name: "Linked \(depth+1)", depth:depth+1)
    }
}

// Preference Key to send title back down to DetailStackView
struct ListTitleKey: PreferenceKey {
    static var defaultValue: String = ""

    static func reduce(value: inout String, nextValue: () -> String) {
        value = nextValue()
    }
}

extension View {
    func listTitle(_ title: String) -> some View {
        self.preference(key: ListTitleKey.self, value: title)
    }
}

// Embed the list view in a NavigationStackView
struct DetailStackView: View {
    var item: NamedItem

    @ObservedObject var navigationStack = NavigationStack()

    @State var toolbarTitle: String = ""

    var body: some View {
        List {
            NavigationStackView(noGroup: true, navigationStack: navigationStack) {
                DetailListView(item: item, linked: item.linked)
                    .listTitle(item.name)
            }
        }
        .listStyle(PlainListStyle())
        .animation(nil)

        // Updated title
        .onPreferenceChange(ListTitleKey.self) { value in
            toolbarTitle = value
        }
        .navigationBarTitleDisplayMode(.inline)
        .navigationTitle("\(toolbarTitle) \(self.navigationStack.depth)")
        .toolbar(content: {
            ToolbarItem(id: "BackB", placement: .navigationBarLeading, showsByDefault: self.navigationStack.depth > 0) {
                Button(action: {
                    self.navigationStack.pop()
                }, label: {
                    Image(systemName: "chevron.left")
                })
                .opacity(self.navigationStack.depth > 0  ? 1.0 : 0.0)
            }
        })
    }
}

struct DetailListView: View {

    var item: NamedItem
    var linked: NamedItem

    let sections = (0...10).map({NamedItem(name: "\($0)")})

    // Use a Navigation Stack instead of a NavigationLink
    @State private var isSelected: UUID? = nil
    @EnvironmentObject private var navigationStack: NavigationStack

    var body: some View {
        Text(item.name)
        PushView(destination: linkedDetailView,
                 tag: linked.id, selection: $isSelected) {
            listLinkedItem("  LinkedView", "Item")
        }

        ForEach(sections) { section in
            if section.name != "0" {
                sectionDetails(section)
            }
        }
    }

    // Ensure that the linked view has a toolbar button to return to this view
    var linkedDetailView: some View {
        DetailListView(item: linked, linked: linked.linked)
            .listTitle(linked.name)
    }

    let info = (0...12).map({NamedItem(name: "\($0)")})

    func sectionDetails(_ section: NamedItem) -> some View {
        Section(header: Text("Section \(section.name)")) {
            Group {
                listItem("ID", "\(section.id)")
            }
            Text("")
            ForEach(info) { ch in
                listItem("Item \(ch.name)", "\(ch.id)")
            }
        }
    }

    func buttonAction() {
        self.isSelected = linked.id
    }

    // Use a button to select the linked view with a single click
    func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
        HStack {
            Button(title, action: buttonAction)
                .foregroundColor(Color.blue)
            Text(value)
                .padding(.leading, 10)
        }
    }

    func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
        HStack {
            Text(title)
                .frame(width: 200, alignment: .leading)
            Text(value)
                .padding(.leading, 10)
        }
    }
}
Guy Brooker
  • 726
  • 7
  • 11