Exclude UIView from animating on autorotation - ios

TL;DR
Need to keep autorotation, but exclude one UIView from autorotating on orientation change, how?
Back story
I need to keep a UIView stationary during the animation accompanied by autorotation (which happens on orientation change). Similar to how the iOS camera app handles the rotation (i.e controls rotate in their place).
Things I've tried
Returning false from shouldAutorotate(), subscribing to UIDeviceOrientationDidChangeNotification, and trying to manually handle the rotation event for each view separately.
Works well if you don't need to change any of your UIViews' places, otherwise it's a pain figuring out where it should end up and how to get it there
Placing a non rotating UIWindow under the main UIWindow, and setting the main UIWindow background colour to clear.
This works well if it's only one item, but I don't want to manage a bunch of UIWindows
Inverse rotation I.e rotating the UIView in the opposite direction to the rotation. Not reliable, and looks weird, it's also vertigo inducing
Overriding the animation in the viewWillTransitionToSize method. Failed
And a bunch of other things that would be difficult to list here, but they all failed.
Question
Can this be done? if so, how?
I'm supporting iOS8+
Update This is how the views should layout/orient given #Casey's example:

I have faced with same problem and found example from Apple, which helps to prevent UIView from rotation: https://developer.apple.com/library/ios/qa/qa1890/_index.html
However, if UIView is not placed in the center of the screen, you should handle new position manually.

i think part of the reason this is so hard to answer is because in practice it doesn't really make sense.
say i make a view that uses autolayout to look like this in portrait and landscape:
if you wanted to prevent c from rotating like you are asking, what would you expect the final view to look like? would it be one of these 3 options?
without graphics of the portrait/landscape view you are trying to achieve and a description of the animation you are hoping for it'll be very hard to answer your question.
are you using NSLayoutConstraint, storyboard or frame based math to layout your views? any code you can provide would be great too

If you're wanting to have the same effect as the camera app, use size classes (see here and here).
If not, what is wrong with creating a UIWindow containing a view controller that doesn't rotate? The following code seems to work for me (where the UILabel represents the view you don't want to rotate).
class ViewController: UIViewController {
var staticWindow: UIWindow!
override func viewDidLoad() {
super.viewDidLoad()
showWindow()
}
func showWindow() {
let frame = CGRect(x: 10, y: 10, width: 100, height: 100)
let vc = MyViewController()
let label = UILabel(frame: frame)
label.text = "Hi there"
vc.view.addSubview(label)
staticWindow = UIWindow(frame: frame)
staticWindow.rootViewController = MyViewController()
staticWindow.windowLevel = UIWindowLevelAlert + 1;
staticWindow.makeKeyAndVisible()
staticWindow.rootViewController?.presentViewController(vc, animated: false, completion: nil)
}
}
class MyViewController: UIViewController {
override func shouldAutorotate() -> Bool {
return false
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return false
}
override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
}

Related

Swift: Keeping background ViewController active and responsive when presenting overlapping ViewController [duplicate]

