だいぶお決まりな処理な気がするのでまとめてみる。 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 へアップロード
いくつか手順を踏まないといけない。
CocoaPods で簡単に入れられる。
pod 'AWSiOSSDKv2'
pod 'AWSCognitoSync'
からの pod install
Bridging-Header
に以下を追記。
#import "AWSCore.h"
#import "S3.h"
これで完了。
ではまずは準備。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 フレームワークの資料が少なかったりでもっといいやり方があるのかもしれないので、その場合はシェアしてくださると嬉しいです。