How to animate a slider(UIView) while switching pages inside an UIPageViewController - ios

I want to implement something similar to Instagram activity screen header:
I added two buttons for Following and You which are insides an UIView(header). For the bottom blue bar I added an UIView.
Where exactly should I animate that UIView bar to move from Following to You screen when switching pages inside UIPageViewController?
Or maybe there is a better solution to do this ?

For something like this, I like using property observers to keep the API nice and clean for calling code. In the following example I am doing just that and manually adjusting the frame values, but in practice you'll probably want to use Auto Layout and animate your constraints.
class MyHeader: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
#IBOutlet weak var selectionView: UIView!
var buttons = [UIButton]()
var selectedTabIndex: Int {
didSet {
UIView.animateWithDuration( 0.5) {
let targetButton = self.buttons[ self.selectedTabIndex ]
var frame = self.selectionView.frame
frame.origin.x = targetButton.frame.minX
frame.origin.y = targetButton.frame.maxY - frame.height
frame.size.width = targetButton.frame.minX
self.selectionView.frame = frame
}
}
}
}

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.

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

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
}

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
}
}

Chaning height of UIView

I'm trying to change the height and width of UIView using Swift and it's not working. Tried both codes below, still no progress.
self.contentView.frame.size.height = 100
self.contentView.bounds.size.height = 100
contentView is a subview of the main view.
I guess you are using constraints. And when you use constraints you should change not the frame but constraints itself. It should looks like this:
In storyboard, pick height constraint for your view and create outlet to controller:
And then change it's constant value:
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
func changeHeight() {
heightConstraint.constant = 200
// uncomment to perform changes with animation
// UIView.animateWithDuration(0.3) { () -> Void in
self.view.layoutIfNeeded()
// }
}

UIScrollView containing CGRect line graph not scrolling (Swift)

I'm trying to make a line graph drawn using a CGRect scrollable by the user. I've set its container view to be scrollable (i want a horizontal scroll so the line can extend past the screen), but it doesn't appear to be having any effect. Here's the code in my actual view controller:
#IBOutlet weak var containerView: UIScrollView!
#IBOutlet weak var graphView: GraphView!
override func viewDidLoad() {
super.viewDidLoad()
logWeightButton.enabled = false
logWeightField.delegate = self
logWeightField.keyboardType = .NumberPad
self.view.addSubview(containerView)
containerView.addSubview(graphView)
containerView.contentSize = graphView.rectDisplay.size
pageLoad()
}
and here's the code declaring the CGRect(within a UIView class)
override func drawRect(rect: CGRect) {
if dataPoints.count == 0 {
return
} else {
self.drawLine(dataPoints, rect: rect, xCoordinates: nil, color: UIColor.blackColor())
if xCoordinates.count != 0 {
self.drawLine(actualPoints, rect: rect, xCoordinates: xCoordinates, color: UIColor.redColor())
}
rectDisplay = rect
}
}
Figured out that I needed to turn off auto-layout in the storyboard file inspector - if it's not disabled, it can overwrite scroll views.
I also made sure to enable scroll with
containerView.scrollEnabled = true
and set the containerView frame to
self.view.frame
I don't have enough reputation to post an image, so here it is:
http://i.stack.imgur.com/ZOBJp.png
The graph now scrolls, but my whole display is a mess (I believe because of setting the container frame to the size of the view), so the cycle continues.
FINAL UPDATE:
Learned that the frame of the scroll view has to be smaller than its content for it to scroll (which is why setting it to the view frame worked - the width of the view was less than the width of the line graph), so I set my frame to
containerView.frame = CGRect(x: 43.0, y: 147.0, width: 300.0, height: 117.0)
And everything works now. Hope this is helpful for someone else!

Resources