Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 797 Vote(s) - 3.49 Average
  • 1
  • 2
  • 3
  • 4
  • 5
How can I load an UIImage into a SwiftUI Image asynchronously?

#1
In SwiftUI there are some `.init` methods to create an Image but none of them admits a block or any other way to load an UIImage from network/cache...

I am using [Kingfisher](

[To see links please register here]

) to load images from network and cache inside a list row, but the way to draw the image in the view is to re-render it again, which I would prefer to not do. Also, I am creating a fake image(only coloured) as placeholder while the image gets fetched.
Another way would be to wrap all inside a custom view and only re-render the wrapper. But I haven't tried yet.

This sample is working right now.
Any idea to improve the current one will be great


Some view using the loader
```swift
struct SampleView : View {

@ObjectBinding let imageLoader: ImageLoader

init(imageLoader: ImageLoader) {
self.imageLoader = imageLoader
}

var body: some View {
Image(uiImage: imageLoader.image(for: "https://url-for-image"))
.frame(width: 128, height: 128)
.aspectRatio(contentMode: ContentMode.fit)
}

}
```

```swift
import UIKit.UIImage
import SwiftUI
import Combine
import class Kingfisher.ImageDownloader
import struct Kingfisher.DownloadTask
import class Kingfisher.ImageCache
import class Kingfisher.KingfisherManager

class ImageLoader: BindableObject {

var didChange = PassthroughSubject<ImageLoader, Never>()
private let downloader: ImageDownloader
private let cache: ImageCache
private var image: UIImage? {
didSet {
dispatchqueue.async { [weak self] in
guard let self = self else { return }
self.didChange.send(self)
}
}
}
private var task: DownloadTask?
private let dispatchqueue: DispatchQueue

init(downloader: ImageDownloader = KingfisherManager.shared.downloader,
cache: ImageCache = KingfisherManager.shared.cache,
dispatchqueue: DispatchQueue = DispatchQueue.main) {
self.downloader = downloader
self.cache = cache
self.dispatchqueue = dispatchqueue
}

deinit {
task?.cancel()
}

func image(for url: URL?) -> UIImage {
guard let targetUrl = url else {
return UIImage.from(color: .gray)
}
guard let image = image else {
load(url: targetUrl)
return UIImage.from(color: .gray)
}
return image
}

private func load(url: URL) {
let key = url.absoluteString
if cache.isCached(forKey: key) {
cache.retrieveImage(forKey: key) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let value):
self.image = value.image
case .failure(let error):
print(error.localizedDescription)
}
}
} else {
downloader.downloadImage(with: url, options: nil, progressBlock: nil) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let value):
self.cache.storeToDisk(value.originalData, forKey: url.absoluteString)
self.image = value.image
case .failure(let error):
print(error.localizedDescription)
}
}
}
}

}
```
Reply

#2
## SwiftUI 3

Starting from iOS 15 we can now use [`AsyncImage`](

[To see links please register here]

):

```
AsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.frame(width: 50, height: 50)
```

---

## SwiftUI 2

Here is a native SwiftUI solution that supports **caching** and multiple loading states:
```
import Combine
import SwiftUI

struct NetworkImage: View {
@StateObject private var viewModel = ViewModel()

let url: URL?

var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}
```
```
extension NetworkImage {
class ViewModel: ObservableObject {
@Published var imageData: Data?
@Published var isLoading = false

private static let cache = NSCache<NSURL, NSData>()

private var cancellables = Set<AnyCancellable>()

func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
if let data = Self.cache.object(forKey: url as NSURL) {
imageData = data as Data
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
if let data = $0 {
Self.cache.setObject(data as NSData, forKey: url as NSURL)
self?.imageData = data
}
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}
```

(The above code doesn't use any third-party libraries, so it's easy to change the `NetworkImage` in any way.)

---

**Demo**

[![enter image description here][1]][1]

```
import Combine
import SwiftUI

struct ContentView: View {
@State private var showImage = false

var body: some View {
if showImage {
NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))
.frame(maxHeight: 150)
.padding()
} else {
Button("Load") {
showImage = true
}
}
}
}
```

(I used an exceptionally large Stack Overflow logo to show the loading state.)


[1]:
Reply

#3
With the release of iOS 15 and macOS 12 in 2021, SwiftUI provides native `AsyncImage` view that enables loading images asynchronously. Bear in mind that you'll still have to fall back to a custom implementation for earlier OS versions.

```swift
AsyncImage(url: URL(string: "https://example.com/tile.png"))
```

The API itself also provides various ways to customise the image or provide a placeholder, for example:

```
AsyncImage(url: URL(string: "https://example.com/tile.png")) { image in
image.resizable(resizingMode: .tile)
} placeholder: {
Color.green
}
```

