fix(layout): fix rotation crash and improve iPad split view

iPadLayout was rendering TodayView without workoutVM, crashing with
a fatal EnvironmentObject error on any device where hSizeClass goes
.regular (iPad, iPhone Plus/Max in landscape). Rewrote to use proper
NavigationSplitView with List(selection:) driving the detail column.

Also caches calendarIcon as a static func + @State instead of
recomputing UIGraphicsImageRenderer on every body evaluation.
This commit is contained in:
kutesir
2026-05-27 14:01:34 +03:00
parent f67429de0b
commit d7ec351dd4

View File

@@ -11,8 +11,10 @@ struct ContentView: View {
@AppStorage("hasOnboarded") private var hasOnboarded = false @AppStorage("hasOnboarded") private var hasOnboarded = false
@State private var showOnboarding = false @State private var showOnboarding = false
@State private var selectedTab = 0 @State private var selectedTab = 0
@State private var iPadSelection: Int? = 0
@State private var calIconImage: Image = ContentView.buildCalIcon()
private var calendarIcon: Image { private static func buildCalIcon() -> Image {
let day = Calendar.current.component(.day, from: Date()) let day = Calendar.current.component(.day, from: Date())
let size = CGSize(width: 26, height: 26) let size = CGSize(width: 26, height: 26)
let renderer = UIGraphicsImageRenderer(size: size) let renderer = UIGraphicsImageRenderer(size: size)
@@ -21,14 +23,12 @@ struct ContentView: View {
let lw: CGFloat = 1.5 let lw: CGFloat = 1.5
let cr: CGFloat = 4.5 let cr: CGFloat = 4.5
// Outer rounded rectangle body
let body = CGRect(x: lw / 2, y: 3, width: size.width - lw, height: size.height - 3 - lw / 2) let body = CGRect(x: lw / 2, y: 3, width: size.width - lw, height: size.height - 3 - lw / 2)
let bodyPath = UIBezierPath(roundedRect: body, cornerRadius: cr) let bodyPath = UIBezierPath(roundedRect: body, cornerRadius: cr)
color.setStroke() color.setStroke()
bodyPath.lineWidth = lw bodyPath.lineWidth = lw
bodyPath.stroke() bodyPath.stroke()
// Header divider line
let divY = body.minY + 7.5 let divY = body.minY + 7.5
let divPath = UIBezierPath() let divPath = UIBezierPath()
divPath.move(to: CGPoint(x: body.minX + lw / 2, y: divY)) divPath.move(to: CGPoint(x: body.minX + lw / 2, y: divY))
@@ -37,13 +37,11 @@ struct ContentView: View {
divPath.lineWidth = 1 divPath.lineWidth = 1
divPath.stroke() divPath.stroke()
// Ring bumps at top
color.setFill() color.setFill()
for rx in [size.width * 0.3, size.width * 0.7] { for rx in [size.width * 0.3, size.width * 0.7] {
UIBezierPath(roundedRect: CGRect(x: rx - 1.5, y: 0.5, width: 3, height: 5.5), cornerRadius: 1.5).fill() UIBezierPath(roundedRect: CGRect(x: rx - 1.5, y: 0.5, width: 3, height: 5.5), cornerRadius: 1.5).fill()
} }
// Date number centred in body below header
let s = "\(day)" let s = "\(day)"
let font = UIFont.systemFont(ofSize: floor(size.height * 0.38), weight: .bold) let font = UIFont.systemFont(ofSize: floor(size.height * 0.38), weight: .bold)
let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: color] let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: color]
@@ -133,7 +131,7 @@ struct ContentView: View {
KisaniTabBar( KisaniTabBar(
selection: $selectedTab, selection: $selectedTab,
calIcon: calendarIcon, calIcon: calIconImage,
showMatrix: showMatrixTab, showMatrix: showMatrixTab,
showWorkout: showWorkoutTab showWorkout: showWorkoutTab
) )
@@ -146,42 +144,27 @@ struct ContentView: View {
// MARK: - iPad // MARK: - iPad
@ViewBuilder @ViewBuilder
private func iPadLayout() -> some View { private func iPadLayout() -> some View {
NavigationSplitView(columnVisibility: .constant(.all)) { NavigationSplitView {
List { List(selection: $iPadSelection) {
iPadSidebarItem("Tasks", icon: "checkmark", tag: 0) Label("Tasks", systemImage: "checkmark").tag(0)
iPadSidebarItem("Calendar", icon: "calendar", tag: 1) Label("Calendar", systemImage: "calendar").tag(1)
if showMatrixTab { if showMatrixTab { Label("Matrix", systemImage: "square.grid.2x2").tag(2) }
iPadSidebarItem("Matrix", icon: "square.grid.2x2", tag: 2) if showWorkoutTab { Label("Workout", systemImage: "dumbbell").tag(3) }
} Label("Settings", systemImage: "gearshape").tag(4)
if showWorkoutTab {
iPadSidebarItem("Workout", icon: "dumbbell", tag: 3)
}
iPadSidebarItem("Settings", icon: "gearshape", tag: 4)
} }
.navigationTitle("Kisani Cal") .navigationTitle("Kisani Cal")
.listStyle(.sidebar) .listStyle(.sidebar)
} detail: { } detail: {
TodayView().environmentObject(taskVM) switch iPadSelection ?? 0 {
case 1: CalendarView().environmentObject(taskVM).environmentObject(workoutVM)
case 2: MatrixView().environmentObject(taskVM)
case 3: WorkoutView().environmentObject(workoutVM)
case 4: SettingsView().environmentObject(workoutVM)
default: TodayView().environmentObject(taskVM).environmentObject(workoutVM)
}
} }
.tint(AppColors.accent) .tint(AppColors.accent)
} }
private func iPadSidebarItem(_ label: String, icon: String, tag: Int) -> some View {
NavigationLink(destination: iPadDetail(tag: tag)) {
Label(label, systemImage: icon)
}
}
@ViewBuilder
private func iPadDetail(tag: Int) -> some View {
switch tag {
case 0: TodayView().environmentObject(taskVM).environmentObject(workoutVM)
case 1: CalendarView().environmentObject(taskVM).environmentObject(workoutVM)
case 2: MatrixView().environmentObject(taskVM)
case 3: WorkoutView().environmentObject(workoutVM)
default: SettingsView().environmentObject(workoutVM)
}
}
} }
// MARK: - Custom Tab Bar // MARK: - Custom Tab Bar