UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Day 77 Challenge

Forums > 100 Days of SwiftUI

I'm struggling a lot with the Day 77 Challenge

Right now, my main issue is that I don't understand how to make one sheet pop up with a PHPickerViewController, but then have another sheet pop up immediately after an image is selected, so that the user can enter a name for the selected photo.

So far I have this in ContentView...

import SwiftUI

struct ContentView: View {
  @StateObject private var viewModel = ViewModel()

  var body: some View {
    NavigationView {
      List {
        ForEach(viewModel.namedFaces) { face in
          HStack {
            Image(systemName: "person")
            Text(face.name)
          }
        }
      }
      .navigationTitle("Names to Faces")
      .toolbar {
        Button {
          viewModel.showingImagePicker.toggle()
        } label: {
          Image(systemName: "plus")
        }
      }
      .sheet(isPresented: $viewModel.showingImagePicker) {
        ImagePicker(image: $viewModel.selectedImage)
      }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

and this is the ViewModel for it

import SwiftUI

extension ContentView {
  @MainActor class ViewModel: ObservableObject {
    @Published var namedFaces: [NamedFace]
    @Published var selectedImage: UIImage?
    @Published var showingImagePicker = false

    let savePath = FileManager.documentsDirectory.appendingPathComponent("NamedFaces")

    init() {
      do {
        let data = try Data(contentsOf: savePath)
        namedFaces = try JSONDecoder().decode([NamedFace].self, from: data)
      } catch {
        namedFaces = []
      }
    }

    func addFace() {
      if let imageData = selectedImage?.jpegData(compressionQuality: 0.8) {
        let newNamedFace = NamedFace(id: UUID(), name: "", imageData: imageData)
        namedFaces.append(newNamedFace)
      }
    }

    func save() {
      do {
        let data = try JSONEncoder().encode(namedFaces)
        try data.write(to: savePath, options: [.atomic])
      } catch {
        print("Unable to save data")
      }
    }
  }
}

and this is the data model...

import SwiftUI

struct NamedFace: Identifiable, Codable {
  static let example = NamedFace(id: UUID(), name: "", imageData: UIImage(systemName: "person.crop.square")?.jpegData(compressionQuality: 1) ?? Data())

  var id: UUID
  var name: String
  var imageData: Data
  var image: Image? {
    if let uiImage = UIImage(data: imageData) {
      return Image(uiImage: uiImage)
    } else {
      return nil
    }
  }
}

With what I have right now, I am able to run the code and tap the (+) button to get the image picker to pop up and let me select an image. But I don't know how to make that image selection trigger another sheet to pop up with another view.

1      

In a past project, we used the .sheet(item:) modifier on a view to detect when an optional value was set, and display a sheet based on that. But when I try to use that here I run into a problem telling me that a UIImage? cannot be used because it is not identifiable (or maybe it was not equatable or comparable). So maybe the solution is to create a property wrapper for selectedImage to make it an Identifiable/comparable/equatable UIImage. Then I might be able to make this work.

I'm not sure if that will cause problems when I try to pass a binding to a wrapped property to the ImagePicker though...

I'm not currently working on the project now, and this kind of seems like I'm sending myself on a wild goose chase, but if somebody doesn't stop me with a better solution before I get home, I think I'll try that I guess.

1      

@ostrich is trying to apply Swift to a logic problem...

I don't understand how to make one sheet pop up with a PHPickerViewController,
but then have another sheet pop up immediately after an image is selected

This is a great time to stare at the lights in your room.

Perhaps you have two lights in your computer room? On is a ceiling lamp, the other is a desk lamp near your computer. What are the controls to turn these lights on or off?

You have two light switches?! Yes? Each switch has a state: On or Off. Or you can think of them as true or false.

So what you really have is not a Swift "show a pop up" problem. What you have is a logical, "How do I control the lights?" problem.

If you energize one of the switches, it will turn on the overhead lamp, but not the desk lamp. When you turn the overhead lamp OFF, you want the desk lamp to automatically turn on. Then when you turn the desk lamp off, you want to ensure the overhead lamp is also off.

Think of the states of your app's sheets as light switches.

Under what conditions do you want the sheet with the PHPickerViewController to be displayed? When that view is hidden (i.e. @State var is set from true to false) you now want the other sheet to be displayed. What @State var do you need to turn on?

This is a logic problem! Return here please, and share your logical solution.

1      

I was able to get it to work after changing my data model a bit and giving it a different type of initializer.

Although, using this modifieer still doesn't work with a binding to UIImage? because it must conform to identifiable I guess.

.sheet(item: $UIImage?)

Error message: Instance method 'sheet(item:onDismiss:content:)' requires that 'UIImage' conform to 'Identifiable'

But I eventually got it to work using this modifier instead

.onChange(of: $UIImage?) {
  showingEditView.toggle()
}

I think I was just running into a problem with going this route because of the way that my EditView initializer was set up before.

If you want to see how I have changed the files I mentioned above to solve the problem, they look like this now (although, the project still isn't complete yet.

ContentView.swift

import SwiftUI

struct ContentView: View {
  @StateObject private var viewModel = ViewModel()

  var body: some View {
    NavigationView {
      List {
        ForEach(viewModel.namedFaces) { namedFace in
          HStack {
            Image(uiImage: (namedFace.image ?? UIImage(systemName: "person.crop.square")!))
              .resizable()
              .scaledToFit()
              .frame(width: 100, height: 100)
              .background(.black)
              .clipShape(RoundedRectangle(cornerRadius: 10))
              .overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(.blue, lineWidth: 1))
              .padding([.trailing])

            Text(namedFace.name)
              .font(.title)
              .foregroundColor(.blue)
          }
        }
      }
      .navigationTitle("Names to Faces")
      .toolbar {
        Button {
          viewModel.showingImagePicker.toggle()
        } label: {
          Image(systemName: "plus")
        }
        .sheet(isPresented: $viewModel.showingImagePicker) {
          ImagePicker(image: $viewModel.selectedImage)
        }
      }
      .onChange(of: viewModel.selectedImage) { _ in
        viewModel.showingEditView.toggle()
      }
      .sheet(isPresented: $viewModel.showingEditView) {
        EditView(namedFace: NamedFace(id: UUID(), name: "", image: viewModel.selectedImage)) { newNamedFace in
          viewModel.addNamedFace(newNamedFace: newNamedFace)
        }
      }
    }
  }
}

ContentView-ViewModel.swift

import SwiftUI

extension ContentView {
  @MainActor class ViewModel: ObservableObject {
    @Published var namedFaces: [NamedFace]
    @Published var selectedImage: UIImage?
    @Published var showingImagePicker = false
    @Published var showingEditView = false

    let savePath = FileManager.documentsDirectory.appendingPathComponent("NamedFaces")

    init() {
      do {
        let data = try Data(contentsOf: savePath)
        namedFaces = try JSONDecoder().decode([NamedFace].self, from: data)
      } catch {
        namedFaces = []
      }
    }

    func addNamedFace(newNamedFace: NamedFace) {
      namedFaces.append(newNamedFace)
    }

    func save() {
      do {
        let data = try JSONEncoder().encode(namedFaces)
        try data.write(to: savePath, options: [.atomic])
      } catch {
        print("Unable to save data")
      }
    }
  }
}

NamedFace.swift

import SwiftUI

struct NamedFace: Codable, Comparable, Equatable, Identifiable {
  static let example = NamedFace(id: UUID(), name: "James Bond", image: UIImage(systemName: "person.crop.square"))

  var id: UUID
  var name: String
  var image: UIImage?

  enum CodingKeys: CodingKey {
    case id
    case name
    case image
  }

  init(id: UUID, name: String, image: UIImage?) {
    self.id = id
    self.name = name
    self.image = image
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decode(UUID.self, forKey: .id)
    self.name = try container.decode(String.self, forKey: .name)
    let imageData = try container.decode(Data.self, forKey: .image)
    self.image = UIImage(data: imageData)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
    try container.encode(name, forKey: .name)
    let imageData = image?.jpegData(compressionQuality: 0.8)
    try container.encode(imageData, forKey: .image)
  }

  static func ==(lhs: Self, rhs: Self) -> Bool {
    lhs.id == rhs.id
  }

  static func <(lhs: Self, rhs: Self) -> Bool {
    lhs.name < rhs.name
  }
}

1      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.