Search code examples
iosswiftswiftui

How to open specific View in SwiftUI app using AppIntents


I'm very new to Intents in Swift. Using the Dive Into App Intents video from WWDC 22 and the Booky example app, I've gotten my app to show up in the Shortcuts app and show an initial shortcut which opens the app to the main view. Here is the AppIntents code:

import AppIntents

enum NavigationType: String, AppEnum, CaseDisplayRepresentable {
    case folders
    case cards
    case favorites

    // This will be displayed as the title of the menu shown when picking from the options
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Navigation")
    
    static var caseDisplayRepresentations: [Self:DisplayRepresentation] = [
        .folders: DisplayRepresentation(title: "Folders"),
        .cards: DisplayRepresentation(title: "Card Gallery"),
        .favorites: DisplayRepresentation(title: "Favorites")
    ]
}

struct OpenCards: AppIntent {
    
    // Title of the action in the Shortcuts app
    static var title: LocalizedStringResource = "Open Card Gallery"
    // Description of the action in the Shortcuts app
    static var description: IntentDescription = IntentDescription("This action will open the Card gallery in the Hello There app.", categoryName: "Navigation")
    // This opens the host app when the action is run
    static var openAppWhenRun = true
    
    @Parameter(title: "Navigation")
    var navigation: NavigationType

    @MainActor // <-- include if the code needs to be run on the main thread
    func perform() async throws -> some IntentResult {
                ViewModel.shared.navigateToGallery()
            return .result()
    }
    
    static var parameterSummary: some ParameterSummary {
        Summary("Open \(\.$navigation)")
    }
    
}

And here is the ViewModel:

import SwiftUI

class ViewModel: ObservableObject {
    
    static let shared = ViewModel()
    
    @Published var path: any View = FavoritesView()
    
    // Clears the navigation stack and returns home
    func navigateToGallery() {
        path = FavoritesView()
    }
}

Right now, the Shortcut lets you select one of the enums (Folders, Cards, and Favorites), but always launches to the root of the app. Essentially no different then just telling Siri to open my app. My app uses a TabView in its ContentView with TabItems for the related Views:

            .tabItem {
                Text("Folders")
                Image(systemName: "folder.fill")
            }
            NavigationView {
                GalleryView()
            }
            .tabItem {
                Text("Cards")
                Image(systemName: "rectangle.portrait.on.rectangle.portrait.angled.fill")
            }
            NavigationView {
                FavoritesView()
            }
            .tabItem {
                Text("Favs")
                Image(systemName: "star.square.on.square.fill")
            }
            NavigationView {
                SettingsView()
            }
            .tabItem {
                Text("Settings")
                Image(systemName: "gear")
            }

How can I configure the AppIntents above to include something like "Open Favorites View" and have it launch into that TabItem view? I think the ViewModel needs tweaking... I've tried to configure it to open the FavoritesView() by default, but I'm lost on the proper path forward.

Thanks!

[EDIT -- updated with current code]


Solution

  • You're on the right track, you just need some way to do programmatic navigation.

    With TabView, you can do that by passing a selection argument, a binding that you can then update to select a tab. An enum of all your tabs works nicely here. Here's an example view:

    struct SelectableTabView: View {
        enum Tabs {
            case first, second
        }
        
        @State var selected = Tabs.first
        
        var body: some View {
            // selected will track the current tab:
            TabView(selection: $selected) {
                Text("First tab content")
                    .tabItem {
                        Image(systemName: "1.circle.fill")
                    }
                    // The tag is how TabView knows which tab is which:
                    .tag(Tabs.first)
    
                VStack {
                    Text("Second tab content")
                    
                    Button("Select first tab") {
                        // You can change selected to navigate to a different tab:
                        selected = .first
                    }
                }
                .tabItem {
                    Image(systemName: "2.circle.fill")
                }
                .tag(Tabs.second)
            }
        }
    }
    

    So in your code, ViewModel.path could be an enum representing the available tabs, and you could pass a binding to path ($viewModel.path) to your TabView. Then you could simply set path = .favorites to navigate.