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

DDa*_*s25 31 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()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

final class PostListViewModel: ObservableObject {

    init() {
        fetchPosts()
    }

    @Published var posts = [Post]()

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


}
Run Code Online (Sandbox Code Playgroud)
struct Post: Codable, Hashable, Identifiable {

    let id: String
    let title: String
    let path: String
    let description: String
}
Run Code Online (Sandbox Code Playgroud)

用户界面

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)

                }

            }
        }

}
Run Code Online (Sandbox Code Playgroud)

来自post.title和的文本post.description显示正确,但没有显示来自Image()。如何使用来自我的服务器的 URL 与我的图像一起显示?

Mac*_*c3n 45

iOS 15 更新:

您可以通过以下方式使用 asyncImage:
AsyncImage(url: URL(string: "https://your_image_url_address"))
Run Code Online (Sandbox Code Playgroud)

有关 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()
    }
}
Run Code Online (Sandbox Code Playgroud)

你也可以把它作为你的 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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

此外,如果您需要更多,本教程是一个很好的参考

  • 图像。onReceive 没有被调用。没有图像显示。 (2认同)

Avi*_*oss 17

结合@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.
    }
  )

}
Run Code Online (Sandbox Code Playgroud)
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()
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 为了让占位符正常工作,我必须删除使用初始空白 UIImage() 调用的 imageLoader 的第一个实例。我用 `.onReceive(imageLoader.$image.dropFirst())` 替换它 (4认同)

MrM*_*ins 12

试试这个实现:

    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
Run Code Online (Sandbox Code Playgroud)

看起来很简单吧?此功能可以将图像保存在缓存中,也可以发出异步图像请求。

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

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 }
    }
}
Run Code Online (Sandbox Code Playgroud)

完毕!

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


Pra*_*tti 11

AsyncImageiOS 15+ 中具有动画事务、占位符和网络阶段状态!

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

AsyncImage(url: URL(string: "https://your_image_url_address"))
Run Code Online (Sandbox Code Playgroud)

AsyncImage从 URL 下载图像,无需URLSession样板。然而,苹果建议在等待最佳用户体验时使用占位符,而不是简单地下载图像并在加载时不显示任何内容。哦,我们还可以显示错误状态的自定义视图,并添加动画以进一步改进阶段转换。: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)
Run Code Online (Sandbox Code Playgroud)

处理网络结果状态

要在请求失败、成功、未知或正在进行时显示不同的视图,我们可以使用阶段处理程序。这会动态更新视图,类似于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))
Run Code Online (Sandbox Code Playgroud)

笔记

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


Med*_*dhi 11

使用 loader 的 iOS 15+ 示例:

AsyncImage(
    url: URL(string: "https://XXX"),
    content: { image in
        image.resizable()
            .aspectRatio(contentMode: .fit)
            .frame(maxWidth: 200, maxHeight: 100)
    },
    placeholder: {
        ProgressView()
    }
)
Run Code Online (Sandbox Code Playgroud)


Nai*_*hta 8

对于 iOS 13、14(之前AsyncImage)并使用最新的属性包装器(无需使用PassthroughSubject<Data, Never>()

主视图

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

创建自定义ImageView

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)
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用发布者创建服务层以从 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()
    }
    
}
Run Code Online (Sandbox Code Playgroud)


Sh_*_*han 5

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

AsyncImage(url: URL(string: "https://www.thiscoolsite.com/img/nice.png"))
Run Code Online (Sandbox Code Playgroud)


Li *_*Jin 5

您可以使用 KingFisher 和 SDWebImage

  1. KingFisher https://github.com/onevcat/Kingfisher

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

     WebImage(url: url)
    
    Run Code Online (Sandbox Code Playgroud)