Search code examples
iosswiftaws-appsync

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?


Solution

  • 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