More in the [Apple Developer Documentation.][1]


[1]:

[To see links please register here]

Reply

#4
import SwiftUI
struct UrlImageView: View {
@ObservedObject var urlImageModel: UrlImageModel

init(urlString: String?) {
urlImageModel = UrlImageModel(urlString: urlString)
}

var body: some View {
Image(uiImage: urlImageModel.image ?? UrlImageView.defaultImage!)
.resizable()
.scaledToFill()
}

static var defaultImage = UIImage(systemName: "photo")
}

class UrlImageModel: ObservableObject {
@Published var image: UIImage?
var urlString: String?

init(urlString: String?) {
self.urlString = urlString
loadImage()
}

func loadImage() {
loadImageFromUrl()
}

func loadImageFromUrl() {
guard let urlString = urlString else {
return
}

let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url, completionHandler:
getImageFromResponse(data:response:error:))
task.resume()
}


func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?)
{
guard error == nil else {
print("Error: \(error!)")
return
}
guard let data = data else {
print("No data found")
return
}

DispatchQueue.main.async {
guard let loadedImage = UIImage(data: data) else {
return
}
self.image = loadedImage
}
}
}


And using like this:

UrlImageView(urlString: "https://developer.apple.com/assets/elements/icons/swiftui/swiftui-96x96_2x.png").frame(width:100, height:100)



Reply

#5
A simpler and cleaner way to load an image in SwiftUI is to use the renowned Kingfisher library.



1. Add `Kingfisher` via Swift Package Manager

> Select File > Swift Packages > Add Package Dependency. Enter
>

[To see links please register here]


> in the "Choose Package
> Repository" dialog. In the next page, specify the version resolving
> rule as "Up to Next Major" with "5.8.0" as its earliest version.

>After
> Xcode checking out the source and resolving the version, you can
> choose the "KingfisherSwiftUI" library and add it to your app target.

2. `import KingfisherSwiftUI`
3. `KFImage(myUrl)`

Done! It's that easy
Reply

#6
I would just use the `onAppear` callback

import Foundation
import SwiftUI
import Combine
import UIKit
struct ImagePreviewModel {
var urlString : String
var width : CGFloat = 100.0
var height : CGFloat = 100.0
}

struct ImagePreview: View {
let viewModel: ImagePreviewModel
@State var initialImage = UIImage()
var body: some View {
Image(uiImage: initialImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.width, height: self.height)
.onAppear {
guard let url = URL(string: self.viewModel.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }

RunLoop.main.perform {
self.initialImage = image
}

}.resume()
}
}
var width: CGFloat { return max(viewModel.width, 100.0) }
var height: CGFloat { return max(viewModel.height, 100.0) }
}
Reply

#7
Pass your Model to ImageRow struct which contains url.

import SwiftUI
import Combine

struct ContentView : View {
var listData: Post
var body: some View {
List(model.post) { post in
ImageRow(model: post) // Get image
}
}
}

/********************************************************************/
// Download Image

struct ImageRow: View {
let model: Post
var body: some View {
VStack(alignment: .center) {
ImageViewContainer(imageUrl: model.avatar_url)
}
}
}

struct ImageViewContainer: View {
@ObjectBinding var remoteImageURL: RemoteImageURL

init(imageUrl: String) {
remoteImageURL = RemoteImageURL(imageURL: imageUrl)
}

var body: some View {
Image(uiImage: UIImage(data: remoteImageURL.data) ?? UIImage())
.resizable()
.clipShape(Circle())
.overlay(Circle().stroke(Color.black, lineWidth: 3.0))
.frame(width: 70.0, height: 70.0)
}
}

class RemoteImageURL: BindableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(imageURL: String) {
guard let url = URL(string: imageURL) else { return }

URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }

DispatchQueue.main.async { self.data = data }

}.resume()
}
}
/********************************************************************/





Reply

#8
Define the `imageLoader` as [`@ObjectBinding`][1]:

@ObjectBinding private var imageLoader: ImageLoader


----------

It would make more sense to init the view with the url for the image :

struct SampleView : View {

var imageUrl: URL

private var image: UIImage {
imageLoader.image(for: imageUrl)
}

@ObjectBinding private var imageLoader: ImageLoader

init(url: URL) {
self.imageUrl = url
self.imageLoader = ImageLoader()
}

var body: some View {
Image(uiImage: image)
.frame(width: 200, height: 300)
.aspectRatio(contentMode: ContentMode.fit)
}
}

For example :

//Create a SampleView with an initial photo
var s = SampleView(url: URL(string: "https://placebear.com/200/300")!)
//You could then update the photo by changing the imageUrl
s.imageUrl = URL(string: "https://placebear.com/200/280")!

[1]:

[To see links please register here]

Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through