异步下载图片时,SwiftUI和Combine不能正常工作 [英] SwiftUI and Combine not working smoothly when downloading image asynchrously
问题描述
当我尝试使用SwiftUI&结合异步下载图像,效果很好.然后,我尝试将其实现为动态列表,发现只有一行(最后一行)可以正确显示,其他单元格中的图像丢失了.我已经用断点跟踪了代码,并且我确定图像下载过程在其他过程中是成功的,但是只有最后一行会触发@ObjectBinding来更新图像.请检查我的示例代码,并让我知道是否有任何错误.谢谢!
struct UserView:视图{变量名称:字符串@ObjectBinding var loader:ImageLoaderinit(name:String,loader:ImageLoader){self.name =名称self.loader =加载程序}var body:some View {HStack {Image(uiImage:loader.image ?? UIImage()).onAppear {self.loader.load()}文字("\(名称)")}}}struct用户{命名:字符串let imageUrl:字符串}struct ContentView:查看{@State var用户:[用户] = []var body:some View {NavigationView {列表(users.identified(by:\ .name)){用户在UserView(名称:user.name,加载器:ImageLoader(with:user.imageUrl))}.navigationBarTitle(Text("Users")).navigationBarItems(跟踪:按钮(操作:{self.didTapAddButton()}, 标签: {Text("+").font(.system(size:36.0))}))}}func didTapAddButton(){fetchUser()}func fetchUser(){API.fetchData {(使用者)self.users.append(用户)}}}类ImageLoader:BindableObject {让didChange = PassthroughSubject< UIImage ?,从不>()var urlString:字符串var任务:URLSessionDataTask?var image:UIImage?= UIImage(named:"user"){didSet {didChange.send(图片)}}初始化(带有urlString:字符串){print(初始化一个新的加载器")self.urlString = urlString}func load(){让url = URL(string:urlString)!let task = URLSession.shared.dataTask(with:url){(data,_,error)in如果错误== nil {DispatchQueue.main.async {self.image = UIImage(数据:数据!)}}}task.resume()self.task =任务}func cancel(){如果让任务=任务{task.cancel()}}}类API {静态函数fetchData(完成:@转义(用户)->无效){let request = URLRequest(URL:URL(string:"https://randomuser.me/api/")!)let task = URLSession.shared.dataTask(with:request){(data,_,error)in防护错误==无其他{返回}做 {让json =试试JSONSerialization.jsonObject(with:data !, options:[])as?[字串:任何]警卫let results = json!["results"] as?[[String:Any]],让nameDict = results.first!["name"]为?[String:String],让pictureDict = results.first!["picture"]为?[字符串:字符串]否则{return}let name ="\(nameDict [" last]!)\(nameDict [" first]!)"让imageUrl = pictureDict [缩略图"]让用户=用户(名称:名称,imageUrl:imageUrl!)DispatchQueue.main.async {完成(用户)}}捕获让错误{打印(error.localizedDescription)}}task.resume()}}
无论列表中有多少项,每个图像都应成功下载.
@ObjectBinding中似乎存在错误.我不确定,也无法确认.我想创建一个最小的示例代码来确定,如果可以,请向Apple报告一个错误.似乎SwiftUI有时不会使视图无效,即使基于它的@ObjectBinding调用了didChange.send()也是如此.我发布了我自己的问题(
用@EnvironmentObject替换@ObjectBinding的代码 :
导入SwiftUI进口联合收割机struct UserView:查看{变量名称:字符串@EnvironmentObject var loader:ImageLoaderinit(name:String){self.name =名称}var body:some View {HStack {Image(uiImage:loader.image ?? UIImage()).onAppear {self.loader.load()}文字("\(名称)")}}}struct用户{命名:字符串let imageUrl:字符串}struct ContentView:查看{@State var用户:[用户] = []var body:some View {NavigationView {列表(users.identified(by:\ .name)){用户在UserView(名称:user.name).environmentObject(ImageLoader(with:user.imageUrl))}.navigationBarTitle(Text("Users")).navigationBarItems(跟踪:按钮(操作:{self.didTapAddButton()}, 标签: {Text("+").font(.system(size:36.0))}))}}func didTapAddButton(){fetchUser()}func fetchUser(){API.fetchData {(使用者)self.users.append(用户)}}}类ImageLoader:BindableObject {让didChange = PassthroughSubject< UIImage ?,从不>()var urlString:字符串var任务:URLSessionDataTask?var image:UIImage?= UIImage(named:"user"){didSet {didChange.send(图片)}}初始化(带有urlString:字符串){print(初始化一个新的加载器")self.urlString = urlString}func load(){让url = URL(string:urlString)!let task = URLSession.shared.dataTask(with:url){(data,_,error)in如果错误== nil {DispatchQueue.main.async {self.image = UIImage(数据:数据!)}}}task.resume()self.task =任务}func cancel(){如果让任务=任务{task.cancel()}}}类API {静态函数fetchData(完成:@转义(用户)->无效){let request = URLRequest(URL:URL(string:"https://randomuser.me/api/")!)let task = URLSession.shared.dataTask(with:request){(data,_,error)in防护错误==无其他{返回}做 {让json =试试JSONSerialization.jsonObject(with:data !, options:[])as?[字串:任何]警卫let results = json!["results"] as?[[String:Any]],让nameDict = results.first!["name"]为?[String:String],让pictureDict = results.first!["picture"]为?[字符串:字符串]否则{return}let name ="\(nameDict [" last]!)\(nameDict [" first]!)"让imageUrl = pictureDict [缩略图"]让用户=用户(名称:名称,imageUrl:imageUrl!)DispatchQueue.main.async {完成(用户)}}捕获让错误{打印(error.localizedDescription)}}task.resume()}}
When I tried to use SwiftUI & Combine to download image asynchrously, it works fine. Then, I try to implement this into a dynamic list, and I found out there is only one row(the last row) will be show correctly, images in other cells are missing. I have trace the code with breakpoints and I'm sure the image download process is success in others, but only the last row will trigger the @ObjectBinding to update image. Please check my sample code and let me know if there's any wrong. Thanks!
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()
}
}
every images should be downloaded successfully no matter how many items in the list.
There seems to be a bug in @ObjectBinding. I am not sure and I cannot confirm yet. I want to create a minimal example code to be sure, and if so, report a bug to Apple. It seems that sometimes SwiftUI does not invalidate a view, even if the @ObjectBinding it is based upon has its didChange.send() called. I posted my own question (@BindableObject async call to didChange.send() does not invalidate its view (and never updates))
In the meantime, I try to use EnvironmentObject whenever I can, as the bug doesn't seem to be there.
Your code then works with very few changes. Instead of using ObjectBinding, use EnvironmentObject:
Code Replacing @ObjectBinding with @EnvironmentObject:
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不能正常工作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!