Setting origin of container to another container trouble (Swift 4.2) - ios

I'm trying to set the origin and width/height of one UIView (red) to a second UIView (blue).
I am calling UIView.frame.origin or size and for some reason the y origin doesn't work.
I've also tried with layout constraints (see it commented out below), but this is overriding my blue fully constrained view.
Then I have a button that animates the red view to the side so you can see the blue view underneath, but I can't get them to line up to start with. Below is my code. In interface builder, I have both UIViews set up as containers. Blue is fully constrained with auto layout and red has no constraints.
import UIKit
class ViewController: UIViewController
{
#IBOutlet weak var blueContainer: UIView!
#IBOutlet weak var redContainer: UIView!
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
print(redContainer.frame)
redContainer.frame.origin.x = view.frame.width/2
redContainer.frame.size.width = view.frame.width
//try to line up y with origin and size
redContainer.frame.origin.y = blueContainer.frame.origin.y
redContainer.frame.size.height = blueContainer.frame.size.height
//also tried by using constraints
//redContainer.topAnchor.constraint(equalTo: blueContainer.topAnchor).isActive = true
//redContainer.heightAnchor.constraint(equalTo: blueContainer.heightAnchor).isActive = true
print(redContainer.frame)
}
#IBAction func slideRed(_ sender: Any) {
if redContainer.frame.origin.x == 0 {
UIView.animate(withDuration: 0.5) {
self.redContainer.frame.origin.x = self.view.frame.width/2
}
button.setTitle("Come Back Red!", for: .normal)
} else {
UIView.animate(withDuration: 0.5) {
self.redContainer.frame.origin.x = 0
}
button.setTitle("Go Away Red!", for: .normal)
}
}
}

ViewDidLoad does not guarantee the view has laid out its constraints. So when blueContainer's frame and size is zero, you will not see any effect on redContainer. You should use viewDidLayoutSubviews to get the correct frame and size from blueContainer.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
redContainer.frame.origin.x = view.frame.width/2
redContainer.frame.size.width = view.frame.width
//try to line up y with origin and size
redContainer.frame.origin.y = blueContainer.frame.origin.y
redContainer.frame.size.height = blueContainer.frame.size.height
}

Related

Add/ expand animation will cause unwanted UIScrollView scrolling

I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.

How to rotate iOS label and make it stick to edge of screen?

I've got the following structure for example:
I want to rotate my label by 270degrees to achieve this:
via CGAffineTransform.rotated next way:
credentialsView.text = "Developed in EVNE Developers"
credentialsView.transform = credentialsView.transform.rotated(by: CGFloat(Double.pi / 2 * 3))
but instead of expected result i've got the following:
So, what is the correct way to rotate view without changing it's bounds to square or whatever it does, and keep leading 16px from edge of screen ?
I tried a lot of ways, including extending of UILabel to see rotation directly in storyboard, putted dat view in stackview with leading and it also doesn't helps, and etc.
Here is the solution which will rotate your label in an appropriate way forth and back to vertical-horizontal state. Before running the code, set constraints for your label in storyboard: leading to 16 and vertically centered.
Now check it out:
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
// Your leading constraint from storyboard, initially set to 16
#IBOutlet weak var leadingConstraint: NSLayoutConstraint!
var isHorizontal: Bool = true
var defaultLeftInset: CGFloat = 16.0
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
label.text = "This is my label"
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapAction)))
}
#objc func tapAction() {
if self.isHorizontal {
// Here goes some magic
// constraints do not depend on transform matrix,
// so we have to adjust a leading one to fit our requirements
leadingConstraint.constant = defaultLeftInset - label.frame.width/2 + label.frame.height/2
self.label.transform = CGAffineTransform(rotationAngle: .pi/2*3)
}
else {
leadingConstraint.constant = defaultLeftInset
self.label.transform = .identity
}
self.isHorizontal = !self.isHorizontal
}
}

Update constraints swift layoutIfNeeded doesn't work

I need to update constraints (height of my CollectionView) when request is done and I have images from server, so height of View also will change.
Result screen
what I need screen
My code:
DispatchQueue.main.async {
self.cvActivity.alpha = 0
self.collectionView.reloadData()
self.collectionView.heightAnchor.constraint(equalToConstant: self.cellWidth * 2).isActive = true
self.collectionView.setNeedsUpdateConstraints()
self.collectionView.setNeedsLayout()
self.view.layoutIfNeeded()
}
Well, basic idea by #J. Doe is correct, here some code explanation (for simplicity i used UIView, not UICollectionView):
import UIKit
class ViewController: UIViewController {
#IBOutlet var heightConstraint: NSLayoutConstraint! // link the height constraint to your collectionView
private var height: CGFloat = 100 // you should keep constraint height for different view states somewhere
override func updateViewConstraints() {
heightConstraint.constant = height // set the correct height
super.updateViewConstraints()
}
// update height by button press with animation
#IBAction func increaseHight(_ sender: UIButton) {
height *= 2
UIView.animate(withDuration: 0.3) {
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded() // if you call `layoutIfNeeded` on your collectionView, animation doesn't work
}
}
}
Result:
Define a height for your collectionView, create an outlet from that constraint and increase the constant of that constraint and call layoutifneeded in an animation block
You need to make object of your Height constraint from storyboard
#IBOutlet weak var YourHeightConstraintName: NSLayoutConstraint!
YourConstraintName.constant = valueYouWantToGive
---------OR--------
collectionViewOutlet.view.frame = CGRect(x:
collectionViewOutlet.frame.origin.x , y:
collectionViewOutlet.frame.origin.y, width:
collectionViewOutlet.frame.width, height:
yourheightValue)

