ios - RxSwift:如何在不创建类/结构属性的情况下为最后的网络响应创建缓存?
问题描述
我正在开发使用 IP Stack API 进行地理定位的 iOS 应用程序。我想通过首先询问外部(公共)IP 地址来优化 IP 堆栈 Api 的使用,然后在该 IP 没有更改的情况下重新使用该 IP 的 lat 响应。
所以我所追求的是我每次在https://www.ipify.org上询问外部 IP,然后用给定的 IP 地址询问https://ipstack.com 。如果我第二次询问但 IP 没有更改,则重新使用最后一个响应(或者实际上以 IP 作为键和响应作为值的缓存字典)。
我有一个解决方案,但我对代码中的这个缓存属性不满意。它是某种状态,而代码的其他一些部分可以改变它。我正在考虑scan()
在 RxSwfit 中使用一些运算符,但我就是想不出任何新的想法。
class ViewController: UIViewController {
@IBOutlet var geoButton: UIButton!
let disposeBag = DisposeBag()
let API_KEY = "my_private_API_KEY"
let provider = PublicIPProvider()
var cachedResponse: [String: Any] = [:] // <-- THIS
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
// .flatMap to ignore all nil values,
// $0 - my structure to contains IP address as string
let fetchedIP = provider.currentPublicIP()
.timeout(3.0, scheduler: MainScheduler.instance)
.flatMapLatest { Observable.from(optional: $0.ip) }
.distinctUntilChanged()
// excuse me my barbaric URL creation, it's just for demonstration
let geoLocalization = fetchedIP
.flatMapLatest { ip -> Observable<Any> in
// check if cache contains response for given IP address
guard let lastResponse = self.cachedResponse[ip] else {
return URLSession.shared.rx.json(request: URLRequest(url: URL(string: "http://api.ipstack.com/\(ip)?access_key=\(API_KEY)")! ))
.do(onNext: { result in
// store cache as a "side effect"
print("My result 1: \(result)")
self.cachedResponse[ip] = result
})
}
return Observable.just(lastResponse)
}
geoLocalization
.subscribe(onNext: { result in
print("My result 2: \(result)")
})
.disposed(by: disposeBag)
}
}
是否有可能实现相同的功能但var cachedResponse: [String: Any] = [:]
在我的班级中没有财产?
解决方案
我的天啊!我花了很多时间来回答这个问题(见下文),然后意识到有一个更简单的解决方案。只需在 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
andlock
存在以保护data(for:)
可以在任何线程上调用的。)
我还对您的代码提出了其他一些改进建议。
而不是使用
flatMapLatest
来解开一个可选项,只需过滤掉可选项。但是在这种情况下,空字符串和零字符串有什么区别?最好使用 nil 合并运算符并过滤掉空。由于您在 IBAction 中有代码,我假设
currentPublicIP()
它只发出一个值并完成或出错。通过让它返回一个 Single 来明确这一点。如果它确实发出多个值,那么您正在为每个函数调用创建一个新链,并且它们都将发出值。这不太可能是您想要的。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)
}
}
推荐阅读
- autohotkey - AHK - 如何在按住时绑定和在按下时绑定以暂停
- django - 在单元测试中添加不记名令牌 Django - ValueError: int() 的无效文字,基数为 10
- javascript - 在从 React Icons 导入的 Icons 上使用 useRef
- python - 如何从单个命令中获取多个值?
- puppeteer - 禁用 Chrome “恢复页面?Chrome 未正确关闭”和其他浏览器消息
- postgresql - TypeORM PostgreSQL 无法连接
- python - ModuleNotFoundError:没有名为“src.testproject”的模块
- c# - 将图片框的图像从用户控件更改为带有图像链接的表单
- c# - JsonConvert 反序列化 Avro 文件得到“解析值时遇到意外字符:O. Path '', line 0, position 0。” 错误
- jestjs - 使用 NEXT/IMAGE 开玩笑时出错:`next/image` 上的 src 属性无效,主机名“res.cloudinary.com”未在`next.config.js` 中的图像下配置