I am attempting to retrieve a PFFile image based off an objectID attached to the same object I am pulling the imageFile from. All other details are being pulled correctly (Strings) however, when I pull the PFFile, a random image from that class loads, not usually the matching image for that object, below is my code for how I am querying it and I am unsure where my logic is incorrect.
#objc(mapView:didTapMarker:) func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
if z == 0 {
print("\(marker) tapped")
let lat = marker.position.latitude as Double
let long = marker.position.longitude as Double
let tempMarker = getMarkerForLongAndLat(long: long, lat: lat, markers: markers)
let index = getIndexForMarker(marker: tempMarker)
getInfo(self.markersID[index]) {
PFFile in
self.imageFile?.getDataInBackground(block: { (imageData, error) in
if(imageData != nil) {
self.fullImage = UIImage(data: imageData!)
}
})
self.performSegue(withIdentifier: "playerSegue", sender: nil)
}
}
return true
}
func getInfo(_ markerID : String, completion: #escaping (PFFile) -> Void)
{
self.group.enter()
print("printing")
print(markerLat)
print(markerLong)
print(postsLat)
print(postsLong)
print("A")
let query = PFQuery(className:"Posted")
print("b")
print(markerID)
//query.whereKey("GPS", equalTo: PFGeoPoint(latitude: markerLat, longitude: markerLong))
query.whereKey("objectId", equalTo: "\(markerID)")
query.findObjectsInBackground { (objects, error) in
print("c")
if(error == nil)
{
print("d")
for object in objects!
{
print("made it")
self.imageFile = (object.object(forKey: "ImageFile") as! PFFile?)
self.userofVideo = object.object(forKey: "User") as AnyObject?
self.hour = object.object(forKey: "Hour") as AnyObject?
self.minutes = object.object(forKey: "minutes") as AnyObject?
self.streetName = object.object(forKey: "Location") as AnyObject?
self.tempLat = (object.object(forKey: "GPS")! as AnyObject).latitude as Double
self.tempLong = (object.object(forKey: "GPS")! as AnyObject).longitude as Double
self.currentText = (object.object(forKey: "text")! as Any? as! String?)
self.tempCoordinates = CLLocationCoordinate2D(latitude: self.tempLat! as CLLocationDegrees, longitude: self.tempLong! as CLLocationDegrees)
//print(tempLong)
}
}
else
{
print("didn't make it")
print(error)
}
completion(self.imageFile!)
}
self.group.leave()
}
Add the getDataInBackground inside the for loop. Here is a quick swift 3 example for your code.
#IBOutlet weak var fullImage : UIImageView!
let imageFile = (object.object(forKey: "ImageFile") as? PFFile)
imageFile?.getDataInBackground (block: { (data, error) -> Void in
if error == nil {
if let imageData = data {
//Here you can cast it as a UIImage or do what you need to
self.fullImage.image = UIImage(data:imageData)
}
}
})
Related
So I'm trying to store data retrieved from my Firestore database into an object. My database has a collection of users, and each user has a collection of classes. I want to be able to get the logged in users collection of classes and store them in an array of objects. Most of what I've tried so far can pull data but it won't save it into anything because its able access the data from within the completion handler. Any help would be great, here's the code I'm working with rn:
db.collection("users").whereField("uid", isEqualTo: uid).addSnapshotListener { (querySnapshot, error) in
if error == nil && querySnapshot != nil {
let docId = querySnapshot?.documents[0].documentID
db.collection("users").document(docId!).collection("classes").addSnapshotListener { (querySnap, error) in
guard let documents = querySnap?.documents else{print("No Classes");return}
var imageData:UIImage?
retrievedClasses = documents.map { (querySnap) -> UserClass in
let data = querySnap.data()
if let decodedData = Data(base64Encoded: data["class_img"] as! String, options: .ignoreUnknownCharacters){
imageData = UIImage(data: decodedData)
}
return UserClass.init(name: data["class_name"] as! String, desc: data["class_desc"] as! String, img: imageData!, color: data["class_color"] as! String, link: data["class_link"] as! String, location: data["class_location"] as! GeoPoint, meetingTime: data["meeting_time"] as! Dictionary<String,String>)
}
print(retrievedClasses[0].printClass())
}
}
}
As I understand you need to do something like this:
func getUserClasses(for userID: String, completion: #escaping (Result<[UserClass], Error>) -> Void) {
db.collection("users").whereField("uid", isEqualTo: userID).addSnapshotListener { (querySnapshot, error) in
if error == nil && querySnapshot != nil {
let docId = querySnapshot?.documents[0].documentID
db.collection("users").document(docId!).collection("classes").addSnapshotListener { (querySnap, error) in
guard let documents = querySnap?.documents else {
print("No Classes")
completion(.failure("No Classes"))
return
}
let retrievedClasses = documents.map { (querySnap) -> UserClass in
let data = querySnap.data()
var imageData: UIImage?
if let decodedData = Data(base64Encoded: data["class_img"] as! String,
options: .ignoreUnknownCharacters) {
imageData = UIImage(data: decodedData)
}
let user = UserClass(name: data["class_name"] as! String,
desc: data["class_desc"] as! String,
img: imageData!,
color: data["class_color"] as! String,
link: data["class_link"] as! String,
location: data["class_location"] as! GeoPoint,
meetingTime: data["meeting_time"] as! Dictionary<String,String>)
return user
}
completion(.success(retrievedClasses))
}
} else {
completion(.failure("Request Error"))
}
}
}
Then you will able to use data after request completion:
getUserClasses(for: "123") { result in
switch result {
case .success(let allClasses):
retrievedClasses = allClasses // retrievedClasses is a property in your class which you are going to use
if !retrievedClasses.isEmpty() {
print(retrievedClasses[0].printClass())
}
case .failure(let error):
print(error)
}
}
I followed the Firebase tutorial by Ray Wenderlich (Link) and adopted his way of initializing the object (in my case of type "Location") with the snapshot from the observe-method:
class Location:
init(snapshot: FIRDataSnapshot) {
identifier = snapshot.key
let snapshotValue = snapshot.value as! [String : AnyObject]
type = snapshotValue["type"] as! String
name = snapshotValue["name"] as! String
address = snapshotValue["address"] as! String
latitude = Double(snapshotValue["latitude"] as! String)!
longitude = Double(snapshotValue["longitude"] as! String)!
avatarPath = snapshotValue["avatarPath"] as! String
ref = snapshot.ref
}
LocationsViewController:
databaseHandle = locationsRef?.queryOrdered(byChild: "name").observe(.value, with: { (snapshot) in
var newLocations:[Location] = []
for loc in snapshot.children {
let location = Location(snapshot: loc as! FIRDataSnapshot)
newLocations.append(location)
}
self.locations = newLocations
self.tableView.reloadData()
})
This really works like a charm, but now I'm trying to load the image stored under the storage reference "avatarPath".
My attempt worked but the images take a ling time to load. Is there a better way/place to load these images?
My attempt 1:
databaseHandle = locationsRef?.queryOrdered(byChild: "name").observe(.value, with: { (snapshot) in
var newLocations:[Location] = []
for loc in snapshot.children {
let location = Location(snapshot: loc as! FIRDataSnapshot)
newLocations.append(location)
}
self.locations = newLocations
self.tableView.reloadData()
//Load images
for loc in self.locations {
let imagesStorageRef = FIRStorage.storage().reference().child(loc.avatarPath)
imagesStorageRef.data(withMaxSize: 1*1024*1024, completion: { (data, error) in
if let error = error {
print(error.localizedDescription)
} else {
loc.avatarImage = UIImage(data: data!)!
self.tableView.reloadData()
}
})
}
})
My 2nd Attempt (inside Location class):
init(snapshot: FIRDataSnapshot) {
identifier = snapshot.key
let snapshotValue = snapshot.value as! [String : AnyObject]
type = snapshotValue["type"] as! String
name = snapshotValue["name"] as! String
address = snapshotValue["address"] as! String
latitude = Double(snapshotValue["latitude"] as! String)!
longitude = Double(snapshotValue["longitude"] as! String)!
avatarPath = snapshotValue["avatarPath"] as! String
ref = snapshot.ref
super.init()
downloadImage()
}
func downloadImage() {
let imagesStorageRef = FIRStorage.storage().reference().child(self.avatarPath)
imagesStorageRef.data(withMaxSize: 1*1024*1024, completion: { (data, error) in
if let error = error {
print(error.localizedDescription)
} else {
self.avatarImage = UIImage(data: data!)!
}
})
}
Thank you in advance!
Nico
The best way you can accomplish that is to load asynchronous inside the loading of the cell function. I mean:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
DispatchQueue.main.async {
let imagesStorageRef = FIRStorage.storage().reference().child(self.locations[indexPath.row].avatarPath)
imagesStorageRef.data(withMaxSize: 1*1024*1024, completion: { (data, error) in
if let error = error {
print(error.localizedDescription)
} else {
locations[indexPath.row].avatarImage = UIImage(data: data!)!
tableView.reloadRows(at indexPaths: [indexPath], with animation: .none)
}
})
}
}
In first attempt try changing your code as:
DispatchQueue.main.async {
for loc in self.locations {
let imagesStorageRef = FIRStorage.storage().reference().child(loc.avatarPath)
imagesStorageRef.data(withMaxSize: 1*1024*1024, completion: { (data, error) in
if let error = error {
print(error.localizedDescription)
} else {
loc.avatarImage = UIImage(data: data!)!
self.tableView.reloadData()
}
})
}
}
When the value of iImage is printed out it says: " size {3024, 4032} orientation 3 scale 1.000000", but then I get the error: "fatal error: unexpectedly found nil while unwrapping an Optional value" on the next line. How can the image I just got be nil?
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
let iImage = info[UIImagePickerControllerOriginalImage] as! UIImage
print(iImage)
createEvent(iImage)//where it is saved to cloudkit with location and title
EventPageViewController().eventPic.image = iImage
self.dismiss(animated: true, completion: nil);
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
let eventPageViewController:EventPageViewController = storyboard?.instantiateViewController(withIdentifier: "EventPage") as! EventPageViewController
self.present(eventPageViewController, animated: true, completion: nil)
}
func loadEvent(_ completion: #escaping (_ error:NSError?, _ records:[CKRecord]?) -> Void)
{
let date = NSDate(timeInterval: -60.0 * 180, since: NSDate() as Date)
let predicate = NSPredicate(format: "creationDate > %#", date)
let query = CKQuery(recordType: "Event", predicate: predicate)
CKContainer.default().publicCloudDatabase.perform(query, inZoneWith: nil){
(records, error) in
if error != nil {
print("error fetching events: \(error)")
completion(error as NSError?, nil)
} else {
print("found events")
completion(nil, records)
guard let records = records else {
return
}
for record in records
{
self.drawEvents(record["LocationF"] as! CLLocation, title1: record["StringF"] as! String)
if let asset = record["Picture"] as? CKAsset,
let data = NSData(contentsOf: asset.fileURL),
let image1 = UIImage(data: data as Data)
{
//EventPageViewController().eventPic.image = image1
}
}
}
}
}
func drawEvents(_ loc: CLLocation, title1: String)
{
mapView.delegate = self
let center = CLLocationCoordinate2D(latitude: loc.coordinate.latitude, longitude: loc.coordinate.longitude)
let lat: CLLocationDegrees = center.latitude
let long: CLLocationDegrees = center.longitude
self.pointAnnotation1 = MKPointAnnotation()
self.pointAnnotation1.title = title1
self.pointAnnotation1.subtitle = "Event"
self.pointAnnotation1.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: long)
self.pinAnnotationView = MKPinAnnotationView(annotation: self.pointAnnotation1, reuseIdentifier: nil)
self.mapView.addAnnotation(self.pinAnnotationView.annotation!)
}
This is because you are creating a new instance of your ViewController here EventPageViewController(). If this delegate method is in EventPageViewController, you can just call self.eventPic.image = iImage.
It looks like eventPic is a UIImageView as IBOutlet. So your instance of EventPageViewController hasn't finished loading its view and connecting outlets.
You should have a UIImage property in EventPageViewController.
class EventPageViewController {
var eventImage: UIImage?
#IBOutlet var eventPic:UIImageView! //you should have better name like eventImageView
func viewDidLoad() {
super.viewDidLoad()
eventPic?.image = eventImage
///rest goes here
}
}
so from you picker delegate you can access like:
let eventPageViewController = EventPageViewController()
eventPageViewController.eventImage = iImage
Hope this helps.
I have 4 functions containing Parse query.findObjectsInBackgroundWithBlock. These are being called to grab data and then populate the table view. Using dispatch groups.
Here is two examples of my parse querys
func getEventImages() {
print("getEventImages enter")
dispatch_group_enter(self.group)
let query = PFQuery(className: "events")
query.orderByAscending("eventDate")
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!, error: NSError!) -> Void in
// Initialize your array to contain all nil objects as
// placeholders for your images
if error == nil {
self.eventMainImageArray = [UIImage?](count: objects.count, repeatedValue: nil)
for i in 0...objects.count - 1 {
let object: AnyObject = objects[i]
let mainImage = object["mainImage"] as! PFFile
//dispatch_group_enter(self.group)
mainImage.getDataInBackgroundWithBlock({
(imageData: NSData!, error: NSError!) -> Void in
if (error == nil) {
let mainImage = UIImage(data:imageData)
self.eventMainImageArray[i] = mainImage
print("getEventImages appended")
}
else {
print("error!!")
}
})
}
}
print("getEventImages leave")
dispatch_group_leave(self.group)
}
}
func getEventInfo() {
print("eventInfo enter")
dispatch_group_enter(group)
let query = PFQuery(className: "events")
query.orderByAscending("eventDate")
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!,error: NSError!) -> Void in
self.eventNameArray = [String?](count: objects.count, repeatedValue: nil)
self.eventInfoArray = [String?](count: objects.count, repeatedValue: nil)
self.eventDateArray = [NSDate?](count: objects.count, repeatedValue: nil)
self.eventTicketsArray = [String?](count: objects.count, repeatedValue: nil)
if error == nil {
for i in 0...objects.count - 1 {
let object: AnyObject = objects[i]
let eventName = object["eventName"] as! String
let eventInfo = object["eventInfo"] as! String
let eventDate = object["eventDate"] as! NSDate
let eventTicket = object["Tickets"] as! String
self.eventNameArray[i] = eventName
self.eventInfoArray[i] = eventInfo
self.eventDateArray[i] = eventDate
self.eventTicketsArray[i] = eventTicket
print("event info appended")
}
}
print("event info leave")
dispatch_group_leave(self.group)
}
}
And my dispatch_group_nofity
dispatch_group_notify(group, dispatch_get_main_queue()) { () -> Void in
print("Finished reloadDataFromServer()")
self.tableView.reloadData()
self.refreshControl?.finishingLoading()
}
}
The problem is that its hit and miss if the data gets retrieved quick enough before dispatch_group_leave(self.group) is called leading to reloading the tableview data too soon. I need to get this so the dispatch_group_leave gets called when the appending is completed.
There is no need for two methods to retrieve the data, no need to unpack the data into multiple arrays and no need to use dispatch groups.
All you need is a simple method to retrieve your event data
var events:[PFObject]=[PFObject]()
func getEventInfo() {
let query = PFQuery(className: "events")
query.orderByAscending("eventDate")
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!,error: NSError!) -> Void in
if error==nil {
self.events=objects as! [PFObject]
self.tableView.reloadData()
} else {
print("Something went wrong! - \(error)"
}
self.refreshControl?.finishingLoading()
}
}
Then, you haven't shown your cellForRowAtIndexPath but you would have something like
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! MyTableViewCell
let event=self.events[indexPath.row]
cell.eventName.text=event["eventName"] as? String
cell.eventInfo.text=event["eventInfo"] as? String
if let mainImageFile=event["mainImage"] as? PFFile {
mainImageFile.getDataInBackgroundWithBlock({
(imageData: NSData!, error: NSError!) -> Void in
if (error == nil) {
let mainImage = UIImage(data:imageData)
cell.mainImage= mainImage
}
else {
print("error!!")
}
}
return cell;
}
You can use a PFImageView or a framework like SDWebImage to handle image caching and putting a placeholder image in place while the image is loaded.
If you want to update an event is as easy as
var event=self.events[someindex];
event["eventName"]=newValue
event.saveInBackground()
Right now I have a mapview that populates a bunch of pins based on a category, for example, 'Asian Food'. WHen i click the callout accessory of that annotation, the app navigates to a detail viewcontroller, which I hope to display the address and other properties. I am getting the information from the Yelp API CLient. Here is my model:
import UIKit
import MapKit
var resultQueryDictionary:NSDictionary!
class Resturant: NSObject {
var name: String!
var thumbUrl: String!
var address: String!
var jsonData: NSData!
var location: NSDictionary // location
init(dictionary: NSDictionary) {
name = dictionary["name"] as? String
thumbUrl = dictionary["thumbUrl"] as? String
address = dictionary["address"] as? String
self.location = dictionary["location"] as? NSDictionary ?? [:]
}
class func searchWithQuery(map: MKMapView, query: String, completion: ([Resturant]!, NSError!) -> Void) {
YelpClient.sharedInstance.searchWithTerm(query,sort: 0, radius: 1069, success: { (operation: AFHTTPRequestOperation!, response: AnyObject!) -> Void in
let responseInfo = response as! NSDictionary
resultQueryDictionary = responseInfo
println(responseInfo)
let dataArray = responseInfo["businesses"] as! NSArray
for business in dataArray {
let obj = business as! NSDictionary
var yelpBusinessMock: YelpBusiness = YelpBusiness(dictionary: obj)
var annotation = MKPointAnnotation()
annotation.coordinate = yelpBusinessMock.location.coordinate
annotation.title = yelpBusinessMock.name
annotation.subtitle = yelpBusinessMock.displayAddress
attractionDetailAddressString = yelpBusinessMock.displayAddress
map.addAnnotation(annotation)
}
}) { (operation: AFHTTPRequestOperation!, error: NSError!) -> Void in
println(error)
}
}
// term: String, deal: Bool, radius: Int, sort: Int, categories: String, success: (AFHTTPRequestOperation!, AnyObject!) -> Void, failure: (AFHTTPRequestOperation!, NSError!) -> Void) -> AFHTTPRequestOperation! {
class func searchWithQueryWithRadius(map: MKMapView, term: String, deal: Bool, radius: Int, sort: Int, categories: String, completion: ([Resturant]!, NSError!) -> Void) {
YelpClient.sharedInstance.searchWithTerm(term, deal: false, radius: radius, sort: sort,categories: categories, success: { (operation: AFHTTPRequestOperation!, response: AnyObject!) -> Void in
let responseInfo = response as! NSDictionary
resultQueryDictionary = responseInfo
println(responseInfo)
let dataArray = responseInfo["businesses"] as! NSArray
for business in dataArray {
let obj = business as! NSDictionary
var yelpBusinessMock: YelpBusiness = YelpBusiness(dictionary: obj)
var annotation = MKPointAnnotation()
annotation.coordinate = yelpBusinessMock.location.coordinate
annotation.title = yelpBusinessMock.name
map.addAnnotation(annotation)
}
}) { (operation: AFHTTPRequestOperation!, error: NSError!) -> Void in
println(error)
}
}
}
In my Attractions ViewController (where the user taps categories to populate the map with pins), I have this code for when the callout accessory is tapped:
func mapView(mapView: MKMapView!, annotationView view: MKAnnotationView!, calloutAccessoryControlTapped control: UIControl!) {
if (control == view.rightCalloutAccessoryView) {
let selectedLocation = view.annotation;
let selectedCoordinate = view.annotation.coordinate;
var latitude = selectedCoordinate.latitude
var longitude = selectedCoordinate.longitude
var location:CLLocation = CLLocation(latitude: latitude, longitude: longitude)
let businessPlacemark = MKPlacemark(coordinate: selectedCoordinate, addressDictionary: nil)
indicatedMapItem = selectedCoordinate;
let resturantMock:Resturant = Resturant(dictionary: resultQueryDictionary)
attractionDict = resturantMock.location;
performSegueWithIdentifier("attractionToDetail", sender: self);
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var attractionsDetailViewController:AttractionsDetailViewController = segue.destinationViewController as! AttractionsDetailViewController
attractionsDetailViewController.attractionLocation = indicatedMapItem;
attractionsDetailViewController.attractionLocationDetail = self.attractionDict
}
In my Detail View Controller, I attempt to get the data for the specific business that is tapped.
func getYelpData() {
var businessMock:Resturant = Resturant(dictionary: resultQueryDictionary)
var address:NSDictionary = attractionLocationDetail //dictionary
attractionDetailAddressString = businessMock.location["display_address"] as? String;
self.addressLabel.text = attractionDetailAddressString
}
However, this displays that address is nil.
Basically I want to know how to get one instance of the business from the large NSDictionary of Businesses and then retrieve the data for that individual business in my detail VC.
This is my Github repo if you want to look at all the code:
https://github.com/ssharif6/Park-N-Go
Thanks for any help!
It looks as though your problem may be here on this line
attractionDict = resturantMock.location;
Your dictionary will have a location key, but doesn't have the keys as suggested here on that location sub dictionary:
init(dictionary: NSDictionary) {
name = dictionary["name"] as? String
thumbUrl = dictionary["thumbUrl"] as? String
address = dictionary["address"] as? String
self.location = dictionary["location"] as? NSDictionary ?? [:]
}
So naturally all those values will be nil when your VC gets loaded.
Those keys however do exist on the resturantMock.business dictionary - is that what you meant to write? :)