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

SwiftUI按钮点击调用异步函数无响应问题求助

汇率转换APP无响应问题排查与优化建议

我一直在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 03:25:13