Cant resize a UIButton with CGRectMake

I have a button with a image on it, I am using UIView.animateWithDuration to assign a new size to the view as well as the image view.
The view scales up correctly, but there is no change to the image view
Code Snippet:
#IBAction func imageButtonDidTouch(sender: AnyObject) {
UIView.animateWithDuration(0.7) {
/*** Executes Properly ***/
self.dialogView.frame = CGRectMake(0, 0, self.screenWidth!, self.screenHeight!)
/*** Does Not Execute ****/
self.imageButton.frame = CGRectMake(0, 0, self.screenWidth!, 240)
/*** Executes Properly ***/
self.likeButton.hidden = true
self.shareButton.hidden = true
self.userButton.hidden = true
self.headerView.hidden = true
self.dialogView.layer.cornerRadius = 0
}
}
Screenshot Before Button Press:
Screenshot After Button Press:
Screen Width and Screen Height Have been Defined in viewDidLoad()
screenWidth = UIScreen.mainScreen().bounds.width
screenHeight = UIScreen.mainScreen().bounds.height
EDIT 1:
imageButton declaration
#IBOutlet weak var imageButton: UIButton!
EDIT 2:
I added a completion handler to the animate function:
#IBAction func imageButtonDidTouch(sender: AnyObject) {
print("Height Before:", self.imageButton.frame.height)
print("Width Before:",self.imageButton.frame.width)
UIView.animateWithDuration(0.7, animations: {
/*** Executes Properly ***/
self.dialogView.frame = CGRectMake(0, 0, self.screenWidth!, self.screenHeight!)
/*** Does Not Execute ****/
self.imageButton.frame = CGRectMake(0, 0, self.screenWidth!, 240)
/*** Executes Properly ***/
self.likeButton.hidden = true
self.shareButton.hidden = true
self.userButton.hidden = true
self.headerView.hidden = true
self.dialogView.layer.cornerRadius = 0
}) { (true) in
print("Height After:", self.imageButton.frame.height)
print("Width After:",self.imageButton.frame.width)
}
}
Console Screenshot:
View Hierarchy:
Check before setting imageButton size wether or not are you receiving you desired screenWidth.
The right place to get the UIScreen frame data is in viewDidLayoutSubviews as it is called after the subViews have been laid out in screen also it is called after every time your device changes orientation such as your width will be different when your user goes into landscape mode.This is called after viewWillAppear:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
screenWidth = UIScreen.mainScreen().bounds.width
screenHeight = UIScreen.mainScreen().bounds.height
}
Then:-
If you are initialising and declaring your button Programatically :
print(self.screenWidth)
self.imageButton.frame.size = CGSizeMake(self.screenWidth,240)
If you are using AutoLayout for the constraints, then :-
Create an #IBOutlet of your button's width constraint in your class
#IBOutlet weak var widthConstraint: NSLayoutConstraint!
Then set it accordingly.
widthConstraint = self.screenWidth
Note:
For conditions not using Auto Layout, when views are embedded within a view the CGRectMake function will only be executed for the parent view and not for the child view.
If needed to perform the operation with CGRectMake, Auto Layout needs to be disabled

Centering subviews in UIView does't work in swift?

I have a UIView(called innerView) inside a UIView(outerView). The outerView has Autolayout constraints and is always centered in the root view. The innerView is just placed in the outerView arbitrarily without any constraints. And they are all linked to the view controller by outlets.
I want the innerView to be always centered inside the outerView. Of course, i can use autolayout, but i just have to test if i can move it by code(because i found it is a problem in my real project)
unfortunately, i find i can't move the innerView with code. Anyone knows the reason?
here is the code:
import UIKit
class ViewController: UIViewController {
// outerView is 300 X 300
#IBOutlet weak var outerView: UIView!
// innerView is 140 X 140, and it is the subview of outerView
#IBOutlet weak var innerView: UIView!
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
innerView.center = CGPoint(x: outerView.bounds.midX, y: outerView.bounds.midY)
innerView.autoresizingMask = .None
// result is : (150.0, 150.0) which is correct
print(innerView.center)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// result is : (78.0, 222.0) which is not correct
print(innerView.center)
}
}
As of viewDidLayoutSubviews, it is just an event to inform you, that all subviews of viewcontroller's root view are positioned at the desired places. You are not guaranteed that all the rest subviews of those subviews will also be aligned, since it is a responsibility of the parent view itself.
So move your center innerView code into:
override func viewDidAppear(animated: Bool){
super.viewDidAppear(animated)
innerView.center = CGPoint(x:outerView.bounds.midX , y:outerView.bounds.midY)
}
If support orientation:
override func willAnimateRotationToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {
innerView.center = CGPoint(x:outerView.bounds.midX , y:outerView.bounds.midY)
}
Try this:
innerView.center = outerView.convertPoint(outerView.center, fromView: outerView.superview)

Resources