为啥我的 segue 不等到完成处理程序完成?

Posted

技术标签:

【中文标题】为啥我的 segue 不等到完成处理程序完成?【英文标题】:Why does my segue not wait until completion handler finished?为什么我的 segue 不等到完成处理程序完成? 【发布时间】:2018-09-23 21:06:44 【问题描述】:

我有一个基于页面的应用程序,使用 RootViewController、ModelViewController、DataViewController 和 SearchViewController。

在我的 searchViewController 中,我搜索一个项目,然后将该项目添加或删除到包含在 Manager 类(和 UserDefaults)中的数组中,modelViewController 使用它来实例化 DataViewController 的实例,并使用加载的正确信息数据对象。根据添加或删除项目,我使用 Bool 来确定使用了哪个 segue,addCoin 或 removeCoin,以便 RootViewController(PageView) 将显示数组中的最后一页(添加页面时)或第一个(删除时)。

一切正常,直到我遇到无法诊断的错误,问题是当我添加页面时,应用程序崩溃,给我一个“打开可选值时意外发现 nil”

这似乎是问题函数,在 searchViewController 'self.performSegue(withIdentifier: "addCoin"' 似乎被立即调用,即使没有调度:

@objc func addButtonAction(sender: UIButton!) 

    print("Button tapped")

    if Manager.shared.coins.contains(dataObject) 
        Duplicate()
     else if Manager.shared.coins.count == 5 
        max()
     else 
        Manager.shared.addCoin(coin: dataObject)

        CGPrices.shared.getData(arr: true, completion:  (success) in
            print(Manager.shared.coins)

            DispatchQueue.main.async 
                self.performSegue(withIdentifier: "addCoin", sender: self)
            
        )

    

    searchBar.text = ""

意思是在我的DataViewController中,这个函数会找到nil:

func getIndex() 
    let index = CGPrices.shared.coinData.index(where:  $0.id == dataObject )!
    dataIndex = index

我不知道为什么它不等待完成。

我也收到这个关于线程的错误:

[Assert] Cannot be called with asCopy = NO on non-main thread.

这就是为什么我尝试使用 dispatch que 进行 push segue

这是我的 searchViewController 完整代码:

import UIKit

class SearchViewController: UIViewController, UISearchBarDelegate 

    let selectionLabel = UILabel()
    let searchBar = UISearchBar()
    let addButton = UIButton()
    let removeButton = UIButton()

    var filteredObject: [String] = []
    var dataObject = ""

    var isSearching = false

    //Add Button Action.
    @objc func addButtonAction(sender: UIButton!) 

        print("Button tapped")

        if Manager.shared.coins.contains(dataObject) 
            Duplicate()
         else if Manager.shared.coins.count == 5 
            max()
         else 
            Manager.shared.addCoin(coin: dataObject)

            CGPrices.shared.getData(arr: true, completion:  (success) in
                print(Manager.shared.coins)

                DispatchQueue.main.async 
                    self.performSegue(withIdentifier: "addCoin", sender: self)
                
            )

        

        searchBar.text = ""
    

    //Remove button action.
    @objc func removeButtonActon(sender: UIButton!) 

        print("Button tapped")

        if Manager.shared.coins.contains(dataObject) 
            Duplicate()
         else if Manager.shared.coins.count == 5 
            max()
         else 
            Manager.shared.removeCoin(coin: dataObject)

            self.performSegue(withIdentifier: "addCoin", sender: self)
        

        searchBar.text = ""
    

    //Prepare for segue, pass removeCoinSegue Bool depending on remove or addCoin.
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) 

        if segue.identifier == "addCoin" 

            if let destinationVC = segue.destination as? RootViewController 
                destinationVC.addCoinSegue = true
            

         else if segue.identifier == "addCoin" 

            if let destinationVC = segue.destination as? RootViewController 
                destinationVC.addCoinSegue = false
            
        
    

    //Remove button action.
    @objc func removeButtonAction(sender: UIButton!) 

        if Manager.shared.coins.count == 1 
            removeAlert()
         else 
            Manager.shared.removeCoin(coin: dataObject)

            print(Manager.shared.coins)
            print(dataObject)

            searchBar.text = ""
            self.removeButton.isHidden = true

            DispatchQueue.main.async 
                self.performSegue(withIdentifier: "removeCoin", sender: self)
            
        
    

    //Search/Filter the struct from CGNames, display both the Symbol and the Name but use the ID as dataObject.
    func filterStructForSearchText(searchText: String, scope: String = "All") 

        if !searchText.isEmpty 
            isSearching = true

            filteredObject = CGNames.shared.coinNameData.filter 

                // if you need to search key and value and include partial matches
                // $0.key.contains(searchText) || $0.value.contains(searchText)

                // if you need to search caseInsensitively key and value and include partial matches
                $0.name.range(of: searchText, options: .caseInsensitive) != nil || $0.symbol.range(of: searchText, options: .caseInsensitive) != nil
                
                .map $0.id 

         else 
            isSearching = false
            print("NoText")
        
    

    //Running filter function when text changes.
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) 

        filterStructForSearchText(searchText: searchText)

        if isSearching == true && filteredObject.count > 0 

            addButton.isHidden = false
            dataObject = filteredObject[0]
            selectionLabel.text = dataObject

            if Manager.shared.coins.contains(dataObject) 
                removeButton.isHidden = false
                addButton.isHidden = true
             else 
                removeButton.isHidden = true
                addButton.isHidden = false
            

         else 
            addButton.isHidden = true
            removeButton.isHidden = true
            selectionLabel.text = "e.g. btc/bitcoin"
        

    

    override func viewDidLoad() 
        super.viewDidLoad()

        //Setup the UI.
        self.view.backgroundColor = .gray
        setupView()
    

    override func viewDidLayoutSubviews() 

    

    //Hide keyboard
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) 
        self.view.endEditing(true)
    

    //Alerts
    func removeAlert() 
        let alertController = UIAlertController(title: "Can't Remove", message: "\(dataObject) can't be deleted, add another to delete \(dataObject)", preferredStyle: .alert)

        alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))

        self.present(alertController, animated: true, completion: nil)
    

    func Duplicate() 
        let alertController = UIAlertController(title: "Duplicate", message: "\(dataObject) is already in your pages!", preferredStyle: .alert)

        alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))

        self.present(alertController, animated: true, completion: nil)
    

    func max() 
        let alertController = UIAlertController(title: "Maximum Reached", message: "\(dataObject) can't be added, you have reached the maximum of 5 coins. Please delete a coin to add another.", preferredStyle: .alert)

        alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))

        self.present(alertController, animated: true, completion: nil)
    

