异步下载图片时,SwiftUI和Combine不能正常工作 [英] SwiftUI and Combine not working smoothly when downloading image asynchrously

查看:150
本文介绍了异步下载图片时,SwiftUI和Combine不能正常工作的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我尝试使用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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