~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
74 lines · 3.5 KB Raw History
import SwiftUI

/// Somewhere in the app you can push to.
///
/// Screens used to keep one `@State` optional per destination and one
/// `navigationDestination(item:)` per optional — fifteen and seventeen of them
/// respectively on the feed. That represents far more states than are legal
/// (nothing stopped two being non-nil at once) and it puts every destination's
/// body inside a closure that only runs once somebody taps, which is most of
/// what the test suite can't reach.
///
/// A route is a value instead. The stack holds an array of them, so where the
/// user is is something you can read, assert on and restore.
///
/// Cases carry identifiers only — never a model object, a closure or a binding.
/// A route outlives the data it points at: it survives relaunch, and the record
/// it names may have changed by the time it's popped back to.
enum Route: Hashable, Codable {
    case gallery(uri: String)
    case profile(did: String)
    case hashtag(String)
    /// `name` is the H3 cell's display name, carried because it titles the
    /// screen and isn't derivable from the index without a lookup.
    case location(h3Index: String, name: String)
    case galleryFavorites(uri: String)
    /// Every gallery shot on one camera, by the name the appview normalised.
    case camera(String)
    /// The cameras index — every camera the explore chips only show the top of.
    case cameras
    /// The locations index: every place on a map, with a sheet over it. With
    /// a `cell` the sheet opens on that place's galleries, the way a chip on
    /// explore wants it; without one it opens on the index. `galleryCount`
    /// rides along for the heading, since the map screen only knows counts
    /// for the places it has pins for.
    case locations(cell: String? = nil, name: String? = nil, galleryCount: Int = 0)
}

/// The view a `Route` names.
///
/// Registered once per `NavigationStack` with `navigationDestination(for:)`,
/// which is the arrangement Apple's migration guide prescribes. Being a plain
/// `Route -> View` function, it can be exercised directly rather than through a
/// tap.
struct RouteDestination: View {
    let route: Route
    let client: XRPCClient

    /// Set by the gallery detail screen when it deletes what it was showing, so
    /// the list underneath can drop the row. It lives on the builder rather than
    /// in the `Route` because a route has to stay a plain value — carrying a
    /// binding would make it uncodable and tie it to one particular caller.
    var deletedGalleryUri: Binding<String?> = .constant(nil)

    var body: some View {
        switch route {
        case let .gallery(uri):
            GalleryDetailView(client: client, galleryUri: uri, deletedGalleryUri: deletedGalleryUri)
        case let .profile(did):
            ProfileView(client: client, did: did)
        case let .hashtag(tag):
            HashtagFeedView(client: client, tag: tag)
        case let .location(h3Index, name):
            LocationFeedView(client: client, h3Index: h3Index, locationName: name)
        case let .galleryFavorites(uri):
            FollowListView(client: client, mode: .galleryFavorites(uri))
        case let .camera(name):
            CameraFeedView(client: client, camera: name)
        case .cameras:
            CamerasView(client: client)
        case let .locations(cell, name, galleryCount):
            LocationsView(client: client, initialPlace: LocationsView.InitialPlace(cell: cell, name: name, galleryCount: galleryCount))
        }
    }
}