I used the code shown below to present a new view controller in a seamless sliding motion. The actual animation works perfectly but when the animation has finished, all of the views disappear and i am left with a blank screen. There is an error posted in the console saying
"Unbalanced calls to begin/end appearance transitions for < Rocket_Game18GameViewController: 0x155e288f0 >"
class SlideRightToLeft: UIStoryboardSegue {
override func perform() {
var sourceVC:UIViewController = self.sourceViewController as UIViewController
var destVC:UIViewController = self.destinationViewController as UIViewController
let sourceVCFrame = sourceVC.view.frame
let width = destVC.view.frame.size.width
sourceVC.view.addSubview(destVC.view)
destVC.view.frame = CGRectOffset(sourceVCFrame, width, 0)
UIView.animateWithDuration(1.5, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
sourceVC.view.frame = CGRectOffset(sourceVCFrame, -width, 0)
}, completion: {
finished in
destVC.view.removeFromSuperview()
sourceVC.presentViewController(destVC, animated: false, completion: nil)
})
}
}
Please ask if you need any more information, Thanks!
I have now figured out what i was doing wrong by converting someone else's objective-c code into swift. It turns out that my whole custom animation idea was wrong and when iOS7 was released, they changed how it was done. Here is the link to the blog where i found the solution incase anyone else would like to see it
https://github.com/jbradforddillon/TransitioningExample
Related
I have a view controller (OrangeVC) that I add to a class that contains a new keyWindow(NewKeyWindowClass). A button in a different vc is tapped and it triggers this new window to get shown over the app's main window and it animates from the right side bottom of the screen to fill to the top. The animation works fine, it starts from the bottom and fills the screen with a new vc with a orange background. The problem is once the OrangeVC is added to the NewKeyWindowClass the orangeVC's deinit keeps getting triggered.
Why is it's deinit running?
Class that goes inside Animator Class:
class OrangeController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
}
deinit {
print("OrangeVC -Deinit")
}
}
AnimatorClass:
import UIKit
class NewKeyWindowClass: NSObject {
func animateOrangeVCFromBottomToTop() {
guard let keyWindow = UIApplication.shared.keyWindow else { return }
let orangeVC = OrangeController()
// 1. starting frame
orangeVC.view.frame = CGRect(x: keyWindow.frame.width - 10, y: keyWindow.frame.height - 10, width: 10, height: 10)
keyWindow.addSubview(orangeVC.view)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
// 2. ending frame
orangeVC.view.frame = keyWindow.frame
})
}
}
Button from a different class that triggers the animation:
#IBAction func triggerAnimationButtonPressed(_ sender: UIButton) {
let newKeyWindowClass = NewKeyWindowClass()
newKeyWindowClass.animateOrangeVCFromBottomToTop()
}
I got the answer from this reddit
An iOS application must have a rootViewController, create one and set
the keyWindow.rootViewController property to it. Then present your
view controller from that. Or just the rootViewController to be your
View Controller actually.
The reason the RedVC kept running it's deinit was because the keyWindow didn't have a rootViewController. I added the RedVC's view as a subview to the keyWindow keyWindow.addSubview(orangeVC.view) instead of making it it's rootVC:
keyWindow.rootViewController = redVC
Once I added it that the RedVC's deinit no longer ran when the animation occurred.
It should be noted that although it stopped the deinit from running I lost the animation and it also made the original keyWindow disappear. I should actually add this to a different UIWindow.
I coded a custom segue emulating a "Push Left". I takes screenshots of the 2 views, animates them from right to left, then present the destination view and remove the overlaying screenshots. I'm afraid that this ultimately results in stacking views on top of one another, which should be avoided in this case. I can't figure out how to properly dismiss the underlaying view once the animation is completed. I tried to use navigationController?.pushViewController instead of present but my attempts were not successful. How could I solve this issue ?
My custom segue :
class SeguePushLeft: UIStoryboardSegue
{
override func perform()
{
self.source.view.isUserInteractionEnabled = false
self.destination.view.isUserInteractionEnabled = false
let slideViewOut = UIImageView(image: source.view.capture()!)
let slideViewIn = UIImageView(image: destination.view.capture()!)
let screenWidth = source.view.frame.size.width
self.source.view.addSubview(slideViewIn)
self.source.view.addSubview(slideViewOut)
slideViewIn.transform = CGAffineTransform(translationX: screenWidth, y: 0)
UIView.animate(withDuration: 0.4,
delay: 0.0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: UIViewAnimationOptions.curveEaseOut,
animations: {
slideViewIn.transform = CGAffineTransform.identity
slideViewOut.transform = CGAffineTransform(translationX: -screenWidth, y: 0)
}, completion: { finished in
DispatchQueue.main.async{
(self.source as UIViewController).present((self.destination as UIViewController), animated: false, completion: {
self.source.view.isUserInteractionEnabled = true
self.destination.view.isUserInteractionEnabled = true
slideViewIn.removeFromSuperview()
slideViewOut.removeFromSuperview()
})
}
})
}
}
[Please forgive my previous too-hasty answer. I'm entering another rather than editing the first, because of all the comments that now blot the first.]
The problem is simple: you're doing this wrong. What you wish to do is a present transition, but with a custom animation. That's perfectly viable, and you can certainly do it with a custom segue, but the way you're doing it is not how you do that. You do it with a custom transition animation. There is a fixed way of implementing that, and it isn't how you're going about things. You need to read up on how to write a custom transition animation for a presentation transition and you'll be all set.
I have created an animated label with UIView that is glitches when switching between View Controllers within a Tab Bar.
I've attached a GIF of what's occurring and a picture of my storyboard layout below, respectively.
Glitching Label
Storyboard Setup
Caveat: I realize that my code is not-so-elegant, to put it nicely, but does anyone know why this is happening or how I can
A) stop this from happening?
and/or
B) fix it?
Is it my code (probably), a bug within the simulator (not likely), or am I approaching the animation incorrectly?
I'm using this
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
self.updateGSLabel(textArray: ["Game Mode", "Play"], index : 0)
}
func updateGSLabel(textArray : [String], index : Int){
UIView.animate(withDuration: 1.25, animations: {
self.gameModeLabel.alpha = 0.0
}) { (success) in
self.gameModeLabel.text = textArray[index]
UIView.animate(withDuration: 1.25, animations: {
self.gameModeLabel.alpha = 1.0
}) { (success) in
var newIndex = index
if(newIndex == textArray.count - 1) {
newIndex = -1
}
self.updateGSLabel(textArray: textArray, index : newIndex + 1)
}
}
}
EDIT
This may have something to do with closure parameters, but I don't know. If someone could elaborate better that'd be appreciated.
I tried to link to a page about it, but apparently I need at least 10 rep to post more than 2 links, which I think I've already done so I don't know how that's possible.
I am trying to get a view to animate from the centre of the screen, to leave the screen after 1 second when the view loads.
The problem I am having is that after a millisecond of the view being in the original (correct) position upon loading, it then snaps to the new position and animates back to the original position.
I have the following code in viewDidLoad
override func viewDidAppear(animated: Bool) {
UIView.animateWithDuration(0.7, delay: 1, options: .CurveEaseOut, animations: {
var loadingViewFrame = self.loadingView.frame
loadingViewFrame.origin.y += 600
self.loadingView.frame = loadingViewFrame
}, completion: { finished in
print("moved")
})
}
I have tried putting this code in a button action and it works fine, so is there some other method I should be using when animating on viewWillAppear or is there something else I have missed?
I have removed all autolayout constraints because I read that they may cause some problems.
I also have other code in viewDidAppear as well as viewWillAppear and viewDidLoad which I could show here if you think it is useful, but I have commented out all of this code to leave with only the basic code and the same error is still occurring.
Thanks a lot for your help.
Edit 1
I have moved the code to viewDidLayoutSubviews and have used dispatch_once to ensure it is only done once. The image still animates from the new position to the original position, but now the image is not located in the original position for a millisecond upon loading.
This is the code I have added
var token: dispatch_once_t = 0
override func viewDidLayoutSubviews() {
dispatch_once(&token) {
UIView.animateWithDuration(0.7, delay: 1, options: .CurveEaseOut, animations: {
var loadingViewFrame = self.loadingView.frame
loadingViewFrame.origin.x += 600
self.loadingView.frame = loadingViewFrame
}, completion: { finished in
print("moved")
})
}
}
First off, you need to call super.viewDidAppear(animated) when you override viewDidAppear: in your view controller subclass.
Unfortunately, it seems to work just fine for me with this view controller so there must be something else going on...
class ViewController: UITableViewController {
var loadingView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
loadingView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
loadingView.backgroundColor = UIColor.redColor()
loadingView.center = view.center
view.addSubview(loadingView)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(0.7, delay: 1, options: .CurveEaseOut, animations: {
var loadingViewFrame = self.loadingView.frame
loadingViewFrame.origin.y += 600
self.loadingView.frame = loadingViewFrame
}, completion: nil)
}
}
Your issue is most-likely because layoutSubviews may be called after viewDidAppear: (I believe this is different for iOS 8 vs iOS 9) so the changes you made get overridden almost immediately. You can confirm this by overriding viewDidLayoutSubviews in your UIViewController subclass and breakpointing viewDidLoad, viewWillAppear:, viewDidAppear:, and viewDidLayoutSubviews to see what order they happen in.
One thing you can do to achieve a similar effect is use a dispatch_once block with a once_token that is a property of your class to execute the animation in viewDidLayoutSubviews. This will insure that your animation is executed once per instance of your class after the initial view layout has occurred. This might be what you're looking for.
If you could provide more of your view controller code or a github link I may be able to give you a better, less potentially hacky, answer about what is going on.
I want to notify users that an action has been completed in the background. Currently, the AppDelegate receives notification of this:
func didRecieveAPIResults(originalRequest: String, apiResponse: APIResponse) {
if(originalRequest == "actionName") {
// do something
}
}
I'd really like to display a pop over notification (e.g. "Awarded points to 10 students") over the currently active view.
I know how to do this with NSNotification, but that means I have to add a listener to each of the views. An alternative to that would be great!
The next part of question is how do I actually get the view to fade in and then fade out again in front of whatever view I have - be that a table view, collection view or whatever else. I've tried the following code (in the viewDidLoad for the sake of testing):
override func viewDidLoad() {
// set up views
let frame = CGRectMake(0, 200, 320, 200)
let notificationView = UIView(frame: frame)
notificationView.backgroundColor = UIColor.blackColor()
let label = UILabel()
label.text = "Hello World"
label.tintColor = UIColor.whiteColor()
// add the label to the notification
notificationView.addSubview(label)
// add the notification to the main view
self.view.addSubview(notificationView)
print("Notification should be showing")
// animate out again
UIView.animateWithDuration(5) { () -> Void in
notificationView.hidden = true
print("Notification should be hidden")
}
}
The view does appear without the hiding animation, but with that code in it hides straight away. I'm also not sure how to stick this to the bottom of the view, although perhaps that's better saved for another question. I assume I'm doing a few things wrong here, so any advice pointing me in the right direction would be great! Thanks!
For your notification issue, maybe UIAlertController suits your needs?
This would also solve your issues with fading in/out a UIView
func didRecieveAPIResults(originalRequest: String, apiResponse: APIResponse) {
if(originalRequest == "actionName") {
// Creates an UIAlertController ready for presentation
let alert = UIAlertController(title: "Score!", message: "Awarded points to 10 students", preferredStyle: UIAlertControllerStyle.Alert)
// Adds the ability to close the alert using the dismissViewControllerAnimated
alert.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.Cancel, handler: { action in alert.dismissViewControllerAnimated(true, completion: nil)}))
// Presents the alert on top of the current rootViewController
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
}
UIAlertController
When adding a subview you want to be on top of everything else, do this:
self.view.addSubview(notificationView)
self.view.bringSubviewToFront(notificationView)
Fading a UIView by changing the alpha directly:
For testing, you should be calling this in your viewDidAppear so that the fading animation starts after the view actually is shown.
// Hides the view
UIView.animateWithDuration(5) { () -> Void in
notificationView.alpha = 0
}
// Displays the view
UIView.animateWithDuration(5) { () -> Void in
notificationView.alpha = 0
}
This solution takes up unnecessary space in your code, I would recommend extensions for this purpose.
Extensions:
Create a Extensions.swift file and place the following code in it.
Usage: myView.fadeIn(), myView.fadeOut()
import UIKit
extension UIView {
// Sets the alpha to 0 over a time period of 0.15 seconds
func fadeOut(){
UIView.animateWithDuration(0.15, animations: {
self.alpha = 0
})
}
// Sets the alpha to 1 over a time period of 0.15 seconds
func fadeIn(){
UIView.animateWithDuration(0.15, animations: {
self.alpha = 1
})
}
}
Swift 2.1 Extensions
Hope this helps! :)