{"id":25317604,"url":"https://github.com/theleftbit/asyncstateviewdemo","last_synced_at":"2025-04-07T17:36:51.643Z","repository":{"id":182567298,"uuid":"668716729","full_name":"theleftbit/AsyncStateViewDemo","owner":"theleftbit","description":null,"archived":false,"fork":false,"pushed_at":"2024-06-13T19:47:40.000Z","size":32,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-02-13T19:43:06.457Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/theleftbit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-20T12:42:28.000Z","updated_at":"2024-06-13T19:47:44.000Z","dependencies_parsed_at":"2024-06-13T22:30:45.182Z","dependency_job_id":"0f0ef9c0-6fe8-4f0f-a8a6-afbacbe84342","html_url":"https://github.com/theleftbit/AsyncStateViewDemo","commit_stats":null,"previous_names":["theleftbit/asyncstateviewdemo"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theleftbit%2FAsyncStateViewDemo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theleftbit%2FAsyncStateViewDemo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theleftbit%2FAsyncStateViewDemo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theleftbit%2FAsyncStateViewDemo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/theleftbit","download_url":"https://codeload.github.com/theleftbit/AsyncStateViewDemo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247698836,"owners_count":20981422,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-02-13T19:39:35.307Z","updated_at":"2025-04-07T17:36:51.620Z","avatar_url":"https://github.com/theleftbit.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"Hello SwiftUI team,\n\nIn order to ease our transition to SwiftUI, we created a generic view called `AsyncStateView` that helps developers build UIs where the data is coming from remote web servers.\n\nThis, paired with SwiftUI's data-driven approach, has enabled our developers to be more productive and build more engaging user interfaces.\n\nThis tech was used to build the new Standings for the MLB app and we're excited with the result, just had some questions on some behaviour we are seeing.\n\nWe found found that sometimes, some interaction glitches occurred due to invalidation of `body`.\n\nYou can checkout the project and run it to see the structure:\n\n-  The `DemoApp` creates a `RootView`\n- `RootView` will simulate fetching some \"Means of Transport\" categories and put that as some sort of \"selectable tabs\" on the top of the UI. It does so by using `AsyncStateView`.\n- When that fetch is completed, the details for the Selected Tab will be fetched and displayed on a ScrollView. It also does this using `AsyncStateView`\n\nAnd here is a video of the glitches that we are seeing: \n\nhttps://github.com/theleftbit/AsyncStateViewDemo/assets/869981/ace720af-d6da-40d9-9118-15dab6a314b8\n\nDuring our debugging, we narrowed down the problem to `AsyncStateView`'s `body` [implementation](https://github.com/theleftbit/AsyncStateViewDemo/blob/main/AsyncStateViewDemo/AsyncStateView.swift#L61):\n\n```swift\n  public var body: some View {\n    actualView\n      .task(id: id) {\n        await fetchData()\n      }\n  }\n\n  @ViewBuilder\n  private var actualView: some View {\n    switch currentOperation.phase {\n    case .idle, .loading:\n      loadingView\n    case .loaded(let data):\n      hostedViewGenerator(data)\n    case .error(let error):\n      errorViewGenerator(error, {\n        fetchData()\n      })\n    }\n  } \n```\n\nChanging the `actualView` implemetation from a `@ViewBuilder` to a `VStack` fixes the issue we are seeing changing the selected element in the `TabView` on top. \n\nWe don't know why this change would have any effect, but we are inclined to think that this is due to SwiftUI not being able to figure out the Structural Identity of this View when using a `@ViewBuilder`, which brings the question: Why? \n\nWe tried debugging with `Self._printChanges()` but couldn't see any significant changes. We also tried using `Group` but the result is the same. Why is a `VStack` with one element better in terms of generating a stable Structural Identity? Or maybe that is not the problem, but `VStack` is a workaround? We also started wondering if maybe creating the views inside the `body` using an `@escaping` closure would be a problem, but `AsyncImage` (among other Views in the SDK) do it like this, so we couldn't conclude anything in that front.\n\nSo, if you where to only make this change (swap `@ViewBuilder` for `VStack` in AsyncStateView.swift line 61) and run the project, the behaviour would be almost be correct: if you scroll all the way to the \"Feet\" tab and select it, it would also glitch.\n\nhttps://github.com/theleftbit/AsyncStateViewDemo/assets/869981/6eb35c73-d1db-4ccc-bd5e-4015766ff6f6\n\nTurns out that we are using `@ViewBuilder` in another place, now in the `ContentView` to decide what view to display after a user's [selection:](https://github.com/theleftbit/AsyncStateViewDemo/blob/main/AsyncStateViewDemo/Views/ContentView.swift#L21)\n\nChanging that to a `VStack` with just one element _also_ fixes this issue. Which brings again the question? Why? Isn't this what `@ViewBuilder` or `Group` is for? to create views using conditional logic and applying view modifiers to it? We tried breaking this logic apart in a different subview but coudln't see any different result. Only wrapping it in a `VStack` would do.\n\nWe are asking these questions because, even though we have a valid workaround, `AsyncStateView` is one of the core types that we are using to build the rest of the UI and we'd like to know if there are any deficiencies on it's implementation. \n\nThanks for your help and sorry for the long long question, but lots of moving pieces and this is as narrowed down as we could do it. \n\nNote: Filed as FB12999346\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftheleftbit%2Fasyncstateviewdemo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftheleftbit%2Fasyncstateviewdemo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftheleftbit%2Fasyncstateviewdemo/lists"}