Load image from Firestore and save it to cache in Swift - ios

I'm working on an app that has to load some images and data from server on every launch (to make sure it's using up-to-date info). I'm using Firestore as a DB and currently storing images in it as an URL to Firebase storage.
Is it somehow possible to store an actual image in Firestore? And how can I cache loaded image? Either from
UIImage(contentsOf: URL)
or from Firestore?

Try this Asynchronous image downloader with cache support as a UIImageView category - http://cocoadocs.org/docsets/SDWebImage
It is called sdwebimage really easy to use

I don't know if that's the most efficient way of solving my problem but I did it the following way:
In my Firestore DB I stored references to images in Cloud Storage. Then when app starts for the first time, it loads those files from Firestore DB using default methods AND saves those images in app's container (Documents folder) using Swift's FileManager().
Next time the app starts, it goes through references array and skips the files which are already in app's container.

You could use the bytes type in Firestore (see a list of types) to save whatever binary data you want (use NSData on iOS), but this is almost certainly not what you actually want to do. The limit for the size of an entire document is 1 MB, and images can easily exceed that. Also, you'll be paying the cost of downloading that image to the client any time that document is read, which could be wasteful.
You'll be far better off storing the actual file data in Cloud Storage (using the Firebase SDK on the client), then storing a reference or URL to that in the document, and fetch it from there only when needed.

You could use https://github.com/pinterest/PINRemoteImage, this framework use https://github.com/pinterest/PINCache
import PINRemoteImage
extension UIImageView {
public func setImageFrom(urlString: String!, animated: Bool = false) {
guard let urlString = urlString else {
return
}
guard let url = URL(string: urlString) else {
return
}
layer.removeAllAnimations()
pin_cancelImageDownload()
image = nil
if !animated {
pin_setImage(from: url)
} else {
pin_setImage(from: url, completion: { [weak self] result in
guard let _self = self else { return }
_self.alpha = 0
UIView.transition(with: _self, duration: 0.5, options: [], animations: { () -> Void in
_self.image = result.image
_self.alpha = 1
}, completion: nil)
})
}
}
}
....
UIImageView(). setImageFrom(urlString: "https://ssssss")

Related

Export all images from Core Data SwiftUI

