Unable to center UIView inside UIPageViewController - ios

I'm having problems centering horizontally a view inside a UIPageViewController.
My ViewController contains a UIView with the following constraints:
Height 400
20 Align Leading to SafeArea
20 Align Trailing to SafeArea
Top Space to NavigationBar 10
Inside this view, I have a container view aligned 10 to top, bottom, left and right.
The ContainerView links to a UIPageViewController, as can be seen in the image above.
In interface builder I can see all elements centered properly.
However, if I run the app, I get this:
I've been wrapping my head around this for a few days now, without any luck.
I deleted every single view and added them again, deleted the UIPageViewController and created it again, no luck.
What's strange is that I never had any issues with UIPageViewControllers so far. In fact, I have another section like this in the app (different content) and there everything is OK.
Any suggestions on what I could be missing?

I managed to figure this out but only after encountering a similar issue with some UITextFields and other UIViews.
It seams that myContainerView.frame.size.width returns the width of self.view.frame.size.width instead of what should return.
In this case, the leading position of the ContainerView (20, relatively to the Safe Area) + self.view.frame.size.width made the view to look uncentered.
So I had to do some math here to fix it, although it seems more like a hack than an actual fix.
let mainViewWidth: CGFloat! = self.view.frame.size.width
let externalConstraints: CGFloat = 10*4
let imageViewWidth: CGFloat! = mainViewWidth - externalConstraints
let titleLabelWidth: CGFloat! = mainViewWidth - externalConstraints
let textLabelWidth: CGFloat! = mainViewWidth - (externalConstraints + 20*2)
clIVPaceContentLeading.constant = 0.0
clIVPaceContentWidth.constant = imageViewWidth
clLBLPageContentTitleLeading.constant = 0.0
clLBLPageContentTitleWidth.constant = titleLabelWidth
clLBLPageContentTextLeading.constant = 20.0
clLBLPageContentTextWidth.constant = textLabelWidth
Anyone has any idea on why this is happening?

Related

How to animate centered square to the top

I have a UIView in a portrait-only app.
The view is centered vertically and horizontally with AutoLayout ("manually" using storyboards).
The width equals the (main)view.width * 0.9
The height is the same size of the width (it is a square).
I want to tap a button inside this UIView and animate it only vertically until it reaches the top border of the screen (eg. height*0.9, 10 pts, whatever is possible).
When I click again, I want to reposition back the view to its original position (centered as it was when I first tapped).
During the transition the square should not be tappable.
After reading many posts I could not understand what's the best way to do this (I red mainly developers saying old techniques using centerX should be avoided and lamentations about some versions of the SO behaving in strange ways).
I suppose I should find a way to get the current "position" of the constraints and to assign a constraint the "final" position, but I was not able to do it.
Any help is appreciated
You are all going the wrong way about this.
Add one constraint that pins the view to the top, and add one constraint that pins the view to centerY. It will complain, so pick one and disable it (I think the property in Interface Builder is called Installed).
If the initial state is the view in the center, disable the constraint that pins it to the top, and viceversa.
Now write IBOutlets for both constraints in your controller and connect them to those constraints. Make sure the declaration of that variable is not weak, otherwise the variable will become nil when the constraint is disabled.
Whenever you want to toggle your animation you can enable one constraint and disable the other.
#IBOutlet var topConstraint: NSLayoutConstraint!
#IBOutlet var centerConstraint: NSLayoutConstraint!
func toggleState(moveToTop: Bool) {
UIView.animateWithDuration(0.25) {
self.topConstraint.isActive = moveToTop
self.centerConstraint.isActive = !moveToTop
self.view.layoutIfNeeded()
}
}
While you can use Autolayout to animate - to take the constraint constraining the centerY and set its constant to a value that would move to the top (e.g., constant = -(UIScreen.main.bounds.height / 2)), I would recommend using view's transform property.
So to move the view to the top you can use:
let topMargin = CGFloat(20)
let viewHalfHeight = self.view.bounds.height / 2
let boxHalfHeight = self.box.bounds.height / 2
UIView.animate(withDuration: 0.2) {
box.transform = CGAffineTransform.identity
.translatedBy(x: 0, y: -(viewHalfHeight - (boxHalfHeight + topMargin)))
}
You are moving box.center related to the view.center - so if you want to move the box to the top, you have to move its center by half a view's height (because the view's centerY is exactly height / 2 far from the view's top). That is not enough though, because then only a bottom half of the box is visible (now the box.centerY == view.top). Therefore you have to move it back by the box.bounds.height / 2 (in my code boxHalfHeight) - to make the top half visible. And to that boxHalfHeight you add topMargin so that there is some margin to the top.
Then, to move the box back to original position:
UIView.animate(withDuration: 0.2) {
box.transform = CGAffineTransform.identity
}
EDIT
If you really want to go with autolayout, you have to have a reference to the centerY constraint, so for example if it is created this way:
let boxCenterYConstraint = self.box.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
boxCenterYConstraint.isActive = true
Then you can try this:
// calculating the translation is the same
let topMargin = CGFloat(20)
let viewHalfHeight = self.view.bounds.height / 2
let boxHalfHeight = self.box.bounds.height / 2
let diff = -(viewHalfHeight - (boxHalfHeight + topMargin))
boxCenterYConstraint.constant = diff
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
And animation back:
boxCenterYConstraint.constant = 0
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}

