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
25k
5
Share
Firestore, Cloud Storage を用いた アプリ内での画像の扱い方
Cookpad Tech Kitchen #16 コメルコテックバナシ
miup
July 19, 2018
More Decks by miup
See All by miup
Algolia with Firebase
miup
2
1.5k
Firestore のクエリと全文検索
miup
7
3.6k
Firebase Cloud Messaging 入門編
miup
0
5.2k
The way of truly serverless application
miup
1
4.5k
Other Decks in Programming
See All in Programming
PHPでバイナリをパースして理解するASN.1
muno92
PRO
0
460
Hive Metastoreを通して学ぶIceberg REST Catalog ― 仕様から実装まで
okumin
0
150
SkillsをS3 Filesに置く時のあれこれ
watany
3
1.6k
HTML-Aware ERB: The Path to Reactive Rendering @ RubyKaigi 2026, Hakodate, Japan
marcoroth
0
720
🦞OpenClaw works with AWS
licux
1
370
AgentCore Optimizationを始めよう!
licux
3
250
KMP × Kotlin 2.3 - How Android Got Slower While iOS Builds Improved by 47%
rio432
0
200
過去のレビュー知見をSkillsで資産化した話
pkshadeck
PRO
1
2k
AIベース静的検査器の偽陽性率を抑える工夫3選
orgachem
PRO
4
460
Agentic Elixir
whatyouhide
0
450
AWSはOSSをどのように 考えているのか?
akihisaikeda
0
120
Skillは並べた。動かなかった。契約で繋いだ。— 65個のSkillから、自走する開発サイクルへ
junholee
0
620
Featured
See All Featured
Docker and Python
trallard
47
3.8k
A Modern Web Designer's Workflow
chriscoyier
698
190k
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.3k
Stewardship and Sustainability of Urban and Community Forests
pwiseman
0
200
Fireside Chat
paigeccino
42
3.9k
Ecommerce SEO: The Keys for Success Now & Beyond - #SERPConf2024
aleyda
1
2k
The SEO identity crisis: Don't let AI make you average
varn
0
460
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
140
Designing for Timeless Needs
cassininazir
1
220
brightonSEO & MeasureFest 2025 - Christian Goodrich - Winning strategies for Black Friday CRO & PPC
cargoodrich
3
700
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
65
55k
Faster Mobile Websites
deanohume
310
31k
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Α͠ͳʹΩϟογϡ͢Δ