getting names displaying from firebase - ios

I am trying to display the contents of a firebase database. I know that I am reading them correctly as I am able to print them as they are read in. The problem is when I call the method to display them on screen, they are "out of range".
I know this means the the methods are being called simultaneously therefore the array is empty. I have tried the "Sleep()" method and doesn't work.
//arrays of names and descriptions
var Names:[String] = []
var Desctiptions: [String] = []
inital method
override func viewDidLoad() {
super.viewDidLoad()
getRestauraunt()
//create slides
scrollView.delegate = self
slides = createSlides()
setupSlideScrollView(slides: slides)
}
func getRestauraunt(){
let db = Firestore.firestore()
db.collection("Test").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
//print("Names: ",name," Description: ",description)
self.Names.append(name)
self.Desctiptions.append(description)
}
}
}
}
create slides method
func createSlides() -> [Slide] {
//firebase link
let slide1:Slide = Bundle.main.loadNibNamed("Slide", owner: self, options: nil)?.first as! Slide
slide1.labelTitle.text = Names[0]
}
I would like if someone could show me how to get the 'createSlides()' method to wait until the 'getRestauraunts()' method has finished. Thank you

Just call it from the end of the getrestaurant()'s getDocuments closure
func getRestauraunt(){
//as before...
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
self.Names.append(name)
self.Desctiptions.append(description)
}
self.createSlides()
}
}
}
As an aside, it might also be worth creating a simple Document struct with name and description properties, and just having the one array: [Document]

Related

Filtering data fetched from API, so that objects can be searched by name in a search bar

