I am updating my app to adapt it for iPhone X. All views work fine by now except one. I have a view controller that presents a custom UIView that covers the whole screen. Before I was using UIScreen.main.bounds to find out the size of the view before all layout was done (I need it for putting the correct itemSize for a collectionView). I thought that now I could do something like
UIScreen.main.bounds.size.height - safeAreaInsets.bottom to get the right usable size. The problem is, safeAreaInsets returns (0,0,0,0) trying on an iPhone X (Simulator). Any ideas? In other views, I get the right numbers for safeAreaInsets.
Thank you!
I recently had a similar problem where the safe area insets are returning (0, 0, 0, 0) as soon as viewDidLoad is triggered. It seems that they are set fractionally later than the rest of the view loading.
I got round it by overriding viewSafeAreaInsetsDidChange and doing my layout in that instead:
override func viewSafeAreaInsetsDidChange() {
// ... your layout code here
}
I already figure out the solution: I was doing all the implementation in the init of the view. safeAreaInsets has the correct size in layoutSubviews()
I've run into this issue too trying to move up views to make way for the keyboard on the iPhone X. The safeAreaInsets of the main view are always 0, even though I know the subviews have been laid out at this point as the screen has been drawn. A work around I found, as and mentioned above, is to get the keyWindow and check its safe area insets instead.
Obj-C:
CGFloat bottomInset = [UIApplication sharedApplication].keyWindow.safeAreaInsets.bottom;
Swift:
let bottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom
You can then use this value to adjust constraints or view frames as required.
I have a view which is a subview inside another view.
I found that I can't get safeAreaInsets correctly, it always return 0, in that view on iPhoneX even if I put it in layoutSubviews.
The final solution is I use following UIScreen extension to detect safeAreaInsets which can work like a charm.
extension UIScreen {
func widthOfSafeArea() -> CGFloat {
guard let rootView = UIApplication.shared.keyWindow else { return 0 }
if #available(iOS 11.0, *) {
let leftInset = rootView.safeAreaInsets.left
let rightInset = rootView.safeAreaInsets.right
return rootView.bounds.width - leftInset - rightInset
} else {
return rootView.bounds.width
}
}
func heightOfSafeArea() -> CGFloat {
guard let rootView = UIApplication.shared.keyWindow else { return 0 }
if #available(iOS 11.0, *) {
let topInset = rootView.safeAreaInsets.top
let bottomInset = rootView.safeAreaInsets.bottom
return rootView.bounds.height - topInset - bottomInset
} else {
return rootView.bounds.height
}
}
}
I try to use "self.view.safeAreaInset" in a view controller. First, it is a NSInsetZero when I use it in the controller's life cycle method "viewDidLoad", then I search it from the net and get the right answer, the log is like:
ViewController loadView() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewDidLoad() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewWillAppear() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewDidLayoutSubviews() SafeAreaInsets :UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
ViewController viewDidAppear() SafeAreaInsets :UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
so you can choice the right method that you need the safeAreaInset and use it!
Swift iOS 11,12,13+
var insets : UIEdgeInsets = .zero
override func viewDidLoad() {
super.viewDidLoad()
insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? .zero
//Or you can use this
insets = self.view.safeAreaInsets
}
In my case I was adding a UICollectionView inside viewDidLoad()
collectionView = UICollectionView(frame: view.safeAreaLayoutGuide.layoutFrame, collectionViewLayout: createCompositionalLayout())
Unfortunately at this stage safeAreaLayoutGuide is still zero.
I solved it by adding:
override func viewDidLayoutSubviews() {
collectionView.frame = view.safeAreaLayoutGuide.layoutFrame
}
the viewDidAppear(_:) method of the container view controller that extends the safe area of its embedded child view controller to account for the views in .
Make your modifications in this method because the safe area insets for a view are not accurate until the view is added to a view hierarchy.
override func viewDidAppear(_ animated: Bool) {
if (#available(iOS 11, *)) {
var newSafeArea = view.safeAreaInsets
// Adjust the safe area to accommodate
// the width of the side view.
if let sideViewWidth = sideView?.bounds.size.width {
newSafeArea.right += sideViewWidth
}
// Adjust the safe area to accommodate
// the height of the bottom view.
if let bottomViewHeight = bottomView?.bounds.size.height {
newSafeArea.bottom += bottomViewHeight
}
// Adjust the safe area insets of the
// embedded child view controller.
let child = self.childViewControllers[0]
child.additionalSafeAreaInsets = newSafeArea
}
}
I've come across the same problem. In my case the view I'm inserting would be sized correctly after calling view.layoutIfNeeded(). The view.safeAreaInsets was set after this, but only the top value was correct. The bottom value was still 0 (this on an iPhone X).
While trying to figure out at what point the safeAreaInsets are set correctly, I've added a breakpoint on the view's safeAreaInsetsDidChange() method. This was being called multiple times, but only when I saw CALayer.layoutSublayers() in the backtrace the value had been set correctly.
So I've replaced view.layoutIfNeeded() by the CALayer's counterpart view.layer.layoutIfNeeded(), which resulted in the safeAreaInsets to be set correctly right away, thus solving my problem.
TL;DR
Replace
view.layoutIfNeeded()
by
view.layer.layoutIfNeeded()
[UIApplication sharedApplication].keyWindow.safeAreaInsets return none zero
Just try self.view.safeAreaInsets instead of UIApplication.shared.keyWindow?.safeAreaInsets
Safe area insets seems to not fill on iOS 11.x.x devices when requested via application keyWindow.
View layout is never guaranteed until layoutSubviews or viewDidLayoutSubviews. Never rely on sizes before these lifecycle methods. You will get inconsistent results if you do.
To calculate safe area safeAreaInsets, try to obtain it in viewWIllAppear(), as in didLoad() the view have not been formed.
You will have the correct inset in willAppear!
In case you cannot subclass, you can use this UIView extension.
It gives you an API like this:
view.onSafeAreaInsetsDidChange = { [unowned self] in
self.updateSomeLayout()
}
The extension adds an onSafeAreaInsetsDidChange property using object association. Then swizzles the UIView.safeAreaInsetsDidChange() method to call the closure (if any).
extension UIView {
typealias Action = () -> Void
var onSafeAreaInsetsDidChange: Action? {
get {
associatedObject(for: "onSafeAreaInsetsDidChange") as? Action
}
set {
Self.swizzleSafeAreaInsetsDidChangeIfNeeded()
set(associatedObject: newValue, for: "onSafeAreaInsetsDidChange")
}
}
static var swizzled = false
static func swizzleSafeAreaInsetsDidChangeIfNeeded() {
guard swizzled == false else { return }
swizzle(
method: "safeAreaInsetsDidChange",
originalSelector: #selector(originalSafeAreaInsetsDidChange),
swizzledSelector: #selector(swizzledSafeAreaInsetsDidChange),
for: Self.self
)
swizzled = true
}
#objc func originalSafeAreaInsetsDidChange() {
// Original implementaion will be copied here.
}
#objc func swizzledSafeAreaInsetsDidChange() {
originalSafeAreaInsetsDidChange()
onSafeAreaInsetsDidChange?()
}
}
It uses some helpers (see NSObject+Extensions.swift and NSObject+Swizzle.swift), but you don't really need it if you use sizzling and object association APIs directly.
Related
I have simple ViewController which displays images using UIScrollView which has constraints attaching it to the (top, leading, trailing) of the superView, and a UIPageControl, it works fine on iPhoneX simulator
When I run it on iPad Pro 9.7" simulator, the output is
After changing the View as attribute in the storyboard from iPhoneX to iPadPro 9.7" it worked well
This is the logic I use to calculate scrollviewContentSize & slidesSize
override internal func viewDidLoad() {
super.viewDidLoad()
tutorialScrollView.delegate = self
viewModel = TutorialViewModel()
configurePages()
setupSlideScrollView(slides: slides)
configurePageControl()
}
private func configurePages() {
if let viewModel = viewModel {
createSlides(tutotialPages: viewModel.getTutorialPages())
}
}
private func createSlides(tutotialPages: [TutorialPage]) {
for page in tutotialPages {
if let slide = Bundle.main.loadNibNamed(BUNDLE_ID, owner: self, options: nil)?.first as? TutorialSlideView {
slide.configure(title: page.title, detail: page.details, image: page.image)
slides.append(slide)
}
}
}
private func setupSlideScrollView(slides: [TutorialSlideView]) {
tutorialScrollView.contentSize = CGSize(width: view.frame.width * (CGFloat(slides.count)), height: tutorialScrollView.frame.height)
tutorialScrollView.isPagingEnabled = true
for i in 0 ..< slides.count {
slides[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: tutorialScrollView.frame.height)
tutorialScrollView.addSubview(slides[i])
}
}
Can anyone find the problem?
Did you try to print view's frame in setupSlideScrollView method to ensure it is correct? There is no guarantee that it will be correct in the viewDidLoad method if you use AutoLayout. Sometimes it will be, sometimes not. I assume in this particular case, it happened to be correct on iPhone X, but incorrect on iPad.
If that's the problem, you should set contentSize and slides' frames in viewDidLayoutSubviews. Adding slides as subviews should stay in viewDidLoad/setupSlideScrollView because viewDidLayoutSubviews usually gets called multiple times.
What would be the most proper way to get both top and bottom height for the unsafe areas?
Try this :
In Objective C
if (#available(iOS 11.0, *)) {
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
CGFloat topPadding = window.safeAreaInsets.top;
CGFloat bottomPadding = window.safeAreaInsets.bottom;
}
In Swift
if #available(iOS 11.0, *) {
let window = UIApplication.shared.keyWindow
let topPadding = window?.safeAreaInsets.top
let bottomPadding = window?.safeAreaInsets.bottom
}
In Swift - iOS 13.0 and above
// Use the first element from windows array as KeyWindow deprecated
if #available(iOS 13.0, *) {
let window = UIApplication.shared.windows.first
let topPadding = window.safeAreaInsets.top
let bottomPadding = window.safeAreaInsets.bottom
}
To get the height between the layout guides you just do
let guide = view.safeAreaLayoutGuide
let height = guide.layoutFrame.size.height
So full frame height = 812.0, safe area height = 734.0
Below is the example where the green view has frame of guide.layoutFrame
Swift 4, 5
To pin a view to a safe area anchor using constraints can be done anywhere in the view controller's lifecycle because they're queued by the API and handled after the view has been loaded into memory. However, getting safe-area values requires waiting toward the end of a view controller's lifecycle, like viewDidLayoutSubviews().
This plugs into any view controller:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let topSafeArea: CGFloat
let bottomSafeArea: CGFloat
if #available(iOS 11.0, *) {
topSafeArea = view.safeAreaInsets.top
bottomSafeArea = view.safeAreaInsets.bottom
} else {
topSafeArea = topLayoutGuide.length
bottomSafeArea = bottomLayoutGuide.length
}
// safe area values are now available to use
}
I prefer this method to getting it off of the window (when possible) because it’s how the API was designed and, more importantly, the values are updated during all view changes, like device orientation changes.
However, some custom presented view controllers cannot use the above method (I suspect because they are in transient container views). In such cases, you can get the values off of the root view controller, which will always be available anywhere in the current view controller's lifecycle.
anyLifecycleMethod()
guard let root = UIApplication.shared.keyWindow?.rootViewController else {
return
}
let topSafeArea: CGFloat
let bottomSafeArea: CGFloat
if #available(iOS 11.0, *) {
topSafeArea = root.view.safeAreaInsets.top
bottomSafeArea = root.view.safeAreaInsets.bottom
} else {
topSafeArea = root.topLayoutGuide.length
bottomSafeArea = root.bottomLayoutGuide.length
}
// safe area values are now available to use
}
None of the other answers here worked for me, but this did.
var topSafeAreaHeight: CGFloat = 0
var bottomSafeAreaHeight: CGFloat = 0
if #available(iOS 11.0, *) {
let window = UIApplication.shared.windows[0]
let safeFrame = window.safeAreaLayoutGuide.layoutFrame
topSafeAreaHeight = safeFrame.minY
bottomSafeAreaHeight = window.frame.maxY - safeFrame.maxY
}
All of the answers here are helpful, Thanks to everyone who offered help.
However as i see that that the safe area topic is a little bit confused which won’t appear to be well documented.
So i will summarize it here as mush as possible to make it easy to understand safeAreaInsets, safeAreaLayoutGuide and LayoutGuide.
In iOS 7, Apple introduced the topLayoutGuide and bottomLayoutGuide properties in UIViewController,
They allowed you to create constraints to keep your content from being hidden by UIKit bars like the status, navigation or tab bar
It was possible with these layout guides to specify constraints on content,
avoiding it to be hidden by top or bottom navigation elements (UIKit bars, status bar, nav or tab bar…).
So for example if you wanna make a tableView starts from the top screen you have done something like that:
self.tableView.contentInset = UIEdgeInsets(top: -self.topLayoutGuide.length, left: 0, bottom: 0, right: 0)
In iOS 11 Apple has deprecated these properties replacing them with a single safe area layout guide
Safe area according to Apple
Safe areas help you place your views within the visible portion of the overall interface. UIKit-defined view controllers may position special views on top of your content. For example, a navigation controller displays a navigation bar on top of the underlying view controller’s content. Even when such views are partially transparent, they still occlude the content that is underneath them. In tvOS, the safe area also includes the screen’s overscan insets, which represent the area covered by the screen’s bezel.
Below, a safe area highlighted in iPhone 8 and iPhone X-series:
The safeAreaLayoutGuide is a property of UIView
To get the height of safeAreaLayoutGuide:
extension UIView {
var safeAreaHeight: CGFloat {
if #available(iOS 11, *) {
return safeAreaLayoutGuide.layoutFrame.size.height
}
return bounds.height
}
}
That will return the height of the Arrow in your picture.
Now, what about getting the top "notch" and bottom home screen indicator heights?
Here we will use the safeAreaInsets
The safe area of a view reflects the area not covered by navigation bars, tab bars, toolbars, and other ancestors that obscure a view controller's view. (In tvOS, the safe area reflects the area not covered by the screen's bezel.) You obtain the safe area for a view by applying the insets in this property to the view's bounds rectangle. If the view is not currently installed in a view hierarchy, or is not yet visible onscreen, the edge insets in this property are 0.
The following will show the unsafe area and there distance from edges on iPhone 8 and one of iPhone X-Series.
Now, if navigation bar added
So, now how to get the unsafe area height? we will use the safeAreaInset
Here are to solutions however they differ in an important thing,
First One:
self.view.safeAreaInsets
That will return the EdgeInsets, you can now access the top and the bottom to know the insets,
Second One:
UIApplication.shared.windows.first{$0.isKeyWindow }?.safeAreaInsets
The first one you are taking the view insets, so if there a navigation bar it will be considered , however the second one you are accessing the window's safeAreaInsets so the navigation bar will not be considered
Swift 5, Xcode 11.4
`UIApplication.shared.keyWindow`
It will give deprecation warning. ''keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes' because of connected scenes. I use this way.
extension UIView {
var safeAreaBottom: CGFloat {
if #available(iOS 11, *) {
if let window = UIApplication.shared.keyWindowInConnectedScenes {
return window.safeAreaInsets.bottom
}
}
return 0
}
var safeAreaTop: CGFloat {
if #available(iOS 11, *) {
if let window = UIApplication.shared.keyWindowInConnectedScenes {
return window.safeAreaInsets.top
}
}
return 0
}
}
extension UIApplication {
var keyWindowInConnectedScenes: UIWindow? {
return windows.first(where: { $0.isKeyWindow })
}
}
In iOS 11 there is a method that tells when the safeArea has changed.
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
let top = view.safeAreaInsets.top
let bottom = view.safeAreaInsets.bottom
}
safeAreaLayoutGuide
When the view is visible onscreen, this guide reflects the portion of the view that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. (In tvOS, the safe area reflects the area not covered the screen's bezel.) If the view is not currently installed in a view hierarchy, or is not yet visible onscreen, the layout guide edges are equal to the edges of the view.
Then to get the height of the red arrow in the screenshot it's:
self.safeAreaLayoutGuide.layoutFrame.size.height
Swift 5 Extension
This can be used as a Extension and called with: UIApplication.topSafeAreaHeight
extension UIApplication {
static var topSafeAreaHeight: CGFloat {
var topSafeAreaHeight: CGFloat = 0
if #available(iOS 11.0, *) {
let window = UIApplication.shared.windows[0]
let safeFrame = window.safeAreaLayoutGuide.layoutFrame
topSafeAreaHeight = safeFrame.minY
}
return topSafeAreaHeight
}
}
Extension of UIApplication is optional, can be an extension of UIView or whatever is preferred, or probably even better a global function.
This works for the entire view life cycle as a simple 2-line solution in Swift:
let top = UIApplication.shared.windows[0].safeAreaInsets.top
let bottom = UIApplication.shared.windows[0].safeAreaInsets.bottom
I personally needed it in the viewDidLoad and view.safeAreaInsets isn't calculated yet.
I'm working with CocoaPods frameworks and in case UIApplication.shared is unavailable then I use safeAreaInsets in view's window:
if #available(iOS 11.0, *) {
let insets = view.window?.safeAreaInsets
let top = insets.top
let bottom = insets.bottom
}
UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
CGFloat fBottomPadding = window.safeAreaInsets.bottom;
For SwiftUI:
Code
private struct SafeAreaInsetsKey: EnvironmentKey {
static var defaultValue: EdgeInsets {
UIApplication.shared.windows[0].safeAreaInsets.insets
}
}
extension EnvironmentValues {
var safeAreaInsets: EdgeInsets {
self[SafeAreaInsetsKey.self]
}
}
private extension UIEdgeInsets {
var insets: EdgeInsets {
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
}
}
Usage
struct MyView: View {
#Environment(\.safeAreaInsets) private var safeAreaInsets
var body: some View {
Text("Ciao")
.padding(safeAreaInsets)
}
}
Or using SwiftUI GeometryReader api.
struct V: View {
var body: some View {
GeometryReader { geometry in
Text("\(geometry.safeAreaInsets.top)")
Text("\(geometry.safeAreaInsets.bottom)")
}
}
}
Code might not work, but illustrates the idea.
Objective-C
Who had the problem when keyWindow is equal to nil.
Just put the code above in viewDidAppear (not in viewDidLoad)
For iOS 13+/Swift 5, nothing else here worked for me but this:
if #available(iOS 13.0, *) {
topPadding = UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0
bottomPadding = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0
}
A more rounded approach
import SnapKit
let containerView = UIView()
containerView.backgroundColor = .red
self.view.addSubview(containerView)
containerView.snp.remakeConstraints { (make) -> Void in
make.width.top.equalToSuperView()
make.top.equalTo(self.view.safeArea.top)
make.bottom.equalTo(self.view.safeArea.bottom)
}
extension UIView {
var safeArea: ConstraintBasicAttributesDSL {
if #available(iOS 11.0, *) {
return self.safeAreaLayoutGuide.snp
}
return self.snp
}
var isIphoneX: Bool {
if #available(iOS 11.0, *) {
if topSafeAreaInset > CGFloat(0) {
return true
} else {
return false
}
} else {
return false
}
}
var topSafeAreaInset: CGFloat {
let window = UIApplication.shared.keyWindow
var topPadding: CGFloat = 0
if #available(iOS 11.0, *) {
topPadding = window?.safeAreaInsets.top ?? 0
}
return topPadding
}
var bottomSafeAreaInset: CGFloat {
let window = UIApplication.shared.keyWindow
var bottomPadding: CGFloat = 0
if #available(iOS 11.0, *) {
bottomPadding = window?.safeAreaInsets.bottom ?? 0
}
return bottomPadding
}
}
For those of you who change to landscape mode, you gotta make sure to use viewSafeAreaInsetsDidChange after the rotation to get the most updated values:
private var safeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
override func viewSafeAreaInsetsDidChange() {
if #available(iOS 11.0, *) {
safeAreaInsets = UIApplication.shared.keyWindow!.safeAreaInsets
}
}
Swift 4
if let window = UIApplication.shared.windows.first {
let topPadding = window.safeAreaInsets.top
let bottomPadding = window.safeAreaInsets.bottom
}
Use from class
class fitToTopInsetConstraint: NSLayoutConstraint {
override func awakeFromNib() {
if let window = UIApplication.shared.windows.first {
let topPadding = window.safeAreaInsets.top
self.constant += topPadding
}
}
}
class fitToBottomInsetConstraint: NSLayoutConstraint {
override func awakeFromNib() {
if let window = UIApplication.shared.windows.first {
let bottomPadding = window.safeAreaInsets.bottom
self.constant += bottomPadding
}
}
}
You will see safe area padding when you build your application.
extension UIViewController {
var topbarHeight: CGFloat {
return
(view.window?.safeAreaInsets.top ?? 0) +
(view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0) +
(self.navigationController?.navigationBar.frame.height ?? 0.0)
}
}
Here's a free function based on other answers that should be callable once your rootController is layed out from anywhere. You can use it as a free standing function.
func safeAreaInsets() -> UIEdgeInsets? {
(UIApplication
.shared
.keyWindow?
.rootViewController)
.flatMap {
if #available(iOS 11.0, *) {
return $0.view.safeAreaInsets
} else {
return .init(
top: $0.topLayoutGuide.length,
left: .zero,
bottom: $0.bottomLayoutGuide.length,
right: .zero
)
}
}
}
For iPhone 14 devices use like following.
let app = UIApplication.shared
var statusBarHeight: CGFloat = 0.0
let window = app.windows.filter {$0.isKeyWindow}.first
statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
let topPadding = window?.safeAreaInsets.top ?? 0.0
statusBarHeight = statusBarHeight >= topPadding ? statusBarHeight:topPadding
Here is a simple answer to find safe area height for all iphone
let window = UIApplication.shared.windows[0]
let SafeAreaHeight = window.safeAreaLayoutGuide.layoutFrame.size.height
I'm trying to implement a view very similar to Evernote's screen in which you add a New Note.
It seems like a UITableView embedded in a NavigationController. This tableview contains static cells (2 or 3) with the bottom one being a UITextView in which you add the content of the note, but when you scroll on the textView, the other cells that contain a textField and another control.
How can this be achieved? I know that Apple doesn't recommend a TextView inside a ScrollView, and doing it with table view it gets a bit weird with all the scrolling from the table and text view.
Here are some examples:
Any suggestions?
Thank you!
Firstly, They disabled text view scrolling and set its size to about screen size. Secondly, once text view's text is out of frame, expand it(calculate its size again).
So I found my problem, when I was setting the constraints for the content view (view inside scrollview) I set an Equal value for its height. To fix it I just made that relationship to Greater or Equal than... it now expands.
The other problem now is that when showing the keyboard it is not scrolling to the text I tap to. (The insets are properly setup though)
// MARK: Notififations from the Keyboard
func didShowKeyboard (notification: NSNotification) {
if momentTextView.isFirstResponder() {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.size.height, right: 0)
scrollView.scrollIndicatorInsets = scrollView.contentInset
let caretPosition = momentTextView.caretRectForPosition(momentTextView.selectedTextRange!.start)
let newHeight = caretPosition.height * 1.5
let newCaretPosition = CGRect(x: caretPosition.origin.x, y: caretPosition.origin.y, width: caretPosition.width, height: newHeight)
scrollView.scrollRectToVisible(newCaretPosition, animated: true)
}
}
}
func willHideKeyboard (notification: NSNotification) {
if momentTextView.isFirstResponder() {
scrollView.contentInset = UIEdgeInsetsZero
scrollView.scrollIndicatorInsets = UIEdgeInsetsZero
}
}
I am building a chat. Everything seem to be quite ok but I bumped into sort of 'buggy' problem.
i got UIViewController with UITextView bar for entering message and UITableView.
They are in this constraint: "V:|-(64)-[chatTable][sendMessageBar]-(keyboard)-|".
When the keyboard is not out - the constant of this constraint is 0. and after keyboard is out - i increase the constant to keyboard height.
when the keyboard is not out:
self.table.contentSize = (375.0,78.5)
self.table.bounds = (0.0,-490.0,375.0,568.5)
self.table.frame = (0.0,64.0,375.0,568.5)
self.table.subviews[0].frame (UITableViewWrapperView) = (0.0,0.0,375.0,568.5)
self.table.subviews[0].frame (UITableViewWrapperView) = (0.0,0.0,375.0,568.5)
and when the keyboard comes out:
self.table.contentSize = (375.0,78.5)
self.table.bounds = (0.0,-274.0,375.0,352.5
self.table.frame = (0.0,64.0,375.0,352.5)
self.table.subviews[0].frame (UITableViewWrapperView) = (0.0,-137.5,375.0,137.5)
self.table.subviews[0].frame (UITableViewWrapperView) = (0.0,0.0,375.0,137.5)
So the UITableViewWrapperView, after I increase constraints constant, differs in size to its superview - UITableView. Is there a way to fix this ? I would assume that UITableViewWrapperView would change its frame and bounds according to UITableView but it does not.
Any ideas where is the problem or how could I work around it ?
ADDING:
After some more research - it seems that it happens somewhere between viewWillLayoutSubviews and viewDidLayoutSubviews. It is kinda weird tho:
override func viewWillLayoutSubviews() {
println("WrapperView Frame :991: \(self.table.subviews[0].frame)") \\ WrapperView Frame :991: (0.0,0.0,375.0,568.5)
super.viewWillLayoutSubviews()
println("WrapperView Frame :992: \(self.table.subviews[0].frame)") \\ WrapperView Frame :992: (0.0,0.0,375.0,568.5)
}
override func viewDidLayoutSubviews() {
println("WrapperView Frame :6: \(self.table.subviews[0].frame)") \\ WrapperView Frame :6: (0.0,-137.5,375.0,137.5)
super.viewDidLayoutSubviews()
println(">> viewDidLayoutSubviews")
}
So it seems that something happens there that messes up the UITableViewWrapperView
The following fixed it for me:
func fixTableViewInsets() {
let zContentInsets = UIEdgeInsetsZero
tableView.contentInset = zContentInsets
tableView.scrollIndicatorInsets = zContentInsets
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
fixTableViewInsets()
}
I discovered that at viewWillAppear() that the insets were all 0. But at viewDidAppear(), they had been modified to apparently offset for navigation bar, etc. This makes the UITableViewWrapperView different from the UITableView.
I changed the insets in its own routine so that it was easier to experiment with calling it from different places. The viewWillLayoutSubviews() let it get changed before being presented - placing the change in viewDidAppear() caused the table to jerk.
I ran into this today and while the fix suggested by #anorskdev works nicely, it seems that the root cause of the issue is the automaticallyAdjustsScrollViewInsets property of UIViewController, which is true by default. I turned it off in my storyboard and the problem went away. Look for the "Adjust Scroll View Insets" checkbox in the View Controller inspector and make sure it's unchecked.
It seems that it is a bug (fighting with this bug took all day for me)
Finally this workaround helped:
for (UIView *subview in tableView.subviews)
{
if ([NSStringFromClass([subview class]) isEqualToString:#"UITableViewWrapperView"])
{
subview.frame = CGRectMake(0, 0, tableView.bounds.size.width, tableView.bounds.size.height);
}
}
After small investigation I have found this solution with setting all the safeAreaInsets and layoutMargins on the UITableView to zero:
Swift 4 snipset:
class CustomTableView: UITableView {
override var safeAreaInsets: UIEdgeInsets {
get {
return .zero
}
}
override var layoutMargins: UIEdgeInsets {
get {
return .zero
}
set {
super.layoutMargins = .zero
}
}
}
The main problem is safeAreaInsets introduced in tvOS 11.0 - the UITableViewWrapperView just took the properties from the parent view (UITableView) and renders the content with safeAreaInsets.
I was facing the same issue on tvOS 11.3, and neither of suggestions related with zero insets or scroll disable did the job, except looping through tableView's subviews and setting the UITableViewWrapperView's frame to the tableView's frame.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
for view in tableView.subviews {
if String(describing: type(of: view)) == "UITableViewWrapperView" {
view.frame = CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: tableView.bounds.size.height)
}
}
}
In iOS 11 UITableViewWrapperView has gone, so this problem may occur only on later iOS versions. I faced it on iOS10 when I pushed custom UIViewController in UINavigationController stack.
So, the solution is to override property automaticallyAdjustsScrollViewInsets in custom view controller like this:
override var automaticallyAdjustsScrollViewInsets: Bool {
get {
return false
}
set {
}
}
Objective C version of this answer given by anorskdev
- (void) viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[tableView setContentInset:UIEdgeInsetsZero];
[tableView setScrollIndicatorInsets:UIEdgeInsetsZero];
}
edit: Turning off automaticallyAdjustsScrollViewInsets on the hosting ViewController, as suggested by Steve Roy in this answer, also worked and is the one I went with, as it seems cleaner to disable the behaviour rather than correcting it afterwards.
If I draw my chart inside - (void)drawRect:(CGRect)rect is just enough to set [_chartView setContentMode:UIViewContentModeRedraw] and this method will be called when device changes it's orienatation and it's possible to calculate f.e. new center point for my chart.
If I create a view like a subview using - (id)initWithFrame:(CGRect)frame and then add it in view controller like [self.view addSubview:chartView];. How in this case I can handle rotation to redraw my chart?
While a preferred solution requires zero lines of code, if you must trigger a redraw, do so in setNeedsDisplay, which in turn invokes drawRect.
No need to listen to notifications nor refactor the code.
Swift
override func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
}
Objective-C
- (void)layoutSubviews {
[super layoutSubviews];
[self setNeedsDisplay];
}
Note:
layoutSubviews is a UIView method, not a UIViewController method.
To make your chart rendered correctly when device orientation changes you need to update chart's layout, here is the code that you should add to your view controller:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_chartView.frame = self.view.bounds;
[_chartView strokeChart];
}
Zero Lines of Code
Use .redraw
Programmatically invoking myView.contentMode = .redraw when creating the custom view should suffice. It is a single flag in IB and, as such, the 0 lines of code prefered way. See Stack Overflow How to trigger drawRect on UIView subclass.
Go here to learn how to receive notifications for when the device orientation changes. When the orientation does change, just call [chartView setNeedsDisplay]; to make drawRect: get called so you can update your view. Hope this helps!
The code you'll add to your view controller:
- (void)updateViewConstraints
{
[super updateViewConstraints];
[_chartView setNeedsDisplay];
}
Unfortunately some answers suggest to override controller methods, but I've some custom UITableViewCells with a shadow around and rotating the device stretches the cells but doesn't redraw the shadow. So I don't want to put my code within a controller to (re)draw a subview.
The solution for me is to listen to a UIDeviceOrientationDidChange notification within my custom UITableViewCell and then call setNeedsDisplay() as suggested.
Swift 4.0 Code example in one of my custom UITableViewCells
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChangeNotification), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
#objc func deviceOrientationDidChangeNotification(_ notification: Any) {
Logger.info("Orientation changed: \(notification)")
setNeedsLayout()
}
BTW: Logger is a typealias to SwiftyBeaver.
Thanks to #Aderis pointing me to the right direction.
I tried "setNeedsDisplay" in a dozen ways and it didn't work for adjusting the shadow of a view I was animating to a new position.
I did solve my problem with this though. If your shadow doesn't seem to want to cooperate/update, you could try just setting the shadow to nil, then setting it again:
UIView.animateKeyframes(withDuration: 0.2 /*Total*/, delay: 0.0, options: UIViewKeyframeAnimationOptions(), animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 10/10, animations:{
// Other things to animate
//Set shadow to nil
self.viewWithShadow.layer.shadowPath = nil
})
}, completion: { finished in
if (!finished) { return }
// When the view is moved, set the shadow again
self.viewWithShadow.layer.shadowPath = UIBezierPath(rect: self.descTextView.bounds).cgPath
})
If you don't even have a shadow yet and need that code, here's that:
func addShadowTo (view: UIView) {
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.gray.cgColor
view.layer.shadowOffset = CGSize( width: 1.0, height: 1.0)
view.layer.shadowOpacity = 0.5
view.layer.shadowRadius = 6.0
view.layer.shadowPath = UIBezierPath(rect: view.bounds).cgPath
view.layer.shouldRasterize = true
}
Thanks Keenle, for the above ObjC solution. I was messing around with creating programmed constraints all morning, killing my brain, not understanding why they would not recompile/realign the view. I guess because I had created the UIView programmatically using CGRect...this was taking precedence.
However, here was my solution in swift:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myUIView.center = CGPoint(x: (UIScreen.mainScreen().bounds.width / 2), y: (UIScreen.mainScreen().bounds.height / 2))
}
So relieved! :)