Firestore, Cloud Storage を用いた アプリ内での画像の扱い方

Ebb5511374cdb5d46e5587608a899284?s=47 miup
July 19, 2018

Firestore, Cloud Storage を用いた アプリ内での画像の扱い方

Cookpad Tech Kitchen #16 コメルコテックバナシ

Ebb5511374cdb5d46e5587608a899284?s=128

miup

July 19, 2018
Tweet

Transcript

  1. 4.

    Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion event trigger update model save model completion save resized image download image Ұ࿈ͷྲྀΕ
  2. 7.

    static func saveData(_ data: Data, path: String, completion: ((StorageMetadata?, Error?)

    -> Void)? = nil) { let refPath = Storage.storage().reference(withPath: path) refPath.putData(data, metadata: nil) { (metadata, error) in completion?(metadata, error) } } 4XJGU
  3. 8.

    Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion save model completion Firestore ͱ Storage ͷ࿈ܞ
  4. 9.

    Cookpad Inc. All Rights Reserved. Firestore ͱ Storage ͷ࿈ܞ wอଘ

    wStorage ʹ Data Λอଘ wStorageReferencePath Λ Firestore ʹอଘ͢Δ
  5. 10.

    class Imageɹ{ let id: String let originalRefPath: String let fileName:

    String static func create(image: UIImage, completion: ((Image?, Error?) -> Void)? = nil) { let newImageRef = Firestore.firestore().collection("/images").document() let fileName = "\(Int(Date().timeIntervalSince1970 * 1000)).jpg" let storageRefPath = "images/\(newImageRef.documentID)/\(fileName)" saveData(UIImageJPEGRepresentation(image, 0.75)!, path: storageRefPath) { (_, error) in if let error = error { completion?(nil, error); return } let image = Image(id: newImageRef.documentID, originalRefPath: storageRefPath, fileName: fileName) newImageRef.setData([ "createdAt": FieldValue.serverTimestamp(), "updatedAt": FieldValue.serverTimestamp(), "originalRefPath": storageRefPath, "fileName": fileName]) { error in if let error = error { completion?(nil, error); return } completion?(image, nil) } } } } 4XJGU
  6. 13.

    Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion event trigger save model completion ը૾ͷϦαΠζ
  7. 14.

    export interface Image extends Tart.Timestamps { fileName: string originalRefPath: string

    } export const resizeImage = functions.firestore.document('images/{imageID}').onCreate((snapshot, context) => { const image = new Tart.Snapshot<Image>(snapshot) console.log(image) }) 5ZQF4DSJQU
  8. 15.

    Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wͳͥ Storage ͷ onCreate

    Λ࢖Θͳ͍ͷ͔ wStorage ͷ onCreate ͷஈ֊Ͱ͸·ͩ Firestore ʹσʔλ͸ແ͍
 ϦαΠζͯ͠΋ͦͷ৘ใΛ Firestore ʹॻ͖ࠐΊͳ͍Մೳੑ w΋͠ Firestore ΁ͷอଘ͕ࣦഊ͍ͯͨ͠Βσʔλͷෆ੔߹΍
 Τϥʔ͕ൃੜ
  9. 16.

    Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion event trigger save model completion download image ը૾ͷϦαΠζ
  10. 17.

    export async function resize(image: Tart.Snapshot<Image>) { const imageID = image.ref.id

    const fileName = image.data.fileName const filePath = `images/${imageID}/${fileName}` // instantiate Google Storage Bucket const bucket = gcs().bucket(JSON.parse(process.env.FIREBASE_CONFIG!).storageBucket) const file = await bucket.file(filePath).get().then(result => { return result[0] }) // /tmp/${fileName} const tempFilePath = path.join(os.tmpdir(), fileName) await file.download({ destination: tempFilePath }) } 5ZQF4DSJQU
  11. 19.

    enum ResizeType { Large = 'large', Medium = 'medium', Small

    = 'small', Thumbnail = 'thumbnail' } function resizeInfo(resizeType: ResizeType): string { switch (resizeType) { case ResizeType.Large: return '1242x1242>' case ResizeType.Medium: return '495x495>' case ResizeType.Small: return '252x252>' case ResizeType.Thumbnail: return '144x144>' default: return '' } } function fieldValueName(resizeType: ResizeType): string { switch (resizeType) { case ResizeType.Large: return 'largeRefPath' case ResizeType.Medium: return 'mediumRefPath' case ResizeType.Small: return 'smallRefPath' case ResizeType.Thumbnail: return 'thumbnailRefPath' default: return '' } } 5ZQF4DSJQU
  12. 21.

    export async function resize(image: Tart.Snapshot<Image>) { ... const resizeTypes: ResizeType[]

    = [ ResizeType.Large, ResizeType.Medium, ResizeType.Small, ResizeType.Thumbnail] await Promise.all(resizeTypes.map(type => { return resizeImage(tempFilePath, fileName, type) })) } function resizeImage(filePath: string, fileName: string, resizeType: ResizeType) { const dest = path.join(os.tmpdir(), `${resizeType}_${fileName}`) return spawn('convert', [filePath, '-thumbnail', resizeInfo(resizeType), `${dest}`]) .then(() => { return dest }) } 5ZQF4DSJQU
  13. 22.

    Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion event trigger save model completion save resized image download image ը૾ͷϦαΠζ
  14. 23.

    export async function resize(image: Tart.Snapshot<Image>) { ... await Promise.all(resizeTypes.map(type =>

    { return resizeImage(tempFilePath, fileName, type) .then(path => uploadToBucket(bucket, path, type, fileName, filePath)) })) } function uploadToBucket(bucket: Bucket, source: string, prefix: string, fileName: string, filePath: string) { const destName = `${prefix}_${fileName}` const destDir = path.dirname(filePath) const dest = path.join(destDir, destName) return bucket .upload(source, { destination: dest, metadata: metadata }) // delete tmp/${ResizeType.prefix}_${fileName} .then(() => fs.unlinkSync(source)) } 5ZQF4DSJQU
  15. 24.

    Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion event trigger update model save model completion save resized image download image ը૾ͷϦαΠζ
  16. 26.

    export async function resize(image: Tart.Snapshot<Image>) { ... await Promise.all(resizeTypes.map(type =>

    { return resizeImage(tempFilePath, fileName, type) .then(path => uploadToBucket(bucket, path, type, fileName, filePath)) .then(() => updateImageModel(image, type)) })) } function updateImageModel(image: Tart.Snapshot<Image>, resizeType: ResizeType) { const key = fieldValueName(resizeType) const updateInfo: { [id: string]: string } = {} const refPath = `images/${image.ref.id}/${resizeType}_${image.data.fileName}` updateInfo[key] = refPath return image.update(updateInfo) } 5ZQF4DSJQU
  17. 29.

    class Image: Object { enum Size { case large case

    medium case small case thumbnail case original } func getRef(of size: Size) -> StorageReference? { let path: String? switch size { case .large: path = [largeRefPath, mediumRefPath, smallRefPath, thumbnailRefPath, originalRefPath].compactMap { $0 }.first case .medium: path = [mediumRefPath, largeRefPath, smallRefPath, thumbnailRefPath, originalRefPath].compactMap { $0 }.first case .small: path = [smallRefPath, mediumRefPath, largeRefPath, thumbnailRefPath, originalRefPath].compactMap { $0 }.first case .thumbnail: path = [thumbnailRefPath, smallRefPath, mediumRefPath, largeRefPath, originalRefPath].compactMap { $0 }.first case .original: path = originalRefPath } guard let refPath = path else { return nil } return Storage.storage().reference().root().child(refPath) } } 4XJGU
  18. 30.

    class ImageCell: UICollectionViewCell, Reusable, NibType { typealias Dependency = Image

    private var id: String? @IBOutlet private weak var imageView: UIImageView! func inject(_ dependency: Image) { id = dependency.id imageView.load(dependency.getRef(of: .small)!) { [weak self] in return self?.id == dependency.id } } override func prepareForReuse() { super.prepareForReuse() id = nil imageView.image = nil } } 4XJGU IUUQTRJJUBDPNNJV1JUFNTDFBEEGF
  19. 32.

    Cookpad Inc. All Rights Reserved. ύϑΥʔϚϯε޲্ͷͨΊʹ΍͍ͬͯΔ͜ͱ wΩϟογϡ wImage ([ID: Image])

    => ࣗલͰ૊ΜͰ͍Δ wը૾࣮ମ([RefPath: UIImage]) => ImageStore (github.com/miuP/ImageStore) wը૾࣮ମ ([RefPath: UIImage]) ͚ͩͩͱଞͷϞσϧ͔Β
 ࢀরΛऔΔͱ͖ʹ Image ϞσϧͷऔಘͰҰॠϩʔυ͕૸Δ
  20. 33.

    // cellForItemAtIndexPath ͱ͔ cell ͷ configure ϝιου products[indexPath.item].image.get { [weak

    self] image in self?.productImageView.load(image.getRef(of: .large) } 4XJGU ηϧ͕ϩʔυ͞ΕΔ౓ʹඇಉظͰಡΈࠐΉ͜ͱʹͳΔ
  21. 34.

    extension UIImageView { func load(firebaseImageID imageID: String, size: Image.Size) {

    if imageID.isEmpty { return } if let storageRef = FirebaseImageCache.shared.retrieveImageReference(imageID: imageID, of: size) { load(storageRef) } else { Firestore.firestore().document("images/\(imageID)").getDocument { [weak self] snapshot, _ in guard let snapshot = snapshot else { return } let image = Image(id: snapshot.documentID, data: snapshot.data()!) FirebaseImageCache.shared.setImageReferences(imageID: imageID, image: image) if let storageRef = image.getRef(of: size) { self?.load(storageRef) } } } } } imageView.load(firebaseImageID: products[indexPath.item].image.documentID, size: .small) 4XJGU
  22. 36.
  23. 37.

    Cookpad Inc. All Rights Reserved. σϞΞϓϦ
 ( github.com/miuP/FirestoreStorageSample ) wը૾Λ౤ߘͯ͠ҰཡͱৄࡉΛݟΔ͚ͩ

    wΩϟογϡ wϦαΠζ wΩϟογϡ͕͏·͘ಈ࡞͍ͯ͠ΔͷΛݟΔͨΊʹ Image Λݟʹߦ ͘ͷͰ͸ͳ͘ Image Λϥοϓͨ͠ Dummy ͱ͍͏ϞσϧΛ࡞ͬͨ ⚠༗ྉϓϥϯ͡Όͳ͍ͱಈ͖·ͤΜʂʂʂ
  24. 38.

    Cookpad Inc. All Rights Reserved. ·ͱΊ wFirestore ͱ Cloud Strage

    ͷ࿈ܞ wجຊ͸ RefPath Λը૾༻ͷ Model ʹ࣋ͨͤΔ wϦαΠζ͸ΫϥΠΞϯτͰ͸ͳ͘ CloudFunctions Ͱ wΑ͠ͳʹΩϟογϡ͢Δ