Swift 3: Get distance of button to bottom of screen

I want to get the distance of a button to the bottom of the screen in Swift 3.
I can see the correct value in the Storyboard when I look at the distance (alt key) but unfortunately I am not able to calculate it manually.
The value I am looking for is the same as the constant in the "Vertical Spacing to Bottom Layout" constraint for the button.
view.frame.maxY - randomButton.frame.maxY
gave me a value way too high.
view.frame.size.height - (button.frame.size.height + button.frame.origin.y)
I think its ok! Hope it helps
If your button is not a direct successor of the view controller's view (aka, the hierarchy is something like ViewController's View -> SomeOtherView->Button), you won't get the right math by simply using frames. You will have to translate the button's Y-position to the coordinate space of the window object or your view controller's main view.
Take a look at this question: Convert a UIView origin point to its Window coordinate system
let realOrigin = someView.convert(button.frame.origin, to: self.view)
Then apply the math suggested by Lucas Palaian.
let bottomSpace = view.frame.maxY - (button.frame.height + realOrigin.y)
Another work around, in case something really wild and wierd is going on there, is to drag and drop an outlet of the button's bottom constraint. (Select the constraint from the view hierarchy in Interface Builder, hold the control key and drag the constraint to your view controller.) Then in your code you can access the constant.
let bottomSpace = myButtonBottomConstraint.constant
Use this to have it from the bottom of the button to the bottom of the view (Tested):
view.frame.size.height - randomButton.frame.size.height/2 - randomButton.frame.origin.y
I needed to find the distance from the bottom edge of the UICollectionView
to the bottom edge of the screen.
This code works for me:
#IBOutlet weak var collectionView: UICollectionView!
private var bottomSpace = CGFloat()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
bottomSpace = UIScreen.main.bounds.height - collectionView.frame.maxY
}
This method is called several times it gives the correct result.

How do i make the constraints resize the buttons correctly?

