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?
Related
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.
I have a collection on Firestore and I listen for changes like this:
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
})
}
I only want to listen for documents that are actually added to that collection.
In fact, the problem is that whenever I invoke this function I receive all the documents of the collection as added documents and then I also receive documents added later.
How can I listen just for actually added later documents, ignoring the ones already present in the collection? Searching online I didn't find any solution to this issue.
EDIT:
This is the way I tried to solve the problem:
func createMatchesListener(){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
getUidsAlreadyMade { uidsAlreadyMade in
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
let data = change.document.data()
let userId = data["uid"] as? String ?? ""
if uidsAlreadyMade.contains(userId) == false{
//means the uid is newly created in the collection, do stuff accordingly
arrayOfUidsAlreadyMade.append(currentUid)
}
}
if change.type == .removed{
// if the document has been removed, remove also the id from the array of uids
let data = change.document.data()
let currentUid = data["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.removeAll { $0 == currentUid }
}
})
})
}
}
func getUidsAlreadyMade(completion: #escaping ([String]) -> Void){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
db.collection("Matches").document(currentUid).collection("Matches").getDocuments { snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
arrayOfUidsAlreadyMade.removeAll()
snapshot?.documents.forEach({ doc in
let dict = doc.data()
let userId = dict["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.append(userId)
})
completion(arrayOfUidsAlreadyMade)
}
}
A simple solution is to include a timestamp in your Firestore documents.
Suppose your documents store Tasks, for example
documentId
task: "get dinner"
timestamp: 20211123
and suppose your app doesn't care about past tasks, only new ones.
When the tasks are read, update the timestamp as to when that occurred.
Then each time after that you want to read only 'new data' specify that in your listener, keeping track of when the last read timestamp was:
db.collection("task").whereField("timestamp", isGreaterThan: lastReadTimeStamp).addSnapshotListener...
The above will only read in tasks that occured after the prior timestamp and add a Listener (reading in all of the new tasks so you can populate the UI).
You can store an array with the ID of the documents that you already have stored in the device. That way, all that you need to do before doing things is checking that document's id is not in your array
There's no way of preventing Firestore from returning the initial snapshot of documents when a document listener is added, so just use a boolean to keep track of the initial snapshot and ignore it.
var listenerDidInit = false
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
if listenerDidInit {
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
} else {
listenerDidInit = true
}
})
}
private var listener: ListenerRegistration?
self.listener = db.collection("Matches") // matchesListener
listener!.remove()
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.
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]
func updateFirebase(){
myFun = thisIsMyFunTextView.text
IAm = iAmTextView.text
var profileKey = String()
profileRef.queryOrdered(byChild: "uid").queryEqual(toValue: userID).observe(.value, with:{
snapshot in
for item in snapshot.children {
guard let data = item as? FIRDataSnapshot else { continue }
guard let dict = data.value as? [String: Any] else { continue }
guard let profileKey = dict["profileKey"] else { continue }
self.profileRef.child(profileKey as! String).child("bodyOfIAM").setValue(IAm)
self.profileRef.child(profileKey as! String).child("bodyOfThisIsMyFun").setValue(myFun)
}
})
}
#IBAction func backButtonClicked(_ sender: Any) {
updateFirebase()
DispatchQueue.main.asyncAfter(deadline: .now() + 4, execute: {
self.dismiss(animated: true)
})
}
myFun and IAm are successfully defined by the changes to the textviews by the user. I can't extract the childByAutoID value without triggering this for in loop that does not end once called, continuing even as a new view controller is presented. The "bodyOfThisIsMyFun" vacillates between the old value and the new value during this loop while the "bodyOfIAM" gets correctly redefined right away and stays that way like it should. How do I get the extracted new values to replace the old values here?
I needed to add this line of code at the end of the for...in statement:
self.profileRef.removeAllObservers()