炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

(Swift2) Alamofire の upload の進捗を通知で受け取ってプログレスバーを表示する


Swift2.2, Xcode7

前回の記事で Alamofire の upload メソッドの API クライアントを作成しました。

(Swift2) Alamofire の upload 用 API クライアントを作成する

今回は upload 用の API クライアントに手を加えて、ファイルのアップロードの進捗をプログレスバーに表示したいと思います。

  1. Alamofire の upload.progress メソッドに通知を設定する ( API.swift )
  2. API.call の Start と End で通知を設定する
  3. View と ViewController で通知を受け取って、プログレスバーの View を表示する ( UploadProgressUIView.swift )

1. Alamofire の upload.progress メソッドに通知を設定する ( API.swift )

Alamofire がプログレス表示用のメソッドを用意してくれているので、それを利用することで簡単に進捗を確認することができます。

以前作成した API.swift に .progress メソッドを追加します。

・ API.swift

class func call<T: UploadProtocol, V where T.ResponseType == V>(request: T, completion: (Result<V, NSError>) -> Void){
    Alamofire.upload(
        request.method,
        request.baseURL + request.path,
        headers: request.headers,
        multipartFormData: request.multipartFormData,
        encodingCompletion: { encodingResult in
            switch encodingResult {
            case .Success(let upload, _, _):
                upload.progress { (_, totalBytesWritten, totalBytesExpectedToWrite) in
                    let progress: Float = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
                    NSNotificationCenter.defaultCenter().postNotificationName("UploadProgressNotification", object: nil, userInfo: ["progress": progress])
                }
                upload.responseJSON { response in
                    ....

2. API.call の Start と End で通知を設定する

アップロードリクエストの Start と End を通知で受け取れる用に API を call に通知の設定をします。

let image = UIImage(named: "test")
let humanQuality: CGFloat = 0.8
let imageData = UIImageJPEGRepresentation(image, humanQuality)

NSNotificationCenter.defaultCenter().postNotificationName("UploadStartNotification", object: nil)
API.call(Endpoint.UploadPhoto(imageData: imageData)) { response in
    NSNotificationCenter.defaultCenter().postNotificationName("UploadCompleteNotification", object: nil)
    switch response {
    case .Success(let result):
        print("success \(result)")
    case .Failure(let error):
        print("failure \(error)")
    }
}

3. View と ViewController で通知を受け取って、プログレスバーの View を表示する ( UploadProgressUIView.swift )

UploadProgressNotification の通知を受け取って、プログレスバーを更新する View を作成します。複数画面で使い回せるように Xib ファイルを利用しました。Xib ファイルの呼び出し方についてはこちらの記事がとても参考になりました。 カスタムViewをNibから初期化し、IBDesignableとIBInspectableで便利に使う

・ UploadProgressUIView.xib

f:id:t-namikata:20160523233117p:plain

・ UploadProgressUIView.swift

class UploadProgressUIView: UIView {
    @IBOutlet weak var progressView: UIProgressView!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.initialize()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.initialize()
    }

    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
    
    func initialize() {
        guard let view = self.settingXib() else {
            return
        }
        self.addSubview(view)
        self.layoutXib(view)
        self.progressView.setProgress(0, animated: false)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.setProgress(_:)), name: "UploadProgressNotification", object: nil)
    }
    
    func setProgress(notification: NSNotification) {
        guard let progress = notification.userInfo?["progress"] as? Float else {
            return
        }
        // progressBar の更新はメインスレッドで実行する必要があり、
        // notification による通知の受信はメインスレッドで実行されない為
        // main_queue でメインスレッドにして実行する必要がある
        dispatch_async(dispatch_get_main_queue()) {
            self.progressView.setProgress(progress, animated: true)
        }
    }
    
    private func settingXib() -> UIView? {
        let bundle = NSBundle(forClass: self.dynamicType)
        let nib = UINib(nibName: "UploadProgress", bundle: bundle)
        guard let view = nib.instantiateWithOwner(self, options: nil).first as? UIView else {
            return nil
        }
        return view
    }
    
    private func layoutXib(view: UIView) {
        view.translatesAutoresizingMaskIntoConstraints = false
        let bindings = ["view": view]
        addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|",
            options:NSLayoutFormatOptions(rawValue: 0),
            metrics:nil,
            views: bindings))
        addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|",
            options:NSLayoutFormatOptions(rawValue: 0),
            metrics:nil,
            views: bindings))
    }
}

UploadProgressUIView を表示したい ViewController では upload の start と end の通知を受け取って UploadProgressUIView の addSubview と removeFromSuperview を行います。 NavigationBar の下に表示するようにしています。

class UploadViewController: UIViewController {
    private let uploadProgressView = UploadProgressUIView()

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.addUploadProgressView), name: "UploadStartNotification", object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.removeUploadProgressView), name: "UploadCompleteNotification", object: nil)
    }
    
    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }


    func addUploadProgressView() {
        self.uploadProgressView.alpha = 1
        self.view.addSubview(self.uploadProgressView)
        self.settingProgressView()
    }
    
    func removeUploadProgressView() {
        UIView.animateWithDuration(3, animations: {
            self.uploadProgressView.alpha = 0
        }, completion: { finished in
            self.uploadProgressView.removeFromSuperview()
        })
    }

    private func settingProgressView() {
        guard let navigationBarHidden = self.navigationController?.navigationBarHidden else {
            return
        }
        guard !navigationBarHidden else {
            return
        }        
        let constraints = UIUtility.topLayoutGuideAutoLayoutItem(self.uploadProgressView, viewController: self)
        self.view.addConstraints(constraints)
        self.uploadProgressView.translatesAutoresizingMaskIntoConstraints = false
        self.view.bringSubviewToFront(self.uploadProgressView)
    }
struct UIUtility {
    static func topLayoutGuideAutoLayoutItem(item: UIView, viewController: UIViewController) -> [NSLayoutConstraint] {
        let top = NSLayoutConstraint(
            item: item,
            attribute: .Top,
            relatedBy: .Equal,
            toItem: viewController.topLayoutGuide,
            attribute: .Bottom,
            multiplier: 1.0,
            constant: 0
        )
        
        let width = NSLayoutConstraint(
            item: item,
            attribute: .Width,
            relatedBy: .Equal,
            toItem: nil,
            attribute: .Width,
            multiplier: 1.0,
            constant: UIScreen.mainScreen().bounds.width
        )
        return [top, width]
    }
}