Search code examples
bindingviewmodeloption-type

Binding with optional struct


I get the error "Value of optional type 'Photo?' must be unwrapped to refer to member 'name' of wrapped base type 'Photo'" when I try to send a optional struct on a binding of a TextField.

The content view code example:

import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ContentViewViewModel()
    
    private var photos = [
        Photo(id: UUID(), name: "Exclamation", data: (UIImage(systemName: "exclamationmark.triangle")?.jpegData(compressionQuality: 1))!),
        Photo(id: UUID(), name: "Circle", data: (UIImage(systemName: "circle.fill")?.jpegData(compressionQuality: 1))!)
    ]
    
    var body: some View {
        VStack {
            DetailView(viewModel: viewModel)
        }
        .onAppear {
            viewModel.selectedPhoto = photos[0]
        }
    }
}

The view model code example:

import Foundation

@MainActor
final class ContentViewViewModel: ObservableObject {
    @Published var photos = [Photo]()
    @Published var selectedPhoto: Photo?
}

The detail view code example (that uses the content view's view model):

import SwiftUI

struct DetailView: View {
    @ObservedObject var viewModel: ContentViewViewModel
    
    var body: some View {
        TextField("Photo name here...", text: $viewModel.selectedPhoto.name)
    }
}

Note that for some reasons I need the selectedPhoto property be optional.


Solution

  • You can create your own custom @Binding, so you can handle the process between getting data and set the updated here is an example:

    • If what you want is to select the photo in a List or Foreach you can bind directly to the array.

      import SwiftUI

       struct Photo {
           let id: UUID
           var name: String
           let data: Data
       }
      
       @MainActor
       final class ContentViewViewModel: ObservableObject {
           @Published var photos = [Photo]()
           @Published var selectedPhoto: Photo?
       }
      
       struct optionalBinding: View {
      
           @StateObject var viewModel = ContentViewViewModel()
      
           private var photos = [
               Photo(id: UUID(), name: "Exclamation", data: (UIImage(systemName: "exclamationmark.triangle")?.jpegData(compressionQuality: 1))!),
               Photo(id: UUID(), name: "Circle", data: (UIImage(systemName: "circle.fill")?.jpegData(compressionQuality: 1))!)
           ]
      
           var body: some View {
               VStack {
                   DetailView2(viewModel: viewModel)
      
                   Button("Select Photo") {
                       viewModel.selectedPhoto = photos[0]
                   }
               }
           }
       }
      
       struct DetailView2: View {
           @ObservedObject var viewModel: ContentViewViewModel
      
           var body: some View {
               Text(viewModel.selectedPhoto?.name ?? "No photo selected")
               TextField("Photo name here...", text: optionalBinding())
           }
      
           func optionalBinding() -> Binding<String> {
               return Binding<String>(
                   get: {
                       guard let photo = viewModel.selectedPhoto else {
                           return ""
                       }
                       return photo.name
                   },
                   set: {
                       guard let _ = viewModel.selectedPhoto else {
                           return
                       }
                       viewModel.selectedPhoto?.name = $0
                       //Todo: also update the array
                   }
               )
           }
       }
      

    enter image description here