Nested closures does not like argument list - uiview

A UIView needs to change a warning label depending on the completion handler of a custom control:
voucherInputView.completionHandler = {[weak self] (success: Bool) -> Void in
self?.proceedButton.enabled = success
self?.warningLabel.alpha = 1.0
if success
{
self?.warningLabel.text = "Code you entered is correct"
self?.warningLabel.backgroundColor = UIColor.greenColor()
}
else
{
self?.warningLabel.text = "Code you entered is incorrect"
self?.warningLabel.backgroundColor = UIColor.orangeColor()
}
UIView.animateWithDuration(NSTimeInterval(1.0), animations:{ ()-> Void in
self?.warningLabel.alpha = 1.0
})
The final animation block shows an error in the form.
Cannot invoke 'animateWithDuration' with an argument list of type '(NSTimeInterval), animations: ()-> Void)'
If i call this somewhere outside of the completion closure it works.

The problem is that the closure is implicitly returning the result of this expression:
self?.warningLabel.alpha = 1.0
but the closure itself is declared as returning Void.
Adding an explicit return should resolve the problem:
UIView.animateWithDuration(NSTimeInterval(1.0), animations: { ()-> Void in
self?.warningLabel.alpha = 1.0
return
})

Antonio's solution also applies with nested closures, like doing an AFNetworking request within UITableViewRowAction handler.
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]? {
let cleanRowAction = UITableViewRowAction(style: UITableViewRowActionStyle.Default, title: "Do Stuff", handler: {[weak self](action: UITableViewRowAction!, indexPath: NSIndexPath!) in
AFHTTPSessionManager(baseURL: NSURL(string: "http://baseurl")).PUT("/api/", parameters: nil, success: { (task: NSURLSessionDataTask!, response: AnyObject!) -> Void in
// Handle success
self?.endEditing()
return
}, failure: { (task: NSURLSessionDataTask!, error: NSError!) -> Void in
// Handle error
self?.endEditing()
return
})
return
})
cleanRowAction.backgroundColor = UIColor.greenColor()
return [cleanRowAction]
}

Related

How can I update a view's progress bar using the AppSync S3ObjectManager?

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

ambiguous use of method

My build completes successfully. Then after some time this error pops up:
"Ambiguous use of 'addObjectsDidChangeNotificationObserver(handler:)'"
What I do not understand is why this happens because the addObjectsDidChangeNotificationObserver method is only declared once in the project and the second occurence shown by Xcode is the use of the method itself.
Here is the code where the error is shown and which Xcode also shows me as first candidate:
public init?(object: Managed, changeHandler: #escaping (ChangeType) -> ()) {
guard let moc = object.managedObjectContext else { return nil }
objectHasBeenDeleted = !type(of: object).defaultPredicate.evaluate(with: object)
token = moc.addObjectsDidChangeNotificationObserver(handler: {
[unowned self] note in
guard let changeType = self.changeType(of: object, in: note) else { return }
self.objectHasBeenDeleted = changeType == .delete
changeHandler(changeType)
})
}
and the implementation of addObjectsDidChangeNotificationObserver(), which Xcode shows me as second candidate:
extension NSManagedObjectContext {
public func addObjectsDidChangeNotificationObserver(handler: #escaping (ObjectsDidChangeNotification) -> ()) -> NSObjectProtocol {
let nc = NotificationCenter.default
return nc.addObserver(forName: .NSManagedObjectContextObjectsDidChange, object: self, queue: nil) { note in
let wrappedNote = ObjectsDidChangeNotification(note: note)
handler(wrappedNote)
}
}
}
Ok, the problem seems to be solved now.
Apparently, I messed up with the access modifiers, but good to know that something like that can cause an ambiguous error

UICollectionView load items on top - load more option

