为啥我的 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 不等到完成处理程序完成?的主要内容,如果未能解决你的问题,请参考以下文章