I have the below view where I want to load an image from gallery using PhotosPicker and display it to the user. I also want to convert the image to Data so I can store it in SwiftData database.
After updating to iOS 18, xcode 16.0 beta and changing strict concurrency from minimal to complete and swift version from 5 to 6, I receive the error Sending main actor-isolated value of type 'PhotosPickerItem' with later accesses to nonisolated context risks causing data races
on line if let data = try? await selectedPhoto?.loadTransferable(type: Data.self)
.
I had many more errors but I managed to get rid of them. The app is working as expected, but I have no idea how to solve this error.
Any help would be really appreciated.
Note: for simplicity, I removed all other properties of Model
from the code below.
struct AddModel: View {
@Environment(ViewModel.self) private var viewModel
@Environment(.dismiss) private var dismiss
@State private var showPhotosPicker: Bool = false
@State private var selectedPhoto: PhotosPickerItem?
@State private var selectedPhotoData: Data?
var body: some View {
Form {
photoSection
}
.toolbar {
toolbarItems
}
}
}
// MARK: - View Components
extension AddShoeView {
@ViewBuilder
private var photoSection: some View {
Section {
VStack(spacing: 12) {
ZStack {
if let selectedPhotoData, let uiImage = UIImage(data: selectedPhotoData) {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 12))
} else {
Image(systemName: "square.fill")
.resizable()
.foregroundStyle(.secondary)
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Text("Add Photo")
.font(.callout)
.fontWeight(.semibold)
.foregroundStyle(.white)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color(uiColor: .secondarySystemBackground))
.cornerRadius(20)
}
.frame(maxWidth: .infinity)
.photosPicker(isPresented: $showPhotosPicker, selection: $selectedPhoto, photoLibrary: .shared())
.onTapGesture {
showPhotosPicker.toggle()
}
.task(id: selectedPhoto) {
if let data = try? await selectedPhoto?.loadTransferable(type: Data.self) {
selectedPhotoData = data
}
}
}
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets())
}
@ToolbarContentBuilder
private var toolbarItems: some ToolbarContent {
ToolbarItem(placement: .confirmationAction) {
Button {
viewiewModel.addModel(image: selectedPhotoData)
dismiss()
} label: {
Text("Save")
}
}
}
}
@Observable
final class ViewModel: {
@ObservationIgnored private var modelContext: ModelContext
private(set) var models: [Model] = []
init(modelContext: ModelContext) {
self.modelContext = modelContext
fetchModels()
}
func addModel(image: Data?) {
let model = Model(image: image)
modelContext.insert(shoe)
save()
}
private func fetchModels() {
do {
let descriptor = FetchDescriptor<Model>()
self.models = try modelContext.fetch(descriptor)
} catch {
print("Fetching shoes failed, (error.localizedDescription)")
}
}
private func save() {
do {
try modelContext.save()
} catch {
print("Saving context failed, (error.localizedDescription)")
}
fetchModels()
}
}
2
Answers
As a solution I found this code working really well. Not sure if this is the right approach, but for now it is good enough for me.
First of all, I created a
AddItemViewModel
to hold theselectedPhoto
andselectedPhotoData
and made this to conform to@unchecked Ssendable
. My intention is somehow get rid of the@unchecked
, but I still have a lot to learn.. :)Create a
@State private var vm = AddViewModel()
inside theAddModel
view and use its properties instead of theAddModel
view properties.Also make the original
ViewModel
conform to@unchecked Sendable
and fix the remaining errors related to theAddModel
view properties not being available anymore and use those from the newly createdAddItemViewModel
instead.For now I am not gonna accept my answer, so everyone seeing this, I hope you'll find a better and more professional solution. :)
Try using
Task.detached
to wraploadTransferable
? In Xcode 16 the whole SwiftUI view is decorated by @MainActor, so the task function andTask
inside is running on MainActor, which may not be expected.