Swift - How do I save images into an array and access them? - ios

I have a map that allows me to add pins (annotations) to it. When I click on a pin, the app goes to another view controller and shows images downloaded from online. I "save" these images to an array but later I cannot access them. For example, when I tap "Back" to go to the previous view controller and tap on the same pin from before, instead of showing the images I originally downloaded and saved, new images are downloaded from online. Essentially, the images in the array are replaced. How can I save the images and retrieve them? Sorry for long amounts of code, I shortened as much as I could.
This is my class for the images:
class Image {
var image: UIImage
init(image: UIImage) {
self.image = image
}
}
This is my class for the pins. Notice, an array of type Image from the Image class is in here:
class Pin: Hashable {
var hashValue: Int {
get {
return "\(latitude.hashValue),\(longitude.hashValue)".hashValue
}
}
let latitude: Double
let longitude: Double
var images = Array<Image>()
init(latitude: Double, longitude: Double)
{
self.latitude = latitude
self.longitude = longitude
}
}
// must be declared in the global scope! and not just in the class scope
func ==(lhs: Pin, rhs: Pin) -> Bool
{
return lhs.hashValue == rhs.hashValue
}
I add pins to a Set like so (no duplicates of pins allowed). Also, the selected pin is sent to the secondViewController:
class MapViewController: UIViewController, MKMapViewDelegate, UIGestureRecognizerDelegate {
var pins = Set<Pin>()
func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!) {
// Add pin to set
let selectedCoordinatePoint = Pin(latitude: latFromPin, longitude: lonFromPin)
var select = "\(latFromPin.hashValue),\(lonFromPin.hashValue)".hashValue
pins.insert(selectedCoordinatePoint)
//Goto to next view controller and show data depending on the pin selected
for pin in pins {
if pin.hashValue == select.hashValue {
dispatch_async(dispatch_get_main_queue()) {
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let secondViewController = self.storyboard!.instantiateViewControllerWithIdentifier("CollectionViewControllerID") as! CollectionViewController
secondViewController.pin = pin
self.navigationController?.pushViewController(secondViewController, animated: true)
}
}
}
}
}
I download images from online and append to an array on this secondViewController (I shortened the code here):
class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, MKMapViewDelegate {
var pin: Pin!
override func viewWillAppear(animated: Bool) {
if let photosArray = photosDictionary["photo"] as? [[String:AnyObject]] {
// println("photosArray = \(photosArray )")
var count = 0
for photo in photosArray {
// 6 - Grab 21 random images
if count <= 20 {
// Grabs 1 image
let randomPhotoIndex = Int(arc4random_uniform(UInt32(photosArray.count)))
let photoDictionary = photosArray[randomPhotoIndex] as [String:AnyObject]
// 7 - Get the image url
let imageUrlString = photoDictionary["url_m"] as? String
let imageURL = NSURL(string: imageUrlString!)
// 8 - If an image exists at the url, append to array
let imageData = NSData(contentsOfURL: imageURL!)
let finalImage = UIImage(data: imageData!)
var image = Image(image: finalImage!)
self.pin.images.append(image)
count += 1
println(self.pin.images.count)
}
}
}
}
}

I'm not I understand what your asking exactly...you are making network calls to populate your image array. If you just want to pass the image array from VC to VC, you can use prepareForSegue to pass the value.
If you want to pull these down and then store them locally you need to read up on persistence. For example you can use something like Core Data, Parse, Realm...I personally use Parse because of it's ease. This is a good tutorial along the lines of what you're doing I think. It uses Parse to store the image data: http://www.appcoda.com/instagram-app-parse-swift/

Related

How can I use a URL for an image in Swift?

