Donald Dang
Donald Dang

Reputation: 61

ForEach Loop requires that Image Conform to 'Hashable' when trying to show an array of Images?

I'm trying to create a view, that allows the user to upload multiple images and display them in a HStack. I'm able to display a singular image by simply putting:

image?.resizable().scaledToFit() So in order to display multiple images, I thought to append it to an array loadImages(), and use a ForEach loop in order to display the images, by defining a

@State private var images: [Image]? = [], and using images?.append(image) to add it to the images array.

Since it's an optional, in my ForEach loop, I unwrapped it with an empty array, but this is where I get the error Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'Image' conform to 'Hashable'. To my understanding, by adding id: \.self should allow it to conform to Hashable already - is there any special properties that Image contains that prevents it from conforming to Hashable, or is there something fundamental I'm missing here?

For reference, the full code is:

import SwiftUI

struct ImagePickerTestView: View {
    
    @State private var images: [Image]? = []
    @State private var image: Image?
    @State private var isShowingImagePicker = false
    @State private var inputImage: UIImage?
    
    var body: some View {
        NavigationView {
            VStack {
                ZStack {
                    Rectangle()
                        .fill(Color.secondary)
                    
                    Text("Tap To Select Photos")
                        .foregroundColor(.white)
                        .font(.headline)
                }
                .onTapGesture {
                    isShowingImagePicker = true
                }
                HStack {
                    ForEach(images ?? [], id: \.self) { img in
                        img
                            .resizable()
                            .scaledToFit()
                    }
                }
                /*
                //insert code to display images here
                //old implementation was
                image?
                    .resizable()
                    .scaledToFit()
                 */
                
            }
            .padding([.horizontal, .bottom])
            .navigationTitle("Select Images")
            .onChange(of: inputImage) { _ in
                loadImages()
            }
            .sheet(isPresented: $isShowingImagePicker) {
                ImagePicker(image: $inputImage)
            }
        }
    }

    func loadImages() {
        guard let inputImage = inputImage else { return }
        let image = Image(uiImage: inputImage)
        images?.append(image)
    }
}



#Preview {
    ImagePickerTestView()
}

and the imagePicker is:

import PhotosUI
import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var image: UIImage?
    
    func makeUIViewController(context: Context) -> PHPickerViewController {
        var config = PHPickerConfiguration()
        config.filter = .images
        let picker = PHPickerViewController(configuration: config)
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        let parent: ImagePicker
        
        init(_ parent: ImagePicker) {
            self.parent = parent
        }
        
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)
            
            guard let provider = results.first?.itemProvider else { return }
            
            if provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { image, _ in
                    self.parent.image = image as? UIImage
                }
            }
        }
    }
}


, which simply allows you to select a photo.

Upvotes: 2

Views: 1691

Answers (2)

Benzy Neez
Benzy Neez

Reputation: 21730

A better solution might be to wrap each image with an Identifiable wrapper. For example:

struct IdentifiableImage: Identifiable {
    let id = UUID()
    let image: Image
}

If the array is then changed to be [IdentifiableImage], the ForEach can be simplified to the following:

ForEach(images) { img in
    img.image
        .resizable()
        .scaledToFit()
}

Upvotes: 3

burnsi
burnsi

Reputation: 7754

To my understanding, by adding id: .self should allow it to conform to Hashable already

This is the reason you are struggling. with id: \. ... you are marking what property of your collection item what should be used to identify your individual items. This property has to conform to Hashable. By using self and an Image as collection item Image would need to conform to Hashable what it does not.

As allready pointed out in the comments, SwiftUI Views are not intended to be used in Arrays or be stored in any other way. Store the source for the View and build it when necessary.

The easiest way would be to create a model struct that conforms to Identifiable.

e.g.:

struct ImageModel: Identifiable{
    var id = UUID()
    var image: UIImage
}

than a possible implementation would look like:

struct ImagePickerTestView: View {
    
    @State private var images: [ImageModel] = []
    @State private var isShowingImagePicker = false
    
    var body: some View {
        NavigationView {
            VStack {
                ZStack {
                    Rectangle()
                        .fill(Color.secondary)
                    
                    Text("Tap To Select Photos")
                        .foregroundColor(.white)
                        .font(.headline)
                }
                .onTapGesture {
                    isShowingImagePicker = true
                }
                HStack {
                    ForEach(images) { model in
                        Image(uiImage: model.image)
                            .resizable()
                            .scaledToFit()
                    }
                }
            }
            .padding([.horizontal, .bottom])
            .navigationTitle("Select Images")
            .sheet(isPresented: $isShowingImagePicker) {
                // instead of an selection variable pass on the model array
                ImagePicker(images: $images)
            }
        }
    }
}



#Preview {
    ImagePickerTestView()
}

import PhotosUI
import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    // change implementation from single var to array
    @Binding var images: [ImageModel]
    
    func makeUIViewController(context: Context) -> PHPickerViewController {
        var config = PHPickerConfiguration()
        config.filter = .images
        let picker = PHPickerViewController(configuration: config)
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        let parent: ImagePicker
        
        init(_ parent: ImagePicker) {
            self.parent = parent
        }
        
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)
            
            guard let provider = results.first?.itemProvider else { return }
            
            if provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { image, _ in
                    // Check if an image has been selected
                    guard let image = image as? UIImage else {return}
                    // append the model
                    self.parent.images.append(ImageModel(image: image))
                    
                }
            }
        }
    }
}

Upvotes: 2

Related Questions