我正在尝试构建一个小型的货币转换器,问题是我的completionHandler不工作。因此,函数执行后,输入货币不会立即更改
我已经尝试过实现completionHandler;但是,还没有成功
class CurrencyExchange: ViewController {
//Outlets
@IBOutlet weak var lblCurrency: UILabel!
@IBOutlet weak var segOutputCurrency: UISegmentedControl!
@IBOutlet weak var txtValue: UITextField!
@IBOutlet weak var segInputCurrency: UISegmentedControl!
//Variables
var inputCurrency: String!
var currencyCNY: Double!
var currencyEUR: Double!
var currencyGBP: Double!
var currencyJPY: Double!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.isNavigationBarHidden = true
}
@IBAction func btnConvert(_ sender: Any) {
assignOutput()
if txtValue.text == "" {
self.lblCurrency.text = "Please insert value"
} else {
let inputValue = Double(txtValue.text!)!
if segOutputCurrency.selectedSegmentIndex == 0 {
let output = Double(inputValue * currencyCNY!)
self.lblCurrency.text = "\(output)¥"
} else if segOutputCurrency.selectedSegmentIndex == 1 {
let output = Double(inputValue * currencyEUR!)
self.lblCurrency.text = "\(output)€"
} else if segOutputCurrency.selectedSegmentIndex == 2 {
let output = Double(inputValue * currencyGBP!)
self.lblCurrency.text = "\(output)"
} else if segOutputCurrency.selectedSegmentIndex == 3 {
let output = Double(inputValue * currencyJPY!)
self.lblCurrency.text = "\(output)"
}
}
}
func assignOutput() {
let currencies = ["EUR", "JPY", "CNY", "USD"]
inputCurrency = currencies[segInputCurrency.selectedSegmentIndex]
Alamofire.request("https://api.exchangeratesapi.io/latest?base=\(inputCurrency!)").responseJSON { (response) in
let result = response.result
let jsonCurrencies = JSON(result.value!)
let dictContent = jsonCurrencies["rates"]
self.currencyCNY = dictContent["CNY"].double
self.currencyEUR = dictContent["EUR"].double
self.currencyGBP = dictContent["GBP"].double
self.currencyJPY = dictContent["JPY"].double
}
}
}
预期的结果是,每次调用btnConvert函数时,都会调用assignInput和assignOutput函数,并将变量设置为正确的值。我是一个初学者,所以任何帮助都会非常感谢。
最佳答案
完成处理程序的基本思想是,您有一些异步方法(即,稍后完成的方法),并且您需要给调用方机会,在异步方法完成时提供它希望异步方法执行的操作。因此,假设assignOutput
是异步方法,那么您将使用一个完成处理程序转义闭包重构该方法。
就我个人而言,我会将这个转义闭包配置为返回Result
类型:
例如:
func assignOutput(completion: @escaping (Result<[String: Double]>) -> Void) {
let inputCurrency = ...
Alamofire.request("https://api.exchangeratesapi.io/latest?base=\(inputCurrency)").responseJSON { response in
switch response.result {
case .failure(let error):
completion(.failure(error))
case .success(let value):
let jsonCurrencies = JSON(value)
guard let dictionary = jsonCurrencies["rates"].dictionaryObject as? [String: Double] else {
completion(.failure(CurrencyExchangeError.currencyNotFound)) // this is just a custom `Error` type that I’ve defined
return
}
completion(.success(dictionary))
}
}
}
然后你可以这样使用它:
assignOutput { result in
switch result {
case .failure(let error):
print(error)
case .success(let dictionary):
print(dictionary)
}
}
通过使用
Result
类型,您可以在代码中检查.failure
或.success
的良好一致性模式。话虽如此,我还是建议其他一些改进:
我不会从另一个视图控制器
ViewController
创建这个视图控制器子类。它应该是UIViewController
的子类。(从技术上讲,您可以重新对自己的自定义视图控制器子类进行子类划分,但这种情况非常少见。坦率地说,当视图控制器子类中有太多子类时,可能是代码味道表明视图控制器中有太多子类。)
我会给这个视图控制器一个类名,明确地指明对象的类型,例如
CurrencyExchangeViewController
,而不仅仅是CurrencyExchange
。当你开始把这些大的视图控制器分解成一些更容易管理的东西时,这个习惯将在未来带来回报。您在四个不同的地方有接受的货币列表:
在你的故事板中
segOutputCurrency
在你的故事板中
segInputCurrency
在你的
btnConvert
程序中在你的
assignOutput
程序中这会使代码变得脆弱,如果您更改货币顺序、添加/删除货币等,则很容易出错。最好将货币列表放在一个位置,以编程方式更新
UISegmentedControl
中的viewDidLoad
销售点,然后让您的例程都引用允许使用的货币的单个数组。您应该避免使用
!
强制展开运算符。例如,如果网络请求失败,然后引用result.value!
,则应用程序将崩溃。你想优雅地处理发生在你控制之外的错误。如果要格式化货币,请记住,除了货币符号之外,还应该考虑并非所有地区都使用
.
来表示小数点(例如,您的欧洲用户可能使用,
)。因此,我们通常使用NumberFormatter
将计算出的数字转换回字符串。下面,我只使用了
NumberFormatter
作为输出,但是在解释用户的输入时也应该使用它。但我会留给读者的。在处理货币时有一个更微妙的点,在货币符号之上和之外,即结果应该显示多少个小数位。(例如,在处理日元时,通常没有小数点,而欧元和美元则有两个小数点。)
如果需要,您可以编写自己的转换例程,但我可能会将所选的货币代码与
Locale
标识符相关联,这样您就可以利用适合于每种货币的符号和小数位数。我将使用NumberFormatter
s格式化数字的字符串表示形式。出口名称的约定通常是一些函数名,后面跟着控件类型。例如,您可能有
inputTextField
或currencyTextField
和outputLabel
或convertedLabel
。同样,我可以将@IBAction
重命名为didTapConvertButton(_:)
我个人不喜欢使用SwiftyJSON,尽管有这个名字,但我还是觉得它不太适合我。我会用
JSONDecoder
。综合起来,你可能会得出这样的结论:
// CurrencyViewController.swift
import UIKit
import Alamofire
// types used by this view controller
struct Currency {
let code: String // standard three character code
let localeIdentifier: String // a `Locale` identifier string used to determine how to format the results
}
enum CurrencyExchangeError: Error {
case currencyNotSupplied
case valueNotSupplied
case currencyNotFound
case webServiceError(String)
case unknownNetworkError(Data?, HTTPURLResponse?)
}
struct ExchangeRateResponse: Codable {
let error: String?
let base: String?
let rates: [String: Double]?
}
class CurrencyExchangeViewController: UIViewController {
// outlets
@IBOutlet weak var inputTextField: UITextField!
@IBOutlet weak var inputCurrencySegmentedControl: UISegmentedControl!
@IBOutlet weak var outputCurrencySegmentedControl: UISegmentedControl!
@IBOutlet weak var resultLabel: UILabel!
// private properties
private let currencies = [
Currency(code: "EUR", localeIdentifier: "fr_FR"),
Currency(code: "JPY", localeIdentifier: "jp_JP"),
Currency(code: "CNY", localeIdentifier: "ch_CH"),
Currency(code: "USD", localeIdentifier: "en_US")
]
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.isNavigationBarHidden = true
updateCurrencyControls()
}
@IBAction func didTapConvertButton(_ sender: Any) {
let inputIndex = inputCurrencySegmentedControl.selectedSegmentIndex
let outputIndex = outputCurrencySegmentedControl.selectedSegmentIndex
guard inputIndex >= 0, outputIndex >= 0 else {
resultLabel.text = errorMessage(for: CurrencyExchangeError.currencyNotSupplied)
return
}
guard let text = inputTextField.text, let value = Double(text) else {
resultLabel.text = errorMessage(for: CurrencyExchangeError.valueNotSupplied)
return
}
performConversion(from: inputIndex, to: outputIndex, of: value) { result in
switch result {
case .failure(let error):
self.resultLabel.text = self.errorMessage(for: error)
case .success(let string):
self.resultLabel.text = string
}
}
}
func updateCurrencyControls() {
outputCurrencySegmentedControl.removeAllSegments()
inputCurrencySegmentedControl.removeAllSegments()
enumerateCurrencies { index, code in
outputCurrencySegmentedControl.insertSegment(withTitle: code, at: index, animated: false)
inputCurrencySegmentedControl.insertSegment(withTitle: code, at: index, animated: false)
}
}
}
// these might better belong in a presenter or view model rather than the view controller
private extension CurrencyExchangeViewController {
func enumerateCurrencies(block: (Int, String) -> Void) {
for (index, currency) in currencies.enumerated() {
block(index, currency.code)
}
}
func errorMessage(for error: Error) -> String {
switch error {
case CurrencyExchangeError.currencyNotFound:
return NSLocalizedString("No exchange rate found for those currencies.", comment: "Error")
case CurrencyExchangeError.unknownNetworkError:
return NSLocalizedString("Unknown error occurred.", comment: "Error")
case CurrencyExchangeError.currencyNotSupplied:
return NSLocalizedString("You must indicate the desired currencies.", comment: "Error")
case CurrencyExchangeError.valueNotSupplied:
return NSLocalizedString("No value to convert has been supplied.", comment: "Error")
case CurrencyExchangeError.webServiceError(let message):
return NSLocalizedString(message, comment: "Error")
case let error as NSError where error.domain == NSURLErrorDomain:
return NSLocalizedString("There was a network error.", comment: "Error")
case is DecodingError:
return NSLocalizedString("There was a problem parsing the server response.", comment: "Error")
default:
return error.localizedDescription
}
}
func performConversion(from fromIndex: Int, to toIndex: Int, of value: Double, completion: @escaping (Result<String?>) -> Void) {
let originalCurrency = currencies[fromIndex]
let outputCurrency = currencies[toIndex]
fetchExchangeRates(for: originalCurrency.code) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let exchangeRates):
guard let exchangeRate = exchangeRates.rates?[outputCurrency.code] else {
completion(.failure(CurrencyExchangeError.currencyNotFound))
return
}
let outputValue = value * exchangeRate
let locale = Locale(identifier: outputCurrency.localeIdentifier)
let string = formatter(for: locale).string(for: outputValue)
completion(.success(string))
}
}
/// Currency formatter for specified locale.
///
/// Note, this formats number using the current locale (e.g. still uses
/// your local grouping and decimal separator), but gets the appropriate
/// properties for the target locale's currency, namely:
///
/// - the currency symbol, and
/// - the number of decimal places.
///
/// - Parameter locale: The `Locale` from which we'll use to get the currency-specific properties.
/// - Returns: A `NumberFormatter` that melds the current device's number formatting and
/// the specified locale's currency formatting.
func formatter(for locale: Locale) -> NumberFormatter {
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
currencyFormatter.locale = locale
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currencyFormatter.currencyCode
formatter.currencySymbol = currencyFormatter.currencySymbol
formatter.internationalCurrencySymbol = currencyFormatter.internationalCurrencySymbol
formatter.maximumFractionDigits = currencyFormatter.maximumFractionDigits
formatter.minimumFractionDigits = currencyFormatter.minimumFractionDigits
return formatter
}
}
}
// this might better belong in a network service rather than in the view controller
private extension CurrencyExchangeViewController {
func fetchExchangeRates(for inputCurrencyCode: String, completion: @escaping (Result<ExchangeRateResponse>) -> Void) {
Alamofire.request("https://api.exchangeratesapi.io/latest?base=\(inputCurrencyCode)").response { response in
guard response.error == nil, let data = response.data else {
completion(.failure(response.error ?? CurrencyExchangeError.unknownNetworkError(response.data, response.response)))
return
}
do {
let exchangeRates = try JSONDecoder().decode(ExchangeRateResponse.self, from: data)
if let error = exchangeRates.error {
completion(.failure(CurrencyExchangeError.webServiceError(error)))
} else {
completion(.success(exchangeRates))
}
} catch {
completion(.failure(error))
}
}
}
}
如上所述,我可能会将扩展中的一些内容移动到不同的对象中,但我怀疑即使是上面的更改也有一点需要一次完成,所以我已经停止了重构。
关于swift - Swift4中的completeHandler返回字符串,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/56210626/