User interaction on UITableView blocks processes - swift

I have a TabelViewController. The data inside the TableViewCells is being updated with a high frequency (lets assume 10 Hz) via the tableview.reloadData(). This works so far. But when I scroll the TableView, the update is paused, until the user interaction ends. Also all other processes inside my app are paused. How can I fix this?
Here is an example TableViewController. If you run this on your emulator and check the output inside the debug area, you will notice, that not only the update of the graphics (here a label) is paused, but also the notifications, when you scroll and hold the tableview. This is also the case, if you have a process in another class.
It's interesting, that this blocking of processes is not the case if you interact with a MapView. What am I missing here?
import UIKit
class TableViewController: UITableViewController {
var text = 0
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.recieveNotification), name: NSNotification.Name(rawValue: "testNotificatiion"), object: nil)
scheduledTimerWithTimeInterval()
}
func scheduledTimerWithTimeInterval(){
timer = Timer.scheduledTimer(timeInterval: 1/10, target: self, selector: #selector(self.updateCounting), userInfo: nil, repeats: true)
}
#objc func updateCounting(){
text += 1
let userInfo = ["test": text]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "testNotificatiion"), object: nil, userInfo: userInfo)
tableView.reloadData()
}
#objc func recieveNotification(notification: Notification){
if let userInfo = notification.userInfo! as? [String: Int]
{
if let recieved = userInfo["test"] {
print("Notification recieved with userInfo: \(recieved)")
}
}
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: UITableViewCell?
cell = tableView.dequeueReusableCell(withIdentifier: "rightDetail", for: indexPath)
cell?.detailTextLabel?.text = String(text)
return cell!
}
}

Timer's don't fire when a UITableView is being scrolled. Check out this answer here by Quinn The Eskimo.
Change your method to be like this and it'll work even while scrolling.
func scheduleMyTimer() {
let t = Timer(timeInterval: 1.0, repeats: true) { _ in
self.updateCounting()
}
RunLoop.current.add(t, forMode: RunLoop.Mode.common)
}
CoreLocation callbacks also aren't firing when a UITableView is being scrolled. Here's a fix for that.
From the documentation for CLLocationManagerDelegate:
Core Location calls the methods of your delegate object on the runloop
from the thread on which you initialized CLLocationManager. That
thread must itself have an active run loop, like the one found in your
app’s main thread.
So I changed the CLLocationManager init from this:
class AppDelegate {
var lm = CLLocationManager()
}
To this:
class AppDelegate {
var lm: CLLocationManager!
var thread: Thread!
func didFinishLaunching() {
thread = Thread {
self.lm = CLLocationManager()
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
// empty on purpose
}
RunLoop.current.run()
// If no input sources or timers are attached
// to the run loop, the run() method exits immediately,
// so we add a Timer just before the call.
}
thread.start()
}
}
Then the delegate callbacks kept firing even while scrolling. This answer helped.

Related

What causes the error in App Delegate: Type 'AppDelegate' does not conform to protocol 'UITableViewDataSource?

