Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

miup
July 19, 2018

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

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

miup

July 19, 2018
Tweet

More Decks by miup

Other Decks in Programming

Transcript

  1. Cookpad Inc.
    Firestore, Cloud StorageΛ༻͍ͨ
    ΞϓϦ಺Ͱͷը૾ͷѻ͍ํ
    ࡾӜ࿨໵

    View Slide

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

    View Slide

  3. Cookpad Inc. All Rights Reserved.
    ࠓ೔࿩͢͜ͱ
    w'JSFCBTFΛόοΫΤϯυʹஔ͘ΞϓϦͰͷը૾ͷѻ͍ํ
    wCloud StorageʹσʔλΛอଘ
    wFirestoreͱStorageͷ࿈ܞ
    wϦαΠζ
    wνϡʔχϯά
    wσϞ

    View Slide

  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
    Ұ࿈ͷྲྀΕ

    View Slide

  5. Cookpad Inc. All Rights Reserved.
    ࠓ೔࿩͞ͳ͍͜ͱ
    wModel ͷઃܭ
    wKomerco Ͱͷ࣮ࡍͷίʔυͱ͸ҟͳΓ·͢
    wϧʔϧ
    ͜ͷ৔Ͱ࿩͞ͳ͍͚ͩͰޙͰฉ͍ͯ΋Β͑Ε͹͓࿩͠·͢ʂ

    View Slide

  6. Cookpad Inc. All Rights Reserved.
    $MPVE4UPSBHF
    $MPVE'VODUJPOT
    $MPVE'JSFTUPSF
    save image
    completion
    ը૾ͷอଘ

    View Slide

  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

    View Slide

  8. Cookpad Inc. All Rights Reserved.
    $MPVE4UPSBHF
    $MPVE'VODUJPOT
    $MPVE'JSFTUPSF
    save image
    completion
    save model completion
    Firestore ͱ Storage ͷ࿈ܞ

    View Slide

  9. Cookpad Inc. All Rights Reserved.
    Firestore ͱ Storage ͷ࿈ܞ
    wอଘ
    wStorage ʹ Data Λอଘ
    wStorageReferencePath Λ Firestore ʹอଘ͢Δ

    View Slide

  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

    View Slide

  11. ը૾ΛϦαΠζ

    View Slide

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

    View Slide

  13. Cookpad Inc. All Rights Reserved.
    $MPVE4UPSBHF
    $MPVE'VODUJPOT
    $MPVE'JSFTUPSF
    save image
    completion
    event trigger
    save model completion
    ը૾ͷϦαΠζ

    View Slide

  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(snapshot)
    console.log(image)
    })
    5ZQF4DSJQU

    View Slide

  15. Cookpad Inc. All Rights Reserved.
    ը૾ͷϦαΠζ
    wͳͥ Storage ͷ onCreate Λ࢖Θͳ͍ͷ͔
    wStorage ͷ onCreate ͷஈ֊Ͱ͸·ͩ Firestore ʹσʔλ͸ແ͍

    ϦαΠζͯ͠΋ͦͷ৘ใΛ Firestore ʹॻ͖ࠐΊͳ͍Մೳੑ
    w΋͠ Firestore ΁ͷอଘ͕ࣦഊ͍ͯͨ͠Βσʔλͷෆ੔߹΍

    Τϥʔ͕ൃੜ

    View Slide

  16. Cookpad Inc. All Rights Reserved.
    $MPVE4UPSBHF
    $MPVE'VODUJPOT
    $MPVE'JSFTUPSF
    save image
    completion
    event trigger
    save model completion
    download image
    ը૾ͷϦαΠζ

    View Slide

  17. export async function resize(image: Tart.Snapshot) {
    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

    View Slide

  18. Cookpad Inc. All Rights Reserved.
    ը૾ͷϦαΠζ
    wϦαΠζ৘ใͷఆٛ
    w֤αΠζʹରͯ͠ɺը૾αΠζͱϑΝΠϧ໊ͷ prefix Λఆٛ͢Δ

    View Slide

  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

    View Slide

  20. Cookpad Inc. All Rights Reserved.
    ը૾ͷϦαΠζ
    w࣮ࡍʹϦαΠζ͍ͯ͘͠
    wࠓఆٛͨ͠3FTJ[F5ZQFΛ౉ͯ͠ϦαΠζ͢Δ
    wprocess-promises ͷ spawn Ͱ ImageMagick Λ࣮ߦ

    View Slide

  21. export async function resize(image: Tart.Snapshot) {
    ...
    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

    View Slide

  22. Cookpad Inc. All Rights Reserved.
    $MPVE4UPSBHF
    $MPVE'VODUJPOT
    $MPVE'JSFTUPSF
    save image
    completion
    event trigger
    save model completion
    save resized image
    download image
    ը૾ͷϦαΠζ

    View Slide

  23. export async function resize(image: Tart.Snapshot) {
    ...
    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

    View Slide

  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
    ը૾ͷϦαΠζ

    View Slide

  25. Cookpad Inc. All Rights Reserved.
    ը૾ͷϦαΠζ
    wImage Ϟσϧͷߋ৽Λߦ͏
    wϦαΠζͨ͠ը૾ͷ Path ΛϞσϧʹॻ͖ࠐΜͰ͓͘

    View Slide

  26. export async function resize(image: Tart.Snapshot) {
    ...
    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, 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

    View Slide

  27. ಡΈࠐΈ

    View Slide

  28. Cookpad Inc. All Rights Reserved.
    ը૾ͷಡΈࠐΈ
    wඞཁͳͱ͜ΖͰඞཁͳαΠζΛϩʔυ͢Δ
    wΫϥΠΞϯτͷϞσϧʹ΋αΠζΛ enum Ͱఆٛ
    wαϜωΠϧͰ͸খ͍͞΍ͭ
    wৄࡉը໘ʹߦͬͨΒͰ͔͍΍ͭ

    View Slide

  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

    View Slide

  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

    View Slide

  31. ͦͷଞͰ΍͍ͬͯΔ͜ͱ

    View Slide

  32. Cookpad Inc. All Rights Reserved.
    ύϑΥʔϚϯε޲্ͷͨΊʹ΍͍ͬͯΔ͜ͱ
    wΩϟογϡ
    wImage ([ID: Image]) => ࣗલͰ૊ΜͰ͍Δ
    wը૾࣮ମ([RefPath: UIImage]) => ImageStore (github.com/miuP/ImageStore)
    wը૾࣮ମ ([RefPath: UIImage]) ͚ͩͩͱଞͷϞσϧ͔Β

    ࢀরΛऔΔͱ͖ʹ Image ϞσϧͷऔಘͰҰॠϩʔυ͕૸Δ

    View Slide

  33. // cellForItemAtIndexPath ͱ͔ cell ͷ configure ϝιου
    products[indexPath.item].image.get { [weak self] image in
    self?.productImageView.load(image.getRef(of: .large)
    }
    4XJGU
    ηϧ͕ϩʔυ͞ΕΔ౓ʹඇಉظͰಡΈࠐΉ͜ͱʹͳΔ

    View Slide

  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

    View Slide

  35. Cookpad Inc. All Rights Reserved.
    ύϑΥʔϚϯε޲্ͷͨΊʹ΍͍ͬͯΔ͜ͱ
    wΫϥΠΞϯτͰjpegѹॖ͢Δ
    wࣦഊͨ͠ͱ͖ʹ͋ͱ͔ΒϦαΠζͰ͖ΔΑ͏ʹ͓ͯ͘͠
    w੒ޭͨ͠Β্͛Δϑϥά
    wϧʔϧͪΌΜͱॻ͘ (update, list͸࢖Θͳ͚Ε͹࠹͍Ͱ͠·͓͏)

    View Slide

  36. σϞ

    View Slide

  37. Cookpad Inc. All Rights Reserved.
    σϞΞϓϦ

    ( github.com/miuP/FirestoreStorageSample )
    wը૾Λ౤ߘͯ͠ҰཡͱৄࡉΛݟΔ͚ͩ
    wΩϟογϡ
    wϦαΠζ
    wΩϟογϡ͕͏·͘ಈ࡞͍ͯ͠ΔͷΛݟΔͨΊʹ Image Λݟʹߦ
    ͘ͷͰ͸ͳ͘ Image Λϥοϓͨ͠ Dummy ͱ͍͏ϞσϧΛ࡞ͬͨ
    ⚠༗ྉϓϥϯ͡Όͳ͍ͱಈ͖·ͤΜʂʂʂ

    View Slide

  38. Cookpad Inc. All Rights Reserved.
    ·ͱΊ
    wFirestore ͱ Cloud Strage ͷ࿈ܞ
    wجຊ͸ RefPath Λը૾༻ͷ Model ʹ࣋ͨͤΔ
    wϦαΠζ͸ΫϥΠΞϯτͰ͸ͳ͘ CloudFunctions Ͱ
    wΑ͠ͳʹΩϟογϡ͢Δ

    View Slide