I'm trying to build out a simple way for my users to export their data outside of the app.. nothing that needs to be imported back in, just some way for them to back up the data for reference purposes. I have a Core Data Entity Project and the users are able to individually share a project in order to save the project data and images using the standard iOS Share Sheet. Works great.
However I'd like there to be a solution to export everything at once, not just individual projects one at a time.
I have part of it working, where I can export the data from Core Data (that isn't an image) into a CSV for users to reference. However I'm stuck on finding the best way to get all the Images exported in a similar singular button. Allowing the user to pick a location where a Folder would be created containing the images would be ideal.
Here's my code for the CSV export which works great:
func exportCSV() {
let fileName = "Metadata_Export_\(Date()).csv"
let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
var csvText = "Name,Date,Project_Description\n"
for project in projects {
csvText += "\(project.person?.name ?? "-"),\(project.date ?? Date()),\(project.bodyText ?? "-"),\n"
}
do {
try csvText.write(to: path!, atomically: true, encoding: String.Encoding.utf8)
} catch {
print("Failed to create file")
print("\(error)")
}
print(path ?? "not found")
var filesToShare = [Any]()
filesToShare.append(path!)
let av = UIActivityViewController(activityItems: filesToShare, applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
isShareSheetShowing.toggle()
}
Now I just need to get the Images exported out. Images are saved in Core Data as Binary objects, and will be written to File if they're larger than 128kb (and therefore written to blob in CD if less than 128kb).
The images are stored in CD as Optionals, project.image1, project.image2, project.image3, and project.image4
I've looked at examples using fileManager and other solutions, but I'm not sure on the correct approach to pursue since many of those are actually alternatives to saving images in Core Data - not necessarily configuring user interaction for picking where to export images.
Can the above exportCSV function be adapted to a similar result for the project's images? My app supports iOS 14 and later, if that makes a difference. Thanks for any suggestions/direction!
=== UPDATE ===
I've discovered fileExporter() which seems like a promising solution. I've been able to implement a simple POC of this method by exporting an Image I have stored in my Assets folder. Has anyone used this method to achieve exporting all images out of Core Data?
I can add the modifier to my view:
.fileExporter(isPresented: $exportFile, documents: [
ImageDocument(image: UIImage(named: "testimage"))
],
contentType: .png, onCompletion: { (result) in
if case .success = result {
print("Success")
} else {
print("Failure")
}
})
}
Using an ImageDocument Struct as follows:
struct ImageDocument: FileDocument {
static var readableContentTypes: [UTType] { [.jpeg, .png, .tiff] }
var image: UIImage
init(image: UIImage?) {
self.image = image ?? UIImage()
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let image = UIImage(data: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.image = image
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
// You can replace tiff representation with what you want to export
return FileWrapper(regularFileWithContents: image.jpegData(compressionQuality: 1)!)
}
}
So how can I have it include an Array of all images?

Does showing system image(SF symbols) use a networking call in Swift?

I'm creating an application in ios where I load images from an api using a UITableView and UITableViewCell.
Since the UITableView reuses cells, old images were appearing when I scroll fast. In order to prevent this, I set a default image using a system image(SF symbols).
I also use a cache to store urls to images.
Everything works as it should but now I think of it I'm sending a network request to retrieve that systemImage each time which seems incredibly inefficient since I was using a cache in order to reduce the total network calls in the first place.
Is there way around this or is this a tradeoff I must make?
Code is below.
//use default image from SF symbols
let defaulticon = UIImage(systemName: "photo")?.withTintColor(.gray, renderingMode: .alwaysOriginal)
DispatchQueue.main.async {
cell.mealImage.image = defaulticon
}
guard cell.meal?.strMealThumb != nil else {
print("Category Image doesn't exist")
return
}
//use cache
if let imageData = model.imagecache.object(forKey: cell.meal!.strMealThumb as NSString) {
print("using cache")
DispatchQueue.main.async {
cell.mealImage.image = imageData
}
}
else {
let url = URL(string: cell.meal!.strMealThumb)
let session = URLSession.shared.dataTask(with: url!) { data, response, error in
if error == nil && data != nil {
let image = UIImage(data: data!)
//self.model.imagecache[cell.meal!.strMealThumb] = image
self.model.imagecache.setObject(image!, forKey: cell.meal!.strMealThumb as NSString)
DispatchQueue.main.async {
cell.mealImage.image = image
}
}
}
session.resume()
}
}
Override prepareForReuse method in UITableViewCell and add code in this function to clean up unrequited data that could persist from previous usage of the cell. In your example assign the default image in this function to produce better result.
You asked:
I set a default image using a system image(SF symbols).
...
Everything works as it should but now I think of it I'm sending a network request to retrieve that systemImage each time which seems incredibly inefficient since I was using a cache in order to reduce the total network calls in the first place.
No, UIImage(systemName:) does not make a network request. And it caches the image, itself, as the documentation says:
This method checks the system caches for an image with the specified name and returns the variant of that image that is best suited for the main screen. If a matching image object is not already in the cache, this method creates the image from the specified system symbol image. The system may purge cached image data at any time to free up memory. Purging occurs only for images that are in the cache but are not currently being used.
FWIW, you can empirically verify that this does not perform a network request disconnecting from the network and trying to use it. You will see it works fine, even when disconnected.
FWIW, there is a very small performance gain (less than a millisecond?) by keeping a reference to that tinted system image and reusing it, rather than fetching the cached system image and re-tinting it. But the performance improvement is negligible.

PHFetchRequest returns nil data mixed with optional data for images?

I am learning how to use the PHFetchRequest to get images from the user's photo library and then display them in a scroll view for a custom image picker but I am getting mostly nil returned data and some returned as optional data that can be un-wrapped.
I back up all my photos on ICloud, could this be the reason I am getting nil??
Below is the function within my struct that fetches and appends the data to an empty array variable...
What am I doing wrong? Thanks guys!
XCode log showing nil and optional returns
func getAllImages() {
let request = PHAsset.fetchAssets(with: .image, options: .none)
DispatchQueue.global(qos: .background).async {
let options = PHImageRequestOptions()
options.isSynchronous = true
request.enumerateObjects { (asset, _, _ ) in
PHCachingImageManager.default().requestImage(for: asset, targetSize: .init(), contentMode: .default, options: options) { (image, _) in
print(image?.pngData())
// I had to coalesce with an empty UIImage I made as an extension
let data1 = Images(image: (image ?? UIImage.emptyImage(with: CGSize(width: 10, height: 10)))!, selected: false)
self.data.append(data1)
}
}
if request.count == self.data.count {
self.getGrid()
}
}
}
You are seeing nil data because the image resource is in cloud and not on your phone. Optional is expected because it is not guaranteed to exist on your phone (like it's in cloud in your case).
You can instruct fetch request to fetch the photo from cloud (if needed) using PHImageRequestOptions.isNetworkAccessAllowed option. This is false by default.
A Boolean value that specifies whether Photos can download the requested image from iCloud.
Discussion
If true, and the requested image is not stored on the local device, Photos downloads the image from iCloud. To be notified of the download’s progress, use the progressHandler property to provide a block that Photos calls periodically while downloading the image.
If false (the default), and the image is not on the local device, the PHImageResultIsInCloudKey value in the result handler’s info dictionary indicates that the image is not available unless you enable network access.
progressHandler Notes
You don't need to implement this to allow iCloud photo downloads. The iCloud photo downloads will work without this as well.
In case you plan to implement this to show progress on UI, you should keep in mind that these calls are fired multiple times for one download. So you can't consider your photo download to be complete upon first call in progressHandler callback.
Discussion
If you request an image whose data is not on the local device, and you have enabled downloading with the isNetworkAccessAllowed property, Photos calls your block periodically to report progress and to allow you to cancel the download.

Extensive memory usage when uploading assets (images, videos) to firebase in Swift?

Suppose I have an array of UIImage called photos, they are to be uploaded to Firebase storage. I wish to do the following things:
Upload them to Firebase storage
Get paths of the uploaded photos and store in an array called uploadedAssets (paths, not download url, it looks like this: "photos/folder_name/photo_id"), where "folder_name" is randomly generated and "photo_id" is an integer, representing the order of photos
Call Cloud Function and pass uploadedAssets to it. The server then uses the paths to find all pictures and generates a thumbnail for each one.
Finally, store the original photos' download urls and thumbnails' download urls in database.
I have something that's working, but uses too much memory (300+MB when uploading only 4 pictures):
// Swift
let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue.init(label: "AssetQueue")
var uploadedAssets = [String]()
let folderName: String = UUID().uuidString
dispatchQueue.async {
for i in 0..<photos.count {
dispatchGroup.enter()
let photo: UIImage = photos[i]
let fileName: String = "\(folderName)/\(i)"
let assetRef = Storage.storage().reference().child("photos/\(fileName)")
let metaData = StorageMetaData()
metaData.contentType = "image/jpg"
if let dataToUpload = UIImageJPEGRepresentation(photo, 0.75) {
assetRef.putData(
dataToUpload,
metaData: metaData,
completion: { (_, error) in
uploadedAssets.append("photos/\(fileName)")
dispatchGroup.leave()
}
)
}
}
}
dispatchGroup.notify(queue: dispatchQueue) {
Alamofire.request(
"https://<some_url>",
method: .post,
parameters: [
"uploadedAssets": uploadedAssets
]
)
}
And the code that generates thumbnails runs on server side, therefore, in my opinion, is irrelevant, I won't post it here. So, the above code snippet consumes 300+MB of memory when there are 4 photos to upload. After successfully uploaded those photos, the memory usage stays at 300+MB and never drops. When I try to upload more, say another 4 photos, it could even go up to 450+MB. I know that's not normal, but can't seem to figure out why this would happen?

Adding image from Firebase to UITableViewCell

I want to retrieve the image that is stored in the storage of an user and place it next to his name in a custom UITableViewCell. The problem now is that the tableview will load when the images aren't done downloading (I think?), causing the application to crash because the image array is nil. So what is the correct way to load the tableview? I think, for the user experience, it is important that the tableviewcell image should be shown even if the images aren't done downloading, and present them a default image that is saved in the assists. I thought about making an array with UIImages that links to the default asset of loading an image and changing the image to the profile picture when it is done downloading. But I really have no clue how to do that. This is what I got so far about downloading the image:
let storage = FIRStorage.storage()
let storageRef = storage.reference(forURL: "link.appspot.com")
channelRef?.observeSingleEvent(of: .value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject]{
for each in snapDict{
let UIDs = each.value["userID"] as? String
if let allUIDS = UIDs{
let profilePicRef = storageRef.child((allUIDS)+"/profile_picture.png")
profilePicRef.data(withMaxSize: 1 * 500 * 500) { data, error in
if let error = error {
}
if (data != nil)
{
self.playerImages.append(UIImage (data: data!)!)
}
}
}
let userNames = each.value["username"] as? String
if let users = userNames{
self.players.append(users)
}
}
}
self.tableView.reloadData()
})
This is in the cellForRow
cell.playersImage.image = playerImages[indexPath.row] as UIImage
My rules, haven't changed it from the default rules:
service firebase.storage {
match /b/omega-towers-f5beb.appspot.com/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
Thank you.
Regarding user experience, you are correct. It is standard to have some sort of default image when loading an image from a URL. A great library to use for image caching and using default assets in its' place is AlamofireImage
Vandan Patel's answer is correct in saying you need to ensure your array is not nil when loading the tableview. You will be given a completion block to handle any extra work you would like to do with your image, using the AlamofireImage library.
This is all assuming you are getting a correct image URL back for your Firebase users.
You should call tableView.reloadData() when the images are done downloading. One important thing, initialize your playerImages as playerImages = [UIImage]() instead of playerImages: [UIImage]!. if it's empty, it wouldn't show your array is nil.
Update:
if let players = playerImages {
//code
}

Resources