I've added a stoplight image and red, yellow, and green buttons. I want to have the buttons resize to iPhone 4S and iPhone 6S screens, but the buttons either disappear off the page or are the wrong size for the iPhone 4S. I thought the number of point would resize proportionately, but it appears it does not. Any help would be appreciated, I really want to understand constraints but I am just not getting it! Normally I would just do a x-position/screensize, y-position/screensize to relocated it, but this could be noticeably too long.
Here is the constraints of the latest incorrect location. When I try to select the stoplight image, it won't provide a constraint for the leading and trailing edge to the stoplight image.
The yellow button is placed against the stoplight image, but it won't resize.
The easiest solution would be to give all images fixed values for their width and height constraints. Then you can align the spotlightImage in the superview as you wish and define the alignment of the circle images relative to the stoplight image.
However, if you would like to stretch the width of the stoplight image depending on the width of the screen, this is a complex problem. I played around a bit trying to define all constraints in storyboard, but could not come up with a proper solution. What one ideally would like to do, for example, is define the centreX of the circles proportionally to the spotlight image's width. Similarly for the y position. Unfortunately this is not possible.
In code one have a little bit more control. Here is a solution that will work. It is not pretty, because you are actually recalculating the width of the spotlightImage, but it works :-)
class ViewController: UIViewController {
lazy var stopLightImageView: UIImageView = {
return UIImageView(image: UIImage(named:"stopLight"))
}()
lazy var circleImageView: UIImageView = {
return UIImageView(image: UIImage(named:"circle"))
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
private func setupViews() {
//Values at start. This is used to calculate the proportional values, since you know they produce the correct results.
let stoplightStartWidth: CGFloat = 540
let stoplightStartHeight: CGFloat = 542
let circleStartWidth: CGFloat = 151
let circleStartLeading: CGFloat = 231
let circleStartTop: CGFloat = 52
let screenWidth = UIScreen.mainScreen().bounds.size.width
let stoplightMargin: CGFloat = 20
self.view.addSubview(stopLightImageView)
stopLightImageView.translatesAutoresizingMaskIntoConstraints = false
//stoplightImage constraints
stopLightImageView.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor, constant: stoplightMargin).active = true
stopLightImageView.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor, constant: -stoplightMargin).active = true
stopLightImageView.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor, constant: 0).active = true
stopLightImageView.heightAnchor.constraintEqualToAnchor(stopLightImageView.widthAnchor, multiplier: stoplightStartWidth/stoplightStartHeight).active = true
self.view.addSubview(circleImageView)
circleImageView.translatesAutoresizingMaskIntoConstraints = false
//circle constraints
circleImageView.widthAnchor.constraintEqualToAnchor(stopLightImageView.widthAnchor, multiplier: circleStartWidth/stoplightStartWidth).active = true
circleImageView.heightAnchor.constraintEqualToAnchor(circleImageView.widthAnchor, multiplier: 1).active = true
let stoplightWidth = screenWidth - 2*stoplightMargin
let stoplightHeight = stoplightWidth * stoplightStartHeight/stoplightStartWidth
circleImageView.leadingAnchor.constraintEqualToAnchor(stopLightImageView.leadingAnchor, constant: stoplightWidth*circleStartLeading/stoplightStartWidth).active = true
circleImageView.topAnchor.constraintEqualToAnchor(stopLightImageView.topAnchor, constant: stoplightHeight*circleStartTop/stoplightStartHeight).active = true
}
}
Constraints are tricky, and it looks like you have a lot going on there. It's hard to tell you exactly what to do for this so, here's what I would try to do if I was having this issue(hopefully one works for you):
Set the images in the Attributes Inspector to either Aspect Fit or Redraw... That should fix your issue with them being different shapes.
Also look through the list of constraints to see if one relies on another, (for example the red and yellow seem to have similar constraints). If they rely on each other, ensure to satisfy any constraints that aren't yet - based off of the "parent" image.
Select everything and set to "Reset to Suggested Constraints". Build and run. If that doesn't fix it then there's only a few things left you can do.
Remove all the constraints on every object. Start with the black image and add missing constraints... or set it to "Center Horizontally in Container". Right click and drag the image or asset to your "view" or to the yellow "First" circle located above.
Hopefully this helps.

How to center the subviews of UIScrollView

