SwiftUI按钮点击调用异步函数无响应问题求助
我一直在Xcode Playgrounds中学习Swift,已成功编写调用获取最新汇率API的代码,使用async和await确保API调用完成后再进行转换。现在我正在开发SwiftUI版本的基础APP,接收用户输入的GBP金额,通过API转换为AED并显示结果。但运行APP输入数值点击Convert按钮后无任何反应,怀疑是按钮Action中的Task调用存在问题或其他错误,附上代码寻求帮助及相关设计模式建议。
现有代码片段
Exchange.swift 模块
// // Exchange.swift // CurrencyConvertorApp // // Created by XXXXXX on 05/03/2023. // import Foundation // MARK: - Information struct Information: Codable { let success: Bool let query: Query let info: Info let date: String let result: Double } // MARK: - Info struct Info: Codable { let timestamp: Int let rate: Double } // MARK: - Query struct Query: Codable { let from, to: String let amount: Int } // MARK: - Code let url = "https://api.apilayer.com/exchangerates_data/convert?to=AED&from=GBP&amount=1" var request = URLRequest(url: URL(string: url)!,timeoutInterval: Double.infinity) func setUpRequest() { request.httpMethod = "GET" request.addValue("MY KEY REMOVED", forHTTPHeaderField: "apikey") } func getExchangeValue(from request: URLRequest, to: String, from: String, amount: Double) async -> Double? { do { let (data, _) = try await URLSession.shared.data(for: request) let information = try JSONDecoder().decode(Information.self, from: data) return amount * information.result } catch let error { print(error) return nil } } func buttonPressConvert (valueToConvert: Double) async -> Double? { let exchange_value = await getExchangeValue(from: request, to:"GBP", from:"AED", amount: valueToConvert ) return exchange_value }
ContentView.swift
// // ContentView.swift // CurrencyConvertorApp // // Created by XXXXXXXXX on 05/03/2023. // import SwiftUI struct ContentView: View { @State private var GBPvalue = "" @State private var myString = "" @State private var myDouble = 0.0 var body: some View { VStack { Text("Currentcy Convertor App Version 0.1") .padding() Text("Beta Version") TextField("Enter GBP value here", text: $GBPvalue) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.center) .padding() Button("Convert") { Task { let myDouble = Double(GBPvalue) let exchangeResult = await getExchangeValue(from: request, to:"GBP", from:"AED", amount: myDouble ?? 0.0 ) let myString = String(exchangeResult ?? 0.0) } } Text(myString) .foregroundColor(.green) .font(.system(size: 60)) .border(.green) .padding() } .padding() .onAppear() { setUpRequest() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
问题排查与修复方案
你的代码里有几个关键问题导致点击按钮后无反应:
1. 局部变量覆盖@State变量
按钮的Task里重新定义了myDouble和myString,这两个是局部变量,根本没给视图绑定的@State变量赋值,所以UI完全不会更新。必须直接给@State变量赋值,去掉局部变量的let声明:
Button("Convert") { Task { if let inputAmount = Double(GBPvalue) { let exchangeResult = await getExchangeValue(from: request, to:"AED", from:"GBP", amount: inputAmount ) myString = String(exchangeResult ?? 0.0) } else { myString = "无效金额" } } }
2. API请求参数完全错误
你定义的全局request用的是固定URL(to=AED&from=GBP&amount=1),但调用getExchangeValue时传的是to:"GBP", from:"AED",参数完全搞反了,而且不管用户输入多少金额,请求的都是固定amount=1的接口,返回结果自然不对。必须动态生成请求URL,不要用全局的request:
// 删掉全局的url和request,修改getExchangeValue函数 func getExchangeValue(to: String, from: String, amount: Double) async -> Double? { guard let url = URL(string: "https://api.apilayer.com/exchangerates_data/convert?to=\(to)&from=\(from)&amount=\(amount)") else { print("无效URL") return nil } var request = URLRequest(url: url, timeoutInterval: Double.infinity) request.httpMethod = "GET" request.addValue("MY KEY REMOVED", forHTTPHeaderField: "apikey") do { let (data, _) = try await URLSession.shared.data(for: request) let information = try JSONDecoder().decode(Information.self, from: data) return information.result } catch let error { print(error) return nil } }
3. 全局变量引发的状态问题
全局的request和url很容易导致状态混乱,比如setUpRequest在onAppear调用,但如果多次创建视图,全局变量会被重复修改。尽量避免全局变量,把请求逻辑封装成局部或者类方法。
设计模式建议
为了让代码更易维护和扩展,推荐使用以下模式:
1. MVVM架构
把API调用、数据处理逻辑放到ViewModel里,ContentView只负责UI展示和用户交互:
- 创建
CurrencyConverterViewModel类,用@Published标记需要更新UI的属性 - ViewModel里封装转换方法,处理异步请求和错误
- ContentView用
@StateObject持有ViewModel,监听状态变化
2. 网络服务封装
把汇率API请求封装成单独的ExchangeService类,负责所有网络相关操作,方便复用和单元测试:
- 服务类里包含API密钥、请求构建、数据解析逻辑
- ViewModel依赖这个服务类,而不是直接写请求代码
3. 状态管理优化
在ViewModel里增加加载状态、错误提示等属性,让UI能反馈当前操作状态,比如显示加载动画、错误提示文本。
优化后的完整代码示例
ExchangeService.swift
import Foundation struct Information: Codable { let success: Bool let query: Query let info: Info let date: String let result: Double } struct Info: Codable { let timestamp: Int let rate: Double } struct Query: Codable { let from, to: String let amount: Int } class ExchangeService { private let apiKey = "MY KEY REMOVED" func convertCurrency(from: String, to: String, amount: Double) async -> Double? { guard let url = URL(string: "https://api.apilayer.com/exchangerates_data/convert?to=\(to)&from=\(from)&amount=\(amount)") else { print("无效URL") return nil } var request = URLRequest(url: url, timeoutInterval: Double.infinity) request.httpMethod = "GET" request.addValue(apiKey, forHTTPHeaderField: "apikey") do { let (data, _) = try await URLSession.shared.data(for: request) let information = try JSONDecoder().decode(Information.self, from: data) return information.result } catch { print("API调用错误: \(error)") return nil } } }
CurrencyConverterViewModel.swift
import Foundation class CurrencyConverterViewModel: ObservableObject { @Published var conversionResult: String = "" @Published var isLoading: Bool = false private let exchangeService = ExchangeService() func convert(gbpAmount: String) { guard let amount = Double(gbpAmount), amount > 0 else { conversionResult = "请输入有效金额" return } isLoading = true Task { defer { isLoading = false } if let result = await exchangeService.convertCurrency(from: "GBP", to: "AED", amount: amount) { conversionResult = String(format: "%.2f", result) } else { conversionResult = "转换失败" } } } }
ContentView.swift
import SwiftUI struct ContentView: View { @State private var gbpValue = "" @StateObject private var viewModel = CurrencyConverterViewModel() var body: some View { VStack(spacing: 20) { Text("货币转换APP v0.1") .font(.title) .padding() Text("Beta版本") .font(.subheadline) .foregroundColor(.gray) TextField("输入GBP金额", text: $gbpValue) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.center) .keyboardType(.decimalPad) Button("转换") { viewModel.convert(gbpAmount: gbpValue) } .disabled(viewModel.isLoading) .padding() if viewModel.isLoading { ProgressView() } else { Text(viewModel.conversionResult) .foregroundColor(.green) .font(.system(size: 60)) .border(.green) .padding() } } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
内容的提问来源于stack exchange,提问作者Kapi