I am quite a beginner when it comes to Swift and programming in general so I apologise if my question is not very concise.
I am trying to build an iOS application in which users can find any kind of house plant. For each plant there is an image and some general info such as common name, Latin name, watering, height, etc. For this I am using the API at this link: "https://rapidapi.com/mnai01/api/house-plants2/"
For about a week I have been trying to implement a search bar so that users would be able to easily search for plants based on their common name. The problem is that it does not work and no plants are being returned when I search for one.
How I tried to implement it:
In the APICaller class I created a function which is meant to compare the common name of the plant with the text the user inputs in the search bar, and if they match then those plants would be displayed
func searchForPartialMatch(in plantsArray: [[String: Any]], for searchString: String) -> [[String:Any]]
{
return plantsArray.filter {
($0["Latin name"] as? String)?.lowercased().contains(searchString.lowercased()) == true || (($0["Common name"] as? [String])?.filter { $0.lowercased().contains(searchString.lowercased())
}.count ?? 0) > 0
}
}
Then, in order to fetch the data from the API, I implemented the getAllSearch function. The route used for the data fetch is meant to return all the plants from the API, and this is a bit tricky because there is no query parameter which could be used to make the plants searchable by it. What I tried to do in this function is to fetch only the data for the plants whose common names match the text in the search bar, by filtering them.
func getAllSearch (completion: #escaping (Result<[Plant], Error>) -> Void) {
let urlString = "\(Constants.baseURL)/all/?rapidapi-key=\(Constants.API_KEY)"
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in
guard let data = data, error == nil else {return}
do {
let results = try JSONDecoder().decode([Plant].self, from: data)
let plantsArray = results.map { $0.toDictionary() }
// Filter the results, example: Snake plant
let filteredArray = self.searchForPartialMatch(in: plantsArray, for: "Snake plant")
// for the line below I get the errors "Argument type '[String : Any]' does not conform to expected type 'Decoder'" and "Incorrect argument label in call (have 'dictionary:', expected 'from:')"
let filteredPlants = filteredArray.map { Plant(dictionary: $0) }
completion(.success(filteredPlants))
} catch {
completion(.failure(APIError.failedTogetData))
}
}
task.resume()
}
In the SearchViewController these are the pieces of code which have to do with the search bar data:
First I declared these variables:
public var plants: [Plant] = [Plant]()
public var filteredPlants: [Plant] = []
This is my searchController:
private let searchController: UISearchController = {
let controller = UISearchController(searchResultsController: SearchResultsViewController())
let textFieldInsideSearchBar = controller.searchBar.value(forKey: "searchField") as? UITextField
textFieldInsideSearchBar?.textColor = UIColor.white
textFieldInsideSearchBar?.attributedPlaceholder = NSAttributedString(string: "Search for a plant",
attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
return controller
}()
in the viewDidLoad() I have these:
navigationItem.searchController = searchController
searchController.searchResultsUpdater = self
then I implemented a filtering function once again, which I don't know if it is needed anymore
func filterContentForSearchTest(searchText: String, name: String = "Janet") {
filteredPlants = plants.filter({ (plant: Plant) in
let doesTextMatch = (name == "Snake plant") || (plant.common_name?.first == name)
if isSearchBarEmpty() {
return doesTextMatch
} else {
return doesTextMatch && plant.common_name!.first!.lowercased().contains(searchText.lowercased())
}
})
}
func isSearchBarEmpty() -> Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
and finally, this is the function which updates the search results:
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
guard let name = searchBar.text,
let resultsController = searchController.searchResultsUpdater as? SearchResultsViewController else {
return
}
filterContentForSearchTest(searchText: searchController.searchBar.text!, name: name)
APICaller.shared.getAllPlants() { result in
switch result {
case .success(let plants):
resultsController.plants = plants
DispatchQueue.main.async {
resultsController.searchResultsCollectionView.reloadData()
print("Filtered Plants: ", self.plants)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
My question is where exactly am I going wrong and what can I do to fix it so that the searched plants can be returned properly. Any kind of help and piece of advice is welcomed. Once again I apologise if the question isn't asked properly, I am a baby developer and there is still very much to learn xD Thank you for taking the time to read it all.

'Invalid document reference. Document references must have an even number of segments' when trying to edit/delete new entry

I am able to delete the logged in users document when my app is first loaded. However, if I create a new entry, long press the new entry from the table view and select delete, I get a crash. I thought it had something to do with the document ID not being saved but I couldn't figure out why. If the same newly created entry is deleted after the app is closed and reopened then it will delete with no problem, but if I leave the app open and delete immediately after creating a new document, it will crash.
class BudgetViewController: UIViewController: {
var budgetData = [Transaction]()
func showAdd() {
let modalViewController = AddCategory()
modalViewController.addCategoryCompletion = { newCategories in
self.budgetData.append(newCategories)
self.tableView.reloadData()
}
modalViewController.modalPresentationStyle = .overFullScreen
modalViewController.modalTransitionStyle = .crossDissolve
modalViewController.selectionDelegate = self
present(modalViewController, animated: true, completion: nil)
}
#objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer){
if gestureRecognizer.state == .began {
let touchPoint = gestureRecognizer.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
let cell = CategoryCell()
var data = budgetData[indexPath.row]
let modalViewController = EditCategory()
modalViewController.deleteCategory = { row in
self.deletedRow = row
self.deleteRow()
}
modalViewController.documentID = data.trailingSubText ?? ""
modalViewController.modalPresentationStyle = .overFullScreen
modalViewController.modalTransitionStyle = .crossDissolve
present(modalViewController, animated: true, completion: nil)
modalViewController.row = indexPath.row
print("longpressed\(indexPath.row)\(data.trailingSubText)")
}
}
}
override func viewDidLoad() {
loadNewData()
}
func loadNewData() {
guard let user = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(user).collection("Category").getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
for document in snapshot!.documents {
let data = document.data()
let title = data["title"] as? String ?? ""
let uid = data["uid"] as? String ?? ""
let documentID = document.documentID
// let timeStamp = data["timeStamp"] as? Date
let newSourse = Transaction(title: title, dateInfo: "0% out of spent", image: UIImage.gymIcon, amount: 12, annualPercentageRate: 12, trailingSubText: documentID, uid: uid)
self.budgetData.append(newSourse)
}
self.tableView.reloadData()
}
}
}
class AddCategory: UIViewController {
#objc func saveAction(){
guard let uid = Auth.auth().currentUser?.uid else { return }
let newCategory = Transaction(title: textField.text ?? "", dateInfo: "0% out of spent", image: UIImage.gymIcon, amount: 12, annualPercentageRate: 23, trailingSubText: "", uid: uid)
db.collection("users").document(uid).collection("Category").addDocument(data: newCategory.dictionary)
self.dismiss(animated: false, completion: {
self.addCategoryCompletion?(newCategory)
})
self.dismiss(animated: false, completion: nil)
print("selected")
}
}
}
class EditCategory: UIViewController {
func deleteAction(){
guard let user = Auth.auth().currentUser?.uid else { return }
print("document::\(self.documentID)")
// let budget = textField.text
db.collection("users").document(user).collection("Category").document(documentID).delete { (err) in
if let err = err {
print(err.localizedDescription)
}else{
self.dismiss(animated: false, completion: {
self.deleteCategory?(self.row)
})
print("deleted successfully")
}
}
}
}
The error is strongly suggesting that user is nil or empty at the time you run this code:
guard let user = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(user).collection("Category").getDocuments()
This almost certainly means that a user was not signed in at the time. Your code needs to check currentUser for nil before trying to access its uid property. nil means that no user is currently signed in.
The user will not be signed in immediately at app launch. You should use an auth state listener to get a callback when the user object becomes available.
Maybe not the best approach, but it is working now. I called
self.budgetData.removeAll()
self.loadNewData()
self.tableView.reloadData()
inside of my callback
modalViewController.addCategoryCompletion = { newCategories in
self.budgetData.append(newCategories)
self.tableView.reloadData()
}`
Based on the presented code, when a new category is added to Firebase
let newCategory = Transaction(title: textField.text ?? ...)
db.collection("users").document(uid).collection("Category").addDocument(data: newCategory
you're not getting a valid documentId from Firebase first. So therefore that object exists in your dataSource with no documentId so when you try to remove it, there's a crash.
A couple of options
Option 1: Create a firebase reference first, which will provide a Firebase documentId that you can add to the object when writing. See the docs. Like this
let newCategoryRef = db.collection("Category").document()
let docId = newCategoryRef.documentId
...add docId to the category object, then add to dataSource
or
Option 2: Add an observer to the node (see Realtime Updates) so when a new document is written, the observers event will fire and present the newly added document, which will contain a valid documentId, and then craft an category object based on that data and add that object to your dataSource array. In this case, you don't need to add it to the dataSource array when writing as it will auto-add after it's written based on the observers .added event.

SLComposeServiceViewController refresh configurationItems?

I followed this guide on how to setup a SLComposeViewController, and it includes on how to set up configuration items.
I'm pulling from a MongoDB database to get the most recently modified item. Here is my configurationItems():
override func configurationItems() -> [Any]! {
let configurationItems = [editConfigurationItem]
return configurationItems
}
and here is my editConfigurationItem variable:
lazy var editConfigurationItem: SLComposeSheetConfigurationItem = {
func getFirstCategory(completion: #escaping(String) -> ()) {
DispatchQueue.main.async {
let email: String = customKeychainWrapperInstance.string(forKey: "email") ?? ""
let password: String = customKeychainWrapperInstance.string(forKey: "password") ?? ""
app.login(credentials: Credentials.emailPassword(email: email, password: password)) { (maybeUser, error) in
DispatchQueue.main.sync {
guard error == nil else {
print("Login failed: \(error!)");
return
}
guard let _ = maybeUser else {
fatalError("Invalid user object?")
}
print("Login succeeded!");
}
let user = app.currentUser!
let configuration = user.configuration(partitionValue: user.id)
let predata = try! Realm(configuration: configuration)
let unsortedData = predata.objects(Tiktoks.self)
let data = unsortedData.sorted(byKeyPath: "date", ascending: false)
let firstCategory = data[0].category
let firstCategoryId = data[0]._id
print(firstCategory)
print(firstCategoryId)
self.selectedId = firstCategoryId
completion(firstCategory)
}
}
print("done")
}
let item = SLComposeSheetConfigurationItem()!
item.title = "collection"
item.valuePending = true
getFirstCategory() { (firstCategory) in
item.valuePending = false
print("completed")
item.value = firstCategory // Using self as the closure is running in background
}
item.tapHandler = self.editConfigurationItemTapped
return item
}()
(Sorry for the messiness of the code, I'm new to Swift)
So far it works, but the item.value variable doesn't get updated in the UI. It "infinitely loads" until you click on the configuration item. When the configuration item is tapped to go to another view though, the actual variable from the database shows for a split second before showing the next view. When you go back from that other view, the actual variable is there.
It looks like to me that the configuration item isn't updating. I see that there is a reloadConfigurationItems(), but by putting that in my editConfigurationItem will cause a loop I would think (and it also errors out too). The documentation even says:
In particular, you don’t need to call this method after you change a configuration item property, because the SLComposeServiceViewController base class automatically detects and responds to such changes.
but it looks like it's not detecting the changes.
Is there a way to refresh item.value and item.valuePending?

Retrieving Firebase Database String and converting String to Integer for label not working

Saving data to Firebase and retrieving data to display in label is working but when I try to add an Int to the label it overwrites the label data.
I add to the label with
var pointsCount = 0
func addPoints(_ points: NSInteger) {
pointsCount += points
pointsLabel.text = "\(self.pointsCount)"
}
Then I save the label contents to Firebase as a string.
func saveToFirebase() {
let userID = Auth.auth().currentUser!.uid
let points: String = ("\(self.pointsCount)")
let savedScores = ["points": points,
"blah": blah,
"blahblah": blahblah]
Database.database().reference().child("users").child(userID).updateChildValues(savedScores, withCompletionBlock:
{
(error, ref) in
if let error = error
{
print(error.localizedDescription)
return
}
else
{
print("Data saved successfully!")
}
})
}
I retrieve the string from the Realtime Database and convert it to an Int.
func retrieveFromFirebase() {
guard let userID = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(userID).child("points").observeSingleEvent(of: .value) {
(snapshot)
in
guard let points = snapshot.value as? String else { return }
let pointsString = points
if let pointsInt = NumberFormatter().number(from: pointsString) {
let retrievedPoints = pointsInt.intValue
self.pointsLabel.text = "\(retrievedPoints)"
} else {
print("NOT WORKING")
}
}
The label displays the retrieved data from the database perfectly.
Then if I try to add more points to the label, it erases the retrieved data and starts adding from 0 as if the label displayed nil.
I've been searching for answers all day for what seems to be a rather simple problem but haven't been able to figure it out due to my lack of experience.
I have tried separating everything and saving the data as an integer and retrieving the data back as an integer but the issue seems to be from the addPoints function.
Please let me know what I'm doing wrong.
The solution ended up being as simple as adding an 'if' statement to the points function.
Instead of...
func addPoints(_ points: NSInteger) {
pointsCount += points
pointsLabel.text = "\(self.pointsCount)"
}
It needed to be...
func addPoints(_ points: NSInteger) {
if let text = self.pointsLabel.text, var pointsCount = Int(text)
{
pointsCount += points
pointsLabel.text = "\(pointsCount)"
}
}

Fetched data from core data missing some info

I have two entities in my core data model: CardCollection (class name CardCollectionMO) and Card (CardMO). A CardCollection can have many cards. Here's my code for saving the CardCollection with a set of new cards, which seems to be working correctly.
func saveCardInfo() {
let currentDate = createDate()
let collectionNumber = arc4random_uniform(1000000)
let cardcollection = CardCollectionMO.insertNewCollection(collectionNumber: Int64(collectionNumber), collectionTitle: collectionTitle.text!, collectionDescription: collectionDescription.text!, numberOfCards: Int16(cards.count), dateCreated: String(currentDate))
var newCard = Set<CardMO>()
for card in cards {
if let addedCard = CardMO.insertNewCard(questionText: card.questionText, answerText: card.answerText, cardNum: Int16(card.cardNum), questionImage: UIImageJPEGRepresentation(card.questionImage, 1)! as NSData) {
addedCard.cardcollection = cardcollection
newCard.insert(addedCard)
}
}
cardcollection?.addToCards(newCard as NSSet)
}
But, strange things happen when fetching the data back. Here's the code.
func fetchData() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedObjectContext = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<CardCollectionMO>(entityName: "CardCollection")
do {
let results = try managedObjectContext.fetch(request)
if results.count > 0 {
for result in results {
if let collectionTitle = result.collectionTitle {
print(collectionTitle)
}
if let listOfCards = result.cards?.allObjects as? [CardMO] {
for card in listOfCards {
if let questionText = card.questionText {
print(questionText)
}
}
The code above works fine while the app is running but if I stop and restart the app, the last card in each collection is removed. I've done a listOfCards.count and its like it doesn't exist. But, if I just fetch all the cards from the Card entity, they're there. Any thoughts on what I'm doing wrong and why the last card in the collection isn't being fetched?

Resources