I'm a beginner in creating a custom view. I'm trying to create a custom UIView with a scrollview and buttons that will look like this:
I'm adding a view(view with label of page number) inside of scrollView depending on the the number of pages. Is that how it should be?
Currently it looks like this:
My question is how can I center the subviews of scrollview? and next is what's wrong with this code? Why is that I can only see 1 label inside the view? and the other doesn't show up. How can I scroll to the selected page if the page number is not visible already in the scrollview?
Here's my code:
func addPageNumberViewWithCount(count: Int) {
var pageNumberViewX: CGFloat! = 0
let pageNumberViewDistance: CGFloat! = 10
for i in 1...count {
let pageNumberView = UIView(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
pageNumberView.backgroundColor = UIColor.redColor()
pageNumberView.layer.cornerRadius = pageNumberView.frame.height / 2
pageNumberView.layer.masksToBounds = true
pageNumberView.clipsToBounds = true
// add number label
let label = UILabel(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
label.center = pageNumberView.center
label.text = "\(i)"
label.textAlignment = .Center
pageNumberView.addSubview(label)
// update x for next view
pageNumberViewX = pageNumberView.frame.origin.x + pageNumberView.frame.width + pageNumberViewDistance
// add view inside scrollview
scrollView.addSubview(pageNumberView)
if i == count {
scrollView.contentSize = CGSizeMake(pageNumberViewX + pageNumberView.frame.width, 30)
}
}
}
Part of my answer will go to providing a solution to your question,and another part of my answer will go toward strongly suggesting that this not be the method you use to complete your desired tasks.
At this point, AutoLayout and Interface Builder have come a long way. Where they used to be difficult to use because of their inconsistency and unpredictability, they are now highly predictable and consistent as long as you understand the tools and how to use them.
Apple's suggested method for completing this task (which I mostly stand behind) is creating a .xib file (nib) to lay out the base components of the design, and to load the nib into the view or view controller whenever that design should be used. My question for you: have you tried this, or have you determined for some reason that this would be an unsatisfactory solution to your problem? AutoLayout exists to solve these problems not just in allowing you to achieve your desired solution in this one situation but to achieve it in other situations as well, with varying screen sizes and device types.
Now, if you were to simply ignore all of that and continue on your path, there would be a few good ways to handle your problem. One suggested solution I have:
1) Wrap your pageNumberView in another view. Constrain that view to the size of the scrollView. Doing this gives the scrollView content with which to base its scrollable content size, and gives the inner pageNumberView something to compare itself to.
2) Center the pageNumberView horizontally in its container (the new view that we just created).
Doing this, the page numbers should now center themselves in the container until they reach a size where they exceed the width of the scrollView. At that point, they will then continue to expand, making the area horizontally scrollable.
I can provide code examples of how you would do this, but frankly I would much prefer if you scrapped the idea of doing things this way and instead opted for the AutoLayout method at least, and perhaps even the Interface Builder method. I started out with iOS the same way you did, trying to do everything in code. It really isn't the best way to do things, at least with regard to iOS.
Edit: I've provided an example of how this would look in Interface Builder using UINib. I've populated the view with an example of 5 pages to show what it is like. I will see if I can make a GIF or something similar to show what each of the subviews look like.
For the OP, my suggestion would be this: Use this for reference, and go learn the constraints system. It is extremely unlikely that you will find success with iOS if you do not learn and utilize the constraints system. Coding in X values to a UIView's frame is only going to create a product with poor, inconsistent performance across devices, and will take much, much longer than it would to take the time to learn constraints.
Perhaps you should have a UICollectionView with a cell for each of these buttons. That's a better way of doing this, and you can lay it out again when the screen rotates and it changes width.
Those cells will layout offset to the left. You can solve that this way:
let pageNumberViewTotalWidth = 30 * count + (pageNumberViewDistance * count - 1)
self.collectionView.contentInset.left = (self.collectionView.frame.size.width - pageNumberViewTotalWidth) / 2
The labels aren't showing up because you're setting their frame's x to be the same as the page number view's x. It's frame should be relative to it's superview, in this case pageNumberView.
First Question of yours "how can I center the subviews of scrollview?"
Solution: lets suppose you have in total 50 pages and you want to show 5 pages at a time in the scrollview.
Then make 10 subviews of equal widths where each subview width will be equal to visible portion of the collection view that is
self.view.size.width - 2*(width of toggle button)
Then in each container view add 5 of your pageNumberView placed at equal distance
lets pageNumberViewWidth = container.width/5 - 2*margin
now pageNumberView frame will be (margin,0,pageNumberViewWidth,height)
In this way in each container view your pageNumberViews will be placed equally and it will look as if you have centred them.
Second Question "Why is that I can only see 1 label inside the view?"
Answer : Its because you are setting label frame incorrectly
let label = UILabel(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
Here label is the subview of pageNumberView So you have to set its frame according to its parent's view which is pageNumberView, so change it to
let label = UILabel(frame: CGRectMake(0, 0, 30, 30))
First time it was right because pageNumberViewX is 0 for first iteration after that it become some positive value which makes its frame shifted to right but its parent's width is small so its not visible to you.
Third Question : "How can I scroll to the selected page if the page number is not visible already in the scrollview?"
For this you need to find the frame of your selected page:
you can do that by using the offset that you used to create pageNumberView.
(width of each pageNumberView)*pageNumber = starting point of the required pageNumberView.
let frame : CGRect = CGRectMake(calculated offset above, 0,30, 30)
//where you want to scroll
self.scrollView.scrollRectToVisible(frame, animated:true)
I hope this will help you in solving your problem
Edit for first problem
func addPageNumberViewWithCount(count: Int) {
var containerViewX: CGFloat! = 0
let pageNumberViewDistance: CGFloat! = 10
let pageNumberViewPerSubview = 5
var numberOfSubview = count/pageNumberViewPerSubview
if(count % pageNumberViewPerSubview > 0){
numberOfSubview = numberOfSubview + 1
}
var pagesLeft = count
for i in 1...numberOfSubview {
var pageNumberViewX: CGFloat! = 0
let containerView : UIView = UIView(frame:CGRectMake(containerViewX,0,scrollView.frame.size.width,scrollView.frame.size.height))
if(pagesLeft < pageNumberViewPerSubview){
for k in 1...pagesLeft{
}
}
else{
for j in 1...pageNumberViewPerSubview{
let pageNumberView = UIView(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
pageNumberView.backgroundColor = UIColor.redColor()
pageNumberView.layer.cornerRadius = pageNumberView.frame.height / 2
pageNumberView.layer.masksToBounds = true
pageNumberView.clipsToBounds = true
// add number label
let label = UILabel(frame: CGRectMake(0, 0, 30, 30))
label.text = "\(i)"
label.textAlignment = .Center
pageNumberView.addSubview(label)
// update x for next view
pageNumberViewX = pageNumberView.frame.origin.x + pageNumberView.frame.width + pageNumberViewDistance
containerView.addSubview(pageNumberView)
}
containerViewX = containerViewX + scrollView.frame.size.width
// add view inside scrollview
scrollView.addSubview(containerView)
pagesLeft = pagesLeft - pageNumberViewPerSubview
}
if i == count {
scrollView.contentSize = CGSizeMake(numberOfSubview*scrollView.frame.size.width, 30)
}
}
}

Get View height based on margin constraints after runtime Swift

I have a circle in the centre of a screen with a margin constraint of 50 on either end. Hence, the width of the circle is dependent on the screen size.
So, what I tried was this:
Approach 1
I set up the margins in the storyboard to define the circle width (50 on left and right)
Then I used the following code:
#IBOutlet weak var helpButHeight: NSLayoutConstraint!
#IBOutlet weak var helpBut: UIButton!
ViewDidLoad:
helpButHeight.constant = helpBut.frame.size.width
This didn't work.
Since the screen width is 400, and the margin is 50 on either end, then helpBut.frame.size.width should have given me 300.
Instead it gave me 46.
Approach 2
This was my work-around:
ViewDidLoad:
let screenSize: CGRect = UIScreen.mainScreen().bounds
helpButHeight.constant = screenSize.width - 100
because 100 = 50 + 50, the two margins.
Works fine !
Question
Why did I have to do this? Why did the first approach not work? Why 46 and not 300?
The reason is that constraints haven't kicked in, in the viewDidLoad function. The lifecycle looks something like
viewDidLoad -- Constraints haven't set
viewWillAppear -- Constraints haven't set
viewWillLayoutSubviews -- Constraints are setting
viewDidLayoutSubviews -- Constraints are set
viewDidAppear -- Constraints are set
If you want any view to be in center just put the horizontal/vertical center constraint...No code required.. If you want to have padding just put the left and right constraints...Just to remind you don't use both of them together...It'll break...

Resources