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. Cookpad Inc. Firestore, Cloud StorageΛ༻͍ͨ ΞϓϦ಺Ͱͷը૾ͷѻ͍ํ ࡾӜ࿨໵

  2. Cookpad Inc. All Rights Reserved. ࣗݾ঺հ w໊લࡾӜ࿨໵ wܦྺ w17৽ଔೖࣾ wiOSྺ໿5೥

    wTwitter: __miup wGithub: miuP
  3. Cookpad Inc. All Rights Reserved. ࠓ೔࿩͢͜ͱ w'JSFCBTFΛόοΫΤϯυʹஔ͘ΞϓϦͰͷը૾ͷѻ͍ํ wCloud StorageʹσʔλΛอଘ wFirestoreͱStorageͷ࿈ܞ

    wϦαΠζ wνϡʔχϯά wσϞ
  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 Ұ࿈ͷྲྀΕ
  5. Cookpad Inc. All Rights Reserved. ࠓ೔࿩͞ͳ͍͜ͱ wModel ͷઃܭ wKomerco Ͱͷ࣮ࡍͷίʔυͱ͸ҟͳΓ·͢

    wϧʔϧ ͜ͷ৔Ͱ࿩͞ͳ͍͚ͩͰޙͰฉ͍ͯ΋Β͑Ε͹͓࿩͠·͢ʂ
  6. Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion ը૾ͷอଘ
  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
  8. Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion save model completion Firestore ͱ Storage ͷ࿈ܞ
  9. Cookpad Inc. All Rights Reserved. Firestore ͱ Storage ͷ࿈ܞ wอଘ

    wStorage ʹ Data Λอଘ wStorageReferencePath Λ Firestore ʹอଘ͢Δ
  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
  11. ը૾ΛϦαΠζ

  12. Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wΦϦδφϧαΠζ͚ͩͩͱ࢖͍উख͕ѱ͍ wCloudFunctions ͰϦαΠζΛ͢Δ wImageMagic

    Λ࢖༻ wKomerco Ͱ͸4αΠζʹϦαΠζ͍ͯ͠Δ
  13. Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion event trigger save model completion ը૾ͷϦαΠζ
  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
  15. Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wͳͥ Storage ͷ onCreate

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

    completion event trigger save model completion download image ը૾ͷϦαΠζ
  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
  18. Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wϦαΠζ৘ใͷఆٛ w֤αΠζʹରͯ͠ɺը૾αΠζͱϑΝΠϧ໊ͷ prefix Λఆٛ͢Δ

  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
  20. Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ w࣮ࡍʹϦαΠζ͍ͯ͘͠ wࠓఆٛͨ͠3FTJ[F5ZQFΛ౉ͯ͠ϦαΠζ͢Δ wprocess-promises ͷ

    spawn Ͱ ImageMagick Λ࣮ߦ
  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
  22. Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image

    completion event trigger save model completion save resized image download image ը૾ͷϦαΠζ
  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
  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 ը૾ͷϦαΠζ
  25. Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wImage Ϟσϧͷߋ৽Λߦ͏ wϦαΠζͨ͠ը૾ͷ Path

    ΛϞσϧʹॻ͖ࠐΜͰ͓͘
  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
  27. ಡΈࠐΈ

  28. Cookpad Inc. All Rights Reserved. ը૾ͷಡΈࠐΈ wඞཁͳͱ͜ΖͰඞཁͳαΠζΛϩʔυ͢Δ wΫϥΠΞϯτͷϞσϧʹ΋αΠζΛ enum Ͱఆٛ

    wαϜωΠϧͰ͸খ͍͞΍ͭ wৄࡉը໘ʹߦͬͨΒͰ͔͍΍ͭ
  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
  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
  31. ͦͷଞͰ΍͍ͬͯΔ͜ͱ

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

    => ࣗલͰ૊ΜͰ͍Δ wը૾࣮ମ([RefPath: UIImage]) => ImageStore (github.com/miuP/ImageStore) wը૾࣮ମ ([RefPath: UIImage]) ͚ͩͩͱଞͷϞσϧ͔Β
 ࢀরΛऔΔͱ͖ʹ Image ϞσϧͷऔಘͰҰॠϩʔυ͕૸Δ
  33. // cellForItemAtIndexPath ͱ͔ cell ͷ configure ϝιου products[indexPath.item].image.get { [weak

    self] image in self?.productImageView.load(image.getRef(of: .large) } 4XJGU ηϧ͕ϩʔυ͞ΕΔ౓ʹඇಉظͰಡΈࠐΉ͜ͱʹͳΔ
  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
  35. Cookpad Inc. All Rights Reserved. ύϑΥʔϚϯε޲্ͷͨΊʹ΍͍ͬͯΔ͜ͱ wΫϥΠΞϯτͰjpegѹॖ͢Δ wࣦഊͨ͠ͱ͖ʹ͋ͱ͔ΒϦαΠζͰ͖ΔΑ͏ʹ͓ͯ͘͠ w੒ޭͨ͠Β্͛Δϑϥά wϧʔϧͪΌΜͱॻ͘

    (update, list͸࢖Θͳ͚Ε͹࠹͍Ͱ͠·͓͏)
  36. σϞ

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

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

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