I think error stems from elsewhere, not AppDelegate.
So, in a viewcontroller type file (class homepage) I want to fetch info from firebase and display. After finishing that section of code, App delegate gives an error.
I've removed the UITableViewDataSource from AppDelegate, then it runs, but doesn't display Firebase info.
class homepage: UITableViewController, CLLocationManagerDelegate{
var people = [Userx]()
#IBOutlet weak var table: UITableView!
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return people.count
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
as! ViewControllerTableViewCell
let person: Userx = people[indexPath.row]
cell.lblName.text = person.Education
cell.lblgenre.text = person.WhatIamConsideringBuying
return cell
}
var locationManager = CLLocationManager()
override func viewDidLoad() {
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Sign Out", style: .plain, target: self, action: #selector(signOut))
super.viewDidLoad()
let databaseRef = Database.database().reference()
databaseRef.child("Education").observe(DataEventType.value, with: {snapshot in
if snapshot.childrenCount>0{
self.people.removeAll()
for people in snapshot.children.allObjects as! [DataSnapshot] {
let peopleObject = people.value as? [String: AnyObject]
let peopleEducation = peopleObject?["Education"]
let peopleWhatIamConsideringBuying = peopleObject?["WhatIamConsideringBuying"]
let peoplePhotoPosts = peopleObject?["PhotoPosts"]
let people = Userx(Education: peopleEducation as! String?, WhatIamConsideringBuying: peopleWhatIamConsideringBuying as! String?, PhotoPosts: peoplePhotoPosts as AnyObject)
self.people.append(people)
}
self.table.reloadData()
}
})
///this is different file code for class ViewControllerTableViewCell
class ViewControllerTableViewCell: UITableViewCell {
#IBOutlet weak var lblName: UILabel!
#IBOutlet weak var lblgenre: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Since the error may be in storyboard, the path of homepage is:Homepage Scene - Homepage - Table - Cell - Content View - label 1 and label 2. DataSource and Delegate from Table is connected to Homepage via Outlets.
I just want the error in AppDelegate to disappear so that data can be fetched from firebase.
Here is App Delegate code:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITableViewDelegate, UITableViewDataSource {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
Database Structure: https://imgur.com/a/CLknEWu
Write this code in your viewDidLoad() method
table.dataSource = self
table.delegate = self
And update your code in cellForRowAt
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ViewControllerTableViewCell
if let person = people[indexPath.row] {
cell.lblName.text = person.Education
cell.lblgenre.text = person.WhatIamConsideringBuying
}
return cell
}
To Prevent App from crash
override func viewDidLoad() {
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Sign Out", style: .plain, target: self, action: #selector(signOut))
super.viewDidLoad()
let databaseRef = Database.database().reference()
databaseRef.child("Education").observe(DataEventType.value, with: {snapshot in
if snapshot.childrenCount>0{
self.people.removeAll()
for people in snapshot.children.allObjects as! [DataSnapshot] {
let peopleObject = people.value as? [String: AnyObject]
let peopleEducation = peopleObject?["Education"]
let peopleWhatIamConsideringBuying = peopleObject?["WhatIamConsideringBuying"]
let peoplePhotoPosts = peopleObject?["PhotoPosts"]
if let peopleEducation = peopleEducation as? String {
if let whatIAmConsideringBuying = peopleWhatIamConsideringBuying as? String? {
if let photoPosts = photoPost as? AnyObject {
let people = Userx(Education: peopleEducation, WhatIamConsideringBuying: whatIAmConsideringBuying, PhotoPosts: photoPosts)
self.people.append(people)
}
}
}
}
self.table.reloadData()
}
})

When Keyboard Present, Choose What Causes View To Slide Up

I have a tableView with a textField within row 0 and textView within row 3. My tableview currently slides up every time when the keyboard is present. When the tableView slides up, you can't see the textField within row 0. How do I disable this for row 0 and just keep for row 3? I tried using using Protocol & Delegates to try to encapsulate the function only for row 3, but that doesn't work.
class CreateEditItemController: UIViewController, CreateItemDescriptionCellDelegate {
#IBOutlet weak var tableView: UITableView!
func handleKeyboardShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y == 0 {
//self.view.frame.origin.y -= keyboardSize.height
self.view.frame.origin.y -= 200
}
}
}
func handleKeyboardHide(notification: NSNotification) {
if self.view.frame.origin.y != 0 {
self.view.frame.origin.y = 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
...
case 3:
let cell = tableView.dequeueReusableCell(withIdentifier: "CreateItemDescriptionCell", for: indexPath) as! CreateItemDescriptionCell
cell.delegate = self
return cell
default:
return tableView.cellForRow(at: indexPath)!
}
}
}
protocol CreateItemDescriptionCellDelegate: class {
func handleKeyboardShow(notification: NSNotification)
func handleKeyboardHide(notification: NSNotification)
}
class CreateItemDescriptionCell: UITableViewCell, UITextViewDelegate {
//IBOUTLETS
#IBOutlet weak var notesTextView: UITextView!
weak var delegate: CreateItemDescriptionCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
notesTextView.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func handleKeyboardShow(notification: NSNotification) {
delegate?.handleKeyboardShow(notification: notification)
}
#objc func handleKeyboardHide(notification: NSNotification) {
delegate?.handleKeyboardHide(notification: notification)
}
}
What you are trying to do is possible after some mathematics but i would recommend using a third party pod for this instead of doing this manually on evert controller.
Add this to your pod file:
# IQKeyboardManager: Codeless drop-in universal library allows to prevent issues of keyboard sliding up
# https://github.com/hackiftekhar/IQKeyboardManager
pod 'IQKeyboardManagerSwift'
For further detail and documentation view:
https://github.com/hackiftekhar/IQKeyboardManager
The only line you would have to write would be :
// Enabling IQKeyboardManager
IQKeyboardManager.shared.enable = true
in
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
This will solve all your problems and you wont have to calculate the frames or anything.
There are a few routes you can go down, but for simplicities sake, there are a few libraries you can use to get all the calculations down for you and you don't need to worry about it.
One I use all the time is:
TPKeyboardAvoiding - https://github.com/michaeltyson/TPKeyboardAvoiding
IQKeyboardManagerSwift - https://github.com/hackiftekhar/IQKeyboardManager
And many others.

