异步下载图像时,SwiftUI和Combine无法正常工作

时间:2019-07-08 15:11:02

标签: swift swiftui combine

当我尝试使用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 个答案:

答案 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:

enter image description here

用@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()
    }
}