Requirement - I have a requirement in which I am receiving a JSON dictionary from which I am retrieving an array of images and content text. Then I have to display all the images with corresponding contents in a collection view.
Update - Above all I need to calculate the cell size based on image size scaled to the a constant width for which I fell that(may not be correct) I need all images to be downloaded completely then reload collection view
Problem - But the problem is that when I download the images in background thread and populate in separate arrays.Then the image cannot be added in the same order as they were in the JSON Dictionary since I am downloading them in a concurrent queue.
My Solution - So I thought of downloading them by putting everything in a serial queue which has made my retrieving data very slow. What can be an efficient alternative for this?
Code -
let serialQueue = dispatch_queue_create("my serial queue", nil)
dispatch_async(serialQueue, {
print("This is first Method")
for var i=0;i<self.resultArr.count;i++//resultArr is my array of data's in the jsonDic
{
sleep(2)
print(self.resultArr[i].valueForKey("profile_pic")! as! String)
if self.resultArr[i].valueForKey("profile_pic")! as! String != "Null" && self.resultArr[i].valueForKey("profile_pic")! as! String != "null" && self.resultArr[i].valueForKey("profile_pic")! as! String != "NULL" && self.resultArr[i].valueForKey("profile_pic")! as! String != ""
{
let imageUrl = UrlClass.imageUrlWithoutExtension + String(self.resultArr[i].valueForKey("profile_pic")!)
print(imageUrl)
let url = NSURL(string: imageUrl)
let imageData = NSData(contentsOfURL: url!)
self.contentlabelArr.insertObject(String(self.resultArr[i].valueForKey("content")!), atIndex: i)
if imageData != nil && imageData?.length > 0
{
print("this is \(i) image")
print(UIImage(data: imageData!))
self.imageArr.insertObject(UIImage(data: imageData!)!, atIndex: i)
}
else
{
print("\(i) image has nill")
self.imageArr.insertObject(UIImage(named: "logo.png")!, atIndex: i)
}
}
else
{
print("\(i) image has nill")
self.contentlabelArr.insertObject(String(self.resultArr[i].valueForKey("content")!), atIndex: i)
self.imageArr.insertObject(UIImage(named: "logo.png")!, atIndex: i)
}
print("\(i) times 5 is \(i * 5)")
if self.imageArr.count==self.resultArr.count
{
print(self.resultArr.count)
print(self.imageArr.count)
dispatch_async(dispatch_get_main_queue(),
{
print(self.resultArr.count)
print(self.imageArr.count)
print(self.imageArr)
print(self.contentlabelArr)
self.collectionView?.reloadData()
})
}
A more efficient way would be to create a data model object which will represent you image link and the optional UIImage. Something like this:
class NetworkImage {
let imageURL: String!
let image: UIImage?
}
Now when you receive your JSON with image links array, you can create your data model array, which will respect the order:
let dataModel: [NetworkImage]
So when you will retrieve your images asynchronously, you can update your dataModel with your image, so no order will be affected.
The idea can be evolved suiting your needs.
You should never use sync operations for this kind of jobs.
you definitely can keep the order if you use a concurrent queue. i think your code as it stands pretty much doesnt use the queue correctly at all (and why is there a sleep(2)?) your concurrent queue should be inside the forloop so it can fire off the different blocks at the same time, and they will use the correct index of the for loop that was assigned to them to place the resulting image in the correct array location
let sema = dispatch_semaphore_create(2); //depending how many downloads you want to go at once
for i in 0..<self.resultArr.count {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
//download images here, order of execution will not be guaranteed, but after they are finished, they will always put the images in the array at 'i' so it doesnt matter
dispatch_semaphore_signal(sema);
})
}
You may play around with this sample solution, utilising dispatch groups:
//: Playground - noun: a place where people can play
import UIKit
import Dispatch
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
class Record {
init(text: String, imageURL: String) {
self.text = text
self.imageURL = imageURL
self.image = nil
}
var text: String
var imageURL: String
var image: String?
}
extension Record: CustomStringConvertible {
var description: String {
return "text: \(text), imageURL: \(imageURL), image: \(image)"
}
}
// Fetch text and image url, but no image.
func fetchRecords(completion: ([Record]?, ErrorType?) -> ()) {
let delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC)))
dispatch_after(delayInNanoSeconds, dispatch_get_global_queue(0, 0)) {
let result: [Record] = [
Record(text: "Aaa", imageURL: "path/image1"),
Record(text: "Bbb", imageURL: "path/image2"),
Record(text: "Ccc", imageURL: "path/image3")
]
completion(result, nil)
}
}
// fetch an image
func fetchImage(url: String, completion: (String?, ErrorType?) -> () ) {
let delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC)))
dispatch_after(delayInNanoSeconds, dispatch_get_global_queue(0, 0)) {
let image = url
completion(image, nil)
}
}
// Put everything together:
// 1) Fetch an array of records, omitting the image
// 2) When this is finished, in parallel, for each record
// fetch each image.
// 3) When all is finished, call the completion handler containing
// the records including the images
func fetchRecordsWithImages(completion: ([Record]?, ErrorType?) -> () ) {
fetchRecords { (result, error) in
if let records = result {
let grp = dispatch_group_create()
records.forEach { record in
dispatch_group_enter(grp)
fetchImage(record.imageURL) { (image, error) in
if let image = image {
record.image = image
}
dispatch_group_leave(grp)
}
}
dispatch_group_notify(grp, dispatch_get_global_queue(0, 0)) {
completion(records, nil)
}
}
}
}
fetchRecordsWithImages() { (records, error) in
if let records = records {
print("Records: \(records)")
}
}
Console:
Records: [text: Aaa, imageURL: path/image1, image: Optional("path/image1"), text: Bbb, imageURL: path/image2, image: Optional("path/image2"), text: Ccc, imageURL: path/image3, image: Optional("path/image3")]
Related
I've received an array object from the server, and then I want to download images with one property on that object. then I want to update UI with array objects and images (view model). I'm downloading images on a background thread, but I'm getting images with delay and the object doesn't fills at all, whats I'm doing wrong?
func presentCoinse(_ list: Home.Models.CoinseListResponse) {
var coins = [Home.Models.coinsViewModel]()
for item in list {
getImage(symbol: item.symbol) { image in
let i = Home.Models.coinsViewModel(image: image,
symbol: item.symbol,
name: item.name,
buyPrice: item.buyPrice,
sellPrice: item.sellPrice,
change24Hource: item.symbol)
coins.append(i)
}
}
viewController?.displayCoinsList(viewModel: coins)
}
private func getImage(symbol: String, complation: #escaping(_ image: Data?) -> Void) {
queue.async {
if let url = URL(string: "\(CDN_URL)\(symbol).png") {
let data = try? Data(contentsOf: url)
DispatchQueue.main.async {
complation(data)
}
}
}
}
The getImage method contains async operations, in this situation the closure will be called after the return of the method.
And, the method displayCoinsList(viewModel:) is called before every async getImage.
You can use the DispatchGroup class.
func presentCoinse(_ list: Home.Models.CoinseListResponse) {
var coins = [Home.Models.coinsViewModel]()
let dispatchGroup = DispatchGroup() // add a dispatch group
for item in list {
dispatchGroup.enter() // increment group counter before async call
getImage(symbol: item.symbol) { image in
defer {
dispatchGroup.leave() // decrease group counter in every condition with 'defer'
}
let i = Home.Models.coinsViewModel(image: image,
symbol: item.symbol,
name: item.name,
buyPrice: item.buyPrice,
sellPrice: item.sellPrice,
change24Hource: item.symbol)
coins.append(i)
}
}
dispatchGroup.notify(queue: .main) {
// wait all async calls are completed
viewController?.displayCoinsList(viewModel: coins)
}
}
private func getImage(symbol: String, complation: #escaping(_ image: Data?) -> Void) {
queue.async {
if let url = URL(string: "\(CDN_URL)\(symbol).png") {
let data = try? Data(contentsOf: url)
complation(data)
} else {
complation(nil)
}
}
}
NOTE: In getImage Method is required to call complation In every condition. If you forgot to call it the DispatchGroup can’t receive a notify and you’ll be blocked.
Pay attention that you're setting coins to empty list, and then you call displayCoinstList with that empty list. the list is appened and updated asyncronically.
You should trigger the view controller to reload the coins list when it's ready. I'd make the coins list prior going to fetecg the images, and then upon receiving the images - the UIImageView will render itself.
func presentCoinse(_ list: Home.Models.CoinseListResponse) {
// async part srarts here
var coins = [Home.Models.coinsViewModel]()
for item in list {
getImage(symbol: item.symbol) { image in
let i = Home.Models.coinsViewModel(image: image,
symbol: item.symbol,
name: item.name,
buyPrice: item.buyPrice,
sellPrice: item.sellPrice,
change24Hource: item.symbol)
coins.append(i)
}
}
// async part ends here
viewController?.displayCoinsList(viewModel: coins)
}
What you did is equivalent to:
func presentCoinse(_ list: Home.Models.CoinseListResponse) {
var coins = [Home.Models.coinsViewModel]()
viewController?.displayCoinsList(viewModel: coins) // coins in empty at this point)
for item in list {
getImage(symbol: item.symbol) { image in
let i = Home.Models.coinsViewModel(image: image,
symbol: item.symbol,
name: item.name,
buyPrice: item.buyPrice,
sellPrice: item.sellPrice,
change24Hource: item.symbol)
coins.append(i)
}
}
}
I have some code that reads data from Firebase on a custom loading screen that I only want to segue once all of the data in the collection has been read (I know beforehand that there won't be more than 10 or 15 data entries to read, and I'm checking to make sure the user has an internet connection). I have a loading animation I'd like to implement that is started by calling activityIndicatorView.startAnimating() and stopped by calling activityIndicatorView.stopAnimating(). I'm not sure where to place these or the perform segue function in relation to the data retrieval function. Any help is appreciated!
let db = Firestore.firestore()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else{
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
}
You don't need to know the progress of the read as such, just when it starts and when it is complete, so that you can start and stop your activity view.
The read starts when you call getDocuments.
The read is complete after the for loop in the getDocuments completion closure.
So:
let db = Firestore.firestore()
activityIndicatorView.startAnimating()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else {
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
DispatchQueue.main.async {
activityIndicatorView.stopAnimating()
}
}
As a matter of style, having multiple arrays with associate data is a bit of a code smell. Rather you should create a struct with the relevant properties and create a single array of instances of this struct.
You should also avoid force unwrapping.
struct PackageInfo {
let id: String
let name: String
let imageId: String
let radius: String
}
...
var packages:[PackageInfo] = []
...
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else if let documents = snapshot?.documents {
self.packages = documents.compactMap { doc in
if let title = doc.get("title") as? String,
let imageId = doc.get("imgID") as? String,
let radius = doc.get("radius") as? String {
return PackageInfo(id: doc.documentID, name: title, imageId: imageId, radius: radius)
} else {
return nil
}
}
}
There is no progress reporting within a single read operation, either it's pending or it's completed.
If you want more granular reporting, you can implement pagination yourself so that you know how many items you've already read. If you want to show progress against the total, this means you will also need to track the total count yourself though.
So I have an array of images I've accessed from my xcassets for demonstration purposes. There are 150 images I'm trying to save to my parse server at one time using parse frameworks. Here is the code I have so far. The problem I have is my app cpu goes to 100% in the tests and drops to 0. Also the images aren't saving to parse. I was hoping someone could help me find an efficient way to save 150 images to parse.
var imageNameList: [String] {
var imageNameList2:[String] = [] //[NSMutableArray]()
for i in 0...149 {
let imageName = String(format: "pic_%03d", Int(i))
imageNameList2.append(imageName)
}
return imageNameList2
}
#IBAction func Continue(_ sender: Any) {
for imageName in imageNameList {
var objectForSave:PFObject = PFObject(className: "Clo")
let object:UIImage = UIImage(named: imageName)!
let tilesPF = imageNameList.map({ name in
let data = UIImagePNGRepresentation(object as! UIImage)!
let file = PFFile(data: data)
let tile = PFObject(className: "Tile")
tile["tile"] = file
})
objectForSave["tiles"] = tilesPF
objectForSave.saveInBackground(block: { responseObject, error in
//you'll want to save the object ID of the PFObject if you want to retrieve a specific image later
})
}
}
The trouble is that the tight for-loop launches all of those requests concurrently causing some part of the http stack to bottleneck.
Instead, run the requests sequentially as follows (in my best approximation of Swift)...
func doOne(imageName: String, completion: (success: Bool)->()) {
var objectForSave:PFObject = PFObject(className: "Clo")
let object:UIImage = UIImage(named: imageName)!
// ... OP code that forms the request
objectForSave.saveInBackground(block: { responseObject, error in
success(error == nil)
})
}
func doMany(imageNames: Array<String>, completion: (success: Bool)->()) {
if (imageNames.count == 0) return completion(YES)
let nextName = imageNames[0];
self.doOne(imageName:imageNames[0] completion: {(success: Bool) -> Void in
if (success) {
let remainingNames = imageNames[1..imageNames.count-1]
self.doMany(imageNames: remainingNames completion:completion)
} else {
completion(NO)
})
}
In English, just in case I goofed the Swift, the idea is to factor out a single request into it's own function with a completion handler. Build a second function that takes an array of arguments to the network request, and use that array like a to-do list: do the first item on the list, when it completes, call itself recursively to do the remaining items.
I have a data structure that I have created that caches items using a combination of a Dictionary and a Queue. The idea is that items are added to the "Cache" and once it reaches its limit, the oldest item in the cache is removed:
struct Cache<A: Hashable, B> {
fileprivate var theCache = [A:B]()
fileprivate var keyQueue = Queue<A>()
fileprivate var maxItems: Int!
init(maxItems: Int) {
self.maxItems = maxItems
}
subscript(key : A?) -> B? {
get {
if key != nil {
return theCache[key!]
}
return nil
}
set(newValue) {
// If value being added to cache, check that the max no. of items isn't exceeded.
// If it is then remove the first item of the cache queue and add value to queue.
if key != nil {
if (theCache.count + 1 > maxItems) {
removeFirstItem()
}
addValue(key!, value: newValue)
}
}
}
/**
Tells us how many items there are in the cache
- returns: The count of the cache
*/
var count: Int {
return theCache.count
}
mutating func clear() {
theCache.removeAll()
for _ in 0..<keyQueue.count {
_ = keyQueue.dequeue()
}
}
fileprivate mutating func addValue(_ key: A, value: B?) {
theCache[key] = value // Memory leak 2
keyQueue.enqueue(key)
}
fileprivate mutating func removeFirstItem() {
let firstItemKey = keyQueue.dequeue()
if firstItemKey != nil {
theCache.removeValue(forKey: firstItemKey!)
}
}
}
I'm using this to cache UIImages in a UICollectionView, and have a strong reference to this cache from my model class.
Inside my cellForItemAt method, I have the following code to check for cached images. If there is a cached image then it adds that to the UICollectionCell otherwise it downloads it, adds it to the cache, then adds it to the UICollectionCell:
if let cellImage = model.imageCache[urlString] {
cell.imageView.image = cellImage
}
else {
model.downloadImage(
urlString,
completion: { [weak self] (error, image) -> () in
if let strongSelf = self {
if error == nil {
// Store the image in to our cache
strongSelf.model.imageCache[urlString] = image
// Update the cell with our image
if let cellToUpdate = collectionView.cellForItem(at: indexPath) as? MyCollectionCell {
cellToUpdate.imageView.image = image
}
}
else {
print("Error downloading image: \(error!.localizedDescription)")
}
}
}
)
}
However, according to Xcode 8 this whole process is causing a memory leak for the UIImages. Can anyone help me out with where the memory leak is occurring and how I can prevent it?
EDIT
Here's my downloadImage function. I've also added "Memory leak 1" and "Memory leak 2" as comments next to my code to show the backtrace of the memory leak xcode shows me:
Alamofire.request(urlString, method: .get)
.authenticate(usingCredential: credentials!)
.validate()
.downloadProgress { bytesRead, totalBytesRead, totalBytesExpectedToBeRead in
DispatchQueue.main.async {
progress?(Float(totalBytesRead)/Float(totalBytesExpectedToBeRead))
}
}
.responseData { response in
// Convert the downloaded data in to a UIImage object
var image: UIImage?
if response.result.error == nil {
image = UIImage(data: response.data!) // Memory leak 1
}
completion(response.result.error, image)
}
[UPDATE]
I have added actual code snippet in order to make my question clear.
Say we want to store uiimages into an array, which are fetched from the internet.
I have this code snippet:
// Somewhere in a loop
{
var story = Story()
story.imgUrl = "http:\(imgUrl)"
/// Donwload image, and replace in the top
if let imgUrl = story.imgUrl {
if let url = NSURL(string: imgUrl) {
let request = NSURLRequest(URL: url)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {
(response, data, error) -> Void in
if let data = data {
story.image = UIImage(data: data)
var i = 0
for a in self.Stories {
print("iv image \(i++) is \(a.image)")
}
print("Received image for story as \(story.image) into story \(story)")
// Should one set the image view?
if let _ = self.imageView {
if let indexPath = self.tableView?.indexPathForSelectedRow {
if stories.count == indexPath.section { // is that the currently selected section?
self.imageView?.image = story.image
self.imageView?.hidden = false
print("Set imageView withstory \(story.tag.string)")
}
}
}
}
}
}
}
stories.append(story)
}
/// Switch data source
self.Stories = stories
This doesn't store the image property value into the destination array.
Though the image is ok in the block, if I iterate through the destination array, my image is nil
image: Optional(<UIImage: 0x7ff14b6e4b20> size {100, 67} orientation 0 scale 1.000000))
iv image 0 is nil
How to achieve above functionality?
[INITIAL QUESTION]
Say we want to store element i.e UIImages which i have fetched from the internet. I have this code snippet:
var array = []
let imageView = UIImageView(image:nil)
array.append(imageView)
// and later or, in an synch block
imageView.image = image
This doesn't store the image property value in the array.
How could I do that?
Gotcha!
The point is that Story was defined as struct. And structs are passed by values unlike classes.
To make the code working, I just changed from struct Story {} to class Story {}, and voila!