I know there are several posts on SE relating to this issue but I couldn't get around them to find a proper solution for my situation.
I've a map view inside a view controller. I'm presenting another view controller with modalPresentationStyle set as custom(housing the card view) on top of the map view VC:
What I want for this scenario is that the map view to still be able to recognise touches when the location card view is presented along with the location card still recognising touches inside itself. Like how Apple Maps functions when the below list view is presented.
I know I can't pass touches through view controllers but I can't decide in between:
dumping the location card into the map view which I'll highly dislike,
presenting the location card via addChildViewController which I'm not quite sure how to implement in order to achieve the effect I want.
This SE question gives a detailed answer and a small sample project on mimicking Apple Maps' interface although I still can't figure out how to solve my issue.
I went with the option of dumping the location card view into the map view. BUT I didn't like it.
The thing was that I didn't want any of the logic of the location card inside the map view. But at the same time, iOS didn't provided an infrastructure and touch transfer between view controllers is basically not possible and the suggested workarounds seemed hard to implement.
First off, I migrated the location card view from a view controller to a struct.
Then, I defined a protocol named LocationCardPresentable which when extended, requires Self to be of type MapVC. Therefore, inside the protocol extension, I can work like if I'm inside MapVC but actually not. The protocol extension has various methods for presenting and dismissing the location card which I can call from inside MapVC.
So I managed to fix this just now by using a combination of loadView() and point(inside) to replace the rootView in the presented ViewController.
First, make a subclass of UIView which you will use as your rootView in any ViewControllers where you want touches to be passed on the ViewControllers underneath:
IgnoreTouchesView
import UIKit
class IgnoreTouchesView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for view in self.subviews {
if view.isKind(of: UIButton) {
let foundButtonView = view
if foundButtonView.isUserInteractionEnabled && !foundButtonView.isHidden && foundButtonView.point(inside: self.convert(point, to: foundButtonView), with: event) {
return true
}
}
}
return false
}
The point(inside) function checks if the user taps on any UIButtons in the OverlayVC, in that case touches should still be handled. Any other touches get passed on to the VC below. This could be done different by only ignoring the rootView but I haven't figured out quiet how to do this yet.
Then, in the VC that you want to present, add the following:
let ignoreView: IgnoreTouchesView = {
let view = IgnoreTouchesView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func loadView() {
view = ignoreView
}
Now add the ViewControllers to your original ViewController:
lazy var overlayVC : OverlayVC = {
let vc = SongOverlayVC()
vc.view.translatesAutoresizingMaskIntoConstraints = false
vc.delegate = self
return vc
}()
viewDidLoad(){
view.addSubview(songOverlayVC.view)
}
And that's it :)
Let me know if you run into any issues.

How can I mimic the bottom sheet from the Maps app?

Can anyone tell me how I can mimic the bottom sheet in the new Apple Maps app in iOS 10?
In Android, you can use a BottomSheet which mimics this behaviour, but I could not find anything like that for iOS.
Is that a simple scroll view with a content inset, so that the search bar is at the bottom?
I am fairly new to iOS programming so if someone could help me creating this layout, that would be highly appreciated.
This is what I mean by "bottom sheet":
I don't know how exactly the bottom sheet of the new Maps app, responds to user interactions. But you can create a custom view that looks like the one in the screenshots and add it to the main view.
I assume you know how to:
1- create view controllers either by storyboards or using xib files.
2- use googleMaps or Apple's MapKit.
Example
1- Create 2 view controllers e.g, MapViewController and BottomSheetViewController. The first controller will host the map and the second is the bottom sheet itself.
Configure MapViewController
Create a method to add the bottom sheet view.
func addBottomSheetView() {
// 1- Init bottomSheetVC
let bottomSheetVC = BottomSheetViewController()
// 2- Add bottomSheetVC as a child view
self.addChildViewController(bottomSheetVC)
self.view.addSubview(bottomSheetVC.view)
bottomSheetVC.didMoveToParentViewController(self)
// 3- Adjust bottomSheet frame and initial position.
let height = view.frame.height
let width = view.frame.width
bottomSheetVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
}
And call it in viewDidAppear method:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
addBottomSheetView()
}
Configure BottomSheetViewController
1) Prepare background
Create a method to add blur and vibrancy effects
func prepareBackgroundView(){
let blurEffect = UIBlurEffect.init(style: .Dark)
let visualEffect = UIVisualEffectView.init(effect: blurEffect)
let bluredView = UIVisualEffectView.init(effect: blurEffect)
bluredView.contentView.addSubview(visualEffect)
visualEffect.frame = UIScreen.mainScreen().bounds
bluredView.frame = UIScreen.mainScreen().bounds
view.insertSubview(bluredView, atIndex: 0)
}
call this method in your viewWillAppear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
Make sure that your controller's view background color is clearColor.
2) Animate bottomSheet appearance
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(0.3) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.mainScreen().bounds.height - 200
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
}
}
3) Modify your xib as you want.
4) Add Pan Gesture Recognizer to your view.
In your viewDidLoad method add UIPanGestureRecognizer.
override func viewDidLoad() {
super.viewDidLoad()
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
view.addGestureRecognizer(gesture)
}
And implement your gesture behaviour:
func panGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.view)
let y = self.view.frame.minY
self.view.frame = CGRectMake(0, y + translation.y, view.frame.width, view.frame.height)
recognizer.setTranslation(CGPointZero, inView: self.view)
}
Scrollable Bottom Sheet:
If your custom view is a scroll view or any other view that inherits from, so you have two options:
First:
Design the view with a header view and add the panGesture to the header. (bad user experience).
Second:
1 - Add the panGesture to the bottom sheet view.
2 - Implement the UIGestureRecognizerDelegate and set the panGesture delegate to the controller.
3- Implement shouldRecognizeSimultaneouslyWith delegate function and disable the scrollView isScrollEnabled property in two case:
The view is partially visible.
The view is totally visible, the scrollView contentOffset property is 0 and the user is dragging the view downwards.
Otherwise enable scrolling.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
let gesture = (gestureRecognizer as! UIPanGestureRecognizer)
let direction = gesture.velocity(in: view).y
let y = view.frame.minY
if (y == fullView && tableView.contentOffset.y == 0 && direction > 0) || (y == partialView) {
tableView.isScrollEnabled = false
} else {
tableView.isScrollEnabled = true
}
return false
}
NOTE
In case you set .allowUserInteraction as an animation option, like in the sample project, so you need to enable scrolling on the animation completion closure if the user is scrolling up.
Sample Project
I created a sample project with more options on this repo which may give you better insights about how to customise the flow.
In the demo, addBottomSheetView() function controls which view should be used as a bottom sheet.
Sample Project Screenshots
- Partial View
- FullView
- Scrollable View
Update iOS 15
In iOS 15, you can now use the native UISheetPresentationController.
if let sheet = viewControllerToPresent.sheetPresentationController {
sheet.detents = [.medium(), .large()]
// your sheet setup
}
present(viewControllerToPresent, animated: true, completion: nil)
Notice that you can even reproduce its navigation stack using the overcurrentcontext presentation mode:
let nextViewControllerToPresent: UIViewController = ...
nextViewControllerToPresent.modalPresentationStyle = .overCurrentContext
viewControllerToPresent.present(nextViewControllerToPresent, animated: true, completion: nil)
Legacy
I released a library based on my answer below.
It mimics the Shortcuts application overlay. See this article for details.
The main component of the library is the OverlayContainerViewController. It defines an area where a view controller can be dragged up and down, hiding or revealing the content underneath it.
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
Implement OverlayContainerViewControllerDelegate to specify the number of notches wished:
enum OverlayNotch: Int, CaseIterable {
case minimum, medium, maximum
}
func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int {
return OverlayNotch.allCases.count
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
heightForNotchAt index: Int,
availableSpace: CGFloat) -> CGFloat {
switch OverlayNotch.allCases[index] {
case .maximum:
return availableSpace * 3 / 4
case .medium:
return availableSpace / 2
case .minimum:
return availableSpace * 1 / 4
}
}
SwiftUI (12/29/20)
A SwiftUI version of the library is now available.
Color.red.dynamicOverlay(Color.green)
Previous answer
I think there is a significant point that is not treated in the suggested solutions: the transition between the scroll and the translation.
In Maps, as you may have noticed, when the tableView reaches contentOffset.y == 0, the bottom sheet either slides up or goes down.
The point is tricky because we can not simply enable/disable the scroll when our pan gesture begins the translation. It would stop the scroll until a new touch begins. This is the case in most of the proposed solutions here.
Here is my try to implement this motion.
Starting point: Maps App
To start our investigation, let's visualize the view hierarchy of Maps (start Maps on a simulator and select Debug > Attach to process by PID or Name > Maps in Xcode 9).
It doesn't tell how the motion works, but it helped me to understand the logic of it. You can play with the lldb and the view hierarchy debugger.
Our view controller stacks
Let's create a basic version of the Maps ViewController architecture.
We start with a BackgroundViewController (our map view):
class BackgroundViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
}
We put the tableView in a dedicated UIViewController:
class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
lazy var tableView = UITableView()
override func loadView() {
view = tableView
tableView.dataSource = self
tableView.delegate = self
}
[...]
}
Now, we need a VC to embed the overlay and manage its translation.
To simplify the problem, we consider that it can translate the overlay from one static point OverlayPosition.maximum to another OverlayPosition.minimum.
For now it only has one public method to animate the position change and it has a transparent view:
enum OverlayPosition {
case maximum, minimum
}
class OverlayContainerViewController: UIViewController {
let overlayViewController: OverlayViewController
var translatedViewHeightContraint = ...
override func loadView() {
view = UIView()
}
func moveOverlay(to position: OverlayPosition) {
[...]
}
}
Finally we need a ViewController to embed the all:
class StackViewController: UIViewController {
private var viewControllers: [UIViewController]
override func viewDidLoad() {
super.viewDidLoad()
viewControllers.forEach { gz_addChild($0, in: view) }
}
}
In our AppDelegate, our startup sequence looks like:
let overlay = OverlayViewController()
let containerViewController = OverlayContainerViewController(overlayViewController: overlay)
let backgroundViewController = BackgroundViewController()
window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController])
The difficulty behind the overlay translation
Now, how to translate our overlay?
Most of the proposed solutions use a dedicated pan gesture recognizer, but we actually already have one : the pan gesture of the table view.
Moreover, we need to keep the scroll and the translation synchronised and the UIScrollViewDelegate has all the events we need!
A naive implementation would use a second pan Gesture and try to reset the contentOffset of the table view when the translation occurs:
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
But it does not work. The tableView updates its contentOffset when its own pan gesture recognizer action triggers or when its displayLink callback is called. There is no chance that our recognizer triggers right after those to successfully override the contentOffset.
Our only chance is either to take part of the layout phase (by overriding layoutSubviews of the scroll view calls at each frame of the scroll view) or to respond to the didScroll method of the delegate called each time the contentOffset is modified. Let's try this one.
The translation Implementation
We add a delegate to our OverlayVC to dispatch the scrollview's events to our translation handler, the OverlayContainerViewController :
protocol OverlayViewControllerDelegate: class {
func scrollViewDidScroll(_ scrollView: UIScrollView)
func scrollViewDidStopScrolling(_ scrollView: UIScrollView)
}
class OverlayViewController: UIViewController {
[...]
func scrollViewDidScroll(_ scrollView: UIScrollView) {
delegate?.scrollViewDidScroll(scrollView)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
delegate?.scrollViewDidStopScrolling(scrollView)
}
}
In our container, we keep track of the translation using a enum:
enum OverlayInFlightPosition {
case minimum
case maximum
case progressing
}
The current position calculation looks like :
private var overlayInFlightPosition: OverlayInFlightPosition {
let height = translatedViewHeightContraint.constant
if height == maximumHeight {
return .maximum
} else if height == minimumHeight {
return .minimum
} else {
return .progressing
}
}
We need 3 methods to handle the translation:
The first one tells us if we need to start the translation.
private func shouldTranslateView(following scrollView: UIScrollView) -> Bool {
guard scrollView.isTracking else { return false }
let offset = scrollView.contentOffset.y
switch overlayInFlightPosition {
case .maximum:
return offset < 0
case .minimum:
return offset > 0
case .progressing:
return true
}
}
The second one performs the translation. It uses the translation(in:) method of the scrollView's pan gesture.
private func translateView(following scrollView: UIScrollView) {
scrollView.contentOffset = .zero
let translation = translatedViewTargetHeight - scrollView.panGestureRecognizer.translation(in: view).y
translatedViewHeightContraint.constant = max(
Constant.minimumHeight,
min(translation, Constant.maximumHeight)
)
}
The third one animates the end of the translation when the user releases its finger. We calculate the position using the velocity & the current position of the view.
private func animateTranslationEnd() {
let position: OverlayPosition = // ... calculation based on the current overlay position & velocity
moveOverlay(to: position)
}
Our overlay's delegate implementation simply looks like :
class OverlayContainerViewController: UIViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard shouldTranslateView(following: scrollView) else { return }
translateView(following: scrollView)
}
func scrollViewDidStopScrolling(_ scrollView: UIScrollView) {
// prevent scroll animation when the translation animation ends
scrollView.isEnabled = false
scrollView.isEnabled = true
animateTranslationEnd()
}
}
Final problem: dispatching the overlay container's touches
The translation is now pretty efficient. But there is still a final problem: the touches are not delivered to our background view. They are all intercepted by the overlay container's view.
We can not set isUserInteractionEnabled to false because it would also disable the interaction in our table view. The solution is the one used massively in the Maps app, PassThroughView:
class PassThroughView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self {
return nil
}
return view
}
}
It removes itself from the responder chain.
In OverlayContainerViewController:
override func loadView() {
view = PassThroughView()
}
Result
Here is the result:
You can find the code here.
Please if you see any bugs, let me know ! Note that your implementation can of course use a second pan gesture, specially if you add a header in your overlay.
Update 23/08/18
We can replace scrollViewDidEndDragging with
willEndScrollingWithVelocity rather than enabling/disabling the scroll when the user ends dragging:
func scrollView(_ scrollView: UIScrollView,
willEndScrollingWithVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
switch overlayInFlightPosition {
case .maximum:
break
case .minimum, .progressing:
targetContentOffset.pointee = .zero
}
animateTranslationEnd(following: scrollView)
}
We can use a spring animation and allow user interaction while animating to make the motion flow better:
func moveOverlay(to position: OverlayPosition,
duration: TimeInterval,
velocity: CGPoint) {
overlayPosition = position
translatedViewHeightContraint.constant = translatedViewTargetHeight
UIView.animate(
withDuration: duration,
delay: 0,
usingSpringWithDamping: velocity.y == 0 ? 1 : 0.6,
initialSpringVelocity: abs(velocity.y),
options: [.allowUserInteraction],
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Try Pulley:
Pulley is an easy to use drawer library meant to imitate the drawer
in iOS 10's Maps app. It exposes a simple API that allows you to use
any UIViewController subclass as the drawer content or the primary
content.
https://github.com/52inc/Pulley
I wrote my own library to achieve the intended behaviour in ios Maps app. It is a protocol oriented solution. So you don't need to inherit any base class instead create a sheet controller and configure as you wish. It also supports inner navigation/presentation with or without UINavigationController.
See below link for more details.
https://github.com/OfTheWolf/UBottomSheet
You can try my answer https://github.com/SCENEE/FloatingPanel. It provides a container view controller to display a "bottom sheet" interface.
It's easy to use and you don't mind any gesture recognizer handling! Also you can track a scroll view's(or the sibling view) in a bottom sheet if needed.
This is a simple example. Please note that you need to prepare a view controller to display your content in a bottom sheet.
import UIKit
import FloatingPanel
class ViewController: UIViewController {
var fpc: FloatingPanelController!
override func viewDidLoad() {
super.viewDidLoad()
fpc = FloatingPanelController()
// Add "bottom sheet" in self.view.
fpc.add(toParent: self)
// Add a view controller to display your contents in "bottom sheet".
let contentVC = ContentViewController()
fpc.set(contentViewController: contentVC)
// Track a scroll view in "bottom sheet" content if needed.
fpc.track(scrollView: contentVC.tableView)
}
...
}
Here is another example code to display a bottom sheet to search a location like Apple Maps.
iOS 15 in 2021 adds UISheetPresentationController, which is Apple's first public release of an Apple Maps-style "bottom sheet":
UISheetPresentationController
UISheetPresentationController lets you present your view controller as a sheet. Before you present your view controller, configure its sheet presentation controller with the behavior and appearance you want for your sheet.
Sheet presentation controllers specify a sheet's size based on a detent, a height where a sheet naturally rests. Detents allow a sheet to resize from one edge of its fully expanded frame while the other three edges remain fixed. You specify the detents that a sheet supports using detents, and monitor its most recently selected detent using selectedDetentIdentifier.
https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
This new bottom sheet control is explored in WWDC Session 10063: Customize and Resize Sheets in UIKit
Unfortunately....
In iOS 15, the UISheetPresentationController has launched with only medium and large detents.
A small detent is notably absent from the iOS 15 API, which would be required to display an always-presented "collapsed" bottom sheet like Apple Maps:
Custom smaller Detents in UISheetPresentationController?
The medium detent was released to handle use cases such as the Share Sheet or the "••• More" menu in Mail: a button-triggered half sheet.
In iOS 15, Apple Maps is now using this UIKit sheet presentation rather than a custom implementation, which is a huge step in the right direction. Apple Maps in iOS 15 continues to show the "small" bar, as well as a 1/3rd height bar. But those view sizes are not public API available to developers.
UIKit engineers at the WWDC 2021 Labs seemed to know that a small detent would be a hugely popular UIKit component. I would expect to see an API expansion for iOS 16 next year.
We’ve just released a pure Swift Package supporting iOS 11.4+ which provides you a BottomSheet with theme and behavior options you can customize. This component is easy to use, and flexible. You can find it here: https://github.com/LunabeeStudio/LBBottomSheet.
A demo project is available in this repository too.
For example, it supports different ways to manage the needed height, and also adds to the controller behind it the ability to detect height changes and adapt its bottom content inset.
You can find more information on the GitHub repository and in the documentation: https://lbbottomsheet.lunabee.studio.
I think it can help you to do what you’re looking for. Don’t hesitate to tell me if you have comments/questions :)
Here you can see one of all the possible BottomSheet configurations:
**for iOS 15 Native Support available for this **
#IBAction func openSheet() {
let secondVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController")
// Create the view controller.
if #available(iOS 15.0, *) {
let formNC = UINavigationController(rootViewController: secondVC!)
formNC.modalPresentationStyle = UIModalPresentationStyle.pageSheet
guard let sheetPresentationController = formNC.presentationController as? UISheetPresentationController else {
return
}
sheetPresentationController.detents = [.medium(), .large()]
sheetPresentationController.prefersGrabberVisible = true
present(formNC, animated: true, completion: nil)
} else {
// Fallback on earlier versions
}
}
iOS 15 finally adds a native UISheetPresentationController!
Official documentation
https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
I recently created a component called SwipeableView as subclass of UIView, written in Swift 5.1 . It support all 4 direction, has several customisation options and can animate and interpolate different attributes and items ( such as layout constraints, background/tint color, affine transform, alpha channel and view center, all of them demoed with the respective show case ). It also supports the swiping coordination with the inner scroll view if set or auto detected. Should be pretty easy and straightforward to be used ( I hope 🙂)
Link at https://github.com/LucaIaco/SwipeableView
proof of concept:
Hope it helps
If you are looking for a SwiftUI 2.0 solution that uses View Struct, here it is:
https://github.com/kenfai/KavSoft-Tutorials-iOS/tree/main/MapsBottomSheet
Maybe you can try my answer https://github.com/AnYuan/AYPannel, inspired by Pulley. Smooth transition from moving the drawer to scrolling the list. I added a pan gesture on the container scroll view, and set shouldRecognizeSimultaneouslyWithGestureRecognizer to return YES. More detail in my github link above. Wish to help.

