mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 07:04:35 +00:00
Stop macOS crashes while opening and stepping through exercises.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
eae70af2b2
commit
c561c0acb5
3 changed files with 42 additions and 19 deletions
|
|
@ -10,7 +10,9 @@ final class AppModel: ObservableObject {
|
|||
@Published var mode: ColorMode
|
||||
@Published var lang: UiLang
|
||||
@Published var tab: AppTab = .practice
|
||||
@Published var practicePath: [Route] = []
|
||||
@Published var practicePath = NavigationPath()
|
||||
/// Bumps whenever a full path replace is scheduled, so a queued push cannot land afterwards.
|
||||
private var pathGeneration = 0
|
||||
@Published var searchQuery = ""
|
||||
@Published var tick = 0
|
||||
@Published var update = UpdateState()
|
||||
|
|
@ -48,13 +50,17 @@ final class AppModel: ObservableObject {
|
|||
case .stats:
|
||||
tab = .progress
|
||||
case .welcome, .subjects:
|
||||
tab = .practice
|
||||
practicePath = []
|
||||
showPracticeRoot()
|
||||
default:
|
||||
applyPracticeRoute(r, replace: practicePath.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func showPracticeRoot() {
|
||||
tab = .practice
|
||||
schedulePracticePath(NavigationPath())
|
||||
}
|
||||
|
||||
func goPage(_ page: String?) {
|
||||
guard let page, let r = routeFromPage(page) else { return }
|
||||
go(r)
|
||||
|
|
@ -66,8 +72,7 @@ final class AppModel: ObservableObject {
|
|||
case .stats:
|
||||
tab = .progress
|
||||
case .welcome, .subjects:
|
||||
tab = .practice
|
||||
practicePath = []
|
||||
showPracticeRoot()
|
||||
default:
|
||||
// Switching tabs and assigning a deep NavigationStack path in the
|
||||
// same turn crashes SwiftUI on macOS (boundPathChange / unexpectedError).
|
||||
|
|
@ -76,18 +81,31 @@ final class AppModel: ObservableObject {
|
|||
}
|
||||
|
||||
/// Sets the practice tab path safely, especially when jumping from Search.
|
||||
///
|
||||
/// The sidebar-style tab view keeps one navigation column for every tab.
|
||||
/// A typed `[Route]` path does not match the other tabs, so the column
|
||||
/// traps in `boundPathChange` while an exercise is being opened. The path
|
||||
/// is therefore type-erased, and a push is applied on the next turn instead
|
||||
/// of replacing the whole stack during the click.
|
||||
private func applyPracticeRoute(_ r: Route, replace: Bool) {
|
||||
let stack = replace ? r.stack : (practicePath + [r])
|
||||
if !replace, practicePath.last == r { return }
|
||||
let switching = tab != .practice
|
||||
tab = .practice
|
||||
if switching || replace {
|
||||
practicePath = []
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.practicePath = stack
|
||||
}
|
||||
schedulePracticePath(NavigationPath(r.stack))
|
||||
} else {
|
||||
practicePath = stack
|
||||
let generation = pathGeneration
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, self.pathGeneration == generation else { return }
|
||||
self.practicePath.append(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func schedulePracticePath(_ path: NavigationPath) {
|
||||
pathGeneration += 1
|
||||
practicePath = NavigationPath()
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.practicePath = path
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ struct GraduatelyApp: App {
|
|||
|
||||
struct NativeRoot: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
@State private var progressPath = NavigationPath()
|
||||
@State private var settingsPath = NavigationPath()
|
||||
@State private var searchPath = NavigationPath()
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
|
|
@ -49,17 +52,17 @@ struct NativeRoot: View {
|
|||
}
|
||||
}
|
||||
Tab(vm.tr("stats"), systemImage: "chart.bar.fill", value: .progress) {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $progressPath) {
|
||||
StatsScreen(vm: vm)
|
||||
}
|
||||
}
|
||||
Tab(vm.tr("settings"), systemImage: "gearshape.fill", value: .settings) {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $settingsPath) {
|
||||
SettingsScreen(vm: vm)
|
||||
}
|
||||
}
|
||||
Tab(vm.tr("search"), systemImage: "magnifyingglass", value: .search) {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $searchPath) {
|
||||
SearchScreen(vm: vm)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ struct GraduatelyApp: App {
|
|||
CommandGroup(replacing: .newItem) {}
|
||||
CommandGroup(after: .sidebar) {
|
||||
Button(vm.tr("search_home")) {
|
||||
vm.tab = .practice
|
||||
vm.practicePath = []
|
||||
vm.showPracticeRoot()
|
||||
}
|
||||
.keyboardShortcut("1", modifiers: [.command])
|
||||
Button(vm.tr("stats")) {
|
||||
|
|
@ -47,6 +46,9 @@ struct GraduatelyApp: App {
|
|||
|
||||
struct MacRoot: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
@State private var progressPath = NavigationPath()
|
||||
@State private var settingsPath = NavigationPath()
|
||||
@State private var searchPath = NavigationPath()
|
||||
|
||||
var body: some View {
|
||||
tabs
|
||||
|
|
@ -77,17 +79,17 @@ struct MacRoot: View {
|
|||
}
|
||||
}
|
||||
Tab(vm.tr("stats"), systemImage: "chart.bar.fill", value: AppTab.progress) {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $progressPath) {
|
||||
StatsScreen(vm: vm)
|
||||
}
|
||||
}
|
||||
Tab(vm.tr("settings"), systemImage: "gearshape.fill", value: AppTab.settings) {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $settingsPath) {
|
||||
SettingsScreen(vm: vm)
|
||||
}
|
||||
}
|
||||
Tab(vm.tr("search"), systemImage: "magnifyingglass", value: AppTab.search, role: .search) {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $searchPath) {
|
||||
SearchScreen(vm: vm)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue