読者です 読者をやめる 読者になる 読者になる

ボクココ

サービス開発を成功させるまでの歩み

iOS8 で画像をアルバム or カメラで取得し、S3 へアップロードする

iOS

だいぶお決まりな処理な気がするのでまとめてみる。 iOS8 からは Photos Framework という画像を扱うフレームワークがあり、これを利用していく。 UIImagePickerController を使えば、画像選択まではそれなりにうまくいくのだが、 S3 へアップロードとなるとそのファイルパスが必要になってきて、そのファイルパスをどうやってとってくればいいのかが難しいところ。

画像は特に、自前のサーバへアップロードするんじゃなくて、S3に直接アップロードしたほうが負荷的にも優しいし、マルチパートのPOSTを実装する必要もなくなるので、基本的には採用すべきだと思う。そのURLだけを自前サーバにポストすればいいだけにしよう。

画像をリサイズして色々なサイズの画像をS3に保存したいというようなケースも多くあると思う。その時は自前サーバのAPIで、アプリから渡したS3のURLにある画像を一度サーバに取り込んでからリサイズし、再度S3へアップロードという形の方がいいと思う。とにかくアプリから画像をアップロードする処理の実装はiOSでもAndroidでも大変だからオススメしないw

以下手順をまとめる。

UIImagePickerController を表示

タイプを指定して presentViewController すればいいだけ。とても簡単だ。

        var actionSheet = UIAlertController(title:"Image", message: "Select the image", preferredStyle: UIAlertControllerStyle.ActionSheet)
        var actionCancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: {action in
            //nothing
        })
        var actionNormal1 = UIAlertAction(title: "From Album", style: UIAlertActionStyle.Default, handler: {action in
            let imagePickerVc = UIImagePickerController()
            imagePickerVc.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
            imagePickerVc.delegate = self
            self.presentViewController(imagePickerVc, animated: true, completion: nil)
        })
        var actionNormal2 = UIAlertAction(title: "From Camera", style: UIAlertActionStyle.Default, handler: {action in
            let imagePickerVc = UIImagePickerController()
            imagePickerVc.sourceType = UIImagePickerControllerSourceType.Camera
            imagePickerVc.delegate = self
            self.presentViewController(imagePickerVc, animated: true, completion: nil)           
        })
        actionSheet.addAction(actionCancel)
        actionSheet.addAction(actionNormal1)
        actionSheet.addAction(actionNormal2)
        
        self.presentViewController(actionSheet, animated: true, completion: nil)

とりあえずこれだけでアルバムとカメラそれぞれ起動できる。

取得した画像のパスを取得

カメラで撮った場合、画像はまだどこにも保存されない状態で戻ってくるので、いったんどこかに保存して、そのパスを取得する必要がある。アルバムに保存してもいいんだけど、割と撮った写真はそのアプリだけのための一時的な画像であることが多いので、他の適当な場所に保存することにする。

UIImagePickerController が終わったタイミングで呼ばれるメソッドを定義してその中に処理を書く。

    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
            self.dismissViewControllerAnimated(true, completion: nil)
        // from camaera
        if (info.indexForKey(UIImagePickerControllerOriginalImage) != nil) {
            let tookImage: UIImage = info[UIImagePickerControllerOriginalImage] as UIImage
            var imagePath = NSHomeDirectory()
            imagePath = imagePath.stringByAppendingPathComponent("Documents/face.png")
            var imageData: NSData = UIImagePNGRepresentation(tookImage)
            let isSuccess = imageData.writeToFile(imagePath, atomically: true)
            if isSuccess {
                let fileUrl: NSURL = NSURL(fileURLWithPath: imagePath)!
                uploadToS3(fileUrl)
            }
            return
        }
        
        // from album
        var pickedURL:NSURL = info[UIImagePickerControllerReferenceURL] as NSURL
        let fetchResult: PHFetchResult = PHAsset.fetchAssetsWithALAssetURLs([pickedURL], options: nil)
        let asset: PHAsset = fetchResult.firstObject as PHAsset
        
        PHImageManager.defaultManager().requestImageDataForAsset(asset, options: nil, resultHandler: {(imageData: NSData!, dataUTI: String!, orientation: UIImageOrientation, info: [NSObject : AnyObject]!) in
            let fileUrl: NSURL = info["PHImageFileURLKey"] as NSURL
            self.uploadToS3(fileUrl)
        })
    }

このパスを取得する方法だけど、かなり色々試行錯誤した結果の上でのコードなので、他にいいやり方があるのかもしれない。。

これが終わったら、いよいよアップロードだ。

S3 へアップロード

いくつか手順を踏まないといけない。

AWS SDK for iOS のインストール

CocoaPods で簡単に入れられる。

pod 'AWSiOSSDKv2'
pod 'AWSCognitoSync'

