I have a UITableViewController in my app with a UIRefreshControl added to it. Sometimes however (I'm not sure how to reproduce this, it happens every now and then), I get some extra whitespace at the top of the table view with the refresh control being offset even below that.
This is what it looks like (idle on the left, being pulled down on the right):
I don't have any clue what could be causing this. In my viewdidload I'm only instantiating the refresh control and calling an update function that sets the attributed title. I've moved adding the refresh control to the table view into the viewDidAppear as I've read elsewhere. This is what that code looks like:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl = UIRefreshControl()
updateData()
}
override func viewDidAppear(animated: Bool) {
refreshControl!.addTarget(self, action: "updateData", forControlEvents: UIControlEvents.ValueChanged)
tableView.insertSubview(self.refreshControl!, atIndex: 0)
}
func updateData() {
//...
ServerController.sendParkinglotDataRequest() {
(sections, plotList, updateError) in
//...
// Reload the tableView on the main thread, otherwise it will only update once the user interacts with it
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
// Update the displayed "Last update: " time in the UIRefreshControl
let formatter = NSDateFormatter()
formatter.dateFormat = "dd.MM. HH:mm"
let updateString = NSLocalizedString("LAST_UPDATE", comment: "Last update:")
let title = "\(updateString) \(formatter.stringFromDate(NSDate()))"
let attributedTitle = NSAttributedString(string: title, attributes: nil)
self.refreshControl!.attributedTitle = attributedTitle
})
}
}
Do you need to add the refresh control as a subview of the tableView? I think all you need to do is assign self.refreshControl. According to the documentation:
The default value of this property is nil.
Assigning a refresh control to this property adds the control to the
view controller’s associated interface. You do not need to set the
frame of the refresh control before associating it with the view
controller. The view controller updates the control’s height and width
and sets its position appropriately.
Adding a subview in viewDidAppear could get executed more than once. If you push a controller from a cell and pop back this will get called again. It could be that insertSubview checks if the refresh already has a parent and removes it first, so might not be your issue. You should only do the insert when the controller appears for the first time.
updateData could also be getting added multiple times.
So I think you only need to assign self.refreshControl and then add a handler for the refresh action as you do now using addTarget but this time do it on self.refreshControl.
You can also do all this from storyboard. In storyboard you select the UITableViewController and on the attribute inspector simply set the Refreshing attribute to enabled. This adds a UIRefreshControl into the table and you can see it on the view hierarchy. You can then simply CTRL drag as normal from the refresh control into the .h file and add an action for valueChange which will be fired when you pull down on the refresh control in the table.
Well, I believe that your described behavior might not necessarily be caused by the refresh control.
According to the fact that you don't have any other subviews below your table view I would recommend you to try to place a "fake"-view below your table view. I usually prefer an empty label with 0 side length.
I had similar issues like yours where my table view insets were broken in some cases. And as soon as I used this "fake" subview the problems disappeared. I've read about this issue in some other threads, too. And the solution was this. Seems to be an odd behavior/bug.
Give it a try :)
Related
I have added a UI Button inside of a stack view which is inside of a table view in my storyboard. When I click on my button the correct output is printed in my debugger console but there is no indication in the app that the button has been clicked (no default animation). I have tried looking at my view hierarchy and changing all of the parent views to clip to bounds. Any idea why the button is functioning but not being animated to the user?
The quick fix to your problem is to set delaysContentTouches = false for your table view.
According to the Apple Docs,
If the value of this property is true, the scroll view delays handling the touch-down gesture until it can determine if scrolling is the intent. If the value is false, the scroll view immediately calls touchesShouldBegin(_:with:in:). The default value is true.
See the class description for a fuller discussion.
Alternatively, if you have subclassed the UIScrollView, you can get the same thing done by overriding the following function,
class MyScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return type(of: view) == UIButton.self
}
}
In order to display a text field right above the user's keyboard, I overrode inputAccessoryView in my custom view controller.
I also made sure that the view controller may become the first responder by overriding canBecomeFirstResponder (and returning true) and by calling self.becomeFirstResponder() in viewWillAppear().
Now, as I am displaying some messages as UICollectionViewCells in my view controller, I want to scroll down whenever the keyboard shows up. So I added a notification in viewDidLoad():
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: Notification.Name.UIKeyboardDidShow, object: nil)
keyboardDidShow() then calls the scrolling function:
#objc private final func scrollToLastMessage() {
// ('messages' holds all messages, one cell represents a message.)
guard messages.count > 0 else { return }
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true)
}
Indeed, by setting breakpoints in Xcode, I found out that the function gets triggered after the keyboard has appeared. But additionally, it also triggers after I resigned the first responder (f.ex. by hitting the return key [I resign the first responder and return true in textFieldShouldReturn ]) and the keyboard has disappeared. Although I think that it shouldn't: as the Apple docs say:
Posted immediately after the display of the keyboard.
The notification also triggers when accessing the view controller, so after the main view has appeared and when clicking on a (customized) UICollectionViewCell (the cell does not have any editable content, only static labels or image views, so the keyboard shouldn't even appear).
To give some more information: I pretty much followed this tutorial on Youtube: https://www.youtube.com/watch?v=ky7YRh01by8
The UIKeyboardDidShow notification may be posted more often than you might expect, not just when it initially appears. For example, when the frame changes after it was already visible, UIKeyboardDidShow is posted.
However you can know if the keyboard is truly visible by inspecting the keyboard's end frame from within the userInfo dictionary. This will tell you its size and position on screen, which you can then use to determine how best to react in your user interface.
I'm using probably a little bit exotic way of initialization of my UI components. I create them programmatically and among them is a UITableView instance, I set its background color immediately upon initialization, like this:
class MyViewController: UIViewController {
...
let tableView = UITableView().tap {
$0.backgroundColor = .black
$0.separatorStyle = .none
}
...
}
where tap is extension function:
func tap(_ block: (Self) -> Void) -> Self {
block(self)
return self
}
This worked very well in my previous project which was created in Xcode 8 and then migrated to Xcode 9 without breaking anything. But now I've created brand new project in Xcode 9 and copy-pasted above-mentioned extension to it, but seems like something went wrong. When my view controller appears on screen table has white background and default separator insets.
This seems to affect only some of the properties because others are working as they should have (e.g. $0.register(nib: UINib?, forCellReuseIdentifier: String) registers required cell class and $0.showsVerticalScrollIndicator = false hides scroll indicator).
Perhaps some of you, guys, could give me an idea what's the heart of the matter.
Here's full code, to reproduce the issue simply create a new project and replace ViewController.swift's content. As you can see, table has correct rowHeight (160) but resets its background color.
As for "before view appears" statement: I've printed table's background color in viewDidLoad, viewWillAppear and viewDidAppear like this:
print(#function, table.backgroundColor.debugDescription)
– it changes its color only in the last debug print:
viewDidLoad() Optional(UIExtendedGrayColorSpace 0 1)
viewWillAppear Optional(UIExtendedGrayColorSpace 0 1)
viewDidAppear Optional(UIExtendedSRGBColorSpace 1 1 1 1)
I ended up moving the initialization to lazy var's function – turns out initializing UITableView during the initialization of it's view controller has some side effects.
I have a subclass of UICollectionViewController that is nested inside a UINavigationController. The collection contains several cells (currently, 3) and each cell is as big as the full screen.
When the whole thing is shown, the collection view initally scrolls to a specific cell (which works flawlessly for each cell):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // this is set in the beginning
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
}
}
However, the collection view refuses to scroll horizontally, hereafter, as if the user interaction was disabled. I am not sure what is happening, but this is what I have checked so far:
user interaction is enabled for the collection view
the next cell (right or left, depending on the scroll direction) is requested correctly which I found out by inspecting collectionView:cellForItemAtIndexPath:
the requested imagePath is the right one
scrollToItemAtIndexPath... does not work either if I try to trigger a scroll programmatically after everything has been loaded (nothing happens)
scrollRectToVisible... does neither
setting collectionView?.contentInset = UIEdgeInsetsZero before the programmatic scroll attempts take place does not change anything
the content size of the collection view is 3072x768 (= 3 screens, i.e. 3 cells)
Which bullet points are missing, here?
Although the post did not precisely tackle the root of my problem it forced me to ponder the code that I posted. If you look at it you will see that it basically says: Whenever the views need to be layouted, scroll to the cell at position currentlyPresentedPhotoCellIndexPath. However, and this you cannot see without any context, this variable is only set once, when the whole controller is being initialized. Thus, when you try to scroll, the layout changes, the controller then jumps back to the initial cell and it looks like nothing happens, at all.
To change this, you just have to enforce a single scroll, e.g. by doing this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // only once possible
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
currentlyPresentedPhotoCellIndexPath = nil // because of this line
// "initiallyPresentedPhotoCellIndexPath" would probably a better name
}
}
A big thanks to Mr.T!
I am trying to get a Refresh Control to link to an IBAction function in my View Controller. When I ctrl-drag it only gives me the option to create an Outlet.
I added the Refresh Control simply by setting Refreshing to Enabled in the Attributes Inspector.
Everything else is set up properly as far as I'm aware, the View Controller is set in the Identity inspector and I'm attempting to link to the correct Class.
Any Advice
I know it's not your solution of your problem.But you can create Refresh Control programmatically.Like...
var refreshCntrl : UIRefreshControl!
refreshCntrl = UIRefreshControl()
refreshCntrl.tintColor = UIColor.whiteColor()
refreshCntrl.attributedTitle = NSAttributedString(string: "Loading...")
refreshCntrl.addTarget(self, action:("refreshControlValueChanged"), forControlEvents: UIControlEvents.ValueChanged)
atableView.addSubview(refreshCntrl)
//Method of RefreshControl
func refreshControlValueChanged(){
atableView.reloadData()
refreshCntrl.endRefreshing()
}
Happened to me too, it gives you an Outlet option because you didn't select the button as you thought but the bar. Hence, for letting it set an Action you need to higlight the Bar Button in your controller's top-down menu on the left column.
Hope it helps!