{"primaryContentSections":[{"kind":"content","content":[{"anchor":"Overview","level":2,"type":"heading","text":"Overview"},{"type":"paragraph","inlineContent":[{"type":"text","text":"When you’re building an app that displays images the first thing you need to do is build an image cache. An image cache provides numerous benefits. It lets your app work offline, makes sure you never have to redownload images you’ve already saved, and it makes your app faster by skipping unnecessary network requests."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"In this tutorial we’ll build an image cache, connect it to a SwiftUI View, and demonstrate a good pattern for querying cached data. While the cache we’ll build is for images, the same approach works for any data, whether you want to save videos, HTML, or even custom files. You can even use "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/ObjectStorage"},{"type":"text","text":" to cache your app’s models, as discussed in "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/Using-ObjectStorage"},{"type":"text","text":"."}]},{"anchor":"ImageCache","level":2,"type":"heading","text":"ImageCache"},{"type":"paragraph","inlineContent":[{"type":"text","text":"Below is the "},{"type":"codeVoice","code":"ImageCache"},{"type":"text","text":", and like any cache it has an operation to save data and an operation to retrieve data."}]},{"type":"codeListing","syntax":"swift","code":["import Bodega","","final class ImageCache: ObservableObject {",""," \/\/ 1"," private let imageStore = SQLiteStorageEngine.default(appendingPath: \"Images\")",""," init() { }",""," \/\/ 2"," func cache(image: UIImage, forKey key: CacheKey) async throws {"," guard let data = image.pngData() else { return }"," try await self.imageStore.write(data, key: key)"," }",""," \/\/ 3"," func image(forKey key: CacheKey) async -> UIImage? {"," guard let imageData = await self.imageStore.read(key: key) else { return nil }"," return UIImage(data: imageData)"," }","","}"]},{"type":"orderedList","items":[{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"We create an instance of "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/SQLiteStorageEngine"},{"type":"text","text":" ("},{"type":"codeVoice","code":"imagesStore"},{"type":"text","text":") to hold the images we will be caching. The document "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/Using-StorageEngines"},{"type":"text","text":" discusses the differences between "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/SQLiteStorageEngine"},{"type":"text","text":" and "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/DiskStorageEngine"},{"type":"text","text":" in depth, you can use either for this task but we’ll choose to use "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/SQLiteStorageEngine"},{"type":"text","text":"."}]}]},{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"Our "},{"type":"codeVoice","code":"cache(image: UIImage, forKey: CacheKey)"},{"type":"text","text":" function does one thing and does it well. If the image passed into the function can be converted to "},{"type":"codeVoice","code":"Data"},{"type":"text","text":", we will write that "},{"type":"codeVoice","code":"Data"},{"type":"text","text":" into the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine"},{"type":"text","text":". If it can’t write the "},{"type":"codeVoice","code":"Data"},{"type":"text","text":" to the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine"},{"type":"text","text":" we’ll return early instead. If there are any errors thrown when writing the data they will be provided to the caller of the "},{"type":"codeVoice","code":"cache"},{"type":"text","text":" function."}]}]},{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"Our "},{"type":"codeVoice","code":"image(forKey: CacheKey)"},{"type":"text","text":" function is similarly focused, tasked with retrieving an image from the cache if that image exists in our cache. If you haven’t yet retrieved an image from the server then it won’t be in the cache. This case is very common so it doesn’t make sense to throw errors, instead a "},{"type":"codeVoice","code":"nil"},{"type":"text","text":" value signals to us that we have a reason to attempt retrieving an image from our API."}]}]}]},{"anchor":"ImageFetchingAPI","level":2,"type":"heading","text":"ImageFetchingAPI"},{"type":"paragraph","inlineContent":[{"type":"text","text":"Now that we have a cache for storing images, we’ll need to fetch images to store. We won’t build a real API for the purposes of this tutorial, but this approach should work for fetching any image from the internet."}]},{"type":"codeListing","syntax":"swift","code":["struct ImageFetchingAPI {",""," func download(url: URL) async -> UIImage {"," \/\/ Make a network call download an image"," return UIImage()"," }","","}"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"For our "},{"type":"codeVoice","code":"ImageFetchingAPI"},{"type":"text","text":" our "},{"type":"codeVoice","code":"download(url: URL)"},{"type":"text","text":" function will use the "},{"type":"codeVoice","code":"url"},{"type":"text","text":" parameter provided to download an image."}]},{"anchor":"ProfileHeaderView","level":2,"type":"heading","text":"ProfileHeaderView"},{"type":"paragraph","inlineContent":[{"type":"text","text":"Having the ability to save, load, and download images is great, but for the user to enjoy the image we’ll need to put the image into a "},{"type":"codeVoice","code":"View"},{"type":"text","text":". Let’s imagine we’re a navigation bar that shows a user of our Jolene app their avatar if an avatar exists."}]},{"type":"paragraph","inlineContent":[{"type":"image","identifier":"ProfileHeaderview.png"}]},{"type":"codeListing","syntax":"swift","code":["import SwiftUI","","struct ProfileHeaderView: View {",""," \/\/ 1 "," @StateObject private var imageCache = ImageCache()",""," \/\/ 2"," @State private var avatarImage: UIImage?",""," private static let avatarCacheKey = CacheKey(\"username-avatar\")",""," var body: some View {"," HStack {"," \/\/ 3"," if let headerImage = self.avatarImage {"," Image(uiImage: headerImage)"," .frame(width: 32.0, height: 32.0)"," } else {"," Rectangle()"," .background(Color.blue)"," .cornerRadius(8.0)"," .frame(width: 32.0, height: 32.0)"," }",""," Spacer()",""," Text(\"Jolene 🌻\")",""," Spacer()"," }"," .frame(alignment: .center)"," }.task({"," \/\/ 4"," if let cachedImage = await self.imageCache.image(forKey: Self.avatarCacheKey) {"," self.avatarImage = cachedImage"," } else { "," let imageAPI = ImageFetchingAPI()"," let avatarImage = await imageAPI.fetchAvatar()"," self.avatarImage = avatarImage"," try? await self.imageCache.cache(image: avatarImage, forKey: Self.avatarCacheKey)"," }"," })"," }","}"]},{"type":"orderedList","items":[{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"Our "},{"type":"codeVoice","code":"ProfileHeaderview"},{"type":"text","text":" needs to have an instance of "},{"type":"codeVoice","code":"ImageCache"},{"type":"text","text":", that way we can retrieve the image we want to display from our cache. Some developers may prefer to put this property into a "},{"type":"codeVoice","code":"ViewModel"},{"type":"text","text":", and that’s a configuration Bodega supports. Bodega isn’t prescriptive, it only focuses on storing and loading data, you can choose the rest and figure out what approach is best for you."}]}]},{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"We’ll create an "},{"type":"codeVoice","code":"avatarImage"},{"type":"text","text":" property to store the avatar image we will retrieve. It will be "},{"type":"codeVoice","code":"nil"},{"type":"text","text":" by default, but will be updated when we fetch an image from either our cache or from the API. Marking the property with "},{"type":"codeVoice","code":"@State"},{"type":"text","text":" signals to our "},{"type":"codeVoice","code":"ProfileHeaderView"},{"type":"text","text":" to automatically refresh when an image is retrieved, no matter the source of the image."}]}]},{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"Here we have an if condition depending on whether "},{"type":"codeVoice","code":"avatarImage"},{"type":"text","text":" exists or not. If "},{"type":"codeVoice","code":"avatarImage"},{"type":"text","text":" is "},{"type":"codeVoice","code":"nil"},{"type":"text","text":" we will render a lovely blue rectangle that serves as a placeholder for when we retrieve an image. If "},{"type":"codeVoice","code":"avatarImage"},{"type":"text","text":" is not "},{"type":"codeVoice","code":"nil"},{"type":"text","text":", we will display the user’s avatar as expected. The beauty of the cache is that if we’ve already downloaded the image before we won’t have to wait for a network request to our API, instead the user will immediately see the avatar image as expected."}]}]},{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"There are two distinct pathways in our "},{"type":"codeVoice","code":".task"},{"type":"text","text":", so let’s go over the "},{"type":"codeVoice","code":"if"},{"type":"text","text":" and the "},{"type":"codeVoice","code":"else"},{"type":"text","text":" blocks separately."}]}]}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"The "},{"type":"codeVoice","code":".task"},{"type":"text","text":" will run when the view appears, immediately checking to see if the image already exists in the cache. If it does we will set "},{"type":"codeVoice","code":"avatarImage"},{"type":"text","text":" to the image we find in the cache so the user immediately sees their avatar in the header."}]},{"type":"codeListing","syntax":"swift","code":["if let cachedImage = await self.imageCache.image(forKey: Self.avatarCacheKey) {"," self.avatarImage = cachedImage","}"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"If the image isn’t yet cached we will end up in the "},{"type":"codeVoice","code":"else"},{"type":"text","text":" block. In that case we will"}]},{"type":"orderedList","items":[{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"Download the user’s avatar from our API,"}]}]},{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"Set the result to "},{"type":"codeVoice","code":"avatarImage"},{"type":"text","text":","}]}]},{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"Finish up the process by calling "},{"type":"codeVoice","code":"imageCache.cache(image: avatarImage, forKey: Self.avatarCacheKey)"},{"type":"text","text":" to ensure that the next time we need this avatar we have it cached."}]}]}]},{"type":"codeListing","syntax":"swift","code":["let imageAPI = ImageFetchingAPI()","let avatarImage = await imageAPI.download(url: URL(string: \"https:\/\/image.redpanda.club\/random\")!)","self.avatarImage = avatarImage","try? await self.imageCache.cache(image: avatarImage, forKey: Self.avatarCacheKey)"]},{"anchor":"Further-Exploration","level":2,"type":"heading","text":"Further Exploration"},{"type":"paragraph","inlineContent":[{"type":"text","text":"Building a performant and reliable cache can be a difficult task, but with Bodega’s help we were able to build one in only a few lines of code. There are many complex abstractions you can build much more simply using Bodega."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"As we saw above Bodega is fully usable and useful on its own, but it’s also the foundation of "},{"type":"reference","isActive":true,"identifier":"https:\/\/github.com\/mergesort\/Boutique"},{"type":"text","text":". Boutique helps you build a complete SwiftUI, UIKit, or AppKit app that works fully offline with the help of a similar caching approach as built for our image cache. But Boutique goes above and beyond that, providing realtime updates to your views so they’re always showing the most up to date data based on your cached data. If you’d like to build an app with all of these capabilities in only a few lines of code, it’s easy to get started with "},{"type":"reference","isActive":true,"identifier":"https:\/\/build.ms\/boutique\/docs"},{"type":"text","text":"."}]}]}],"schemaVersion":{"major":0,"minor":2,"patch":0},"sections":[],"variants":[{"paths":["\/documentation\/bodega\/building-an-image-cache"],"traits":[{"interfaceLanguage":"swift"}]}],"identifier":{"url":"doc:\/\/Bodega\/documentation\/Bodega\/Building-An-Image-Cache","interfaceLanguage":"swift"},"abstract":[{"type":"text","text":"Bodega takes data management off your plate, making once complex problems like building an image cache much simpler."}],"kind":"article","metadata":{"roleHeading":"Article","title":"Building An Image Cache","role":"article","modules":[{"name":"Bodega"}]},"hierarchy":{"paths":[["doc:\/\/Bodega\/documentation\/Bodega"]]},"references":{"doc://Bodega/documentation/Bodega/DiskStorageEngine":{"role":"symbol","title":"DiskStorageEngine","fragments":[{"kind":"keyword","text":"class"},{"kind":"text","text":" "},{"kind":"identifier","text":"DiskStorageEngine"}],"abstract":[{"type":"text","text":"A "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine"},{"type":"text","text":" based on saving items to the file system."}],"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/DiskStorageEngine","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"DiskStorageEngine"}],"url":"\/documentation\/bodega\/diskstorageengine"},"https://build.ms/boutique/docs":{"title":"Boutique’s documentation","titleInlineContent":[{"type":"text","text":"Boutique’s documentation"}],"type":"link","identifier":"https:\/\/build.ms\/boutique\/docs","url":"https:\/\/build.ms\/boutique\/docs"},"https://github.com/mergesort/Boutique":{"title":"Boutique","titleInlineContent":[{"type":"text","text":"Boutique"}],"type":"link","identifier":"https:\/\/github.com\/mergesort\/Boutique","url":"https:\/\/github.com\/mergesort\/Boutique"},"doc://Bodega/documentation/Bodega/StorageEngine":{"role":"symbol","title":"StorageEngine","fragments":[{"kind":"keyword","text":"protocol"},{"kind":"text","text":" "},{"kind":"identifier","text":"StorageEngine"}],"abstract":[{"type":"text","text":"A "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine"},{"type":"text","text":" represents a data storage mechanism for saving and persisting data."}],"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"StorageEngine"}],"url":"\/documentation\/bodega\/storageengine"},"ProfileHeaderview.png":{"alt":"Profile Header View","type":"image","identifier":"ProfileHeaderview.png","variants":[{"url":"\/images\/ProfileHeaderview.png","traits":["1x","light"]}]},"doc://Bodega/documentation/Bodega/Using-StorageEngines":{"role":"article","title":"Using StorageEngines","abstract":[{"type":"text","text":"The "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine"},{"type":"text","text":" is at the heart of what makes Bodega, Bodega."}],"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/Using-StorageEngines","kind":"article","type":"topic","url":"\/documentation\/bodega\/using-storageengines"},"doc://Bodega/documentation/Bodega/SQLiteStorageEngine":{"role":"symbol","title":"SQLiteStorageEngine","fragments":[{"kind":"keyword","text":"class"},{"kind":"text","text":" "},{"kind":"identifier","text":"SQLiteStorageEngine"}],"abstract":[{"type":"text","text":"A "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine"},{"type":"text","text":" based on an SQLite database."}],"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/SQLiteStorageEngine","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"SQLiteStorageEngine"}],"url":"\/documentation\/bodega\/sqlitestorageengine"},"doc://Bodega/documentation/Bodega/Using-ObjectStorage":{"role":"article","title":"Using ObjectStorage","abstract":[{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/ObjectStorage"},{"type":"text","text":" serves as unified layer over "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine"},{"type":"text","text":", allowing you to work with type-safe Swift models rather than "},{"type":"codeVoice","code":"Data"},{"type":"text","text":"."}],"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/Using-ObjectStorage","kind":"article","type":"topic","url":"\/documentation\/bodega\/using-objectstorage"},"doc://Bodega/documentation/Bodega":{"role":"collection","title":"Bodega","abstract":[{"type":"text","text":"A simple store all your basic needs, and also so much more. 🐱"}],"identifier":"doc:\/\/Bodega\/documentation\/Bodega","kind":"symbol","type":"topic","url":"\/documentation\/bodega"},"doc://Bodega/documentation/Bodega/ObjectStorage":{"role":"symbol","title":"ObjectStorage","fragments":[{"kind":"keyword","text":"class"},{"kind":"text","text":" "},{"kind":"identifier","text":"ObjectStorage"}],"abstract":[{"type":"text","text":"A unified layer over a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/StorageEngine"},{"type":"text","text":" primitives, allowing you to read, write, and save Swift objects."}],"identifier":"doc:\/\/Bodega\/documentation\/Bodega\/ObjectStorage","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"ObjectStorage"}],"url":"\/documentation\/bodega\/objectstorage"}}}