ios - Rx swift : How to create cache for last network response without creating class/struct property?

标签 ios swift caching rx-swift reactive

我正在开发使用 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 有多个请求的情况。它通过将相同的响应对象推送给所有观察者来实现这一点。 (schedulerlock 的存在是为了保护可以在任何线程上调用的 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/

相关文章:

ios - 在 AVPlayer 中切换视频会在更改时创建 Flash

ios - 在 Swift 5 中更改 UIView 宽度

swift - Xcode 9.2 编译速度太慢

ios - RxSwift 差异库崩溃 `NSInternalInconsistencyException`

caching - 让 leiningen 缓存包

ios - Crashlytics——我们需要为商业应用分析付费吗

iphone - [indexPath 行] 中的 EXC_BAD_ACCESS

xcode - Playground Xcode 7.2.1 中无法访问图像资源

.net - 如何在控制台应用程序中使用 System.Web.Caching.Cache?

linux - 页面上的某些图像未通过 Varnish Cache-304 未修改