UITableView is resetting its background color before view appears - ios

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.

Related

UISheetPresentationController displaying differently on different phones

I created a UIStoryboardSegue to make a "Bottom Sheet segue". Our designer shared a screenshot of the app on his phone and the bottom sheet is displaying differently, despite the fact we are both on the same iOS version.
On mine and my simulator, when the bottom sheet opens, it lightens the source view and then shrinks it down a little, so it appears just barely behind the bottom sheet
On the same screen on the designers device, it dims the background and leaves the source view full size, showing the top of the buttons in the navigation bar
I've noticed the Apple maps bottom sheet behaves like the designers, no shrinking of the background view. But I can't see any settings that would affect this. How can I stop the sheet from resizing the source view on mine and function like it's supposed to?
Here's my code:
import UIKit
public class BottomSheetLargeSegue: UIStoryboardSegue {
override public func perform() {
guard let dest = destination.presentationController as? UISheetPresentationController else {
return
}
dest.detents = [.large()]
dest.prefersGrabberVisible = true
dest.preferredCornerRadius = 30
source.present(destination, animated: true)
}
}
Found a hack to force it to never minimise the source view at least, not really what I wanted, but keeps it consistent. Supposedly, .large() is always supposed to minimize the source view, you can avoid this in iOS 16 by creating a custom detent that is just a tiny bit smaller than large like so:
let customId = UISheetPresentationController.Detent.Identifier("large-minus-background-effect")
let customDetent = UISheetPresentationController.Detent.custom(identifier: customId) { context in
return context.maximumDetentValue - 0.1
}
dest.detents = [customDetent]
And as a bonus, found a way to control the dimming on the bottom sheet overlay. There is a containerView property on the presentationController, but it is nil when trying to access it in while in the segue. If you force code to run on the main thread, after the call to present, you can access the containerView and set your own color / color animation
e.g.
...
...
source.present(destination, animated: true)
DispatchQueue.main.async {
dest.containerView?.backgroundColor = .white
}

How to disable default keyboard navigation in Mac Catalyst app?

I noticed that I can step through rows in a UITableView in a Mac Catalyst app by pressing the up and down arrow keys on my Mac keyboard. However, this interferes with the existing functionality in one of my view controllers. Is there a way to disable this?
I can't find any reference to this functionality in the UITableView documentation. The Human Interface Guidelines for Mac Catalyst mentions "automatic support for fundamental Mac features, such as ... keyboard navigation," so I guess this is an intentional feature, but I can't find any further reference to it or documentation for it.
I haven't seen any other examples of "automatic" keyboard navigation in my app, but ideally Apple would publish a complete list so we could know how to work with, or if needed, disable, the built-in functionality.
Update as of 2021/11/06
It looks like Apple has been changing how the default focus system works and my previous solution is no longer working or required.
UIKeyCommand has a new wantsPriorityOverSystemBehavior: Bool property which needs to be set to true in order for our subclasses to receive certain types of commands, including the arrow key commands.
As of at least Xcode 13.1 and macOS 11.6, maybe eariler, we can now simply add the following to a UITableViewController subclass to replace the default focus behavior with custom keyboard navigation handing:
class TableViewController: UITableViewController {
override var keyCommands: [UIKeyCommand]? {
let upArrowCommand = UIKeyCommand(
input: UIKeyCommand.inputUpArrow,
modifierFlags: [],
action: #selector(handleUpArrowKeyPress)
)
upArrowCommand.wantsPriorityOverSystemBehavior = true
let downArrowCommand = UIKeyCommand(
input: UIKeyCommand.inputDownArrow,
modifierFlags: [],
action: #selector(handleDownArrowKeyPress)
)
downArrowCommand.wantsPriorityOverSystemBehavior = true
return [
upArrowCommand,
downArrowCommand
]
}
#objc
func handleUpArrowKeyPress () {
}
#objc
func handleDownArrowKeyPress () {
}
}
Previous answer (no longer working or required)
Catalyst automatically assigns UIKeyCommands for the up/down arrows to UITableView instances. This does not happen on iOS. You can see this in action by setting a break point in viewDidLoad() of a UITableViewController and inspecting tableView.keyCommands.
So I created a very simple UITableView subclass and disabled the default keyCommmands by returning nil:
class KeyCommandDisabledTableView: UITableView {
override var keyCommands: [UIKeyCommand]? {
return nil
}
}
I then updated my UITableViewController subclass to use the new KeyCommandDisabledTableView subclass:
class MyTableViewController: UITableViewController {
override func loadView() {
self.view = KeyCommandDisabledTableView(
frame: .zero,
style: .plain // or .grouped
)
}
}
Et voilà! The default arrow key handling is gone and my app's custom arrow key handling is now being called.
Here's another solution I received from Apple DTS. Just add this to the table view delegate:
func tableView(_ tableView: UITableView, canFocusRowAt indexPath: IndexPath) -> Bool {
return false
}
This works in macOS 11.6 and 12.0. I don't have a 10.15 or 11.5 Mac to test with, so I'll keep my earlier resignFirstResponder solution, too.
I further noticed that the default arrow key navigation only begins after clicking a row in a table, so I guessed the table must be assuming the first responder role. I added this to my table's delegate class:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
#if TARGET_OS_MACCATALYST
[tableView performSelector:#selector(resignFirstResponder) withObject:nil afterDelay:0.1];
#endif
}
That fixed it! Now the default keyboard navigation turns off as soon as it turns on, and doesn't interfere with my app's custom keyboard navigation.
(It didn't work without the delay.)
iOS 14 / macOS 11 makes it much easier to disable this behavior thanks to UITableView and UICollectionView's selectionFollowsFocus property:
tableView.selectionFollowsFocus = false

UIView doesn't change at runtime

I've had this working in other variations but something seems to elude me in the change from objective-c to swift as well as moving some of the setup into it's own class.
So i have:
class ViewController: UIViewController, interfaceDelegate, scrollChangeDelegate{
let scrollControl = scrollMethods()
let userinterface = interface()
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
}
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
}
}
This sets everything up correctly but the problem occurs when I change loadMenu() at runtime. So if the user calls loadMenu("AnotherMenu") it won't change the UIView. It will call the right functions but it won't update the view. Although if I call loadMenu("AnotherMenu") at the start, the correct menu will display. Or if I call loadMenu("Start") and then loadMenu("AnotherMenu") then the menu displayed will be "AnotherMenu". As in:
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
loadMenu("AnotherMenu")
}
When I list all the subviews each time loadMenu() is called, they look correct. Even during runtime. But the display is not updated. So something isn't getting the word. I've tried disabling Auto Layout after searching for similar issues but didn't see a difference.
Try adding setNeedsDisplay() to loadMenu
Eg
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
view.setNeedsDisplay()
}
setNeedsDisplay() forces the view to reload the user interface.
I didn't want to post the whole UIView class as it is long and I thought unrelated. But Dan was right that he would need to know what was going on in those to figure out the answer. So I created a dummy UIView class to stand in and intended to update the question with that. I then just put a button on the ViewController's UIView. That button was able to act on the view created by the dummy. So the problem was in the other class. Yet it was calling the methods of the ViewController and seemingly worked otherwise. So then the issue must be that its acting on an instanced version? The way the uiview class worked, it uses performSelector(). But in making these methods into their own class, I had just lazily wrote
(ViewController() as NSObjectProtocol).performSelector(selector)
when it should have been
(delegate as! NSObjectProtocol).performSelector(selector)
so that was annoying and I wasted the better part of a day on that. But thanks again for the help.

