skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 the selectedPhoto and selectedPhotoData 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.. :)

    @Observable
    final class AddItemViewModel: @unchecked Sendable {
    
        var showPhotosPicker: Bool = false
    
        var selectedPhoto: PhotosPickerItem?
        var selectedPhotoData: Data?
    
        init(selectedPhoto: PhotosPickerItem? = nil, selectedPhotoData: Data? = nil) {
            self.selectedPhoto = selectedPhoto
            self.selectedPhotoData = selectedPhotoData
        }
    
        func loadPhoto() async {
            if let data = try? await selectedPhoto?.loadTransferable(type: Data.self) {
                await MainActor.run {
                    selectedPhotoData = data
                }
            }
        }
    }
    

    Create a @State private var vm = AddViewModel() inside the AddModel view and use its properties instead of the AddModel view properties.

    Also make the original ViewModel conform to @unchecked Sendable and fix the remaining errors related to the AddModel view properties not being available anymore and use those from the newly created AddItemViewModel 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. :)


  2. Try using Task.detached to wrap loadTransferable? In Xcode 16 the whole SwiftUI view is decorated by @MainActor, so the task function and Task inside is running on MainActor, which may not be expected.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search