You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift:如何用Timer确保API获取的current_price每分钟稳定更新?

Swift定时获取加密货币价格异常修复方案

问题现象

点击trackBtn触发价格获取逻辑,预期每分钟通过API拉取最新加密货币价格,但实际current_price仅首次更新后,后续每隔11-12分钟才刷新,定时器已配置为60秒触发一次。

关联代码

模型类

struct CryptoPrices: Decodable {
    var current_price: Double?
    mutating func update(current_price: Double) -> CryptoPrices {
        self.current_price = current_price
        return self
    }
}

CryptoManager业务类

import Combine
import Alamofire

class CryptoManager {
    static let shared = CryptoManager()
    var timer: Timer?
    private var cancellables: Set<AnyCancellable> = []
    var currentCryptoPrice: CryptoPrices?       

    func getCryptoPrices(symbol: String) -> AnyPublisher<CryptoPrices, Error> {
        let url = "https://example.com/api/v3/coins/markets?vs_currency=usd&ids=\(symbol)&order=market_cap_desc&per_page=100&page=1&sparkline=false&locale=en"

        var urlRequest = URLRequest(url: URL(string: url)!)
        urlRequest.httpMethod = HTTPMethod.get.rawValue
        urlRequest.cachePolicy = .reloadIgnoringLocalCacheData

        return AF.request(urlRequest)
            .publishDecodable(type: [CryptoPrices].self, decoder: JSONDecoder())
            .tryMap { response in
                guard let cryptoPricesArray = response.value else {
                    throw AFError.responseValidationFailed(reason: .dataFileNil)
                }

                guard let cryptoPrice = cryptoPricesArray.first else {
                    throw NSError(domain: "Invalid response format", code: 0, userInfo: nil)
                }

                if let updatedPrice = cryptoPrice.current_price {
                    return CryptoPrices(current_price: updatedPrice)
                } else {
                    return CryptoPrices(current_price: nil)
                }
            }
            .mapError { $0 as Error }
            .eraseToAnyPublisher()
    }
    
    func startFetchingWithTimer(symbol: String) -> AnyPublisher<CryptoPrices, Error> {
        let timerPublisher = Timer.publish(every: 60, on: .main, in: .common)
            .autoconnect()
            .map { _ in () }

        return timerPublisher
            .flatMap { _ in
                self.fetchCryptoPrices(symbol: symbol)
                    .handleEvents(receiveOutput: { cryptoPrices in
                        print("Received crypto prices: \(cryptoPrices)")
                    })
            }
            .eraseToAnyPublisher()
    }

    private func fetchCryptoPrices(symbol: String) -> AnyPublisher<CryptoPrices, Error> {
        getCryptoPrices(symbol: symbol)
            .handleEvents(receiveCompletion: { completion in
                switch completion {
                case .failure(let error):
                    print("Error fetching crypto prices: \(error)")
                case .finished:
                    break
                }
            })
            .eraseToAnyPublisher()
    }

    private var subscriptions = Set<AnyCancellable>()
}

控制器代码

import Combine

@IBOutlet weak var minimumRate: UITextField!
@IBOutlet weak var maximumRate: UITextField!
private let cryptoManager: CryptoManager
private var userNotificationCenter: UNUserNotificationCenter 
var symbol: String!

 func startMonitoring(symbol: String, minRate: Double, maxRate: Double) {
    guard let minRateText = minimumRate.text, let maxRateText = maximumRate.text else {
        return
    }

    guard let minRate = Double(minRateText), let maxRate = Double(maxRateText) else {
        return
    }

    print("Minimum Rate: \(minRate), Maximum Rate: \(maxRate)")

    self.cryptoManager.startFetchingWithTimer(symbol: symbol)
        .sink(receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                print("Error fetching price: \(error)")
            case .finished:
                break
            }
        }, receiveValue: { cryptoPrices in
            if let currentPrice = cryptoPrices.current_price {
                print("Received price: \(currentPrice)")
                if currentPrice <= minRate {
                    print("Min rate:", minRate)
                    print(currentPrice, "after checking the price has dropped")
                    self.sendNotification(symbol: symbol, price: currentPrice)
                } else if currentPrice >= maxRate {
                    print("Max rate:", maxRate)
                    print(currentPrice, "after checking the price has increased")
                    self.sendNotification(symbol: symbol, price: currentPrice)
                } else {
                    print("Price is the same")
                }
            }
        })
        .store(in: &self.subscriptions)
}

核心修复点

1. 限制并发请求数量

默认flatMap会无限制创建新的请求订阅,如果API响应耗时超过60秒,会导致定时器事件积压,后续请求被延迟处理。修改startFetchingWithTimer方法,添加maxPublishers: .max(1)确保同一时间只有一个请求执行:

func startFetchingWithTimer(symbol: String) -> AnyPublisher<CryptoPrices, Error> {
    let timerPublisher = Timer.publish(every: 60, on: .main, in: .common)
        .autoconnect()
        .map { _ in () }

    return timerPublisher
        // 限制同时仅一个请求在执行
        .flatMap(maxPublishers: .max(1)) { _ in
            self.fetchCryptoPrices(symbol: symbol)
                .handleEvents(receiveOutput: { cryptoPrices in
                    print("Received crypto prices: \(cryptoPrices)")
                })
        }
        .eraseToAnyPublisher()
}

2. 强制禁用服务器端缓存

虽然设置了URLRequest.CachePolicy.reloadIgnoringLocalCacheData,但服务器可能仍返回缓存数据。在getCryptoPrices方法中添加请求头,强制服务器返回最新数据:

var urlRequest = URLRequest(url: URL(string: url)!)
urlRequest.httpMethod = HTTPMethod.get.rawValue
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
// 添加缓存控制头
urlRequest.setValue("no-cache, no-store, must-revalidate", forHTTPHeaderField: "Cache-Control")
urlRequest.setValue("no-cache", forHTTPHeaderField: "Pragma")
urlRequest.setValue("0", forHTTPHeaderField: "Expires")

3. 统一订阅管理

CryptoManager中存在重复的订阅存储变量cancellables和subscriptions,保留一个即可,避免订阅管理混乱:

// 删除其中一个,保留统一的订阅容器
private var cancellables: Set<AnyCancellable> = []

4. 确保控制器订阅不被释放

确认控制器的subscriptions是类级别的实例属性,如果是局部变量会在方法执行完毕后被释放,导致订阅失效:

// 在控制器类顶部声明
private var subscriptions = Set<AnyCancellable>()

验证方法

修改后运行应用,查看控制台打印的Received price:日志,确认每分钟都会输出最新价格;同时检查API请求的响应头,确认Cache-Control已生效,服务器未返回缓存数据。

内容的提问来源于stack exchange,提问作者Rustin Wilde

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 11:30:55