Почему мой переход не ждет завершения обработчика завершения? - PullRequest
0 голосов
/ 24 сентября 2018

У меня есть приложение на основе страницы, использующее RootViewController, ModelViewController, DataViewController и SearchViewController.

В моем searchViewController я ищу элемент, а затем добавляю или удаляю этот элемент в массив, содержащийся вкласс Manager (и UserDefaults), который modelViewController использует для создания экземпляра DataViewController с правильной информацией, загруженной с использованием dataObject.В зависимости от того, был ли элемент добавлен или удален, я использую Bool, чтобы определить, какой переход использовался, addCoin или removeCoin, так что RootViewController (PageView) покажет либо последнюю страницу в массиве (при добавлении страницы), либопервый (после удаления).

Все работало нормально, пока я не столкнулся с ошибкой, которую я не могу диагностировать, проблема в том, что при добавлении страницы приложение вылетает, давая мне «неожиданно найденный ноль»при развертывании необязательного значения "

Это кажется проблемной функцией, в 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.

, поэтому я пытаюсь выполнить push-переход, используя диспетчеризацию que

Вот мой searchViewController fКод ULL:

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
    }

}

Редактировать: класс CGPrices с методом getData:

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()
    }

}

1 Ответ

0 голосов
/ 24 сентября 2018

Я понял.

Проблема была в моем методе 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)"

Это исправит обнаружение nil в DataViewController, но приложение все равно будет аварийно завершать работу при обновлении элементов пользовательского интерфейса в фоновом потоке, так как переход вызывался внутри обработчика завершения метода getData.чтобы исправить это, я использовал DispatchQue.Main.Async в segue внутри метода getData в функции addButton, чтобы гарантировать, что все обновляется в главном потоке, например так:

 @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 = ""
}

Спасибо за всекомментарии, поскольку они помогли мне понять это, и я многому научился в этом.Надеемся, что это может помочь кому-то другому в их мыслительном процессе при отладке, так как он может оказаться настолько захваченным одной областью проблемы и забыть сделать шаг назад и заглянуть в другие области.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...