Why doesn't my View respond to a gesture using a gestureRecognizer?

Having just spent a day beating my head against the keyboard, I thought I'd share my diagnosis and solution.
Situation: You add a custom View of custom class CardView to an enclosing view myCards in your app and want each card to respond to a tap gesture (for example to indicate you want to discard the card). Typical code you start with:
In your ViewController:
class MyVC : UIViewController, UIGestureRecognizerDelegate {
...
func discardedCard(sender: UITapGestureRecognizer) {
let cv : CardView = sender.view! as! CardView
...
}
In your myCards construction:
cv = CardView(card: card)
myCards.addSubview(cv)
cv.userInteractionEnabled = true
...
let cvTap = UITapGestureRecognizer(target: self, action: Selector("discardedCard:"))
cvTap.delegate = self
cv.addGestureRecognizer(cvTap)
I found the arguments here very confusing and the documentation not at all helpful. It isn't clear that the target: argument refers to the class that implements discardedCard(sender: UITapGestureRecognizer) . If you're constructing the recognizer and cards in your ViewController that's going to be self. If you want to move the discardedCard into your custom View class (for example), then replace self with CardView in my example, including on the delegate line.
Testing the above code, I found that the discardedCard function was never called. What was going on?
So a day later, here's what I had to fix. I hope this checklist is useful to somebody else. I'm new to iOS (coming from Android), so it may be obvious to you veterans:
Make sure the touched view (cv in the example) has userInteractionEnabled=true . Note that if you use your own constructor it will be set false by default
Make sure all enclosing views also have userInteractionEnabled=true
Others have posted that the order of the delegate statement and addGestureRecognizer statement made a difference; I didn't find that using Xcode 7.2 and iOS 9.2
Most important: Make sure your touched view is fully within the bounds of the enclosing views. In my example, I was building a myCards container that didn't have the width set correctly and was cutting off the right-most cards (and since clipping is disabled by default, this wasn't visually obvious until I looked in the debugger at the View hierarchy)

UITableView and UIRefreshControl being moved down for unknown reason

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 :)

Resources