UI altering only works in viewDidAppear? - ios

I'm manually changing the height of the UINavigationBar in my custom UINavigationController subclass. For some reason when I add the code to alter the height in the viewDidAppear function the bar's height is correctly changed, but when I try putting the code in the viewDidLoad nothing happens. Here's how I'm doing it:
class PetNavigationVC: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
let bounds = self.navigationBar.bounds
self.navigationBar.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 74)
}
}
Can anybody help me understand why this is the case?

As mentioned in comment, in viewDidLoad method the constraints aren't even calculated and nothing is really loaded except for your class values. Also adding code to viewDidLoad can be a bit tricky too, because user can see what's happening right on demant :D
You should adjust your frames or any UI related stuff in viewWillAppear func or in method DidLayoutSubviews...

Related

Swift - Interactive dismissing form sheet doesn't work iOS 13

I'm presenting my UIViewController with modalPresentationStyle = .formSheet. Everything works as expected except interactive dismissing this controller (I drag form sheet from the top down). Controller is dismissed but it's presentingViewController doesn't move back. It remains stucked with black edges around.
If I dismiss presented controller using dismiss(animated:completion:), presentingViewController moves back as expected so it seems it doesn't work just when user dismiss it by dragging.
If you need any other details let me know. Thank you. 🙏
I'm using iOS 13.3 and Xcode 11.2.1.
Update:
I found a problem. 🎉 This view which I presented contained UITableView. I was using my method to set its tableFooterView:
extension UITableView {
func setFooterView(_ view: UIView) {
view.frame = bounds
view.setNeedsLayout()
view.layoutIfNeeded()
let height = view.systemLayoutSizeFitting(
CGSize(width: view.frame.width, height: UIView.layoutFittingCompressedSize.height)
).height
var frame = view.frame
frame.size.height = height
view.frame = frame
tableFooterView = view
sectionFooterHeight = 0
}
}
...and I called this method in my view controller in viewDidLayoutSubviews() since I thought that this is where I should set this footer view (Simply because we were using it like this in various projects 🤷🏻‍♂️):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.setFooterView(footerView)
}
But I wasn't right. Simply calling it in viewDidLoad() resolved it.
override func viewDidLoad() {
super.viewDidLoad()
tableView.setFooterView(footerView)
}
No the question is: why?
If anyone could provide correct explanation why this happens I would be grateful and I would reward him/her with bounty. 🎁

How to set the status bar height to 20pt when on a call like the App Store does?

When on a phone call while in the iOS App Store the status bar changes from 40pt tall to 20pt tall when reading an article. How do I do this in my app?
this is really an interesting question, took me sometimes to figure it out.
if you notice app store in such viewcontroller didn't have a status bar, which means the green bar should not be shown at all. this is the first hint.
you can access the status bar by UIApplication.shared.value(forKey: "statusBarWindow") as! UIWindow
according to point number one this means the status bar is not hidden but actually its statusBarWindow.frame.origin.y just shifted up.
please not you need to handle iPhone x separately
please make sure the status bar is not hidden
note that this is not the only right way, right now you have the view itself, you can change the origin or size or even try to get what inside this view and hide them or change their frames too etc.
here is an example how you can do it.
class ViewController: UIViewController {
var isHidden:Bool = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.5) { () -> Void in
let statusBarWindow = UIApplication.shared.value(forKey: "statusBarWindow") as! UIWindow
statusBarWindow.frame = CGRect(x: 0, y: -20, width: statusBarWindow.frame.size.width, height: 40.0)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override var prefersStatusBarHidden: Bool{
return isHidden
}
}
also attached an image with the result of this code, hopefully, this answer work for you too.

Flickering of ViewController where textField is first responder

