Search code examples
swiftuiphotosuiswiftui-image

SwiftUI Image display rotates on device


My test app grabs a photo from the device, and then displays it. Simple.

On the simulator, it works great, but on a device, it rotates the image. Sometimes 90 degrees, sometimes 180, sometimes it's not rotated.

Here is the code I am using, minus the button that initiates PhotoPicker:

struct ContentView: View {
    @State private var theImage: Image = Image(systemName : "chart.bar.doc.horizontal")
    @State private var selectedPhoto: PhotosPickerItem? = nil
    
    var body: some View {
        VStack {
            theImage
                .resizable()
                .scaledToFit()
                .foregroundColor(.white)
            
            PhotosPicker(selection: $selectedPhoto, matching: .images) {
                GetPhotoButton()
            }
            .task(id: selectedPhoto, {
                await loadImage(from: selectedPhoto)
                selectedPhoto = nil
            })
        }
        .padding()
    }
    
    private func loadImage(from item: PhotosPickerItem?) async {
        if let theItem = item {
            do {
                let image = try await theItem.loadTransferable(type: Image.self)
                if let image = image {
                    theImage = image
                }
            } catch {
                print("Failed to load image: \(error)")
            }
        }
    }
}

Solution

  • Thanks to @outlaw for pointing me in the right direction.

    In Apple's documentation for PhotosPickerItem, they state:

    Important: Image only supports PNG file types through its Transferable conformance ...

    So it appears that by using loadTransferable(type: Image.self) we are losing important information about the asset, namely its orientation.

    So here is a custom Transferable SwiftUI protocol (thanks to Itsuki) to cover formats other than PNG:

    struct TransferableImage: Transferable {
        let image: Image
        
        enum TransferError: Error {
            case importFailed
        }
        
        static var transferRepresentation: some TransferRepresentation {
            DataRepresentation(importedContentType: .image) { data in
                guard let uiImage = UIImage(data: data) else {
                    throw TransferError.importFailed
                }
                let image = Image(uiImage: uiImage)
                return TransferableImage(image: image)
            }
        }
    }
    

    And replace the .task(id: with an .onChange(of:

    PhotosPicker(selection: $selectedPhoto, matching: .images) {
        GetPhotoButton()
    }
    .onChange(of: selectedPhoto) {
        Task {
            if let loaded = try? await selectedPhoto?.loadTransferable(type: TransferableImage.self) {
                theImage = loaded.image
            } else {
                print("Failed to load image")
            }
        }
    }
    

    And voila, it works for all images!