feat: home screen quick actions (long press app icon)

Adds four UIApplicationShortcutItems registered dynamically at launch:
  • Add Task — opens the add-task sheet immediately, any tab
  • Today — switches to the Today tab
  • Calendar — switches to the Calendar tab
  • Workout — switches to the Workout tab (only registered if the
    workout tab is enabled in settings; re-registers when the setting changes)

Cold-launch and foreground-activation paths both handled via AppDelegate.
ShortcutHandler singleton bridges UIKit callbacks to SwiftUI via @Published.
This commit is contained in:
kutesir
2026-05-30 02:38:15 +03:00
parent e0e4f709b6
commit cab5c4ef05
4 changed files with 116 additions and 3 deletions

View File

@@ -9,9 +9,11 @@ struct ContentView: View {
@AppStorage("showMatrixTab") private var showMatrixTab = true
@AppStorage("showWorkoutTab") private var showWorkoutTab = true
@AppStorage("hasOnboarded") private var hasOnboarded = false
@State private var showOnboarding = false
@State private var selectedTab = 0
@StateObject private var tabState = FloatingTabState()
@State private var showOnboarding = false
@State private var selectedTab = 0
@State private var showQuickAddTask = false
@StateObject private var tabState = FloatingTabState()
@ObservedObject private var shortcuts = ShortcutHandler.shared
@State private var iPadSelection: Int? = 0
@State private var calIconImage: Image = ContentView.buildCalIcon()
@@ -92,6 +94,27 @@ struct ContentView: View {
.onChange(of: workoutVM.doneSets) { _ in
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
}
.onChange(of: shortcuts.pending) { action in
guard let action else { return }
switch action {
case .addTask:
showQuickAddTask = true
case .today:
withAnimation(KisaniSpring.micro) { selectedTab = 0 }
case .calendar:
withAnimation(KisaniSpring.micro) { selectedTab = 1 }
case .workout:
withAnimation(KisaniSpring.micro) { selectedTab = showWorkoutTab ? 3 : 0 }
}
shortcuts.consume()
}
.sheet(isPresented: $showQuickAddTask) {
AddTaskSheet(prefilledDate: Date())
.environmentObject(taskVM)
.presentationDetents([.height(150)])
.presentationDragIndicator(.visible)
}
.onChange(of: showWorkoutTab) { _ in ShortcutHandler.registerShortcuts() }
}
// MARK: - iPhone

View File

@@ -25,6 +25,22 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
NotificationManager.shared.requestPermission()
ShortcutHandler.registerShortcuts()
// Cold launch from a shortcut
if let item = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
ShortcutHandler.shared.handle(item.type)
return false // tells UIKit we handled it
}
return true
}
func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
ShortcutHandler.shared.handle(shortcutItem.type)
completionHandler(true)
}
}

View File

@@ -0,0 +1,70 @@
import UIKit
import SwiftUI
// MARK: - Quick action identifiers
enum QuickAction: String {
case addTask = "com.kutesir.kisanical.addTask"
case today = "com.kutesir.kisanical.today"
case calendar = "com.kutesir.kisanical.calendar"
case workout = "com.kutesir.kisanical.workout"
}
// MARK: - Singleton observable handler
final class ShortcutHandler: ObservableObject {
static let shared = ShortcutHandler()
@Published var pending: QuickAction?
private init() {}
func handle(_ type: String) {
guard let action = QuickAction(rawValue: type) else { return }
DispatchQueue.main.async { self.pending = action }
}
func consume() {
pending = nil
}
}
// MARK: - Shortcut registration
extension ShortcutHandler {
static func registerShortcuts() {
let ud = UserDefaults.standard
let showWorkout = ud.object(forKey: "showWorkoutTab") as? Bool ?? true
var items: [UIApplicationShortcutItem] = [
UIApplicationShortcutItem(
type: QuickAction.addTask.rawValue,
localizedTitle: "Add Task",
localizedSubtitle: "Create a new reminder",
icon: UIApplicationShortcutIcon(systemImageName: "plus.circle.fill")
),
UIApplicationShortcutItem(
type: QuickAction.today.rawValue,
localizedTitle: "Today",
localizedSubtitle: "Tasks & agenda",
icon: UIApplicationShortcutIcon(systemImageName: "checkmark.circle")
),
UIApplicationShortcutItem(
type: QuickAction.calendar.rawValue,
localizedTitle: "Calendar",
localizedSubtitle: "Month view",
icon: UIApplicationShortcutIcon(systemImageName: "calendar")
),
]
if showWorkout {
items.append(UIApplicationShortcutItem(
type: QuickAction.workout.rawValue,
localizedTitle: "Workout",
localizedSubtitle: "Today's session",
icon: UIApplicationShortcutIcon(systemImageName: "dumbbell.fill")
))
}
UIApplication.shared.shortcutItems = items
}
}