I have a view that I assign as the inputAccessoryView of my view controller. I adjusted the constraints so that it's above the home indicator on iPhone X (X, XR, XS, XS Max). I keep that inputAccessoryView pinned to the bottom of the screen when the keyboard is hidden by setting canBecomeFirstResponder to true in my view controller.
It works on all phones except for the X family. On all of the X phones, my view is not getting any touch events - they are hitting the view below it, as if inputAccessoryView wasn't there.
If I constrain the bottom to superview instead of safeArea, it works, but then it's too close to the home indicator, which I don't want:
In this radar ("inputAccessoryViews should respect safe area inset with external keyboard on iPhone X"), Apple engineers say that this is not a bug and that developers should constrain one of the views to its parent's safeAreaLayoutGuide:
Engineering has provided the following feedback regarding this issue: It’s your responsibility to respect the input accessory view’s safeAreaInsets. We designed it this way so developers could provide a background view (i.e., see Safari’s Find on Page input accessory view) and lay out the content view with respect to safeAreaInsets. This is fairly straightforward to accomplish. Have a view hierarchy where you have a container view and a content view. The container view can have a background color or a background view that encompasses its entire bounds, and it lays out it’s content view based on safeAreaInsets. If you’re using autolayout, this is as simple as setting the content view’s bottomAnchor to be equal to it’s superview’s safeAreaLayoutGuide.
I believe that's exactly what I'm doing, but I'm clearly missing something since it breaks on X.
GitHub project is here: https://github.com/nambatee/HorizontallyScrollableToolbarAccessoryView/tree/master/Horizontally%20Scrollable%20Toolbar%20Accessory%20View
Your problem is actually is that you are not setting a height for inputAccessoryView and your scrollView is out of bounds that's why you are not getting the touch events and just to prove that if you go to your top level view and set it to clips to bounds and run your project this is what you get
So why is this happening? it's because you view is not having an intrinsicContentSize you can give it an intrinsicContentSize by subclassing UIView and override intrinsicContentSize and return what ever width and height you want just like this
class CustomView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize.zero
}
}
Don't forget to set your view to be CustomView in code and in the nib file
private lazy var horizontallyScrollableToolbarAccessoryView: CustomView? = {
let nibName = "HorizontallyScrollableToolbarAccessoryView"
let view = Bundle.main.loadNibNamed(nibName, owner: nil, options: nil)?.first as? CustomView
return view
}()
you can also provide the height through a height constraint like this
override func viewDidLoad() {
super.viewDidLoad()
self.horizontallyScrollableToolbarAccessoryView?.heightAnchor.constraint(equalToConstant: 80).isActive = true
}
but you will still need to set your intrinsicContentSize to be CGSize.Zero
class CustomView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize.zero
}
}
and that's what you get at the end a working scrollView
Related
I want to build a TikTok-like vertical CollectionView in which each cell is an UIViewController.
I'm having issues with managing safe area insets in those content View Controllers.
Since they are inside the scroll view, I don't want their safe-area insets to update as their change their position.
I've tried "disabling" safe area insets on some view by overriding safeAreaInsets property and returning zero insets.
// In my subclass of UICollectionViewCell
// MyCell.swift
override var safeAreaInsets: UIEdgeInsets {
.zero
}
However, it doesn't always work - especially when I add another UIViewController's view inside that view.
Here's the video that demonstrates the issue: Video link
What is the best way to control safe area insets in scroll views?
Is there any way to force UIView/UIViewController to have specific safe area insets that doesn't change as the view is changing its position (and going beneath the iPhones notch)?
I have a nested collectionView which I would like to get the readableContentGuide for after rotation in order to set the content inset correctly.
This is what it looks like:
I have already tried to subclass the collection view and get the value from layoutMarginsDidChange, traitCollectionDidChange, and layoutSubviews.
However the value I get there is always the previous value (i.e when I'm in portrait I get the landscape value and vice versa)
I have also tried to set the inset in the collectionView's collectionView(_:layout:insetForSectionAt:).
Currently, the only solution that seems to work is observing the bounds of the collection view, but that feels a bit hacky.
Any thoughts on how this can be done?
If you are using Autolayout on storyboard you should activate "Follow Readable Width" option for the superview. First, make sure that the collection view is attached to the superview's margins. Then go to the superview and open Size Inspector, and select the option:
Thanks matt for the answer.
For programmatic Autolayout you don't need margins, just attach the collection view to the readableContentGuide of the superview. Like this:
let cv = collectionView
// Guide of the superview
let readableGuide = view.readableContentGuide
NSLayoutConstraint.activateConstraints([
cv.topAnchor.constraintEqualToAnchor(readableGuide.topAnchor),
cv.bottomAnchor.constraintEqualToAnchor(readableGuide.bottomAnchor),
cv.rightAnchor.constraintEqualToAnchor(readableGuide.rightAnchor),
cv.leftAnchor.constraintEqualToAnchor(readableGuide.leftAnchor)
])
If you prefer frame-based programmatic layout you don't need to use layoutMarginsDidChange, traitCollectionDidChange of the superview, neither observe bounds. The best place for layout code is viewWillLayoutSubviews() func of the controller. This will handle any bounds changes including interface rotations.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionView.frame = view.readableContentGuide.layoutFrame
collectionView.collectionViewLayout.invalidateLayout()
}
Here I explain why we need to invalidateLayout().
I'm having trouble getting this working, best explained what i am trying to do with this image of my storyboard.
The main goal is to make that container scrollable, with its size dependent on the view controller that gets added into it via code.
Here is how i have laid out my views.
Obviously my constraints are what is messing me up, but not exactly sure how to get them to behave as expected, I've tried pinning the container to the scroll view and then the scroll view to the main view. The screen appears how i want but it just doesn't scroll.
Any help is much appreciated!
It's impossible from your question & screenshots to determine what constraints you actually do have set up. And your question doesn't even particularly make it clear whether your constraints are behaving properly (aside from the scroll view not scrolling, which may or may not be related to constraints).
First, we need to make sure our constraints are hooked up correctly. The container view for which you're going to put a scroll view into should have four constraints. One for each side, left & right, pinning it to those edges. One for the bottom to pin it to the toolbar, and one for the top to pin it to your top views. It doesn't need any more constraints. Now, the toolbar at the bottom and your views at the top need to have their constraints set up so that they have a constant height per device/orientation, and does not care about the size of the container view. If your constraints are set up correctly, then resizing this view in your interface builder file should be changing the size of the container view. If this isn't happening, head back to the drawing board because something isn't quite right.
If you are confident your constraints are set up correctly, there are a few other things about the scrollview itself that could prevent scrolling.
First, check the most obvious. Is the scrolling enabled property set to true on your scroll view? If not... of course it's not going to scroll.
The other thing that could be happening is that your content view is not larger than the scroll view. Make sure that you've properly set up autolayout for the contents of the scroll view, otherwise the content view will not be larger than the scroll view, and no scrolling will be happening.
So for anyone wondering how to do this, i finally figured out how to do it after a lot of trial and error.
Basically what I did was remove the container view from the storyboard, set my constraints on the scroll view as i normally would:
and then added a container view as a subview of the scroll view in code:
var containerView : UIView!
var currentViewController: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
self.containerView = UIView()
self.scrollView.addSubview(self.containerView)
}
To swap between different view controllers using my UISegmentControl and have the scroll view scroll the content no matter how large the new view controller is i added the following code in my UISegmentControl functions
#IBAction func segControlValueChanged(sender: AnyObject) {
if let vc = viewControllerForSelectedSegmentIndex(sender.selectedSegmentIndex) {
self.currentViewController!.view.removeFromSuperview()
self.currentViewController!.removeFromParentViewController()
displayCurrentTab(sender.selectedSegmentIndex)
}
}
func displayCurrentTab(tabIndex: Int){
if let vc = viewControllerForSelectedSegmentIndex(tabIndex) {
self.addChildViewController(vc)
self.containerView.addSubview(vc.view)
vc.didMoveToParentViewController(self)
vc.view.frame.size = CGSize(width: self.view.frame.width, height: vc.view.frame.height) //had to add this because otherwise for some reason my new vc width would not increase or decrease to its parent view
self.currentViewController = vc
self.scrollView.contentSize = vc.view.frame.size
}
}
func viewControllerForSelectedSegmentIndex(index: Int) -> UIViewController {
//instantiate and return your view controller here
}
Some other notes:
I disabled "resize view from NIB" in storyboard for all my view controllers i plan to add to the container
I'm attempting to make a UIScrollView only allow zooming in the horizontal direction. The scroll view is setup with a pure autolayout approach. The usual approach is as suggested in a number of Stack Overflow answers (e.g. this one) and other resources. I have successfully used this in apps before autolayout existed. The approach is as follows: in the scroll view's content view, I override setTransform(), and modify the passed in transform to only scale in the x direction:
override var transform: CGAffineTransform {
get { return super.transform }
set {
var t = newValue
t.d = 1.0
super.transform = t
}
}
I also reset the scroll view's content offset so that it doesn't scroll vertically during the zoom:
func scrollViewDidZoom(scrollView: UIScrollView) {
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
}
This works very nicely when not using autolayout:
However, when using autolayout, the content offset ends up wrong during zooming. While the content view only scales in the horizontal direction, it moves vertically:
I've put an example project on GitHub (used to make the videos in this question). It contains two storyboards, Main.storyboard, and NoAutolayout.storyboard, which are identical except that Main.storyboard has autolayout turned on while NoAutolayout does not. Switch between them by changing the Main Interface project setting and you can see behavior in both cases.
I'd really rather not switch off autolayout as it solves a number of problems with implementing my UI in a much nicer way than is required with manual layout. Is there a way to keep the vertical content offset correct (that is, zero) during zooming with autolayout?
EDIT: I've added a third storyboard, AutolayoutVariableColumns.storyboard, to the sample project. This adds a text field to change the number of columns in the GridView, which changes its intrinsic content size. This more closely shows the behavior I need in the real app that prompted this question.
Think I figured it out. You need to apply a translation in the transform to offset the centering UIScrollView tries to do while zooming.
Add this to your GridView:
var unzoomedViewHeight: CGFloat?
override func layoutSubviews() {
super.layoutSubviews()
unzoomedViewHeight = frame.size.height
}
override var transform: CGAffineTransform {
get { return super.transform }
set {
if let unzoomedViewHeight = unzoomedViewHeight {
var t = newValue
t.d = 1.0
t.ty = (1.0 - t.a) * unzoomedViewHeight/2
super.transform = t
}
}
}
To compute the transform, we need to know the unzoomed height of the view. Here, I'm just grabbing the frame size during layoutSubviews() and assuming it contains the unzoomed height. There are probably some edge cases where that's not correct; might be a better place in the view update cycle to calculate the height, but this is the basic idea.
Try setting translatesAutoresizingMaskIntoConstraints
func scrollViewDidZoom(scrollView: UIScrollView) {
gridView.translatesAutoresizingMaskIntoConstraints = true
}
In the sample project it works. If this is not sufficient, try creating new constraints programmatically in scrollViewDidEndZooming: might help.
Also, if this does not help, please update the sample project so we can reproduce the problem with variable intrinsicContentSize()
This article by Ole Begemann helped me a lot How I Learned to Stop Worrying and Love Cocoa Auto Layout
WWDC 2015 video
Mysteries of Auto Layout, Part 1
Mysteries of Auto Layout, Part 2
And so luckily, there's a flag for that. It's called translatesAutoResizingMask IntoConstraints [without space].
It's a bit of a mouthful, but it pretty much does what it says. It makes views behave the way that they did under the Legacy Layout system but in an Auto Layout world.
Although #Anna's solution works, UIScrollView provides a way of working with Auto Layout. But because scroll views works a little differently from other views, constraints are interpreted differently too:
Constraints between the edges/margins of scroll view and its contents attaches to the scroll view's content area.
Constraints between the height, width, or centers attach to the scroll view’s frame.
Constraints between scroll view and views outside scroll view works like an ordinary view.
So, when you add a subview to the scroll view pinned to its edges/margins, that subview becomes the scroll view's content area or content view.
Apple suggests the following approach in Working with Scroll Views:
Add the scroll view to the scene.
Draw constraints to define the scroll view’s size and position, as normal.
Add a view to the scroll view. Set the view’s Xcode specific label to Content View.
Pin the content view’s top, bottom, leading, and trailing edges to the scroll view’s corresponding edges. The content view now defines
the scroll view’s content area.
(...)
(...)
Lay out the scroll view’s content inside the content view. Use constraints to position the content inside the content view as normal.
In your case, the GridView must be inside the content view:
You can keep the grid view constraints that you have been using but, now, attached to content view. And for the horizontal-only zooming, keep the code exactly as it is. Your transform overriding handle it very well.
I'm not sure if this satisfies your requirement of 100% autolayout, but here's one solution, where the UIScrollView is set with autolayout and gridView added as a subview to it.
Your ViewController.swift should look like this:
// Add an outlet for the scrollView in interface builder.
#IBOutlet weak var scrollView: UIScrollView!
// Remove the outlet and view for gridView.
//#IBOutlet var gridView: GridView!
// Create the gridView here:
var gridView: GridView!
// Setup some views
override func viewDidLoad() {
super.viewDidLoad()
// The width for the gridView is set to 1000 here, change if needed.
gridView = GridView(frame: CGRect(x: 0, y: 0, width: 1000, height: self.view.bounds.height))
scrollView.addSubview(gridView)
scrollView.contentSize = CGSize(width: gridView.frame.width, height: scrollView.frame.height)
gridView.contentMode = .ScaleAspectFill
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return gridView
}
func scrollViewDidZoom(scrollView: UIScrollView) {
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
}
I want to create simple custom view and use it for navigationItem.titleView. My custom view is very simple, has only image view and label: [image]-[label]. I have xib file where I have defined all constraints (both subviews has constraint to superview - I want to height and width of subviews determine height and width of entire custom view). The problem is that when I instantiate view from xib like this:
class func titleViewWithTitle(title: String, icon: UIImage?) -> TitleView {
let titleView = NSBundle.mainBundle().loadNibNamed("TitleView", owner: self, options: nil).first as TitleView
titleView.title.text = title
titleView.icon.image = icon
titleView.layoutSubviews()
return titleView
}
the size of this view is not determined by it's children. It has the size that was manually set in Interface Builder. What can I do to force the view to dynamically calculate size?
In your custom view, override the layoutSubviews method and add this:
CGSize targetSize = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
Create a CGRectFrame from this and set this frame as a new frame for self (which is the custom view).
This will basically find the smallest possible frame that still encompasses its content in a way that all content is shown.
Note: All subviews of this custom view must have autolayout compression resistance set properly. I.e. they must resist shrinking.