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

时间:2020-03-13 21:47:27

标签: swift swiftui

因此,我正尝试使用从我的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与图像一起显示?

4 个答案:

答案 0 :(得分:10)

试试这个实现:

    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

答案 1 :(得分:9)

首先,您需要从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,这是一个很好的参考

答案 2 :(得分:3)

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

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

答案 3 :(得分:1)

iOS 15 更新

SwiftUI 现在有一个 AsyncImage 类。AsyncImage 使我们能够从 URL 下载图像,而无需与 URLSession 交互。但是,Apple 建议在等待时使用占位符,而不是简单地下载图像并在加载时显示空图像。

如果需要,我们可以使用 transaction: 添加动画并更改基础 Image 属性。例如不同的纵横比模式或使图像可调整大小。

这是一个例子:

AsyncImage(
  url: "https://dogecoin.com/assets/img/doge.png",
  transaction: .init(animation: .easeInOut)
) { image in
  image
    .resizable()
    .aspectRatio(contentMode: .fit)
}. placeholder: {
  Color.gray
}
  .frame(width: 500, height: 500)
  .mask(RoundedRectangle(cornerRadius: 16)

要在请求失败或正在进行时显示不同的视图,我们可以使用阶段。这会在加载期间和请求完成后更新视图。

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 修饰符。在这里,Apple 建议使用 await 来防止阻塞主线程 (Swift 5.5)。