Glitch when interactively dismissing modal

We have run into this issue when implementing interactive dismissal of a modal view controller (dragging modal down should dismiss it) via UIPercentDrivenInteractiveTransition.
Setup:
setup UIViewController embedded in UINavigationController with at least one button in UINavigationBar
modally present another UIViewController embedded in UINavigationController with at least one button in UINavigationBar
setup UIPanGestureRecognizer on modaly presented UINavigationController to drive UIPercentDrivenInteractiveTransition
drag modal down "holding" it by point on UINavigationBar
Issue:
while slowly dragging down, animation glitches causing modal view to jump up and down
glitch only appears when :
both UINavigationBars have at least one button on them
you "hold" modal by the point on UINavigationBar
Minimal example can be downloaded from github repo.
Has anyone come accross such an issue? Are there any workarounds? Is there some flaw in our setup?
Update
Issue has been simulated on running project above on iPhone 5 simulator with iOS 9.3, OSX 10.11.4, compiled with Xcode 7.3.1.
Update 2
Further investigation showed, that issue is probably not in the animation: For some reason in given setup pan.translationInView(view) is returning unexpected values which causes animation to jump.
Partial workaround
Based on Vladimir's idea we partially fixed the issue by overriding hitTest method of UINavigationBar:
class DraggableNavigationBar: UINavigationBar {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, withEvent: event) else { return nil }
if view is UIControl || pointIsInsideNavigationButton(point) {
return view
} else {
return nil
}
}
private func pointIsInsideNavigationButton(point: CGPoint) -> Bool {
return subviews
.filter { $0.frame.contains(point) }
.filter { String($0.dynamicType) == "UINavigationItemButtonView" }
.isEmpty == false
}
}
Very interesting glitch. I found a partial solution of this problem few days ago, and since nobody found a full solution, I'll post this, maybe it will be helpful.
If you override hitTest method of UINavigationBar you can get rid of this issue when you dragging modal by holding on UINavigationBar:
extension UINavigationBar {
override public func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, withEvent: event) else { return nil }
if view.isKindOfClass(UIControl) {
return super.hitTest(point, withEvent: event)
} else {
return nil
}
}
}
Unfortunately if you drag modal by holding on UIBarButtonItem on UINavigationBar, glitch still be present.
You can also try another approach.
As you noticed, pan.translationInView(view) returns incorrect values which causes animation to jump.
You need to compare this value to y coordinate of modal view during dragging. You can get this value by checking presentation layer of the modal view controller:
...
let translation = pan.translationInView(view)
if let layer = view.layer.presentationLayer() {
print(layer.frame.origin.y)
}
...
You can see that when pan.translationInView(view) starts to show wrong value, layer.frame.origin.y still will be correct in that moment. You can compare these two values and find the pattern when value is incorrect, and change it to correct by adding few points to translation.y value.
I don't have a complete solution but I was able to reduce the glitch by certain amount. I could reproduce this issue on iPhone 5s with iOS 9.3.2 [by dragging down the screen holding Navigation Bar]
The problem seems to be in the UIView.animateWithDuration block of DismissalAnimator. By commenting out the delay and options i.e keeping them to the defaults you can reduce the jumping of the view. You could also try and check for diffrent UIViewAnimationOptions for which you get minimum jump.
UIView.animateWithDuration(0.3,
animations: {
dismissedView.frame = finalFrame
},
completion: { _ in
let didComplete = !transitionContext.transitionWasCancelled()
transitionContext.completeTransition(didComplete)
}
)
There is a question which seems to be dealing with same issue you are facing. And the responses varying from disabling auto layout, putting layoutIfNeeded in animation block [tried, both didn't work].

Animating changes to presentingViewController during interactive dismissal

I'm working on a UIPresentationController subclass similar to Mail.app's open draft behavior. When a view controller is presented, it doesn't go all the way to the top and the presenting view controller scales down as if it were falling back.
The basic gist of it is below:
class CustomPresentationController : UIPresentationController {
// Create a 40pt space above the view.
override func frameOfPresentedViewInContainerView() -> CGRect {
let frame = super.frameOfPresentedViewInContainerView()
let insets = UIEdgeInsets(top: 40, left: 0, bottom: 0, right: 0)
return UIEdgeInsetsInsetRect(frame, insets)
}
// Scale down when expanded is true, otherwise identity.
private func setScale(expanded expanded: Bool) {
if expanded {
let fromMeasurement = presentingViewController.view.bounds.width
let fromScale = (fromMeasurement - 30) / fromMeasurement
presentingViewController.view.transform = CGAffineTransformMakeScale(fromScale, fromScale)
} else {
presentingViewController.view.transform = CGAffineTransformIdentity
}
}
// Scale down alongside the presentation.
override func presentationTransitionWillBegin() {
presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ context in
self.setScale(expanded: true)
}, completion: { context in
self.setScale(expanded: !context.isCancelled())
})
}
// Scale up alongside the dismissal.
override func dismissalTransitionWillBegin() {
presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ context in
self.setScale(expanded: false)
}, completion: { context in
self.setScale(expanded: context.isCancelled())
})
}
// Fix the scaled view's frame on orientation change.
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
guard let bounds = containerView?.bounds else { return }
presentingViewController.view.bounds = bounds
}
}
This works fine for a non-interactive presentation or dismissal. When performing an interactive dismissal, however, all animations on presentingViewController.view run non-interactively. That is, the scaling will happen in the ~300ms it normally takes to dismiss rather than staying at 3% complete when 3% dismissed.
You can see this in a sample project is available on GitHub. and a video of the issue is on YouTube.
I've tried the following approaches but they all yield the same result:
A parallel animation as seen above.
Animating in the UIViewControllerAnimatedTransitioning.
Using a CABasicAnimation an manually adjusting the timing of the container view's layer.
The problem was that presentingViewController is not a descendent of the presentation's containerView. UIPercentDrivenInteractiveTransition works by setting containerView.layer.speed to zero and setting containerView.layer.timeOffset to reflect the percent complete. Since the view in question was not part of the hierarchy, its speed stayed at 1 and it completed as normal.
This is explicitly stated in the documentation for animateAlongsideTransition(_:,completion:):
Use this method to perform animations that are not handled by the animator objects themselves. All of the animations you specify must occur inside the animation context’s container view (or one of its descendants). Use the containerView property of the context object to get the container view. To perform animations in a view that does not descend from the container view, use the animateAlongsideTransitionInView:animation:completion: method instead.
As the documentation indicates, switching to animateAlongsideTransitionInView(_:,animation:,completion:) fixes the problem:
// Scale up alongside the dismissal.
override func dismissalTransitionWillBegin() {
presentingViewController.transitionCoordinator()?.animateAlongsideTransitionInView(presentingViewController.view, animation: { context in
self.setScale(expanded: false)
}, completion: { context in
self.setScale(expanded: context.isCancelled())
})
}
The comment on that method in the header is a lot more direct about this than the documentation:
// This alternative API is needed if the view is not a descendent of the container view AND you require this animation to be driven by a UIPercentDrivenInteractiveTransition interaction controller.
Thank you for the answer, had this issue on iOS 9.x and 10.x devices - using animateAlongsideTransition(in:animation:completion:) did the job.
Interestingly, on iOS 11.x animate(alongsideTransition:completion:) works correctly for presentingViewController too (there is no need to use animateAlongsideTransition(in:animation:completion:)) - looks like Apple lifted something under the hood in the latest iOS.

