Retrieve an image from UserDefaults - swift

I am able to successfully save my image in user defaults as I use my app and retrieve it just fine but am having trouble figuring out how to retrieve it in a simple via init(). In my example it will returns the AppValue.avatar (my default image) but won't return the stored image. It can't find the previously stored image so it substitutes my default image it fails because I have UIImage in the published variable. I think it must have to be retrieved as data but if I change UIImage in the init() to Data Xcode is not happy.
class UserSettings: ObservableObject {
#Published var avatar: UIImage {
didSet {
/// Convert to data using .pngData() on the image so it will store. It won't take the UIImage straight up.
let pngRepresentation = avatar.pngData()
UserDefaults.standard.set(pngRepresentation, forKey: "avatar")
printSave(forKey: "avatar", value: avatar)
}
}
init() {
self.avatar = UserDefaults.standard.object(forKey: "avatar") as? UIImage ?? AppValue.avatar
}
}

It's simply a matter of keeping mental track of what type a thing is.
You saved the image as its pngData (correctly). This is not a UIImage; it is a Data. Your pngRepresentation, which is what gets saved, is a Data.
Hence when you retrieve the image and say as? UIImage, that test fails. It is not a UIImage. It's a Data.
Therefore, fetch data(forKey:) (or say as? Data instead of as? UIImage). Now you have the Data. then call UIImage.init(data:) to retrieve the image.

Related

save a image into binary data by creating a image object

In my swift code the goal is to save a image into core data. My code right now is not working. Its not the right type. The code works if it is a string but trying to save it to binary data is not working. I tried creating a UIImage object and it is not working. Core data binary is called "pic"
import UIKit;import CoreData
class ViewController: UIViewController {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let co = UIImage(named: "a.png")
let entityA = NSEntityDescription.insertNewObject(forEntityName: "Info" , into: context)
entityA.setValue(co,forKey: "pic")
}
}
I haven't worked with Core Data for quite a while, but you almost certainly want to serialize your image as Data and then save it as a BLOB (Binary Large Object).
You should be able to use the UIImage pngData() function to convert it to data.
Check out this tutorial for more details.

How can I automatically asynchronously set a new property of an existing struct after initialization?

A struct Item I am using has a property named path which contains a String of the location of an image. I'm trying to make it so that when an instance of Item is created, the image will automatically be fetched and set to a new image property.
Are extensions the way to go for this? I can't seem to figure out how to do it.
extension Item {
var image: UIImage {
if self.path == nil {
return UIImage(named: "default.png")!
}
ASYNC_CALL_HERE {
// need to do something with the image here
}
}
}
Or is there a better way?
Note: Item is not my struct.
First, I should point out that your attempt is trying to do something quite different from what you said you want to do in the first paragraph.
If we go by what you said in the first paragraph, you want a stored property that is initialised when Item is initialised. Unfortunately, you can't add stored properties in extensions, so you can't use an extension. Item is a struct, so you can't subclass it either. The only option I see is to create a wrapper class:
class ItemWrapper {
let item: Item
var image: UIImage?
init(<insert parameters of Item.init>) {
item = Item(<insert parameters of Item.init>)
if item.path == nil {
image = UIImage(named: "default.png")!
} else {
ASYNC_CALL_HERE { fetchedImage in
image = fetchedImage
}
}
}
}
However, if it doesn't have to be a stored property, we can use a similar approach as the one in your attempt. Rather than fetching the image immediately after Item is initialised, we only fetch it when the caller needs it. For this, we could use an extension to add a method called getImage. You might want to use a dictionary stored somewhere (or something else) as a cache, to simulate a stored property.
func getImage(completion: #escaping (UIImage) -> Void) {
guard let path = item.path else {
completion(UIImage(named: "default.png")!)
return
}
if let image = getImageFromCache(item.path!) {
completion(image)
return
}
ASYNC_CALL_HERE { fetchedImage in
completion(fetchedImage)
storeImageInCache(fetchedImage, path)
}
}

downloading and caching images from url asynchronously

