当我尝试使用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()
}
}
无论列表中有多少项,每个图像都应成功下载。
答案 0 :(得分:2)
@ObjectBinding中似乎有一个错误。我不确定,也无法确认。我想创建一个最小的示例代码来确定,如果可以,请向Apple报告一个错误。似乎SwiftUI有时不会使视图无效,即使基于它的@ObjectBinding调用了didChange.send()也是如此。我发布了自己的问题(@BindableObject async call to didChange.send() does not invalidate its view (and never updates))
同时,由于该bug似乎不存在,因此我会尽可能尝试使用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()
}
}