
SwiftUI does some property wrapper magic to (very efficiently) refresh your views, but what if you want to force a refresh for some reason? Here’s the techniques I’m currently using to do that.
The tricks are below, but just so you can see them in context, here’s the sample app we’re working on. It’s a list of cars so you can keep track of how many of each kind you own. Here’s our data:
struct Car: Identifiable, Codable, Equatable {
var id = UUID()
let model: String
var number = 0
}
class Cars: ObservableObject {
@Published var items = [Car]()
init() {
let car1 = Car(model: "Station wagon")
let car2 = Car(model: "Sedan")
let car3 = Car(model: "Hatchback")
items = [car1, car2, car3]
}
func increment(_ car: Car) -> Bool {
let index = items.firstIndex(of: car)
if let index = index {
items[index].number += 1
return true
} else {
return false
}
}
deinit {
}
}
The app is a list inside a navigation stack. The view for each car is split out into a subview:
struct ContentView: View {
@StateObject var cars = Cars()
var body: some View {
NavigationView {
List {
ForEach(cars.items) { car in
CarView(car: car, cars: cars)
}
}
.navigationTitle("Cars")
}
}
}
struct CarView: View {
var car: Car
var cars: Cars
var body: some View {
HStack {
Text("\(Int.random(in: 10...99)) ")
VStack(alignment: .leading) {
Text(car.model)
.font(.headline)
}
Spacer()
Text(" \(car.number) ")
Button {
if !cars.increment(car) {
print("Unexpected error - car not found:\(car.model)")
}
} label: {
Image(systemName: "plus")
}
}
}
}
The random Int is just so we can get a visual indication that our view has refreshed.
All the other tricks rely on this trick. SwiftUI reacts to any @State property changing, so to force a change, there just needs to be a @State property we change. I add a:
@State var refresh = false
to my ContentView. Whenever this changes, ie refresh.toggle() the view will be redrawn. Of course we don’t want just the ContentView redrawn, but the subview as well, so it needs to be passed into the subview. We don’t do anything with it there, just pass it in.
struct ContentView: View {
@StateObject var cars = Cars()
@State private var refresh = false
var body: some View {
NavigationView {
List {
ForEach(cars.items) { intItem in
CarView(car: intItem, cars: cars, refresh: refresh)
}
}
.navigationTitle("Cars")
}
}
}
struct CarView: View {
var car: Car
var cars: Cars
var refresh: Bool
var body: some View {
HStack {
Text("\(Int.random(in: 10...99)) ")
VStack(alignment: .leading) {
Text(car.model)
.font(.headline)
}
Spacer()
Text(" \(car.number) ")
Button {
if !cars.increment(car) {
print("Unexpected error - car not found:\(car.model)")
}
} label: {
Image(systemName: "plus")
}
}
}
}
That’s the infrastructure in place, so now we can use it. We could add a button that the user could click to refresh, but 2022 users have been trained to pull down on views to make them refresh, so let’s do that. Just add a .refreshable() modifier to our list, then toggle refresh in the closure.
struct ContentView: View {
@StateObject var cars = Cars()
@State private var refresh = false
var body: some View {
NavigationView {
List {
ForEach(cars.items) { intItem in
CarView(car: intItem, cars: cars, refresh: refresh)
}
}
.navigationTitle("Cars")
.refreshable {
refresh.toggle()
}
}
}
}
Now when the user pulls the list down, the familiar wait wheel appears, the view, and the car subviews, all redraw.
In my Habit app, the activities are going to appear with a temporal description of when they should be done, like “next week”, “in a few minutes”, or “tomorrow”. Those will be created from the date/time the activity was last done, how often the activity should be done, and the current date/time. So if our app has been in the background and we open it up, we want fresh calculations. We can do that by detecting a scenePhase change to .active.
We have to declare a variable for it, and then add the .onChange to the view:
struct ContentView: View {
@StateObject var cars = Cars()
@State private var refresh = false
@Environment(\.scenePhase) var scenePhase
var body: some View {
NavigationView {
List {
ForEach(cars.items) { intItem in
CarView(car: intItem, cars: cars, refresh: refresh)
}
}
.navigationTitle("Cars")
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
refresh.toggle()
}
}
}
}

Even if our user leaves the app open and foregrounded, eventually the descriptions will be out of date, so another thing we could do is use a timer. We have to add a timer property, and add an .onReceive() to the view. The timer interval in seconds is set when the timer is created. The example below is going to trigger every second.
struct ContentView: View {
@StateObject var cars = Cars()
@State private var refresh = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
List {
ForEach(cars.items) { intItem in
CarView(car: intItem, cars: cars, refresh: refresh)
}
}
.navigationTitle("Cars")
}
.onReceive(timer, perform: { _ in
refresh.toggle()
})
}
}