我正在开发使用 IP Stack API 进行地理定位的 iOS 应用程序。我想通过首先请求外部(公共(public))IP 地址来优化 IP Stack Api 的使用,然后在该 IP 未更改的情况下重新使用 lat 响应。
所以我所追求的是我每次都询问 https://www.ipify.org关于外部IP,然后问https://ipstack.com使用给定的 IP 地址。如果我第二次询问但 IP 未更改,则重新使用上次响应(或实际缓存的字典,其中 IP 作为键,响应作为值)。
我有一个解决方案,但我对代码中的缓存属性不满意。它是某种状态,代码的其他部分可以改变它。我正在考虑在 RxSwfit 中使用一些 scan()
运算符,但我想不出任何新想法。
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()
}
使用上面的方法可以隔离你的状态,并为其他需要缓存的情况创建一个很好的可重用组件。
我知道它看起来比您当前的代码更复杂,但可以优雅地处理在返回任何响应之前对同一 key 有多个请求的情况。它通过将相同的响应对象推送给所有观察者来实现这一点。 (scheduler
和 lock
的存在是为了保护可以在任何线程上调用的 data(for:)
。)
我还对您的代码提出了其他一些改进建议。
不是使用
flatMapLatest
来解包一个可选的,而是过滤掉可选的。但是在这种情况下,空 String 和 nil String 有什么区别呢?更好的方法是使用 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)
}
}
关于ios - Rx swift : How to create cache for last network response without creating class/struct property?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53863865/