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
MCPでVibe Working。そして、結局はContext Eng(略)/ Working with Vibe on MCP And Context Eng
rkaga
5
2.2k
Introducing ReActionView: A new ActionView-compatible ERB Engine @ Rails World 2025, Amsterdam
marcoroth
0
640
複雑なドメインに挑む.pdf
yukisakai1225
5
1.1k
TDD 実践ミニトーク
contour_gara
1
290
Compose Multiplatform × AI で作る、次世代アプリ開発支援ツールの設計と実装
thagikura
0
140
@Environment(\.keyPath)那么好我不允许你们不知道! / atEnvironment keyPath is so good and you should know it!
lovee
0
110
AIでLINEスタンプを作ってみた
eycjur
1
230
Namespace and Its Future
tagomoris
6
700
AIを活用し、今後に備えるための技術知識 / Basic Knowledge to Utilize AI
kishida
21
5.6k
意外と簡単!?フロントエンドでパスキー認証を実現する WebAuthn
teamlab
PRO
2
720
MCPとデザインシステムに立脚したデザインと実装の融合
yukukotani
4
1.4k
AIコーディングAgentとの向き合い方
eycjur
0
270
Featured
See All Featured
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
229
22k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
53
2.9k
Balancing Empowerment & Direction
lara
3
620
RailsConf 2023
tenderlove
30
1.2k
Building Applications with DynamoDB
mza
96
6.6k
What's in a price? How to price your products and services
michaelherold
246
12k
Practical Orchestrator
shlominoach
190
11k
XXLCSS - How to scale CSS and keep your sanity
sugarenia
248
1.3M
What’s in a name? Adding method to the madness
productmarketing
PRO
23
3.7k
The Pragmatic Product Professional
lauravandoore
36
6.9k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Into the Great Unknown - MozCon
thekraken
40
2k
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Α͠ͳʹΩϟογϡ͢Δ