Skip to content

ras0q/vista

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Vista

Vista is a SwiftUI-first server-state management library inspired by TanStack Query.

It keeps asynchronous server state close to a View without requiring a dedicated ViewModel for loading, error, cache, and mutation state.

Requirements

  • iOS 17+
  • macOS 14+
  • Swift 5.9+

Installation

Add Vista to your Swift Package Manager dependencies.

dependencies: [
    .package(url: "https://github.com/ras0q/vista.git", branch: "main")
]

Then add the product to your target.

dependencies: [
    .product(name: "Vista", package: "vista")
]

Core idea

Declare async server state at the top of your SwiftUI view.

extension QueryKey {
    static func user(_ id: Int) -> Self {
        QueryKey("user:\(id)")
    }
}

struct UserScreen: View {
    @Query<User> private var user
    @Mutation<Void, Void> private var follow

    init(userID: Int) {
        _user = Query(.user(userID)) {
            try await UserAPI.fetchUser(id: userID)
        }
        _follow = Mutation { () in
            try await UserAPI.followUser(id: userID)
        }
    }

    var body: some View {
        // Render from user and follow directly.
        EmptyView()
    }
}

Vista does not fetch automatically. Your view decides when work starts.

Provide a QueryClient

Inject one QueryClient from your app root so all child views share the same cache.

import SwiftUI
import Vista

@main
struct DemoApp: App {
    private let queryClient = QueryClient()

    var body: some Scene {
        WindowGroup {
            NavigationStack {
                UserScreen(userID: 1)
            }
            .environment(\.queryClient, queryClient)
        }
    }
}

If you do not inject a client, Vista falls back to a default client and logs a warning.

Query example

This example shows explicit fetch timing, loading state, cached data, refresh, and error rendering.

import SwiftUI
import Vista

struct User: Decodable, Sendable {
    let id: Int
    let name: String
    let bio: String
}

extension QueryKey {
    static func user(_ id: Int) -> Self {
        QueryKey("user:\(id)")
    }
}

enum UserAPI {
    static func fetchUser(id: Int) async throws -> User {
        let url = URL(string: "https://example.com/users/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(User.self, from: data)
    }
}

struct UserScreen: View {
    @Query<User> private var user

    init(userID: Int) {
        _user = Query(
            .user(userID),
            options: QueryOptions(
                staleTime: .seconds(30),
                gcTime: .seconds(300)
            )
        ) {
            try await UserAPI.fetchUser(id: userID)
        }
    }

    var body: some View {
        content
            .navigationTitle("Profile")
            .onAppear {
                Task {
                    await user.fetchIfNeeded()
                }
            }
            .refreshable {
                await user.refetch()
            }
    }

    @ViewBuilder
    private var content: some View {
        if user.isLoading {
            ProgressView("Loading user...")
        } else if let data = user.data {
            VStack(alignment: .leading, spacing: 12) {
                Text(data.name)
                    .font(.title.bold())

                Text(data.bio)
                    .foregroundStyle(.secondary)

                if let error = user.error {
                    Text(error.localizedDescription)
                        .foregroundStyle(.red)
                }

                Button("Reload") {
                    Task {
                        await user.refetch()
                    }
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
        } else if let error = user.error {
            ContentUnavailableView(
                "Failed to load user",
                systemImage: "exclamationmark.triangle",
                description: Text(error.localizedDescription)
            )
        } else {
            ContentUnavailableView("No user", systemImage: "person.slash")
        }
    }
}

Mutation example

Mutations are local to the view property. They are explicit, and follow-up invalidation is explicit too.

import SwiftUI
import Vista

enum UserAPI {
    static func followUser(id: Int) async throws {
        var request = URLRequest(url: URL(string: "https://example.com/users/\(id)/follow")!)
        request.httpMethod = "POST"
        _ = try await URLSession.shared.data(for: request)
    }
}

struct UserScreen: View {
    @Query<User> private var user
    @Mutation<Void, Void> private var follow

    init(userID: Int) {
        _user = Query(.user(userID)) {
            try await UserAPI.fetchUser(id: userID)
        }
        _follow = Mutation { () in
            try await UserAPI.followUser(id: userID)
        }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            if let user = user.data {
                Text(user.name)
                    .font(.title.bold())
            }

            Button(follow.isPending ? "Following..." : "Follow") {
                Task {
                    do {
                        try await follow.run(())
                        await user.invalidate()
                        await user.refetch()
                    } catch {
                    }
                }
            }
            .disabled(follow.isPending)

            if let error = follow.error {
                Text(error.localizedDescription)
                    .foregroundStyle(.red)
            }
        }
        .padding()
        .onAppear {
            Task {
                await user.fetchIfNeeded()
            }
        }
    }
}

Initial data example

Use initialData when you already have a value and want the query to start in a success state.

_user = Query(
    .user(userID),
    initialData: cachedUser,
    initialDataUpdatedAt: cachedUserUpdatedAt
) {
    try await UserAPI.fetchUser(id: userID)
}

Behavioral notes

  • fetchIfNeeded() fetches only when data is missing, stale, or invalidated.
  • refetch() always runs the fetch closure.
  • invalidate() marks the exact key as stale and does not refetch automatically.
  • reset() clears observer-local state.
  • isLoading is only for the initial load when no data exists yet.
  • isFetching is true for any active fetch, including refetches.

About

Vista is a SwiftUI-first server-state management library inspired by TanStack Query.

Resources

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages