如何在 SwiftUI 中显示来自 url 的图像

Posted

技术标签:

【中文标题】如何在 SwiftUI 中显示来自 url 的图像【英文标题】:How to display Image from a url in SwiftUI 【发布时间】:2020-06-25 21:24:40 【问题描述】:

所以我正在尝试使用从我的 Node JS 服务器获取的数据创建内容提要。

我在这里从我的 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()
    

将变量设置为从 API 获取的数据

final class PostListViewModel: ObservableObject 

    init() 
        fetchPosts()
    

    @Published var posts = [Post]()

    private func fetchPosts() 
        Webservice().getAllPosts 
            self.posts = $0
        
    



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)

                

            
        


post.titlepost.description 的文本显示正确,但 Image() 没有显示任何内容。如何使用服务器中的 URL 与我的图像一起显示?

【问题讨论】:

【参考方案1】:

ios 15 更新:

您可以通过这种方式使用 asyncImage:
AsyncImage(url: URL(string: "https://your_image_url_address"))

有关 Apple 开发人员文档的更多信息: AsyncImage

使用 ObservableObject(iOS 15 之前)

首先你需要从 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)  data, response, error in
            guard let data = data else  return 
            DispatchQueue.main.async 
                self.data = data
            
        
        task.resume()
    

你也可以把它作为你的 Webservice 类函数的一部分。

然后在您的 ContentView 结构中,您可以通过这种方式设置 @State 图像:

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()
        
    

此外,如果您需要更多信息,这个tutorial 是一个很好的参考

【讨论】:

如果我在使用class WebService 获取所有数据时已经获取了路径,是否需要从 url 获取图像? 是的,在您的数据中,您只需获取图像 URL,而不是图像本身,为了加载图像,您应该从数据中获取数据并将其转换为 UIImage 这个解决方案有效,但是当我开始在我的应用中滚动时,图像开始消失?我用这个作为我的图片网址:ImageView(withURL: "http://localhost:8000/\(post.path)") ***.com/questions/60710997/… 图片。 onReceive 没有被调用。没有图片显示。【参考方案2】:

试试这个实现:

    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

看起来很简单,对吧? 该函数可以将图片保存在缓存中,也可以进行异步图片请求。

现在,将其复制到一个新文件中:

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 
    

完成!

原始源代码:https://github.com/V8tr/AsyncImage

【讨论】:

AsyncImage(url: URL(string: item.imageUrl)!, placeholder: Text("Loading ...") , image: Image(uiImage: $0).resizable() ) .frame(width: 80, height: 57) Only Text Loading .... visible....没有图像下载。 我只加载了几张图片。其余的只是返回“正在加载...”文本。 @EthanStrider 图片来自 https 吗?也许你需要允许 https 执行:***.com/questions/49611336/… @MrMins 我使用的是https URL,但将AllowsArbitraryLoads 键设置为YES(根据链接信息)没有帮助。 @EthanStrider 能给我发一个示例网址吗?【参考方案3】:

iOS 15 中的新增功能,SwiftUI 有一个专用的AsyncImage,用于从互联网下载和显示远程图像。在最简单的形式中,您只需传递一个 URL,如下所示:

AsyncImage(url: URL(string: "https://www.thiscoolsite.com/img/nice.png"))

【讨论】:

【参考方案4】:

适用于 iOS 13、14AsyncImage 之前)和最新的属性包装器(无需使用 PassthroughSubject&lt;Data, Never&gt;()

主视图

import Foundation
import SwiftUI
import Combine

struct TransactionCardRow: View 
    var transaction: Transaction

    var body: some View 
        CustomImageView(urlString: "https://***.design/assets/img/logos/so/logo-***.png") // This is where you extract urlString from Model ( transaction.imageUrl)
    

创建 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)
            
    

创建服务层以使用 Publisher 从 url 字符串下载图像

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()
    
    

【讨论】:

【参考方案5】:

结合@naishta(iOS 13+)和@mrmins(占位符和配置)答案,并公开Image(而不是UIImage)以允许对其进行配置(调整大小、剪辑等)

用法示例:

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()
    

【讨论】:

【参考方案6】:

AsyncImage 在 iOS 15+ 中带有动画事务、占位符和网络阶段状态!

正如其他答案所涵盖的那样,AsyncImage 是在SwiftUI 中实现此目的的推荐方法,但新的View 比此处显示的标准配置功能强大得多:

AsyncImage(url: URL(string: "https://your_image_url_address"))

AsyncImage 从没有 URLSessions 样板的 URL 下载图像。但是,Apple 建议在等待最佳 UX 时使用占位符,而不是简单地下载图像并在加载时不显示任何内容。哦,我们还可以显示错误状态的自定义视图,并添加动画以进一步改进阶段转换。 :D

动画

我们可以使用transaction: 添加动画并在状态之间更改底层Image 属性。占位符可以具有不同的方面模式、图像或具有不同的修饰符。例如.resizable.

这是一个例子:

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)

处理网络结果状态

要在请求失败、成功、未知或正在进行时显示不同的视图,我们可以使用阶段处理程序。这会动态更新视图,类似于URLSessionDelegate 处理程序。在参数中使用 SwiftUI 语法在状态之间自动应用动画。

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))

注意

我们不应该在所有需要从 URL 加载图像的情况下使用 AsyncImage。相反,当需要根据请求下载图像时,最好使用.refreshable.task 修饰符。仅谨慎使用AsyncImage,因为每次View 状态更改(简化请求)都会重新下载图像。在这里,Apple 建议 await 防止阻塞主线程 0 (Swift 5.5+)。

【讨论】:

【参考方案7】:
            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 结构 CustomNetworkImageView: 查看 var urlString: 字符串 @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 类 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()

【讨论】:

【参考方案8】:

您可以使用 KingFisher 和 SDWebImage

    翠鸟 https://github.com/onevcat/Kingfisher

     var body: some View 
         KFImage(URL(string: "https://example.com/image.png")!)
     
    

    SDWebImagehttps://github.com/SDWebImage/SDWebImageSwiftUI

     WebImage(url: url)
    

【讨论】:

【参考方案9】:

更短的简单解决方案:

extension URL 
    var image: UIImage? 
        try? UIImage(data: Data(contentsOf: self))
    


这是同步的,所以如果你在主线程上这样做会导致加载延迟。

【讨论】:

以上是关于如何在 SwiftUI 中显示来自 url 的图像的主要内容,如果未能解决你的问题,请参考以下文章

如何在 iOS 中显示来自 JSON URL 的图像?

如何在 React 中显示来自 JSON URL 数组的图像

如何在网站中显示来自 Firestore 下载 URL 的图像

如何在 Android 上显示来自 URL 的图像

如何在互联网上显示来自 json url 的图像

Flutter:如何优先显示来自 Asset 的图像,如果找不到,则显示来自 URL 的图像?