异步下载图像时SwiftUI和Combine工作不顺畅
Posted
技术标签:
【中文标题】异步下载图像时SwiftUI和Combine工作不顺畅【英文标题】:SwiftUI and Combine not working smoothly when downloading image asynchrously 【发布时间】:2019-07-08 15:11:02 【问题描述】:当我尝试使用 SwiftUI & Combine 异步下载图像时,它工作正常。然后,我尝试将其实现为动态列表,我发现只有一行(最后一行)可以正确显示,其他单元格中的图像丢失。我已经用断点跟踪代码,并且我确信图像下载过程在其他人中是成功的,但只有最后一行会触发 @ObjectBinding 来更新图像。请检查我的示例代码,如果有任何错误,请告诉我。谢谢!
struct UserView: View
var name: String
@ObjectBinding var loader: ImageLoader
init(name: String, loader: ImageLoader)
self.name = name
self.loader = loader
var body: some View
HStack
Image(uiImage: loader.image ?? UIImage())
.onAppear
self.loader.load()
Text("\(name)")
struct User
let name: String
let imageUrl: String
struct ContentView : View
@State var users: [User] = []
var body: some View
NavigationView
List(users.identified(by: \.name)) user in
UserView(name: user.name, loader: ImageLoader(with: user.imageUrl))
.navigationBarTitle(Text("Users"))
.navigationBarItems(trailing:
Button(action:
self.didTapAddButton()
, label:
Text("+").font(.system(size: 36.0))
))
func didTapAddButton()
fetchUser()
func fetchUser()
API.fetchData (user) in
self.users.append(user)
class ImageLoader: BindableObject
let didChange = PassthroughSubject<UIImage?, Never>()
var urlString: String
var task: URLSessionDataTask?
var image: UIImage? = UIImage(named: "user")
didSet
didChange.send(image)
init(with urlString: String)
print("init a new loader")
self.urlString = urlString
func load()
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url) (data, _, error) in
if error == nil
DispatchQueue.main.async
self.image = UIImage(data: data!)
task.resume()
self.task = task
func cancel()
if let task = task
task.cancel()
class API
static func fetchData(completion: @escaping (User) -> Void)
let request = URLRequest(url: URL(string: "https://randomuser.me/api/")!)
let task = URLSession.shared.dataTask(with: request) (data, _, error) in
guard error == nil else return
do
let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
guard
let results = json!["results"] as? [[String: Any]],
let nameDict = results.first!["name"] as? [String: String],
let pictureDict = results.first!["picture"] as? [String: String]
else return
let name = "\(nameDict["last"]!) \(nameDict["first"]!)"
let imageUrl = pictureDict["thumbnail"]
let user = User(name: name, imageUrl: imageUrl!)
DispatchQueue.main.async
completion(user)
catch let error
print(error.localizedDescription)
task.resume()
无论列表中有多少项目,每张图片都应该成功下载。
【问题讨论】:
【参考方案1】:@ObjectBinding 中似乎存在错误。我不确定,也无法确认。我想创建一个最小的示例代码来确定,如果是这样,请向 Apple 报告错误。有时 SwiftUI 似乎不会使视图无效,即使它所基于的 @ObjectBinding 调用了它的 didChange.send() 。我发布了我自己的问题 (@BindableObject async call to didChange.send() does not invalidate its view (and never updates))
与此同时,我会尽可能使用 EnvironmentObject,因为似乎不存在该错误。
然后,您的代码只需进行很少的更改即可工作。不要使用 ObjectBinding,而是使用 EnvironmentObject:
用@EnvironmentObject 替换@ObjectBinding 的代码:
import SwiftUI
import Combine
struct UserView: View
var name: String
@EnvironmentObject var loader: ImageLoader
init(name: String)
self.name = name
var body: some View
HStack
Image(uiImage: loader.image ?? UIImage())
.onAppear
self.loader.load()
Text("\(name)")
struct User
let name: String
let imageUrl: String
struct ContentView : View
@State var users: [User] = []
var body: some View
NavigationView
List(users.identified(by: \.name)) user in
UserView(name: user.name).environmentObject(ImageLoader(with: user.imageUrl))
.navigationBarTitle(Text("Users"))
.navigationBarItems(trailing:
Button(action:
self.didTapAddButton()
, label:
Text("+").font(.system(size: 36.0))
))
func didTapAddButton()
fetchUser()
func fetchUser()
API.fetchData (user) in
self.users.append(user)
class ImageLoader: BindableObject
let didChange = PassthroughSubject<UIImage?, Never>()
var urlString: String
var task: URLSessionDataTask?
var image: UIImage? = UIImage(named: "user")
didSet
didChange.send(image)
init(with urlString: String)
print("init a new loader")
self.urlString = urlString
func load()
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url) (data, _, error) in
if error == nil
DispatchQueue.main.async
self.image = UIImage(data: data!)
task.resume()
self.task = task
func cancel()
if let task = task
task.cancel()
class API
static func fetchData(completion: @escaping (User) -> Void)
let request = URLRequest(url: URL(string: "https://randomuser.me/api/")!)
let task = URLSession.shared.dataTask(with: request) (data, _, error) in
guard error == nil else return
do
let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
guard
let results = json!["results"] as? [[String: Any]],
let nameDict = results.first!["name"] as? [String: String],
let pictureDict = results.first!["picture"] as? [String: String]
else return
let name = "\(nameDict["last"]!) \(nameDict["first"]!)"
let imageUrl = pictureDict["thumbnail"]
let user = User(name: name, imageUrl: imageUrl!)
DispatchQueue.main.async
completion(user)
catch let error
print(error.localizedDescription)
task.resume()
【讨论】:
以上是关于异步下载图像时SwiftUI和Combine工作不顺畅的主要内容,如果未能解决你的问题,请参考以下文章
使用 Combine 和 SwiftUI 对点击的按钮进行更改
swiftui+combine:为啥滚动 LazyVGrid 时 isFavoriteO 改变了?
将 AppKit/UIKit 中的 Key-Value Observation 转换为 Combine 和 SwiftUI