如何在SwiftUI中从URL显示图像

86

我正在尝试使用从我的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来显示图片?

11个回答

123

iOS 15更新:

您可以按以下方式使用asyncImage:
AsyncImage(url: URL(string: "https://your_image_url_address"))

更多有关苹果开发者文档的信息: 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) { [weak self] data, response, error in
            guard let data = data, self != nil else { return }
            DispatchQueue.main.async { [weak self]
                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()
        }
    }
}

此外,如果您需要更多参考资料,这个教程是一个不错的选择


1
这个解决方案可行,但当我在我的应用程序中开始滚动时,图片开始消失?我正在使用这个作为我的图像网址:ImageView(withURL: "http://localhost:8000/\(post.path)") - DDavis25
1
https://stackoverflow.com/questions/60710997/images-disappear-in-list-as-i-scroll-swiftui-swift - DDavis25
你能在我的另一个stackoverflow帖子中展示给我如何做吗? - DDavis25
5
Image.onReceive 方法没有被调用,图片未能显示。 - nitin.agam
有关 iOS 15 中 AsyncImage 的更详细指南,请查看:https://dev59.com/S1IH5IYBdhLWcg3wL6hJ#67919836。 - Pranav Kasetti
显示剩余8条评论

26

尝试使用此实现:

    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


异步图像(url: URL(string: item.imageUrl)!, 占位符: { Text("加载中...") }, 图像: { Image(uiImage: $0).resizable() }) .frame(width: 80, height: 57)只有文本“加载中…”可见,没有图像获取下载。 - nitin.agam
只有少数几张图片能够加载成功,其他的都只显示“正在加载…”的文本。 - Ethan Strider
@EthanStrider 这些图片是从https获取的吗? 也许你需要允许https执行: https://dev59.com/uqrka4cB1Zd3GeqPbUk1 - Benjamin RD
@EthanStrider,你能给我发一个样本URL吗? - Benjamin RD
@PaulB 我相信是的,它是一个内存缓存。 - smat88dd
显示剩余2条评论

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

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

iOS 15及以下版本非常棒。 - Manish
4
为了让占位符正常工作,我不得不删除第一个调用初始空UIImage()的imageLoader实例。我用.onReceive(imageLoader.$image.dropFirst())替换了它。 - NSRover

11

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

正如其他答案所提到的,AsyncImage 是在 SwiftUI 中实现此功能的推荐方式,但是这个新的View比这里展示的标准配置更加强大:

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

AsyncImage不需要使用URLSession繁琐的代码即可从URL下载图片。但是,苹果建议在等待最佳用户体验时使用占位符,而不是仅仅下载图像并在加载时显示空白。哦,我们还可以显示自定义视图来处理错误状态,并添加动画以进一步改善相位转换。 :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状态更改时图像将被重新下载(简化请求)。在这里,苹果公司建议使用await来防止阻塞主线程0(Swift 5.5+)。


9

iOS 15+带有加载器的示例:

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

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

创建自定义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)
            }
    }
}

创建一个服务层,使用发布者从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

iOS 15 中,SwiftUI 提供了一种专门用于下载和显示来自互联网的远程图像的 AsyncImage。在其最简单的形式中,您只需传递一个 URL,如下所示:

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

5

0

你也可以尝试我的方法。这是文档链接。

https://sdwebimage.github.io/documentation/sdwebimageswiftui/

这是我的代码片段

struct SettingsProfileImageSectionView: View {
            var body: some View {
                ZStack(alignment: .leading) {
                    Color(hex: "fcfcfc")
                    HStack(spacing: 20) {
                        Spacer()
                            .frame(width: 4)
                        CustomImageView(imageManager: ImageManager(url: URL(string: imageURL)))         }
                }
                .frame(height: 104)
            }
        }

从URL加载图像

struct CustomImageView: View {
        @State private var myImage: UIImage = UIImage(named: "Icon/User")!
        @ObservedObject var imageManager: ImageManager
        var body: some View {
            Image(uiImage: myImage)
                .resizable()
                .frame(width: 56.0, height: 56.0)
                .background(Color.gray)
                .scaledToFit()
                .clipShape(Circle())
                .onReceive(imageManager.$image) { image in
                    if imageManager.image != nil {
                        myImage = imageManager.image!
                    }
                }
                .onAppear {self.imageManager.load()}
                .onDisappear { self.imageManager.cancel() }
        }
    }

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

文件私有结构体CustomNetworkImageView: View { var urlString: String @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()
}

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接