Keep UI Elements in place when transitioning between UIViewControllers - ios

I am wondering what is the best practice to keep some of the UI elements in place when going forward/backward between UIViewController for example if I am using UINavigationController.
To be specific. I am making an app that has several similar view controllers (they can be instances of one main view controller). Then user clicks the next button and goes to the next page; or swipe back to go to the previous page. I have a progress bar on top and one or more buttons on bottom that I wish to keep static in place while the rest of the content are changing with an animation (a simple push might work).
Now my question is, if is it better put the content inside a container view? or to implement custom transition to keep those items in place while moving the rest?
Here is an image of the concept:

In the navigation controller delegate you can implement navigationController(_ navigationController:, animationControllerFor:, from:, to:) to return a custom UIViewControllerAnimatedTransitioning object. If you go this route you will have to implement the whole animation yourself though.
If you want to keep the basic animations from UINavigationController and just keep your elements steady you can go another route. In your view controller implement viewWillAppear: and/or viewWillDisappear:. In there you can get the transitionCoordinator and call animate(alongsideTransition:, completion:) on that. With that you can run your custom animations in parallel to the system provided animations.
To keep fixed elements you add another copy of the fixed elements to the container view you can get from the context object that is passed to your block. In the completion block you then can remove it again.
Sounds complicated, but it actually is rather easy if you look at the code:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
let fixedViewCopy = UIView(...)
fixedViewCopy.frame = self.fixedView.frame
transitionCoordinator?.animate(
alongsideTransition: { context in
context.containerView.addSubview(fixedViewCopy)
},
completion: { _ in
fixedViewCopy.removeFromSuperview()
}
)
}

Related

Change value for animation in animateTransition using UIPercentDrivenInteractiveTransition

