0

I have an array of struct with a image. I need to save it to file for persistence. I can convert it to Image or base64.

struct p: Identifiable, Hashable {
  var image: UIImage? = nil
  var name: String = ""
}

struct pD: Codable {
  var image: String = ""   //base64
  var name: String = ""
}

var pA = [p]
var pDA = [pD]

func convertImage2base64()

What is the cheapest way to convert p to pD? I have JSON file with image and name which I import by manually building array of struct from JSON of base64 image and name.

Whole point to get to pD is so that I can JSONdecode at a snap instead of rebuilding a whole new temporary array of struct. I feel array transformation with map might make it work but I just can't figure it out.

I am currently copying individual element of array with convertImage2base64() in a loop. It seems to be too expensive of a process.

Data set isn't worth the Core Data setup.

0

2 Answers 2

1

Storing images as Base64 is very expensive, in storage space, memory, and computation. I do not recommend it unless there's a particular need. If you want a static JSON file, then I would recommend storing images in separate files (as PNGs or JPGs or whatever they are) and storing their URLs in the JSON if necessary. If all you're storing is the name and image, then I would just put them in a directory and let the name be the filename. Then the filesystem is your database. There's no need for JSON in that case. Small mounts of metadata can also be put in the filename if needed.

If you want to store everything in a single file, look at binary property lists. They can store Data directly without the wasteful conversion to Base64, and support Codeable directly.


If this is also your server protocol, as opposed to just the persistence model that you described, and you prefer to manage it as Base64-encoded JSON rather than a more efficient binary protocol, than Codable can handle that directly.

extension P: Codable {
    enum CodingKeys: String, CodingKey {
        case image, name
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encodeIfPresent(image?.pngData()?.base64EncodedString(), forKey: .image)
    }

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

Encoding this way locally for persistent storage is pretty inefficient, but as you say, iPhones can handle the computation. It's a lot of disk churn, though, if there are many small changes (which will degrade the user's flash storage, which has a finite number of writes), but it should work.

Sign up to request clarification or add additional context in comments.

1 Comment

The png in question is a simple sub 10k files. My quick test show, iPhone can handle 500 such elements easily, basically a user information with tiny image of the person. I am trying to send this data back and forth around the servers so it will be much friendly to handle a single json than to deal with file system. Trying to turn all to string and deal with type conversion during encode/decode. Looking for cheap and elegant way to encode/decode between image in array of struct.
0

This solution uses userdefault and it is not a good choice for large amounts of data.

struct p: Codable {
    var imgData: Data
    var name: String
    
    public init(img: UIImage,name: String) {
        self.imgData = img.pngData()!
        self.name = name
    }
}

Use Userdefault Helper like this to store and get data:

class UserDefaultsHandler: NSObject {
    static let shared = UserDefaultsHandler()
    let userDefaults: UserDefaults?
    
    private override init() {
        userDefaults = UserDefaults.standard
    }
    
    func set<T>(obj: T?, forKey key: String) where T: Encodable {
          guard let tempValue = obj else {
              userDefaults?.set(nil, forKey: key)
              return
          }
          let jsonEncoder = JSONEncoder()
          guard let jsonData = try? jsonEncoder.encode(tempValue) else {
              userDefaults?.set(nil, forKey: key)
              return
          }
          let json = String(data: jsonData, encoding: .utf8)
          userDefaults?.set(json, forKey: key)
      }
     
      func get<T: Decodable>(key: String, type: T.Type) -> T? {
          let jsonString = userDefaults?.object(forKey: key) as? String
          let jsonData = jsonString?.data(using: .utf8)
          let decoder = JSONDecoder()
          return try? decoder.decode(type, from: jsonData ?? Data())
      }
}

// Set an Get Example

  let arr = [p(img: img, name: "Apple"), p(img: img, name: "Mango"), p(img: img, name: "Pinapple")]
                UserDefaultsHandler.shared.set(obj: arr, forKey: "ImageArray")


 guard let arr = UserDefaultsHandler.shared.get(key: "ImageArray", type: [p].self) else { return }

2 Comments

I am trying to decode this. it looks like you can decode when the data is in UserDefaults because it is part of key data instead of Foundation type. And just grab the img data as string without any conversion. *sigh..
It saves in the userdefault because 'p' conform to 'Encodable', for that reason made that property Data instead of UIImage.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.