If/Else based on existence of object in array? - ios

I want to trigger an else statement if there is no object at the indexPath i look for in an array.
The array is
let exerciseSets = unsortedExerciseSets.sorted { ($0.setPosition < $1.setPosition) }
Then i use let cellsSet = exerciseSets[indexPath.row]
There is a chance, when a user is adding a new cell, that it wont already have an exerciseSet at the indexPath to populate it (adding a new set to an existing array of sets) and so I need to run an else statement to set up a blank cell rather than attempting to populate it and crashing my app.
However if i use if let then i get this error:
Initializer for conditional binding must have Optional type, not
'UserExerciseSet'
Here is the whole function for context if needed:
func configure(_ cell: NewExerciseTableViewCell, at indexPath: IndexPath) {
if self.userExercise != nil {
print("RESTORING CELLS FOR THE EXISTING EXERCISE")
let unsortedExerciseSets = self.userExercise?.exercisesets?.allObjects as! [UserExerciseSet]
let exerciseSets = unsortedExerciseSets.sorted { ($0.setPosition < $1.setPosition) }
if let cellsSet = exerciseSets[indexPath.row] { // this causes a creash when user adds new set to existing exercise as it cant populate, needs an else statement to add fresh cell
cell.setNumber.text = String(cellsSet.setPosition)
cell.repsPicker.selectRow(Int(cellsSet.setReps), inComponent: 0, animated: true)
let localeIdentifier = Locale(identifier: UserDefaults.standard.object(forKey: "locale") as! String)
let setWeight = cellsSet.setWeight as! Measurement<UnitMass>
let formatter = MassFormatter()
formatter.numberFormatter.locale = localeIdentifier
formatter.numberFormatter.maximumFractionDigits = 2
if localeIdentifier.usesMetricSystem {
let kgWeight = setWeight.converted(to: .kilograms)
let finalKgWeight = formatter.string(fromValue: kgWeight.value, unit: .kilogram)
let NumericKgResult = finalKgWeight.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
cell.userExerciseWeight.text = NumericKgResult
} else {
let lbsWeight = setWeight.converted(to: .pounds)
let finalLbWeight = formatter.string(fromValue: lbsWeight.value, unit: .pound)
let NumericLbResult = finalLbWeight.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
cell.userExerciseWeight.text = NumericLbResult
}
} else {
cell.setNumber.text = String((indexPath.row) + 1)
}

You could do something crazy like this:
if let cellSet = (indexPath.row < exerciseSets.count ? exerciseSets[indexPath.row] : nil) {
//
}
but it would be more straightforward to do:
if indexPath.row < exerciseSets.count {
let cellSet = exerciseSets[indexPath.row]
...
}

OK, so your problem is that you are trying to access a value in an array that may or may not be there.
If you just try to access the value based on indexPath you can crash because indexPath may refer to a value not there. On the other hand, the array does not return an optional so you can't use if let either.
I kind of like the idea of using an optional, so how about introducing a function that could return an optional to you if it was there.
Something like:
func excerciseSet(for indexPath: IndexPath, in collection: [UserExcerciseSet]) -> UserExcerciseSet? {
guard collection.count > indexPath.row else {
return nil
}
return collection[indexPath.row]
}
and then you can say:
if let cellsSet = exerciseSet[for: indexPath, in: excerciseSets] {
//It was there...showtime :)
} else {
cell.setNumber.text = String((indexPath.row) + 1)
}
Hope that helps you.

Just check the index against your array count:
if indexPath.item < exerciseSets.count {
// Cell exists
let cellsSet = exerciseSets[indexPath.row]
} else {
// cell doesn't exists. populate new one
}

Related

How to use Firestore query pagination with TableVIew

I am trying to use Firestore pagination with swift TableView. Here is my code which loads the first 4 posts from firestore.
func loadMessages(){
let postDocs = db
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.limit(to: 4)
postDocs.addSnapshotListener { [weak self](querySnapshot, error) in
self?.q.async{
self!.posts = []
guard let snapshot = querySnapshot else {
if let error = error {
print(error)
}
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
let nextDocs = Firestore.firestore()
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.start(afterDocument: lastSnapshot)
if let postsTemp = self?.createPost(snapshot){
DispatchQueue.main.async {
self!.posts = postsTemp
self!.tableView.reloadData()
}
}
}
}
}
func createPost(_ snapshot: QuerySnapshot) ->[Post]{
var postsTemp = [Post]()
for doc in snapshot.documents{
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String,
let userName = doc.get(K.FStore.poster) as? String,
let uID = doc.get(K.FStore.userID) as? String,
let postDate = doc.get("postTime") as? String,
let votesForLeft = doc.get("votesForLeft") as? Int,
let votesForRight = doc.get("votesForRight") as? Int,
let endDate = doc.get("endDate") as? Int{
let post = Post(firstImageUrl: firstImage,
secondImageUrl: secondImage,
firstTitle: firstTitle,
secondTitle: secondTitle,
poster: userName,
uid: uID,
postDate: postDate,
votesForLeft: votesForLeft,
votesForRight:votesForRight,
endDate: endDate)
postsTemp.insert(post, at: 0)
}else{
}
}
return postsTemp
}
Here is my delegate which also detects the end of the TableView:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
cell.delegate = self
let seconds = post.endDate
let date = NSDate(timeIntervalSince1970: Double(seconds))
let formatter = DateFormatter()
formatter.dateFormat = "M/d h:mm"
if(seconds <= Int(Date().timeIntervalSince1970)){
cell.timerLabel?.text = "Voting Done!"
}else{
cell.timerLabel?.text = formatter.string(from: date as Date)
}
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
cell.userName.setTitle(post.poster, for: .normal)
cell.firstImageView.layer.cornerRadius = 8.0
cell.secondImageView.layer.cornerRadius = 8.0
if(indexPath.row + 1 == posts.count){
print("Reached the end")
}
return cell
}
Previously I had an addSnapshotListener without a limit on the Query and just pulled down all posts as they came. However I would like to limit how many posts are being pulled down at a time. I do not know where I should be loading the data into my model. Previously it was being loaded at the end of the addSnapshotListener and I could still do that, but when do I use the next Query? Thank you for any help and please let me know if I can expand on my question any more.
There is a UITableViewDelegate method called tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) that will be called just before a cell is loading.
You could use this one to check if the row at IndexPath is in fact the cell of the last object in your tableview's datasource. Something like datasource.count - 1 == IndexPath.row (The -1 is to account for item 0 being the first item in an array, where as it already counts as 1).
If that object is indeed the last one in your datasource, you could make a call to Firebase and add items to the datasource. Before mutating the datasource, make sure to check the new number of objects the show (the ones already loaded + new ones) has to be larger than the current number of objects in the datasource, otherwise the app will crash.
You also might want to give your user a heads up that you're fetching data. You can trigger that heads up also in the delegate method.

Problem with deleting an entity from other ViewController in CoreData

people. Im learning how to work with CoreData and i have a question.
I have a "WordArrayEntity" which have oneToMany relationship with "WordEntity" and Nulify deletion rule.
So at first when my app start im fetching info from my CoreData to special array
var wordEntities:[[WordEntity]] = []
After it my tableView recieves an attribute that it needs.
As i understand i can delete entities by this method
context.delete(wordEntity)
everything works fine when i do this on my tableView.
But, when i move to editor ViewController and try to add new Entity or delete previous nothing happens.
This is my code to move to next view controller
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let controller = self.storyboard?.instantiateViewController(withIdentifier: "ShowWordVC") as? ShowWordVC {
controller.word = SingleTonForEntities.shared.wordEntities[indexPath.section][indexPath.row]
controller.wordIndex = indexPath.row
self.present(controller, animated: true)
}
}
This is my method to delet word, which perfectly works at tableView
func deleteWordFromVocabulary(word: WordEntity,indexPath: Int) {
var wordStartsFromLetter = false
if let firstCharacter:Character = word.englishWord?.first {
var index = 0
for array in wordEntities {
if SingleTon.shared.sectionName[index].lowercased() == firstCharacter.lowercased() {
var newArray = array
context.delete(word)
newArray.remove(at: indexPath)
wordEntities[index] = newArray
wordStartsFromLetter = true
}
index += 1
}
}
if wordStartsFromLetter == false {
var newArray = wordEntities[26]
context.delete(word)
newArray.remove(at: indexPath)
wordEntities[26] = newArray
}
saveContext()
}
When i try to save word after editing or a new one i have the following code
#IBAction func saveButtonIsPressed(_ sender: UIButton) {
keyboardDissapears()
guard let englishWord = self.englishWordTextField.text,
!englishWord.isEmpty,
let wordImage = addedImageView.image,
let belarusianWord = self.belarusianWordTextField.text,
!belarusianWord.isEmpty,
let englishDefinition = self.englishDefinitionTextView.text,
!englishDefinition.isEmpty,
let belarusianDefinition = self.belarusianDefinitionTextView.text,
!belarusianDefinition.isEmpty else {
createAndShowAlert()
return
}
if editModeIsActivated == true {
if let wordToDelete = word, let wordIndex = wordIndex {
SingleTonForEntities.shared.deleteWordFromVocabulary(word: wordToDelete, indexPath: wordIndex)
}
}
word!.englishDefinition = englishDefinition
word!.belarusianDefinition = belarusianDefinition
word!.englishWord = englishWord
word!.belarusianWord = belarusianWord
let data = wordImage.pngData()
word!.wordImage = data
SingleTonForEntities.shared.addNewWordToVocabulary(word: self.word)
self.createAndShowDoneProgressAlert()
}
at first im checking if my fields are empty. If they aren't empty and we are in edit mode i delete the "WordEntity" from our context and then from our array.
And then i try to save a new word and add it to context with this method
func addNewWordToVocabulary(word: WordEntity!) {
var wordStartsFromLetter = false
word.englishWord = word.englishWord!.trimmingCharacters(in: .whitespacesAndNewlines)
if let firstCharacter:Character = word.englishWord?.first {
var index = 0
for array in wordEntities {
if SingleTon.shared.sectionName[index].lowercased() == firstCharacter.lowercased() {
var newArray = array
newArray.append(word)
for element in wordEntitesArray {
if element.arrayName == SingleTon.shared.sectionName[index].lowercased() {
element.addToWordEntities(word)
}
}
wordEntities[index] = newArray
wordStartsFromLetter = true
}
index += 1
}
}
if wordStartsFromLetter == false {
var newArray = wordEntities[26]
newArray.append(word)
wordEntities[26] = newArray
}
saveContext()
}
And there is a question. What am i doing wrong?
When i try to add new word to vocabulary - my app crashes.
But when im in edit mode and after it adding a new word to vocabulary - it just returns an empty tableViewCell.
I am new to CoreData and working with for about a week, but i would be glad to here what am i doing wrong and what i should do in such situations.
P.S. Everything worked well with UserDefaults

