I'm trying to live search at my PHP API with Swift. Until now i've done this thing.
var filteredData = [Products]()
func getSearch(completed: #escaping DownloadComplete, searchString: String) {
let parameters: Parameters = [
"action" : "search",
"subaction" : "get",
"product_name" : searchString,
"limit" : "0,30"
]
Alamofire.request(baseurl, method: .get, parameters: parameters).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let result = responseData.result
if let dict = result.value as? Dictionary<String, AnyObject>{
if let list = dict["products_in_category"] as? [Dictionary<String, AnyObject>] {
if self.filteredData.isEmpty == false {
self.filteredData.removeAll()
}
for obj in list {
let manPerfumes = Products(productDict: obj)
self.filteredData.append(manPerfumes)
}
}
}
completed()
}
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
if (searchController.searchBar.text?.characters.count)! >= 3 {
self.getSearch(completed: {
self.searchResultTable.reloadData()
self.searchResultTable.setContentOffset(CGPoint.zero, animated: true)
}, searchString: searchController.searchBar.text!)
} else {
self.searchResultTable.reloadData()
}
}
}
And the table view is being updated with the filteredData.
How can i throttle the search so lets say when the user writes
"example" -> shows the results with example
then he erase the "le" ->
"examp" -> if the previous request is not completed, cancel it -> make request for "examp" and show the data in table view!
P.S. from another answer i found
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.reload), object: nil)
self.perform(#selector(self.reload), with: nil, afterDelay: 0.5)
}
func reload() {
print("Doing things")
}
Although if I try to replace "self.reload" with my function, I get an error
cannot convert value of type () to expected argument type selector
Use DispatchWorkItem with Swift 4 !
// Add a searchTask property to your controller
var searchTask: DispatchWorkItem?
// then in your search bar update method
// Cancel previous task if any
self.searchTask?.cancel()
// Replace previous task with a new one
let task = DispatchWorkItem { [weak self] in
self?.sendSearchRequest()
}
self.searchTask = task
// Execute task in 0.75 seconds (if not cancelled !)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.75, execute: task)
Hope it helps !
Your error was because you probably forgot the #selector() part.
Here's how it should look:
func searchBar() {
NSObject.cancelPreviousPerformRequests(withTarget: self,
selector: #selector(self.getSearch(completed:searchString:)),
object: nil)
perform(#selector(self.getSearch(completed:searchString:)),
with: nil, afterDelay: 0.5) }
You get the error because you didn't enclose your function in #selector
Now, as for the arguments, here's a function for that:
perform(#selector(getSearch:completion:searchString), with: <some completion>, with: "search string")
disclaimer: I am a writer.
Throttler could be the right tool to get it done.
You can do debounce and throttle without going reactive programming using Throttler like,
import Throttler
// advanced debounce, running a first task immediately before initiating debounce.
for i in 1...1000 {
Throttler.debounce {
print("debounce! > \(i)")
}
}
// debounce! > 1
// debounce! > 1000
// equivalent to debounce of Combine, RxSwift.
for i in 1...1000 {
Throttler.debounce(shouldRunImmediately: false) {
print("debounce! > \(i)")
}
}
// debounce! > 1000
Throttler also can do advanced debounce, running a first event immediately before initiating debounce that Combine and RxSwift don't have by default.
You could, but you may need a complex implementation yourself for that.
Related
I've centralized API calls for my App in a class called APIService.
Calls look like the one below:
// GET: Attempts getconversations API call. Returns Array of Conversation objects or Error
func getConversations(searchString: String = "", completion: #escaping(Result<[Conversation], APIError>) -> Void) {
{...} //setting up URLRequest
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let _ = data
else {
print("ERROR: ", error ?? "unknown error")
completion(.failure(.responseError))
return
}
do {
{...} //define custom decoding strategy
}
let result = try decoder.decode(ResponseMultipleElements<[Conversation]>.self, from: data!)
completion(.success(result.detailresponse.element))
}catch {
completion(.failure(.decodingError))
}
}
dataTask.resume()
}
I'm executing API calls from anywhere in the Application like so:
func searchConversations(searchString: String) {
self.apiService.getConversations(searchString: searchString, completion: {result in
switch result {
case .success(let conversations):
DispatchQueue.main.async {
{...} // do stuff
}
case .failure(let error):
print("An error occured \(error.localizedDescription)")
}
})
}
What I would like to achieve now is to execute func searchConversations for each character tapped by the user when entering searchString.
This would be easy enough by just calling func searchConversations based on a UIPressesEvent being fired. Like so:
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
{...} // handle special cases
default:
super.pressesEnded(presses, with: event)
searchConversations(searchString: SearchText.text)
}
}
My problem is this now:
Whenever a new character is entered, I'd like to cancel the previous URLSession and kick-off a new one. How can I do that from inside the UIPressesEvent handler?
The basic idea is to make sure the API returns an object that can later be canceled, if needed, and then modifying the search routine to make sure to cancel any pending request, if any:
First, make your API call return the URLSessionTask object:
#discardableResult
func getConversations(searchString: String = "", completion: #escaping(Result<[Conversation], APIError>) -> Void) -> URLSessionTask {
...
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
...
}
dataTask.resume()
return dataTask
}
Have your search routine keep track of the last task, canceling it if needed:
private weak var previousTask: URLSessionTask?
func searchConversations(searchString: String) {
previousTask?.cancel()
previousTask = apiService.getConversations(searchString: searchString) { result in
...
}
}
We frequently add a tiny delay so that if the user is typing quickly we avoid lots of unnecessary network requests:
private weak var previousTask: URLSessionTask?
private weak var delayTimer: Timer?
func searchConversations(searchString: String) {
previousTask?.cancel()
delayTimer?.invalidate()
delayTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { [weak self] _ in
guard let self = self else { return }
self.previousTask = self.apiService.getConversations(searchString: searchString) {result in
...
}
}
}
The only other thing is that you probably want to change your network request error handler so that the “cancel” of a request isn’t handled like an error. From the URLSession perspective, cancelation is an error, but from our app’s perspective, cancelation is not an error condition, but rather an expected flow.
You can achieve this by using a timer,
1) Define a timer variable
var requestTimer: Timer?
2) Update searchConversations function
#objc func searchConversations() {
self.apiService.getConversations(searchString: SearchText.text, completion: {result in
switch result {
case .success(let conversations):
DispatchQueue.main.async {
{...} // do stuff
}
case .failure(let error):
print("An error occured \(error.localizedDescription)")
}
})
}
3) Update pressesEnded
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
{...} // handle special cases
default:
super.pressesEnded(presses, with: event)
self.requestTimer?.invalidate()
self.requestTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(searchConversations), userInfo: nil, repeats: false)
}
}
I'm using AWSAppSyncClient to upload files but I'm struggling to connect the upload progress hook with the view.
AWSAppSyncClient is a property of the the application delegate initialized with an S3ObjectManager. The object manager method upload has access to the upload progress via the AWSTransferUtilityUplaodExpression:
expression.progressBlock = {(task, progress) in
DispatchQueue.main.async(execute: {
// Can we update the controller's progress bar here?
print("Progress: \(Float(progress.fractionCompleted))")
})
}
My controller invokes the upload by calling perform:
var appSyncClient: AWSAppSyncClient? // retrieved from the app delegate singleton
appSyncClient?.perform(mutation: CreatePostMutation(input: input)) { (result, error) in ...
What I am struggling with: how do I provide the S3ObjectManager a reference to the controller? I thought of instantiating the AWSAppSyncClient in each controller, and maybe using some sort of delegate pattern?
It's probably overkill to instantiate a new client on each view controller. Setup & teardown take a bit of time & system resources to perform, and you'd probably prefer to keep those activities separate from the view controller in any case, just for separation of responsibilities.
There isn't really a good way of registering a per-object listener, since mutations are queued for eventual, asynchronous delivery. Your delegate idea seems like the best approach at this point.
NOTE: Code below is untested, and not thread-safe.
For example, you could declare a singleton delegate that manages watchers for individual views that need to report progress:
class AppSyncS3ObjectManagerProgressWatcher {
typealias ProgressSubscription = UUID
static let shared = AppSyncS3ObjectManagerProgressWatcher()
private var watchers = [UUID: AppSyncS3ObjectManagerProgressDelegate?]()
func add(_ watcher: AppSyncS3ObjectManagerProgressDelegate) -> ProgressSubscription {
let subscription = UUID()
weak var weakWatcher = watcher
watchers[subscription] = weakWatcher
return subscription
}
func remove(_ subscription: ProgressSubscription?) {
guard let subscription = subscription else {
return
}
watchers[subscription] = nil
}
}
extension AppSyncS3ObjectManagerProgressWatcher: AppSyncS3ObjectManagerProgressDelegate {
func progressReportingExpression(forDownloadingObject object: AWSS3ObjectProtocol) -> AWSS3TransferUtilityDownloadExpression {
let expression = AWSS3TransferUtilityDownloadExpression()
expression.progressBlock = { _, progress in
self.didReportProgress(forDownloadingObject: object, progress: progress)
}
return expression
}
func progressReportingExpression(forUploadingObject object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol) -> AWSS3TransferUtilityUploadExpression {
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = { _, progress in
self.didReportProgress(forUploadingObject: object, progress: progress)
}
return expression
}
func didReportProgress(forDownloadingObject object: AWSS3ObjectProtocol, progress: Progress) {
for watcher in watchers.values {
watcher?.didReportProgress(forDownloadingObject: object, progress: progress)
}
}
func didReportProgress(forUploadingObject object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol, progress: Progress) {
for watcher in watchers.values {
watcher?.didReportProgress(forUploadingObject: object, progress: progress)
}
}
}
Wherever you conform S3TransferUtility to S3ObjectManager, you would do something like:
extension AWSS3TransferUtility: AWSS3ObjectManager {
public func download(s3Object: AWSS3ObjectProtocol, toURL: URL, completion: #escaping ((Bool, Error?) -> Void)) {
let completionBlock: AWSS3TransferUtilityDownloadCompletionHandlerBlock = { task, url, data, error -> Void in
if let _ = error {
completion(false, error)
} else {
completion(true, nil)
}
}
let progressReportingExpression = AppSyncS3ObjectManagerProgressWatcher
.shared
.progressReportingExpression(forDownloadingObject: s3Object)
let _ = self.download(
to: toURL,
bucket: s3Object.getBucketName(),
key: s3Object.getKeyName(),
expression: progressReportingExpression,
completionHandler: completionBlock)
}
public func upload(s3Object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol, completion: #escaping ((_ success: Bool, _ error: Error?) -> Void)) {
let completionBlock : AWSS3TransferUtilityUploadCompletionHandlerBlock = { task, error -> Void in
if let _ = error {
completion(false, error)
} else {
completion(true, nil)
}
}
let progressReportingExpression = AppSyncS3ObjectManagerProgressWatcher
.shared
.progressReportingExpression(forUploadingObject: s3Object)
let _ = self.uploadFile(
s3Object.getLocalSourceFileURL()!,
bucket: s3Object.getBucketName(),
key: s3Object.getKeyName(),
contentType: s3Object.getMimeType(),
expression: progressReportingExpression,
completionHandler: completionBlock
).continueWith { (task) -> Any? in
if let err = task.error {
completion(false, err)
}
return nil
}
}
}
And then in the progress reporting view:
override func awakeFromNib() {
super.awakeFromNib()
progressSubscription = AppSyncS3ObjectManagerProgressWatcher.shared.add(self)
}
func didReportProgress(forUploadingObject object: AWSS3InputObjectProtocol & AWSS3ObjectProtocol, progress: Progress) {
// TODO: Filter by object local URI/key/etc to ensure we're updating the correct progress
print("Progress received for \(object.getKeyName()): \(progress.fractionCompleted)")
self.progress = progress
}
As I noted, this code is untested, but it should outline a general approach for you to start from. I'd welcome your feedback and would like to hear what approach you eventually settle on.
Finally, please feel free to open a feature request on our issues page: https://github.com/awslabs/aws-mobile-appsync-sdk-ios/issues
I've been pouring over stack overflow for ages trying to find a way out of this error:
unexpected non void return value in void function
that I am getting with returning a Bool within my function.
i just can't seem to dig my way out of this one. I'm sure its something to do with async but I'm not very familiar with these types of functions.
class CheckReachability {
class func setupReachability (hostName:String?, useClosures: Bool) -> Bool{
var reachability : Reachability!
var connected = false
let reachability2 = hostName == nil ? Reachability() : Reachability(hostname: hostName!)
reachability = reachability2
try! reachability?.startNotifier()
if useClosures {
reachability2?.whenReachable = { reachability in
DispatchQueue.main.async {
connected = true
print("Reachable....")
}
}
reachability2?.whenUnreachable = { reachability in
DispatchQueue.main.async {
connected = false
print("Not Connected....")
}
}
} else {
NotificationCenter.default.addObserver(self, selector: Selector(("reachabilityChanged:")), name: ReachabilityChangedNotification, object: reachability2)
}
return connected
}
}
calling this from viewdidload on another vc doesn't allow enough time to get a true result
let connected = CheckReachability.setupReachability(hostName: nil, useClosures: true)
if connected {
Your question is confusing because the code you posted does not have the error you describe. However, you're trying to create a function that returns a result from an async function. That is not how async works.
Async functions start to do a task in the background, where that task won't be finished before it's time to return.
You need to adjust your thinking. Instead of trying to return a result from your function, you need to write your function to take a completion handler. You then call the completion handler once the long-running task has finished (which is after your function has returned.)
#bubuxu provided you with code showing how to modify your function as I described.
If you want to write a checking class to listen to the reachability, define it as a singleton and pass the completeBlock to it like this:
class CheckReachability {
static let shared = CheckReachability()
var reachability: Reachability?
func setupReachability(hostName:String?, completeBlock: ((Bool) -> Void)? = nil) {
reachability = hostName == nil ? Reachability() : Reachability(hostname: hostName!)
try? reachability?.startNotifier()
if let block = completeBlock {
reachability?.whenReachable = { reachability in
DispatchQueue.main.async {
print("Reachable....")
block(true)
}
}
reachability?.whenUnreachable = { reachability in
DispatchQueue.main.async {
print("Not Connected....")
block(false)
}
}
} else {
// If we don't use block, there is no point to observe it.
NotificationCenter.default.addObserver(self, selector: #selector(reachabilityChanged(_:)), name: .ReachabilityChangedNotification, object: nil)
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc func reachabilityChanged(_ notification: Notification) {
// ?? what should we do here?
}
}
I going to hide a cell after half an hour time interval from populating time. If app is in not running state then also performs the same operation. Please Help me. Thank's in advance.
Here is the function delay that can help you to process delays even in background state (delay will be processed immediately after app becomes active if time is ok). And with this code you can easy cancel this delay if needed But this solution will not work for the situation when the app is not running (for this case I will give another solution):
import Foundation
import UIKit
typealias dispatch_cancelable_closure = (_ cancel : Bool) -> Void
#discardableResult
func delay(_ time:TimeInterval, closure: #escaping ()->Void) -> dispatch_cancelable_closure? {
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(time * 1000))) {
// closure()
// }
//
// return nil
func dispatch_later(_ clsr:#escaping ()->Void) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(time * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: clsr)
}
var closure:(()->Void)? = closure
var cancelableClosure:dispatch_cancelable_closure?
let delayedClosure:dispatch_cancelable_closure = { cancel in
if closure != nil {
if (cancel == false) {
// DispatchQueue.main.async {
// closure?()
// }
DispatchQueue.main.async(execute: closure!)
// DispatchQueue.main.async(execute: closure as! #convention(block) () -> Void);
}
}
closure = nil
cancelableClosure = nil
}
cancelableClosure = delayedClosure
dispatch_later {
if let delayedClosure = cancelableClosure {
delayedClosure(false)
}
}
return cancelableClosure;
}
func cancel_delay(_ closureToCancel:dispatch_cancelable_closure?) {
if closureToCancel != nil {
closureToCancel!(true)
}
}
But for case when your application is not running, you need to save the time when you want to remove the cell in NSDefaults before application comes into background, and when application becomes active you can use this delay function to set the rest of the time (or if time expired you can remove the cell immidiatly)
func checkIfFriend()->Bool{
request(.POST, "", parameters:["":""]).responseJSON{_,_,jsonData in
if something{
return true}
else{
return false
}
}
It appears that "return true/false" has to be in the same level than function is and not inside another function (in this case the Alamofire one).
In that case, how can I return bool in checkIfFriend function depending on what the request return?
Your checkIfFriend() function does not run on the same thread as your Alamofire request (Which runs asynchronously). You should use a callback function/ completion handler as shown below:
func checkIfFriend(completion : (Bool, Any?, Error?) -> Void) {
request(.POST, "", parameters:["":""]).responseJSON{_,_,jsonData in
if something{
completion(true, contentFromResponse, nil)
//return true
}else{
completion(false, contentFromResponse, nil)
// return false
}
}
//Then you can call your checkIfFriend Function like shown below and make use
// of the "returned" bool values from the completion Handler
override func viewDidLoad() {
super.viewDidLoad()
var areWeFriends: Bool = Bool()
var responseContent: Any = Any()
checkIfFriend(completion: { (success, content, error) in
areWeFriends = success // are We Friends will equal true or false depending on your response from alamofire.
//You can also use the content of the response any errors if you wish.
})
}