I am trying to implement swipe up/down to dismiss view controller and everything is working great. If I start swiping up, it will finish the animation in the upwards direction and vice versa.
The problem appears when the user first swipes up but then decides to swipe the view controller down - how do I change the value in the animation to dismiss view controller to the bottom rather than to the top (set by the user's first swipe).
In my UIViewControllerAnimatedTransitioning controller I define the animation like this:
func animateTransition(transitionContext:UIViewControllerContextTransitioning) {
UIView.animateWithDuration(duration, animations: {
guard let transitionDelegate = self.transitionDelegate else {return}
snapshot.frame.origin.y = transitionDelegate.shouldAnimateUp ? -snapshot.frame.height : snapshot.frame.height
TransitionDelegate points to UIPercentDrivenInteractiveTransition controller (where the swipe gesture is defined). ShouldAnimateUp is defined in UIPanGestureRecognizer function like this:
shouldAnimateUp = translatedView.center.y < translatedView.frame.height / 2
That is if the view is in upper half, shouldAnimateUp = true and the other way around.
But unfortunately, when I call finishInteractiveTransition() func it uses the value which was initially set in UIView.animateWithDuration in UIViewControllerAnimatedTransitioning controller when dismissViewControllerAnimated(true, completion: nil) was called.
So, is there any way to change values for animation in UIViewControllerAnimatedTransitioning controller after dismissViewControllerAnimated(true, completion: nil) is called?
PS: I kind of struggled to define my problem in words (words are hard 🙄 ), so please tell me if you need additional info or if I should try to rewrite my explanation. Also, an additional hint: I would like the animation to work like image dismissal works in official Twitter app.
OK, I see what they're doing. It's actually more complicated that "if you swipe down and then up, it animates scene off upward". If you swipe down and back up, the Twitter app will cancel if you don't get much past where you started, but will go up if you pass where you started by some considerable portion (and are still swiping in that upward).
I must confess that I'm not crazy about this UX, because two very similar gestures can result in very different behavior. If you swipe down, keep your finger down, and then flick back upward, there is a seemingly arbitrary nature as to whether it's interpreted as a cancelation of the downward swipe or as an upward swipe. I personally think that if the user initiates a demonstrable downward gesture, that dragging back up should merely be a cancelation of the transition. But that's not the question here.
Anyway, if this is what you want to do, there are a couple of ways to achieve it:
You could consider using view property animator-based animations, which handle mid-flight changes to the animation more elegantly than older animation techniques. So, you theoretically could just addAnimations to your UIViewPropertyAnimator. But it seems messy to me.
See Advances in UIKit Animations and Transitions for more information about view property animators and how to use them with interruptible custom transitions.
When dismissing, you might not use UIViewControllerAnimatedTransitioning at all. Just animate the dismissal yourself.
For example, when you present, the UIPresentationController subclass can keep the presenting view (by returning false from should​Remove​Presenters​View). Then, when the gesture recognizer starts, it might not initiate a custom, interactive custom transition with dismiss at all, but instead merely adjust the frame of the presented scene (and modify the opacity of the dimming chrome). Then, at the end of the gesture, complete the animation manually and then the completion block can dismiss the presented view controller with no animation at all (because you've animated it yourself).
IMHO, this is architecturally inelegant. A view controller has no business mucking about with chrome that should be owned by the presentation controller. But, it works.
We should recognize that they might not have "presented" the full screen image at all. For example, they could have done simple view controller containment, but just added the child view controller scene to the existing scene. Then, the gesture could do whatever frame and dimming layer opacity changes it wanted, and when its done, just do the final animation and in the completion block, just remove the child view controller (e.g. willMove and removeFromParentViewController).
If I were to do this, I'd probably lean towards option 3, though I'd wager that it might not feel very satisfying for you, having invested time in custom interactive transitions already. Regardless, these are a couple of approaches you could consider.

Assigning two views to a single Container View using swift in iOS 8

I am trying to assign two view controllers to a Container View using the Interface Builder. I tried to do so, but whenever I try to "embed" the second view controller to my Container View, instead of adding another VC to it, it just replaces the one that was embedded already.
Ultimately, my main goal is to have a screen that has the following elements (in order, from top to bottom):
-A navigation bar
-A view of height 50 that contains a segmented controller (which will switch between tableVCs)
-A main view, which will contain my Container view
-A tabbed bar
My current setup is almost as described above. Here is a picture:
The view controller I am interested in the most is in the one with the highlighted container(HomeViewController). The approach I am currently using is hacky, because I currently have 2 container views, one on top of another, and they embed the 2 table view controllers depicted to the right (one per container).
I do not like this approach very much because both containers get instantiated whenever the main VC (Home View Controller) is instantiated, therefore making 2 network calls by default to load their content, possibly slowing down the device and maybe using more memory than needed.
Ideally, I would load the content of one table view controller that is mapped to one of the segmented controls. Then, I would have a mechanism that somehow instantiates the second table view controller whenever I go to the second button in the segmented control (and possibly deallocating/getting rid of the other VCs), and so on with the third. Or somehow be able to display/alternate between 2 or more view controllers in an area (view) inside my HomeViewController.
Currently I have this simple code that switches (hides and shows) between container views in my HomeViewController:
#IBAction func segmentChanged(sender: AnyObject) {
switch segmentedControl.selectedSegmentIndex{
case 0:
println("index1 selected")
containerView1.hidden = false
containerView2.hidden = true
break
case 1:
println("index2 selected")
containerView1.hidden = true
containerView2.hidden = false
break
default:
containerView1.hidden = false
containerView2.hidden = true
break
}
}
As I said, this only switches between the views that are loaded already in my view controller, with the data in them already.
I just wanted to see if what I am trying to code is doable, or if I am actually tackling the problem the right way, although I doubt I am doing so.
Thank you for reading my post and for your advice in advance.
Cheers!
Add embed segue to NavigationController, add ViewController as rootViewController to NavigationController, from rootViewController add as many segue as you wish. To load controller you need just override segue class to push without navigation.
class NoAnimationSegue: UIStoryboardSegue {
override func perform() {
self.sourceViewController.navigationController?.pushViewController(self.destinationViewController, animated: false)
}
}

Full-window view in front of a collection view

I'm trying to implement the following behavior:
Long press on a collection view brings a full-window view (call it LetterView) to the front
Subsequent gestures/touches are only processed by the LetterView.
(edit: I should mention that I want a transparency effect of seeing the collectionview items beneath LetterView)
I seem to be running into behavior that everyone else is trying to implement, though - my touches get processed by both the LetterView and the collection view. I.e. I can scroll the collection view AND have hits processed by my topmost view. Showing the view hierarchy in XCode clearly shows LetterView at the front, and both the UICollectionView and the LetterView are subviews of UICollectionWrapperView.
LetterView is a UIView subclass with a UIViewController subclass. It's added to the view hierarchy programmatically, inside my UICollectionViewController subclasses's viewDidLoad method, like so:
super.viewDidLoad()
letterDrawingViewController = LetterDrawingViewController()
let viewFrame : CGRect = self.collectionView!.frame
letterDrawingViewController.view = LetterDrawingView.init(frame:viewFrame)
letterDrawingView = letterDrawingViewController.view
self.addChildViewController(letterDrawingViewController)
letterDrawingViewController.didMoveToParentViewController(self)
collectionView?.addSubview(letterDrawingView)
It doesn't appear to be a first responder issue, as I tried overriding canBecomeFirstResponder in LetterView and assigning it first responder status when I move it to the front
I tried setting userInteractionEnabled=FALSE on the CollectionView, but keeping it true on the LetterView after I moved LetterView to the front. This disabled all touch events for both views
I tried setting exclusiveTouch=True for LetterView when I moved it to the front. This didn't appear to do anything.
Aside from any specific tips, are there any general techniques for debugging hit-testing like this? According to the docs on hit-testing in iOS, iOS should prefer the "deepest" subview that returns yes for hitTest:withEvent:, which, since LetterView is a subview of collectionview, and in front of all it's cells, should be the front? Is there any logging I can enable to see a hit test over the view hierarchy in action?
Thank you!
Nate.
If letterView is full screen, you probably don't want to add it as a subview of the collection view like you are. Maybe try adding it to the application's window instead and see how that does. At least in that instance it should intercept all the touch events.
Another method, although admittedly a more fragile feeling one, would be to enable and disable user interaction on the collectionView as you present and dismiss letterView.
So, when letterView is about to be presented, you can call
self.collectionView.userInteractionEnabled = NO;
and if you also know when that view is about to be dismissed you can call
self.collectionView.userInteractionEnabled = YES;
The only thing here to worry about is that you don't get into a bad state where your letterView is not presenting and your collectionView is also ignoring a user's touch. That will feel totally broken.
Whilst I think you can deal with your issue somewhat easily I think you are making a design mistake. It feels like you are trying to code this thinking like a web developper by adding a child view to your view and trying to intercept the touches there like one would do in a modern JavaScript single page app. In iOS I think this is bad design. You should segue or present the new viewController using the methods provided by apple.
So your code should look soothing like:
letterDrawingViewController = LetterDrawingViewController()
self.presentViewController(letterDrawingViewController, animated: true, completion: nil)
iOS8 has the added benefit of allowing you to have awesome custom transitions. Take a look at this : http://www.appcoda.com/custom-segue-animations/

touchesBegan delay after presenting UIView

I'm trying to load a UIView and then right away detect touches on that new view. Currently, overriding touchesBegan gives a delay of around a second.
So, I load up the UIView and immediately keep tapping on the screen. It takes around a second for touchesBegan to be called. From that point on, all is well. However, I can't afford the ~second wait initially.
I have stripped back all the code in the UIView to just the barebones incase anything was clogging up the main thread, but the delay still persists.
How can I go about getting immediate feedback of touches from a newly presented UIView? Thanks.
-- EDIT BELOW --
I've been playing around with this for the past few hours. Even when creating a custom UIWindow and overriding sendEvent, the UITouchPhase gets halted when the new view is displayed. To begin receiving events again I have to take my finger off the screen and place it back on the screen. (I don't want to have to do this).
The problem seems to lie with the segue to the new view controller. When it segues, the touch phase is ended. If I simply add a subview to the current view controller, I see the desired functionality (i.e. instant responding to touch).
Given my newly presented view contains a lot of logic, I wanted to wrap it all up in it's own view controller rather than add it to the presenter view controller. Is there a way for me to do this and use 'addSubview` to present it? This should hopefully achieve the desired effect.
In the end, I created a custom view controller with it's own xib. Where I would have segued, I now instantiate that custom view controller and append it's view. This has eliminated the touch lag.
Have you disabled multi-touch? There's an inherent delay while the controller waits to see if there's a follow up touch (on all single touches). The initial sluggishness might be from loading up the multi-touch code and deciding what to do about it.
myViewController.view.multipleTouchEnabled=NO;
As to your final question, look into view controller containment. Since iOS 5 Apple has provided the hooks officially and safely to present one view controller as a sub view of another.
Sadly I've no insight as to the greater issue.
I found an answer that worked for me from a similar question asked here:
iOS: Why touchesBegan has some delay in some specific area in UIView
This solution isn't check-marked on that thread, so I'll copy it here to make it easier to find.
override func viewDidAppear(animated: Bool) {
let window = view.window!
let gr0 = window.gestureRecognizers![0] as UIGestureRecognizer
let gr1 = window.gestureRecognizers![1] as UIGestureRecognizer
gr0.delaysTouchesBegan = false
gr1.delaysTouchesBegan = false
}

where does UIPageViewController cache the previous and next controllers?

When UIPageViewController calls the datasource with the viewControllerBeforeViewController and viewControllerAfterViewController methods, it is obtaining the view controller that will be displayed when the user swipes again. Is there a writable property in which it keeps this data until it needs to use it? The reason I ask is that I want the user to be able to swipe to go forward or backward, or simply tap to advance to the next slide. However, if I tap to advance from the first slide to the second (advancing using the setViewControllers method), I cannot swipe backwards; there is no back controller to go back to. This affects only the second slide. So I need to be able to set the previous controller programmatically.
Any help would be greatly appreciated.
Thanks.
I had the similar issue and discovered, that UIPageViewController store its cached View Controller pages in the childViewControllers array.
There is no access to the cached view controllers. You can maybe keep a pointer when you implement the Data Source methods to enable swiping:
– pageViewController:viewControllerBeforeViewController:
– pageViewController:viewControllerAfterViewController:
As described in Kelin's answer, the children array contains the "loaded" view controllers. This information can also be used for propagating changes among loaded view controllers. (A common scenario would be ending the refresh of a refresh control.)
In Swift you can do something like this:
for vc in children {
if let tableVC = vc as? UITableViewController {
tableVC.refreshControl?.endRefreshing()
}
}
(Where self is of type UIPageViewController)

Resources