簡単にFirebase Storageに複数のファイルをアップロードする[swift]

# 概要
複数のファイルのアップロードを以下のように扱えるようにStorageUploaderを作りました。どれくらいアップロードされているのか監視もできます。

```swift:sample.swift
let uploader = StorageUploader()

// アップロードしたいファイルを追加
uploader.addFile(file:URL.init("video"), childRef:videoChildRef, metadata:videoMetadata)
uploader.addData(data:imageData, childRef:imageChildRef, metadata:imageMetadata)

// アップロード状況の監視
uploader.progressHandler = {completedCount, totalCount in
    print("\(100 * completedCount / totalCount)%")
}

// アップロード完了時の処理
uploader.completeHandler = { metadatas in
    print("complete")
}

// アップロード失敗時の処理
uploader.failureHandler = { error in
    // switch (StorageErrorCode(rawValue: error.code)!) { ...
    print("fail")
}

// アップロード開始
uploader.start()
```
# StorageUploaderのコード
```swift:StorageUploader.swift
import Foundation
import FirebaseStorage

class StorageUploader {
    fileprivate struct SUComponent {
        var file:URL?
        var data:Data?
        var childRef:StorageReference?
        var metadata:StorageMetadata?
        var isComplete = false
    }

    var progressHandler: (_ completedUnitCount: Int64, _ totalUnitCount: Int64) -> Void = {_, _ in }
    var completeHandler: (_ metaDatas: [StorageMetadata]) -> Void = {_ in }
    var failureHandler: (_ error: NSError?) -> Void = {_ in }
    
    fileprivate var components = [SUComponent]()
    fileprivate var uploadTasks = [StorageUploadTask]()
    fileprivate var totalUnitCount:Int64 = 1
    fileprivate var completedUnitCount:Int64 = 0
    fileprivate var isfailure = false
    
    func addFile(file:URL, childRef:StorageReference, metadata:StorageMetadata) {
        var uploadInfo = SUComponent()
        uploadInfo.file = file
        uploadInfo.childRef = childRef
        uploadInfo.metadata = metadata
        components.append(uploadInfo)
    }
    
    func addData(data:Data, childRef:StorageReference, metadata:StorageMetadata) {
        var uploadInfo = SUComponent()
        uploadInfo.data = data
        uploadInfo.childRef = childRef
        uploadInfo.metadata = metadata
        components.append(uploadInfo)
    }
    
    func start() {
        prepareUpload()
        progressHandler(completedUnitCount, totalUnitCount)
    }
    
    fileprivate func prepareUpload() {
        components.forEach { uploadInfo in
            let childRef = uploadInfo.childRef
            let metadata = uploadInfo.metadata
            if let file = uploadInfo.file {
                guard let uploadTask = childRef?.putFile(from: file, metadata: metadata) else {
                    return
                }
                observeStatus(uploadTask)
            } else if let data = uploadInfo.data {
                guard let uploadTask = childRef?.putData(data, metadata: metadata) else {
                    return
                }
                observeStatus(uploadTask)
            }
        }
    }
    
    fileprivate func observeStatus(_ uploadTask: StorageUploadTask) {
        uploadTasks.append(uploadTask)
        uploadTask.observe(.progress) { snapshot in
            var currentCompletedUnitCount:Int64 = 0
            var currentTotalUnitCount:Int64 = 0
            self.uploadTasks.forEach { uploadTask in
                currentCompletedUnitCount += (uploadTask.snapshot.progress?.completedUnitCount ?? 0)
                currentTotalUnitCount += (uploadTask.snapshot.progress?.totalUnitCount ?? 0)
            }
            self.completedUnitCount = currentCompletedUnitCount
            self.totalUnitCount = currentTotalUnitCount
            self.progressHandler(currentCompletedUnitCount, currentTotalUnitCount)
        }
        uploadTask.observe(.success) { snapshot in
            if self.completedUnitCount == self.totalUnitCount {
                let metaDatas = self.uploadTasks.map { uploadTask -> StorageMetadata in
                    return uploadTask.snapshot.metadata ?? StorageMetadata()
                }
                self.completeHandler(metaDatas)
            }
        }
        uploadTask.observe(.failure) { snapshot in
            if !self.isfailure {
                self.isfailure = true
                self.uploadTasks.forEach { uploadTask in
                    if uploadTask.snapshot.progress?.isCancellable ?? false {
                        uploadTask.cancel()
                    }
                }
                let error = snapshot.error as NSError?
                self.failureHandler(error)
            }
        }
    }
}
```
# エラーハンドリング
エラーから以下の情報がわかる。

```swift:sample.swift
switch (StorageErrorCode(rawValue: error.code)!) {
case .unknown:
    // An unknown error occurred
    break
case .objectNotFound:
    // No object exists at the desired reference
    break
case .bucketNotFound:
    // No bucket is configured for Firebase Storage
    break
case .projectNotFound:
    // No project is configured for Firebase Storage
    break
case .quotaExceeded :
    // Quota on your Firebase Storage bucket has been exceeded
    // If you're on the free tier, upgrade to a paid plan
    // If you're on a paid plan, reach out to Firebase support
    break
case .unauthenticated:
    // User is unauthenticated. Authenticate and try again
    break
case .unauthorized:
    // User is not authorized to perform the desired action
    // Check your rules to ensure they are correct
    break
case .retryLimitExceeded :
    // The maximum time limit on an operation (upload, download, delete, etc.) has been exceeded
    // Try uploading again
    break
case .nonMatchingChecksum :
    // File on the client does not match the checksum of the file received by the server
    // Try uploading again
    break
case .downloadSizeExceeded :
    // Size of the downloaded file exceeds the amount of memory allocated for the download
    // Increase memory cap and try downloading again
    break
case .cancelled:
    // User cancelled the operation
    break
case .invalidArgument :
    // An invalid argument was provided
    break
    /* ... */
default:
    // A separate error occurred. This is a good place to retry the upload.
    break
}

```