I building message app with meteor-iOS, everything is going great but I'm trying to make a load more feature but I can't maintain/save the position of scroll view(When I scroll to top I want to load more items but keep the scroll view in the same position as before the insert)
My Code:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
if type == NSFetchedResultsChangeType.Insert {
self.blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertItemsAtIndexPaths([newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.Update {
self.blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadItemsAtIndexPaths([indexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.Move {
self.blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
}
})
)
}
else if type == NSFetchedResultsChangeType.Delete {
self.blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteItemsAtIndexPaths([indexPath!])
}
})
)
}
}
self.collectionView!.performBatchUpdates({ () -> Void in
for operation: NSBlockOperation in self.blockOperations {
operation.start()
}
}, completion: { (finished) -> Void in
self.blockOperations.removeAll(keepCapacity: false)
})
I'm using core data and meteor-ios. those anyone know what I can do? I also tried how to set UITableView's scroll position to previous location when click "load earlier items" button any many other without success :(
Thank you very much!!
Yep, I made if statement between range 20-50 to contentOffset.y. If it between those values so call loadMore() once.
Make bool value to save isLoadFinish and wait until the cell update. If you wouldn't do it the loadMore() will call multiple time.
For UX experience you have to save the old contentOffset.y before calling loadMore() and reset it after the load.
I will need to improve it a lot, simple hack for now.
Good luck.

Swift closure: cannot invoke a function with its argument list

I am writing a Swift function using closure. A should-be-compilable code sample is like this,
import Foundation
typealias PKSynchronizeProgressBlock = (Double) -> Void
typealias PKSynchronizeCompletionBlock = (Bool, NSError?) -> Void
class X {
func synchronizeAppDataWithProgress(
progress: PKSynchronizeProgressBlock?,
completion: PKSynchronizeCompletionBlock?) {
dispatch_async(dispatch_get_main_queue(), {
// Do a lot of downloading, and during the process
// {
// If progress is updated
if (progress != nil) {
progress!(Double(0))
}
//
// If something goes wrong
if (completion != nil) {
completion!(false, nil)
}
// }
dispatch_async(dispatch_get_main_queue(), {
if (completion != nil) {
completion!(true, nil)
}
})
})
}
func foo() {
self.synchronizeAppDataWithProgress({ (progress: Double) -> Void in
self.launchProgressBar.progress = progress
}, completion: { (success: Bool, error: NSError?) -> Void in
if success {
self.launchProgressBar.progress = 1.0
}
else {
print("Failed to synchronize app data with error %#", error!)
}
})
}
}
However, this code does not compile. Xcode says that
cannot invoke 'synchronizeAppDataWithProgress' with an argument list
'(progress: (Double) -> Void, completion: (Bool, NSError?) -> Void)'
What should I do? Did I make any stupid mistake in my code?
Update:
Thanks to #Mario Zannone. I fixed the first two mistakes in my code above. That was: (1) I inserted a redundant progress: in the function call. I have removed that. (2) I updated UI in a thread other than main thread.
But the code still does not work if I don't comment out the following single line in the foo(),
self.launchProgressBar.progress = progress
Do you have any clue why?
Xcode can be picky sometimes with the way arguments are listed inside closures. I've found it best to leave the type inferred. Also be sure to use capture lists to avoid strong reference cycles in your closures.
Using the Alamofire dependency, I've rewritten your code above and it compiles.
import Alamofire
typealias ProgressBlock = (Double) -> Void
typealias CompletionBlock = (Bool, ErrorType?) -> Void
class ExampleDataSource {
func fetchData(progress: ProgressBlock?, completion: CompletionBlock?) {
// Here we use the Alamofire Dependency for progress reporting simplicity.
Alamofire.request(.GET, "https://www.myexampledomain.com")
.progress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
// bytesRead, totalBytesRead, and totalBytesExpectedToRead are Int64
// so we must perform unit conversion
let progressPercentage = Double(totalBytesRead) / Double(totalBytesExpectedToRead)
// here we optionally call the ProgressBlock 'progress' and pass it the progressPercentage
progress?(progressPercentage)
}
.response { request, response, data, error in
// here we usually parse the data, but for simplicity we'll
// simply check to see if it exists.
let completionFlag = (data != nil)
// note that NSError? is interchangable with ErrorType?
completion?(completionFlag, error)
}
}
func performTasks() {
// first we'll set up our closures...
let progressBlock: ProgressBlock = { progress in
// here we update the UI or whatever
// the nice thing about the Alamofire dependency is
// that it's thread-safe :]
}
let completionBlock: CompletionBlock = { success, error in
// here we do whatever we need to do when the
// network operation finishes, or handle the
// errors appropriately
}
// then we'll pass them into our fetchData method
fetchData(progressBlock, completion: completionBlock)
}
}

how to throw errors in a closure in swift?

Please look at the following code:
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let deleteAction = UITableViewRowAction(style: UITableViewRowActionStyle.Default, title: "Delete", handler: {
(action : UITableViewRowAction, indexPath : NSIndexPath) -> Void in
if let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext{
let restaurantToDelete = self.fetchResultController.objectAtIndexPath(indexPath) as! Restaurant
managedObjectContext.deleteObject(restaurantToDelete)
// Saving managedObjectContext instance, and catch errors if it fails
do {
try managedObjectContext.save()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
})
return deleteAction
}
the error message from Xcode is : Invalid conversion from throwing function of type '(UITableViewRowAction, NSIndexPath) throws -> Void' to non-throwing function type '(UITableViewRowAction, NSIndexPath) -> Void'
I know the problem is managedObjectContext.save() will throw errors and this is not allowed in the completion handler. I found some blog articles where they modified the closure parameters in order to make the error handling in a closure workable. While here the definition of the function is given by apple, so how can i fix this issue? Thanks a lot! :D
the compiler is adding throws to the signature of your block because your catch clause is not exhaustive: the pattern match let error as NSError can fail... see the documentation
the signature of the closure argument is (UITableViewRowAction, NSIndexPath) -> Void, however the compiler is inferring the type of the closure that you are providing to be (UITableViewRowAction, NSIndexPath) throws -> Void
by adding another catch clause (with no pattern) after the one you already have the compiler will see that you are catching the exception locally and it will no longer infer that the signature of the closure you are providing includes throws:
do {
try managedObjectContext.save()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
} catch {}
Not possible, because the closure can be invoked at any time, probably not at execution time of your function, so where should the error propagate to?
You have to call out to another function which can handle the error:
func handleError(error: ErrorType) {
switch error {
...
}
}
and then call this function with your caught error inside the closure

Resources