SwiftUI .id(_:) Pros and Cons
SwiftUI is amazing. Until it decides your UI doesn’t need updating. Sometimes you change data, but the view doesn't update. For example, animations don’t restart. And you sit there wondering: why?
Let’s talk about the .id(_:) modifier, and when it’s appropriate to use it.
A Bad Usage Example
Recently I solved a view refresh issue using the SwiftUI id modifier. Later I understood that the situation may signal architecture problems, improved the data flow and gave up on using id.
class MainViewModel: ObservableObject, Identifiable {
let course: EducationCourse?
@Published var currentLessonIndex: Int = 0
var currentUnit: EducationUnit? {
guard let course = course else {
return nil
}
guard currentLessonIndex < course.lessons.count else {
return nil
}
return course.lessons[currentLessonIndex]
}
var unitProgressExplanation: String {
guard let currentUnit = currentUnit else {
return ""
}
let current = currentLessonIndex + 1
let total = currentUnit.lessons.count
return "Lesson \(current)/\(total)"
}
}
struct ProgressViewModel {
let explanation: String
}
@ViewBuilder
var progressSection: some View {
let progressViewModel = ProgressViewModel(explanation: viewModel.unitProgressExplanation)
ProgressView(progressViewModel)
.id(viewModel.unitProgressExplanation)
}
In this case the progress view uses its own view model struct. It needs to be recreated with a new view model on the explanation string update. You may use the .id(_:) modifier which makes the view external identity related to the provided property.
.id(viewModel.unitProgressExplanation)
If the property value changes, SwiftUI thinks that it should replace the entire view. The old view is destroyed, its state is discarded, a brand new view is created.
Refactoring
Using .id(_:) gives you control, but the price is performance. If you can avoid using id, just do it. More suitable solution may be providing the main view model object inside the progress view. Here's how we can refactor the code above to give up the id modifier using protocols and generics.
protocol ProgressProviderProtocol: ObservableObject {
var unitProgressExplanation: String { get }
}
class MainViewModel: ObservableObject, Identifiable, ProgressProviderProtocol {
let course: EducationCourse?
@Published var currentLessonIndex: Int = 0
var currentUnit: EducationUnit? {
guard let course = course else {
return nil
}
guard currentLessonIndex < course.lessons.count else {
return nil
}
return course.lessons[currentLessonIndex]
}
var unitProgressExplanation: String {
guard let currentUnit = currentUnit else {
return ""
}
let current = currentLessonIndex + 1
let total = currentUnit.lessons.count
return "Lesson \(current)/\(total)"
}
}
@ViewBuilder
var progressSection: some View {
ProgressView(progressProvider: viewModel)
}
struct ProgressView<P: ProgressProviderProtocol>: View {
@ObservedObject var progressProvider: P
}
A Good Usage Example
In some cases using the id modifier may be reasonable.
struct CelebrationView {
body {
Icon("star")
.onAppear {
runCelebrationAnimation()
}
}
}
milestoneSection {
CelebrationView()
.id(progressMilestone)
}
Whenever the milestone changes, the old animation view is destroyed, a new one appears and the animation restarts.
Pros
- Predictable refresh behavior.
- Animation reset control.
- Automatic state cleanup.
- Simple one-line implementation.
Cons
- Rebuild cost isn’t free.
- All internal state is lost.
- Can hide architectural issues.
When Apple Developers Should Use It
Appropriate:
- Restarting animations.
- Resetting local state.
- Switching logical modes.
- Forcing lifecycle events.
Avoid:
- Random “fix attempts”.
- Large container hierarchies.
- Situations you don’t understand.
The Final Verdict
.id(_:) may be useful in real apps. But sometimes the need to use it is a signal of architecture problems. SwiftUI is declarative, and if you need to remind who’s in charge, it may be a bad sign.