从 TableView 打开 ViewController 时延迟几秒钟
Lag of several seconds when opening ViewController from TableView
我正在尝试调试一个非常困难的问题,但我无法深入了解它,所以我想知道是否有人可以分享一些建议。
我有一个 TableView,它在调用 didSelectRowAt 时转换到不同的 VC,虽然点击被立即注册,但背景中的某些东西导致新的 VC 仅显示为5 秒后,我无法弄清楚是什么原因造成的。
到目前为止我尝试了什么:
- 将 iCloud 任务移动到全局线程
- 注释掉所有 iCloud 功能并在本地保存数据
- 禁用 Hero pod 并使用带或不带动画的内置 segue
- 注释掉 tableview.reloadData() 调用
- 注释掉 viewDidAppear 中的所有内容
- 运行 这在 iOS12 和 iOS13 GM 上都有,所以这不是 OS 问题
- 分析应用程序,在那里我看不到任何不寻常的地方,但话又说回来我对分析器不是很熟悉
对于冗长的代码转储,我深表歉意,但由于我不确定是什么原因造成的,我想提供尽可能多的细节。
非常感谢您分享任何见解。
主要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
}
}
}
表格视图扩展
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([=12=]) })
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 { [=13=].completed == false }
completedGoals = goals.filter { [=13=].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)
}
通常如果我无法弄清楚是什么导致了问题,我会执行 "binary search" 风格的调试。
您提到您已经注释掉了整个 viewDidAppear,但我假设您还没有尝试使用 viewDidLoad。
在这种情况下,我将注释掉 viewDidLoad 中的所有代码,运行 它并查看延迟是否仍然存在。
如果延迟没有了,我会把viewDidLoad中的一半代码注释掉,重新运行。通常,一旦我找到导致延迟的那一半,我会注释掉那一半 "bad" 代码并重复,直到我找到导致问题的确切行。
通常在触发 UI 代码和让它生效之间有很长的延迟是从后台线程执行 UIKit 调用的症状。 (这可能会导致各种不良结果,但长时间的响应延迟很常见。)
看一眼你的代码我没有看到任何明显的东西,但你发布了一大堆代码,我现在没有时间费力地浏览它。我建议在您执行 UIKit 调用的不同位置设置断点,看看它们是否从除线程 1(主线程)以外的任何线程中断。 – Duncan C 昨天删除
我正在尝试调试一个非常困难的问题,但我无法深入了解它,所以我想知道是否有人可以分享一些建议。
我有一个 TableView,它在调用 didSelectRowAt 时转换到不同的 VC,虽然点击被立即注册,但背景中的某些东西导致新的 VC 仅显示为5 秒后,我无法弄清楚是什么原因造成的。
到目前为止我尝试了什么: - 将 iCloud 任务移动到全局线程 - 注释掉所有 iCloud 功能并在本地保存数据 - 禁用 Hero pod 并使用带或不带动画的内置 segue - 注释掉 tableview.reloadData() 调用 - 注释掉 viewDidAppear 中的所有内容 - 运行 这在 iOS12 和 iOS13 GM 上都有,所以这不是 OS 问题 - 分析应用程序,在那里我看不到任何不寻常的地方,但话又说回来我对分析器不是很熟悉
对于冗长的代码转储,我深表歉意,但由于我不确定是什么原因造成的,我想提供尽可能多的细节。
非常感谢您分享任何见解。
主要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
}
}
}
表格视图扩展
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([=12=]) })
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 { [=13=].completed == false }
completedGoals = goals.filter { [=13=].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)
}
通常如果我无法弄清楚是什么导致了问题,我会执行 "binary search" 风格的调试。
您提到您已经注释掉了整个 viewDidAppear,但我假设您还没有尝试使用 viewDidLoad。
在这种情况下,我将注释掉 viewDidLoad 中的所有代码,运行 它并查看延迟是否仍然存在。
如果延迟没有了,我会把viewDidLoad中的一半代码注释掉,重新运行。通常,一旦我找到导致延迟的那一半,我会注释掉那一半 "bad" 代码并重复,直到我找到导致问题的确切行。
通常在触发 UI 代码和让它生效之间有很长的延迟是从后台线程执行 UIKit 调用的症状。 (这可能会导致各种不良结果,但长时间的响应延迟很常见。)
看一眼你的代码我没有看到任何明显的东西,但你发布了一大堆代码,我现在没有时间费力地浏览它。我建议在您执行 UIKit 调用的不同位置设置断点,看看它们是否从除线程 1(主线程)以外的任何线程中断。 – Duncan C 昨天删除