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

все изображения должны быть загружены успешно, независимо от того, сколько элементов в списке.


person Nick Lee    schedule 08.07.2019    source источник


Ответы (1)


Похоже, в @ObjectBinding есть ошибка. Я не уверен и пока не могу подтвердить. Чтобы быть уверенным, я хочу создать минимальный пример кода, и если да, то сообщить об ошибке в Apple. Кажется, что иногда SwiftUI не делает представление недействительным, даже если привязка @ObjectBinding, на которой он основан, имеет свой didChange.send (). Я разместил свой вопрос (асинхронный вызов @BindableObject для didChange.send () не отменяет его представление (и никогда не обновляет) )

А пока я стараюсь использовать EnvironmentObject всякий раз, когда могу, поскольку ошибки, похоже, нет.

Затем ваш код работает с очень небольшими изменениями. Вместо использования ObjectBinding используйте EnvironmentObject:

введите описание изображения здесь

Код, заменяющий @ObjectBinding на @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()
    }
}
person kontiki    schedule 10.07.2019