如何在 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 的图像的主要内容,如果未能解决你的问题,请参考以下文章