SwiftUI: Changes to Unused State Variables in the Body May Be Ignored
This issue arose from a project I was working on for the “100 Days of SwiftUI” challenge. The application allows users to select a photo from their library and assign a name to it. While it’s a simple app, it presented a puzzling challenge.
Below is the initial version of the code I attempted. It failed at an unexpected point—can you identify the issue?
import SwiftUI
import PhotosUI
struct ContentView: View {
@State private var namedPhotos: [NamedPhoto] = []
@State private var pickerItem: PhotosPickerItem?
@State private var currentNamedPhoto: NamedPhoto? = nil
@State private var isAskNamePresented = false
var body: some View {
PhotosPicker("Select a photo", selection: $pickerItem, matching: .images)
.onChange(of: pickerItem) {_, newItem in
Task {
if let data = try await newItem?.loadTransferable(type: Data.self) {
currentNamedPhoto = NamedPhoto(photo: data, name: "")
isAskNamePresented = true
}
}
}
.sheet(isPresented: $isAskNamePresented) {
VStack {
if let currentNamedPhoto = currentNamedPhoto {
toImage(from: currentNamedPhoto.photo)
.resizable()
.scaledToFit()
.frame(height: 300)
if let namedPhoto = Binding($currentNamedPhoto) {
TextField("Name", text: namedPhoto.name)
}
Button("Add") {
namedPhotos.append(currentNamedPhoto)
isAskNamePresented = false
}
} else {
Text("No photo selected")
}
}
}
List(namedPhotos, id: \.id) { photo in
VStack(alignment: .leading) {
Text("hi")
toImage(from: photo.photo)
.resizable()
.scaledToFit()
}
}
}
func toImage(from data: Data) -> Image {
if let uiImage = UIImage(data: data) {
return Image(uiImage: uiImage)
}
fatalError("Couldn't convert data to UIImage")
}
}
To briefly explain the code: the PhotosPicker allows users to select a photo from their library. Once the photo data is loaded, the sheet is triggered by updating the variable. This is the key functionality.
However, the first time I selected a photo, the sheet was presented, but its content displayed “No photo selected.” This was unexpected!
The isAskNamePresented variable is set after we assign the photo data to currentNamedPhoto, so the sheet should recognize that currentNamedPhoto is no longer nil.
Initially, I suspected that the code inside the Task was not executed sequentially, but I quickly dismissed this idea. Swift does not behave like Haskell in this regard.
Next, I considered whether the closure might be related to the issue, as the variable captured within the closure has value semantics. This means the value does not change, but I soon realized this assumption was incorrect.
After conducting some research, I found an explanation on StackOverflow: link. It states that changes are ignored because the variable is not directly used within the body property, leading SwiftUI to optimize it out.
To resolve this, I updated the code by adding one line:
var body: some View {
if let currentNamedPhoto = currentNamedPhoto {}
PhotosPicker("Select a photo", selection: $pickerItem, matching: .images)
// ...
Now, everything works as intended!
Alternatively, we can use another initializer for the sheet: link
struct ContentView: View {
@State private var namedPhotos: [NamedPhoto] = []
@State private var pickerItem: PhotosPickerItem?
@State private var currentNamedPhoto: NamedPhoto? = nil
var body: some View {
PhotosPicker("Select a photo", selection: $pickerItem, matching: .images)
.onChange(of: pickerItem) {_, newItem in
Task {
if let data = try await newItem?.loadTransferable(type: Data.self) {
currentNamedPhoto = NamedPhoto(photo: data, name: "")
}
}
}
.sheet(item: $currentNamedPhoto, onDismiss: { }) { item in
var newItem = NamedPhoto(photo: item.photo, name: "")
toImage(from: newItem.photo)
.resizable()
.scaledToFit()
.frame(height: 300)
TextField("Name", text: Binding(get: { newItem.name }, set: { newItem.name = $0 }))
Button("Add") {
namedPhotos.append(newItem)
currentNamedPhoto = nil
}
}
// ...
This approach also works effectively.