I am trying to retrieve certain data from my Firebase Database - the profile image. As you can see, this is from a UITableViewCell. I have an #IBOutlet for my imageView I want to cover.
As the view awakens, you can see that I go through, and make sure that I can get the information. I know how to retrieve data from Firebase, but not photo URLs, and then convert to the photo itself.
I'm not sure why it isn't working. I am getting an error, and will show it below. There is a possibility it is because of the URL unwrapping stuff, or as if the Firebase isn't formatted correctly, which I think it is, though.
Error Message : Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
import UIKit
import FirebaseAuth
import FirebaseDatabase
import Firebase
class ProfileCellControler: UITableViewCell {
#IBOutlet var name : UILabel!
#IBOutlet var rating : UILabel!
#IBOutlet var imageViewPro : UIImageView!
var databaseRefer : DatabaseReference!
var databaseHandle : DatabaseHandle!
override func awakeFromNib() {
super.awakeFromNib()
var urlString = ""
let urll = URL(string: urlString)!
databaseRefer = Database.database().reference()
let userID = Auth.auth().currentUser!.uid
databaseHandle = databaseRefer.child("Users").child(userID).child("Profile").child("Profile Name").observe(.value, with: { (data) in
print(String((data.value as? String)!))
self.name.text = "\(String((data.value as? String)!))"
print("Done")
})
databaseHandle = databaseRefer.child("Users").child(userID).child("Profile").child("Stars").observe(.value, with: { (data) in
print(String((data.value as? String)!))
if ((String((data.value as? String)!)) == "N/A") {
self.rating.text = "No Rating"
} else {
self.rating.text = "\(String((data.value as? String)!)) ★"
}
print("Done")
})
databaseHandle = databaseRefer.child("Users").child(userID).child("Profile").child("Profile Image").observe(.value, with: { (data) in
print(String((data.value as? String)!))
print("Done \(String((data.value as? String)!))")
urlString = (String((data.value as? String)!))
})
ImageService.downloadImage(withURL: urll) { (image) in
self.imageViewPro.image = image
}
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
The string for the URL is found nil because you are creating the call to download the image for your url before the urll has been initialized with a value from the database in:
databaseHandle = databaseRefer.child("Users").child(userID).child("Profile").child("Profile Image").observe(.value, with: { (data) in
print(String((data.value as? String)!))
print("Done \(String((data.value as? String)!))")
urlString = (String((data.value as? String)!))
})
observe(.value, with: ) Is an asynchronous operation thus
ImageService.downloadImage(withURL: urll) { (image) in
self.imageViewPro.image = image
}
Is being called before observe(.value, with:) is resolved. I would recommend moving the callback for the download URL inside of the completion for .observe(:value, :with) or using grand central dispatch to control the flow better.
As a side note, I highly recommend SDWebImage for handling your image downloading needs as it is configurable with a default image for situations such as this when the image fails to load.
Import KingFisher to make your life easier and then..
Download string representation of image from Firebase asynchronically.
Assign downloaded image to imageView with .kf.setImage method.

is it correct to send UIImage between two controllers?

Is it appropriate to send UIImage between two Uiviewcontrollers?
I'm working on assignment 4 of CS193P Spring - Smashtag.
There I have to implement Mention Table View (It's kind of additional data of tweet: mentions, hashtags, images, urls). I have to place images to appropriate cell there. For that purpose I already download it. After that If user tap on one of that images It should segue to another UIViewController where user can zoom and scroll image.
In many examples which I've seen, people send url of image and fetch it again and again (for mention controller and the same image for another one). I think It decreases perfomance. So I send UIImage object between controllers.
But is it correct?
// MARK: - Navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == Storyboard.SearchSegue {
if let svc = segue.destinationViewController as? SearchResultsTableViewController {
if let cell = sender as? TextTableViewCell {
if let text = cell.hashtagLabel.text {
if text.hasPrefix("#") || text.hasPrefix("#") {
svc.searchText = text
}
}
}
} else if let svc = segue.destinationViewController as? ImageViewController {
if let cell = sender as? ImagesTableViewCell {
svc.image =
}
}
}
}
svc.image is var:
var image: UIImage? {
get {
return imageView.image
}
set {
imageView.image = newValue
imageView.sizeToFit()
scrollView?.contentSize = imageView.frame.size
}
}
and cell.imageVar
var imageVar: UIImage? {
get {
return imageField.image
}
set {
imageField.image = newValue
spinner.stopAnimating()
}
}
fetch function
private func fetchImage () {
if let u = url {
spinner?.startAnimating()
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
let contentsOfURL = NSData(contentsOfURL: u)
dispatch_async(dispatch_get_main_queue()) {
if u == self.url {
if let imageData = contentsOfURL {
self.imageVar = UIImage(data: imageData)
} else {
self.spinner.stopAnimating()
}
}
}
}
}
}
That only make sense if you're using different image sizes. For instance, if you're using the image in a cell, you should use a small image, a thumbnail, to make the download faster. And then when you see the detail and want to zoom the image, you should use a bigger image (so you have to retrieve it using the url).
But if you only have one image size, it's totally correct to pass an UIImage through view controller.
You can think of caching the image. You can save the image in document directory and use its link to load in other places. One thing however you need to keep in mind is that you need to keep a track of how long you want the images to be cached this way, and a mechanism for deleting them. If you want the image only for one session you can even choose to use NSCache.

