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

SOLVED: Saving and displaying images using the image picker

Forums > SwiftUI

Hi folks,

I'm messing with the image picker to a select a profile photo based on this 2022 WWDC demo:

https://developer.apple.com/documentation/photokit/bringing_photos_picker_to_your_swiftui_app

It works great, thanks to Apple i've managed to implement the code into my app.

But I don't understand how to save the image and display it later in the app.

I'm using @AppStorage to store other attributes, and I understand @AppStorage cannot be used to save image data.

any pointers?

Thank you!

2      

@Bnerd  

I save my images into the DocumentDirectory, the below is my ImageSaver Class

import Foundation
import UIKit

class ImageSaver: NSObject {
    var successHandler: (() -> Void)?
    var errorHandler: ((Error) ->Void)?

    //***Method to save to Disk
    //The location we want to Save.
    func writeToDisk(image: UIImage, imageName: String) {
        let savePath = FileManager.documentsDirectory.appendingPathComponent("\(imageName).jpg") //Where are I want to store my data
        if let jpegData = image.jpegData(compressionQuality: 0.5) { // I can adjust the compression quality.
            try? jpegData.write(to: savePath, options: [.atomic, .completeFileProtection])
        }
    }
}

To implement the above you will an extension to your FileManager , see attached:

extension FileManager {
    static var documentsDirectory: URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
}

Then you call your Image like the below:

let imagePath = FileManager.documentsDirectory.appendingPathComponent("ImageName.jpg")
                            Image(uiImage: UIImage(contentsOfFile: imagePath.path()) ?? UIImage(systemName: "photo")!)

Give it a try :)

2      

Thank you, that looks promosing, i'd like to use your code but being a noob i'm having trouble understanding how to implement it. Can you give me any pointers? Here's my code so far

import SwiftUI
import PhotosUI
import CoreTransferable

@MainActor
class LogoModel: ObservableObject {

    enum ImageState {
        case empty
        case loading(Progress)
        case success(Image)
        case failure(Error)
    }

    enum TransferError: Error {
        case importFailed
    }

    struct LogoImage: Transferable {
        let image: Image

        static var transferRepresentation: some TransferRepresentation {
            DataRepresentation(importedContentType: .image) { data in
            #if canImport(AppKit)
                guard let nsImage = NSImage(data: data) else {
                    throw TransferError.importFailed
                }
                let image = Image(nsImage: nsImage)
                return LogoImage(image: image)
            #elseif canImport(UIKit)
                guard let uiImage = UIImage(data: data) else {
                    throw TransferError.importFailed
                }
                let image = Image(uiImage: uiImage)
                return LogoImage(image: image)
            #else
                throw TransferError.importFailed
            #endif
            }
        }
    }

    @Published private(set) var imageState: ImageState = .empty

    @Published var imageSelection: PhotosPickerItem? = nil {
        didSet {
            if let imageSelection {
                let progress = loadTransferable(from: imageSelection)

                imageState = .loading(progress)
            } else {
                imageState = .empty
            }
        }
    }

    private func loadTransferable(from imageSelection: PhotosPickerItem) -> Progress {
        return imageSelection.loadTransferable(type: LogoImage.self) { result in
            DispatchQueue.main.async {
                guard imageSelection == self.imageSelection else {
                    print("mob: logoModel. Failed to get the selected item.")
                    return
                }
                switch result {
                case .success(let LogoImage?):
                    self.imageState = .success(LogoImage.image)
                case .success(nil):
                    self.imageState = .empty
                case .failure(let error):
                    self.imageState = .failure(error)
                }
            }
        }
    }
}

// Save the image
class ImageSaver: NSObject {
    var successHandler: (() -> Void)?
    var errorHandler: ((Error) ->Void)?

    //***Method to save to Disk
    //The location we want to Save.
    func writeToDisk(image: UIImage, imageName: String) {
        let savePath = FileManager.documentsDirectory.appendingPathComponent("\(imageName).jpg") //Where are I want to store my data
        if let jpegData = image.jpegData(compressionQuality: 0.5) { // I can adjust the compression quality.
            try? jpegData.write(to: savePath, options: [.atomic, .completeFileProtection])
        }
    }
}

// Used to save the image in the documents directory
extension FileManager {
    static var documentsDirectory: URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
}

2      

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!

You image picker class

import SwiftUI
import PhotosUI

@MainActor
class ImagePicker: ObservableObject {
    let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    @Published var imageSelection: PhotosPickerItem? {
        didSet {
            Task {
                try await loadTransferable(from:imageSelection)
            }
        }
    }

