I have a weird issue with swift. I am trying to flip two views, not the whole screen but it flips the whole screen.
It has been asked before on here but no answer:
flip animation in swift flips whole view not subviews
This is the references to the views I have (I have confirmed they only take up part of the screen):
#IBOutlet var frontview: UIView!
#IBOutlet var backview: UIView!
And this is my code for flipping:
func flipCard() {
if (showingBack) {
UIView.transitionFromView(backview, toView: frontview, duration: 1, options: UIViewAnimationOptions.TransitionFlipFromRight, completion: nil)
showingBack = false
} else {
UIView.transitionFromView(frontview, toView: backview, duration: 1, options: UIViewAnimationOptions.TransitionFlipFromLeft, completion: nil)
showingBack = true
}
}
Thanks for your help
What happens when you call UIView.transitionFromView(backView, toView: frontView ... is it flips the parent of those two views. Presumably the parent in your case is the root view of the ViewController, so to get it to flip just those views instead of the "whole screen" you would just need to embed both backView and frontView in another view.
There's more details in this article.
Try This example. I think you are looking something like this. instead of imageView try UIView in your case.
Related
Goal of code is that on each tap of a view containing 2 UIImageview, to have the bottom image go on top of the to image, and so on each time I tap.
I have a view container with 2 UIImageview on top of each other:
#IBOutlet weak var imagesContainer: UIView!
#IBOutlet weak var imageZero: UIImageView!
#IBOutlet weak var imageOne: UIImageView!
I add a tap gesture in ViewDidLoad:
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImagesContainer(_:)))
self.imagesContainer.addGestureRecognizer(tapGestureRecognizer
Also a top level variable, to control which image to bring on top, and which to push down:
var imageOnTopIsImageZero = true
so that when I tap on the container, the under image comes on top.
While developing I implemented a transition without animation:
#objc func didTapImagesContainer(_ sender: UITapGestureRecognizer) {
let imageToBringOnTop: UIImageView? = imageOnTopIsImageZero ? self.imageOne : self.imageZero
let ImagetoBringDown: UIImageView? = imageOnTopIsImageZero ? self.imageZero : self.imageOne
imageToBringOnTop?.layer.zPosition = 1.0
ImagetoBringDown?.layer.zPosition = 0.0
self.imageOnTopIsImageZero.toggle()
}
This works fine. Now I tried to implement the same transition with animation:
#objc func didTapImagesContainer(_ sender: UITapGestureRecognizer) {
let imageToBringOnTop: UIImageView? = imageOnTopIsImageZero ? self.imageOne : self.imageZero
let ImagetoBringDown: UIImageView? = imageOnTopIsImageZero ? self.imageZero : self.imageOne
UIView.transition(from: ImagetoBringDown!, to: imageToBringOnTop!, duration: 1.0, options: .transitionCrossDissolve, completion: nil)
self.imageOnTopIsImageZero.toggle()
}
The first time I tap on the image, the transition happens correctly, underimage is cross disolved into the on top image.
But on the second tap, imageToBringOnTop is nil!
I really don't understand why the animation has an effect on the view content. What is the cause, and how to resolve this issue?
The docs say:
fromView
The starting view for the transition. By default, this view is removed from its superview as part of the transition.
Detail:
The idea of a transition animation is that you're changing the view hierarchy by replacing one view with another in an animated way. If you are not doing such a 'transition', you can use other (non-transition) animation API like animateWithDuration:delay:options:animations:completion:
Or there is support for keeping two views in the hierarchy, one shown, one hidden, using the transition animation API, if you include UIViewAnimationOptionShowHideTransitionViews in the option set:
UIView.transition(from: ImagetoBringDown!, to: imageToBringOnTop!,
duration: 1.0,
options: [.transitionCrossDissolve, .showHideTransitionViews],
completion: nil)
So "showHideTransitionViews" means 'show/hide the from/to instead of add/remove'
Rather than setting the z positions, transition actually removes the from view from the view hierarchy, and adds the to view to the view hierarchy (documentation):
Parameters
fromView
The starting view for the transition. By default, this view is
removed from its superview as part of the transition.
toView The ending view for the transition. By default, this view is
added to the superview of fromView as part of the transition.
Also note that since your VC is holding a weak reference to the images, their superviews are the only objects holding a strong reference to them. Once one of them is removed from its superview, your VC's weak reference becomes nil.
To fix this, simply use strong references, remove the word weak:
#IBOutlet var imageZero: UIImageView!
#IBOutlet var imageOne: UIImageView!
I have two views in self.view that are container views (via storyboard: each has an embed segue in viewDidLoad which loads up their respective child view controller). They're called self.leftPane and self.rightPane. I'm using auto-layout.
I want to animate a position change of self.leftPane, as it moves from x=0 to x=(negative something). I'm basically trying to make a hamburger menu on the left, in a simple fashion...
#IBOutlet weak var leftPane: UIView! // has child view controller embedded
#IBOutlet weak var rightPane: UIView! // has child view controller embedded
// in/after viewDidAppear
self.leftPaneLeftConstraint.constant = 0 - self.leftPane.frame.size.width + 20.0
UIView.transition(with: self.leftPane, duration: 1.0, options: [], animations: {
self.leftPane.layoutIfNeeded()
}, completion: { (finished) in
self.leftPaneIsFull = false
})
The problem is that there's no animation -- it just jumps. I've tried UIView.transition with different views, and different UIView.animate flavors, all with the same effect (no actual animation).
What am I missing? Thanks!
You should use UIView.animate instead of UIView.transition.I wrote down the code sample.Are you try this code?
#IBOutlet weak var leftPane: UIView! // has child view controller
embedded
#IBOutlet weak var rightPane: UIView! // has child view controller embedded
// in/after viewDidAppear
UIView.animate(withDuration: 1.0, animations: {
self.leftPaneLeftConstraint.constant = 0 - self.leftPane.frame.size.width + 20.0
self.view.layoutIfNeeded()
}, completion: { (success) -> Void in
//You can write something you want
})
}
})
Enjoy it :)
I have an audio player view at the bottom of my application. I want this audio player view to hide with a slide animation at the bottom of the screen once it finishes the last item in the playlist. At the start of the application, I need this audioplayer view to be hidden until the user taps an audio file to play.
The issue I am having is that the audioplayer view won't move offscreen at the beginning of the VC loading.
What's odd is that I have a similar function that moves the audioplayer view offscreen correctly, and everything works fine. This seems like it is only an issue at load time - that initial hiding of the audioplayer view.
Code:
override func viewDidLoad(){
super.viewDidLoad()
...
footerView.backgroundColor = UIColor.clear
footerView.superview?.backgroundColor = UIColor.clear
footerView.playButton.tintColor = UIColor.red
footerView.playButton.borderColor = UIColor.red
initializeFooterView()
...
}
//print statements called #viewDidLoad, but not the translate function
func initializeFooterView(){
print("initFooterView", String(describing: footerView.superview?.frame.origin.y))
print(String(describing: footerView.frame.size.height))
footerView.superview?.frame.origin.y += footerView.frame.size.height
print("initFooterView", String(describing: footerView.superview?.frame.origin.y))
}
//Working function to show/hide audioplayer view... works during runtime
func hideShowFooterView(){
let animationOptions: UIViewAnimationOptions = .curveEaseOut
let keyframeAnimationOptions: UIViewKeyframeAnimationOptions = UIViewKeyframeAnimationOptions(rawValue: animationOptions.rawValue)
if let footerView = self.footerView{
if (footerView.superview?.isHidden)!{
footerView.superview?.isHidden = false
UIView.animateKeyframes(withDuration: 0.3, delay: 0.0, options: keyframeAnimationOptions , animations: {() in
footerView.superview?.frame.origin.y -= (footerView.superview?.frame.size.height)!
}, completion: nil)
}else{
UIView.animateKeyframes(withDuration: 0.3, delay: 0.0, options: keyframeAnimationOptions , animations: {() in
footerView.superview?.frame.origin.y += (footerView.superview?.frame.size.height)!
}, completion: { (completed) in
if completed{
footerView.superview?.isHidden = true
}
})
}
}
}
Cleaned up print statements called from initializeFooterView() that moves audioPlayer view offscreen within viewDidLoad():
initFooterView() - footerView.superview?.frame.origin.y: 0.0
initFooterView() - footerView.frame.size.height = 75.0
initFooterView() - footerView.superview?.frame.origin.y: 75
If you're wondering why I translate the view on y by footerView.frame.size.height in initializeFooterView(), but translate the view by footerView.superview.frame.size.height in the hideShowFooterView(), it's because the height of the superview at viewDidLoad is 763, for some reason, while the height of the footerView's frame is 75 (the correct amount). It translates correctly during runtime, however, so I use the footerView's superview.frame.
Hierarchy of my views:
Container view set up: (footerviewcontroller is segued from a container view... not sure if that's influential)
I have a feeling that there is a conflict in defining my footerview within a storyboard, then trying to change it programmatically during viewDidLoad(). I don't want to have to define everything about footerView programmatically, though :/
It's a little tough to see what you're doing... Are you setting constraints on the footerView but then explicitly setting the frame? If so, that might be part of the issue.
However, since you say it's working fine in hideShowFooterView(), try moving initializeFooterView() from viewDidLoad() to viewWillAppear()
I am trying to get something like this to work. This is the Uber App. Where a user can swipe another view up, in front of a background view.
The background view is fairly simple, it has been done already. The view which will be swiped on top will be a UITableView. I want the user to be able to see just a little top part first when the app launches, then upon swiping a little it should stop in the middle and then after fully swiping up should take it all the way to the top, replacing the Background view.
Frameworks I have looked at are pullable view for iOS. But it is way too old and doesn't get any nice animations across. I have also looked at SWRevealViewController but I can't figure out how to swipe up from below.
I have also tried to use a button so when a user clicks on it, the table view controller appears modally, covering vertical, but that is not what I want. It needs to recognize a gesture.
Any help is greatly appreciated. Thanks.
I'm aware that the question is almost 2 and a half years old, but just in case someone finds this through a search engine:
I'd say that your best bet is to use UIViewPropertyAnimator. There's a great article about it here: http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/
EDIT:
I managed to get a simple prototype working with UIViewPropertyAnimator, here's a GIF of it:
Here's the project on Github: https://github.com/Luigi123/UIViewPropertyAnimatorExample
Basically I have two UIViewControllers, the main one called ViewController and the secondary one called BottomSheetViewController. The secondary view has an UIPanGestureRecognizer to make it draggable, and inside the recognizer's callback function I do 3 things (after actually moving it):
① calculate how much percent of the screen have been dragged,
② trigger the animations in the secondary view itself,
③ notify the main view about the drag action so it can trigger it's animations. In this case I use a Notification, passing the percentage inside notification.userInfo.
I'm not sure how to convey ①, so as an example if the screen is 500 pixels tall and the user dragged the secondary view up to the 100th pixel, I calculate that the user dragged it 20% of the way up. This percentage is exactly what I need to pass into the fractionComplete property inside the UIViewPropertyAnimator instances.
⚠️ One thing to note is that I couldn't make it work with an actual navigation bar, so I used a "normal" view with a label in it's place.
I tried making the code smaller by removing some utility functions like checking if the user interaction is finished, but that means that the user can stop dragging in the middle of the screen and the app wouldn't react at all, so I really suggest you see the entire code in the github repo. But the good news is that the entire code that executes the animations fits in about 100 lines of code.
With that in mind, here's the code for the main screen, ViewController:
import UIKit
import MapKit
import NotificationCenter
class ViewController: UIViewController {
#IBOutlet weak var someView: UIView!
#IBOutlet weak var blackView: UIView!
var animator: UIViewPropertyAnimator?
func createBottomView() {
guard let sub = storyboard!.instantiateViewController(withIdentifier: "BottomSheetViewController") as? BottomSheetViewController else { return }
self.addChild(sub)
self.view.addSubview(sub.view)
sub.didMove(toParent: self)
sub.view.frame = CGRect(x: 0, y: view.frame.maxY - 100, width: view.frame.width, height: view.frame.height)
}
func subViewGotPanned(_ percentage: Int) {
guard let propAnimator = animator else {
animator = UIViewPropertyAnimator(duration: 3, curve: .linear, animations: {
self.blackView.alpha = 1
self.someView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8).concatenating(CGAffineTransform(translationX: 0, y: -20))
})
animator?.startAnimation()
animator?.pauseAnimation()
return
}
propAnimator.fractionComplete = CGFloat(percentage) / 100
}
func receiveNotification(_ notification: Notification) {
guard let percentage = notification.userInfo?["percentage"] as? Int else { return }
subViewGotPanned(percentage)
}
override func viewDidLoad() {
super.viewDidLoad()
createBottomView()
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: receiveNotification(_:))
}
}
And the code for the secondary view (BottomSheetViewController):
import UIKit
import NotificationCenter
class BottomSheetViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var navBarView: UIView!
var panGestureRecognizer: UIPanGestureRecognizer?
var animator: UIViewPropertyAnimator?
override func viewDidLoad() {
gotPanned(0)
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(respondToPanGesture))
view.addGestureRecognizer(gestureRecognizer)
gestureRecognizer.delegate = self
panGestureRecognizer = gestureRecognizer
}
func gotPanned(_ percentage: Int) {
if animator == nil {
animator = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: {
let scaleTransform = CGAffineTransform(scaleX: 1, y: 5).concatenating(CGAffineTransform(translationX: 0, y: 240))
self.navBarView.transform = scaleTransform
self.navBarView.alpha = 0
})
animator?.isReversed = true
animator?.startAnimation()
animator?.pauseAnimation()
}
animator?.fractionComplete = CGFloat(percentage) / 100
}
// MARK: methods to make the view draggable
#objc func respondToPanGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
moveToY(self.view.frame.minY + translation.y)
recognizer.setTranslation(.zero, in: self.view)
}
private func moveToY(_ position: CGFloat) {
view.frame = CGRect(x: 0, y: position, width: view.frame.width, height: view.frame.height)
let maxHeight = view.frame.height - 100
let percentage = Int(100 - ((position * 100) / maxHeight))
gotPanned(percentage)
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.post(name: name, object: nil, userInfo: ["percentage": percentage])
}
}
EDIT: So, some time has passed and now there is a really awesome library called Pulley. It does exactly what I wanted it to do, and its a breeze to setup!
Original answer:
Thanks to both Rikh and Tj3n for giving me hints. I managed to do something very basic, it doesn't have nice animations like Uber but it gets the job done.
With the following code, you can swipe any UIViewController. I use a UIPanGestureRecognizer on my image, which will stay on top of the dragged view at all times. Basically, you use that image and it recognizes where it gets dragged, and it sets the view's frame according to the user's input.
First go to your storyboard and add an identifier for the UIViewController that will be dragged.
Then in the MainViewController, use the following code:
class MainViewController: UIViewController {
// This image will be dragged up or down.
#IBOutlet var imageView: UIImageView!
// Gesture recognizer, will be added to image below.
var swipedOnImage = UIPanGestureRecognizer()
// This is the view controller that will be dragged with the image. In my case it's a UITableViewController.
var vc = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
// I'm using a storyboard.
let sb = UIStoryboard(name: "Main", bundle: nil)
// I have identified the view inside my storyboard.
vc = sb.instantiateViewController(withIdentifier: "TableVC")
// These values can be played around with, depending on how much you want the view to show up when it starts.
vc.view.frame = CGRect(x: 0, y: self.view.frame.height, width: self.view.frame.width, height: -300)
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
swipedOnImage = UIPanGestureRecognizer(target: self, action: #selector(self.swipedOnViewAction))
imageView.addGestureRecognizer(swipedOnImage)
imageView.isUserInteractionEnabled = true
}
// This function handles resizing of the tableview.
func swipedOnViewAction() {
let yLocationTouched = swipedOnImage.location(in: self.view).y
imageView.frame.origin.y = yLocationTouched
// These values can be played around with if required.
vc.view.frame = CGRect(x: 0, y: yLocationTouched, width: UIScreen.main.bounds.width, height: (UIScreen.main.bounds.height) - (yLocationTouched))
vc.view.frame.origin.y = yLocationTouched + 50
}
Final Product
Now, It is possible that my answer might not be the most efficient way of going at this, but I am new to iOS so this is the best I could come up with for the time being.
You can embed that table view inside a custom scroll view that will only handle touch when touch that table view part (override hittest), then drag it up (disable tableview scroll), till the upper part then disable scroll view and enable tableview scroll again
Or, you can just add the swipe gesture into your tableview and change it's frame along and disable swipe when it reach the top
Experiment with those and eventually you will achieve the effect you wanted
As Tj3n pointed out, you could use a UISwipeGesture to display the UITableView. So using constraints (instead of frames) heres how you could go about doing that:
Go to your UIViewController inside your Story board on which you wish to display the UITableView. Drag and drop the UITableView and add a leading, trailing and height to the UITableView. Now add a vertical constraint between the UIViewController and UITableView so that the UITableView appears below the UIViewController(Play around with this vertical value until you can display the top part of the UITableView to suit your need). Create outlets for the vertical spacing constraint and height constraint (in case you need to set a specific height that you can figure out at run time). On the swipe up just animatedly set the vertical constraint to be equal to the negative value of the height sort of like:
topSpaceToViewControllerConstraint.constant = -mainTableViewHeightConstraint.constant
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded()
};
Alternatively
If you want to be able to bring the UITableView up depending on the pan amount (i.e depending on how much the user has moved across the screen or how fast) you should use a UIPanGestureRecognizer instead and try and set frames instead of autoLayout for the UITableView (as I'm not a big fan of calling view.layoutIfNeeded repeatedly. I read somewhere that it is an expensive operation, would appreciate it if someone would confirm or correct this).
func handlePan(sender: UIPanGestureRecognizer) {
if sender.state == .Changed {
//update y origin value here based on the pan amount
}
}
Alternatively using UITableViewController
Doing what you wish to perform is also possible using a UITableViewController if you wish to but it involves a lot of faking and effort by creating a custom UINavigationControllerDelegate mainly to create a custom animation that will use UIPercentDrivenInteractiveTransition to pull the new UITableViewController up using a UIPanGestureRecognizer if you want it depending on the pan amount. Otherwise you can simply add a UISwipeGestureRecognizer to present the UITableViewController but you will still have to again create a custom animation to "fake" the effect you want.
So this is what I am trying to do:
Here is the initial screen:
When the bottom pointing arrow is clicked there is a smaller view that transitions from bottom to top like this:
And as you can see the view in the back is dimmed. And when I click on the back view, the smaller sized view goes away by animating downwards.
There were several things that I tried:
1. I tried to segue modally which seemed to animate properly, namely, from bottom to top, but it covers the entire back view.
2. I tried to make the modal view only half the parent size by trying to replicate this post: Present modal view controller in half size parent controller. However, it did not work.
3. So I decided to put a UIView on top of my back view like so:
And I connected the grey colored view with the #IBOutlet weak var messageView: UIView!. And I tried using this code: UIView.transitionWithView(messageView, duration: 1.0, options: UIViewAnimationOptions.CurveEaseIn, animations: nil, completion: nil). However, nothing seems to be happening. Any suggestions on how to accomplish this?
For the Animation of button:
Add autolayout constraint to the BottomLayout Guide.And create an IBOutlet as
#IBOutlet weak var bottomConstraint: NSLayoutConstraint!
var shouldAnimateView:Bool = true
On the Action of the button you need to animate the View using constraint
#IBAction func showOrHideViewBtn(sender: AnyObject) {
if shouldAnimateView {
self.bottomConstraint.constant = self.view.frame.size.height/2+5
UIView.animateWithDuration(Double(0.2), animations: {
self.bottomConstraint.constant = self.view.frame.size.height/2 - self.toanimateView.frame.size.height
self.view.layoutIfNeeded()
})
}else{
self.bottomConstraint.constant = self.view.frame.size.height/2 - self.toanimateView.frame.size.height
UIView.animateWithDuration(Double(0.2), animations: {
self.bottomConstraint.constant = self.view.frame.size.height/2+5
self.view.layoutIfNeeded()
})
}
shouldAnimateView = !shouldAnimateView
}