SwiftUI看起来很酷,但是有些事情对我来说似乎很难。即使这样,我还是想了解如何最好地以SwiftUI的方式进行操作,而不是包装swiftui之前的控制器并以旧的方式进行操作。因此,让我从一个简单的问题开始-显示具有URL的网络图像。有解决方案,但不是很容易找到,也不是那么容易理解。
我有一个解决方案,希望得到一些反馈。以下是我要执行的操作的示例(图像来自“打开图像”)。
struct ContentView: View {
@State var imagePath: String = "https://farm2.staticflickr.com/440/19711210125_6c12414d8f_o.jpg"
var body: some View {
WebImage(imagePath: $imagePath).scaledToFit()
}
}
我的解决方案是在主体顶部放一些代码以开始下载图像。图像路径具有@Binding属性包装器-如果更改,我想更新视图。还有一个带有@State属性包装器的myimage变量-设置它时,我也想更新我的视图。如果图像加载一切正常,将设置myimage并显示图像。最初的问题是,更改主体内的状态将导致视图无效,并触发另一个无限下载。解决方案似乎很简单(下面的代码)。只需检查imagePath,看看自上次加载某些东西以来它是否已更改。请注意,在下载中,我会立即设置prev,这将触发body的另一次执行。有条件的导致状态更改被忽略。
我读到某处@State检查是否相等,如果值不变则将忽略集合。这种对UIImage的相等性检查将失败。我希望对body进行三个调用:初始调用,设置prev时的调用和设置image时的调用。我想我可以为prev添加一个可变值(即一个简单的类),并避免第二次调用。
请注意,使用扩展名和闭包可以完成Web内容的加载,但这是另一个问题。这样做会使WebImage缩减为仅几行代码。
那么,有没有更好的方法来完成此任务?
//
// ContentView.swift
// Learn
//
// Created by John Morris on 11/26/19.
// Copyright © 2019 John Morris. All rights reserved.
//
import SwiftUI
struct WebImage: View {
@Binding var imagePath: String?
@State var prev: String?
@State var myimage: UIImage?
@State var message: String?
var body: some View {
if imagePath != prev {
self.downloadImage(from: imagePath)
}
return VStack {
myimage.map({Image(uiImage: $0).resizable()})
message.map({Text("\($0)")})
}
}
init?(imagePath: Binding<String?>) {
guard imagePath.wrappedValue != nil else {
return nil
}
self._imagePath = imagePath
guard let _ = URL(string: self.imagePath!) else {
return nil
}
}
func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
func downloadImage(from imagePath: String?) {
DispatchQueue.main.async() {
self.prev = imagePath
}
guard let imagePath = imagePath, let url = URL(string: imagePath) else {
self.message = "Image path is not URL"
return
}
getData(from: url) { data, response, error in
if let error = error {
self.message = error.localizedDescription
return
}
guard let httpResponse = response as? HTTPURLResponse else {
self.message = "No Response"
return
}
guard (200...299).contains(httpResponse.statusCode) else {
if httpResponse.statusCode == 404 {
self.message = "Page, \(url.absoluteURL), not found"
} else {
self.message = "HTTP Status Code \(httpResponse.statusCode)"
}
return
}
guard let mimeType = httpResponse.mimeType else {
self.message = "No mimetype"
return
}
guard mimeType == "image/jpeg" else {
self.message = "Wrong mimetype"
return
}
print(response.debugDescription)
guard let data = data else {
self.message = "No Data"
return
}
if let image = UIImage(data: data) {
DispatchQueue.main.async() {
self.myimage = image
}
}
}
}
}
struct ContentView: View {
var images = ["https://c1.staticflickr.com/3/2260/5744476392_5d025d6a6a_o.jpg",
"https://c1.staticflickr.com/9/8521/8685165984_e0fcc1dc07_o.jpg",
"https://farm1.staticflickr.com/204/507064030_0d0cbc850c_o.jpg",
"https://farm2.staticflickr.com/440/19711210125_6c12414d8f_o.jpg"
]
@State var imageURL: String?
@State var count = 0
var body: some View {
VStack {
WebImage(imagePath: $imageURL).scaledToFit()
Button(action: {
self.imageURL = self.images[self.count]
self.count += 1
if self.count >= self.images.count {
self.count = 0
}
}) {
Text("Next")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
答案 0 :(得分:0)
我建议两件事。首先,您通常希望在下载图像时允许一个占位符视图。其次,您应该缓存图像,否则,如果有tableView之类的东西在屏幕上滚动并回到屏幕上,则您将继续重新下载图像。这是我的一个应用程序中如何解决该问题的示例:
import SwiftUI
import Combine
import UIKit
class ImageCache {
enum Error: Swift.Error {
case dataConversionFailed
case sessionError(Swift.Error)
}
static let shared = ImageCache()
private let cache = NSCache<NSURL, UIImage>()
private init() { }
static func image(for url: URL) -> AnyPublisher<UIImage?, ImageCache.Error> {
guard let image = shared.cache.object(forKey: url as NSURL) else {
return URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap { (tuple) -> UIImage in
let (data, _) = tuple
guard let image = UIImage(data: data) else {
throw Error.dataConversionFailed
}
shared.cache.setObject(image, forKey: url as NSURL)
return image
}
.mapError({ error in Error.sessionError(error) })
.eraseToAnyPublisher()
}
return Just(image)
.mapError({ _ in fatalError() })
.eraseToAnyPublisher()
}
}
class ImageModel: ObservableObject {
@Published var image: UIImage? = nil
var cacheSubscription: AnyCancellable?
init(url: URL) {
cacheSubscription = ImageCache
.image(for: url)
.replaceError(with: nil)
.receive(on: RunLoop.main, options: .none)
.assign(to: \.image, on: self)
}
}
struct RemoteImage : View {
@ObservedObject var imageModel: ImageModel
init(url: URL) {
imageModel = ImageModel(url: url)
}
var body: some View {
imageModel
.image
.map { Image(uiImage:$0).resizable() }
?? Image(systemName: "questionmark").resizable()
}
}