NSWorkspace: runningApplications is not returning all user processes unless I use an NSTimer

I am trying to use NSWorkspace to get all the currently running user applications/processes. When I run the code below, it only returns a subset of all the currently running user applications and displays them in the tableView. Yet, when I uncomment the timer line, all the applications are returned and displayed in the table. I'm not sure why the first call to NSWorkspace.runningApplications doesn't immediately fetch all the currently running user applications? Thank you!
import Cocoa
class ViewController: NSViewController {
// MARK: - Properties
#IBOutlet weak var tableView: NSTableView!
var runningApplications = [NSRunningApplication]()
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
//NSTimer.scheduledTimerWithTimeInterval(2, target: self, selector: #selector(ViewController.refresh(_:)), userInfo: nil, repeats: true)
runningApplications = NSWorkspace.sharedWorkspace().runningApplications
print(runningApplications)
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func refresh(timer: NSTimer){
runningApplications = NSWorkspace.sharedWorkspace().runningApplications
tableView.reloadData()
}
}
// MARK: - NSTableViewDataSource
extension ViewController: NSTableViewDataSource {
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return runningApplications.count
}
}
// MARK: - NSTableViewDelegate
extension ViewController: NSTableViewDelegate {
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
cellView.textField?.stringValue = runningApplications[row].localizedName!
return cellView
}
}

Wait for user to dismiss Modal View before executing code (Swift 2.0)

I'm building an app that asks users to select a location if they don't allow access to their current location using a Modal that Presents Modally as soon as the user clicks 'Deny'. This modal has information displayed as a TableView, and the modal dismisses as soon as the user selects a row. I save this selection in a variable called selectedStop. I want the app to pause until the user selects a location, then as soon as the user selects a location, the app continues and the setUpMap() function executes. I've tried using an infinite while loop in setUpMap() and using a boolean to break out of it as soon as a user selects a row, but the while loop executes before the Modal even pops up.
ViewController.swift
class ViewController: UIViewController {
var selectedStop: Int!
override func viewDidLoad() {
super.viewDidLoad()
// If we don't have access to the user's current location, request for it
if (CLLocationManager.authorizationStatus() != CLAuthorizationStatus.AuthorizedWhenInUse) {
locationManager.requestWhenInUseAuthorization()
}
}
func setUpMap() {
// do stuff with var selectedStop
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .Denied:
// if user denies access, display modal
self.performSegueWithIdentifier("NotifyModally", sender: self)
setUpMap() // need this func to execute AFTER location is selected
break
case .AuthorizedWhenInUse:
setUpMap()
break
default:
break
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "NotifyModally") {
let destViewController:ModalViewController = segue.destinationViewController as! ModalViewController
// send selectedStop var to ModalViewController
destViewController.selectedStop = selectedStop
}
}
}
ModalViewController.swift
class ModalViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var busStops = ["Stop 1", "Stop 2", "Stop 3"]
var selectedStop: Int!
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return busStops.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel!.text = busStops[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedStop = indexPath.row
dismissViewControllerAnimated(true, completion: nil)
}
}
Using a Int variable to pass information will not working since it's a value type which will get copied every time you pass it around. So that means when you change the selectedStop in the didSelectRowAtIndexPath method, the original selectedStop inside ViewController will still be nil or whatever it was.
And then, to answer your question. There are several ways to solve this.
You can either pass a block (instead an int) to the ModalViewController like this:
var stopSelectedHandler: (Int) -> Void = { selectedStop in
// Do something here.
// setUpMap()
}
You'll call this block inside the completion handler of dismissViewControllerAnimated.
You can use notification.
// Do this inside `ViewController`.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "setupMap:", name: "UserDidSelectStop", object: nil)
// And then post the notification inside `didSelectRowAtIndexPath`
NSNotificationCenter.defaultCenter().postNotificationName("UserDidSelectStop", object: nil, userInfo: ["selectedStop": 2])
// Change your setupMap to this
func setupMap(notification: NSNotification) {
guard let selectedStop = notification.userInfo?["selectedStop"] as? Int else { return }
// Now you can use selectedStop.
}
You can also use KVO, delegate, etc. Use whatever suits you.
Put the block like this:
class ViewController: UIViewController {
var stopSelectedHandler: (Int) -> Void = { selectedStop in
// Do something here.
// setUpMap()
}
....
}

