Swift Concurrency Series 07|Common pitfalls when combining SwiftUI with async/await
The real pitfall is usually not in the syntax, but in whether the "page life cycle" and "task life cycle" are aligned.
SwiftUI and async/await are both elegant when viewed individually, but when they are combined, the most easily exposed problem is the life cycle.
More precisely, it is the dislocation between the two life cycles:
- When does a SwiftUI page appear, redraw, or disappear?
- When does an asynchronous task start, pause, end, and cancel?
If these two things are not aligned, even if the code can run today, it will easily grow later:
- Repeat request
- Page flashing
- Write back old results
- The loading status is confusing
- The task is still updated after leaving the page
So what this article really wants to talk about is: SwiftUI asynchronous pages are prone to chaos, and what are the root causes behind these chaos.
1. The most common misjudgment: treating View as a stable object
This is the easiest habit for many developers with a UIKit background to bring in.
Everyone will default to:
- The page appears once
- Request to send once
- Update the current page after the request comes back
But View in SwiftUI is more like a state description, rather than a stable instance that can be held on for a long time.
This means that if the default in your mind is “the View in front of me will always be there, so this task naturally belongs to it”, it will be easy to cause problems later.
SwiftUI doesn’t require that tasks be tied to a stable object, it’s just that it’s easy to fool yourself into thinking you’ve already done so.
2. The biggest pitfall of onAppear is that it is used as a one-time initialization entrance.
Many articles will say: onAppear may be executed multiple times.
This statement is true, but it is not enough.
The real danger is that it is often written as the de facto standard entry for “page initialization”:
.onAppear {
Task {
await loadData()
}
}
The problem is not that this code must be wrong, but that it is easy to mentally add:
“When this page appears, it will only be run once.”
Once you think this way, the following will appear one after another:
- Repeat request
- Repeatedly reset status
- Repeatedly bury points
- The data was cleared just after it was displayed and started again.
So a more stable idea is: **Let the asynchronous process itself be idempotent, deduplicated or replaceable. **
3. The second pitfall: mixing page status and task status together
Common states in a SwiftUI page include:
- Current filter criteria
- Current data content
- Is it loading?
- error message
- Currently running tasks
If these states are not clearly layered, they can easily become lumped together.
The most common bad smells are:
itemsisLoadingerrorisRefreshingkeywordselectedTab
Each value individually makes sense, but it’s hard to tell how they relate to each other. Then the page will enter a very awkward state:
- Looks like he’s in any condition
- But none of the states really express “what semantics the page is in now”
In this case, as soon as the asynchronous result comes back, any status may be changed, and the problem will only be exposed sooner or later.
4. The third pitfall: old results are written back to the current UI
This is one of the most frequent problems in SwiftUI asynchronous pages.
Typical scenarios include:
- Users can quickly switch tabs
- Search keywords change continuously
- Repeatedly switch filter conditions
- The page triggers refresh and first load one after another
On the surface, you may think that you just “sent multiple tasks”, but in fact, the real problem is:
**Although the old task is still legally completed, it no longer corresponds to the current page status. **
Once the old results can still be written to the current UI, the appearance you see is usually just:
- The page flashes
- The list suddenly regresses
- The loading state ends suddenly
- Error message pops up inexplicably
These phenomena are very similar to “small bugs”, but the root causes are very similar: Validity of task results is not managed.
5. The fourth pit: spread all asynchronous entries on the View
If these entries appear on a page at the same time:
onAppear { Task { ... } }refreshable { await ... }onChange(of:) { Task { ... } }- Click the button to open another
Task
They all look legitimate individually, but taken together they quickly become a problem:
**The task relationship is out of control. **
It tends to get harder and harder to answer:
- who is the main entrance
- Who should cancel whom?
- Who is qualified to change the current display status?
- Which round of tasks does a certain status update correspond to?
Therefore, many SwiftUI asynchronous pages have too many entrances, each entrance can directly trigger tasks, and finally there is no unified coordination layer.
6. The fifth pitfall: the default is “as long as the UI can be updated”
SwiftUI hides many UI update details, which gives people the illusion that as long as I finally change the status, the page will naturally refresh.
But the real question is not “will it be refreshed?” but “whether it is qualified to refresh at this time.”
For example:
- Has the current result expired?
- Is the current page still alive?
- Is the current status still matching this round of tasks?
- Should the current modification be made under the main actor semantics?
If these things are not taken seriously, the page may not crash immediately, but it will slowly accumulate a lot of “accidental confusion”.
7. A more stable approach: let the View trigger the intention and let the state layer manage the task
The organization method I prefer is:
- View is responsible for expressing user intent
- ViewModel or state layer is responsible for managing tasks and result qualifications
- View only consumes the sorted state
That said, it’s better for the View to know less about these details:
- Whether to cancel old tasks
- Which result has expired
- Is the current loading loading for the first time or refreshing?
- Whether the error state should overwrite old content
If these problems are left on the View, the page will quickly change from “declarative UI” to “declarative shell wrapped in a layer of decentralized asynchronous logic”.
8. A suggestion closer to actual combat
If a SwiftUI page starts to get complicated, I usually force myself to answer the following questions first:
- What task entries are there on the page?
- Whether similar tasks coexist, replace or ignore.
- Which states are page semantic states and which are just internal process states.
- Whether the old results are still allowed to be changed to the current page.
- After leaving the page, which tasks should be continued and which ones should be stopped.
Once you cannot answer these questions, it usually means that the page task model has not been established yet.
9. Conclusion: The real difficulty of SwiftUI asynchronous pages is life cycle alignment
A common situation is that the main pitfalls between SwiftUI and async/await are syntax.
But the more real situation is:
Viewis not a stable object as you would think on the surface.onAppearis not a one-time initialization semantics- Old results will not automatically expire because they have “expired”
- If there are too many task entries at the page level, it will definitely start to get messy.
So the truly stable SwiftUI asynchronous page is:
First align the page life cycle with the task life cycle, and then write specific asynchronous code.
Only if this is established in advance, the lightness of SwiftUI and the elegance of async/await will truly become advantages, rather than making chaos easier to write.
读完之后,下一步看什么
如果还想继续了解,可以从下面几个方向接着读。