Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Firestore, Cloud Storage を用いた アプリ内での画像の扱い方
Search
miup
July 19, 2018
Programming
5
24k
Firestore, Cloud Storage を用いた アプリ内での画像の扱い方
Cookpad Tech Kitchen #16 コメルコテックバナシ
miup
July 19, 2018
Tweet
Share
More Decks by miup
See All by miup
Algolia with Firebase
miup
2
1.4k
Firestore のクエリと全文検索
miup
7
3.5k
Firebase Cloud Messaging 入門編
miup
0
5k
The way of truly serverless application
miup
1
4.5k
Other Decks in Programming
See All in Programming
SwiftDataを使って10万件のデータを読み書きする
akidon0000
0
140
2分台で1500examples完走!爆速CIを支える環境構築術 - Kaigi on Rails 2025
falcon8823
3
3.7k
コードとあなたと私の距離 / The Distance Between Code, You, and I
hiro_y
0
170
詳しくない分野でのVibe Codingで困ったことと学び/vibe-coding-in-unfamiliar-area
shibayu36
3
5.1k
Leading Effective Engineering Teams in the AI Era
addyosmani
6
450
オープンソースソフトウェアへの解像度🔬
utam0k
16
3k
Domain-centric? Why Hexagonal, Onion, and Clean Architecture Are Answers to the Wrong Question
olivergierke
3
900
登壇は dynamic! な営みである / speech is dynamic
da1chi
0
340
Go言語はstack overflowの夢を見るか?
logica0419
0
350
Introducing ReActionView: A new ActionView-Compatible ERB Engine @ Kaigi on Rails 2025, Tokyo, Japan
marcoroth
3
1k
タスクの特性や不確実性に応じた最適な作業スタイルの選択(ペアプロ・モブプロ・ソロプロ)と実践 / Optimal Work Style Selection: Pair, Mob, or Solo Programming.
honyanya
3
180
CSC305 Lecture 08
javiergs
PRO
0
210
Featured
See All Featured
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.5k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
115
20k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4k
Typedesign – Prime Four
hannesfritz
42
2.8k
Optimising Largest Contentful Paint
csswizardry
37
3.5k
Navigating Team Friction
lara
190
15k
For a Future-Friendly Web
brad_frost
180
10k
Producing Creativity
orderedlist
PRO
347
40k
Imperfection Machines: The Place of Print at Facebook
scottboms
269
13k
We Have a Design System, Now What?
morganepeng
53
7.8k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
55
3k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
37
2.6k
Transcript
Cookpad Inc. Firestore, Cloud StorageΛ༻͍ͨ ΞϓϦͰͷը૾ͷѻ͍ํ ࡾӜ
Cookpad Inc. All Rights Reserved. ࣗݾհ w໊લࡾӜ wܦྺ w17৽ଔೖࣾ wiOSྺ5
wTwitter: __miup wGithub: miuP
Cookpad Inc. All Rights Reserved. ࠓ͢͜ͱ w'JSFCBTFΛόοΫΤϯυʹஔ͘ΞϓϦͰͷը૾ͷѻ͍ํ wCloud StorageʹσʔλΛอଘ wFirestoreͱStorageͷ࿈ܞ
wϦαΠζ wνϡʔχϯά wσϞ
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 Ұ࿈ͷྲྀΕ
Cookpad Inc. All Rights Reserved. ࠓ͞ͳ͍͜ͱ wModel ͷઃܭ wKomerco Ͱͷ࣮ࡍͷίʔυͱҟͳΓ·͢
wϧʔϧ ͜ͷͰ͞ͳ͍͚ͩͰޙͰฉ͍ͯΒ͑Ε͓͠·͢ʂ
Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image
completion ը૾ͷอଘ
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
Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image
completion save model completion Firestore ͱ Storage ͷ࿈ܞ
Cookpad Inc. All Rights Reserved. Firestore ͱ Storage ͷ࿈ܞ wอଘ
wStorage ʹ Data Λอଘ wStorageReferencePath Λ Firestore ʹอଘ͢Δ
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
ը૾ΛϦαΠζ
Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wΦϦδφϧαΠζ͚ͩͩͱ͍উख͕ѱ͍ wCloudFunctions ͰϦαΠζΛ͢Δ wImageMagic
Λ༻ wKomerco Ͱ4αΠζʹϦαΠζ͍ͯ͠Δ
Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image
completion event trigger save model completion ը૾ͷϦαΠζ
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
Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wͳͥ Storage ͷ onCreate
ΛΘͳ͍ͷ͔ wStorage ͷ onCreate ͷஈ֊Ͱ·ͩ Firestore ʹσʔλແ͍ ϦαΠζͯͦ͠ͷใΛ Firestore ʹॻ͖ࠐΊͳ͍Մೳੑ w͠ Firestore ͷอଘ͕ࣦഊ͍ͯͨ͠Βσʔλͷෆ߹ Τϥʔ͕ൃੜ
Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image
completion event trigger save model completion download image ը૾ͷϦαΠζ
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
Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wϦαΠζใͷఆٛ w֤αΠζʹରͯ͠ɺը૾αΠζͱϑΝΠϧ໊ͷ prefix Λఆٛ͢Δ
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
Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ w࣮ࡍʹϦαΠζ͍ͯ͘͠ wࠓఆٛͨ͠3FTJ[F5ZQFΛͯ͠ϦαΠζ͢Δ wprocess-promises ͷ
spawn Ͱ ImageMagick Λ࣮ߦ
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
Cookpad Inc. All Rights Reserved. $MPVE4UPSBHF $MPVE'VODUJPOT $MPVE'JSFTUPSF save image
completion event trigger save model completion save resized image download image ը૾ͷϦαΠζ
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
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 ը૾ͷϦαΠζ
Cookpad Inc. All Rights Reserved. ը૾ͷϦαΠζ wImage Ϟσϧͷߋ৽Λߦ͏ wϦαΠζͨ͠ը૾ͷ Path
ΛϞσϧʹॻ͖ࠐΜͰ͓͘
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
ಡΈࠐΈ
Cookpad Inc. All Rights Reserved. ը૾ͷಡΈࠐΈ wඞཁͳͱ͜ΖͰඞཁͳαΠζΛϩʔυ͢Δ wΫϥΠΞϯτͷϞσϧʹαΠζΛ enum Ͱఆٛ
wαϜωΠϧͰখ͍ͭ͞ wৄࡉը໘ʹߦͬͨΒͰ͔͍ͭ
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
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
ͦͷଞͰ͍ͬͯΔ͜ͱ
Cookpad Inc. All Rights Reserved. ύϑΥʔϚϯε্ͷͨΊʹ͍ͬͯΔ͜ͱ wΩϟογϡ wImage ([ID: Image])
=> ࣗલͰΜͰ͍Δ wը૾࣮ମ([RefPath: UIImage]) => ImageStore (github.com/miuP/ImageStore) wը૾࣮ମ ([RefPath: UIImage]) ͚ͩͩͱଞͷϞσϧ͔Β ࢀরΛऔΔͱ͖ʹ Image ϞσϧͷऔಘͰҰॠϩʔυ͕Δ
// cellForItemAtIndexPath ͱ͔ cell ͷ configure ϝιου products[indexPath.item].image.get { [weak
self] image in self?.productImageView.load(image.getRef(of: .large) } 4XJGU ηϧ͕ϩʔυ͞ΕΔʹඇಉظͰಡΈࠐΉ͜ͱʹͳΔ
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
Cookpad Inc. All Rights Reserved. ύϑΥʔϚϯε্ͷͨΊʹ͍ͬͯΔ͜ͱ wΫϥΠΞϯτͰjpegѹॖ͢Δ wࣦഊͨ͠ͱ͖ʹ͋ͱ͔ΒϦαΠζͰ͖ΔΑ͏ʹ͓ͯ͘͠ wޭͨ͠Β্͛Δϑϥά wϧʔϧͪΌΜͱॻ͘
(update, listΘͳ͚Ε࠹͍Ͱ͠·͓͏)
σϞ
Cookpad Inc. All Rights Reserved. σϞΞϓϦ ( github.com/miuP/FirestoreStorageSample ) wը૾Λߘͯ͠ҰཡͱৄࡉΛݟΔ͚ͩ
wΩϟογϡ wϦαΠζ wΩϟογϡ͕͏·͘ಈ࡞͍ͯ͠ΔͷΛݟΔͨΊʹ Image Λݟʹߦ ͘ͷͰͳ͘ Image Λϥοϓͨ͠ Dummy ͱ͍͏ϞσϧΛ࡞ͬͨ ⚠༗ྉϓϥϯ͡Όͳ͍ͱಈ͖·ͤΜʂʂʂ
Cookpad Inc. All Rights Reserved. ·ͱΊ wFirestore ͱ Cloud Strage
ͷ࿈ܞ wجຊ RefPath Λը૾༻ͷ Model ʹ࣋ͨͤΔ wϦαΠζΫϥΠΞϯτͰͳ͘ CloudFunctions Ͱ wΑ͠ͳʹΩϟογϡ͢Δ