mirror of
https://github.com/jiayouzl/Bitcoin-Monitoring.git
synced 2025-11-25 19:37:50 +08:00
✨ feat(ui): add configurable refresh interval and enhanced menu features
Major feature enhancement for the BTC price monitoring application: ### New Features - **Configurable Refresh Intervals**: Users can now select from 5s, 10s, 30s, 60s options - **Persistent Settings**: User preferences saved via UserDefaults - **GitHub Integration**: Direct link to project repository via menu - **Version Display**: Shows current app version in about dialog - **Debug Logging**: Comprehensive debug output for development (Debug builds only) ### Implementation Details - **AppSettings.swift**: New configuration management class - **RefreshInterval.swift**: Enum defining refresh options with display text - **Enhanced PriceManager**: Dynamic timer management with configurable intervals - **Updated BTCMenuBarApp**: Added refresh settings submenu, GitHub link, and version info - **Debug Infrastructure**: Conditional compilation logging throughout price update flow ### UI/UX Improvements - **Refresh Settings Submenu**: Visual indicators (✓) for current selection - **Enhanced About Dialog**: Shows current refresh interval and app version - **Clean Menu Structure**: Organized with proper separators and SF Symbols - **GitHub Button**: Quick access to project repository ### Code Quality - **Removed Redundancy**: Deleted unused ContentView.swift template file - **Comprehensive Comments**: Added detailed Chinese comments throughout - **Error Handling**: Robust error handling with user-friendly messages - **Architecture Clean**: Follows MVVM pattern with clear separation of concerns ### Files Changed - Modified: BTCMenuBarApp.swift, PriceManager.swift, project.pbxproj - Added: AppSettings.swift, RefreshInterval.swift - Deleted: ContentView.swift (unused template) - Updated: Entitlements, app entry point, and service files BREAKING CHANGE: None - all changes are additive and backward compatible
This commit is contained in:
@@ -78,7 +78,7 @@
|
|||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 1620;
|
LastSwiftUpdateCheck = 1620;
|
||||||
LastUpgradeCheck = 1620;
|
LastUpgradeCheck = 2600;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
4E94106F2EB09F90003658CB = {
|
4E94106F2EB09F90003658CB = {
|
||||||
CreatedOnToolsVersion = 16.2;
|
CreatedOnToolsVersion = 16.2;
|
||||||
@@ -159,6 +159,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
@@ -183,6 +184,7 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
};
|
};
|
||||||
@@ -222,6 +224,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@@ -239,6 +242,7 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -253,8 +257,12 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"test1/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"test1/Preview Content\"";
|
||||||
|
ENABLE_APP_SANDBOX = YES;
|
||||||
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
@@ -263,7 +271,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.4;
|
MACOSX_DEPLOYMENT_TARGET = 12.4;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mark.test1;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mark.test1;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -281,8 +289,12 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"test1/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"test1/Preview Content\"";
|
||||||
|
ENABLE_APP_SANDBOX = YES;
|
||||||
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
@@ -291,7 +303,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.4;
|
MACOSX_DEPLOYMENT_TARGET = 12.4;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mark.test1;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mark.test1;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
55
test1/AppSettings.swift
Normal file
55
test1/AppSettings.swift
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// AppSettings.swift
|
||||||
|
// Bitcoin Monitoring
|
||||||
|
//
|
||||||
|
// Created by Mark on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// 应用配置管理类
|
||||||
|
/// 负责管理用户的刷新间隔设置和其他应用配置
|
||||||
|
@MainActor
|
||||||
|
class AppSettings: ObservableObject {
|
||||||
|
|
||||||
|
// MARK: - Published Properties
|
||||||
|
|
||||||
|
/// 当前选中的刷新间隔
|
||||||
|
@Published var refreshInterval: RefreshInterval = .thirtySeconds
|
||||||
|
|
||||||
|
// MARK: - Private Properties
|
||||||
|
|
||||||
|
private let defaults = UserDefaults.standard
|
||||||
|
private let refreshIntervalKey = "BTCRefreshInterval"
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
init() {
|
||||||
|
loadSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Configuration Methods
|
||||||
|
|
||||||
|
/// 从UserDefaults加载保存的配置
|
||||||
|
/// 如果没有保存的配置,使用默认值(30秒)
|
||||||
|
func loadSettings() {
|
||||||
|
let savedValue = defaults.double(forKey: refreshIntervalKey)
|
||||||
|
|
||||||
|
// 查找匹配的刷新间隔,如果没有匹配的则使用默认值
|
||||||
|
if let savedInterval = RefreshInterval.allCases.first(where: { $0.rawValue == savedValue }) {
|
||||||
|
refreshInterval = savedInterval
|
||||||
|
} else {
|
||||||
|
refreshInterval = .thirtySeconds
|
||||||
|
// 保存默认值,确保下次启动时有正确的配置
|
||||||
|
saveRefreshInterval(.thirtySeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存用户选择的刷新间隔
|
||||||
|
/// - Parameter interval: 要保存的刷新间隔
|
||||||
|
func saveRefreshInterval(_ interval: RefreshInterval) {
|
||||||
|
refreshInterval = interval
|
||||||
|
defaults.set(interval.rawValue, forKey: refreshIntervalKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// BTCMenuBarApp.swift
|
// BTCMenuBarApp.swift
|
||||||
// test1
|
// test1
|
||||||
//
|
//
|
||||||
// Created by zl_vm on 2025/10/28.
|
// Created by Mark on 2025/10/28.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@@ -15,11 +15,23 @@ class BTCMenuBarApp: NSObject, ObservableObject {
|
|||||||
private var statusItem: NSStatusItem?
|
private var statusItem: NSStatusItem?
|
||||||
private var popover: NSPopover?
|
private var popover: NSPopover?
|
||||||
private var priceManager = PriceManager()
|
private var priceManager = PriceManager()
|
||||||
|
private var appSettings = AppSettings()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
setupMenuBar()
|
setupMenuBar()
|
||||||
|
setupConfigurationObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置配置观察者
|
||||||
|
private func setupConfigurationObservers() {
|
||||||
|
// 监听刷新间隔配置变化
|
||||||
|
appSettings.$refreshInterval
|
||||||
|
.sink { [weak self] newInterval in
|
||||||
|
self?.priceManager.updateRefreshInterval(newInterval)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置菜单栏
|
// 设置菜单栏
|
||||||
@@ -134,8 +146,45 @@ class BTCMenuBarApp: NSObject, ObservableObject {
|
|||||||
refreshItem.isEnabled = !priceManager.isFetching
|
refreshItem.isEnabled = !priceManager.isFetching
|
||||||
menu.addItem(refreshItem)
|
menu.addItem(refreshItem)
|
||||||
|
|
||||||
|
// 添加刷新设置子菜单
|
||||||
|
let refreshSettingsItem = NSMenuItem(title: "刷新设置", action: nil, keyEquivalent: "")
|
||||||
|
if let settingsImage = NSImage(systemSymbolName: "timer", accessibilityDescription: "刷新设置") {
|
||||||
|
settingsImage.size = NSSize(width: 16, height: 16)
|
||||||
|
refreshSettingsItem.image = settingsImage
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshSettingsMenu = NSMenu()
|
||||||
|
let currentInterval = priceManager.getCurrentRefreshInterval()
|
||||||
|
|
||||||
|
// 为每个刷新间隔创建菜单项
|
||||||
|
for interval in RefreshInterval.allCases {
|
||||||
|
let isCurrent = (interval == currentInterval)
|
||||||
|
let item = NSMenuItem(
|
||||||
|
title: interval.displayTextWithMark(isCurrent: isCurrent),
|
||||||
|
action: #selector(selectRefreshInterval(_:)),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
item.target = self
|
||||||
|
item.representedObject = interval
|
||||||
|
item.isEnabled = !isCurrent // 当前选中的项不能再次点击
|
||||||
|
|
||||||
|
refreshSettingsMenu.addItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshSettingsItem.submenu = refreshSettingsMenu
|
||||||
|
menu.addItem(refreshSettingsItem)
|
||||||
|
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
|
// 添加GitHub按钮(带GitHub图标)
|
||||||
|
let checkUpdateItem = NSMenuItem(title: "GitHub", action: #selector(checkForUpdates), keyEquivalent: "")
|
||||||
|
if let updateImage = NSImage(systemSymbolName: "star.circle", accessibilityDescription: "GitHub") {
|
||||||
|
updateImage.size = NSSize(width: 16, height: 16)
|
||||||
|
checkUpdateItem.image = updateImage
|
||||||
|
}
|
||||||
|
checkUpdateItem.target = self
|
||||||
|
menu.addItem(checkUpdateItem)
|
||||||
|
|
||||||
// 添加关于按钮(带信息图标)
|
// 添加关于按钮(带信息图标)
|
||||||
let aboutItem = NSMenuItem(title: "关于", action: #selector(showAbout), keyEquivalent: "")
|
let aboutItem = NSMenuItem(title: "关于", action: #selector(showAbout), keyEquivalent: "")
|
||||||
if let infoImage = NSImage(systemSymbolName: "info.circle", accessibilityDescription: "关于") {
|
if let infoImage = NSImage(systemSymbolName: "info.circle", accessibilityDescription: "关于") {
|
||||||
@@ -180,31 +229,70 @@ class BTCMenuBarApp: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 选择刷新间隔
|
||||||
|
@objc private func selectRefreshInterval(_ sender: NSMenuItem) {
|
||||||
|
guard let interval = sender.representedObject as? RefreshInterval else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置到UserDefaults
|
||||||
|
appSettings.saveRefreshInterval(interval)
|
||||||
|
|
||||||
|
// 立即应用新的刷新间隔
|
||||||
|
priceManager.updateRefreshInterval(interval)
|
||||||
|
|
||||||
|
print("✅ 刷新间隔已更新为: \(interval.displayText)")
|
||||||
|
}
|
||||||
|
|
||||||
// 显示关于对话框
|
// 显示关于对话框
|
||||||
@objc private func showAbout() {
|
@objc private func showAbout() {
|
||||||
|
let currentInterval = priceManager.getCurrentRefreshInterval()
|
||||||
|
|
||||||
|
// 获取应用版本信息
|
||||||
|
let version = getAppVersion()
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "BTC价格监控器"
|
alert.messageText = "BTC价格监控器 v\(version)"
|
||||||
alert.informativeText = """
|
alert.informativeText = """
|
||||||
🚀 一个专业的macOS菜单栏应用,用于实时显示BTC价格
|
🚀 一款 macOS 原生菜单栏应用,用于实时显示BTC价格
|
||||||
|
|
||||||
✨ 功能特性:
|
✨ 功能特性:
|
||||||
• 实时显示BTC/USDT价格
|
• 实时显示BTC/USDT价格
|
||||||
• 每30秒自动刷新
|
• 可配置刷新间隔(当前:\(currentInterval.displayText))
|
||||||
• 支持手动刷新 (Cmd+R)
|
• 支持手动刷新 (Cmd+R)
|
||||||
• 智能错误重试机制
|
• 智能错误重试机制
|
||||||
• 优雅的SF Symbols图标
|
• 优雅的SF Symbols图标
|
||||||
|
|
||||||
📊 技术信息:
|
|
||||||
数据来源:币安官方API
|
|
||||||
作者:张雷
|
|
||||||
版本:1.0.0
|
|
||||||
架构:SwiftUI + AppKit
|
|
||||||
"""
|
"""
|
||||||
alert.alertStyle = .informational
|
alert.alertStyle = .informational
|
||||||
alert.addButton(withTitle: "确定")
|
alert.addButton(withTitle: "确定")
|
||||||
alert.runModal()
|
alert.runModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开GitHub页面
|
||||||
|
@objc private func checkForUpdates() {
|
||||||
|
let githubURL = "https://github.com/jiayouzl/Bitcoin-Monitoring"
|
||||||
|
|
||||||
|
// 确保URL有效
|
||||||
|
guard let url = URL(string: githubURL) else {
|
||||||
|
print("❌ 无效的URL: \(githubURL)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用默认浏览器打开URL
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
|
||||||
|
print("✅ 已在浏览器中打开GitHub页面: \(githubURL)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取应用版本信息
|
||||||
|
/// - Returns: 版本号字符串,格式为 "主版本号.次版本号.修订号"
|
||||||
|
private func getAppVersion() -> String {
|
||||||
|
guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
|
||||||
|
return "未知版本"
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
// 退出应用
|
// 退出应用
|
||||||
@objc private func quitApp() {
|
@objc private func quitApp() {
|
||||||
NSApplication.shared.terminate(nil)
|
NSApplication.shared.terminate(nil)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// BTCPriceResponse.swift
|
// BTCPriceResponse.swift
|
||||||
// test1
|
// test1
|
||||||
//
|
//
|
||||||
// Created by zl_vm on 2025/10/28.
|
// Created by Mark on 2025/10/28.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -11,4 +11,4 @@ import Foundation
|
|||||||
struct BTCPriceResponse: Codable {
|
struct BTCPriceResponse: Codable {
|
||||||
let symbol: String
|
let symbol: String
|
||||||
let price: String
|
let price: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// test1
|
|
||||||
//
|
|
||||||
// Created by zl_vm on 2025/10/28.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Image(systemName: "globe")
|
|
||||||
.imageScale(.large)
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
Text("Hello, world!")
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// PriceManager.swift
|
// PriceManager.swift
|
||||||
// test1
|
// test1
|
||||||
//
|
//
|
||||||
// Created by zl_vm on 2025/10/28.
|
// Created by Mark on 2025/10/28.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -17,7 +17,7 @@ class PriceManager: ObservableObject {
|
|||||||
|
|
||||||
private let priceService = PriceService()
|
private let priceService = PriceService()
|
||||||
private var timer: Timer?
|
private var timer: Timer?
|
||||||
private let refreshInterval: TimeInterval = 30.0 // 30秒刷新一次
|
private var currentRefreshInterval: TimeInterval = 30.0 // 当前刷新间隔
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
startPriceUpdates()
|
startPriceUpdates()
|
||||||
@@ -31,28 +31,48 @@ class PriceManager: ObservableObject {
|
|||||||
|
|
||||||
// 开始定时更新价格
|
// 开始定时更新价格
|
||||||
func startPriceUpdates() {
|
func startPriceUpdates() {
|
||||||
|
#if DEBUG
|
||||||
|
print("⏰ [BTC Price Manager] 启动定时器,刷新间隔: \(Int(currentRefreshInterval))秒")
|
||||||
|
#endif
|
||||||
|
|
||||||
// 立即获取一次价格
|
// 立即获取一次价格
|
||||||
Task {
|
Task {
|
||||||
await fetchPrice()
|
await fetchPrice()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置定时器,使用weak self避免循环引用
|
// 设置定时器,使用weak self避免循环引用
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { [weak self] _ in
|
timer = Timer.scheduledTimer(withTimeInterval: currentRefreshInterval, repeats: true) { [weak self] _ in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
await self?.fetchPrice()
|
await self?.fetchPrice()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
print("✅ [BTC Price Manager] 定时器启动成功")
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止价格更新
|
// 停止价格更新
|
||||||
@MainActor
|
@MainActor
|
||||||
func stopPriceUpdates() {
|
func stopPriceUpdates() {
|
||||||
|
#if DEBUG
|
||||||
|
print("⏹️ [BTC Price Manager] 停止定时器")
|
||||||
|
#endif
|
||||||
|
|
||||||
timer?.invalidate()
|
timer?.invalidate()
|
||||||
timer = nil
|
timer = nil
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
print("✅ [BTC Price Manager] 定时器已停止")
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动刷新价格
|
// 手动刷新价格
|
||||||
func refreshPrice() async {
|
func refreshPrice() async {
|
||||||
|
#if DEBUG
|
||||||
|
print("🔄 [BTC Price Manager] 用户手动刷新价格")
|
||||||
|
#endif
|
||||||
|
|
||||||
await fetchPrice()
|
await fetchPrice()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,15 +81,35 @@ class PriceManager: ObservableObject {
|
|||||||
isFetching = true
|
isFetching = true
|
||||||
lastError = nil
|
lastError = nil
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
print("🔄 [BTC Price Manager] 开始获取价格...")
|
||||||
|
#endif
|
||||||
|
|
||||||
// 重试最多3次
|
// 重试最多3次
|
||||||
let maxRetries = 3
|
let maxRetries = 3
|
||||||
|
|
||||||
for attempt in 1...maxRetries {
|
for attempt in 1...maxRetries {
|
||||||
|
#if DEBUG
|
||||||
|
print("📡 [BTC Price Manager] 尝试获取价格 (第\(attempt)次)")
|
||||||
|
#endif
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let price = try await priceService.fetchBTCPrice()
|
let price = try await priceService.fetchBTCPrice()
|
||||||
currentPrice = price
|
currentPrice = price
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.timeStyle = .medium
|
||||||
|
let currentTime = formatter.string(from: Date())
|
||||||
|
print("✅ [BTC Price Manager] 价格更新成功: $\(String(format: "%.2f", price)) | 时间: \(currentTime)")
|
||||||
|
#endif
|
||||||
|
|
||||||
break // 成功获取价格,退出重试循环
|
break // 成功获取价格,退出重试循环
|
||||||
} catch let error as PriceError {
|
} catch let error as PriceError {
|
||||||
|
#if DEBUG
|
||||||
|
print("❌ [BTC Price Manager] 价格获取失败 (第\(attempt)次): \(error.localizedDescription)")
|
||||||
|
#endif
|
||||||
|
|
||||||
if attempt == maxRetries {
|
if attempt == maxRetries {
|
||||||
lastError = error
|
lastError = error
|
||||||
} else {
|
} else {
|
||||||
@@ -77,6 +117,10 @@ class PriceManager: ObservableObject {
|
|||||||
try? await Task.sleep(nanoseconds: UInt64(attempt * 1_000_000_000)) // 递增延迟
|
try? await Task.sleep(nanoseconds: UInt64(attempt * 1_000_000_000)) // 递增延迟
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
#if DEBUG
|
||||||
|
print("❌ [BTC Price Manager] 网络错误 (第\(attempt)次): \(error.localizedDescription)")
|
||||||
|
#endif
|
||||||
|
|
||||||
if attempt == maxRetries {
|
if attempt == maxRetries {
|
||||||
lastError = .networkError(error)
|
lastError = .networkError(error)
|
||||||
} else {
|
} else {
|
||||||
@@ -86,6 +130,14 @@ class PriceManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isFetching = false
|
isFetching = false
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if let error = lastError {
|
||||||
|
print("⚠️ [BTC Price Manager] 价格获取流程结束,最终失败: \(error.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
print("✅ [BTC Price Manager] 价格获取流程结束,成功")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化价格显示
|
// 格式化价格显示
|
||||||
@@ -109,4 +161,34 @@ class PriceManager: ObservableObject {
|
|||||||
var errorMessage: String? {
|
var errorMessage: String? {
|
||||||
return lastError?.localizedDescription
|
return lastError?.localizedDescription
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// MARK: - Refresh Interval Configuration
|
||||||
|
|
||||||
|
/// 更新刷新间隔
|
||||||
|
/// - Parameter interval: 新的刷新间隔
|
||||||
|
func updateRefreshInterval(_ interval: RefreshInterval) {
|
||||||
|
let oldInterval = RefreshInterval.allCases.first { $0.rawValue == currentRefreshInterval }?.displayText ?? "未知"
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
print("⏱️ [BTC Price Manager] 刷新间隔变更: \(oldInterval) → \(interval.displayText)")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
currentRefreshInterval = interval.rawValue
|
||||||
|
|
||||||
|
// 如果定时器正在运行,重启它以应用新的间隔
|
||||||
|
if timer != nil {
|
||||||
|
#if DEBUG
|
||||||
|
print("🔄 [BTC Price Manager] 重启定时器以应用新的刷新间隔")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
stopPriceUpdates()
|
||||||
|
startPriceUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前刷新间隔
|
||||||
|
/// - Returns: 当前的RefreshInterval枚举值
|
||||||
|
func getCurrentRefreshInterval() -> RefreshInterval {
|
||||||
|
return RefreshInterval.allCases.first { $0.rawValue == currentRefreshInterval } ?? .thirtySeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// PriceService.swift
|
// PriceService.swift
|
||||||
// test1
|
// test1
|
||||||
//
|
//
|
||||||
// Created by zl_vm on 2025/10/28.
|
// Created by Mark on 2025/10/28.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -66,4 +66,4 @@ enum PriceError: Error, LocalizedError {
|
|||||||
return "网络错误:\(error.localizedDescription)"
|
return "网络错误:\(error.localizedDescription)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
test1/RefreshInterval.swift
Normal file
39
test1/RefreshInterval.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// RefreshInterval.swift
|
||||||
|
// Bitcoin Monitoring
|
||||||
|
//
|
||||||
|
// Created by Mark on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 刷新间隔选项枚举
|
||||||
|
/// 定义用户可以选择的价格刷新间隔
|
||||||
|
enum RefreshInterval: Double, CaseIterable, Codable {
|
||||||
|
case fiveSeconds = 5
|
||||||
|
case tenSeconds = 10
|
||||||
|
case thirtySeconds = 30
|
||||||
|
case sixtySeconds = 60
|
||||||
|
|
||||||
|
/// 获取刷新间隔的显示文本
|
||||||
|
/// - Returns: 用于在菜单中显示的中文文本
|
||||||
|
var displayText: String {
|
||||||
|
switch self {
|
||||||
|
case .fiveSeconds:
|
||||||
|
return "5秒"
|
||||||
|
case .tenSeconds:
|
||||||
|
return "10秒"
|
||||||
|
case .thirtySeconds:
|
||||||
|
return "30秒"
|
||||||
|
case .sixtySeconds:
|
||||||
|
return "60秒"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取包含当前标记的显示文本
|
||||||
|
/// - Parameter isCurrent: 是否为当前选中的间隔
|
||||||
|
/// - Returns: 带有当前标记的显示文本
|
||||||
|
func displayTextWithMark(isCurrent: Bool) -> String {
|
||||||
|
return isCurrent ? "✓ \(displayText)" : " \(displayText)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.network.client</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<false/>
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// test1App.swift
|
// test1App.swift
|
||||||
// test1
|
// test1
|
||||||
//
|
//
|
||||||
// Created by zl_vm on 2025/10/28.
|
// Created by Mark on 2025/10/28.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|||||||
Reference in New Issue
Block a user