SwiftUI 3 - AsyncImage with Dyanmic Codable Session

SwiftUI3 introduced a new View allowing you to fetch and load images over the internet, here is an example on how to do that.

Sun, July 24 2022

jake hockey

Jake Landers

Developer and Creator

hey

Final Product

1.png

iOS15 gave us a new view called AsyncImage. This view has been around for a long time in flutter, called NetworkImage. This allows you to fetch an image from the internet in real time while also showing preview and error views if anything were to go wrong. I decided to use a public image api for this.

Video

Video Link

Database

I decided to wrap the calls up in a super helpful data fetching function i wrote that is capable of returning dynamic types so you do not need to write the same function tons of times. I will expand on this idea further in another video/article.

1enum methods { 2 static let get = "GET" 3} 4 5enum Database { 6 static let baseUrl = URL(string: "http://shibe.online/api")! 7} 8 9extension Database { 10 static func request<T: Codable>(_ path: String, method: String) async -> T? { 11 guard let url = URL(string: "\(baseUrl)\(path)") else { 12 print("failed to create url components") 13 return nil 14 } 15 16 do { 17 var request = URLRequest(url: url) 18 request.addValue("application/json", forHTTPHeaderField: "Content-Type") 19 request.httpMethod = method 20 // if doing a PUT or POST method, add: 21 // request.httpBody = (object of type Data) = a object that has been encoded with a JSONEncoder(). 22 let (response, _) = try await URLSession.shared.data(for: request) 23 let decoder = JSONDecoder() 24 let data = try decoder.decode(T.self, from: response) 25 return data 26 } catch { 27 print("FATAL -- issue serializing request: \(error)") 28 return nil 29 } 30 } 31} 32
NOTE:

Database.swift

Client

Using this function, fetching data from the internet is a breeze.

1class Client: ObservableObject { 2 @Published var shibes: [String]? 3 @Published var birds: [String]? 4 @Published var cats: [String]? 5 6 func fetchShibes() async { 7 shibes = await Database.request("/shibes?count=25&urls=true&httpsUrls=true", method: methods.get) 8 } 9 10 func fetchBirds() async { 11 birds = await Database.request("/birds?count=25&urls=true&httpsUrls=true", method: methods.get) 12 } 13 14 func fetchCats() async { 15 cats = await Database.request("/cats?count=25&urls=true&httpsUrls=true", method: methods.get) 16 } 17} 18
NOTE:

Client.swift

View

Lastly, we can use these objects to paint our async image. First, we need to figure out how async image works. Here is a simple example.

1AsyncImage(url: URL(string: image)) { phase in 2 if let image = phase.image { 3 image.resizable().aspectRatio(contentMode: .fit) 4 } else if phase.error != nil { 5 // error 6 Color.red 7 } else { 8 // placeholder 9 ImagePlaceHolder() 10 } 11} 12.frame(height: 200) 13.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) 14.padding(.horizontal) 15

We can use this in our dynamic view, like so.

1struct ImageList: View { 2 @Environment(\.colorScheme) var colorScheme 3 4 @Binding var images: [String]? 5 var title: String 6 7 var body: some View { 8 NavigationView { 9 Group { 10 if images != nil { 11 ScrollView { 12 LazyVStack(spacing: 15) { 13 ForEach(images!, id:\.self) { image in 14 AsyncImage(url: URL(string: image)) { phase in 15 if let image = phase.image { 16 image.resizable().aspectRatio(contentMode: .fit) 17 } else if phase.error != nil { 18 // error 19 Color.red 20 } else { 21 // placeholder 22 ImagePlaceHolder() 23 } 24 } 25 .frame(height: 200) 26 .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) 27 .padding(.horizontal) 28 } 29 } 30 } 31 .background(colorScheme == .light ? Color(red: 245/255, green: 245/255, blue: 250/255, opacity: 1) : Color.black) 32 } else { 33 ProgressView() 34 } 35 } 36 .navigationTitle(title) 37 } 38 } 39} 40

Throw in a nice image placeholder:

1struct ImagePlaceHolder: View { 2 @Environment(\.colorScheme) var colorScheme 3 4 @State private var isAnimating = false 5 6 var body: some View { 7 ZStack { 8 colorScheme == .light ? Color.black.opacity(0.3) : Color.white.opacity(0.3) 9 ProgressView() 10 } 11 .opacity(isAnimating ? 0.5 : 1) 12 .onAppear { 13 withAnimation(Animation.easeInOut(duration: 0.8).repeatForever()) { 14 isAnimating = true 15 } 16 } 17 .onDisappear { 18 isAnimating = false 19 } 20 } 21} 22

And finally a host to wrap it all up.

1struct ContentView: View { 2 @StateObject var client = Client() 3 4 var body: some View { 5 TabView { 6 ImageList(images: $client.shibes, title: "Shibes") 7 .tabItem { 8 Label("Shibes", systemImage: "bolt") 9 } 10 .task { 11 await client.fetchShibes() 12 } 13 ImageList(images: $client.birds, title: "Birds") 14 .tabItem { 15 Label("Birds", systemImage: "flame") 16 } 17 .task { 18 await client.fetchBirds() 19 } 20 ImageList(images: $client.cats, title: "Cats") 21 .tabItem { 22 Label("Cats", systemImage: "leaf") 23 } 24 .task { 25 await client.fetchCats() 26 } 27 } 28 } 29} 30

And thats it! The task modifier on a view lets you asynchronously call a function on view appear, and is almost identical to:

1.onAppear { 2 async { 3 await function() 4 } 5} 6

Source Code:

Github

Comments