Swift Combine: завершение отправки после значения отправки

Я работаю над кешированием для своего сетевого модуля. Мой модуль будет возвращать AnyCancallable вызывающей стороне для каждого запроса. Если кэшированные данные недоступны, я использую URLSession.dataTaskPublisher, он отлично работает с двумя событиями: получение данных и завершение. Если доступны кэшированные данные, я буду использовать CurrentValueSubject для создания AnyCancallable для возврата. Я отправляю оба события по этой теме, но на стороне вызывающего абонента он получает уведомление только о завершении, без данных.

cacheSubject.send(cachedData.data)
cacheSubject.send(completion: Subscribers.Completion<Error>.finished)

Удаление завершения отправки, и теперь он может получать данные, но не уведомление о завершении.

Может кто-нибудь, дайте мне знать, что я здесь делаю не так?

Вот полный файл на случай, если он вам понадобится:

public class SSNetworkManager {
    public static let shared = SSNetworkManager()
    
    private var cache: [String: CachedData] = [:]
    private let cacheSubject = CurrentValueSubject<Data, Error>(Data())
    
    @discardableResult public func makeServiceCall<D: Decodable>(forRequest request: SSNetworkRequest<D>, onMainThread: Bool = true) -> AnyPublisher<D, Error>? {
        guard let urlRequest = request.urlRequest else {
            return nil
        }
        
        var cancelable: AnyPublisher<Data, Error>
        
        if let url = urlRequest.url?.absoluteString,
           let cachedData = cache[url],
           cachedData.isValid {
            cancelable = cacheSubject.eraseToAnyPublisher()
            cacheSubject.send(cachedData.data)
            cacheSubject.send(completion: Subscribers.Completion<Error>.finished)
        } else {
            cancelable = URLSession.shared.dataTaskPublisher(for: urlRequest).tryMap {[weak self] (data, response) -> Data in
                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                    throw SSNetworkError(httpCode: (response as? HTTPURLResponse)?.statusCode ?? 0, data: data)
                }
                if request.shouldCacheNow,
                   let url = urlRequest.url?.absoluteString {
                    self?.cache[url] = CachedData(data: data, expirationTime: request.cacheExpirationTime)
                }
                return data
            }.eraseToAnyPublisher()
        }
        
        if onMainThread {
            return cancelable
                .receive(on: RunLoop.main)
                .decode(type: D.self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
        } else {
            return cancelable
                .decode(type: D.self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
        }
    }
}

fileprivate struct CachedData {
    let data: Data
    let expirationTime: Date
    
    var isValid: Bool {
        return Date().compare(expirationTime) != .orderedDescending
    }
}

person Son Nguyen    schedule 19.12.2020    source источник
comment
Одна вещь, которую я могу заметить, это то, что ваше свойство cancellable находится внутри функции, что неверно, я думаю, оно должно быть внутри вашего класса, иначе оно не будет содержать никакого значения после области действия функции.   -  person CrackIt    schedule 19.12.2020
comment
Код кеша выполняется синхронно, поэтому вы отправляете значение до того, как его получит подписчик. И предметы не хранят ценности - их практически нет   -  person New Dev    schedule 19.12.2020
comment
@CrackIt, потом возвращаю, вызывающий отвечает за сохранение ссылки на cancallable   -  person Son Nguyen    schedule 20.12.2020
comment
@NewDev Хороший улов. Спасибо   -  person Son Nguyen    schedule 20.12.2020
comment
@NewDev Я попытался вызвать отправку после возврата подписчика, но он все еще отправляет только завершение   -  person Son Nguyen    schedule 20.12.2020


Ответы (1)


Это неподходящий случай для использования темы. Вместо этого верните издателя, относящегося к каждому случаю:

public class SSNetworkManager {
    // ...
    
    public func makeServiceCall<D: Decodable>(
           forRequest request: SSNetworkRequest<D>, 
           onMainThread: Bool = true
        ) -> AnyPublisher<D, Error>? {

        // consider just returning Empty().eraseToAnyPublisher() instead of nil
        guard let urlRequest = request.urlRequest else {
            return nil
        }

        var resultPublisher: AnyPublisher<D: Error>
        
        if let url = urlRequest.url?.absoluteString,
           let cachedData = cache[url],
           cachedData.isValid {

            resultPublisher = Just(cachedData.data)
                      .setFailureType(to: Error.self)
                      .eraseToAnyPublisher()

        } else {
            resultPublisher = URLSession.shared
                .dataTaskPublisher(for: urlRequest)
                .tryMap { ... }
                .decode(type: D.self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
        }

        return onMainThread
               ? resultPublisher
                    .receive(on: DispatchQueue.main)
                    .eraseToAnyPublisher()
               : resultPublisher
                    .eraseToAnyPublisher()
    }
}
person New Dev    schedule 20.12.2020
comment
Большое спасибо @NewDev, вы избавили меня от головной боли. не знал, что просто существует. А также асинхронная отправка темы не сработала, потому что я создал беспорядок со ссылками. - person Son Nguyen; 20.12.2020