从 TableView 打开 ViewController 时有几秒钟的延迟
Posted
技术标签:
【中文标题】从 TableView 打开 ViewController 时有几秒钟的延迟【英文标题】:Lag of several seconds when opening ViewController from TableView 【发布时间】:2019-09-12 00:07:04 【问题描述】:我正在尝试调试一个非常困难的问题,但我无法深入了解它,所以我想知道是否有人可以分享一些建议。
当调用 didSelectRowAt 时,我有一个 TableView 转换到不同的 VC,当立即注册点击时,后台的某些东西导致新的 VC 仅在 5 秒后出现,我无法弄清楚这是什么原因造成的。
到目前为止我尝试了什么: - 将 iCloud 任务移动到全局线程 - 注释掉整个 iCloud 功能并在本地保存数据 - 禁用 Hero pod 并使用带有或不带有动画的内置 segue - 注释掉 tableview.reloadData() 调用 - 注释掉 viewDidAppear 中的所有内容 - 在 ios12 和 iOS13 GM 上运行它,所以这不是操作系统问题 - 分析应用程序,我看不到任何异常,但我对分析器不是很熟悉
对于冗长的代码转储,我深表歉意,但由于我不确定是什么原因造成的,因此我想提供尽可能多的细节。
非常感谢您分享的任何见解。
主类
import UIKit
import SPAlert
import CoreLocation
import NotificationCenter
import PullToRefreshKit
class List: UIViewController
// Outlets
@IBOutlet weak var plus: UIButton!
@IBOutlet weak var notes: UIButton!
@IBOutlet weak var help: UIButton!
@IBOutlet weak var tableview: UITableView!
@IBOutlet weak var greeting: UILabel!
@IBOutlet weak var temperature: UILabel!
@IBOutlet weak var weatherIcon: UIImageView!
@IBOutlet weak var weatherButton: UIButton!
@IBOutlet weak var greetingToTableview: NSLayoutConstraint!
let locationManager = CLLocationManager()
@IBAction func notesTU(_ sender: Any)
performSegue(withIdentifier: "ToNotes", sender: nil)
@IBAction func notesTD(_ sender: Any)
notes.tap(shape: .square)
@IBAction func plusTU(_ sender: Any)
hero(destination: "SelectionScreen", type: .zoom)
@IBAction func plusTD(_ sender: Any)
plus.tap(shape: .square)
@IBAction func helpTU(_ sender: Any)
performSegue(withIdentifier: "ToHelp", sender: nil)
@IBAction func helpTD(_ sender: Any)
help.tap(shape: .square)
@IBAction func weatherButtonTU(_ sender: Any)
performSegue(withIdentifier: "OpenModal", sender: nil)
selectedModal = "Weather"
// Variables
override var preferredStatusBarStyle: UIStatusBarStyle return .lightContent
// MARK: viewDidLoad
override func viewDidLoad()
super.viewDidLoad()
tableview.estimatedRowHeight = 200
tableview.rowHeight = UITableView.automaticDimension
// Retrieves ideas from the JSON file and assings them to the ideas array
ideas = readJSONIdeas()
goals = readJSONGoals()
ideaStats = readJSONIdeaStats()
decisions = readJSONDecisions()
let time = Calendar.current.component(.hour, from: Date())
switch time
case 21...23: greeting.text = "Good Night"
case 0...4: greeting.text = "Good Night"
case 5...11: greeting.text = "Good Morning"
case 12...17: greeting.text = "Good Afternoon"
case 17...20: greeting.text = "Good Evening"
default: print("Something went wrong with the time based greeting")
temperature.alpha = 0
weatherIcon.alpha = 0
getWeather(temperatureLabel: temperature, iconLabel: weatherIcon)
NotificationCenter.default.addObserver(self, selector: #selector(self.replayOnboarding), name: Notification.Name(rawValue: "com.cristian-m.replayOnboarding"), object: nil)
if iCloudIsOn()
NotificationCenter.default.addObserver(self, selector: #selector(self.reloadAfteriCloud), name: Notification.Name(rawValue: "com.cristian-m.iCloudDownloadFinished"), object: nil)
tableview.configRefreshHeader(with: RefreshHeader(),container:self)
// After the user pulls to refresh, synciCloud is called and the pull to refresh view is left open.
// synciCloud posts a notification for key "iCloudDownloadFinished" once it finishes downloading, which then calls reloadAfteriCloud()
// reloadAfteriCloud() loads the newly downloaded files into memory, reloads the tableview and closes the refresher view
if iCloudIsAvailable() synciCloud()
else
self.alert(title: "It looks like you're not signed into iCloud on this device",
message: "Turn on iCloud in Settings to use iCloud Sync",
actionTitles: ["Got it"],
actionTypes: [.regular],
actions: [nil])
synciCloud()
// Responsive Rules
increasePageInsetsBy(top: 10, left: 20, bottom: 20, right: 20, forDevice: .iPad)
increasePageInsetsBy(top: 0, left: 0, bottom: 14, right: 0, forDevice: .iPhone8)
greetingToTableview.resize(to: 80, forDevice: .iPad)
@objc func replayOnboarding(_ notification:NSNotification)
DispatchQueue.main.asyncAfter(deadline: .now()+0.2)
self.hero(destination: "Onboarding1", type: .zoom)
@objc func reloadAfteriCloud(_ notification:NSNotification)
goals = readJSONGoals()
ideas = readJSONIdeas()
ideaStats = readJSONIdeaStats()
decisions = readJSONDecisions()
tableview.reloadData()
self.tableview.switchRefreshHeader(to: .normal(.none, 0.0))
setWeeklyNotification()
@objc func goalCategoryTapped(_ sender: UITapGestureRecognizer?)
hero(destination: "GoalStats", type: .pushLeft)
@objc func ideaCategoryTapped(_ sender: UITapGestureRecognizer?)
hero(destination: "IdeaStats", type: .pushLeft)
override func viewWillAppear(_ animated: Bool)
tableview.reloadData()
if shouldDisplayGoalCompletedAlert == true
shouldDisplayGoalCompletedAlert = false
SPAlert.present(title: "Goal Completed", preset: .done)
if CLLocationManager.locationServicesEnabled()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
tableview 扩展
import UIKit
extension List: UITableViewDelegate, UITableViewDataSource
// MARK: numberOfSections
func numberOfSections(in tableView: UITableView) -> Int return 3
// MARK: viewForHeader
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
let cell = tableView.dequeueReusableCell(withIdentifier: "CategoryCell") as! CategoryCell
switch section
case 0:
cell.title.text = "Goals"
if goals.count != 0 cell.emptyText.text = "You have \(completedGoals.count) achieved goals"
else cell.emptyText.text = "No goals added yet"
if activeGoals.count > 0 cell.emptyText.removeFromSuperview()
break
case 1:
cell.title.text = "Ideas"
cell.emptyText.text = "No ideas added yet"
if ideas.count > 0 cell.emptyText.removeFromSuperview()
break
case 2:
cell.title.text = "Decisions"
cell.arrow.removeFromSuperview()
cell.emptyText.text = "No decisions added yet"
if decisions.count > 0 cell.emptyText.removeFromSuperview()
break
default: print("Something went wrong with the section Switch")
if section == 0
cell.button.addTarget(self, action: #selector(goalCategoryTapped(_:)), for: .touchUpInside)
else if section == 1
cell.button.addTarget(self, action: #selector(ideaCategoryTapped(_:)), for: .touchUpInside)
return cell.contentView
// MARK: heightForHeader
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
var cellHeight = CGFloat(60)
if (activeGoals.count > 0 && section == 0) || (ideas.count > 0 && section == 1) || (decisions.count > 0 && section == 2)
cellHeight = CGFloat(40)
return cellHeight
// MARK: numberOfRows
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
var numberOfRows: Int = 0
if section == 0 numberOfRows = activeGoals.count
if section == 1 numberOfRows = ideas.count
if section == 2 numberOfRows = decisions.count
return numberOfRows
// cellForRowAt
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if indexPath.section == 0
// Goal Cell
let cell = tableview.dequeueReusableCell(withIdentifier: "GoalCell", for: indexPath) as! GoalCell
cell.goalTitle?.text = activeGoals[indexPath.row].title
if activeGoals[indexPath.row].steps!.count == 1
cell.goalNoOfSteps?.text = "\(activeGoals[indexPath.row].steps?.count ?? 0) Step"
else if activeGoals[indexPath.row].steps!.count > 0
cell.goalNoOfSteps?.text = "\(activeGoals[indexPath.row].steps?.count ?? 0) Steps"
else
cell.goalNoOfSteps?.text = "No more steps"
if goals[indexPath.row].stringDate != "I'm not sure yet"
cell.goalDuration.text = goals[indexPath.row].timeLeft(from: Date())
else
cell.goalDuration.text = ""
cell.selectionStyle = .none
cell.background.hero.id = "goal\(realIndexFor(activeGoalAt: indexPath))"
// Progress Bar
cell.progressBar.configure(goalsIndex: realIndexFor(activeGoalAt: indexPath))
return cell
else if indexPath.section == 1
// Idea Cell
let cell = tableView.dequeueReusableCell(withIdentifier: "IdeaCell", for: indexPath) as! IdeaCell
cell.ideaTitle.text = ideas[indexPath.row].title
if cell.ideaDescription != nil
cell.ideaDescription.text = String(ideas[indexPath.row].description!.filter !"\n\t".contains($0) )
if cell.ideaDescription.text == "Notes" || cell.ideaDescription.text == "" || cell.ideaDescription.text == " " || cell.ideaDescription.text == ideaPlaceholder
cell.ideaDescriptionHeight.constant = 0
cell.bottomConstraint.constant = 16
else
cell.ideaDescriptionHeight.constant = 38.6
cell.bottomConstraint.constant = 22
cell.background.hero.id = "idea\(indexPath.row)"
let image = UIImageView(image: UIImage(named: "delete-accessory"))
image.contentMode = .scaleAspectFit
cell.selectionStyle = .none
return cell
else
// Decision Cell
let cell = tableView.dequeueReusableCell(withIdentifier: "DecisionCell", for: indexPath) as! DecisionCell
cell.title.text = decisions[indexPath.row].title
let image = UIImageView(image: UIImage(named: "delete-accessory"))
image.contentMode = .scaleAspectFit
cell.selectionStyle = .none
return cell
// MARK: didSelectRowAt
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
if indexPath.section == 0
selectedCell = realIndexFor(activeGoalAt: indexPath)
performSegue(withIdentifier: "toGoalDetails", sender: nil)
else if indexPath.section == 1
selectedCell = indexPath.row
performSegue(withIdentifier: "toIdeaDetails", sender: nil)
else
selectedDecision = indexPath.row
hero(destination: "DecisionDetails", type: .zoom)
print("tap")
// MARK: viewForFooter
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
let cell = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 10))
cell.backgroundColor = UIColor(named: "Dark")
return cell
// MARK: heightForFooter
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
let height:CGFloat = 18
return height
// MARK: canEditRowAt
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool return true
// MARK: trailingSwipeActions
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
let action = UIContextualAction(style: .normal, title: nil, handler: (action,view,completionHandler ) in
var message = "This will delete this goal and all its steps permanently"
if indexPath.section == 1 message = "This will delete this idea permanently"
self.alert(title: "Are you sure?",
message: message,
actionTitles: ["No, cancel", "Yes, delete"],
actionTypes: [.regular, .destructive],
actions: [ nil, action1 in
tableView.beginUpdates()
switch indexPath.section
case 0:
deleteGoal(at: realIndexFor(activeGoalAt: indexPath))
tableView.deleteRows(at: [indexPath], with: .fade)
case 1:
deleteIdea(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
case 2:
deleteDecision(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
default: break
tableView.endUpdates()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute:
tableView.reloadData()
)
,
]
)
completionHandler(true)
)
action.image = UIImage(named: "delete-accessory")
action.backgroundColor = UIColor(named: "Dark")
let confrigation = UISwipeActionsConfiguration(actions: [action])
confrigation.performsFirstActionWithFullSwipe = false
return confrigation
打开的 VC
import UIKit
class GoalDetails: UIViewController
// MARK: Variables
var descriptionExpanded = false
var descriptionExists = true
var keyboardHeight = CGFloat(0)
override var preferredStatusBarStyle: UIStatusBarStyle if #available(iOS 13.0, *) return .darkContent else return .default
// MARK: Outlets
@IBOutlet weak var background: UIView!
@IBOutlet weak var steps: UILabel!
@IBOutlet weak var detailsTitle: UILabel!
@IBOutlet weak var detailsDescription: UILabel!
@IBOutlet weak var tableview: UITableView!
@IBOutlet weak var progressBar: UIProgressView!
@IBOutlet weak var plusButton: UIButton!
@IBOutlet var descriptionHeight: NSLayoutConstraint!
@IBOutlet weak var completeGoalButton: UIButton!
@IBOutlet weak var completeGoalButtonHeight: NSLayoutConstraint!
@IBOutlet weak var progressBarHeight: NSLayoutConstraint!
@IBOutlet weak var dismissButton: UIButton!
@IBOutlet weak var editButton: UIButton!
@IBOutlet weak var tableviewBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var topToContainer: NSLayoutConstraint!
@IBOutlet weak var bottomToContainer: NSLayoutConstraint!
@IBOutlet weak var rightToContainer: NSLayoutConstraint!
@IBOutlet weak var leftToContainer: NSLayoutConstraint!
@IBOutlet weak var leftToTableview: NSLayoutConstraint!
@IBOutlet weak var rightToTableview: NSLayoutConstraint!
@IBOutlet weak var leftToEdit: NSLayoutConstraint!
@IBOutlet weak var rightToPlus: NSLayoutConstraint!
// MARK: Outlet Functions
@IBAction func completeThisGoal(_ sender: Any)
shouldDisplayGoalCompletedAlert = true
goals[selectedCell].completed = true
goals[selectedCell].dateAchieved = Date()
activeGoals = goals.filter $0.completed == false
completedGoals = goals.filter $0.completed == true
writeJSONGoals()
hero.dismissViewController()
setWeeklyNotification()
@IBAction func descriptionButtonTU(_ sender: Any)
if descriptionExpanded == false
descriptionHeight.isActive = false
descriptionExpanded = true
else
descriptionHeight.isActive = true
descriptionExpanded = false
@IBAction func swipeDown(_ sender: Any)
dismissButton.tap(shape: .square)
hero.dismissViewController()
@IBAction func dismissTU(_ sender: Any)
hero.dismissViewController()
@IBAction func dismissTD(_ sender: Any)
dismissButton.tap(shape: .square)
@IBAction func plusTU(_ sender: Any)
goals[selectedCell].steps?.append(Step(title: ""))
let numberOfCells = tableview.numberOfRows(inSection: 0)
tableview.reloadData()
tableview.layoutIfNeeded()
DispatchQueue.main.asyncAfter(deadline: .now()+0.2)
let cell = self.tableview.cellForRow(at: IndexPath.init(row: numberOfCells, section: 0)) as? StepCell
cell?.label.becomeFirstResponder()
let indexPath = IndexPath(row: goals[selectedCell].steps!.count - 1, section: 0)
tableview.scrollToRow(at: indexPath, at: .bottom, animated: true)
progressBar.configure(goalsIndex: selectedCell)
configureCompleteGoalButton(buttonHeight: completeGoalButtonHeight, progressBarHeight: progressBarHeight, progressBar: progressBar)
updateNumberofSteps()
@IBAction func plusTD(_ sender: Any)
plusButton.tap(shape: .square)
@IBAction func editTU(_ sender: Any)
performSegue(withIdentifier: "ToGoalEdit", sender: nil)
@IBAction func editTD(_ sender: Any)
editButton.tap(shape: .rectangle)
// MARK: Class Functions
func updateNumberofSteps()
if goals[selectedCell].steps!.count > 0
steps.text = "\(goals[selectedCell].steps?.count ?? 0) Steps"
else
steps.text = "No more steps"
// MARK: viewDidLoad
override func viewDidLoad()
background.hero.id = "goal\(selectedCell)"
self.background.clipsToBounds = true
background.layer.cornerRadius = 16
background.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
updateNumberofSteps()
// Progress Bar
progressBar.configure(goalsIndex: selectedCell)
tableview.emptyDataSetSource = self
tableview.emptyDataSetDelegate = self
configureCompleteGoalButton(buttonHeight: completeGoalButtonHeight, progressBarHeight: progressBarHeight, progressBar: progressBar)
// Responsive Rules
increasePageInsetsBy(top: 0, left: 0, bottom: 14, right: 0, forDevice: .iPad)
if UIDevice.current.userInterfaceIdiom == .pad
detailsTitle.font = UIFont.boldSystemFont(ofSize: 30)
topToContainer.constant = 20
leftToContainer.constant = 40
rightToContainer.constant = 40
bottomToContainer.constant = 40
leftToTableview.constant = 40
rightToTableview.constant = 40
leftToEdit.constant = 40
rightToPlus.constant = 30
increasePageInsetsBy(top: 0, left: 0, bottom: 12, right: 0, forDevice: .iPhone8)
// MARK: viewWillAppear
override func viewWillAppear(_ animated: Bool)
// Deleting a goal from the Edit page seems to also call ViewWillAppear, which causes the app to crash unless checking whether the index exists anymore
// selectedCell already get assigned the real index of this goal
if goals.indices.contains(selectedCell)
detailsTitle.text = goals[selectedCell].title
detailsDescription.text = goals[selectedCell].description
if goals[selectedCell].description == "Reason" || goals[selectedCell].description == ""
descriptionHeight.constant = 0
else
descriptionHeight.constant = 58
被调用的 iCloud 相关函数
func iCloudIsAvailable() -> Bool
// This function checks whether iCloud is available on the device
if FileManager.default.ubiquityIdentityToken != nil return true
else return false
func iCloudIsOn() -> Bool
// This function checks whether the user chose to use iCloud with Thrive
if UserDefaults.standard.url(forKey: "UDDocumentsPath")! == iCloudPath || UserDefaults.standard.url(forKey: "UDDocumentsPath") == iCloudPath
return true
else
return false
func synciCloud()
if iCloudIsAvailable()
do try FileManager.default.startDownloadingUbiquitousItem(at: UserDefaults.standard.url(forKey: "UDDocumentsPath")!)
do
let status = try UserDefaults.standard.url(forKey: "UDDocumentsPath")!.resourceValues(forKeys: [.ubiquitousItemDownloadingStatusKey])
while status.ubiquitousItemDownloadingStatus != .current
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute:
print("iCloud still downloading - \(String(describing: status.ubiquitousItemDownloadingStatus))")
)
DispatchQueue.main.async
NotificationCenter.default.post(name: Notification.Name(rawValue: "com.cristian-m.iCloudDownloadFinished"), object: nil)
print("iCloud up to date! - \(String(describing: status.ubiquitousItemDownloadingStatus))")
catch let error print("Failed to get status: \(error.localizedDescription)")
catch let error print("Failed to download iCloud Documnets Folder: \(error.localizedDescription)")
else
// TODO: Handle use case where iCloud is not available when trying to sync
print("iCloud is not available on this device")
更新:根据 Duncan 的回答,解决问题的方法是将我在 didSelectRowAt
中的三个 Segue 移动到主队列,如下所示:
DispatchQueue.main.async
self.performSegue(withIdentifier: "toGoalDetails", sender: nil)
【问题讨论】:
通常在触发 UI 代码和使其生效之间有很长的延迟是从后台线程执行 UIKit 调用的症状。 (这可能会导致各种不良结果,但响应能力的长时间延迟是常见的。)从您的代码中看,我没有看到任何明显的东西,但是您发布了一整串代码,而我没有现在是时候涉足它了。我建议在你进行 UIKit 调用的不同位置设置断点,看看它们是否从线程 1(主线程)以外的任何线程中断。 嗨,Christian,也许您可以尝试使用 Whatchdog 检查您的代码。在this answer 中提出了建议,通过它您可以确定问题是否存在于阻塞主线程 5 秒内。 如@DuncanC 所述,这是与线程相关的问题。为什么不在一些并发队列中执行这些操作?想法 = readJSONIdeas() 目标 = readJSONGoals() ideaStats = readJSONIdeaStats() 决策 = readJSONDecisions() @SwissMark,我不知道,但我一定会去看看。谢谢。 @CristianMoisei 您能否标记导致问题的 UIKit 调用,并在您的问题底部添加一个部分,显示您使用的修复程序? (我通常用##EDIT
标记我的问题/答案的更改,以便它们脱颖而出。)
【参考方案1】:
通常如果我无法找出问题的原因,我会执行“二分查找”样式调试。
你提到你已经注释掉了整个 viewDidAppear,但我假设你没有用 viewDidLoad 尝试过。
在这种情况下,我会将viewDidLoad中的所有代码注释掉,运行它,看看延迟是否还在。
如果延迟消失了,我将viewDidLoad中的代码注释掉一半,重新运行。一般来说,一旦我找到导致延迟的一半,我会注释掉一半的“坏”代码并重复,直到找到导致问题的确切行。
【讨论】:
谢谢@Hong Wei,今晚我会试试这个,如果有效,我会回复。你说得对,我还没有为主 vc 注释掉 viewDidLoad。【参考方案2】:通常在触发 UI 代码和使其生效之间有很长的延迟是从后台线程执行 UIKit 调用的症状。 (这可能会导致各种糟糕的结果,但长时间的响应延迟是常见的。)
我看你的代码并没有看到任何明显的东西,但是你发布了一大堆代码,我现在没有时间涉足它。我建议在你进行 UIKit 调用的不同位置设置断点,看看它们是否从线程 1(主线程)以外的任何线程中断。——Duncan C 昨天删除
【讨论】:
以上是关于从 TableView 打开 ViewController 时有几秒钟的延迟的主要内容,如果未能解决你的问题,请参考以下文章
如何从其他类重新加载整个 tableview 控制器 [关闭]
Swift:将数据从 tableView 显示到另一个 ViewController(JSON、Alamorife、AlamofireImage)