I have a table view where two cells have UITextView into which the user can enter long data. Following some answers here in Stackoverflow, I implemented a protocol/delegate to detect when the user has finished entering the data which then will be saved in a global dictionary:
class DetailsNewTaskViewController: UITableViewController, TextViewCellDelegate {
var cellData:[String:String] = [:]
func controllerView(controller: UITableViewCell, textViewDidEndEditing: String, atIndex:Int) {
switch(atIndex) {
case 0:
self.cellData["titolo"] = (controller as! LittleTextCell).textView.text
break
case 1:
self.cellData["oggetto"] = (controller as! BigTextCell).textView.text
break
default:
break
}
}
and this is the relative custom cell class:
class LittleTextCell: UITableViewCell, UITextViewDelegate {
#IBOutlet weak var label : UILabel!
#IBOutlet weak var textView : UITextView!
var delegate:TextViewCellDelegate?
var rowIndex:Int?
func textViewDidEndEditing(textView: UITextView) {
delegate?.controllerView(self, textViewDidEndEditing: textView.text, atIndex: rowIndex!)
}
}
where the delegate for textView is the class itself.
And this is a screenshot of the application:
The "problem" is that only AFTER the user taps another cell/field then the text is stored in the global dictionary. What about if the user taps "Fine" button (to save data) without having touched another field after he's finished entering the text? That a fatal nil error is raised. So I would like to know if there is a way to detect that the user has stopped typing in even if he's still inside that cell so that the content is always stored.
Is it possible? Is there a particular method to implement?
UPDATE: the function associated to "Fine" button:
func saveTask(sender:UIButton!) {
self.dateFormatter.dateFormat = "yyyy-MM-dd"
var taskToSave = Task(id: -1,
titolo: self.cellData["titolo"]!,
oggetto: self.cellData["oggetto"]!,
check_mail: self.cellData["check_mail"]!.toBool()!,
id_progetto: self.projects[self.cellData["progetto_nome"]!]!.id,
progetto_nome: nil,
assegnato_a: nil,
id_assegnato_a: self.users[self.cellData["assegnato_a"]!]!.id,
richiesto_da: nil,
id_richiesto_da: self.users[self.cellData["richiesto_da"]!]!.id,
priorita: self.cellData["priorita"]!,
termine_consegna: self.dateFormatter.dateFromString(self.cellData["termine_consegna"]!)!,
stato: self.cellData["stato"]!)
self.taskService.addTaskService(taskToSave) {
(response: String) in
if ((response.rangeOfString("Could not connect to the server.")) != nil) {
dispatch_async(dispatch_get_main_queue()) {
self.alertView.title = "Operazione fallita!"
self.alertView.message = "Impossibile connettersi al server. \n Riprovare."
self.alertView.delegate = self
self.alertView.addButtonWithTitle("OK")
self.alertView.show()
}
println(response)
}
else {
if ((response.rangeOfString("status code: 200")) != nil) {
dispatch_async(dispatch_get_main_queue()) {
self.alertView.title = "Operazione eseguita!"
self.alertView.message = "Task creato correttamente"
self.alertView.delegate = self
self.alertView.addButtonWithTitle("OK")
self.alertView.show()
self.navigationController?.popViewControllerAnimated(true)
}
}
else {
println(response)
}
}
}
}
define global selectedIndexPath
var selectedIndexPath:Int = 0
set it to selected indexPath
func controllerView(controller: UITableViewCell, textViewDidEndEditing: String, atIndex:Int) {
selectedIndexPath = indexPath
switch(atIndex) {
case 0:
self.cellData["titolo"] = (controller as! LittleTextCell).textView.text
break
case 1:
self.cellData["oggetto"] = (controller as! BigTextCell).textView.text
break
default:
break
}
}
In saveTask function get cell with cellForRowAtIndexPath
func saveTask(sender:UIButton!) {
let cell = tableView.cellForRowAtIndexPath(selectedIndexPath)
self.cellData["titolo"] = cell.LittleTextCell.textView.text
self.cellData["oggetto"] = cell.BigTextCell.textView.text
}
just use this line on the top in the method fine in your controller
self._tableView.superview?.endEditing(true);
I was facing the same problem , my textfields in cell and i want to check all the fields in controller.When i get my data so last data is not up to date because after last data, i click on the button not on the text field. So i found the solution. I wrote this line in my method (in your case, this line should be in fine method) before getting my dictionary and after that i had updated data.
Thanks
Related
In my Xcode Project I will like to have a similar view like Snapchat's "Send To..." screen (I have attached a screenshot). I have already made a tableview and populate it and have allowed multiple selection on. I am currently having trouble with two things:
1) Multiple Selection: I can select an cell I want, but when I tap on the search bar and start typing, all my previous selections go away. I am assuming that I need to add all of the names in a array and somehow communicate the array with the table so it shows if this username is in the array then make it selected in the tableview. But I am not sure how to do that. How can I do this?
2) Sending to Bottom Bar (blue in photo): As you may know, in Snapchat as you press on which users you want to send the snap to, their names get added to the bar at the bottom, as you fill up the bar, it because swipe able where you can horizontally scroll through the names you have added. I can append the names to an array and show the array in a label like theirs, but I do not know how to make it so a user can horizontally scroll through it.How do I implement this same feature?
Feel free to answer ANY of the questions! You do not need to do all of them, I just need them answered. Here's my code so far:
class User {
var userID:String?
var userFullName:String?
var userUsername:String?
var userProfileImage:PFFile?
var isPrivate:Bool
init(userID : String, userFullName : String, userUserName : String, userProfileImage : PFFile, isPrivate : Bool) {
self.userID = userID
self.userFullName = userFullName
self.userUsername = userUserName
self.userProfileImage = userProfileImage
self.isPrivate = isPrivate
}
}
var userArray = [User]()
func loadFriends() {
//STEP 1: Find friends
let friendsQuery = PFQuery(className: "Friends") //choosing class
friendsQuery.whereKey("friendOne", equalTo: PFUser.current()?.objectId ?? String()) //finding friends
friendsQuery.limit = self.page //number of users intitally showing
friendsQuery.findObjectsInBackground (block: { (objects, error) -> Void in
if error == nil { //if no error
//clean up
self.friendsArray.removeAll(keepingCapacity: false)
//STEP 2: Find related objects depending on query setting
for object in objects! {
self.friendsArray.append(object.value(forKey: "friendTwo") as! String) //hold array info of friend
}
//STEP 3: Find friend info
let query = PFUser.query()
query?.whereKey("objectId", containedIn: self.friendsArray)
query?.addDescendingOrder("createdAt") //how to order users
query?.findObjectsInBackground(block: { (objects, error) -> Void in
if error == nil {
for object in objects! {
var user : User
let fullname = (object.value(forKey: "fullname") as! String)
let username = (object.object(forKey: "username") as! String)
let profilePhoto = (object.object(forKey: "profilePhoto") as! PFFile)
let objectID = (object.objectId!)
let isPrivate = (object.object(forKey: "isPrivate") as! Bool)
user = User(userID: objectID, userFullName: fullname, userUserName: username, userProfileImage: profilePhoto, isPrivate: isPrivate)
self.userArray.append(user)
}
self.tableView.reloadData()
} else {
print(error!)
}
})
} else {
print(error!)
}
})
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! FriendCell
let user = userArray[indexPath.row]
//add user info to cells
cell.fullnameLabel.text = user.userFullName
cell.usernameLabel.text = user.userUsername
cell.objectID = user.userID!
cell.isPrivate = user.isPrivate
user.userProfileImage?.getDataInBackground (block: { (data, error) in
if error == nil {
cell.profilePhoto.image = UIImage(data: data!)
}
})
})
}
1) Multiple Selection:
You should have a User class (e.g User) that holds user properties instead of maintaining array for each property. Store User object in a Array. User class could be like below:
class User {
var userID:String
var userFullName:String
var userName:String
var userProfileImageUrl:String
init(userID:String,userFullName:String,userName:String,userProfileImageUrl:String) {
self.userID = userID
self.userFullName = userFullName
self.userName = userName
self.userProfileImageUrl = userProfileImageUrl
}
}
You could have a User extension to check if that user is selected or not(e.g isSelected).
import UIKit
import Foundation
private var selectedKey: UInt8 = 0
extension User {
var isSelected:Bool{
get {
return objc_getAssociatedObject(self, &selectedKey) as! Bool
}
set {
objc_setAssociatedObject(self, &selectedKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
Now in your func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell check that user.isSelected == true/false and update your selected/deselected image accordingly.
And update the value of isSelected in func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
2) Sending to Bottom Bar:
For bottom bar add a UICollectionView as a subview in UIView. Create a class overriding UICollectionViewCell that holds a UILabel. You can add flow layout in UICollectionView.
I have given just an idea to start with.Hope it will help you.
I think, you set bool check for every cell in tableView. If cell load again, it will not show check. Because, It check is false.
I need your help! I don´t know how to change an array that is inserted on a TableCell from information I have in another ViewController. It’s a little bit messed up, but I’m gonna show you by my code.
Here I have a ViewController conformed by many switches that correspond to different categories of coupons, this is the code:
class FiltersViewController: UIViewController {
#IBOutlet weak var restaurantsSwitch: UISwitch!
#IBOutlet weak var sportsSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func returnHome(_ sender: Any) {
let vc = self.storyboard!.instantiateViewController(withIdentifier: "home") as! HomeViewController
self.present(vc, animated: false, completion: nil)
}
#IBAction func restaurants(_ sender: UISwitch) {
if restaurantsSwitch.isOn == true{
tuxtlaSwitch.isOn = false
sevillaSwitch.isOn = false
coapaSwitch.isOn = false
coyoacanSwitch.isOn = false
universidadSwitch.isOn = false
polancoSwitch.isOn = false
}
}
#IBAction func sports(_ sender: UISwitch) {
if sportsSwitch.isOn == true{
tuxtlaSwitch.isOn = false
sevillaSwitch.isOn = false
coapaSwitch.isOn = false
coyoacanSwitch.isOn = false
universidadSwitch.isOn = false
polancoSwitch.isOn = false
}
}
}
I’ve only show you two switches at the example with the purpose of not filling this with many code, but there are like 15 switches.
And in the other ViewController, which is connected to this one, the HomeViewController, contains coupons that comes from a JSON, and conforms an array of ten items displayed on a TableViewCell, the code:
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var data : NSArray = []
var mainData : NSArray = []
var couponsImg : [UIImage] = []
var couponsTitle : [String] = []
var couponsDesc : [String] = []
var couponsCat : [String] = []
func getCoupons(){
let miURL = URL(string: RequestConstants.requestUrlBase)
let request = NSMutableURLRequest(url: miURL!)
request.httpMethod = "GET"
if let data = try? Data(contentsOf: miURL! as URL) {
do {
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSDictionary
let parseJSON = json
let object = parseJSON?["object"] as! NSDictionary
let mainCoupon = object["mainCoupon"] as! NSArray
let coupons = object["coupons"] as! NSArray
self.mainData = mainCoupon
self.data = coupons
self.couponImg1 = (mainCoupon[0] as AnyObject).value(forKey: "urlImage") as! String
self.couponImg2 = (mainCoupon[1] as AnyObject).value(forKey: "urlImage") as! String
self.couponTitle1 = (mainCoupon[0] as AnyObject).value(forKey: "nameStore") as! String
self.couponTitle2 = (mainCoupon[1] as AnyObject).value(forKey: "nameStore") as! String
self.couponDesc1 = (mainCoupon[0] as AnyObject).value(forKey: "promoDescription") as! String
self.couponDesc2 = (mainCoupon[1] as AnyObject).value(forKey: "promoDescription") as! String
self.couponCat1 = (mainCoupon[0] as AnyObject).value(forKey: "category") as! String
self.couponCat2 = (mainCoupon[1] as AnyObject).value(forKey: "category") as! String
self.couponsImg = [couponImage1!, couponImage2!, couponImage3!, couponImage4!, couponImage5!, couponImage6!, couponImage7!, couponImage8!, couponImage9!, couponImage10!]
self.couponsTitle = [couponTitle1, couponTitle2, couponTitle3, couponTitle4, couponTitle5, couponTitle6, couponTitle7, couponTitle8, couponTitle9, couponTitle10]
self.couponsDesc = [couponDesc1, couponDesc2, couponDesc3, couponDesc4, couponDesc5, couponDesc6, couponDesc7, couponDesc8, couponDesc9, couponDesc10]
self.couponsCat = [couponCat1, couponCat2, couponCat3, couponCat4, couponCat5, couponCat6, couponCat7, couponCat8, couponCat9, couponCat10]
} catch {
let error = ErrorModel()
error.phrase = "PARSER_ERROR"
error.code = -1
error.desc = "Parser error in get Notifications action"
}
}
}
#IBAction func showFilters(_ sender: Any) {
let vc = self.storyboard!.instantiateViewController(withIdentifier: "filters") as! FiltersViewController
self.present(vc, animated: false, completion: nil)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! HomeTableViewCell
cell.couponImg.image = couponsImg[indexPath.row]
cell.couponTitle.text = couponsTitle[indexPath.row]
cell.couponDescription.text = couponsDesc[indexPath.row]
cell.couponCategory.text = couponsCat[indexPath.row]
return cell
}
(Again I’ve only showed you two coupons for the example). The thing is that I need to apply some filters to the coupons on the TableCell. The first time the view appear it shows the 10 coupons correctly, but when I go to the filters an put it some of them ON it doesn’t make a difference, the method I was trying to use was something like this, first have an instance of the FiltersViewController class:
var filters = FilterViewController()
if filters.isMovingToParentViewController == true {
if filters.restaurantsSwitch.isOn == false {
self.couponsImg.remove(at: 0)
self.couponsImg.remove(at: 1)
self.couponsImg.remove(at: 2)
}
if filters.sportsSwitch.isOn == false {
self.couponsImg.remove(at: 3)
self.couponsImg.remove(at: 4)
self.couponsImg.remove(at: 5)
}
}
In the example bellow I’m trying to say that if a have the restaurant switch off, I’m going to delete the corresponding coupons of the restaurant category, and the same with the sports switch. But first of all I don’t know where to include this logic, in which method? And also I don’t know if this instruction is correct for my purposes. Can somebody give me a hand please???
Your logic is not working because you're instantiating a new FilterViewController, different from the FilterViewController associated with you screen.
You can solve this using delegate.
First, create the delegate:
protocol FilterDelegate {
func updateTable() }
Then, In your FilterViewController add this line:
weak var delegate:FilterDelegate?
You HomeViewController have to conform with this delegate, so:
class HomeViewController: FilterDelegate ... {
func updateTable() {
/* GET THE DATA FILTERED HERE */
tableview.reloadData()
}
In your FilterViewController:
#IBAction func returnHome(_ sender: Any) {
let vc = self.storyboard!.instantiateViewController(withIdentifier: "home") as! HomeViewController
self.delegate = vc
self.present(vc, animated: false, completion: nil)
delegate?.updateTable()
}
I think that should work.
EDIT:
Another approach is to create a segue between these two vcs and pass the which filters are active using the "prepare" function . Then you can take this information in your HomeVC and load your table based on the filters in the viewDidLoad function.
1 - Create a object Filters:
class Filters {
var tuxtlaSwitchIsOn: Bool
var sevillaSwitchIsOn: Bool
...
init(tuxtlaSwitchIsOn: Bool, sevillaSwitchIsOn: Bool, ...) {
self.tuxtlaSwitchIsOn = tuxtlaSwitchIsOn
self.sevillaSwitchIsOn = sevillaSwitchIsOn
...
}
}
2 - Add a attribute Filters to your HomeVC
class HomeViewController : ... {
...
var filtersActive: Filters?
...
}
3 - In your FilterViewController instantiate a Filter object indicating which filters are on
4 - In your FilterViewController prepare funs pass the Filter object to HomeVC
5 - In your HomeVC, get the Filter object and filter your data based on it.
Sure here is what you need. So you have a set of array filled with data and you want to apply filter on them. First, you need to create another array for filter results. This is because when user removes the filter, you still want to show the full list. To simplify, say you only have an array Foo: [String]. So you need to create another array called FooFiltered: [String] to hold the search result. Your can leave it empty when the view controller is loaded.
Next, in your filter section, it's recommended to use array filter technology like this post, but it's okay if you want to do it in your way. So all you need to do is to get elements from Foo array that match certain criteria and copy them into FooFiltered array. Here let me show you an example of doing filter manually
func filter() {
FooFiltered = [String]() //Clean up every time before search
for str in Foo {
if str == "criteria" {
FooFiltered.append(str)
}
}
}
Now you have a list of filtered items. You need a flag to tell table view which set of array to display. Say you have a flag called showSearchResult that is set to false originally. When you do the filter, set it to true. So your cellForRow will look like
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if showSearchResult {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
cell.textField.text = FooFiltered[indexPath.row]
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
cell.textField.text = Foo[indexPath.row]
return cell
}
}
You also need to update this flag to all your table view delegate method, like numberOfRowsInSection, etc.
Finally, with these codes, your table view is configured to show full results or filtered results base on the flag and you are setting that flag in the filter() function. The last thing to do is to ask tableView to reload data when the filter is done. So modify your filter function like this and you should be all set.
func filter() {
FooFiltered = [String]() //Clean up every time before search
showSearchResul = true
for str in Foo {
if str == "criteria" {
FooFiltered.append(str)
}
}
self.tableView.reloadData()
}
I've been on stack for a while now but never needed to ask a question as I've always found the answers after some searching, but now I'm stuck for real. I've been searching around and going through some trial and error for an answer and I keeping getting the same error. I'm basically making a profile page with a tableView on the bottom half of the screen. The top half is loading fine filling in the current user's information. All connections to the view controller and cell view controller seem good. The table view, however, will appear with no data and crash while loading with the fatal error:
unexpectedly found nil while unwrapping an optional value.
I also believe the cellForRowAtIndexPath is not being called at all because "test" is not printing to the logs.
I'm using the latest versions of Swift and Parse.
I'm relatively new to swift so I'll go ahead and post my entire code here and any help at all is appreciated.
import UIKit
import Parse
import ParseUI
class profileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
#IBOutlet var profilePic: UIImageView!
#IBOutlet var userName: UILabel!
#IBOutlet var userBio: UILabel!
var image: PFFile!
var username = String()
var userbio = String()
var content = [String]()
#IBAction func logout(sender: AnyObject) {
PFUser.logOut()
let Login = storyboard?.instantiateViewControllerWithIdentifier("ViewController")
self.presentViewController(Login!, animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
profilePic.layer.borderWidth = 1
profilePic.layer.masksToBounds = false
profilePic.layer.borderColor = UIColor.blackColor().CGColor
profilePic.layer.cornerRadius = profilePic.frame.height/2
profilePic.clipsToBounds = true
tableView.delegate = self
tableView.dataSource = self
self.tableView.rowHeight = 80
self.hideKeyboardWhenTappedAround()
if let nameQuery = PFUser.currentUser()!["name"] as? String {
username = nameQuery
}
if PFUser.currentUser()!["bio"] != nil {
if let bioQuery = PFUser.currentUser()!["bio"] as? String {
userbio = bioQuery
}
}
if PFUser.currentUser()!["icon"] != nil {
if let iconQuery = PFUser.currentUser()!["icon"] as? PFFile {
image = iconQuery
}
}
self.userName.text = username
self.userBio.text = userbio
if image != nil {
self.image.getDataInBackgroundWithBlock { (data, error) -> Void in
if let downIcon = UIImage(data: data!) {
self.profilePic.image = downIcon
}
}
}
// Do any additional setup after loading the view.
var postsQuery = PFQuery(className: "Posts")
postsQuery.whereKey("username", equalTo: username)
postsQuery.findObjectsInBackgroundWithBlock( { (posts, error) -> Void in
if error == nil {
if let objects = posts {
self.content.removeAll(keepCapacity: true)
for object in objects {
if object["postText"] != nil {
self.content.append(object["postText"] as! String)
}
self.tableView.reloadData()
}
}
}
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
print(content.count)
return content.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let profCell = self.tableView.dequeueReusableCellWithIdentifier("profCell", forIndexPath: indexPath) as! profTableViewCell
print("test")
profCell.userPic.layer.borderWidth = 1
profCell.userPic.layer.masksToBounds = false
profCell.userPic.layer.borderColor = UIColor.blackColor().CGColor
profCell.userPic.layer.cornerRadius = profCell.userPic.frame.height/2
profCell.userPic.clipsToBounds = true
profCell.userPic.image = self.profilePic.image
profCell.name.text = self.username
profCell.content.text = content[indexPath.row]
return profCell
}
}
I let it sit for a few days and I came back to realize a very dumb mistake I made. I working with around 15 view controllers right now and realized I had a duplicate of the one I posted above with the same name. I now understand why you say working with storyboards is very sticky. Though, I did not need it, I appreciate the help and I can say I learned a few things.
You probably need to register the class you are using for the custom UITableViewCell:
self.tableView.registerClass(profTableViewCell.self, forCellReuseIdentifier: "profCell")
Unless you're using prototyped cells in IB, this registration isn't done automatically for you.
As such when you call the dequeue method (with the ! forced unwrap) you're going to have issues. The dequeueReusableCellWithIdentifier:forIndexPath: asserts if you didn't register a class or nib for the identifier.
when you register a class, this always returns a cell.
The older (dequeueReusableCellWithIdentifier:) version returns nil in that case, and you can then create your own cell.
You should use a ? during the as cast to avoid the crash, although you'll get no cells!
One other reminder, you should always use capitals for a class name, ProfTableViewCell not profTableViewCell, it's just good pratice.
Much more information here in the top answer by iOS genius Rob Mayoff: Assertion failure in dequeueReusableCellWithIdentifier:forIndexPath:
You have to create a simple NSObject Class with image, username and userbio as optional values. Then you have to declare in your profileviewcontroller a var like this:
var allProfiles = [yourNSObjectClass]()
In your cellForRowAtIndexPath add:
let profile = yourNSObjectClass()
profile = allProfiles[indexPath.row]
cell.username.text = profile.username
And go on.
Use also this:
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
instead of this:
self.tableView.reloadData()
I have a table view controller and above the cell is a segmented control. The segmented control has 3 options. Past Posts, Current Posts, and Future Posts. I am trying to figure out how to load the specific data into the table view depending on what index is selected on the segmented control.
For example if Past Posts is selected I want to load the Past Post data from Parse Server into the table view. Or of Future Posts is selected load the Future Posts date from Parse Server into the table view.
I am not at all sure how to load the "selected" data, then remove and load different data if the index changes. Any help is much appreciated!
Also, I know how to fetch data from Parse Server. I only mention that to explain where my data is coming from.
I would do something creating a controller that performs the fetch, the parsing, and returns a closure with the associated identifier if it ever changes, you can still use this approach. Something along these lines.
UPDATE
With help from Rob's answer I wanted to put a little context into my answer for completeness.
typealias PostsCompletionClosure = (requestIdentifier : String, posts : [Post])->Void
class PostController {
func fetchPastPosts(requestIdentifier : String,
completion : PostsCompletionClosure,
queue : dispatch_queue_t?) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let queryParams = ["status" : "past"]
self.performQuery(queryParams, completion: { (requestID, posts) in
dispatch_async(queue != nil ? queue : dispatch_get_main_queue()) {
completion(requestIdentifier : requestIdentifier, posts : posts)
}
})
}
}
func fetchCurrentPosts(requestIdentifier : String,
completion : PostsCompletionClosure,
queue : dispatch_queue_t?) {
// Same as Above
}
func fetchFuturePosts(requestIdentifier : String,
completion : PostsCompletionClosure,
queue : dispatch_queue_t?) { {
// Same as Above
}
private func performQuery(queryParams: [String : String],
completion : PostsCompletionClosure) {
let query = PFQuery(className: "Posts")
for {key, value) in queryParams {
query.whereKey(key, equalTo: value)
}
query.findObjectsInBackgroundWithBlock { objects, error in
guard let error == nil else {
// Handle Error
return
}
if let results = objects as? [Post] {
dispatch_get_main_queue()) {
completion(requestIdentifier : requestIdentifier, posts : posts)
}
})
}
}
You can even create a post request queue for the segment requests, and cancel all prior operations if you are about to start a new one, thus never even giving it an opportunity to reload your data in the first place.
Here is a possible approach on how to implement the viewController :)
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
#IBOutlet var segnmentControl: UISegmentedControl!
var posts: [Post]?
var activeRequestId: String = ""
// This should prolly be injected or a singleton
let postsController = PostController()
override func viewDidLoad() {
super.viewDidLoad()
didSelectSelegment(segnmentControl)
}
#IBAction func didSelectSelegment(sender: UISegmentedControl) {
posts = nil
tableView.reloadData()
activeRequestId = "\(sender.selectedSegmentIndex)"
switch sender.selectedSegmentIndex {
case 0:
self.postsController.fetchPastPosts(activeRequestId, completion: { (requestIdentifier, posts) in
self.reloadDataWith(requestIdentifier, posts : [Post])
})
case 1:
self.postsController.fetchCurrentPosts(activeRequestId, completion: { (requestIdentifier, posts) in
self.reloadDataWith(requestIdentifier, posts : [Post])
})
case 2:
self.postsController.fetchFuturePosts(activeRequestId, completion: { (requestIdentifier, posts) in
self.reloadDataWith(requestIdentifier, posts : [Post])
})
default:
fatalError("unexpected segment index")
}
}
func reloadDataWith(requestIdentifier : String,
posts : [Post]) {
if self.requestIdentifier == requestIdentifier {
self.posts = posts
self.tableView.reloadData()
}
}
}
The basic idea would be that as the segmented control changes, you would initiate a PFQuery that would populate your model, and then trigger the reloading of the table. For example, something like:
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var posts: [Post]?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func didChangeValueForSegmentedControl(sender: UISegmentedControl) {
// first empty the table
posts = nil
tableView.reloadData()
// prepare new query
let query = PFQuery(className: "Posts")
switch sender.selectedSegmentIndex {
case 0:
query.whereKey("status", equalTo: "past")
case 1:
query.whereKey("status", equalTo: "current")
case 2:
query.whereKey("status", equalTo: "future")
default:
fatalError("unexpected segment index")
}
// now perform query
query.findObjectsInBackgroundWithBlock { objects, error in
guard error == nil else {
// report error
return
}
guard let searchResults = objects as? [Post] else {
// handle situation where results were not an array of `Post` objects
return
}
self.posts = searchResults
self.tableView.reloadData()
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts?.count ?? 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! PostCell
let post = posts![indexPath.row]
// configure `cell` using `post`
return cell
}
}
Now, those whereKey clauses are certainly not right, and that would change depending how your object model was set up, but this illustrates the basic idea. Initiate PFQuery on the basis of which segmented control was selected and then update the results accordingly.
Now, this all makes a lot of assumptions (that you've defined your table view and specified the view controller as its data source; that you've hooked up the outlet for the table view; that you've hooked up the IBAction for valueChanged on the segmented control; that you've defined a cell prototype with a custom cell type; etc.), but it illustrates the key parts of the solution.
I'm querying images from my Parse backend, and displaying them in order in a UITableView. Although I'm downloading and displaying them one at a time, they're appearing totally out of order in my table view. Each image (album cover) corresponds to a song, so I'm getting incorrect album covers for each song. Would someone be so kind as to point out why they're appearing out of order?
class ProfileCell: UITableViewCell {
#IBOutlet weak var historyAlbum: UIImageView!
}
class ProfileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var historyAlbums = [PFFile]()
var albumCovers = [UIImage]()
// An observer that reloads the tableView
var imageSet:Bool = false {
didSet {
if imageSet {
// Reload tableView on main thread
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) { // 1
dispatch_async(dispatch_get_main_queue()) { // 2
self.tableView.reloadData() // 3
}
}
}
}
}
// An observer for when each image has been downloaded and appended to the albumCovers array. This then calls the imageSet observer to reload tableView.
var dataLoaded:Bool = false {
didSet {
if dataLoaded {
let albumArt = historyAlbums.last!
albumArt.getDataInBackgroundWithBlock({ (imageData, error) -> Void in
if error == nil {
if let imageData = imageData {
let image = UIImage(data: imageData)
self.albumCovers.append(image!)
}
} else {
println(error)
}
self.imageSet = true
})
}
self.imageSet = false
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Queries Parse for each image
var query = PFQuery(className: "Songs")
query.whereKey("user", equalTo: PFUser.currentUser()!.email!)
query.orderByDescending("listenTime")
query.limit = 20
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if error == nil {
if let objects = objects as? [PFObject] {
for object in objects {
if let albumCover = object["albumCover"] as? PFFile {
// Appending each image to albumCover array to convert from PFFile to UIImage
self.historyAlbums.append(albumCover)
}
self.dataLoaded = true
}
}
} else {
println(error)
}
self.dataLoaded = false
})
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var profileCell = tableView.dequeueReusableCellWithIdentifier("ProfileCell", forIndexPath: indexPath) as! ProfileCell
profileCell.historyAlbum.image = albumCovers[indexPath.row]
return profileCell
}
}
}
The reason you are getting them out of order is that you are firing off background tasks for each one individually.
You get the list of objects all at once in a background thread. That is perfectly fine. Then once you have that you call a method (via didset) to iterate through that list and individually get each in their own background thread. Once each individual thread is finished it adds it's result to the table array. You have no control on when those background threads finish.
I believe parse has a synchronous get method. I'm not sure of the syntax currently. Another option is to see if you can "include" the image file bytes with the initial request, which would make the whole call a single background call.
Another option (probably the best one) is to have another piece of data (a dictionary or the like) that marks a position to each of your image file requests. Then when the individual background gets are finished you know the position that that image is supposed to go to in the final array. Place the downloaded image in the array at the location that the dictionary you created tells you to.
That should solve your asynchronous problems.