如何在 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.title
和 post.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、14(AsyncImage
之前)和最新的属性包装器(无需使用 PassthroughSubject<Data, Never>()
主视图
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
从没有 URLSession
s 样板的 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 的图像的主要内容,如果未能解决你的问题,请参考以下文章
如何在 React 中显示来自 JSON URL 数组的图像