Slide UIInputView with UIViewController like Slack

I'd like to use the UIViewController's input accessory view like this:
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView! {
return self.bar
}
but the issue is that I have a drawer like view and when I slide the view open, the input view stays on the window. How can I keep the input view on the center view like Slack does it.
Where my input view stays at the bottom, taking up the full screen (the red is the input view in the image below):
There are two ways to do this exactly like Slack doing it, Meiwin has a medium post here A Stickler for Details: Implementing Sticky Input Field in iOS to show how he managed to do this which he actually puts an empty UIView as an inputAccessoryView then track it’s coordinates on screen to know where to put his custom view in relation with the empty view, this way can be helpful if you are going to support SplitViewController on iPad, but if you are not interested in this way, you can see how I managed to do this like this image
Here is before swiping
Here is after
All I did was actually taking a snapshot from the inputAccessoryView window and putting it on the NavigationController of the TableViewController
I am using SideMenu from Jon Kent and it’s pretty easy to do it with the UISideMenuNavigationControllerDelegate
var isInputAccessoryViewEnabled = true {
didSet {
self.inputAccessoryView?.isHidden = !self.isInputAccessoryViewEnabled
if self.isInputAccessoryViewEnabled {self.becomeFirstResponder()}
}
}
func sideMenuWillAppear(menu: UISideMenuNavigationController, animated: Bool) {
let inputWindow = UIApplication.shared.windows.filter({$0.className == "UITextEffectsWindow"}).first
self.inputAccessoryViewSnapShot = inputWindow?.snapshotView(afterScreenUpdates: false)
if let snapShotView = self.inputAccessoryViewSnapShot, let navView = self.navigationController?.view {
navView.addSubview(snapShotView)
}
self.isInputAccessoryViewEnabled = false
}
func sideMenuDidDisappear(menu: UISideMenuNavigationController, animated: Bool) {
self.inputAccessoryViewSnapShot?.removeFromSuperview()
self.isInputAccessoryViewEnabled = true
}
I hope that helps :)

Resources