からの pod install

Bridging-Header に以下を追記。

#import "AWSCore.h"
#import "S3.h"

これで完了。

Amazon Cognito の登録

ではまずは準備。Amazon Cognito というAWSサービスをまずは登録する必要がある。

これを登録すると、データ同期の仕組みやユーザー認証の仕組みなども割と手軽に実装できるみたいだ。結構データ同期とか自前で実装すると同期に失敗したときとか戻しが効かなくなったりするので、今度機会があったら使ってみよう。

今回はとりあえずこれを登録して、S3を使える状態にする。登録完了後、AppDelegete に以下を追記。

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        let credentialsProvider = AWSCognitoCredentialsProvider.credentialsWithRegionType(
            AWSRegionType.USEast1,
            accountId: "You AccountId",
            identityPoolId: "your identityPoolId",
            unauthRoleArn: "Your UnauthRoleArn",
            authRoleArn: "Your AuthRoleArn")
        let defaultServiceConfiguration = AWSServiceConfiguration(
            region: AWSRegionType.USEast1,
            credentialsProvider: credentialsProvider)
        AWSServiceManager.defaultServiceManager().setDefaultServiceConfiguration(defaultServiceConfiguration)
        
        credentialsProvider.getIdentityId().continueWithBlock({ (task) -> AnyObject! in
            var myId = credentialsProvider.identityId
            println("myId is \(myId)")
            self.userDefault.setObject(myId, forKey: Conf.KEY_USER_ID)
            self.userDefault.synchronize()
            return nil
        })

        return true
    }

後半の setDefaultServiceConfiguration の呼び出しは、端末固有のIDを生成するのに使っている。これからアップロードする画像の名前を一意にするために、このIDと時刻を使って名前を生成し、S3へアップロードしよう。

S3 の設定

S3アップロード処理を書く前に、S3の設定がいる。そうしないとうまくいっても Permission Denied になってしまう。

まずはBucket を作成しよう。 ちなみにリージョンはus-east-1 がいいみたい? ここら辺は他のネット情報からなのでもしかしたら東京でも大丈夫。

作成したらそのBucketのパーミッションで EveryOne が UploadとRead できるようにしておく。それに追加して、Create Bucket Policy を選択し、以下のJSONを貼る。

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:PutObjectAcl",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
        }
    ]
}

ここのs3:PutObjectAcl がポイント。これをしないと読み込みの時にその画像を閲覧するということができなくなってしまう。

そしたら準備完了。

アップロード処理の記述

そしたらようやくコードが書ける。

    func uploadToS3(fileUrl: NSURL) {
         //make a timestamp variable to use in the key of the video I'm about to upload
        let date:NSDate = NSDate()
        var unixTimeStamp:NSTimeInterval = date.timeIntervalSince1970
        var unixTimeStampString:String = String(format:"%f", unixTimeStamp)
        println("this is my unix timestamp as a string: \(unixTimeStampString)")
        
        // set upload settings
        var myTransferManagerRequest:AWSS3TransferManagerUploadRequest = AWSS3TransferManagerUploadRequest()
        myTransferManagerRequest.bucket = "YOUR_BUCKET_NAME"
        var myId = userDefault.stringForKey(Conf.KEY_USER_ID)
        self.uploadedFileName = "\(myId!)_\(unixTimeStampString).jpg"
        myTransferManagerRequest.key = self.uploadedFileName
        myTransferManagerRequest.body = fileUrl
        myTransferManagerRequest.ACL = AWSS3ObjectCannedACL.PublicRead
        
        var myBFTask:BFTask = BFTask()
        var myMainThreadBFExecutor:BFExecutor = BFExecutor.mainThreadExecutor()
        var myTransferManager:AWSS3TransferManager = AWSS3TransferManager.defaultS3TransferManager()
        myTransferManager.upload(myTransferManagerRequest).continueWithExecutor(myMainThreadBFExecutor, withBlock: { (myBFTask) -> AnyObject! in
            if((myBFTask.result) != nil){
                println("Success!!")
                // send api?
                let s3Path = Conf.AWS_S3_URL + self.uploadedFileName
                println("uploaded s3 path is \(s3Path)")
                
            } else {
                println("upload didn't seem to go through..")
                var myError = myBFTask.error
                println("error: \(myError)")
            }
            return nil
        })       
    }

これで無事、対象のBucketに画像がアップロードされていることだろう。

終わりに

ちゃんとやるなら、たぶんこれにアップロード中は dispatch_asyncでバックグラウンドで処理させることが必要。あとuploading の表示も必要だ。

よくやるはずの処理なのに、まだSwiftでの記事が少なかったり、Photos フレームワークの資料が少なかったりでもっといいやり方があるのかもしれないので、その場合はシェアしてくださると嬉しいです。