Trying to Annotate a MapView and Load a second View Controller Simultaneously - ios

Inside my Primary View Controller I have a MKMapView and TableView. The tableview is using AlamoFire to call an API of user information to populate its cells.
What I would like to happen:
When you select a cell, a second container view, with more detailed user information, will segue into the portion of the screen where the tableview was. At the same time, the Map View will annotate with that user's location.
The problem:
When the cell is selected, the second container view slides onto the screen (good), the Map Annotates (good), but then the info container view disappears again (bad). With a second tap of the originally selected cell, the info container view slides back onto the screen to stay. I need to get rid of this double tap.
It appears that the Map Annotation is negating this process for some reason. When I comment out the map annotation, the VC transition works fine... with one tap.
Any thoughts would be helpful.
Here is the offending code:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.localTableView.deselectRowAtIndexPath(indexPath, animated: true)
dispatch_async(dispatch_get_main_queue(),{
self.populateInfoContainerView(indexPath.row)
self.mapView.selectAnnotation(self.users[indexPath.row], animated: true)
if(self.userInfoContainerView.frame.origin.x == UIScreen.mainScreen().bounds.size.width){
self.showInfoView(true)
} else {
self.hideInfoView(true)
}
})
}
func showInfoView(animated: Bool){
UIView.animateWithDuration(0.33, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: ({
self.userInfoContainerView.frame.origin.x = 0
self.localTableView.frame.origin.x = -UIScreen.mainScreen().bounds.size.width
}), completion: { finished in
//animation complete
})
self.infoVisible = true
}
func hideInfoView(animated: Bool){
let xPos = UIScreen.mainScreen().bounds.size.width
if(animated == true){
UIView.animateWithDuration(0.25, animations: ({
self.userInfoContainerView.frame.origin.x = xPos
self.localTableView.frame.origin.x = 0
}), completion: { finished in
//animation complete
})
} else {
self.userInfoContainerView.frame.origin.x = xPos
}
self.infoVisible = false
}
Thank you.

I believe I have found an answer. I'm not sure why this works and the above version does not, but I'll show you what I did different.
I am now animating the constraints of the TableView and the InfoView.
First, I ctrl+click+drag the constraint from my main.storyboard into my code and created an IBOutlet to animate. I also added a let screenWidth as the constant that I would move them by.
#IBOutlet weak var infoViewLeadingEdge: NSLayoutConstraint!
#IBOutlet weak var tableViewXCenter: NSLayoutConstraint!
let screenWidth = UIScreen.mainScreen().bounds.size.width
Then I replaced the code above in showInfo():
self.userInfoContainerView.frame.origin.x = 0
self.localTableView.frame.origin.x = -UIScreen.mainScreen().bounds.size.width
with this new code:
self.infoViewLeadingEdge.constant -= self.screenWidth
self.tableViewXCenter.constant += self.screenWidth
self.view.layoutIfNeeded()
In my hideInfo() above, I replaced this:
self.userInfoContainerView.frame.origin.x = xPos
self.localTableView.frame.origin.x = 0
with this new code:
self.infoViewLeadingEdge.constant += self.screenWidth
self.tableViewXCenter.constant -= self.screenWidth
self.view.layoutIfNeeded()
Don't forget the self.view.layoutIfNeeded(). If you don't use that code, the animations won't work. Also, the += and -= on the tableViewXCenter seem opposite of what I want the table to do, however, this is the way I needed to put them in order to get the table to move the direction I wanted. It did seem counterintuitive though.
And that's it. I hope this will be helpful to someone.

Related

Why do I see a static text before animation in my tableView

I'm adding simple stack animation to my tableViewCell which animates like cells are being add to a stack. When I tap the UIButton that segues me to the tableView I first see a static cell which has my values and then after a small gap my animation works. I don't know why my tableView shows that cell before animation ? Here is my code for the VC :
override func viewDidAppear(_ animated: Bool) {
animateTable()
}
func animateTable() {
let cells = tableView.visibleCells
let tableHeight: CGFloat = tableView.bounds.size.height
for i in cells {
let cell: UITableViewCell = i as UITableViewCell
cell.transform = CGAffineTransform(translationX: 0, y: tableHeight)
}
var index = 0
for a in cells {
let cell: UITableViewCell = a as UITableViewCell
UIView.animate(withDuration: 1, delay: 0.05 * Double(index), usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
cell.transform = CGAffineTransform(translationX: 0, y: 0);
}, completion: nil)
index += 1
}
}
When your view appears, it would have the UITableView data already populated via a call to reloadData. You do the animation only after the view appears. So at that point, you'll see the existing data for a fraction of a second and then the animation would kick in.
If you don't want the data to appear at all before the animation runs, you might want to not show data as soon as the view loads by doing something like the following - this might not be the best solution, but it is the easiest to implement:
1: Add a new variable to indicate whether you've run the animation or not.
var wasAnimated = false
2: Check this variable in numberOfRowsInSection and return 0 if the animation has not run yet.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if !wasAnimated {
return 0
}
// Rest of the original code
}
3: Set the flag at the beginning of animateTable and then reloadData before you execute the rest of the animation code.
func animateTable() {
wasAnimated = true
tableView.reloadData()
// The rest of the original code
}
That should get you the behaviour you wanted :)

