异步下载图像时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 对点击的按钮进行更改

如何使用 combine Publisher 更改线程?

swiftui+combine:为啥滚动 LazyVGrid 时 isFavoriteO 改变了?

将 AppKit/UIKit 中的 Key-Value Observation 转换为 Combine 和 SwiftUI

SwiftUI 列表 - 远程图像加载 = 图像闪烁(重新加载)

如何使用 SwiftUI 和 Combine 检测 Datepicker 的值变化?