4

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

enter image description here

Ratnesh Jain
  • 671
  • 7
  • 14
  • Have you found solution for this requirement? I am currently seeking solution for exactly same requirement as yours. – loongman May 14 '20 at 01:53
  • Eh, not quite. I have learned that it you do need to build this yourself and that it is not part of the Swift UI / Library. However, I did not learn what the name of the icon is called. If you find this out please let me know. – teamvtechnology May 15 '20 at 13:34
  • I have implemented the feature by following guides on https://fluffy.es/facebook-draggable-bottom-card-modal-1/ and https://fluffy.es/facebook-draggable-bottom-card-modal-2/ Fyi. – loongman May 20 '20 at 07:27
  • Thanks @loongman. This is a great article, but I have one question. It mentions in Step 7 to Add a UI View as a "Handle" View. This to me is the answer to our icon, but it doesn't say how to do this. How do you add a UI View as a "Handle" View? Below is the link to this image with this. https://iosimage.s3.amazonaws.com/2019/62-bottom-card/step7.png – teamvtechnology May 21 '20 at 16:54
  • In my case, the "Handle" view was added as a regular UIView with size (36, 4), cornerRadius (2), plus certain backgroundColor, to the dim view. And pin bottom to top of the card view, so, whenever the card view scrolls, the handle view scroll also. – loongman May 25 '20 at 02:12

3 Answers3

2
  1. 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/

  1. 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.

  1. 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)
}

Grabber example

Alexander
  • 1,228
  • 2
  • 15
  • 29
0

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.

Ratnesh Jain
  • 671
  • 7
  • 14
0

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 iOS Swift UIKit modal present dismiss indicator

You might need to alter some logic here based on your scenario / view controller hierarchy but hopefully this gives you some ideas.

Shawn Frank
  • 4,381
  • 2
  • 19
  • 29