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
Server Side Kotlin Meetup vol.16: 内部動作を理解して ハイパフォーマンスなサーバサイド Kotlin アプリケーションを書こう
ternbusty
3
180
詳しくない分野でのVibe Codingで困ったことと学び/vibe-coding-in-unfamiliar-area
shibayu36
3
4.9k
ALL CODE BASE ARE BELONG TO STUDY
uzulla
11
1.3k
株式会社 Sun terras カンパニーデック
sunterras
0
290
高度なUI/UXこそHotwireで作ろう Kaigi on Rails 2025
naofumi
4
4k
Web Components で実現する Hotwire とフロントエンドフレームワークの橋渡し / Bridging with Web Components
da1chi
3
2.1k
TFLintカスタムプラグインで始める Terraformコード品質管理
bells17
2
160
なぜあの開発者はDevRelに伴走し続けるのか / Why Does That Developer Keep Running Alongside DevRel?
nrslib
3
400
ソフトウェア設計の実践的な考え方
masuda220
PRO
4
570
CSC305 Lecture 05
javiergs
PRO
0
210
iOSエンジニア向けの英語学習アプリを作る!
yukawashouhei
0
190
iOSエンジニア向けの英語学習アプリを作る!
yukawashouhei
0
200
Featured
See All Featured
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
32
2.3k
Thoughts on Productivity
jonyablonski
70
4.9k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4k
How To Stay Up To Date on Web Technology
chriscoyier
791
250k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
231
53k
Fantastic passwords and where to find them - at NoRuKo
philnash
52
3.4k
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Into the Great Unknown - MozCon
thekraken
40
2.1k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
252
21k
BBQ
matthewcrist
89
9.8k
Code Reviewing Like a Champion
maltzj
526
40k
Balancing Empowerment & Direction
lara
4
690
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Α͠ͳʹΩϟογϡ͢Δ