Unwanted white space on left of UIScrollView on iPhone X - ios

UIScrollView works fine without the white space on the left on all iPads or iPhones except for iPhone X. How can I remove the white space?
I use storyboards. Bounce On Scroll/Zoom are all disabled. No white space on iPad or iPhone except for iPhone X. I think it might be something related to the Safe Area thing.

This spacing is from safe area, which is applied to left/right of UIScrollview as content insets in landscape orientation on iPhone X, which can be seen using read-only property UIScrollview.safeAreaInsets.
Following line can be used to get rid of safe area insets when you dont need:
UIScrollview.contentInsetAdjustmentBehavior = .never
The default value being UIScrollViewContentInsetAdjustmentBehavior.automatic includes safe area layout guide margins as content insets.
Note: auto layout constraints has nothing to do with the insets, its just iOS 11 UIScrollview content insets adjustment behavior.

Setting the constraint relative to safeArea is good practise for iPhone-X.
This is how apple says -
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 your case you are giving constraints leading & trailing of scrollView with safeArea, Not superView
Hence if you take risk giving constraint to superview instead of safeArea your object content may clipped, specially when you rotate left, content from the left most will clip under top notch of iPhone-X.
Apple doc for safeAreaLayoutGuide

Well, I solved this issue in non-elegant way. But it works like a charm. (I tried all other answers. Thank for your help, however those answers don't seem to work in my case.)
var leftMargin: CGFloat = 0
var rightMargin: CGFloat = 0
if Device.isPhone() && Device.IS_5_8_INCHES() {
self.leftMargin = 44
self.rightMargin = 44
}
let frame = CGRect(
x: (self.view.frame.width - self.leftMargin - self.rightMargin) * CGFloat(pageIndex),
y: ...
)

Safe Area Layout is responsible for this white space.
1st Option:
Ignore safe area layout for your scrollview and set scrollview's constraints with respect to its super view (or main view). Scrollview automatically handle safe area inset for contents while scrolling.
Landscape View:
Portrait View:
2nd Option:
I do not recommend to remove/change safe area layout for your scroll view and an alternate solution that can solve white space visibility issue:
Set blue background color (that you've applied for your scroll view) to your main view of your view controller, if scroll view is covering entire screen.
set clear color background for your scroll view
Add this code in your viewDidLoad
# IBOutlet var scrollView: UIScrollView!
override func viewDidLoad(){
super.viewDidLoad()
self.view.backgroundColor = UIColor.yellow // set here blue color that you've applied for your scroll view
self.scrollView.backgroundColor = UIColor.clear
}
Here are good reference answers regarding Safe Area Layout, for better understanding:
Safe Area of Xcode 9
Use Safe Area Layout programmatically

Related

how to make buttons in stackview change size depending on screen size (using main.storyboard or code)

I am trying to figure out how to make my three buttons in a stackview have relative sizing and keep the same multiplier spacing on different devices, e.g. the buttons will be bigger if i am on an ipad compared to an iphone. Secondly the spacing between left and right edges of buttons will be bigger on an ipad compared to that of an iphone device. So far I currently have three buttons in a stackview. I have added horizontally and vertical allignment to my stackview. I have played around with adding equal heights and width and changed the multiplier as well as adding constraints to the buttons however it did not get my desired result.
Here is a screenshot of how i'd like my items to be placed on all devices:
UIStackView has only static spacing. You have two options:
Change spacing inside code with viewWillLayoutSubviews. I prefer it over viewDidLayoutSubviews because changes will follow rotation animation. But if your calculations will depend on other subviews(not just self.view), these frames will not be updated yet. You can change constraint.constant like this too.
#IBOutlet var stackView: UIStackView!
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
stackView.spacing = view.bounds.height * 0.05
}
Using only storyboard, you can't add spacing constraint with modifier. But you can add a transparent view which will be your spacer, and apply width/height modifier to it. This can be used both for UIStackView and for plain views.
I prefer adding equal height constraint for all views with same size(like btn1.height = btn2.height, btn1.height = btn3.height), so I can set size with one constraint to all of them(btn1.height = superview.height * 0.1).
Something like this should work for your:
Result:

Does the UIScrollView automatically adjust contentOffset to fit inside the safe area?

I have a UIScrollView and 3 UIViews that I add to it.
I have triple checked the frame of the UIScrollView, the frame of the UIViews, it's contentOffset, the frame of the superview, generally everything relevant.
For some reason, this is what appears when I set the contentOffset to be (0,0):
There's no view in my project that starts from that offset origin point.
The scrollView assumes (0, 0) to be something around (0, 50) which is the height of the navigation bar even though I'm not using one.
EDIT
Weird enough if I open the app while the device is in landscape mode the contentOffset behaves normal. So, it has to be some automatic adjustment because of the chin of the device.
EDIT 2
I opened the app on a device without a chin (iPhone 8) then the contentOffset automatically adjusts to the height of the status bar. Meaning (0, 0) is not (0, 0) but (0, heightOfStatusBar).
EDIT 3
On an iPad it behaves normally even though iPads do have a status bar!
How do I fix this to be (0,0) in every case ?
UIScrollView has a property where it "insets" the content so that the safe areas are not overlapped.
It can be changed by setting setting the contentInsetAdjustmentBehavior property of the UIScrollView instance like so:
scrollView.contentInsetAdjustmentBehavior = .never
Note that it is not the contentOffset, but contentInset that gets adjusted. This is why the content offset is still (0, 0) in all cases.
Also, if you are building for an iOS Development Target lower than iOS 11.0, you might get a build error. In that case you can use the following code instead.
if #available(iOS 11.0, *){
scrollView.contentInsetAdjustmentBehavior = .never
}
References
Apple Developer Docs - contentinsetadjustmentbehavior
No, contentOffset does not affect to safe area and vice versa.
contentOffset is used for programming scroll inside scrollView.
If you want to link UIScrollView to frame's bounds you have to use Autolayout and activate some constraints. Step by step:
set property translatesAutoresizingMaskIntoConstraints of scroll view to false
scrollView.translatesAutoresizingMaskIntoConstraints = false
init constraints
activate constrains (in viewDidLoad)
scrollView.translatesAutoresizingMaskIntoConstraints = false
let constraints: [NSLayoutConstraint] = [
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
scrollView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor)
]
NSLayoutConstraint.activate(constraints)
As a result your scroll view will be linked to edges of phone bounds. And contentOffset wil be equal (0, 0). Instead if you want to link scrollView to safe area you have to do use view.layoutMarginsGuide, where you can find various anchors.
Every UIView object works with parent view coordinating system. Your scrollView does not know anything about status bar, navigation bar and toolbars. For example, frame origin of any UIView is (0,0).
So, in summary:
to link you scroll view to safe area you need to set constraints.
contentOffset is used for programming scroll inside scrollView.

