I'm implementing a user form in an iOS app that uses both a UIPickerView and UIDatePicker as input devices from a user. I've implemented each of these as a view external to the main UIViewController in the scene and have them showing and hiding using autolayout by adding and removing constraints.
Here's my problem: I'm maintaining separate constraints and hide/show methods for animating each of these views in and out. It's a lot of repeat code and I get the sense that there has to be a cleaner way to do this, since it seems to be a very common design pattern in iOS apps. I'm fairly new at this, so I feel like I'm missing something.
Is there a better design pattern than this for using multiple input devices to UIButtons??
Here's a sampling of the code i'm using to maintain this...
var datePickerViewBottomConstraint: NSLayoutConstraint!
var datePickerViewTopConstraint: NSLayoutConstraint!
var storagePickerViewBottomConstraint: NSLayoutConstraint!
var storagePickerViewTopConstraint: NSLayoutConstraint!
#IBAction func storageButtonClicked(sender: UITextField) {
storagePickerView.translatesAutoresizingMaskIntoConstraints = false
storagePickerViewBottomConstraint = storagePickerView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
UIView.animateWithDuration(0.4, animations: {
self.storagePickerViewTopConstraint.active = false
self.storagePickerViewBottomConstraint.active = true
self.view.layoutIfNeeded()
})
}
#IBAction func datePumpedClicked(sender: UIButton) {
datePickerView.translatesAutoresizingMaskIntoConstraints = false
datePickerViewBottomConstraint = datePickerView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
UIView.animateWithDuration(0.4, animations: {
self.datePickerViewTopConstraint.active = false
self.datePickerViewBottomConstraint.active = true
self.view.layoutIfNeeded()
})
}
#IBAction func datePickerDismiss(sender: AnyObject) {
datePumpedLabel.setTitle(Global.dateFormatter.stringFromDate(datePicker.date), forState: UIControlState.Normal)
datePickerView.translatesAutoresizingMaskIntoConstraints = false
datePickerViewTopConstraint = datePickerView.topAnchor.constraintEqualToAnchor(view.bottomAnchor)
UIView.animateWithDuration(0.4, animations: {
self.datePickerViewBottomConstraint.active = false
self.datePickerViewTopConstraint.active = true
self.view.layoutIfNeeded()
})
}
#IBAction func storagePickerDismiss(sender: AnyObject) {
storagePickerView.translatesAutoresizingMaskIntoConstraints = false
storagePickerViewTopConstraint = storagePickerView.topAnchor.constraintEqualToAnchor(view.bottomAnchor)
UIView.animateWithDuration(0.4, animations: {
self.storagePickerViewBottomConstraint.active = false
self.storagePickerViewTopConstraint.active = true
self.view.layoutIfNeeded()
})
}
Here's a screenshot of my storyboard...
Storyboard
You guys, I figured out a better way!
The thing that I was missing originally is that when you remove views from the superview, all the constraints are deleted. This makes things a lot easier.
I kept 1 constraint class variable for the view that is currently being shown, so the view can animate down out of place.
I also added a "setup" function to get the view in place on viewdidload(). Without this the view will always animate in from the top the first time it appears (couldn't figure out a way around this).
Here's what I used:
var secondaryMenuBottomConstraint: NSLayoutConstraint!
func setupSecondaryMenu(secondaryMenu: UIView){
secondaryMenu.translatesAutoresizingMaskIntoConstraints = false
let topConstraint = secondaryMenu.topAnchor.constraintEqualToAnchor(view.bottomAnchor)
let leftConstraint = secondaryMenu.leftAnchor.constraintEqualToAnchor(view.leftAnchor)
let rightConstraint = secondaryMenu.rightAnchor.constraintEqualToAnchor(view.rightAnchor)
view.addSubview(secondaryMenu)
NSLayoutConstraint.activateConstraints([topConstraint, leftConstraint, rightConstraint])
self.view.layoutIfNeeded()
secondaryMenu.removeFromSuperview()
}
func showSecondaryMenu(secondaryMenu: UIView){
//Close any open keyboards
amountTextField.resignFirstResponder()
notesTextField.resignFirstResponder()
//Add the view and then add constraints to show the submenu below the current view
secondaryMenu.translatesAutoresizingMaskIntoConstraints = false
let topConstraint = secondaryMenu.topAnchor.constraintEqualToAnchor(view.bottomAnchor)
let leftConstraint = secondaryMenu.leftAnchor.constraintEqualToAnchor(view.leftAnchor)
let rightConstraint = secondaryMenu.rightAnchor.constraintEqualToAnchor(view.rightAnchor)
view.addSubview(secondaryMenu)
NSLayoutConstraint.activateConstraints([topConstraint, leftConstraint, rightConstraint])
secondaryMenuBottomConstraint = secondaryMenu.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
//animate the view up into place
UIView.animateWithDuration(0.4, animations: {
topConstraint.active = false
self.secondaryMenuBottomConstraint.active = true
self.view.layoutIfNeeded()
})
}
func dismissSecondaryMenu(secondaryMenu: UIView){
if secondaryMenu.superview != nil {
secondaryMenu.translatesAutoresizingMaskIntoConstraints = false
let secondaryMenuTopConstraint = secondaryMenu.topAnchor.constraintEqualToAnchor(view.bottomAnchor)
self.secondaryMenuBottomConstraint.active = false
UIView.animateWithDuration(0.4, animations: {
secondaryMenuTopConstraint.active = true
self.view.layoutIfNeeded()
}, completion: {(finished:Bool) in
secondaryMenu.removeFromSuperview()
})
}
}
Related
I am trying to make a simple e-commerce app with a dynamic barButton to show users their total cart cost. I wanted to add an animation to it. So that the button needs to collapse and expand whenever its value changes. I used layoutIfNeeded() method after updating button's constraints. However the animation is not working. The code is a bit complex so I am sharing a code sample and a video to give you a rough idea what I have done so far. What could be changed to get desired animation?
Gif
Code
class ProductsViewController: UIViewController {
...
var cartBarButton: UIBarButtonItem!
var cartBarButtonWidthConstraint: NSLayoutConstraint?
var price: Double = 0
...
override func viewDidLoad() {
super.viewDidLoad()
configureCartButton()
...
}
func configureCartButton() {
let cartButton = CustomCartButton(color: color, image: image, title: price.currencyString)
cartButton.addTarget(self, action: #selector(updateCartCost), for: .touchUpInside)
cartBarButton = UIBarButtonItem(customView: cartButton)
cartBarButton.customView?.translatesAutoresizingMaskIntoConstraints = false
cartBarButtonWidthConstraint = cartBarButton.expand(considering: price) // expand() method is extension to UIBarButtonItem, returns barButton's widthAnchor constraint
cartBarButtonWidthConstraint?.isActive = true
navigationItem.rightBarButtonItem = cartBarButton
}
#objc private func updateCartCost() {
cartBarButtonWidthConstraint?.isActive = false
cartBarButtonWidthConstraint = cartBarButton.collapse(to: 25) // collapse() method is an extension to UIBarButtonItem
cartBarButtonWidthConstraint?.isActive = true
UIView.animate(withDuration: 1) {
self.view.layoutIfNeeded()
}
price += 90
cartBarButtonWidthConstraint?.isActive = false
cartBarButtonWidthConstraint = cartBarButton.expand(considering: price)
cartBarButtonWidthConstraint?.isActive = true
UIView.animate(withDuration: 1) {
self.view.layoutIfNeeded()
}
self.cartButton.title = self.price.currencyString
}
}
You are changing the constraint to collapse and the constraint to expand at the same time and doing both animations at the same time.
You should move all of the code to do the expansion in the completion handler of the collapse animation. This should allow the two changes to happen one after the other instead being all done at once.
Something like this (not verified it will compile but it gives you the idea):
#objc private func updateCartCost() {
cartBarButtonWidthConstraint?.isActive = false
cartBarButtonWidthConstraint = cartBarButton.collapse(to: 25) // collapse() method is an extension to UIBarButtonItem
cartBarButtonWidthConstraint?.isActive = true
UIView.animate(withDuration: 1, animations: {
self.view.layoutIfNeeded()
}) { finished in
price += 90
cartBarButtonWidthConstraint?.isActive = false
cartBarButtonWidthConstraint = cartBarButton.expand(considering: price)
cartBarButtonWidthConstraint?.isActive = true
UIView.animate(withDuration: 1) {
self.view.layoutIfNeeded()
}
}
self.cartButton.title = self.price.currencyString
}
I want to show / hide UISearchBar programmatically but I am not having the desire results.
What I am trying to do is hide search bar when scrolling down and show it when scrolling up:
I set my UISearchBar like this:
var mySearchcontroller = UISearchController(searchResultsController: nil)
mySearchcontroller.obscuresBackgroundDuringPresentation = false
mySearchcontroller.searchBar.placeholder = "search"
mySearchcontroller.searchBar.delegate = self
definesPresentationContext = true
self.navigationItem.searchController = mySearchcontroller
self.navigationItem.hidesBackButton = true
self.navigationItem.hidesSearchBarWhenScrolling = false
And the result is
I implement the scrollViewDidScroll to make the search bar show or hide when scrolling:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0)
{
navigationItem.hidesSearchBarWhenScrolling = false
}
else
{
navigationItem.hidesSearchBarWhenScrolling = true
}
}
And the result is:
As you realise that the GRP label or a segmented control is been masked by the search bar and I am not having the same effect where the search bar push down all controls (figure 1).
How can I solve this?
You achieve this functionality by following steps:
Give proper Auto-layout to your views.
Set IBOutlet for the Height of your headerview/searchview like:
#IBOutlet weak var constrainHeightHeader: NSLayoutConstraint!
Write the scrollview delegate methods like:
//MARK: - Scrollview delegate
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
//
self.contentOffSet = self.cwProducts.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//
let scrollPos = self.cwProducts.contentOffset.y
if scrollPos == self.contentOffSet{
return
}
if(scrollPos > self.contentOffSet ){
//Fully hide your toolbar
self.constrainHeightHeader.constant = 0
UIView.animate(withDuration: 0.2, animations: {
self.view.layoutIfNeeded()
}, completion: { (status) in
self.headerVW.isHidden = true
})
} else {
if(self.isFromBanner) {
self.constrainHeightHeader.constant = 0
}else{
self.constrainHeightHeader.constant = 50
}
UIView.animate(withDuration: 0.2, animations: {
self.view.layoutIfNeeded()
}, completion: { (status) in
self.headerVW.isHidden = false
})
}
}
I hope you are getting my point and by minor changes in the above code you will get solution.
It's working in my project 100% tested.
I am very new to Xcode, so this could be something very basic that i'm just not finding, but i'm trying to create an app that moves 2 images away from each other (at least one width away) when the user taps the screen, and then have the images return to their original positions when the user taps the screen again, however, i have been unable to find out how to move an image in a specific direction, a specific distance.
i'm also new to Stack Overflow, so i'm sorry if i'm doing something wrong
I am breaking the rules by giving up the answer when it appears you have not googled enough. The general rule is to show the code you have attempted and indicate what part you are having issues with.
The code below will achieve what you are attempting to do.
import UIKit
class slidingIamgesViewController: UIViewController {
#IBOutlet weak var topImage: UIImageView!
#IBOutlet weak var bottomImage: UIImageView!
var doubleTap: Bool! = false
//MARK: View LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
let singleFingerTap = UITapGestureRecognizer(target: self, action: #selector(slidingIamgesViewController.handleSingleTap(_:)))
self.view.addGestureRecognizer(singleFingerTap)
}
// MARK: gestutre recognizer
func handleSingleTap(_ recognizer: UITapGestureRecognizer) {
if (doubleTap) {
UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut, animations: {
var basketTopFrame = self.topImage.frame
basketTopFrame.origin.y += basketTopFrame.size.height
var basketBottomFrame = self.bottomImage.frame
basketBottomFrame.origin.y -= basketBottomFrame.size.height
self.topImage.frame = basketTopFrame
self.bottomImage.frame = basketBottomFrame
}, completion: { finished in
print("Images Moved back!")
})
doubleTap = false
} else {
UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut, animations: {
var basketTopFrame = self.topImage.frame
basketTopFrame.origin.y -= basketTopFrame.size.height
var basketBottomFrame = self.bottomImage.frame
basketBottomFrame.origin.y += basketBottomFrame.size.height
self.topImage.frame = basketTopFrame
self.bottomImage.frame = basketBottomFrame
}, completion: { finished in
print("Images sperated!")
})
doubleTap = true
}
}
}
Make sure in your storyboard to added a Tap Gesture Recognizer.
I'm looking for the best way to invert the place of two UIView, with animation if possible (first i need to change the place, animation is optionnal).
viewController :
So, i want view1 to invert his place with the view2. The views are set with autolayout in the storyboard at the init.
if state == true {
view1 at the top
view2 at the bot
} else {
view 2 at the top
view 1 at the top
}
I've tried to take view.frame.(x, y, width, height) from the other view and set it to the view but it doesn't work.
I think the best way to do this would be to have a topConstraint for both views connected to the header and then change their values and animate the transition. You can do it in a way similar to this:
class MyViewController: UIViewController {
var view1TopConstraint: NSLayoutConstraint!
var view2TopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view1TopConstraint = view1.topAnchor.constraintEqualToAnchor(header.bottomAnchor, constant: 0)
view1TopConstraint.isActive = true
view2TopConstraint = view2.topAnchor.constraintEqualToAnchor(header.bottomAnchor, constant: view1.frame.height)
view2TopConstraint.isActive = true
}
func changeView2ToTop() {
UIView.animateWithDuration(0.2, animations: { //this will animate the change between 2 and 1 where 2 is at the top now
self.view2TopConstraint.constant = 0
self.view1TopConstraint.constant = view2.frame.height
//or knowing that view2.frame.height represents 30% of the total view frame you can do like this as well
// self.view1TopConstraint.constant = view.frame.height * 0.3
self.view.layoutIfNeeded()
})
}
You could also create the NSLayoutConstraint in storyboard and have an outlet instead of the variable I have created or set the top constraint for both views in storyboard at "remove at build time" if you are doing both. This way you won't have 2 top constraints and no constraint warrning
I Made an Example: https://dl.dropboxusercontent.com/u/24078452/MovingView.zip
I just created a Storyboard with 3 View, Header, View 1 (Red) and View 2 (Yellow). Then I added IBoutlets and animated them at viewDid Appear, here is the code:
import UIKit
class ViewController: UIViewController {
var position1: CGRect?
var position2: CGRect?
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
position1 = view1.frame
position2 = view2.frame
UIView.animate(withDuration: 2.5) {
self.view1.frame = self.position2!
self.view2.frame = self.position1!
}
}
}
Hello #Makaille I have try to resolve your problem.
I have made an example, which will help you for your required implementation.
Check here: Source Code
I hope, it will going to help you.
#IBAction func toggle(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
if(sender.isSelected){
let topPinConstantValue = layout_view2TopPin.constant
layout_view1TopPin.constant = topPinConstantValue
layout_view2TopPin.priority = 249
layout_view1BottomPin_to_View2Top.priority = 999
layout_view1TopPin.priority = 999
layout_view2BottomPin_ToView1Top.priority = 249
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
}, completion: { (value) in
if(value){
sender.isUserInteractionEnabled = true
}
})
} else {
let topPinConstantValue = layout_view1TopPin.constant
layout_view2TopPin.constant = topPinConstantValue
layout_view1TopPin.priority = 249
layout_view2BottomPin_ToView1Top.priority = 999
layout_view2TopPin.priority = 999
layout_view1BottomPin_to_View2Top.priority = 249
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
}, completion: { (value) in
if(value){
sender.isUserInteractionEnabled = true
}
})
}
sender.isSelected = !sender.isSelected
}
I have a viewController where am showing image for adding the zooming functionality I added the scrollView in viewController and inside of ScrollView I added ImageView everything is working fine expect of one thing, am hiding, and showing the bars (navigation bar + tab bar) on tap but when hiding them my imageView moves upside see the below images
See here's the image and the bars.
Here I just tapped on the view and bars got hidden but as you can see my imageView is also moved from its previous place, this is what I want to solve I don't want to move my imageView.
This is how am hiding my navigation bar:
func tabBarIsVisible() ->Bool {
return self.tabBarController?.tabBar.frame.origin.y < CGRectGetMaxY(self.view.frame)
}
func toggle(sender: AnyObject) {
navigationController?.setNavigationBarHidden(navigationController?.navigationBarHidden == false, animated: true)
setTabBarVisible(!tabBarIsVisible(), animated: true)
}
any idea how can I hide and show the bars without affecting my other Views?
The problem is that you need to set the constraint of your imageView to your superView, not TopLayoutGuide or BottomLayoutGuide.
like so:
Then you can do something like this to make the it smootly with animation:
import UIKit
class ViewController: UIViewController {
#IBOutlet var imageView: UIImageView!
var barIsHidden = false
var navigationBarHeight: CGFloat = 0
var tabBarHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.hideAndShowBar))
view.addGestureRecognizer(tapGesture)
navigationBarHeight = (self.navigationController?.navigationBar.frame.size.height)!
tabBarHeight = (self.tabBarController?.tabBar.frame.size.height)!
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func hideAndShowBar() {
print("tap!!")
if barIsHidden == false {
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveEaseOut, animations: {
// fade animation
self.navigationController?.navigationBar.alpha = 0.0
self.tabBarController?.tabBar.alpha = 0.0
// set height animation
self.navigationController?.navigationBar.frame.size.height = 0.0
self.tabBarController?.tabBar.frame.size.height = 0.0
}, completion: { (_) in
self.barIsHidden = true
})
} else {
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveEaseOut, animations: {
// fade animation
self.navigationController?.navigationBar.alpha = 1.0
self.tabBarController?.tabBar.alpha = 1.0
// set height animation
self.navigationController?.navigationBar.frame.size.height = self.navigationBarHeight
self.tabBarController?.tabBar.frame.size.height = self.tabBarHeight
}, completion: { (_) in
self.barIsHidden = false
})
}
}
}
Here is the result:
I have created an example project for you at: https://github.com/khuong291/Swift_Example_Series
You can see it at project 37
Hope this will help you.