OMG!Я потратил кучу времени на ответ на этот вопрос (см. Ниже), а затем понял, что есть гораздо более простое решение.Просто передайте правильный параметр кэширования в свой URLRequest, и вы сможете полностью избавиться от внутреннего кэша!Я оставил оригинальный ответ, потому что я также делаю общий обзор вашего кода.
class ViewController: UIViewController {
let disposeBag = DisposeBag()
let API_KEY = "my_private_API_KEY"
let provider = PublicIPProvider()
@IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
let fetchedIP: Maybe<String> = provider.currentPublicIP() // `currentPublicIP()` returns a Single
.timeout(3.0, scheduler: MainScheduler.instance)
.map { $0.ip ?? "" }
.filter { !$0.isEmpty }
// excuse me my barbaric URL creation, it's just for demonstration
let geoLocalization = fetchedIP
.flatMap { (ip) -> Maybe<Any> in
let url = URL(string: "http://api.ipstack.com/\(ip)?access_key=cce3a2a23ce22922afc229b154d08393")!
return URLSession.shared.rx.json(request: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad))
.asMaybe()
}
geoLocalization
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { result in
print("My result 2: \(result)")
})
.disposed(by: disposeBag)
}
}
Оригинальный ответ
Краткий ответ здесь - нет.Лучшее, что вы можете сделать - это обернуть состояние в классе, чтобы ограничить его доступ.Что-то вроде этого общего подхода:
final class Cache<Key: Hashable, State> {
init(qos: DispatchQoS, source: @escaping (Key) -> Single<State>) {
scheduler = SerialDispatchQueueScheduler(qos: qos)
getState = source
}
func data(for key: Key) -> Single<State> {
lock.lock(); defer { lock.unlock() }
guard let state = cache[key] else {
let state = ReplaySubject<State>.create(bufferSize: 1)
getState(key)
.observeOn(scheduler)
.subscribe(onSuccess: { state.onNext($0) })
.disposed(by: bag)
cache[key] = state
return state.asSingle()
}
return state.asSingle()
}
private var cache: [Key: ReplaySubject<State>] = [:]
private let scheduler: SerialDispatchQueueScheduler
private let lock = NSRecursiveLock()
private let getState: (Key) -> Single<State>
private let bag = DisposeBag()
}
Использование вышеизложенного изолирует ваше состояние и создает хороший повторно используемый компонент для других ситуаций, где необходим кеш.
Я знаю, что это выглядит сложнее, чем ваштекущий код, но изящно обрабатывает ситуацию, когда есть несколько запросов на один и тот же ключ, прежде чем любой ответ будет возвращен.Он делает это, передавая один и тот же объект ответа всем наблюдателям.(scheduler
и lock
существуют для защиты data(for:)
, который может быть вызван в любом потоке.)
У меня есть и другие предлагаемые улучшения для вашего кода.
Вместо того, чтобы использовать flatMapLatest
, чтобы развернуть опцию, просто отфильтруйте опцию.Но в этом случае, в чем разница между пустой строкой и нулевой строкой?Лучше было бы использовать оператор слияния ноль и отфильтровать пустые.
Поскольку у вас есть код в IBAction, я предполагаю, что currentPublicIP()
испускает только одно значение и завершается или содержит ошибки.Проясните это, заставив его вернуть сингл.Если действительно испускает несколько значений, то вы создаете новую цепочку с каждым вызовом функции, и все из них будут излучать значения.Вряд ли это то, что вам нужно.
Функция URLSession json(request:)
генерирует в фоновом потоке.Если вы собираетесь что-то делать с UIKit, вам нужно будет наблюдать в главном потоке.
Вот результирующий код с упомянутыми выше настройками:
class ViewController: UIViewController {
private let disposeBag = DisposeBag()
private let provider = PublicIPProvider()
private let responses: Cache<String, Any> = Cache(qos: .userInitiated) { ip in
return URLSession.shared.rx.json(request: URLRequest(url: URL(string: "http://api.ipstack.com/\(ip)?access_key=cce3a2a23ce22922afc229b154d08393")!))
.asSingle()
}
@IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
let fetchedIP: Maybe<String> = provider.currentPublicIP() // `currentPublicIP()` returns a Single
.timeout(3.0, scheduler: MainScheduler.instance)
.map { $0.ip ?? "" }
.filter { !$0.isEmpty }
let geoLocalization: Maybe<Any> = fetchedIP
.flatMap { [weak responses] ip in
return responses?.data(for: ip).asMaybe() ?? Maybe.empty()
}
geoLocalization
.observeOn(MainScheduler.instance) // this is necessary if your subscribe messes with UIKit
.subscribe(onSuccess: { result in
print("My result 2: \(result)")
}, onError: { error in
// don't forget to handle errors.
})
.disposed(by: disposeBag)
}
}