[英]completionHandler in Swift4 return String
我正在嘗試構建一個小型貨幣轉換器,問題是我的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()
使用完成處理程序,我還添加了最小錯誤處理以避免崩潰
//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)
}
}
}
完成處理程序的基本思想是,您有一些異步方法(即稍后完成的方法),並且您需要給調用者提供機會,讓其提供異步方法完成后要執行的操作。 因此,假設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
例程中 這使您的代碼很脆弱,如果更改貨幣的順序,添加/刪除貨幣等,則很容易出錯。最好將一個貨幣列表放在一個位置,以編程方式在viewDidLoad
更新UISegmentedControl
出口,然后然后讓您的例程全部參考允許使用哪種貨幣的單一數組。
您應該避免使用!
強制解包運算符。 例如,如果網絡請求失敗,然后您引用result.value!
,您的應用將崩潰。 您想妥善處理無法控制的錯誤。
如果要設置貨幣格式,請記住,除了貨幣符號外,還應考慮並非所有語言環境都使用.
小數點后一位(例如,您的歐洲用戶可以使用,
)。 因此,我們通常使用NumberFormatter
將計算出的數字轉換回字符串。
下面,我只是將NumberFormatter
用於輸出,但是在解釋用戶的輸入時,您也確實應該使用它。 但我會將其留給讀者。
處理貨幣時,除了貨幣符號之外,還有一個更微妙的要點,即結果應顯示多少個小數位。 (例如,使用日元交易時,通常沒有小數位,而歐元和美元則有兩個小數位。)
您可以根據需要編寫自己的轉換例程,但是我可以將所選的貨幣代碼與Locale
標識符相關聯,這樣您就可以利用該符號和每種貨幣適用的小數位數。 然后使用NumberFormatter
格式化數字的字符串表示形式。
插座名稱的約定通常是一些功能名稱,后跟控件的類型。 例如,你可能有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))
}
}
}
}
如上面的注釋所示,我可能會將擴展中的某些內容移到了不同的對象中,但我懷疑即使是一次也要接受上述更改,所以我在這里停止了重構。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.