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
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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.4k
Firestore のクエリと全文検索
miup
7
3.6k
Firebase Cloud Messaging 入門編
miup
0
5.1k
The way of truly serverless application
miup
1
4.5k
Other Decks in Programming
See All in Programming
「効かない!」依存性注入(DI)を活用したAPI Platformのエラーハンドリング奮闘記
mkmk884
0
290
Claude Code Skill入門
mayahoney
0
460
Claude Codeログ基盤の構築
giginet
PRO
7
3.8k
GC言語のWasm化とComponent Modelサポートの実践と課題 - Scalaの場合
tanishiking
0
140
AI時代の脳疲弊と向き合う ~言語学としてのPHP~
sakuraikotone
1
1.8k
年間50登壇、単著出版、雑誌寄稿、Podcast出演、YouTube、CM、カンファレンス主催……全部やってみたので面白さ等を比較してみよう / I’ve tried them all, so let’s compare how interesting they are.
nrslib
4
650
CS教育のDX AIによる育成の効率化
niftycorp
PRO
0
180
事業会社でのセキュリティ長期インターンについて
masachikaura
0
200
AWS×クラウドネイティブソフトウェア設計 / AWS x Cloud-Native Software Design
nrslib
16
3.5k
RailsのValidatesをSwift Macrosで再現してみた
hokuron
0
150
Kubernetesでセルフホストが簡単なNewSQLを求めて / Seeking a NewSQL Database That's Simple to Self-Host on Kubernetes
nnaka2992
0
190
Geminiをパートナーに神社DXシステムを個人開発した話(いなめぐDX 開発振り返り)
fujiba
0
130
Featured
See All Featured
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
400
コードの90%をAIが書く世界で何が待っているのか / What awaits us in a world where 90% of the code is written by AI
rkaga
61
43k
How to make the Groovebox
asonas
2
2.1k
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
140
It's Worth the Effort
3n
188
29k
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
1
160
The Power of CSS Pseudo Elements
geoffreycrofte
82
6.2k
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
250
Become a Pro
speakerdeck
PRO
31
5.9k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
1.8k
Fashionably flexible responsive web design (full day workshop)
malarkey
408
66k
Marketing to machines
jonoalderson
1
5.1k
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Α͠ͳʹΩϟογϡ͢Δ