I'm creating a app which has a UITableView. When the user selects one row, it transitions to a content page that has an image slider and its content. There can be minimum of 0 and maximum of 4 images per item.
When loading the inner page, I'm checking the image availability using this method:
func validateUrl (stringURL : NSString) -> Bool {
let url = NSURL(string: stringURL as String)
if (NSData(contentsOfURL: url!) == nil){
return false
}
else{
return true
}
}
It's a really simple method to get the proper output and it always works, but it takes lot of time and because of it I have to load images twice (one for this method and second time to display them).
Is there any better way to do this?
I would add to this method some code to catch the data that was received from
NSData(contentsOfURL: url!)
For example a map with the url as key and data as object. this way, to display the picture you first check the map and only if it's not there you download it
try:
if let data = NSData(contentsOfURL: url) {
return true
}else{
return false
}
Related
So I'm trying to download a set of images to display in a collectionview in my app. I have the images in the firebase storage, and their url in my database under the node "img" in each item node with a child "storage" with the url as the key.
I started by creating an empty array of UIImage: var somePicArray = [UIImage]() and then appending the pictures to this array through this funciton:
func test() {
let penis = Database.database().reference().child("kategorier").child("brus")
penis.observeSingleEvent(of: .value) { (snapshot: DataSnapshot) in
for item in snapshot.children {
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "img/storage")
if let url = imageSnap.value as? String {
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
self.testPicArray.append(image!)
self.collectionView?.reloadData()
}
}
}
}
}
}
up until the "if let url" line everything is ok, as I have tried printing the url within this if statement, and it prints the all the urls as strings perfectly.
however I'm a little more uncertain about the seccond part. I have used this block of code elsewhere in my app to display pictures from firebase storage and this works fine. However it does not seem to work within this funciton, and I'm not sure what I'm doing wrong.
In order to display the images in the cells I tried using the following code in the collectionview cellforitematindexpath method:
cell.cellimage.image = somePickArray[2]
I just chose a random number in the array to see if i can display any picture but it just crashes saying index out of range at this line, which I assume is because the array is empty.
When the vc appears the array is empty so
cell.cellimage.image = somePickArray[2]
will crash before array is filled with data , you need to make sure you do
return somePickArray.count
inside numberOfItemsAtRow
I am trying to call the function 'Checks' at the end of my button function, so that the 'Checks' function is able to check the value of labels in the app, and change the background colour, based off the value of the labels that were manipulated within the GoButton function. e.g. if the weather is 'Cloudy', then I'll display some rain clouds, and hide the sun, and hide the rain. Now, when I press the button, the button works fine, but the Checks function is not called, I then have to press the same button a second time, to get it to call?
I have tried placing the self.Checks() line above the catch, and outside of it, but it makes no difference, I still have to press the GoButton twice to get it to have an affect, and change the background.
Button Function:
//Go Button in Main View Controller
#IBAction func GoButton(_ sender: Any) {
//text IS EQUAL TO WHAT IS IN THE TEXT BOX
let text: String = userValue.text!
//API URL TO FETCH JSON DATA
guard let APIUrl = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=" + text + "&appid=***API***KEY***&units=Metric") else { return }
URLSession.shared.dataTask(with: APIUrl) { data, response, error in
guard let data = data else { return }
//JSON DECODER
let decoder = JSONDecoder()
do {
let weatherData = try decoder.decode(MyWeather.self, from: data)
if (self.MainLabel != nil)
{
if let gmain = (weatherData.weather?.first?.main) { //using .first because Weather is stored in an array
print(gmain)
DispatchQueue.main.async {
self.MainLabel.text! = String (gmain)
}
}
}
} catch {
print("Error in fetching data for your location: \(error)")
}
}.resume()
self.Checks()
}
Checks Function:
func Checks() {
//For some reason, this function doesn't get called on the first time you press the button? <<<<<<<<<<<<<<<<<<<<<
if (MainLabel.text! == "Rain") {
rainClouds.isHidden = false
rain.isHidden = false
sun.isHidden = true
} else if (MainLabel.text! == "Drizzle") {
rainClouds.isHidden = false
rain.isHidden = false
sun.isHidden = true
} else if (MainLabel.text! == "Clouds") {
rainClouds.isHidden = false
rain.isHidden = true
sun.isHidden = true
} else if (MainLabel.text! == "Cloudy") {
rainClouds.isHidden = false
rain.isHidden = true
sun.isHidden = true
}
}
Your issue is that you call self.Checks() in the wrong place. You are calling it long before the data is loaded (do some research on asynchronous calls). Move it to inside the completion handler where you set the label's text.
DispatchQueue.main.async {
self.MainLabel.text! = String (gmain)
self.Checks()
}
Unrelated but it is standard practice that function names, variable names, and enum cases start with lowercase letters. Class, struct, and enum names start with uppercase letters.
You have two places in your function that silently return when the guard conditions fail. I would tuck log statements in those blocks to see which one is causing it to exit prematurely the first time you call the function, and then you can work on why.
As a side note, I would also recommend using more descriptive function names, and ones that can't be confused with variable or object names.
I'm using Alamofire to fetch data from server and then put them in an array of CarType objects which CarType is my struct. what I get from server is name , id and iconUrl. from iconUrls i want to download icons and put them in icon. after that I'll use icon and name in a collection view. my Alamofire request is:
var info = [CarType]()
Alamofire.request(.GET,"url")
.responseJSON { response in
for (_,subJson):(String, JSON) in json["result"]
{
let name = subJson["name"].string
let iconUrl = subJson["icon"].string
let id = subJson["id"].int
info.append(CarType(id: id!, name: name!, iconUrl: iconUrl! , image: UIImage()))
}
my struct is:
import Foundation
import UIKit
struct CarType {
var name : String
var id : Int
var iconUrl : String
var icon : UIImage
}
I want to download images before using them in collectionView.
How can i download images (using AlamofireImage) and put them in related carType icon property?
What you are asking is really bad practice in mobile app. Just a case, for example, you made a request and got like 20 items in an array, and in order to put all UIImages in your models, you have to make 20 more requests, also you even don't know if your users will eventually use (view) those icons or no.
Instead, you could fetch the images when the cell (I guess, you will be displaying those icons in a cell) is displayed, for this purpose you could use libs like SDWebImage(objective c) or Kingfisher(swift) which have extensions for UIImageView to make it simple to fetch and display image on. Those libs can also cache the downloaded image.
Also, another suggestion for object mapping. Currently, you are mapping json to your model manually. There are a lot of good libs to handle that for you, which could automate your object mapping process, for example - ObjectMapper
Hope, this was helpful. Good Luck!
I have done functionalities thing in UITableview, added following in CellForRowIndex method:
getDataFromUrl(urlString){(data,response,error) -> Void in
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Store the image in to our cache
if((image) != nil){
// Store the image in to our cache
self.imageCacheProfile[urlString] = image
// Update the cell
DispatchQueue.main.async(execute: {
cell.imgvwProfile?.image = image
})
}
else{
cell.imgvwProfile!.image = UIImage(named: "user")
}
}
}
func getDataFromUrl(_ strUrl:String, completion: #escaping ((_ data: Data?, _ response: URLResponse?, _ error: NSError? ) -> Void)) {
let url:URL = URL(string: strUrl)!
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) {data, response, err in
print("Entered the completionHandler")
}.resume()
}
You also need to declare imageCache to store downloaded images.
var imageCache = [String:UIImage]()
You can use above code in your method and it should work just.
Need some guidance on how to approach this. I'm not sure if I'm completely over complicating it or if it is a mess.
I've got a UICollectionView that I want to always have 9 cells and I'm wanting users to be able to insert posts by tapping on a cell..so if they press on cell 8, they make their post, and it shows up there in the 8th cell with potentially 8 other empty cells around it.
I'm posting/pulling these posts from firebase...so I was thinking the flow needed to look something like:
1) Make an empty array of 9 empty [Posts] so that the cells appear and are clickable.
2) On the firebase observer .. if there are say 3 posts able to be returned, it inserts them into the post array / replaces 3 empty posts.
3) I want the posts to show up sort of randomly throughout the grid, so I figured I'd shuffle the array before reloading the data?
4) I don't really care if the posts back from firebase are in the same spot as they were placed by the user, but I want when a user puts a post for it to stay in the same spot as they placed it, so I figured I'd save a variable to firebase like "position : 8" and I'd say something like "If your user uid = the uid of the post, grab the position number and insert that post at that index of the array.
Is this flow way off base or is there a better way to accomplish this? I haven't seen much about inserting items into certain positions into a table view/collection view.
Edit:
func fillWithBlanks() {
for i in 0 ..< 9 {
let blankPost = Post(timestamp: 99999999999999999, picURL: nil, postKey: "") // 100% need a better timestamp there, just filler for now.
postsArray.append(blankPost)
}
}
and then
DataService.ds.REF_POSTS.queryOrderedByChild("timestamp").queryStartingAtValue(cutoff).observeEventType(.ChildAdded, withBlock: { (snapshot:FIRDataSnapshot) in
let time = snapshot.value!["timestamp"] as? Int
let text = snapshot.value!["text"] as? String
let picURL = snapshot.value!["imageURL"] as? String
let key = snapshot.key
let post = Post(timestamp: time!, picURL: picURL!, postKey: key)
self.postsArray[self.selectedPostIndex] = post
self.collectionView.reloadData()
})
So basically on viewDidLoad I'm calling fillWithBlanks(), and then when I select a cell I set the selectedPostIndex to the index clicked on, create a post, and insert it in where I selected.
This works and I tested it by printing out the cell's timestamp when I press on it, and sure enough when I make a post the timestamp is appropraite to the post and not the 99999999.
My massive issue I'm having are the pictures. When I load up I get 9 cells that are empty but when I make a post it sets the picture I set for the post to all 9 cells, and then if I make 2 posts, I get a very Simon-says'ie flashing in all of my cells between the first picture, second picture, and the blank background from the blank cell.
When I make a post I'm saving a download url to firebase and then I have a caching system that downloads the posts or grabs them in. It all worked before trying to implement the blank posts.
Any ideas what would be causing that? I don't understand why appending a new post would do anything to the blank posts I'm making when the app loads up. I'm assuming I'm having some misunderstanding with classes and my Post array/ Post objects aren't what they need to be.
Edit 2:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let post = postsArray[indexPath.row]
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MainCell", forIndexPath: indexPath) as! PostCell
if let imageURL = post.picURL {
cell.cellImage.loadImageUsingNSCache(imageURL)
}
return cell
}
and the .loadimagesUsingNSCache comes from :
extension UIImageView {
func loadImageUsingNSCache(urlString: String){
if let cachedImage = imageCache.objectForKey(urlString) as? UIImage {
self.image = cachedImage
return
}
let url = NSURL(string: urlString)
NSURLSession.sharedSession().dataTaskWithURL(url!, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) in
if error != nil {
print(error)
return
}
dispatch_async(dispatch_get_main_queue(), {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString)
self.image = UIImage(data: data!)
}
})
}).resume()
}
About the problem with the images, remember collection view cells are reusable items, so you need to tell what's the content of every item inside every time you call collectionView(cellForItemAtIndexPath).
That means you need to provide a value for cell.cellImage every time, even if post.picURL is nil. In that case you can say cell.cellImage = nil or show a default empty picture.
[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!