I faced with a flash of viewController, where searchField is first responder, when I am returning to that controller. i tried i on ios9 , but there is no such a problem, so it looks that it is caused in ios10.
Could you please advice what could be a reason of it and how to solve it?
Thank you in advance for any suggestion.
EDIT:
My View hierarchy is quite simple(its just a demo), but cause the issue happends here as well i am investigating it here. The code looks like that:
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewWillAppear(_ animated: Bool) {
searchBar.becomeFirstResponder()
}
}
I faced this problem too. In my case, the screen on the first page was a stack view inside of a scroll view and I'm still not sure why this was happening, but setting the content inset of the scrollview in viewWillAppear fixed this issue for me. I used this code for that:
let insets = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
scrollView.contentSize = CGSize(width: stackView.frame.width, height: stackView.frame.height)
If this type of solution won't work for you, could you add some more details about the hierarchy of objects on your view?
Maybe setting the contentInset of the UISearchBarController would fix this problem.

Container View bounds issue in Swift

I'm writing a function which can get the height and width of the container view:
// container view's UIViewController
class SelectionView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
func getBounds -> (CGFloat,CGFloat){
let x = self.view.bounds.width / 5
let y = self.view.bounds.height / 15
return x,y
}
}
I write a button to call this getBounds() and it works well, but when I put it in the viewDidLoad() function
override func viewDidLoad() {
super.viewDidLoad()
getBounds()
}
getBounds() returns me different height and width and it is clearly not the bounds of this container view.
I'm pretty sure I've linked this class to the container view!
View layout is not setup in viewDidLoad. Therefore any resizing is not done yet and your size is wrong(probably the same as declared in Storyboard/Xib).
Move getBounds in viewWillLayoutSubviews or viewWillAppear and it will work correctly. Please mind that those method won't be called one time only ;)
The view hasn't been laid out in viewDidLoad, you will likely need to catch it in a later method or in viewDidLayoutSubviews.

UITextView is not scrolled to top when loaded