Center a scrollable InputAccessoryView

All,
I have a scrollable InputAccessoryView attached to my UITextView.
This was created as follows:
create a UIScrollView
add a horizontal UIStackView.
Attach top, bottom, trailing, leading of stack view to scroll view.
Set equal heights between stack and scroll.
Insert lots of buttons into stack view.
set TextView.inputaccessoryview = scrollview.
Voila! (Note - only the last line was programmatic).
So this works fine and correctly scrolls when the content (stack view) is wider than keyboard, BUT when you rotate to landscape (or run on an iPad), it is left aligned.
I'd like the buttons to be centred when the keyboard is wider than the set of buttons.
I've tried embedding the Scrollview into a UIView with Center X, but that doesn't seem to work.
Can anyone give me some pointers?
Thx
Found it!
The answer is to use ContentInset on the scrollbar (which is the InputAccessoryView above). The specific code is:
public void CentreToolbar()
{
var offsetX = Math.Max((scrollView.Bounds.Width - scrollView.ContentSize.Width) / 2, 0);
scrollView.ContentInset = new UIEdgeInsets(0, (nfloat)offsetX, 0, 0);
}
This code should be called anytime the layout of the screen changes - such as during rotation.

Hiding a view when orientation changes to landscape in autolayouts

I am creating a sample application in which i am copying Facebook screens in order to practice auto layouts.
When I run login screen in portrait mode, it looks perfect.
The problem is as soon as the orientation changes to landscape, all the views collapse because of header image, as shown here
What i want is that, in landscape mode, header image disappears so that other views get its space. I don't want to use scrollview.
I tried this:
headerImageView.isHidden = true
But the result came out to be this
The imageview got disappeared but didn't leave its space.
Can anyone suggest me a good solution?
P.s
Sorry for the images being this way because of my reputation.
When using Auto Layout you can leverage Size Classes.
See description below or example here: https://github.com/jonaszmclaren/AutolayoutExample
Set image view for compact width and height (wC hC - iPhone in landscape) and for wR hC (iPhone Plus in landscape) to not installed:
Constraint between text field and image view not enabled for wC hC and wR hC:
And finally for wC hC and wR hC you have to define text fields's top contraint - I did it to the top of the view.
This way, image view for portait will be visible and text view pinned to image view, and in landscape image view will be hidden and text field pinned to top of the view.
The best way is to use scrollView in such type of scenarios. If you don't want to have the scrollView, then you must give the bottom constraint for last button, and set the priority low of that particular constraint. It will work fine for current screen(both landscape and portrait), but when you'll go for small screen i.e 4s or 5, then purpose of auto layout will fail.
If you hide the image than it will only not show to user But Space will be used by Image on screen. Better Approach is you can set the Height Of Image 0 when orientation change to Landscape. You can create the Outlet of Height Constraint of Image and Change it according to Orientation.This method is called before orintation change. You need to Create outlat of Height constraint of Image.
#IBOutlet var heightConstraint : NSLayoutConstraint!
override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval)
{
if toInterfaceOrientation == .landscapeLeft || toInterfaceOrientation == .landscapeRight{
// Imageview height constraint outlate
heightConstraint.constant = 0
}
else{
heightConstraint.constant = 100
}
}
isHidden will just changed the visibility of the view. It will not remove it from that position. To solve this issue create a outlet of height constraint of header view and changed it to 0 on orientation change.
ex:
headerViewHeightConstraint.constant = 0.0
self.view.layoutIfNeeded()
and to restore it on portrait mode set height again.
headerViewHeightConstraint.constant = // height value which you want to set
self.view.layoutIfNeeded()
Another option could be to place your view inside a stack view. Then hiding the headerImageView should recover the unused space.

