Reputation: 2279
I am trying to debug a pretty difficult problem and I cannot get to the bottom of it, so I was wondering if anyone could share some suggestions.
I have a TableView that transitions to a different VC when didSelectRowAt is called, and while the tap is registered immediately, something in the background is causing the new VC to only be presented as much as 5 seconds later and I cannot figure out what is causing this.
What I tried so far: - moving the iCloud tasks to the global thread - commenting out the entirety of the iCloud functions and saving data locally - disabling the Hero pod and using a built in segue with or without an animation - commenting out the tableview.reloadData() calls - commenting out everything in viewDidAppear - running this on both iOS12 and iOS13 GM, so it's not an OS issue - profiling the app, where I couldn't see anything out of the ordinary, but then again I am not very familiar with the profiler
I apologise for the long code dump but since I'm not sure what is causing this, I want to provide as much detail as I can.
Thanks a lot for any insights you might be able to share.
The main class
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
}
}
}
The tableview extension
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
}
}
The VC that gets opened
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
}
}
}
}
The iCloud related functions being called
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")
}
}
didSelectRowAt
to the main queue like this:DispatchQueue.main.async {
self.performSegue(withIdentifier: "toGoalDetails", sender: nil)
}
Upvotes: 0
Views: 679
Reputation: 131408
Usually very long lags between triggering UI code and having it take effect is a symptom of doing UIKit calls from a background thread. (That can lead to all kind of bad outcomes, but long delays in responsiveness is a common one.)
I don't see anything obvious from a glance at your code, but you posted a WHOLE BUNCH of code and I don't have the time to wade through it right now. I suggest setting breakpoints at various locations where you do UIKit calls and see if they break from any thread other than thread 1 (the main thread.) – Duncan C yesterday Delete
Upvotes: 2
Reputation: 1407
Normally if I cannot figure out what is causing the problem, I would perform "binary search" style debugging.
You mentioned that you have commented out the whole of viewDidAppear, but I am assuming you have not tried that with viewDidLoad.
In this case, I will comment out all the code in viewDidLoad, run it and see if the delay is still around.
If the delay is gone, I will comment out half the code in viewDidLoad, rerun. Generally, once I found the half that causes the delay, I would comment out half of that "bad" code and repeat until I find the exact lines causing the problem.
Upvotes: 2