When I have text that does not fill the UITextView, it is scrolled to the top working as intended. When there is more text than will fit on screen, the UITextView is scrolled to the middle of the text, rather than the top.
Here are some potentially relevant details:
In viewDidLoad to give some padding on top and bottom of UITextView:
self.mainTextView.textContainerInset = UIEdgeInsetsMake(90, 0, 70, 0);
The UITextView uses auto layout to anchor it 20px from top, bottom and each side of the screen (done in IB) to allow for different screen sizes and orientations.
I can still scroll it with my finger once its loaded.
EDIT
I found that removing the auto layout constraints and then fixing the width only seems to fix the issue, but only for that screen width.
add the following function to your view controller class...
Swift 3
override func viewDidLayoutSubviews() {
self.mainTextView.setContentOffset(.zero, animated: false)
}
Swift 2.1
override func viewDidLayoutSubviews() {
self.mainTextView.setContentOffset(CGPointZero, animated: false)
}
Objective C
- (void)viewDidLayoutSubviews {
[self.mainTextView setContentOffset:CGPointZero animated:NO];
}
UITextView is a subclass of UIScrollView, so you can use its methods. If all you want to do is ensure that it's scrolled to the top, then wherever the text is added try:
[self.mainTextView setContentOffset:CGPointZero animated:NO];
EDIT: AutoLayout with any kind of scrollview gets wonky fast. That setting a fixed width solves it isn't surprising. If it doesn't work in -viewDidLayoutSubviews then that is odd. Setting a layout constraint manually may work. First create the constraints in IB:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *textViewWidthConstraint;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *textViewHeightConstraint;
then in the ViewController
-(void)updateViewConstraints {
self.textViewWidthConstraint.constant = self.view.frame.size.width - 40.0f;
self.textViewHeightConstraint.constant = self.view.frame.size.height - 40.0f;
[super updateViewConstraints];
}
May still be necessary to setContentOffset in -viewDidLayoutSubviews.
(Another method would be to create a layout constraint for "'equal' widths" and "'equal' heights" between the textView and its superView, with a constant of "-40". It's only 'equal' if the constant is zero, otherwise it adjusts by the constant. But because you can only add this constraint to a view that constraints both views, you can't do this in IB.)
You may ask yourself, if I have to do this, what's the point of AutoLayout? I've studied AutoLayout in depth, and that is an excellent question.
Swift
self.textView.scrollRangeToVisible(NSMakeRange(0, 0))
Objective-C
[self.textView scrollRangeToVisible:(NSMakeRange(0, 0))];
i had same issue! Reset to suggested constrains and just put (y offset)
#IBOutlet weak var textContent: UITextView!
override func viewDidLoad() {
textContent.scrollsToTop = true
var contentHeight = textContent.contentSize.height
var offSet = textContent.contentOffset.x
var contentOffset = contentHeight - offSet
textContent.contentOffset = CGPointMake(0, -contentOffset)
}
For iOS9 and later the textview even on viewWillAppear: is coming with CGRect(0,0,1000,1000). In order for this to work you have to call in viewWillAppear:
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
// * Your code here
After that the textview will have correct CGRect data and you can perform any scrolling operation you may need.
The problem with putting code in viewDidLayoutSubviews and viewWillLayoutSubviews is that these methods are called a lot (during device rotation, resizing views etc ...). If you're reading something from text view, and you rotate the device, you expect that the part of the content you're viewing stays on screen. You do not expect that it scrolls back to top.
Instead of scrolling the content to top, try to keep text view's scrollEnabled property set to NO (false), and turn it back on in viewDidAppear.
If you don't wanna mess with constraints:
override func updateViewConstraints() {
super.updateViewConstraints()
}
override func viewDidLayoutSubviews() {
self.textLabel.setContentOffset(CGPointZero, animated: false)
}
This is an interesting bug. In our project, this is only occurring on devices with an iPhone 5-size screen. It appears that the textview contentOffset changes at some point during the view controller lifecycle. In viewDidLoad and viewWillAppear the textview's contentOffset is 0,0, and by viewDidAppear it's changed. You can see it happening in viewWillLayoutSubviews. Constraints appear to be set up correctly.
This will ensure you don't call a scrolling method unless it's needed:
if textView.contentOffset.y > 0 {
textView.contentOffset = CGPoint(x: 0, y: 0)
// Or use scrollRectToVisible, scrollRangeToVisible, etc.
}
Swift
override func viewDidLoad() {
super.viewDidLoad()
textView.isScrollEnabled = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textView.isScrollEnabled = true
}
For me this works in a different way, I tried all things mentioned above but none of the worked in func viewWillAppear(_ animated: Bool). Which eventually makes textView scrolled up, and in func viewDidAppear(_ animated: Bool) it would scroll after screen appeared.
Below worked for me but got some constraint related issue with keyboard up and down.
override func viewDidLayoutSubviews() {
self.textView.setContentOffset(.zero, animated: false)
}
Below worked as expectation:
override func viewDidLoad() {
super.viewDidLoad()
self.textView.scrollsToTop = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.layoutIfNeeded()
self.textView.setContentOffset(.zero, animated: false)
}
David Rectors answer in Objective C:
#import "TopTextView.h"
#implementation TopTextView
bool scrolled = NO;
- (void) layoutSubviews
{
[super layoutSubviews];
if (!scrolled) {
[self setContentOffset:CGPointMake(0, 0) animated:NO];
scrolled = YES;
}
}
#end
It seems like a terrible idea to handle this issue in code in the view controller because: A. The view controller isn't making any mistake or doing anything wrong, and B, if you have more than one view controller with a wrongly scrolled text view, you end up with redundant code. The solution should be to write code that exists in the text view class. My solution works with Interface Builder where I simply select a custom class for the UITextView and use this class:
import Foundation
import UIKit
class TopTextView: UITextView {
var scrolled = false
override func layoutSubviews() {
super.layoutSubviews()
if scrolled { return }
setContentOffset(.zero, animated: false)
scrolled = true
}
}
This worked for me. I happen to have a view controller with a child view with a UITextView as a child of that view, not with a UITextView as the child of the view controller. I don't know how well this works if the text view is under top or bottom bars but since no edge insets are touched, this should work.
In my case I had to do it like this:
textView.setContentOffset(CGPoint(x: 0, y: -self.textView.adjustedContentInset.top), animated: false)
because the texview was underneath the navigation bar and had an adjusted inset

Resources