Объединить: Издатель иногда теряет ценность и завершает - PullRequest
2 голосов
/ 14 февраля 2020

У меня есть простой Deferred Publisher, который считывает данные с диска, и я отображаю данные в SwiftUI List, Publisher работает в большинстве случаев хорошо, но иногда он не ведет себя хорошо, он просто теряет свое значение (это массив Model объектов) и завершается сообщением finished. Я попытался обойти упомянутый здесь , чтобы использовать оператор buffer для сохранения значения в буфере, потому что я считаю, что конструкция Combine's Publisher не будет передавать данные в нисходящем направлении, если запрос не будет запрошен подписчики и, следовательно, удаление этих данных и завершение, однако использование buffer не решило проблему.

У меня есть код:

enum FileError: Error {
    case someError
}

class ViewModel: ObservableObject {
    @Published var modelArray = [Model]()
    private var subscriptions = Set<AnyCancellable>()
    func readData() {
        DataSource()
            .readFromBundle(resource: "Sample", type: "json")
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                print("Completion: \(completion)")
            }) { array in
                self.modelArray = array
        }.store(in: &subscriptions)
    }
}
struct ContentView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        VStack {
            List(self.viewModel.modelArray) { model in
                Text("\(model.name)")
            }
        }
        .onAppear {
            self.viewModel.readData()
        }
    }
}

struct Model: Codable, Identifiable {
    var id: Int
    var name: String
}

class DataSource {
    private let readQueue = DispatchQueue(label: "ReadQueue", qos: .default, attributes: .concurrent)

    func readFromBundle (resource: String, type:String) -> AnyPublisher<[Model], FileError> {
            Deferred {
                 Future { promise in
                    guard let url = Bundle.main.url(forResource: "Sample", withExtension: "json"),
                      let data = try? Data(contentsOf: url),
                      let modelArray = try? JSONDecoder().decode([Model].self, from: data)
                      else {
                        promise(.failure(.someError))
                        return
                    }
                      promise(.success(modelArray))
                }
            }
           .receive(on: self.readQueue)
           .eraseToAnyPublisher()
        }
}

Это ссылка загрузить рабочий пример проекта.

РЕДАКТИРОВАТЬ:

Среда: Xcode 11.3.1, iOS 13.3 iPhone 11 Pro Max симулятор и устройство.

gif снимок экрана (обратите внимание на вывод консоли)

enter image description here

EDIT2:

если я добавлю любых нижестоящих издателей, например combineLatest, например, перед sink в функции потребителя readData(), тогда будет введено новое поведение, которое связывает асин * c издатель (readFromBundle) с syn c Издатель (combineLatest) приведет к тому, что значение не будет доставлено вообще на iOS 13.3+ устройствах и будет иногда доставляйте на устройствах ниже iOS 13.3, как указано на по этой ссылке .

Ответы [ 3 ]

2 голосов
/ 16 февраля 2020

Давайте посмотрим документацию о .receive(on:)

Указывает планировщик для получения элементов от издателя. Декларация

func receive<S>(on scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.ReceiveOn<Publishers.SubscribeOn<Deferred<Future<[Model], FileError>>, DispatchQueue>, S> where S : Scheduler

Обсуждение

Оператор receive(on:options:) используется для получения результатов в определенном планировщике c, например при выполнении пользовательского интерфейса в основном прогоне l oop. В отличие от subscribe(on:options:), который влияет на восходящие сообщения, receive(on:options:) изменяет контекст выполнения нижестоящих сообщений. В следующем примере запросы к jsonPublisher выполняются в backgroundQueue, но полученные от него элементы выполняются в RunL oop .main.

let jsonPublisher = MyJSONLoaderPublisher() // Some publisher.
let labelUpdater = MyLabelUpdateSubscriber() // Some subscriber that updates the UI.

jsonPublisher
    .subscribe(on: backgroundQueue)
    .receiveOn(on: RunLoop.main)
    .subscribe(labelUpdater)

Параметры

планировщик
Планировщик издатель должен использовать для доставки элементов. Параметры планировщика, которые настраивают доставку элемента. Возвращает

Издатель, который доставляет элементы с использованием указанного планировщика.

в вашем случае это означает

import SwiftUI
import Combine

enum FileError: Error {
    case someError
}

class ViewModel: ObservableObject {
    @Published var modelArray = [Model]()
    private var subscriptions = Set<AnyCancellable>()
    func readData() {
        DataSource()
            .readFromBundle(resource: "Sample", type: "json")
            .sink(receiveCompletion: { completion in
                print("Completion: \(completion)")
            }) { array in
                print("received value")
                self.modelArray = array
        }.store(in: &subscriptions)
    }
}
struct ContentView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        VStack {
            List(self.viewModel.modelArray) { model in
                Text("\(model.name)")
            }
        }
        .onAppear {
            self.viewModel.readData()
        }
    }
}

struct Model: Codable, Identifiable {
    var id: Int
    var name: String
}

class DataSource {
    private let readQueue = DispatchQueue(label: "ReadQueue", qos: .default, attributes: .concurrent)

    func readFromBundle (resource: String, type:String) -> AnyPublisher<[Model], FileError> {
            Deferred {
                 Future { promise in
                    guard let url = Bundle.main.url(forResource: "Sample", withExtension: "json"),
                      let data = try? Data(contentsOf: url),
                      let modelArray = try? JSONDecoder().decode([Model].self, from: data)
                      else {
                        promise(.failure(.someError))
                        return
                    }
                      promise(.success(modelArray))
                }
            }
            .subscribe(on: readQueue)
            .receive(on: RunLoop.main)
           .eraseToAnyPublisher()
        }
}

, который объясняет, почему работает решение Asperi. Разница в том, что нет необходимости снова вызывать .receive (on :) в readData()

разница между DispatchQueue.main и RunLoop.main в вашем примере незначительна.

1 голос
/ 14 февраля 2020

Это похоже на проблему гонок, попробуйте следующее (просто путем чтения кода)

1) явно используйте фоновую очередь

private let readQueue = DispatchQueue(label: "ReadQueue", qos: .background, 
    attributes: .concurrent)

2) планируйте Publisher в этой очереди вместо получения на него

.subscribe(on: self.readQueue)
0 голосов
/ 14 февраля 2020

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

print("ready")
promise(.success(modelArray)) 

, а затем установите точку останова на «еще не загружен», и вы увидите, что «еще не загружено» появляется до того, как «готово» напечатано в консоли. Это не капля издателя.

Как говорит onAppear (), он будет вызван после того, как пользовательский интерфейс был показан ....

if self.viewModel.modelArray.count == 0 {
                Text("not loaded yet")
            } else {
                List(self.viewModel.modelArray) { model in
                    Text("\(model.name)")
                }
            }
...