NSOperationQueue blocks UI update of a UITableView when any cell contains a UISwitch

I'm developing an iPhone app which does a lot of mathematical computations, which take about 5-6 seconds, but repeated several times (so, usually, take about 50-60 seconds in total). The full computation set may be repeated several time all together. Each computation is encapsulated in a NSOperation.
I am having an issue where during first run of computations if an app tries to show a UITableView where any of the cells have a UISwitch, all app UI would freeze until NSOperationQueue with computations becomes empty. (In my app, if user tries to open in-app preferences, which contain such table view, before first run of computations finishes, the UI freezes).
It took me about a week of banging my head at the freezing to pin point conditions when it happens. Below is the code which reproduces the issue in my app. If you run the it and press "Show table" before all 5 operations finish, the UI will freeze until the queue is empty (decrease the added number if operations finish too fast on your machine).
If I replace NSOperationQueue with GCD, the issue disappears. If I replace UISwitch with a UIButton, the issue disappears as well. I'm running Xcode 6.4 (6E35b).
Sometimes the UI does not freeze in this demo code (however, it freezes always in my actual app), but re-running it makes it appear eventually. Also, I have found that setting maxConcurrentOperationCount to 2 on my machine (2 core 2009 MBP) makes the issue disappear, but setting it to higher number makes it occur more frequently.
Any, even remote, suggestions on this would be highly appreciated.
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private let queue = NSOperationQueue()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let viewController = UIViewController()
viewController.view.backgroundColor = UIColor.whiteColor()
viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Show table", style: .Plain, target: self, action: "showTable:")
let navigationController = UINavigationController(rootViewController: viewController)
let screenBounds = UIScreen.mainScreen().bounds
window = UIWindow(frame: screenBounds)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
setUpComputations()
return true
}
func showTable(sender: UIBarButtonItem!) {
let tableViewController = TableViewController()
let navigationController = UINavigationController(rootViewController: tableViewController)
window?.rootViewController?.presentViewController(navigationController, animated: true, completion: nil)
println("showed table")
}
private func setUpComputations() {
// queue.maxConcurrentOperationCount = 2
for i in 0...5 {
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
queue.addOperationWithBlock {
dispatch_async(dispatch_get_main_queue()) {
println("starting operation \(i)")
}
var sum = 0.0
for _ in 0...5 {
while sum < 1000.0 {
sum += 1.0e-6
}
}
dispatch_async(dispatch_get_main_queue()) {
println("finishing operation \(i)")
}
}
}
}
}
internal final class TableViewController: UITableViewController {
func dismiss(sender: UIBarButtonItem!) {
dismissViewControllerAnimated(true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .Done, target: self, action: "dismiss:")
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? UITableViewCell
?? UITableViewCell(style: .Default, reuseIdentifier: "Cell")
cell.textLabel?.text = "Some text"
cell.accessoryView = UISwitch()
// cell.accessoryView = UIButton.buttonWithType(.InfoLight) as! UIButton
return cell
}
}
Addendum: Thanks to Rob's input, I've noticed that setting qualityOfService = .Background on operation, not the queue, seems to fix the problem. Namely, replace setUpComputations above with
private func setUpComputations() {
for i in 0...5 {
let operation = NSBlockOperation {
dispatch_async(dispatch_get_main_queue()) {
println("starting operation \(i)")
}
var sum = 0.0
for _ in 0...5 {
while sum < 1000.0 {
sum += 1.0e-6
}
}
dispatch_async(dispatch_get_main_queue()) {
println("finishing operation \(i)")
}
}
operation.qualityOfService = .Background
queue.addOperation(operation)
}
}