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

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?

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.

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 add records fetched asynchronously from json to the array only when all records are fetched in Swift?

I'm working on a market cluster for apple maps (here is the plugin https://github.com/ribl/FBAnnotationClusteringSwift ) and I want to display all records on my map at once - for that I need to download all the records from remote webservice, add it to json (I've already done that) and then add all fetched points to an array.
My code looks like this:
let clusteringManager = FBClusteringManager()
var array:[FBAnnotation] = []
func loadInitialData() {
RestApiManager.sharedInstance.getRequests { json in
if let jsonData = json.array {
for requestJSON in jsonData {
dispatch_async(dispatch_get_main_queue(),{
if let request = SingleRequest.fromJSON(requestJSON){
let pin = FBAnnotation()
pin.coordinate = CLLocationCoordinate2D(latitude: request.latitude, longitude: request.longitude)
self.array.append(pin)
}
})
}
}
}
}
as you can see I'm appending all pins to the array and at some point I need to use this array here:
self.clusteringManager.addAnnotations(array)
I thought I could just write the line above at the very end of my method loadInitialData, but then the array is still empty. How should I change my code so that it invokes the addAnnotations method when the array is filled with data?
=== EDIT
Just a small add on - I call the method loadInitialData in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
loadInitialData()
}
You may want to use NSOperation. It's API has method addDependency(:) that is just what you're looking for.
The code might look like this:
let queue = NSOperationQueue()
var operations : [NSOperation] = []
RestApiManager.sharedInstance.getRequests { json in
if let jsonData = json.array {
for requestJSON in jsonData {
let newOperation = NSBlockOperation {
SingleRequest.fromJSON(requestJSON){
let pin = FBAnnotation()
pin.coordinate = CLLocationCoordinate2D(latitude: request.latitude, longitude: request.longitude)
self.array.append(pin)
}
operations.append(newOperation)
}
}
}
let finalOperation = NSBlockOperation() {
///array has been populated
///proceed with it
}
operations.forEach { $0.addDependency(finalOperation) }
operations.append(finalOpeartion)
operationQueue.addOperations(operations)
}

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

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/

Swift array: how to append element references in order to update element later on from a synch request?

[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!

Resources