Reputation: 1309
So I'm trying to create a content feed using data fetched from my Node.js server.
Here I fetch data from my API:
class Webservice {
func getAllPosts(completion: @escaping ([Post]) -> ()) {
guard let url = URL(string: "http://localhost:8000/albums")
else {
fatalError("URL is not correct!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try!
JSONDecoder().decode([Post].self, from: data!); DispatchQueue.main.async {
completion(posts)
}
}.resume()
}
}
Set the variables to the data fetched from the API:
final class PostListViewModel: ObservableObject {
init() {
fetchPosts()
}
@Published var posts = [Post]()
private func fetchPosts() {
Webservice().getAllPosts {
self.posts = $0
}
}
}
Post model:
struct Post: Codable, Hashable, Identifiable {
let id: String
let title: String
let path: String
let description: String
}
SwiftUI:
struct ContentView: View {
@ObservedObject var model = PostListViewModel()
var body: some View {
List(model.posts) { post in
HStack {
Text(post.title)
Image("http://localhost:8000/" + post.path)
Text(post.description)
}
}
}
}
The Text from post.title
and post.description
are displayed correctly but nothing displays from Image
. How can I use a URL
from my server to display with my image?
Upvotes: 98
Views: 129706
Reputation: 2305
You also can try my way. This is the documentation link
https://sdwebimage.github.io/documentation/sdwebimageswiftui/
Here is my code Snippet
struct SettingsProfileImageSectionView: View {
var body: some View {
ZStack(alignment: .leading) {
Color(hex: "fcfcfc")
HStack(spacing: 20) {
Spacer()
.frame(width: 4)
CustomImageView(imageManager: ImageManager(url: URL(string: imageURL))) }
}
.frame(height: 104)
}
}
Load image from URL
struct CustomImageView: View {
@State private var myImage: UIImage = UIImage(named: "Icon/User")!
@ObservedObject var imageManager: ImageManager
var body: some View {
Image(uiImage: myImage)
.resizable()
.frame(width: 56.0, height: 56.0)
.background(Color.gray)
.scaledToFit()
.clipShape(Circle())
.onReceive(imageManager.$image) { image in
if imageManager.image != nil {
myImage = imageManager.image!
}
}
.onAppear {self.imageManager.load()}
.onDisappear { self.imageManager.cancel() }
}
}
Upvotes: 0
Reputation: 67
Here's how to do it with using NSCache
in SwiftUI:
import UIKit
class CacheService {
static let shared = CacheService() // Singleton
private init() {}
var imageCache: NSCache<NSString, UIImage> = {
let cache = NSCache<NSString, UIImage>()
cache.countLimit = 100 // limits are imprecise
cache.totalCostLimit = 1024 * 1024 * 100 // limit in 100mb
return cache
}()
func addImage(image: UIImage, name: String) -> String {
imageCache.setObject(image, forKey: name as NSString)
return "Added to cach"
}
func removeImage(name: String) -> String {
imageCache.removeObject(forKey: name as NSString)
return "Removed from cach"
}
func getImage(name: String) -> UIImage? {
return imageCache.object(forKey: name as NSString)
}
}
Upvotes: 0
Reputation: 4719
AsyncImage(url: URL(string: "https://your_image_url_address"))
more info on Apple developers document: AsyncImage
first you need to fetch image from url :
class ImageLoader: ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(urlString:String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let data = data, self != nil else { return }
DispatchQueue.main.async { [weak self]
self?.data = data
}
}
task.resume()
}
}
you can put this as a part of your Webservice class function too.
then in your ContentView struct you can set @State image in this way :
struct ImageView: View {
@ObservedObject var imageLoader:ImageLoader
@State var image:UIImage = UIImage()
init(withURL url:String) {
imageLoader = ImageLoader(urlString:url)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.didChange) { data in
self.image = UIImage(data: data) ?? UIImage()
}
}
}
Also, this tutorial is a good reference if you need more
Upvotes: 142
Reputation: 3225
Example for iOS 15+ with loader :
AsyncImage(
url: URL(string: "https://XXX"),
content: { image in
image.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 200, maxHeight: 100)
},
placeholder: {
ProgressView()
}
)
Upvotes: 14
Reputation: 2147
You can use KingFisher and SDWebImage
KingFisher https://github.com/onevcat/Kingfisher
var body: some View {
KFImage(URL(string: "https://example.com/image.png")!)
}
SDWebImage https://github.com/SDWebImage/SDWebImageSwiftUI
WebImage(url: url)
Upvotes: 5
Reputation: 9915
AsyncImage
with animation transactions, placeholders, and network phase states in iOS 15+!As other answers have covered, AsyncImage
is the recommended way to achieve this in SwiftUI
but the new View
is much more capable than the standard config shown here:
AsyncImage(url: URL(string: "https://your_image_url_address"))
AsyncImage
downloads images from URLs without URLSession
s boilerplate. However, rather than simply downloading the image and displaying nothing while loading, Apple recommends using placeholders while waiting for the best UX. Oh, we can also display custom views for error states, and add animations to further improve phase transitions. :D
We can add animations using transaction:
and change the underlying Image
properties between states. Placeholders can have a different aspect mode, image, or have different modifiers. e.g. .resizable
.
Here's an example of that:
AsyncImage(
url: "https://dogecoin.com/assets/img/doge.png",
transaction: .init(animation: .easeInOut),
content: { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
}, placeholder: {
Color.gray
})
.frame(width: 500, height: 500)
.mask(RoundedRectangle(cornerRadius: 16)
To display different views when a request fails, succeeds, is unknown, or is in progress, we can use a phase handler. This updates the view dynamically, similar to a URLSessionDelegate
handler. Animations are applied automatically between states using SwiftUI syntax in a param.
AsyncImage(url: url, transaction: .init(animation: .spring())) { phase in
switch phase {
case .empty:
randomPlaceholderColor()
.opacity(0.2)
.transition(.opacity.combined(with: .scale))
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.transition(.opacity.combined(with: .scale))
case .failure(let error):
ErrorView(error)
@unknown default:
ErrorView()
}
}
.frame(width: 400, height: 266)
.mask(RoundedRectangle(cornerRadius: 16))
We shouldn't use AsyncImage
for all instances where we need to load an image from a URL. Instead, when images need to be downloaded on request, it's better to use the .refreshable
or .task
modifiers. Only use AsyncImage
sparingly because the image will be re-downloaded for every View
state change (streamline requests). Here, Apple suggests await
to prevent blocking the main thread 0 (Swift 5.5+).
Upvotes: 12
Reputation: 33
Button(action: {
self.onClickImage()
}, label: {
CustomNetworkImageView(urlString: self.checkLocalization())
})
Spacer()
}
if self.isVisionCountryPicker {
if #available(iOS 14.0, *) {
Picker(selection: $selection, label: EmptyView()) {
ForEach(0 ..< self.countries.count) {
Text(self.countries[$0].name?[self.language] ?? "N/A").tag($0)
}
}
.labelsHidden()
.onChange(of: selection) { tag in self.countryChange(tag) }
} else {
Picker(selection: $selection.onChange(countryChange), label: EmptyView()) {
ForEach(0 ..< self.countries.count) {
Text(self.countries[$0].name?[self.language] ?? "N/A").tag($0)
}
}
.labelsHidden()
}
}
fileprivate struct CustomNetworkImageView: View { var urlString: String @ObservedObject var imageLoader = ImageLoaderService() @State var image: UIImage = UIImage()
var body: some View {
Group {
if image.pngData() == nil {
if #available(iOS 14.0, *) {
ProgressView()
.frame(height: 120.0)
.onReceive(imageLoader.$image) { image in
self.image = image
self.image = image
if imageLoader.image == image {
imageLoader.loadImage(for: urlString)
}
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
} else {
EmptyView()
.frame(height: 120.0)
.onReceive(imageLoader.$image) { image in
self.image = image
self.image = image
if imageLoader.image == image {
imageLoader.loadImage(for: urlString)
}
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
} else {
Image(uiImage: image)
.resizable()
.cornerRadius(15)
.scaledToFit()
.frame(width: 150.0)
.onReceive(imageLoader.$image) { image in
self.image = image
self.image = image
if imageLoader.image == image {
imageLoader.loadImage(for: urlString)
}
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
}
}
}
fileprivate class ImageLoaderService: ObservableObject { @Published var image: UIImage = UIImage()
func loadImage(for urlString: String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}
Upvotes: 0
Reputation: 9965
Combining @naishta (iOS 13+) and @mrmins (placeholder & configure) answers, plus exposing Image
(instead UIImage
) to allow configuring it (resize, clip, etc)
Usage Example:
var body: some View {
RemoteImageView(
url: someUrl,
placeholder: {
Image("placeholder").frame(width: 40) // etc.
},
image: {
$0.scaledToFit().clipShape(Circle()) // etc.
}
)
}
struct RemoteImageView<Placeholder: View, ConfiguredImage: View>: View {
var url: URL
private let placeholder: () -> Placeholder
private let image: (Image) -> ConfiguredImage
@ObservedObject var imageLoader: ImageLoaderService
@State var imageData: UIImage?
init(
url: URL,
@ViewBuilder placeholder: @escaping () -> Placeholder,
@ViewBuilder image: @escaping (Image) -> ConfiguredImage
) {
self.url = url
self.placeholder = placeholder
self.image = image
self.imageLoader = ImageLoaderService(url: url)
}
@ViewBuilder private var imageContent: some View {
if let data = imageData {
image(Image(uiImage: data))
} else {
placeholder()
}
}
var body: some View {
imageContent
.onReceive(imageLoader.$image) { imageData in
self.imageData = imageData
}
}
}
class ImageLoaderService: ObservableObject {
@Published var image = UIImage()
convenience init(url: URL) {
self.init()
loadImage(for: url)
}
func loadImage(for url: URL) {
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data else { return }
DispatchQueue.main.async {
self.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}
Upvotes: 17
Reputation: 12343
For iOS 13, 14 (before AsyncImage
) and with the latest property wrappers ( without having to use PassthroughSubject<Data, Never>()
Main View
import Foundation
import SwiftUI
import Combine
struct TransactionCardRow: View {
var transaction: Transaction
var body: some View {
CustomImageView(urlString: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png") // This is where you extract urlString from Model ( transaction.imageUrl)
}
}
Creating CustomImageView
struct CustomImageView: View {
var urlString: String
@ObservedObject var imageLoader = ImageLoaderService()
@State var image: UIImage = UIImage()
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.$image) { image in
self.image = image
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
}
Creating a service layer to download the Images from url string, using a Publisher
class ImageLoaderService: ObservableObject {
@Published var image: UIImage = UIImage()
func loadImage(for urlString: String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}
Upvotes: 9
Reputation: 100503
New in iOS 15 , SwiftUI
has a dedicated AsyncImage
for downloading and displaying remote images from the internet. In its simplest form you can just pass a URL, like this:
AsyncImage(url: URL(string: "https://www.thiscoolsite.com/img/nice.png"))
Upvotes: 5
Reputation: 12034
Try with this implementation:
AsyncImage(url: URL(string: "http://mydomain/image.png")!,
placeholder: { Text("Loading ...") },
image: { Image(uiImage: $0).resizable() })
.frame(idealHeight: UIScreen.main.bounds.width / 2 * 3) // 2:3 aspect ratio
Looks simple, right? This function has the ability to save in cache the images, and also to make an async image request.
Now, copy this in a new file:
import Foundation
import SwiftUI
import UIKit
import Combine
struct AsyncImage<Placeholder: View>: View {
@StateObject private var loader: ImageLoader
private let placeholder: Placeholder
private let image: (UIImage) -> Image
init(
url: URL,
@ViewBuilder placeholder: () -> Placeholder,
@ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:)
) {
self.placeholder = placeholder()
self.image = image
_loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
}
var body: some View {
content
.onAppear(perform: loader.load)
}
private var content: some View {
Group {
if loader.image != nil {
image(loader.image!)
} else {
placeholder
}
}
}
}
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
struct TemporaryImageCache: ImageCache {
private let cache = NSCache<NSURL, UIImage>()
subscript(_ key: URL) -> UIImage? {
get { cache.object(forKey: key as NSURL) }
set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
}
}
class ImageLoader: ObservableObject {
@Published var image: UIImage?
private(set) var isLoading = false
private let url: URL
private var cache: ImageCache?
private var cancellable: AnyCancellable?
private static let imageProcessingQueue = DispatchQueue(label: "image-processing")
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancel()
}
func load() {
guard !isLoading else { return }
if let image = cache?[url] {
self.image = image
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() },
receiveOutput: { [weak self] in self?.cache($0) },
receiveCompletion: { [weak self] _ in self?.onFinish() },
receiveCancel: { [weak self] in self?.onFinish() })
.subscribe(on: Self.imageProcessingQueue)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
func cancel() {
cancellable?.cancel()
}
private func onStart() {
isLoading = true
}
private func onFinish() {
isLoading = false
}
private func cache(_ image: UIImage?) {
image.map { cache?[url] = $0 }
}
}
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}
Done!
Original source code: https://github.com/V8tr/AsyncImage
Upvotes: 27