I am new in swift3.0 I am implementing a custom search box. I wish to know how can i make a search queue such that on text change in searchbox i need to perform search operation with new text and if there is an existing search operation going on cancel that. I also want to include threshold ontextchanged. So that search operation does not get fired very frequently
Your question is somehow general, but let me tell you how I accomplished this in Swift 3 and AFNetworking (this assumes you wish to search for the data on the server).
I hold a reference of the networking manager in the properties of the view controller:
//The network requests manager. Stored here because this view controller extensively uses AFNetworking to perform live search updates when the input box changes.
var manager = AFHTTPRequestOperationManager()
Afterwards, using UISearchController I check to see if there is any text entered in the search box at all and, if it is, I want to make sure there aren't any other ongoing AFNetworking tasks from now by closing any of them which are still running:
//Called when the something is typed in the search bar.
func updateSearchResults (for searchController: UISearchController) {
if !SCString.isStringValid(searchController.searchBar.text) {
searchController.searchResultsController?.view.isHidden = false
tableView.reloadData()
return
}
data.searchText = searchController.searchBar.text!
/**
Highly important racing issue solution. We cancel any current request going on because we don't want to have the list updated after some time, when we already started another request for a new text. Example:
- Request 1 started at 12:00:01
- We clear the containers because Request 2 has to start
- Request 2 started at 12:00:02
- Request 1 finished at 12:00:04. We update the containers because data arrived
- Request 2 finished at 12:00:05. We update the containers because data arrived
- Now we have data from both 1 and 2, something really not desired.
*/
manager.session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) in
dataTasks.forEach { $0.cancel() }
}
/**
Reloads the list view because we have to remove the last search results.
*/
reloadListView()
}
In the end, I also check in the failure closure if the code of the error is not NSURLErrorCancelled. Because, if that happened, I don't display any error message or toast.
//The operation might be cancelled by us on purpose. In this case, we don't want to interfere with the ongoing logic flow.
if (operation?.error as! NSError).code == NSURLErrorCancelled {
return
}
self.retrieveResultListFailureNetwork()
Hope it helps!
Related
I'm currently building a chat app and when I click on a user, it takes me to the chat log controller. Here I call a function fetchChatMessages() in viewdidload() that essentially fetches the conversation from firestore. Problem is, whenever I go to the previous controller and open the chat again, it again fetches the messages.
Not sure if it's fetching from cache or from the server itself. But I did write a print statement under the firestore fetch code that prints every time.
Now I'm new to swift so my question is, in other chat apps, you can see that messages seemed to be fetched just once from the server and after that you add a listener and update the collection view to display the new messages. In my case, it seems like everything is fetched over and over again. Even though I have added a listener and followed a highly acclaimed tutorial.
Also, I added a scroll to bottom code whenever messages are fetched, so every time a new message is fetched, the controller automatically scrolls to the bottom. But this happens every time I open the chat. I was trying to fix this bug where the controller keeps scrolling every time the view appears which made me wonder, am I contacting firebase again and again when the controller is opened?
override func viewDidLoad() {
super.viewDidLoad()
fetchCurrentUser()
fetchMessages()
setupLoadView()
}
//MARK: Fetch Messages
var listener: ListenerRegistration?
fileprivate func fetchMessages(){
print("Fetching Messages")
guard let cUid = Auth.auth().currentUser?.uid else {return}
let query = Firestore.firestore().collection("matches").document(cUid).collection(connect.uid).order(by: "Timestamp")
listener = query.addSnapshotListener { (querySnapshot, error) in
if let error = error{
print("There was an error fetching messages", error.localizedDescription)
return
}
querySnapshot?.documentChanges.forEach({ (change) in
if change.type == .added{
let dictionary = change.document.data()
self.items.append(.init(dictionary: dictionary))
print("FIRESTORE HAS BEEN CONTACTED FETCHING MESSAGES")
}
})
self.collectionView.reloadData()
self.collectionView.scrollToItem(at: [0, self.items.count - 1], at: .bottom, animated: true)
print("Fetched messages")
}
}
//MARK: View Disappears
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent{
listener?.remove()
}
}
I want the controller to remember its state. Like if I scroll the messages, exit the controller and enter it again, it stays at the scrolled position like in whatsapp.
The problem here is that your controller gets deallocated every time you leave the screen, because you probably push the controller on to the stack and pop it afterwards, this will erase all of the internal state of the controller. This behavior is indeed intended (viewDidLoad is called once the screen is loaded). You can solve this problem in several ways. An easy one would be to introduce a singleton service (a service which is shared across the app) which holds the state of the controller, so every time the controller is created it will ask the service for its state. Keep in mind this is not a very good solution, but it should be sufficient as starting point. If you need an example I will edit my answer accordingly later on.
I cannot give you a definite answer on this, because it really depends on what features the app and the server support, in the end I would have some kind of database service and chat services (these should not be accessed over the singleton pattern, but rather via dependency injection). The chat service would define some kind of policy when the data should be fetched, which means the controller should not be aware of this. The chat service would then store the messages via the database layer in some persistent store like user defaults, realm or core data for each chat. Every time the user enters an already fetched chat the chat service will check if persisted data is available if not it will fetch it from the server.
I am having trouble displaying an Alert in case of an Error properly.
My idea is: Everytime I download data from my backend with an completion block, I present an Alert if an error occurs.
query?.findObjectsInBackground(block: { (objects, error) -> Void in
if error != nil {
createAlert(error)
return
} else if let objects = objects {
}
Since I got more than one call in a ViewController at the same time, it may happen to find myself having more than 2 or 3 Alerts presenting at the same time saying e.g. "No Connection to the Internet".
It will constantly reload the Alert and it is a pain in terms of UI.
What is best practice to solve this issue?
My solution idea would be to put everything in a Singleton pattern and make sure no other other Alert is currently displayed.
Are there any better ways?
Instead of using a singleton pattern, you might prefer having an optional property (var noConnectivityAlert) in the class currently responsible for creating the alert.
Instead of the createAlert() method you would have a informUserAboutConnectivity() method.
func informUserAboutConnectivity() {
// If noConnectivityAlert is nil
// the method creates an alert and shows it.
// If the property is NOT nil
// do nothing (since the user is already informed).
}
When the internet connection would come back and then disappear again, some apps in the App Store would show an alert once again.
In that case, when the internet connection comes back you can directly set noConnectivityAlert = nil so that when the connection is lost, things will be handled nicely (a new alert will be created and shown).
By the way, in the iOS SDK, singletons are not used often. There are mostly used for providing a default and most common use case of a class (think of UserDefaults), or (of course) a shared manager/provider.
Every firebase client example I see in Swift seems to oversimplify properly loading data from Firebase, and I've now looked through all the docs and a ton of code. I do admit that my application may be a bit of an edge case.
I have a situation where every time a view controller is loaded, I want to auto-post a message to the room "hey im here!" and additionally load what's on the server by a typical observation call.
I would think the flow would be:
1. View controller loads
2. Auto-post to room
3. Observe childAdded
Obviously the calls are asynchronous so there's no guarantee the order of things happening. I tried to simplify things by using a complete handler to wait for the autopost to come back but that loads the auto-posted message twice into my tableview.
AutoPoster.sayHi(self.host) { (error) in
let messageQuery = self.messageRef.queryLimited(toLast:25).queryOrdered(byChild: "sentAt")
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
if let dict = snapshot.value as? [String: AnyObject] {
DispatchQueue.main.async {
let m = Message(dict, key: snapshot.key)
if m.mediaType == "text" {
self.messages.append(m)
}
self.collectionView.reloadData()
}
}
})
}
Worth noting that this seems very inefficient for an initial load. I fixed that by using a trick with a timer that will basically only allow the collection view to reload maximum every .25s and will restart the timer every time new data comes in. A bit hacky but I guess the benefits of firebase justify the hack.
I've also tried to observe the value event once for an initial load and then only after that observe childAdded but I think that has issues as well since childAdded is called regardless.
While I'm tempted to post code for all of the loading methods I have tried (and happy to update the question with it), I'd rather not debug what seems to not be working and instead have someone help outline the recommended flow for a situation like this. Again, the goal is simply to auto-post to the room that I joined in the conversation, then load the initial data (my auto-post should be the most recent message), and then listen for incoming new messages.
Instead of
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
try replacing with
let childref = FIRDatabase.database().reference().child("ChildName")
childref.queryOrdered(byChild:"subChildName").observe(.value, with: { snapshot in
I am using mapkit to allow a user to search for an address. Pressing another button (the goButton) will ask them to confirm a formatted address populated using the results of their search, and also will send a command to search behind the scenes if the user entered text but did not actually search for the address before selecting goButton. My problem is that because the search is completing asynchronously, the confirmation function is being called too soon.
Is there a way I can tell when the search is completed? I tried putting a bool at the end of searchBarSearchButtonClicked and a while loop before calling confirmHostAddress that checked the value of the bool, but it did not detect a change. Thanks for your help.
#IBAction func goButton(sender: UIButton) {
//check and see if there is an address, if not, do search behind the scenes
if (myAddress == nil)
{
if (mapSearch.text != "")
{
self.searchBarSearchButtonClicked(self.mapSearch)
confirmHostAddress()
}
else
{
alertTextNeeded()
}
}
else
{
confirmHostAddress()
}
}
It looks like your async search is in confirmHostAddress()? That function needs to have a callback block, which will (hopefully) be running on a background thread. Within that callback, use dispatch_async() to enqueue, onto the main queue, the UI change that you want.
Search button click (on main queue) triggers search (on background queue), and search completion triggers (on main queue) UI update.
There must be a better way to perform search on a remote server database than what I am doing right now.
I have a UISearchController installed on top of a table view that shows only 30 rows - Upon the user scrolling down to the bottom another 30 rows get loaded up and so forth...
So the the method I am using to perform the search looks like this:
func searchForWriter(searchString: String!) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
Client.sharedInstance.searchForWriterByName(searchString) {(searchResults: [Writer]?, error: NSError?) -> Void in
if let error = error {
NSLog("Error: %#", error.description)
dispatch_async(dispatch_get_main_queue()) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
} else {
dispatch_async(dispatch_get_main_queue()) {
self.filteredWriters = searchResults!
// TODO: - This needs debugging to get the last searched string?
print("searched for: \(searchString)")
self.writersTableView.reloadData()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
}
}
The UISearchResultsUpdating responds to any change in the searchBar and hence invoking the above searchForWriter method.
The problem is when the search results are obtained back from the server, there is no guarantee these will be for the last searched string?
In the matter of fact in almost most of the cases, this method will take longer time get results for a smaller search string rather a longer one... Hence giving completely frustrating search results
In fact you ask several questions here:
1) What you are looking for concerning the amount of displayed rows is called pagination. Look it up for strategies how to do it, but in any case your server has to support it.
2) Concerning the search on each entered character: You should only start searching when at least n characters are entered (n being 3 or so), search results for only one character probably are useless anyway.
3) Add some timer constraint so that the search only starts when the user did not enter anything for 600 ms or so - this avoids lots of calls to the server for unnecessary intermediate results.