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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

  11. ը૾ΛϦαΠζ

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

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

    Τϥʔ͕ൃੜ

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

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

    View full-size slide

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

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

    View full-size slide

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

    View full-size slide

  33. 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 full-size slide

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

    View full-size slide

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

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

    View full-size slide

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

    View full-size slide