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 标识符相关联,这样您就可以利用符号和适用于每种货币的小数位数。我会使用NumberFormatters 格式化数字的字符串表示形式。

    插座名称的约定通常是一些功能名称,后跟控件类型。例如。你可能有inputTextFieldcurrencyTextFieldoutputLabelconvertedLabel。同样,我可能会将@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 完成处理程序

swift 4中的完成处理程序

返回方法中的 sendAsynchronousRequest 和 completionHandler

session.dataTask 调用错误中的 Swift5 额外参数“completionHandler”

在 NSURLConnection 中的 Swift 中从 completionHandler 中获取数据

textFieldShouldReturn - completionHandler 中的某些方法永远不会被调用