Moving view offscreen at load time not working

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()

How to display UITableViewController as a swipe up gesture in front of another ViewController?

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.

Cell Animation Stops when swipe left/right or pull table view out of screen

I can't figure out why animation of tableViewcell frizzes when tableView is pulled out out of screen or stoppes when you start to close menu.
To let you better understand the problem, here is a gif
I implement Tap Gesture Recognizer at custom UITableViewCell Class
let tap = UITapGestureRecognizer(target: self, action: "tapAction")
self.addGestureRecognizer(tap)
func tapAction() {
let animationWidth = leftMenuWidth * 0.27
UIView.animateWithDuration(0.75, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .AllowUserInteraction, animations: {
self.colorIndicator.frame.size.width += animationWidth
}) { (true) in
UIView.animateWithDuration(0.75, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .AllowUserInteraction, animations: {
self.colorIndicator.frame.size.width -= animationWidth
}, completion: { (true) in
print("Animation Complete")
})
}
Also i implement sliding menu by using this cocoaPods - https://github.com/jonkykong/SideMenu
Thanks.
The cells are likely being reloaded which is causing the animations to reset.
Try tracking the state of whether or not a cell has been tapped so that when it's reloaded you can show the animation as already complete. You can do this by with a simple dictionary.
At the top of your view controller, define:
private var tapped = [Int : Bool]()
Next, in cellForRowAtIndexPath: for your tableView check:
if let isSet = tapped[view.hashValue] where isSet == true {
// display animation complete. You probably don't want to re-animate
// it if it's scrolled back into view, so just get it to the completed state.
}
Finally, switch your tap action from using a gesture in the cell itself to didSelectRowAtIndexPath: inside your tableView:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// remember the cell has been tapped
tapped[view.hashValue] = true
// call your method to display the animation on the cell,
// something like cell.showAnimation(). It shouldn't animate if
// already displaying the completed animation state.
}
Also, I wouldn't recommend using += or -= operators when calculating the frame since calling it multiple times will keep growing or shrinking it. Use explicit values instead, like = animationWidth or = 0.

Perform Animation In viewDidAppear On Objects With Constraints Set By Interface Builder

I have watched the WWDC 2012 presentations on Auto Layout and read the documentation on the view appearance calls.
So I thought I needed to perhaps wait a frame or a second after viewDidAppear just to be safe, but still didn't work. Here was my code:
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
view.layoutIfNeeded()
view.autoresizesSubviews = false
_textButton.frame = CGRectMake(0, 0, 0, 0)
println(_textButton.frame)
let delay:Double = 4*Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue())
{
self._textButton.frame = CGRectMake(0, 0, 0, 0)
println(self._textButton.frame)
}
}
This actually prints out (144.0,6.5,32.0,32.0) twice in a row. Which means even AFTER setting the frame to 0, it was set back to its constraint defaults.
Why is this?
I have another ViewController that looks almost the same as this one, with buttons having the same constraints. But when I close the view, I animate the buttons to slide out to the left with the following code:
#IBAction func takePhotoTap(sender: AnyObject)
{
UIView.animateWithDuration(0.5, animations: animations)
_camera.captureImage(receivePhoto)
}
func animations()
{
var height = CGFloat(_distanceBetweenTopAndMiddleBar)/2
_lowerLens.frame = CGRectMake(_lowerLens.frame.origin.x, _lowerLens.frame.origin.y, _lowerLens.frame.width,-height)
_upperLens.frame = CGRectMake(_upperLens.frame.origin.x, _upperLens.frame.origin.y, _upperLens.frame.width,height)
view.autoresizesSubviews = false
slideOffScreenLeft(_gridLinesButton)
slideOffScreenLeft(_swapCameraButton)
slideOffScreenLeft(_flashButton)
}
func slideOffScreenLeft(obj:UIView)
{
obj.frame = CGRectMake((-obj.frame.width), obj.frame.origin.y, obj.frame.width, obj.frame.height)
}
This works JUST FINE! When they hit the button, these buttons slide off the screen. However when I load the next view I want the buttons to slide in from the right. But, as you can see above, even waiting 4 seconds before trying to set the frames of the buttons has no effect.
Can you suggest what I can do to animate some buttons to slide in the from the screen when the view loads? Why are the constraints overriding my changes in the first case but when I animated a View Controller before closing it with UIView.animateWithDuration the constraints were overriden?
When using AutoLayout, it is best practice to create outlets for your constraints and then modify the constraints, rather than the frames of your objects. I was having a similar problem. This also gets rid of the need for the dispatch_after call.
For example:
#IBOutlet weak var buttonHeight: NSLayoutConstraint!
#IBOutlet weak var buttonWidth: NSLayoutConstraint!
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
// Modify the constraint rather than the frame
self.buttonHeight.constant = 0
self.buttonWidth.constant = 0
}
This will make the button 0x0. You can then do the same for the x,y position.

Resources