    @Published var image: Image? 
    @Published var uiImage: UIImage?

    func loadTransferable(from selection: PhotosPickerItem?) async throws {
        if let data = try await imageSelection?.loadTransferable(type: Data.self) {
            if let uiImage = UIImage(data: data) {
                self.uiImage = uiImage
                self.image = Image(uiImage: uiImage)
            }
        }
    }
}

where you call the picker

@StateObject var imagePicker = ImagePicker()

Add the picker

  PhotosPicker(selection: $imagePicker.imageSelection, matching: .images, photoLibrary: .shared()) {
                            Text("upload")

                        }
                        .buttonStyle(.borderedProminent)

on button click to save

let name = String(Date.timeIntervalSinceReferenceDate)

                            let fm = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]

                            if let image = imagePicker.uiImage?.pngData() {
                                let imageName = name.replacingOccurrences(of: ".", with: "-")
                                let url = fm.appendingPathComponent(imageName)
                                try image.write(to: url)

where ever you want to display it

              ForEach(myData, id:\.self) {item in

                      if let data = try? Data(contentsOf: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(mydata.imageNameData)), let loaded = UIImage(data: data) {
                          HStack {
                              Image(uiImage: loaded)
                                  .resizable()
                                  .frame(width:100, height: 100)

                  }
              }

              you caa use coredata to save the name of images locally while images in documents directory , this will be useful

2      

@Bnerd  

@Flaneur, Full example with your previous model. Your updated LogoModel, with comments on the additions / modifications (not many):

import SwiftUI
import PhotosUI
import CoreTransferable

@MainActor
class LogoModel: ObservableObject {

    enum ImageState {
        case empty
        case loading(Progress)
        case success(Image)
        case failure(Error)
    }

    enum TransferError: Error {
        case importFailed
    }

    @Published var uiImageToSave: UIImage? // Added***

    struct LogoImage: Transferable {
        let image: Image
        let uiImageX: UIImage // Added***

        static var transferRepresentation: some TransferRepresentation {
            DataRepresentation(importedContentType: .image) { data in
            #if canImport(AppKit)
                guard let nsImage = NSImage(data: data) else {
                    throw TransferError.importFailed
                }
                let image = Image(nsImage: nsImage)
                return LogoImage(image: image)
            #elseif canImport(UIKit)
                guard let uiImage = UIImage(data: data) else {
                    throw TransferError.importFailed
                }
                let image = Image(uiImage: uiImage)
                return LogoImage(image: image, uiImageX: uiImage) //Updated***
            #else
                throw TransferError.importFailed
            #endif
            }
        }
    }

    @Published private(set) var imageState: ImageState = .empty

    @Published var imageSelection: PhotosPickerItem? = nil {
        didSet {
            if let imageSelection {
                let progress = loadTransferable(from: imageSelection)

                imageState = .loading(progress)
            } else {
                imageState = .empty
            }
        }
    }

    private func loadTransferable(from imageSelection: PhotosPickerItem) -> Progress {
        return imageSelection.loadTransferable(type: LogoImage.self) { result in
            DispatchQueue.main.async {
                guard imageSelection == self.imageSelection else {
                    print("mob: logoModel. Failed to get the selected item.")
                    return
                }
                switch result {
                case .success(let LogoImage?):
                    self.imageState = .success(LogoImage.image)
                    self.uiImageToSave = LogoImage.uiImageX // Added***
                case .success(nil):
                    self.imageState = .empty
                case .failure(let error):
                    self.imageState = .failure(error)
                }
            }
        }
    }

}

// Save the image
class ImageSaver: NSObject {
    var successHandler: (() -> Void)?
    var errorHandler: ((Error) ->Void)?

    //***Method to save to Disk
    //The location we want to Save.
    func writeToDisk(image: UIImage, imageName: String) {
        let savePath = FileManager.documentsDirectory.appendingPathComponent("\(imageName).jpg") //Where are I want to store my data
        if let jpegData = image.jpegData(compressionQuality: 0.5) { // I can adjust the compression quality.
            try? jpegData.write(to: savePath, options: [.atomic, .completeFileProtection])
            print("Image saved")
        }
    }
}

// Used to save the image in the documents directory
extension FileManager {
    static var documentsDirectory: URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
}

A sample ContentView to test:

import SwiftUI
import PhotosUI

struct ContentView: View {
    @ObservedObject var viewModel: LogoModel

    var body: some View {
        VStack {
            PhotosPicker(selection: $viewModel.imageSelection, matching: .images, photoLibrary: .shared()) {
                switch viewModel.imageState {
                case .empty:
                    Image(uiImage: UIImage(contentsOfFile: FileManager.documentsDirectory.appendingPathComponent("myImage.jpg").path()) ?? Image(systemName: "person.fill")
                        .font(.system(size: 40))
                        .foregroundColor(.blue) as! UIImage).resizable().scaledToFit()
                case .loading:
                    ProgressView()
                case let .success(image):
                    image.resizable().scaledToFit()
                case .failure:
                    Image(systemName: "exclamationmark.triangle.fill")
                        .font(.system(size: 40))
                        .foregroundColor(.red)
                }
            }
            Button {
                imageSaver.writeToDisk(image: viewModel.uiImageToSave!, imageName: "myImage")
            } label: {
                Text("Save Me!").bold()
            }.padding(.top, 100)
        }
    }
}

Once you tap the center image the picker will come up for you to pick your image. The save button will save your image in the DocumentDirectory with the name myImage.jpg. You can adapt as per AmitShrivastava's proposal too. Once you save the center image will persist. If you tap again, you start again, but if you don't hit save the image will change but will not persist :)

General notes: Your model looks ok, but I would keep my ImageSaver struct and extensions in separate files, but its about readability and be able to use your code in the futre.

2      

Thank you @AmitShrivastava and @Bnerd . What great guys and what a super community :) I've been learning Swift for 2 months and it can be tough going, you guys make it so much easier and effecient. Thank you!