Init method for a singleton

I have my file myAPI.swift, and two objet Round and GameStats. My Round object also have an attribute GameStats. So what I want to do is to get the GameStats property that I store inside my users defaults then assign it inside my Round object.
class myAPI: NSObject {
static let sharedInstance = myAPI()
var currentStats: GameStats?
var currentRound: Round?
private init(){
super.init()
self.loadData()
NSLog("Stats have been reload: \(self.currentRound?.gameStats)") // Return nil
// If I try to add this line the app stop running and nothing happens
NSLog("Test Singleton: \(myApp.sharedInstance.currentRound?.gameStats)")
}
func loadData(){
let backupNSData = NSUserDefaults.standardUserDefaults().objectForKey("backupNSData")
if let backupNSData = backupNSData as? NSData{
let backupData = NSKeyedUnarchiver.unarchiveObjectWithData(backupNSData)
if let backupData = backupData as? [String:AnyObject] {
guard let round = backupData["currentRound"] as? Round else {
print("error round loaddata")
return
}
self.currentRound = round
guard let stats = backupData["stats"] as? GameStats else {
print("error guard stats")
return
}
self.currentRound.gameStats = stats
NSLog("Stats reloaded: \(stats)") // This is not nil it works here
}
}
}
When my app crash I call this function to save the data
func backupData(){
var backupData:[String:AnyObject] = [String:AnyObject]()
if let round = self.currentRound {
backupData["currentRound"] = round
ColorLog.purple("Stats saved inside Round \(round.gameStats)")
}
if let stats = self.currentStat {
backupData["stats"] = stats
ColorLog.purple("Stats saved : \(stats)")
}
let backupNSData = NSKeyedArchiver.archivedDataWithRootObject(backupData)
NSUserDefaults.standardUserDefaults().setObject(backupNSData, forKey: "backupNSData")
NSUserDefaults.standardUserDefaults().synchronize()
}
So I have two question,
Is it normal that I can't call my singleton like myApp.sharedInstance.currentRound.id = 5 (for instance) inside the init() (I guess it is but I didn't find anything about that)
Why in my init() method my first NSLog self.currentRound?.gameStats is nil when in the function loadData() it wasn't ? It seems like it's losing its reference since we are leaving the function.
What am I doing right now is adding a currentStats property in my singleton, then when I retrieve data instead of doing self.currentRound.gameStats = stats I do self.currentStats = stats, then self.currentRoud.gameStats = self.currentStats and If I do that it works, I don't really know If I am doing the things right here.
Also my two objects Round and GameStats conform to NSCoding protocol as I implemented #objc func encodeWithCoder and #objc required init?(coder aDecoder: NSCoder) methods for both of them.
Thank for you help.

How can I query parse class for particular object?

