【问题标题】:How to display Image from a url in SwiftUI如何在 SwiftUI 中显示来自 url 的图像
【发布时间】: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 与我的图像一起显示?

【问题讨论】:

    标签: swift swiftui


    【解决方案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)")
    • 图片。 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 执行:stackoverflow.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://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.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

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

                 var body: some View {
                     KFImage(URL(string: "https://example.com/image.png")!)
                 }
                
              2. SDWebImagehttps://github.com/SDWebImage/SDWebImageSwiftUI

                 WebImage(url: url)
                

              【讨论】:

                【解决方案9】:

                更短的简单解决方案:

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

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2021-06-26
                  • 1970-01-01
                  • 2017-01-07
                  • 1970-01-01
                  • 2022-11-10
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多