Food Truck Sample App

repository·main·Indexed 23 days ago

https://github.com/apple/sample-food-truck

A SwiftUI multiplatform sample app for macOS, iPadOS, and iOS demonstrating advanced features including NavigationSplitView, the Layout protocol, Charts, WeatherKit integration, and Live Activities with Dynamic Island support.

Tokens
4.5K
Snippets
8
Records
10
Agent score
34%

What's inside sample-food-truck

  1. How the app manages navigation with NavigationSplitView

    main

    The app uses a NavigationSplitView combined with a NavigationStack to manage its multi-column interface. The Sidebar view controls the selection, which is then used to drive the detail view.

    To set a default view (like TruckView) at launch, use a @State variable to encode the selection using the Panel enum, setting it to .truck by default.

    NavigationSplitView {
        Sidebar(selection: $selection)
    } detail: {
        NavigationStack(path: $path) {
            DetailColumn(selection: $selection, model: model)
        }
    }
    
    // Default selection
    @State private var selection: Panel? = Panel.truck
  2. Construct a dynamic layout using the Layout protocol

    main

    The app implements a custom DiagonalDonutStackLayout using the SwiftUI Layout protocol. This is used in the Truck view to arrange donut thumbnails in a diagonal pattern. The layout logic is defined by overriding placeSubviews(in:proposal:subviews:cache:) to calculate specific CGPoint offsets and proposed sizes for each subview based on its index.

    for index in subviews.indices {
        switch (index, subviews.count) {
        case (_, 1):
            subviews[index].place(
                at: center,
                anchor: .center,
                proposal: ProposedViewSize(size)
            )
            
        case (_, 2):
            let direction = index == 0 ? -1.0 : 1.0
            let offsetX = minBound * direction * 0.15
            let offsetY = minBound * direction * 0.20
            subviews[index].place(
                at: CGPoint(x: center.x + offsetX, y: center.y + offsetY),
                anchor: .center,
                proposal: ProposedViewSize(CGSize(width: size.width * 0.7, height: size.height * 0.7))
            )
        case (1, 3):
            subviews[index].place(
                at: center,
                anchor: .center,
                proposal: ProposedViewSize(CGSize(width: size.width * 0.65, height: size.height * 0.65))
            )
            
        case (_, 3):
            let direction = index == 0 ? -1.0 : 1.0
            let offsetX = minBound * direction * 0.15
            let offsetY = minBound * direction * 0.23
            subviews[index].place(
                at: CGPoint(x: center.x + offsetX, y: center.y + offsetY),
                anchor: .center,
                proposal: ProposedViewSize(CGSize(width: size.width * 0.7, height: size.height * 0.65))
            )
        }
    }
  3. Configure the Food Truck sample project

    main

    The project includes two app targets with different setup requirements:

    Simple app target

    Use this target to build using a Personal Team (standard Apple ID). It runs in the Simulator and on devices via manual trust.

    1. In the Food Truck target's Signing & Capabilities pane, click Add Account and log in with your Apple ID.
    2. Select your name (Personal Team) from the team menu for both the Food Truck and Widgets targets.
    3. Build and run.
    4. On iOS/iPadOS devices, go to Settings > General > VPN & Device Management to trust your developer certificate.

    Requires an Apple Developer membership to support passkeys and full features.

    1. Open the project with Xcode 14.3 or later.
    2. Select the top-level Food Truck project.
    3. For all targets, select your team in the Signing & Capabilities pane.
    4. Add the Associated Domains capability and specify your domain using the webcredentials service.
    5. Ensure an apple-app-site-association (AASA) file is present in your domain's .well-known directory, containing an entry for this app's App ID for the webcredentials service.
    6. In AccountManager.swift, replace all occurrences of example.com with your actual domain name.
  4. Track preparation time with Live Activity

    main

    The app uses ActivityKit to track order preparation time (guaranteed to be 60 seconds or less). For orders with a placed status, a toolbar button transitions the status to preparing and starts a Live Activity. This displays a countdown timer and order details on the iPhone lock screen using an Activity instance with TruckActivityAttributes.

    let timerSeconds = 60
    let activityAttributes = TruckActivityAttributes(
        orderID: String(order.id.dropFirst(6)),
        order: order.donuts.map(\.id),
        sales: order.sales,
        activityName: "Order preparation activity."
    )
    
    let future = Date(timeIntervalSinceNow: Double(timerSeconds))
    
    let initialContentState = TruckActivityAttributes.ContentState(timerRange: Date.now...future)
    
    let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!)
    
    do {
        let myActivity = try Activity<TruckActivityAttributes>.request(attributes: activityAttributes, content: activityContent, 
            pushType: nil)
        print(" Requested MyActivity live activity. ID: \(myActivity.id)")
        postNotification()
    } catch let error {
        print("Error requesting live activity: \(error.localizedDescription)")
    }
  5. Configure the project for WeatherKit

    main

    To use live weather data instead of static fallback data in the Food Truck All target, follow these steps:

    1. Create a unique App ID on the Provisioning Portal and select the WeatherKit service on the App Services tab.
    2. In Xcode, for the Food Truck All target, set the Bundle ID to match your new App ID and add the WeatherKit capability in the Signing & Capabilities tab.
    3. For the Widgets target, set the Bundle ID such that the prefix (before .Widgets) matches the Food Truck All target's bundle ID.
    4. Wait approximately 30 minutes for the service to register your bundle ID.
    5. Build and run the Food Truck All target.
  6. Implement Dynamic Island for Live Activities

    main

    To support iPhone 14 Pro and later, the app implements DynamicIsland to mirror the lock screen information. The implementation defines three distinct states: expanded, compactLeading, compactTrailing, and minimal. It uses DynamicIslandExpandedRegion for the expanded view and provides a widgetURL to allow users to navigate back to the specific order via the foodtruck://order/{orderID} scheme.

    DynamicIsland {
        DynamicIslandExpandedRegion(.leading) {
            ExpandedLeadingView()
        }
    
        DynamicIslandExpandedRegion(.trailing, priority: 1) {
            ExpandedTrailingView(orderNumber: context.attributes.orderID, timerInterval: context.state.timerRange)
                .dynamicIsland(verticalPlacement: .belowIfTooWide)
        }
    } compactLeading: {
        Image("IslandCompactIcon")
            .padding(4)
            .background(.indigo.gradient, in: ContainerRelativeShape())
           
    } compactTrailing: {
        Text(timerInterval: context.state.timerRange, countsDown: true)
            .monospacedDigit()
            .foregroundColor(Color("LightIndigo"))
            .frame(width: 40)
    } minimal: {
        Image("IslandCompactIcon")
            .padding(4)
            .background(.indigo.gradient, in: ContainerRelativeShape())
    }
    .contentMargins(.trailing, 32, for: .expanded)
    .contentMargins([.leading, .top, .bottom], 6, for: .compactLeading)
    .contentMargins(.all, 6, for: .minimal)
    .widgetURL(URL(string: "foodtruck://order/\(context.attributes.orderID)"))
  7. Display a bar chart of popular items using Charts

    main

    The TopFiveDonutsView uses the Charts framework to display sales trends. It utilizes BarMark to create bars, applying a linearGradient for styling and an .annotation to display formatted sales numbers above each bar. The X-axis is customized using .chartXAxis and AxisMarks to show both the item name and a thumbnail via a DonutView within a VStack.

    Chart {
        ForEach(sortedSales) { sale in
            BarMark(
                x: .value("Donut", sale.donut.name),
                y: .value("Sales", sale.sales)
            )
            .cornerRadius(6, style: .continuous)
            .foregroundStyle(.linearGradient(colors: [Color("BarBottomColor"), .accentColor], startPoint: .bottom, endPoint: .top))
            .annotation(position: .top, alignment: .top) {
                Text(sale.sales.formatted())
                    .padding(.vertical, 4)
                    .padding(.horizontal, 8)
                    .background(.quaternary.opacity(0.5), in: Capsule())
                    .background(in: Capsule())
                    .font(.caption)
            }
        }
    }
    .chartXAxis {
        AxisMarks {
            AxisValueLabel {
                let donut = donutFromAxisValue(for: value)
                VStack {
                    DonutView(donut: donut)
                        .frame(height: 35)
                        
                    Text(donut.name)
                        .lineLimit(2, reservesSpace: true)
                        .multilineTextAlignment(.center)
                }
                .frame(idealWidth: 80)
                .padding(.horizontal, 4)
                
            }
        }
    }
  8. End a Live Activity

    main

    When an order status changes to complete, the associated Live Activity must be ended to remove it from the lock screen and Dynamic Island. This is achieved by iterating through active Activity<TruckActivityAttributes> instances, matching the orderID, and calling end(nil, dismissalPolicy: .immediate).

    Task {
        for activity in Activity<TruckActivityAttributes>.activities {
            // Check if this is the activity associated with this order.
            if activity.attributes.orderID == String(order.id.dropFirst(6)) {
                await activity.end(nil, dismissalPolicy: .immediate)
            }
        }
    }
  9. Obtain a weather forecast using WeatherKit

    main

    The app fetches weather data using WeatherService.shared.weather(for:). This is typically performed within a .task modifier. The implementation iterates through available parking spots to find a suitable location, then extracts properties like currentWeather.condition, currentWeather.temperature, and currentWeather.symbolName. It also retrieves legal attribution via WeatherService.shared.attribution to display the required legal links and logos.

    .task(id: city.id) {
        for parkingSpot in city.parkingSpots {
            do {
                let weather = try await WeatherService.shared.weather(for: parkingSpot.location)
                condition = weather.currentWeather.condition
                willRainSoon = weather.minuteForecast?.contains(where: { $0.precipitationChance >= 0.3 })
                cloudCover = weather.currentWeather.cloudCover
                temperature = weather.currentWeather.temperature
                symbolName = weather.currentWeather.symbolName
                
                let attribution = try await WeatherService.shared.attribution
                attributionLink = attribution.legalPageURL
                attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
                
                if willRainSoon == false {
                    spot = parkingSpot
                    break
                }
            } catch {
                print("Could not gather weather information...", error.localizedDescription)
                condition = .clear
                willRainSoon = false
                cloudCover = 0.15
            }
        }
    }
  10. Initialize the FoodTruckApp entry point

    main

    The FoodTruckApp struct serves as the main entry point for the application on both iOS and macOS. It manages the application's core state using @StateObject for the FoodTruckModel and AccountStore.

    When building upon this structure, note that the model and accountStore are injected into the root ContentView to provide data and account management capabilities throughout the app hierarchy.

    @main
    struct FoodTruckApp: App {
        @StateObject private var model = FoodTruckModel()
        @StateObject private var accountStore = AccountStore()
    
        var body: some Scene {
            WindowGroup {
                ContentView(model: model, accountStore: accountStore)
            }
            // ...
        }
    }