I'm trying to download images from my firebase database and load them into collectionviewcells. The images download, however I am having trouble having them all download and load asynchronously.
Currently when I run my code the last image downloaded loads. However, if I update my database the collection view updates and the new last user profile image also loads in but the remainder are missing.
I'd prefer to not use a 3rd party library so any resources or suggestions would be greatly appreciated.
Here's the code that handles the downloading:
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
// checks cache
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//download
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
//error handling
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
I believe the solution lies somewhere in reloading the collectionview I just don't know where exactly to do it.
Any suggestions?
EDIT:
Here is where the function is being called; my cellForItem at indexpath
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell
let user = users[indexPath.row]
cell.nameLabel.text = user.name
if let profileImageUrl = user.profileImageUrl {
cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl)
}
return cell
}
The only other thing that I believe could possibly affect the images loading is this function I use to download the user data, which is called in viewDidLoad, however all the other data downloads correctly.
func fetchUser(){
Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = User()
user.setValuesForKeys(dictionary)
self.users.append(user)
print(self.users.count)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
}, withCancel: nil)
}
Current Behavior:
As for the current behavior the last cell is the only cell that displays the downloaded profile image; if there are 5 cells, the 5th is the only one that displays a profile image. Also when I update the database, ie register a new user into it, the collectionview updates and displays the newly registered user correctly with their profile image in addition to the old last cell that downloaded it's image properly. The rest however, remain without profile images.
I know you found your problem and it was unrelated to the above code, yet I still have an observation. Specifically, your asynchronous requests will carry on, even if the cell (and therefore the image view) have been subsequently reused for another index path. This results in two problems:
If you quickly scroll to the 100th row, you are going to have to wait for the images for the first 99 rows to be retrieved before you see the images for the visible cells. This can result in really long delays before images start popping in.
If that cell for the 100th row was reused several times (e.g. for row 0, for row 9, for row 18, etc.), you may see the image appear to flicker from one image to the next until you get to the image retrieval for the 100th row.
Now, you might not immediately notice either of these are problems because they will only manifest themselves when the image retrieval has a hard time keeping up with the user's scrolling (the combination of slow network and fast scrolling). As an aside, you should always test your app using the network link conditioner, which can simulate poor connections, which makes it easier to manifest these bugs.
Anyway, the solution is to keep track of (a) the current URLSessionTask associated with the last request; and (b) the current URL being requested. You can then (a) when starting a new request, make sure to cancel any prior request; and (b) when updating the image view, make sure the URL associated with the image matches what the current URL is.
The trick, though, is when writing an extension, you cannot just add new stored properties. So you have to use the associated object API to associate these two new stored values with the UIImageView object. I personally wrap this associated value API with a computed property, so that the code for retrieving the images does not get too buried with this sort of stuff. Anyway, that yields:
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// allow supplying of `nil` to remove old image and then return immediately
guard let urlString = urlString else { return }
// check cache
if let cachedImage = ImageCache.shared.image(forKey: urlString) {
self.image = cachedImage
return
}
// download
let url = URL(string: urlString)!
currentURL = url
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.currentTask = nil
// error handling
if let error = error {
// don't bother reporting cancelation errors
if (error as? URLError)?.code == .cancelled {
return
}
print(error)
return
}
guard let data = data, let downloadedImage = UIImage(data: data) else {
print("unable to extract image")
return
}
ImageCache.shared.save(image: downloadedImage, forKey: urlString)
if url == self?.currentURL {
DispatchQueue.main.async {
self?.image = downloadedImage
}
}
}
// save and start new task
currentTask = task
task.resume()
}
}
Also, note that you were referencing some imageCache variable (a global?). I would suggest an image cache singleton, which, in addition to offering the basic caching mechanism, also observes memory warnings and purges itself in memory pressure situations:
class ImageCache {
private let cache = NSCache<NSString, UIImage>()
private var observer: NSObjectProtocol?
static let shared = ImageCache()
private init() {
// make sure to purge cache on memory pressure
observer = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func save(image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
A bigger, more architectural, observation: One really should decouple the image retrieval from the image view. Imagine you have a table where you have a dozen cells using the same image. Do you really want to retrieve the same image a dozen times just because the second image view scrolled into view before the first one finished its retrieval? No.
Also, what if you wanted to retrieve the image outside of the context of an image view? Perhaps a button? Or perhaps for some other reason, such as to download images to store in the user’s photos library. There are tons of possible image interactions above and beyond image views.
Bottom line, fetching images is not a method of an image view, but rather a generalized mechanism of which an image view would like to avail itself. An asynchronous image retrieval/caching mechanism should generally be incorporated in a separate “image manager” object. It can then detect redundant requests and be used from contexts other than an image view.
As you can see, the asynchronous retrieval and caching is starting to get a little more complicated, and this is why we generally advise considering established asynchronous image retrieval mechanisms like AlamofireImage or Kingfisher or SDWebImage. These guys have spent a lot of time tackling the above issues, and others, and are reasonably robust. But if you are going to “roll your own,” I would suggest something like the above at a bare minimum.

Proper way to create models on the server

Consider the app don't store images within models. Instead, it stores URLs, which are used to retrieve images from image cache.
Consider this model:
class Post {
let id: String
let title: String
let imageUrl: URL
}
Let's say the user creates a new post (enters title and selects an image) and the app sends it over to the server.
Question: what is a proper way to write a method which creates a new post on the server?
I was thinking to write something like:
func createModel(_ model: Post, completion: () -> Void)
but the problem is the app doesn't know imageUrl yet.
Another thought was to have a method accepting all the properties of the model:
func createPost(_ title: String, image: UIImage, completion: () -> Void)
but this solution is not reusable: it's impossible to have a generic method for creating a model. Not good.
Also, this approach doesn't allow us to use the model while it's not saved to the backend yet, forcing to show the activity indicator.
Any suggestions how to deal with that?
If you really need immutable model (which I think you don't) then you can choose to save images in app's temporary directory and use the local images for all the newly created posts and server images for all other posts. This way your model will stay the same and you'll get rid of any activity indicator.
class Post {
let id: String
let title: String
var imageUrl: URL
init(id:String,title:String,imageUrl:URL){
self.id = id
self.title = title
self.imageUrl = imageUrl
}
init(id:String,title:String,image:UIImage){
self.id = id
self.title = title
let data = UIImageJPEGRepresentation(image, 0.8)
let url = URL(fileURLWithPath: "\(NSTemporaryDirectory())\(UUID().uuidString).jpeg")
try! data?.write(to: url, options: Data.WritingOptions.atomic)
self.imageUrl = url
}
}

Getting Image from URL for UIView [Swift]

Alright so I've made some handlers and classes to grab data from a URL, its all returning fine, I've checked the URLs are valid and everything.
Anyways, I'm trying to do an NSData(contentsOfURL) on a stored URL in my class for a UIViewController. I'm successfully printing out String Variables like name, type, description, but I'm having difficulty displaying an UIImage into a Image View on the ViewController.
Here is my Code, it's run when the View loads:
func configureView() {
// Update the user interface for the detail item.
if let card = detailCard {
title = card.cardTitle
//Assign Elements
detailDescriptionLabel?.text = card.description
typeLabel?.text = card.cardType
cardTitleLabel?.text = card.cardTitle
costLabel?.text = card.cardCost
print(card.cardImageURL)
if let url = NSURL(string: card.cardImageURL){
let data = NSData(contentsOfURL: url)
imageView.image = UIImage(data: data!)// <--- ERROR HERE: EXC_BAD_INSTRUCTION
}
//Print Log
//print(card.logDescription)
}
}
Like the comment says above, I get the error on the imageView.image = UIImage(data: data!) line:
Thread 1: EXC_BAD_INSTRUCTION
Here is the code for cardImageURL:
var cardImageURL: String {
return String(format: "http://testyard.example.net/display/images/cards/main/%#", image)
}
//This is a computed variable when a "Card" class object is created.
It returns the correct url in a string:
http://testyard.example.net/display/images/cards/main/armor-of-testing.jpg
I've used this code elsewhere, and it worked fine, why is it throwing errors here?
Thanks in advance.
Are you sure the data being downloaded is an image? You should probably use this line:
if let image = UIImage(data: data)`
This should stop the error, though you might need to test that your data actually contains an image.