2      

Hi @Bnerd , In fact I used some of your code from example ContentView in a HStack as follows;

                        HStack {
                            Text("Experimental: Select a logo or profile image from your photos library to display on the welcome screen.")
                                .font(.system(size: 15, weight: .regular, design: .rounded))
                            //EditableCircularLogoImage(viewModel: viewModel)
                                PhotosPicker(selection: $viewModel.imageSelection, matching: .images, photoLibrary: .shared()) {
                                    switch viewModel.imageState {
                                    case .empty:
                                        Image(uiImage: UIImage(contentsOfFile: FileManager.documentsDirectory.appendingPathComponent("mobCustomLogo.jpg").path()) ?? Image(systemName: "person.fill")
                                            .font(.system(size: 40))
                                            .foregroundColor(.blue) as! UIImage).resizable().scaledToFit()
                                    case .loading:
                                        ProgressView()
                                    case let .success(image):
                                        image.resizable().scaledToFit()
                                    case .failure:
                                        Image(systemName: "exclamationmark.triangle.fill")
                                            .font(.system(size: 40))
                                            .foregroundColor(.red)
                                    }
                                }
                                Button {
                                      ImageSaver.writeToDisk(image: viewModel.uiImageToSave!, imageName: "mobCustomLogo")
                                } label: {
                                    Text("Save Me!").bold()
                                }.padding(.top, 100)
                            }

and I get the error: Instance member 'writeToDisk' cannot be used on type 'ImageSaver'; did you mean to use a value of this type instead?

at:

                                      ImageSaver.writeToDisk(image: viewModel.uiImageToSave!, imageName: "mobCustomLogo")

Any idea why? Google is not helping.

2      

Update: I've spend a couple of hours looking at this, no solution yet but I feel it's something to do with uiImageX, or uiImageToSave.

2      

@flaneur imagines a better way to access images:

no solution yet but I feel it's something to do with uiImageX, or uiImageToSave.

Aren't you in luck?! Not sure if you follow other developers, but Stewart Lynch is just finishing up a series where he stores and retrieves images from Core Data. He also touches on retrieving images from the document's directory.

This may not answer your questions, but it seems like it's in the same cricket pitch. So you might find some nuggets.

See -> My Images Tutorial

Book some time, and please return here and tell us what you learned!

2      

@Bnerd  

I removed something by mistake in my solution... In the ContentView previously you need to iniate an instance of the ImageSaver class like this

struct ContentView: View {
    @ObservedObject var viewModel: LogoModel
    let imageSaver = ImageSaver() //Add this.

    var body: some View {

Then your Button action becomes like this:

Button {
      imageSaver.writeToDisk(image: viewModel.uiImageToSave!, imageName: "mobCustomLogo")
             }

2      

Super thanks @Bnerd. @Obelix I feel coredata is overkill for what I need to do at present. The doculent directory is ideal. I'm simply storing a profile photo. Sure I need to learn core data, it's on the to-do list. Thanks for the link.

2      

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.