This should be an easy question for most of you. Presenting view controllers like in the attached photo now have a bar at the top of them (see red arrow) to indicate that the user can swipe down to dismiss the controller. Please help with any of the following questions:
What is the proper term for this icon?
Is it part of swift's ui tools / library or is it just a UIImage?
Can someone provide a simple snippet on how to implement - perhaps it is something similar to the code below
let sampleController = SampleController()
sampleController.POSSIBLE_OPTION_TO_SHOW_BAR_ICON = true
present(sampleController, animated: true, completion: nil)
Please see the red arrow for the icon that I am referring to
grabber
grabber is a small horizontal indicator that can appear at the top edge of a
sheet.
In general, include a grabber in a resizable sheet. A grabber shows people that they can drag the sheet to resize it; they can also
tap it to cycle through the detents. In addition to providing a visual
indicator of resizability, a grabber also works with VoiceOver so
people can resize the sheet without seeing the screen. For developer
guidance, see prefersGrabberVisible.
https://developer.apple.com/design/human-interface-guidelines/ios/views/sheets/
From iOS 15+ UISheetPresentationController has property prefersGrabberVisible
https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801906-prefersgrabbervisible
A grabber is a visual affordance that indicates that a sheet is
resizable. Showing a grabber may be useful when it isn't apparent that
a sheet can resize or when the sheet can't dismiss interactively.
Set this value to true for the system to draw a grabber in the
standard system-defined location. The system automatically hides the
grabber at appropriate times, like when the sheet is full screen in a
compact-height size class or when another sheet presents on top of it.
Playground snippet for iOS 15:
import UIKit
import PlaygroundSupport
let viewController = UIViewController()
viewController.view.frame = CGRect(x: 0, y: 0, width: 380, height: 800)
viewController.view.backgroundColor = .white
PlaygroundPage.current.liveView = viewController.view
PlaygroundPage.current.needsIndefiniteExecution = true
let button = UIButton(primaryAction: UIAction { _ in showModal() })
button.setTitle("Show page sheet", for: .normal)
viewController.view.addSubview(button)
button.frame = CGRect(x: 90, y: 100, width: 200, height: 44)
func showModal {
let viewControllerToPresent = UIViewController()
viewControllerToPresent.view.backgroundColor = .blue.withAlphaComponent(0.5)
viewControllerToPresent.modalPresentationStyle = .pageSheet // or .formSheet
if let sheet = viewControllerToPresent.sheetPresentationController {
sheet.detents = [.medium(), .large()]
sheet.prefersGrabberVisible = true
}
viewController.present(viewControllerToPresent, animated: true, completion: nil)
}
The feature you are asking is not available in UIKit.
You have to implement custom view-controller transition animation with subclassing UIPresentationController for rendering that pull up/down handle.
UIPresentationController (developer.apple.com)
For custom presentations, you can provide your own presentation controller to give the presented view controller a custom appearance. Presentation controllers manage any custom chrome that is separate from the view controller and its contents. For example, a dimming view placed behind the view controller’s view would be managed by a presentation controller. Apple Documentation
This can be achieved by any UIView or you can use any image if you want by adding subview to UIPresentationController's contentView above the presentedView.
To provide the swipe gesture to dismiss/present, you must implement UIPercentDrivenInteractionController.
You can refer to this tutorial below for detailed understanding.
UIPresentationController Tutorial By raywenderlich.com
You should look for presentationDirection = .bottom in your case.
For gesture driven dismissal, you should check below tutorial
Custom-UIViewcontroller-Transitions-getting-started
I hope this might help you.
If you need to add this indicator within the view controller that is being presented if you do not want to do any custom presentations and just work with the default transitions.
The first thing to think about is your view hierarchy, is the indicator going to be part of your navigation bar or perhaps your view does not have navigation bar - so accordingly you probably need some code to find the correct view to add this indicator to.
In my scenario, I needed a navigation bar so my view controllers were within a navigation controller but you could do the same inside your view controllers directly:
1: Subclass a Navigation Controller
This is optional but it would be nice to abstract away all of this customization into the navigation controller.
I do a check to see if the NavigationController is being presented. This might not be the best way to check but since this is not part of the question, refer to these answers to check if a view controller was presented modally or not
class CustomNavigationController: UINavigationController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// this checks if the ViewController is being presented
if presentingViewController != nil {
addModalIndicator()
}
}
private func addModalIndicator() {
let indicator = UIView()
indicator.backgroundColor = .tertiaryLabel
let indicatorSize = CGSize(width: 30, height: 5)
let indicatorX = (navigationBar.frame.width - indicatorSize.width) / CGFloat(2)
indicator.frame = CGRect(origin: CGPoint(x: indicatorX, y: 8), size: indicatorSize)
indicator.layer.cornerRadius = indicatorSize.height / CGFloat(2.0)
navigationBar.addSubview(indicator)
}
}
2: Present the Custom Navigation Controller
let someVC = UIViewController()
let customNavigationController = CustomNavigationController()
customNavigationController.setViewControllers([stationsVC], animated: false)
present(playerNavigationController, animated: true) { }
3: This will produce the following results
You might need to alter some logic here based on your scenario / view controller hierarchy but hopefully this gives you some ideas.
Related
I implemented the share extension and I want animate my View Controller with a crossDissolve, so i set the modalPresentationStyle = .overFullScreen and modalTransitionStyle = crossDissolve but it seems not working. The VC still appear from the bottom to the top and with the new iOS 13 modal style (not completly full screen).
Anyone know how to solve it? It tried both with and without storyboard.
NB: I'm not talking about a normal VC presentation, but the presentation of the share extension, it means that it's another app that present my VC.
One way to do it would be to have the system presented viewcontroller as a container.
And then present your content viewcontroller inside modally.
// this is the entry point
// either the initial viewcontroller inside the extensions storyboard
// or
// the one you specify in the .plist file
class ContainerVC: UIViewController {
// afaik presenting in viewDidLoad/viewWillAppear is not a good idea, but this produces the exact result you are looking for.
// meaning the content slides up when extension is triggered.
override func viewWillAppear() {
super.viewWillAppear()
view.backgroundColor = .clear
let vc = YourRootVC()
vc.view.backgroundColor = .clear
vc.modalPresentationStyle = .overFullScreen
vc.loadViewIfNeeded()
present(vc, animated: false, completion: nil)
}
}
and then use the content viewcontroller to show your root viewcontroller and its view hierarchy.
class YourRootVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let vc = UIViewController() // your actual content
vc.view.backgroundColor = .blue
vc.view.frame = CGRect(origin: vc.view.center, size: CGSize(width: 200, height: 200))
view.addSubview(vc.view)
addChild(vc)
}
}
Basically a container and a wrapper in order to get the control over the views being displayed.
Source: I had the same problem. This solution works for me.
I'm experimenting with UISearchController, but I can't get it right. I present a clean UISearchController from my own UIViewController, and it looks great - but as soon as I rotate the device, it gets shifted a few points up or down.
To recreate this, just do these few steps:
Create a new Single View project
Delete Main.storyboard from the project files and remove its name from the project settings (Project -> General -> Target -> Main Interface)
In AppDelegate.swift:
application didFinishLaunchingWithOptions...{
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
return true
}
ViewController.swift:
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
let button = UIButton(frame: CGRect(x: 20, y: 200, width: 100, height: 40))
button.setTitle("Search", for: .normal)
button.backgroundColor = .black
button.addTarget(self, action: #selector(click), for: .touchUpInside)
self.view.addSubview(button)
}
#objc func click(){
self.present(UISearchController(searchResultsController: nil), animated: true, completion: nil)
}
}
And that's it. This is what I'm seeing:
Presenting the search when the device is in portrait mode looks great in portrait - but if you rotate the device to landscape while presenting the searchbar, it will be wrongly positioned, a few pixels above the top of the screen.
Presenting the search when in landscape will yield the opposite. It looks great in landscape, but when rotating it to portrait the entire search controller view will be pushed down a few pixels.
It's not a matter of height size on the bar. The entire bar gets pushed up/down.
I tried investigating a bit further. I presented the search controller from landscape mode and rotated to portrait, and then debugged the view hierarchy:
To be honest, I'm not quite sure what I'm looking at. The top-most view is a UISearchBarBackground embedded within a _UISearchBarContainerView, which is within a _UISearchControllerView.
As you can see in the size inspector on the right side, the middle-view "container" has y: 5, which makes no sense. When debugging the correct state, y is 0. What's really interesting is that the top-most view has y: -44 in this corrupt situation, and it has the same when it's correct - but you can clearly see that there is some space leftover above it. There seem to be three different y-positions. I don't get it.
I've read some guides on how to implement a UISearchController, but literally every single example I find is people modally presenting a custom ViewController that contains a SearchController. This will result in the entire custom ViewController being animated up from below.
Since the UISearchController is a subclass of UIViewController, I wanted to test out presenting it directly, not as part of a regular UIViewController. This gives the cool effect that the searchBar animates in from above, and the keyboard from below.
But why doesn't this work?
Edit: I just found out that if I enable Hide status bar in the project settings, the UISearchController looks even more correct in landscape than the "correct state" from above, and even animates correctly to portrait. It's super weird, because the status bar doesn't change at all. It was never visible in landscape. Why does this happen? It seems so buggy. Look at these three states:
The first state is when showing search controller from portrait then rotating to landscape (doesn't matter if Hide status bar is enabled or not.
The second state is when showing search controller from landscape if Hide status bar is false
The third state is when showing search controller from landscape if Hide status bar is true.
As stated in the documentation:
Although a UISearchController object is a view controller, you should
never present it directly from your interface. If you want to present
the search results interface explicitly, wrap your search controller
in a UISearchContainerViewController object and present that object
instead.
Try to wrap your UISearchController inside a UISearchContainerViewController.
How about make a custom view contains search bar.
Make it on top of the layers.
And when rotate occurs update frame/constraints of it.
This is covered in guidelines for designing for iPhone X. Now Navigation bar contains search controller. You can instanciate the search controller and set it to navigationItem.searchController. Now it will handle the search controller while rotation.
var search = UISearchController(searchResultsController: nil)
self.navigationItem.searchController = search
You can do this tric
class ViewController: UIViewController {
let sc = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
let button = UIButton(frame: CGRect(x: 20, y: 200, width: 100, height: 40))
button.setTitle("Search", for: .normal)
button.backgroundColor = .black
button.addTarget(self, action: #selector(click), for: .touchUpInside)
self.view.addSubview(button)
}
#objc func click(){
self.present(sc, animated: true, completion: nil)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
var mFrame = sc.view.frame
mFrame.origin.y = 5.0
sc.view.frame = mFrame
}
I have 2 view controllers: "MapScreenVC" and "PullUpMenuVC"; When you tap a button on the "MapScreenVC", the "PullUpMenuVC" should appear, pulling up from the bottom. Them problem I am having is that in regards to the default UIView for the "PullUpMenuVC", any visual changes I make in storyboards do not show up.
That said, when I call my prepareBackgroundView() function, the view changes accordingly upon build.
How can I edit the view in storyboards but still animate the view in code? I feel like I have a misunderstanding of how views are referenced from the storyboard. Is the view that I am animating not the default view of the view controller, which is the same view that should be visually edited in the storyboard?
My "PullUpMenuVC" class:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
func prepareBackgroundView()
{
view.backgroundColor = .white
view.tintColor = defaultTint
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.35) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.main.bounds.height * 1
self?.view.frame = CGRect(0, yComponent, frame!.width, frame!.height)
}
This is a screenshot of my storyboard:
I should also point out that I have connected the "PullUpMenuVC" class to the view controller in my storyboard.
I think you have misunderstood how this is usually done in iOS and Xcode. From your description above I understand you want to do something that looks like a Modal View Presentation.
If you use the default modal view presentation iOS offers for doing that transition between ViewController, you will not have to implement the animations yourself. In fact you can do the whole think in the Storyboard by just hooking up the segues.
I am attaching some reading material that might help you.
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/PresentingaViewController.html
https://www.raywenderlich.com/462-storyboards-tutorial-for-ios-part-2
Custom transition if you need it.
https://www.raywenderlich.com/359-ios-animation-tutorial-custom-view-controller-presentation-transitions
Hope the above help.
I want to display some UI elements, like a search bar, on top of my app's first VC, and also on top of a second VC that it presents.
My solution for this was to create a ContainerViewController, which calls addChildViewController(firstViewController), and view.addSubview(firstViewController.view). And then view.addSubview(searchBarView), and similar for each of the UI elements.
At some point later, FirstViewController may call present(secondViewController), and ideally that slides up onto screen with my search bar and other elements still appearing on top of both view controllers.
Instead, secondViewController is presented on top of ContainerViewController, thus hiding the search bar.
I also want, when a user taps on the search bar, for ContainerViewController to present SearchVC, on top of everything. For that, it's straightforward - containerVC.present(searchVC).
How can I get this hierarchy to work properly?
If I understand correctly, your question is how to present a view controller on top (and within the bounds) of a child view controller which may have a different frame than the bounds of the parent view. That is possible by setting modalPresentationStyle property of the view controller you want to present to .overCurrentContext and setting definesPresentationContext of your child view controller to true.
Here's a quick example showing how it would work in practice:
override func viewDidLoad() {
super.viewDidLoad()
let childViewController = UIViewController()
childViewController.view.backgroundColor = .yellow
childViewController.view.translatesAutoresizingMaskIntoConstraints = true
childViewController.view.frame = view.bounds.insetBy(dx: 60, dy: 60)
view.addSubview(childViewController.view)
addChildViewController(childViewController)
childViewController.didMove(toParentViewController: self)
// Wait a bit...
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
let viewControllerToPresent = UIViewController()
viewControllerToPresent.modalPresentationStyle = .overCurrentContext // sets to show itself over current context
viewControllerToPresent.view.backgroundColor = .red
childViewController.definesPresentationContext = true // defines itself as current context
childViewController.present(viewControllerToPresent, animated: true, completion: nil)
}
}
I've a custom transition between two controller that works close to the iOS mail app, which one stays on top of the other with some implemented scrolling behavior.
If I present a new view controller from the Presented view controller which isn't full screen sized, and then I dismiss this new presented view controller, the previous Presented view controller changes its height and then resizes itself.
I know this might be a little confusing but check the gif example below.
As you can see, If I present this custom image picker and then dismiss it, the view controller which presented it warps to full screen and then resizes to the initial value.
How can I prevent this from happening? I want the ViewController which presents the image picker keeps its height.
After the dismiss you can see the resize happening.
Setting the presenting view controllers size
Since it's a UIViewControllerAnimatedTransitioning I create a custom presentation and the size it's set has it's own identity
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
let containerBounds = self.containerView?.bounds
let origin = CGPoint(x: 0.0, y: ((containerBounds?.size.height)! * 0.05))
let size = CGSize(width: (containerBounds?.size.width)! , height: ((containerBounds?.size.height)! * 0.95))
// Applies the attributes
let presentedViewFrame: CGRect = CGRect(origin: origin, size: size)
return presentedViewFrame
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
Any hint?
thanks
I think that is where the issue is. You are forcing the frame size which is not working out. You should use something like preferredContentSize.
You can simply add this to the viewDidLoad of your CustomPresentationController.
Alternatively you may also try modalPresentationStyle as "Over Current Context"
You can refer very good examples of how you can keep some part of VC as transparent here