QueryEndingAtValue function does not work correctly in Swift firebase

I have been implementing code that is to enable paging scroll to fetch data by a certain amount of data from firebase database.
Firstly, then error says
Terminating app due to uncaught exception 'InvalidQueryParameter',
reason: 'Can't use queryEndingAtValue: with other types than string in
combination with queryOrderedByKey'
The below here is the actual code that produced the above error
static func fetchPostsWith(last_key: String?, completion: #escaping (([PostModel]?) -> Void)) {
var posts = [PostModel]()
let count = 2
let ref = Database.database().reference().child(PATH.all_posts)
let this_key = UInt(count + 1)
let that_key = UInt(count)
let this = ref.queryOrderedByKey().queryEnding(atValue: last_key).queryLimited(toLast: this_key)
let that = ref.queryOrderedByKey().queryLimited(toLast: that_key)
let query = (last_key != nil) ? this : that
query.observeSingleEvent(of: .value) { (snapshot) in
if snapshot.exists() {
for snap in snapshot.children {
if let d = snap as? DataSnapshot {
let post = PostModel(snapshot: d)
print(post.key ?? "")
posts.append(post)
}
}
posts.sort { $0.date! > $1.date! }
posts = Array(posts.dropFirst())
completion(posts)
} else {
completion(nil)
}
}
}
What it tries to do is to fetch a path where all posts are stored by auto id. But the error keeps coming out so I do not know what is wrong. Do you have any idea?
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// index is the last and last fetched then
print(self.feeds.count - 1 == indexPath.row, "index ", self.hasFetchedLastPage, "has fetched last")
if self.feeds.count - 1 == indexPath.row {
let lastKey = self.feeds[indexPath.row].key
if lastKey != self.previousKey {
self.getFeeds(lastKey: lastKey)
} else {
self.previousKey = lastKey ?? ""
}
}
print("Cell Display Number", indexPath.row)
}
func getFeeds(lastKey: String?) {
print(self.isFetching, "is fetching")
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
print(self.isFetching, "is fetching")
FirebaseModel.fetchPostsWith(last_key: lastKey) { ( d ) in
self.isFetching = false
if let data = d {
if self.feeds.isEmpty { //It'd be, when it's the first time.
self.feeds.append(contentsOf: data)
self.tableView.reloadData()
print("Initial Load", lastKey ?? "no key")
} else {
self.hasFetchedLastPage = self.feeds.count < 2
self.feeds.append(contentsOf: data)
self.tableView.reloadData()
print("Reloaded", lastKey ?? "no key")
}
}
}
}
I want to implement a paging enabled tableView. If you can help this code to be working, it is very appreciated.
You're converting your last_key to a number, while keys are always strings. The error message tells you that the two types are not compatible. This means you must convert your number back to a string in your code, before passing it to the query:
let this = ref.queryOrderedByKey().queryEnding(atValue: last_key).queryLimited(toLast: String(this_key))
let that = ref.queryOrderedByKey().queryLimited(toLast: String(that_key))