这里是 DataViewController

import UIKit

class DataViewController: UIViewController 

    @IBOutlet weak var dataLabel: UILabel!

    //Variables and Objects.

    //The dataObject carries the chosen cryptocurrencies ID from the CoinGecko API to use to get the correct data to load on each object.
    var dataObject = String()

    //The DefaultCurrency (gbp, eur...) chosen by the user.
    var defaultCurrency = ""

    //The Currency Unit taken from the exchange section of the API.
    var currencyUnit = CGExchange.shared.exchangeData[0].rates.gbp.unit
    var secondaryUnit = CGExchange.shared.exchangeData[0].rates.eur.unit
    var tertiaryUnit = CGExchange.shared.exchangeData[0].rates.usd.unit

    //Index of the dataObject
    var dataIndex = Int()

    //Objects
    let cryptoLabel = UILabel()
    let cryptoIconImage = UIImageView()
    let secondaryPriceLabel = UILabel()
    let mainPriceLabel = UILabel()
    let tertiaryPriceLabel = UILabel()

    //Custom Fonts.
    let customFont = UIFont(name: "AvenirNext-Heavy", size: UIFont.labelFontSize)
    let secondFont = UIFont(name: "AvenirNext-BoldItalic" , size: UIFont.labelFontSize)

    //Setup Functions

    //Get the index of the dataObject
    func getIndex() 
        let index = CGPrices.shared.coinData.index(where:  $0.id == dataObject )!
        dataIndex = index
    

    //Label
    func setupLabels() 

        //cryptoLabel from dataObject as name.
        cryptoLabel.text = CGPrices.shared.coinData[dataIndex].name

        //Prices from btc Exchange rate.

        let btcPrice = CGPrices.shared.coinData[dataIndex].current_price!
        let dcExchangeRate = CGExchange.shared.exchangeData[0].rates.gbp.value
        let secondaryExchangeRate = CGExchange.shared.exchangeData[0].rates.eur.value
        let tertiaryExchangeRate = CGExchange.shared.exchangeData[0].rates.usd.value

        let realPrice = (btcPrice * dcExchangeRate)
        let secondaryPrice = (btcPrice * secondaryExchangeRate)
        let tertiaryPrice = (btcPrice * tertiaryExchangeRate)

        secondaryPriceLabel.text = "\(secondaryUnit)\(String((round(1000 * secondaryPrice) / 1000)))"
        mainPriceLabel.text = "\(currencyUnit)\(String((round(1000 * realPrice)  /1000)))"
        tertiaryPriceLabel.text = "\(tertiaryUnit)\(String((round(1000 * tertiaryPrice) / 1000)))"
    

    //Image
    func getIcon() 

        let chosenImage = CGPrices.shared.coinData[dataIndex].image
        let remoteImageUrl = URL(string: chosenImage)

        guard let url = remoteImageUrl else  return 

        URLSession.shared.dataTask(with: url)  (data, response, err) in

            guard let data = data else  return 

            do 
                DispatchQueue.main.async 
                    self.cryptoIconImage.image = UIImage(data: data)
                

            
            .resume()
    

    override func viewDidLoad() 
        super.viewDidLoad()

        //        for family in UIFont.familyNames.sorted() 
        //            let names = UIFont.fontNames(forFamilyName: family)
        //            print("Family: \(family) Font names: \(names)")
        //        
        // Do any additional setup after loading the view, typically from a nib.

        self.setupLayout()
        self.getIndex()
        self.setupLabels()
        self.getIcon()
    

    override func viewWillAppear(_ animated: Bool) 
        super.viewWillAppear(animated)

        self.dataLabel!.text = dataObject
        view.backgroundColor = .lightGray
    


