Skip to content

[Bug]: Functions endpoint is blocking SwiftUI render until call completes #954

@thebarndog

Description

@thebarndog

Version

2.43.1

Platform

iOS

Swift Version

6.3

What happened?

Consider the following pseudocode:

struct CustomView: View {
    
    @State private var viewModel = CustomViewModel()

    var body: some View {
        Image(systemName: viewModel.posts.isEmpty ? "tray" : "tray.fill")
        .task { await viewModel.load() }
    }
}

and the view model

@Observable
final class CustomViewModel {

    /// This is using pointfreeco's SQLiteData to simplify the example but the same behavior is observed with SwiftData
    @FetchAll var posts: [Post]

    func load() async {
        let token = sup abase.auth.currentSession!.accessToken
        let options = FunctionOptions(method: .get, query: [], headers: ["Authorization": "Bearer \(accessToken)"]
        do {
            let response: PostListResponse = try await supabase.functions.invoke("posts-v3", options: options, decode: { data, _ in try JSONDecoder().decode(PostListResponse.self, from: data) }
            try await database.write { db in 
                Post.upsert { response.posts }.execute(db)
                Post.delete().where { $0.notIn(response.posts) }
            }
        } catch {}
    }
}

Very simple view and view model that changes the fill state of a system image based on a db query and onAppear, calls an initial load method. On first load, there's a delay because the database query with FetchAll has nothing on disk yet, so the image doesn't transition from empty to filled until after the network call, which is expected.

However on second load, when data is already on disk, the same behavior occurs. Rather than the view getting immediately populated with the @FetchAll data and immediately flipping to the filled icon, it waits until the network call has completed and finished the load method to do so. You can exacerbate this issue by adding a Task.sleep after the network call and the UI state won't update for that additional sleep delay.

What should happen is the UI immediately updates with the results of the db query powered by FetchAll or @Query in SwiftData and the supabase call runs separately from that and populates the db and then a subsequent update happens if needed. Instead, the supabase call seems to be blocking render updates to the view until it's finished, possibly by blocking the main thread.

To further show that it was the supabase and not our client side code that was responsible for this, I replaced the supabase invocation with a Task.sleep that returns stub data from the network, just the supabase invocation, nothing else in the load function and the UI updated immediately, as I would expect it to. Unfortunately I can't share our actual code as that is proprietary but even this simple example should illustrate the issue. I'm not an expert on concurrency but it does seem like something in supabase is blocking the thread its on, which in this case is the main thread, and is causing delayed view renders.

Steps to Reproduce

  1. Create a view dependent on some local database state i.e. state thats quick to fetch but not in memory
  2. call a supabase method on .task in SwiftUI to fetch remote data
  3. Observe that view will not update until after the supabase call has returned, rather than two updates in a row, one from the db and one from the network.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingswift

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions