Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

ը૾ΛϦαΠζ

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

ಡΈࠐΈ

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

ͦͷଞͰ΍͍ͬͯΔ͜ͱ

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

σϞ

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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