UIScrollView not scrolling

I have a UIScrollView which contains many UIImageViews, UILabels, etc... the labels are much longer that the UIScrollView, but when I run the app, I cannot click and scroll down...
Why might this be?
Thanks
It's always good to show a complete working code snippet:
// in viewDidLoad (if using Autolayout check note below):
UIScrollView *myScrollView;
UIView *contentView;
// scrollview won't scroll unless content size explicitly set
[myScrollView addSubview:contentView];//if the contentView is not already inside your scrollview in your xib/StoryBoard doc
myScrollView.contentSize = contentView.frame.size; //sets ScrollView content size
Swift 4.0
let myScrollView
let contentView
// scrollview won't scroll unless content size explicitly set
myScrollView.addSubview(contentView)//if the contentView is not already inside your scrollview in your xib/StoryBoard doc
myScrollView.contentSize = contentView.frame.size //sets ScrollView content size
I have not found a way to set contentSize in IB (as of Xcode 5.0).
Note:
If you are using Autolayout the best place to put this code is inside the -(void)viewDidLayoutSubviews method .
If you cannot scroll the view even after you set contentSize correctly,
make sure you uncheck "Use AutoLayout" in Interface Builder -> File Inspector.
You need to set the contentSize property of the scroll view in order for it to scroll properly.
If you're using autolayout, you need to set contentSize in viewDidLayoutSubviews in order for it to be applied after the autolayout completes.
The code could look like this:
-(void)viewDidLayoutSubviews
{
// The scrollview needs to know the content size for it to work correctly
self.scrollView.contentSize = CGSizeMake(
self.scrollContent.frame.size.width,
self.scrollContent.frame.size.height + 300
);
}
The answer above is correct - to make scrolling happen, it's necessary to set the content size.
If you're using interface builder a neat way to do this is with user defined runtime attributes. Eg:
Try to resize the content size to huge numbers. I couldn't understand why my scroll view doesn't scroll even when its content size seems to be bigger than control size. I discovered that if the content size is smaller than needed, it doesn't work also.
self.scrollView.contentSize = CGSizeMake(2000, 2000);
Instead of 2000 you can put your own big numbers. And if it works, it means that your content size is not big enough when you resize.
The delegate is not necessary for scroll view to work.
Make sure you have the contentSize property of the scroll view set to the correct size (ie, one large enough to encompass all your content.)
Uncheck 'Use Autolayout' did the trick for me.
Environment:
xCode 5.0.2
Storyboards
ios7
In my case I had to set delaysContentTouches to true because the objects inside the scrollView were all capturing the touch events and handling themselves rather than letting the scrollView itself handle it.
Set contentSize property of UIScrollview in ViewDidLayoutSubviews method. Something like this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = CGSizeMake(view.frame.size.width, view.frame.size.height)
}
if you are getting a message (IOS8 / swift) that viewDidLayoutSubviews does not exist, use the following instead
override func viewDidAppear(animated: Bool)
This fixed it for me
The idea of why scroll view is not scrolling because you set the content size for scrolling less than the size of the scroll view, which is wrong.
You should set the content size bigger than the size of your scroll view to navigate through it while scrolling.
The same idea with zooming, you set the min and max value for zooming which will applied through zooming action.
welcome :)
One small addition, all above are the actual reasons why your scroll view might not be scrolling but sometimes mindlessly this could be the reason specially when scrollview is added through code and not IB, you might have added your subviews to the parent view and not to the scrollview this causes the subview to not scroll
and do keep the content size set and bigger than parent view frame (duhh!!)
I made it working at my first try. With auto layout and everything, no additional code. Then a collection view went banana, crashing at run time, I couldn't find what was wrong, so I deleted and recreated it (I am using Xcode 10 Beta 4. It felt like a bug) and then the scrolling was gone. The Collection view worked again, though!
Many hours later.. this is what fixed it for me. I had the following layout:
UIView
Safe Area
Scroll view
Content view
It's all in the constraints. Safe Area is automatically defined by the system. In the worst case remove all constraints for scroll and content views and do not have IB resetting/creating them for you. Make them manually, it works.
For Scroll view I did: Align Trailing/Top to Safe Area. Equal Width/Height to Safe area.
For Content view I did: Align Trailing/Leading/Top/Bottom to Superview (the scroll view)
basically the concept is to have Content view fitting Scrollview, which is fitting Safe Area.
But as such it didn't work. Content view missed the height. I tried all I could and the only one doing the trick has been a Content view height created control-dragging Content view.. to itself. That defined a fixed height, which value has been computed from the Size of the the view controller (defined as freeform, longer than the real display, to containing all my subviews) and finally it worked again!
Add the UIScrollViewDelegate and adding the following code to the viewDidAppear method fixed it for me.
#interface testScrollViewController () <UIScrollViewDelegate>
-(void)viewDidAppear:(BOOL)animated {
self.scrollView.delegate = self;
self.scrollView.scrollEnabled = YES;
self.scrollView.contentSize = CGSizeMake(375, 800);
}
My issue was resolved by:
setting the contentSize on the scrollView to a large height
BUT also I had to fix top and/or bottom constraints on views within the scrollView, which meant the scroll indicators showed on screen but the content did not scroll
Once I removed top and/or bottom constraints bound to the safe area and/or superview, the views inside the scrollView could scroll again and didn't stay fixed to the top of bottom of the screen!
Hope this stops someone else from hours of pain with this particular issue.
yet another fun case:
scrollview.superview.userInteractionEnabled must be true
I wasted 2+hrs chasing this just to figure out the parent
is UIImageView which, naturally, has userInteractionEnabled == false
Something that wasn't mentioned before!
Make sure your outlet was correctly connected to the scrollView! It should have a filled circle, but even if you have filled circle, scrollView may not been connected - so double check! Hover over the circle and see if the actual scrollview gets highlighted! (This was a case for me)
//Connect below well to the scrollView in the storyBoard
#property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
Alot of the time the code is correct if you have followed a tutorial but what many beginners do not know is that the scrollView is NOT going to scroll normally through the simulator. It is suppose to scroll only when you press down on the mousepad and simultaneously scroll. Many Experienced XCode/Swift/Obj-C users are so use to doing this and so they do not know how it could possibly be overlooked by beginners. Ciao :-)
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
// Do any additional setup after the view
}
override func viewWillLayoutSubviews(){
super.viewWillLayoutSubviews()
scrollView.contentSize = CGSize(width: 375, height: 800)
}
This code will work perfectly fine as long as you do what I said up above
If none of the other solutions work for you, double check that your scroll view actually is a UIScrollView in Interface Builder.
At some point in the last few days, my UIScrollView spontaneously changed type to a UIView, even though its class said UIScrollView in the inspector. I'm using Xcode 5.1 (5B130a).
You can either create a new scroll view and copy the measurements, settings and constraints from the old view, or you can manually change your view to a UIScrollView in the xib file. I did a compare and found the following differences:
Original:
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" directionalLockEnabled="YES" bounces="NO" pagingEnabled="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Wsk-WB-LMH">
...
</scrollView>
After type spontaneously changed:
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" customClass="UIScrollView" id="qRn-TP-cXd">
...
</view>
So I replaced the <view> line with my original <scrollView> line.
I also replaced the view's close tag </view> with </scrollView>.
Be sure to keep the id the same as the current view, in this case: id="qRn-TP-cXd".
I also had to flush the xib from Xcode's cache by deleting the app's derived data:
Xcode->Window->Organizer->Projects, choose your project, on the Derived Data line, click Delete...
Or if using a device:
Xcode->Window->Organizer->Device, choose your device->Applications, choose your app, click (-)
Now clean the project, and remove the app from the simulator/device:
Xcode->Product->Clean
iOS Simulator/device->press and hold the app->click the (X) to remove it
You should then be able to build and run your app and have scrolling functionality again.
P.S. I didn't have to set the scroll view's content size in viewDidLayoutSubviews or turn off auto layout, but YMMV.
If your scrollView is a subview of a containerView of some type, then make sure that your scrollView is within the frame or bounds of the containerView. I had containerView.clipsToBounds = NO which still allowed me see the scrollView, but because scrollView wasn't within the bounds of containerView it wouldn't detect touch events.
For example:
containerView.frame = CGRectMake(0, 0, 200, 200);
scrollView.frame = CGRectMake(0, 200, 200, 200);
[containerView addSubview:scrollView];
scrollView.userInteractionEnabled = YES;
You will be able to see the scrollView but it won't receive user interactions.
adding the following code in viewDidLayoutSubviews worked for me with Autolayout. After trying all the answers:
- (void)viewDidLayoutSubviews
{
self.activationScrollView.contentSize = CGSizeMake(IPHONE_SCREEN_WIDTH, 620);
}
//set the height of content size as required
The straightforward programmatically way
To wrap it up
Create a UIScrollView
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
Use a Single Child View to Hold All of Your Content Subviews
private lazy var contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
Add your views
contentView.addSubview(firstSubView)
contentView.addSubview(lastSubView)
scrollView.addSubview(contentView)
view.addSubview(scrollView)
Usually, you only want your content to scroll in one direction. In most cases to scroll vertically. Therefore set the width of the content view to be the width of the scroll view.
NSLayoutConstraint.activate([
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
Attach four constraints (top, bottom, left, right) from our single content view to the scroll view.
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
Make sure you have constraints attached to all four sides of the content view so that it will expand to the size of your content.
// After Adding your subviews to the contentView make sure you've those two constraints set:
firstSubView.topAnchor.constraint(equalTo: contentView.topAnchor),
.
.
.
lastSubView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
Reference: Using UIScrollView with Auto Layout in iOS
After failing with the provided answers in this thread, I stumbled upon this article with the solution.
There are two things not intuitive about setting up the scrollview with autolayout:
The constraints you set up as margin between the contentview and scrollview do not influence the size of the contentview. They really are margins. And to make it work, the contentview should have a fixed size.
The trick to the fixed size is that you can set the width of the contentview equal to that of the scrollview's parent. Just select both views in the tree on the left and add the equal widths constraint.
This is the gist of that article. For a complete explanation, including illustrations, check it out.
I found that with this AutoLayout issue... if I just make the ViewController use UIView instead of UIScrollView for the class... then just add a UIScrollView myself... that it works.
I had the same issue in IB.
After setting the leading, trailing, top and bottom of the scrollView to its superView. I made the following changes to make the containerView scrollable, which worked.
To make the scrollView only scroll on horizontal direction make the constraint with scrollView's centerY = ContainerView's centerY
and to make it vertically scrollable make the scrollView's centerX = ContainerView's centerX
You don’t have to set the content size of the scroll view.
Technical Note TN2154
In case someone made the same mistake like me, I'd like to share my case.
In my case, I mistakenly add a constraint to one of the subviews of scrollview which makes the subview's space to the topLayoutGuide fixed, thus it's location can't be changed, so the scrollview can't be scrolled.

Resources