编辑:使用 getData 方法的 CGPrices 类:

import Foundation

class CGPrices 

    struct Coins: Decodable 
        let id: String
        let name: String
        let symbol: String
        let image: String
        let current_price: Double?
        let low_24h: Double?
        //let price_change_24h: Double?
    

    var coinData = [Coins]()

    var defaultCurrency = ""
    var coins = Manager.shared.coins
    var coinsEncoded = ""

    static let shared = CGPrices()

    func encode() 
        for i in 0..<coins.count 
            coinsEncoded += coins[i]
            if (i + 1) < coins.count  coinsEncoded += "%2C" 
        
        print("encoded")
    

    func getData(arr: Bool, completion: @escaping (Bool) -> ()) 

        encode()

        let urlJSON = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=\(coinsEncoded)"

        guard let url = URL(string: urlJSON) else  return 

        URLSession.shared.dataTask(with: url)  (data, response, err) in

            guard let data = data else  return 

            do 
                let coinsData = try JSONDecoder().decode([Coins].self, from: data)
                self.coinData = coinsData
                completion(arr)

             catch let jsonErr 
                print("error serializing json: \(jsonErr)")
                print(data)
            

            .resume()

    

    func refresh(completion: () -> ()) 
        defaultCurrency = UserDefaults.standard.string(forKey: "DefaultCurrency")!
        completion()
    


【问题讨论】:

你把segue连接到按钮了吗?如果您想通过代码调用 segue,请不要这样做。连接源视图控制器和目标视图控制器之间的 segue。 @DuncanC self.performsegue 在按钮中被调用,我将把它放在哪里以及如何放置以便在按下按钮时执行 segue? 你是如何创建转场的?如果您查看情节提要中的 segue,它的两端与什么相关联? 如果您从一个按钮控制拖动到另一个故事板,您可以创建一个在您点击按钮时触发的转场。这不是您想要的,并且可能是您的问题的原因。 @DuncanC 它从 searchViewController 连接到 RootViewController(pageView) 【参考方案1】:

我想通了。

问题出在我的 getData 方法中,我没有更新硬币数组:

 var coinData = [Coins]()

var defaultCurrency = ""

var coins = Manager.shared.coins
var coinsEncoded = ""

static let shared = CGPrices()

func encode() 
    for i in 0..<coins.count 
        coinsEncoded += coins[i]
        if (i+1)<coins.count  coinsEncoded+="%2C" 
    
    print("encoded")

我需要在 getData 中添加这一行:

func getData(arr: Bool, completion: @escaping (Bool) -> ()) 

//Adding this line to update the array so that the URL is appended correctly.
    coins = Manager.shared.coins

    encode()

let urlJSON = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=\(coinsEncoded)"

这将修复 DataViewController 中的结果为零,但应用程序仍然会在后台线程上更新 UI 元素时崩溃,因为在 getData 方法的完成处理程序中调用了 segue。为了解决这个问题,我在 addButton 函数的 getData 方法内的 segue 上使用了 DispatchQue.Main.Async,以确保所有内容都在主线程上更新,如下所示:

 @objc func addButtonAction(sender: UIButton!) 
    print("Button tapped")
    if Manager.shared.coins.contains(dataObject) 
        Duplicate()
     else if Manager.shared.coins.count == 5 
        max()
     else 

        Manager.shared.addCoin(coin: dataObject)

            print("starting")

        CGPrices.shared.getData(arr: true)  (arr) in
            print("complete")
            print(CGPrices.shared.coinData)
//Here making sure it is updated on main thread.
            DispatchQueue.main.async 
                 self.performSegue(withIdentifier: "addCoin", sender: self)
            

        

    
    searchBar.text = ""

感谢所有 cmets,因为他们帮助我解决了这个问题,我从中学到了很多东西。希望这可以帮助其他人在调试时的思考过程,因为人们可能会陷入问题的一个领域,而忘记后退一步去寻找其他领域。

【讨论】:

以上是关于为啥我的 segue 不等到完成处理程序完成?的主要内容,如果未能解决你的问题,请参考以下文章

在完成处理程序内部执行 Segue

为啥我的完成处理程序永远不会被调用?

不返回要发送到 TableViewController 的项的嵌套完成处理程序

swift UIView 动画完成处理程序首先调用

从线程/ GCD/完成处理程序返回

如何使测试方法等到委托完成处理?