In my ViewController I have a label which displays the name of a club selected in the previous ViewController. I now need to create a query from my parse.com class to retrieve all information on the object that matches the club name displayed as a label. I know how to successfully query parse but I'm finding it hard to find a way to match the query with the label String seeing as the label String can be different depending on what club was selected in the previous view.
Code:
import UIKit
import Parse
class MenuController: UIViewController {
#IBOutlet weak var clubLabel: UILabel!
var clubName = String()
override func viewDidLoad() {
super.viewDidLoad()
clubLabel.text = clubName
}
}
Previous View already queried parse to populate map with club annotations like so:
let annotationQuery = PFQuery(className: "Clubs")
annotationQuery.findObjectsInBackgroundWithBlock{
(clubs, error) -> Void in
if error == nil {
// The find succeeded.
print("Successful query for annotations")
// Do something with the found objects
let myClubs = clubs! as [PFObject]
for club in myClubs {
//data for annotation
let annotation = MKPointAnnotation()
let place = club["location"] as? PFGeoPoint
let clubName = club["clubName"] as? String
let stadiumName = club["stadium"] as? String
annotation.title = clubName
annotation.subtitle = stadiumName
annotation.coordinate = CLLocationCoordinate2DMake(place!.latitude,place!.longitude)
//add annotations
self.mapView.addAnnotation(annotation)
Instead of using default MKPointAnnotation, we will use custom annotations for easier understanding.
First we will create a class ClubAnnotation conforming to the MKAnnotation protocol in a different file.
import UIKit
import MapKit
import Parse
class ClubAnnotation: NSObject, MKAnnotation {
//MARK: - Instance properties
let club:PFObject
//MARK: - Init methods
init(club:PFObject) {
self.club = club
}
//MARK: - MKAnnotation protocol conformance
var title: String? { get {
return club["clubName"]
}
}
var subtitle: String? { get {
club["stadium"]
}
}
var coordinate: CLLocationCoordinate2D { get {
return CLLocationCoordinate2D(latitude: club["location"].latitude, longitude: club["location"].longitude)
}
}
}
I let you handle the error if the fields "stadium", "clubName" or "location" are empty for a Club object, so you can decide how do display it.
Then, in your class including the MKMapView object (and probably conforming to MKMapViewDelegate protocol), replace the handling of your query result :
let annotationQuery = PFQuery(className: "Clubs")
annotationQuery.findObjectsInBackgroundWithBlock{
(clubs, error) -> Void in
if let clubs = clubs where error == nil {
// The find succeeded.
print("Successful query for annotations")
for club in clubs {
self.mapView.addAnnotation(ClubAnnotation(club:club))
}
} else {
// Handle the error
}
}
You realize that i don't have to manually set title, subtitle and coordinate properties since your ClubAnnotation object returns it directly according to it's implementation of MKAnnotation protocol.
Still in this class, add a selectedClub property. This property will be used to save the club from the annotation selected and will be passed to the pushed view controller.
var selectedClub:PFobject!
In your calloutAccessoryControlTapped delegate method, you can then get your PFObject from the annotation, and pass it to a new viewController of do everything you want with it, without having to query it from Parse :
func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
mapView.deselectAnnotation(view.annotation, animated: true)
if let annotation = view.annotation as? ClubAnnotation {
clubSelected = annotation.club
}
}
You now have saved the club object from your custom annotation class, and you have a reference to it. If you want to present or push a new view controller with more information about this club, just override prepareForSegue and set this value to the pushed/presented view controller. You don't need to query other fields of your club since they are all loaded according to your Parse request.
Simply add a whereKey to your PFQuery.
annotationQuery.whereKey(clubName, equalTo: "clubName")
Hope this helps.

Swift - How to save images for the associated map annotation (pin)?

I want to create an object graph of Pins with Images inside of the Pins. I have a class Pin, which stores map annotations (pins). Inside this class, I have an array of images of type Image. My goal is to have each and every pin have it's own set of images based on the coordinate points of that pin. (This is not about Core Data. It is about implementing the Image class below).
Basically, I want to create a Class Image to put the downloaded images there and store it for the selected pin. Once I have the 21 images for the pin, I do not want to re-download new images for that specific pin unless a button is pressed to do so. For example, if a pin is selected for Times Square, New York and I get 21 images of dogs on the street, this will be saved. If I now go back to the map select a different pin and then decide to reselect the same Times Square, New York from before, I should still see the same 21 images of dogs on the street. Only by clicking a button for re-downlaoding will new pictures replace the 21 images of dogs on the street to who knows what is on Flickr for those coordinates.
import Foundation
import UIKit
import MapKit
class Pin: Hashable {
var hashValue: Int {
get {
return "\(latitude.hashValue),\(longitude.hashValue)".hashValue
}
}
let latitude: Double
let longitude: Double
var images = [Image]()
init(latitude: Double, longitude: Double)
{
self.latitude = latitude
self.longitude = longitude
}
}
// must be declared in the global scope! and not just in the class scope
func ==(lhs: Pin, rhs: Pin) -> Bool
{
return lhs.hashValue == rhs.hashValue
}
My Image class is currently empty:
import UIKit
class Image {
}
When a user drops a pin on the map using a long press gesture, this pin is not added to the Set of pins. This happens only when a user selects the pin. See this code:
class MapViewController: UIViewController, MKMapViewDelegate, UIGestureRecognizerDelegate {
var pins = Set<Pin>()
func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!) {
if editAndDoneButtonText.title == "Edit" {
println("Pin Selected")
/* Get lat/long coordinates of a pin by tapping on the it and update appDelegate variables.
Will use this for networking call to get Flickr images and to populate data structure for storage.
*/
var latFromPin = view.annotation.coordinate.latitude
var latString = String(stringInterpolationSegment: latFromPin)
appDelegate.LAT = latString
var lonFromPin = view.annotation.coordinate.longitude
var lonString = String(stringInterpolationSegment: lonFromPin)
appDelegate.LON = lonString
latCollectionView = latFromPin
lonCollectionView = lonFromPin
// Add pin to set
let selectedCoordinatePoint = Pin(latitude: latFromPin, longitude: lonFromPin)
var select = "\(latFromPin.hashValue),\(lonFromPin.hashValue)".hashValue
pins.insert(selectedCoordinatePoint)
// Goto to next view controller and show data depending on the pin selected
for pin in pins {
if pin.hashValue == select.hashValue {
println("SAME: pin.hashValue: \(pin.hashValue), select.hashValue: \(select.hashValue)")
dispatch_async(dispatch_get_main_queue()) {
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let secondViewController = self.storyboard!.instantiateViewControllerWithIdentifier("CollectionViewControllerID") as! CollectionViewController
self.navigationController?.pushViewController(secondViewController, animated: true)
}
}
}
}
// Remove pins from map by tapping on it
if editAndDoneButtonText.title == "Done" {
var annotationToRemove = view.annotation
mapView.removeAnnotation(annotationToRemove)
println("remove pin - didSelectAnnotationView")
}
}
When a pin is selected, the app goes to the next view controller. Inside viewWillAppear, a networking call is made to Flickr to download images that are associated with the lat/lon coordinates of the selected pin. These images are used to be displayed in a collection view.
class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, MKMapViewDelegate {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var arrayOfImages: [UIImage] = [] // Array from Flickr
var pin: Pin!
override func viewWillAppear(animated: Bool) {
// Flickr
// 2 - API Method Arguments
let methodArguments = [
"method": appDelegate.METHOD_NAME,
"api_key": appDelegate.API_KEY,
"extras": appDelegate.EXTRAS,
"lat": appDelegate.LAT,
"lon": appDelegate.LON,
"format": appDelegate.DATA_FORMAT,
"nojsoncallback": appDelegate.NO_JSON_CALLBACK
]
// 3 - Initialize session and url
let session = NSURLSession.sharedSession()
let urlString = appDelegate.BASE_URL + appDelegate.escapedParameters(methodArguments)
let url = NSURL(string: urlString)!
let request = NSURLRequest(URL: url)
// 4 - Initialize task for getting data
let task = session.dataTaskWithRequest(request) { data, response, downloadError in
if let error = downloadError {
println("Could not complete the request \(error)")
} else {
// 5 - Success! Parse the data
var parsingError: NSError? = nil
let parsedResult: AnyObject! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: &parsingError) as! NSDictionary
// println("parsedResult = \(parsedResult)")
if let photosDictionary = parsedResult.valueForKey("photos") as? [String:AnyObject] {
// println("photosDictionary = \(photosDictionary)")
if let photosArray = photosDictionary["photo"] as? [[String:AnyObject]] {
// println("photosArray = \(photosArray )")
var count = 0
for photo in photosArray {
// 6 - Grab 21 random images
if count <= 20 {
// Grabs 1 image
let randomPhotoIndex = Int(arc4random_uniform(UInt32(photosArray.count)))
let photoDictionary = photosArray[randomPhotoIndex] as [String:AnyObject]
// 7 - Get the image url
let imageUrlString = photoDictionary["url_m"] as? String
let imageURL = NSURL(string: imageUrlString!)
// 8 - If an image exists at the url, append to array
let imageData = NSData(contentsOfURL: imageURL!)
let finalImage = UIImage(data: imageData!)
self.arrayOfImages.append(finalImage!) // Append to array outside of this closure
count += 1
println(self.arrayOfImages.count)
}
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.reloadData()
}
}
}
}
}
// 6 - Resume (execute) the task
task.resume()
}
// Collection view......
}
Currently I am using an array called var arrayOfImages: [UIImage] = []. The networking call downloads 21 images (if available) from Flickr for each pin and stores it in this array.
I've been able to store the pins. Can anyone help with this?
Not sure what the question is... but seems like you're storing a collection of Images correctly. All that's left is to implement the Image class. You can create a property within it called photo that has the class UIImage then instantiate those in your network call. Is that what you were looking for?

Resources