Populate SwiftyPickerPopover choices with array

I'm using SwiftyPickerPopover to display popup control in order for user to select a value. Previously at another place in the app I've implemented it as:
let displayStringFor:((String?)->String?)? = { string in
if let s = string {
switch(s){
case "Lhe”:
return "Lh”
case "Khi":
return "Khi”
case "Isb”:
return "Isb"
case "Guj":
return "Guj"
default:
return s
}
}
return nil
}
let p = StringPickerPopover(title: "Select City", choices: ["Lhe”,”Khi”,”Isb”,”Guj”])
.setDisplayStringFor(displayStringFor)
.setDoneButton(
action: { popover, selectedRow, selectedString in
self.cityButton.setTitle(selectedString,for: .normal)
if selectedRow == 0 {
self.cityImage.image = UIImage(named: "Lhe")
} else if selectedRow == 1 {
self.cityImage.image = UIImage(named: "Khi")
} else if selectedRow == 2 {
self.cityImage.image = UIImage(named: "Isb")
} else if selectedRow == 3 {
self.cityImage.image = UIImage(named: "Guj")
}
})
.setCancelButton(action: {_, _, _ in
})
p.appear(originView: sender as! UIView, baseViewController: self)
Which you can see the values are hardcoded here. Now I've an API that gives me all the cities. So I made var mainCitiesArray = [City]() and called the API and parsed data to it.
In mainCitiesArray I've all the cities now which I want to display in this popup. How can I do that?
Do you need just to parse the cities names to string array?
E.G.
let citiesNamesArray = mainCitiesArray.map({
(city: City) -> String in
return city.name
})
And you can pass citiesNamesArray to choises parameter.

Object filter crashes if no results can be found

I have written some code to find a user's favourites out of an array of custom objects. It works absolutely fine unless that object does not exist, in which case it just crashes. I was considering completely rewriting the code a different way, but I imagine there is probably a way to fix it... I just can't figure out how.
Here is my code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("rideCell", forIndexPath: indexPath) as! RideCell
var ride = DataManager.sharedInstance.getRideByName(favouritesArray[indexPath.row] as! String)
if ride != nil {
cell.rideNameLabel.text = ride!.name
var dateFormat = NSDateFormatter()
dateFormat.dateFormat = "h:mm a"
cell.updatedLabel.text = dateFormat.stringFromDate(ride!.updated!)
if ride!.waitTime! == "Closed" {
cell.waitTimeLabel.text = ride!.waitTime!
} else {
cell.waitTimeLabel.text = "\(ride!.waitTime!)m"
}
}
return cell
}
func getRideByName(name: String) -> Ride? {
let result = self.rideArray.filter({
$0.name == name
})
return result[0]
}
Like I say, it works fine if the Strings in favouritesArray can be found, however it crashes if not.
Can anyone suggest what changes I could make to stop the crash, and to just get it to return nil?
Thanks!
You can use the first property on the filtered array:
return result.first
which returns an optional. A better option, though, is to use the indexOf() function (if you're in Swift 2), as it doesn't build an entire array, and stops looking through the rest of the rideArray once it finds what you're looking for:
return self.rideArray.indexOf { $0.name == name }.map { self.rideArray[$0] }
You need to check result's length - you can do so by replacing
return result[0]
with
return result.count == 0 ? nil : result[0]

Resources