Swift4中的completionHandler返回字符串
Posted
技术标签:
【中文标题】Swift4中的completionHandler返回字符串【英文标题】:completionHandler in Swift4 return String 【发布时间】:2019-05-19 18:07:02 【问题描述】:我正在尝试构建一个小型货币转换器,但问题是我的 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 函数都会被调用,并且变量被设置为正确的值。我是初学者,因此我们将不胜感激。
【问题讨论】:
您的assignInput
仅在 1 个条件下而不是在所有条件下调用 completionHandler
。
你做过调试吗?怎么了?有崩溃日志吗?哪些代码被卡住了?您是否在某些行打印了值?添加断点检查代码是否运行?
我知道...我没有继续,因为 Xcode 抛出以下错误:无法将类型“字符串”的值转换为预期的参数类型“[字符串:任意]”
停止编辑问题中的代码,Jakob。您使以前的 cmets 无效。另外,即使您的文字所指的方法assignInput
也不再有效。如果你想用额外的代码来补充你的问题,那很好,但不要以使 cmets 和 questions 无效的方式编辑你的问题。归根结底,澄清问题很好,但改变它们不是。
对不起。我的错。下次学到新东西。
【参考方案1】:
assignOutput()
中需要一个完成处理程序,我还添加了最小错误处理以避免崩溃
//Variables
var inputCurrency = ""
var currencyCNY = 0.0
var currencyEUR = 0.0
var currencyGBP = 0.0
var currencyJPY = 0.0
@IBAction func btnConvert(_ sender: Any)
assignOutput() success in
if success
if txtValue.text!.isEmpty
self.lblCurrency.text = "Please insert value"
else
if 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)"
else
self.lblCurrency.text = "Please enter a number"
else
self.lblCurrency.text = "Could not receive the exchange rates"
func assignOutput(completion: @escaping (Bool) -> Void)
let currencies = ["EUR", "JPY", "CNY", "USD"]
inputCurrency = currencies[segInputCurrency.selectedSegmentIndex]
Alamofire.request("https://api.exchangeratesapi.io/latest?base=\(inputCurrency)").responseJSON (response) in
if let result = response.result.value
let jsonCurrencies = JSON(result)
let dictContent = jsonCurrencies["rates"]
self.currencyCNY = dictContent["CNY"].double
self.currencyEUR = dictContent["EUR"].double
self.currencyGBP = dictContent["GBP"].double
self.currencyJPY = dictContent["JPY"].double
completion(true)
else
completion(false)
【讨论】:
谢谢。我编辑了代码。但是,我不太清楚为什么我需要一个完成处理程序以及如何做到这一点...... 我更新了答案。如果有像 Alamofire 请求这样的异步任务,你需要一个完成处理程序。 @Jakob 为具有名称、值、符号属性的货币创建结构。然后创建货币对象数组 @vadian 不幸的是,Xcode 现在抛出以下错误:Expected parameter type following ':' @RajeshKumarR 我将在一个单独的文件中尝试这个......仍在学习基础知识【参考方案2】:完成处理程序的基本思想是您有一些异步方法(即稍后完成的方法),并且您需要让调用者有机会在异步方法完成后提供它希望异步方法执行的操作。因此,鉴于 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
例程中
这会使您的代码变得脆弱,如果您更改货币顺序、添加/删除货币等,很容易出错。最好在一个地方有一个货币列表,以编程方式更新您的 @987654336 @ outlets in 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))
如上面的 cmets 所示,我可能会将扩展中的一些内容移动到不同的对象中,但我怀疑即使是上述更改也有点需要一次接受,所以我已经停止了我的在那里重构。
【讨论】:
非常感谢!这太详细了,我需要一些时间才能完全理解所有信息。这超出了我的预期!以上是关于Swift4中的completionHandler返回字符串的主要内容,如果未能解决你的问题,请参考以下文章
Swift 4 将参数添加到 URLRequest 完成处理程序
返回方法中的 sendAsynchronousRequest 和 completionHandler
session.dataTask 调用错误中的 Swift5 额外参数“completionHandler”