I have a UIViewController with a UIScrollView as subview. the scrollview size and origin are computed properties.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let sizeWidth = view.frame.size.width - (view.frame.size.width / 5)
let sizeHeight = (view.frame.height / 5) * 3
let originX = (view.width-sizeWidth) / 2
// sizes & positions of container & subviews
slideScrollView.frame = CGRect(x: originX,
y: 50,
width: sizeWidth,
height: sizeHeight)
how it looks like
by tapping the sign in or login button, the scrollview should expand animated. but it is the opposite.
#objc private func loginButtonTapped() {
UIView.animate(withDuration: 5, delay: 0, options: .allowAnimatedContent) {
self.slideScrollView.frame.size.height += (self.view.frame.height / 5) * 1
}
}
this is the result
it should expand, but it sets back to the action height property and expand to regular size,
i hope anyone can tell me why this happens and may have a solution.
Thats because your viewDidLayoutSubviews gets called multiple times (In this case twice as I have noticed by adding debug statements) when you start your animation with UIView.animate(withDuration: and because you always reset the slideScrollView.frame in viewDidLayoutSubviews you see unnecessary side effects.
You can always check this by putting a break point in viewDidLayoutSubviews when loginButtonTapped gets triggered. Refer this question for similar explaination
If your intention to use viewDidLayoutSubviews is to know when view is properly loaded and its frames are updated properly you can always use
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let sizeWidth = view.frame.size.width - (view.frame.size.width / 5)
let sizeHeight = (view.frame.height / 5) * 3
let originX = (view.width-sizeWidth) / 2
// sizes & positions of container & subviews
slideScrollView.frame = CGRect(x: originX,
y: 50,
width: sizeWidth,
height: sizeHeight)
}
Now your loginButtonTapped should work fine.
As per apple docs: viewDidLayoutSubviews Called to notify the view controller that its view has just laid out its subviews.
With that I think my explanation above makes sense, you use UIView.animate(withDuration: to modify the frame of scrollView so obviously View updates/re-renders the subViews hence viewDidLayoutSubviews gets called when you call UIView.animate(withDuration: and because of your frame update code in viewDidLayoutSubviews you see adverse side effects
Related
I have a SettingsVC for my app, it contains a TableView detailing all the available options for my app. I want to show a pop up view whenever the user taps on one of these options to avoid taking the user to another VC. I have currently implemented the pop up view in others parts of app, but there's an issue when trying to present it from the SettingsVC, it seems that the bounds are for the whole size of the tableView and not what is currently on the screen, so if I scroll down, the pop up appears all the way up... Any ideas. Here's my setup method for the pop up view. The animateIn() method is to present the pop up view and the blur view.
func setUpPickerView(){
blurView.bounds = self.view.bounds
pickerView.bounds = CGRect(x: 0, y: 0, width: self.view.bounds.width * 0.75, height: self.view.bounds.height * 0.45)
pickerView.layer.cornerRadius = 15.0
pickerView.layer.cornerRadius = 15.0
animateIn(desiredView: blurView)
animateIn(desiredView: pickerView)
tableView.isUserInteractionEnabled = false
blurView.alpha = 0.6
}
The bounds of a view is the coordinate system inside the view. The frame of a view defines where the view appears in its parent's coordinate system. Your code is only seeing the bounds so the frame is probably defaulting to the top of the screen. I suspect you want something like:
blurView.frame = self.view.bounds
pickerView.frame = CGRect(
x: (blurView.bounds.size.height - self.view.bounds.width * 0.75) / 2
y: (blurView.bounds.size.width - self.view.bounds.height * 0.45) / 2
width: self.view.bounds.width * 0.75
height: self.view.bounds.height * 0.45)
However would you REALLY should probably be doing is setting up the blurView and the pickerView then establishing their locations using constraints.
I'm trying to understand how iOS frame and bounds works.
I put an subView:UIView on UIViewController and a button which can increase subView's frame origin coordinate and change textlabel with its value.
like this,
let subView: UIView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(subView)
subView.backgroundColor = .blue
}
#IBAction func btnMoveBottomView(_ sender: Any) {
subView.frame.origin.y = subView.frame.origin.y + 100
lbFrameInfo.text = String(format:"sub = (%.1f, %.1f)", subView.frame.origin.x, subView.frame.origin.y)
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
After I make this action, I see subView go down but text doesn't change.
on debug console I see this change
po subView.frame.origin
▿ (50.0, 2350.0)
- x : 50.0
- y : 2350.0
updateViewConstraints has been called too.
override func updateViewConstraints() {
print("updateViewConstraints")
super.updateViewConstraints()
}
Instead of increasing frame.origin.x like this, increasing value of leading constant works perfectly.
It would be appreciated if someone can guide me the differences of these and the concept of frame and if it is related with auto-layout things
According to your question,
You have added subView to ViewController view so when u create view
let subView: UIView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
and do this
view.addSubview(subView)
it means your subview will be at 50 position down and away from left of the screen.
so when u do this.
subView.frame.origin.y = subView.frame.origin.y + 100
it will move your frame further down in the screen as you are changing the y position of frame which is the starting position of creating your view on screen.
At its simplest, a view’s bounds refers to its coordinates relative to its own view (as if the rest of your view hierarchy didn’t exist), whereas its frame refers to its coordinates relative to its parent’s view. Frame will reflect the position in its parents view.
This means a few things:
1.If you create a view at X:0, Y:0, width:100, height:100, its frame and bounds are the same.
2.If you move that view to X:100, its frame will reflect that change but its bounds will not. Remember, the bounds is relative to the view’s own space, and internally to the view nothing has changed.
3.If you transform the view, e.g. rotating it or scaling it up, the frame will change to reflect that, but the bounds still won’t – as far as the view is concerned internally, it hasn’t changed
hope it clears your doubts
I have a UIViewController that I set its size in ViewDidLoad like that:
//Setting view's frame
let width = UIScreen.main.bounds.size.width - 10
let height = (43 * UIScreen.main.bounds.height) / 100 //%43 of the screen
self.view.frame = CGRect(x: 5, y: 80, width: width, height: height)
It works great until I try to present another ViewController like that:
//Presenting AutoCompleteVC
self.autocompleteVC = GMSAutocompleteViewController()
self.autocompleteVC.delegate = self
self.present(self.autocompleteVC, animated: true, completion: nil)
After I dismiss this view (self.autocompleteVC.dismiss(animated: true, completion: nil)) the frame of the first ViewController's view changes to the full screen's frame.
Any idea how to fix it?
Thanks!
Set your frame in viewWillLayoutSubviews. The frame is already known then. Like this:
public override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//Setting view's frame
let width = UIScreen.main.bounds.size.width - 10
let height = (43 * UIScreen.main.bounds.height) / 100 //%43 of the screen
self.view.frame = CGRect(x: 5, y: 80, width: width, height: height)
}
I suggest that write your code in viewWillAppear but after set frame, you should write
self.view.setNeedsLayout()
I hope this will work.
The main view of a view controller is positioned and sized by system logic depending on landscape/portrait, splitscreen, modal display, view controller containment...
Instead you should do every content layouting in subviews (depending on main view size) and set background color of the main view to UIColor.clear.
So in your case add a subview and configure it as you wish.
Hint: You should do it with auto layout instead of manipulating the frame.
I am trying to add buttons dynamically to a scroll view after pressing another button.
I created a custom UIView which I want to add to the scroll view.
Below you can find the code how I am trying to do it:
var buttonY: CGFloat = 0 // our Starting Offset, could be 0
for _ in audioCommentsArray {
UIView.animateWithDuration(0.15, animations: {
let commentView = AudioCommentView(frame: CGRectZero)
commentView.frame = CGRect(x: 0, y: buttonY, width: 75, height: 75)
buttonY = buttonY + 75 // we are going to space these UIButtons 75px apart
commentView.alpha = 1.0
audioCommentsListScrollView.addSubview(commentView)
})
}
I want to add these commentView's using a simple animation. However only the last commentView is added correctly to the scrollView whereas the above views are added like this:
Only the background of the commentView is shown whereas the other elements are not visible.
Does anyone have an idea what I might be missing? Adding views using a for loop shouldn't be complicated as I have done this many times before but this time I seem to miss something?
Thank you in advance!
The UIView animations are overlapping and interfering with each other as you process them through the loop. Instead, you should chain them so that the next animation does not interfere with the other. Delete the loop. Then call the animations one after the other in the completion handler. You can call it recursively to ensure each button is animated.
var count = 0
func startButtonsAnimation() {
UIView.animateWithDuration(0.15, animations: {
let commentView = AudioCommentView(frame: CGRectZero)
commentView.frame = CGRect(x: 0, y: buttonY, width: 75, height: 75)
buttonY = buttonY + 75 // we are going to space these UIButtons 75px apart
commentView.alpha = 1.0
audioCommentsListScrollView.addSubview(commentView)
}, completion: { (value: Bool) in
if self.count < self.audioCommentsArray.count {
self.count += 1
self.startButtonsAnimation()
}
})
I have overrided a method as
override func viewDidLayoutSubviews() {
// creating bottom line for textField
let border = CALayer()
let width = CGFloat(1.0)
border.borderColor = UIColor.whiteColor().CGColor
border.frame = CGRect(x: 0, y: emailTextField.frame.size.height - width, width: emailTextField.frame.size.width, height: emailTextField.frame.size.height)
border.borderWidth = width
emailTextField.layer.addSublayer(border)
emailTextField.layer.masksToBounds = true
}
Now whats happening is that when I run my app on Iphone 6, 6+ every thing works fine. But when I run the same code on iphone5 (Simulator + real Device ) viewDidLayoutSubViews is getting called infinite times and my app becoms unresponsive. I solved the problem by using a bool variable. But I do not understand why is this happening. So can someone please explain this to me.
Thanks :-)
As docs says that viewDidLayoutSubviews method need for change layout
Called to notify the view controller that its view has just laid out
its subviews. When the bounds change for a view controller's view,
the view adjusts the positions of its subviews and then the system
calls this method. However, this method being called does not indicate
that the individual layouts of the view's subviews have been
adjusted. Each subview is responsible for adjusting its own layout.
Move your code to viewDidLoad method or to loadView if you are not using xibs or Storyboards
override func viewDidLoad() {
// creating bottom line for textField
let border = CALayer()
let width = CGFloat(1.0)
border.borderColor = UIColor.whiteColor().CGColor
border.frame = CGRect(x: 0, y: emailTextField.frame.size.height - width, width: emailTextField.frame.size.width, height: emailTextField.frame.size.height)
border.borderWidth = width
emailTextField.layer.addSublayer(border)
emailTextField.layer.masksToBounds = true
}
If you would like to change border.frame you can set it to class variable and change it in viewDidLayoutSubviews method
override func viewDidLayoutSubviews() {
self.border.frame = CGRect(x: 0, y: emailTextField.frame.size.height - width, width: emailTextField.frame.size.width, height: emailTextField.frame.size.height)
self.border.borderWidth = width
}
Just replace
emailTextField.layer.addSublayer(border)
with this:
emailTextField.addSublayer(border)
-> you want to add sublayer to a view - as you want to apply masksToBounds property to it either.
Move your implementation inside viewDidLayoutSubviews. I got the same issue. Hope it will work.
override func viewDidLayoutSubviews()
{
//Your CODE
}