Transmission icon indicating copy to clipboard operation
Transmission copied to clipboard

Present a sheet from UIKit

Open qeude opened this issue 7 months ago • 3 comments

I'm trying out this lib and in my app, most of my navigation is still using UIKit because I'm still mixing UIKit and SwiftUI for now.

While the presentation view modifier works in many cases, it would be convenient in many cases to be able to present a sheet right from UIKit like this for instance.

present(viewController, transition: .sheet(.ideal))

Would it be something possible here? Thanks!

qeude avatar Apr 17 '25 09:04 qeude

I recently made all of the presentation controllers open, so you can reuse the transitions. I haven't made a nice, easy to use API like you suggested but I can look into it.

In the meantime, you can use the .ideal detent like so:

import Transmission


let viewController = PresentationHostingController(
    content: Color.blue.frame(height: 200)
)
if let sheetPresentationController = viewController.sheetPresentationController {
    sheetPresentationController.detents = [
        PresentationLinkTransition.SheetTransitionOptions.Detent.ideal.toUIKit(in: sheetPresentationController)
    ]
}
present(viewController, animated: true)

nathantannar4 avatar Apr 17 '25 18:04 nathantannar4

Thanks!

When using SwiftUI, is it needed to use PresentationHostingController? or should UIHostingController have the same behaviour here? Ideally I would like to avoid having to use a specific subclass or UIHostingController for my use case

It seems to work well unless I use a true UIKit UIViewController (not a SwiftUI view wrapped in UIHostingController)

Here without a UIScrollView, the grow animation looks weird, maybe just because of my UIStackView distribution though

https://github.com/user-attachments/assets/ab5b63cd-8212-4d49-8193-2d8422e22385

ViewController

class ScrollViewController: UIViewController {

  private let scrollView: UIView = {
    let scrollView = UIView()
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    return scrollView
  }()

  private let contentStackView: UIStackView = {
    let stackView = UIStackView()
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.axis = .vertical
    stackView.spacing = 20
    stackView.distribution = .fill
    stackView.alignment = .fill
    stackView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 20, right: 20)
    stackView.isLayoutMarginsRelativeArrangement = true
    return stackView
  }()

  // UI Elements
  private let headerImageView: UIImageView = {
    let imageView = UIImageView()
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.contentMode = .scaleAspectFill
    imageView.clipsToBounds = true
    imageView.backgroundColor = .systemGray5
    imageView.heightAnchor.constraint(equalToConstant: 200).isActive = true
    imageView.image = UIImage(systemName: "photo")?.withRenderingMode(.alwaysTemplate)
    imageView.tintColor = .systemGray3
    return imageView
  }()

  private let titleLabel: UILabel = {
    let label = UILabel()
    label.font = .systemFont(ofSize: 24, weight: .bold)
    label.text = "Profile Information"
    return label
  }()

  private let nameField: UITextField = {
    let textField = UITextField()
    textField.placeholder = "Enter your name"
    textField.borderStyle = .roundedRect
    textField.heightAnchor.constraint(equalToConstant: 44).isActive = true
    return textField
  }()

  private let saveButton: UIButton = {
    let button = UIButton(type: .system)
    button.setTitle("Save Profile", for: .normal)
    button.backgroundColor = .systemBlue
    button.setTitleColor(.white, for: .normal)
    button.layer.cornerRadius = 8
    button.heightAnchor.constraint(equalToConstant: 50).isActive = true
    return button
  }()

  override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground
    title = "Profile"
    setupScrollView()
    setupStackView()
  }

  private func setupScrollView() {
    view.addSubview(scrollView)
    scrollView.addSubview(contentStackView)

    NSLayoutConstraint.activate([
      scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
      scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
      scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

      contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
      contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
      contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
      contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
      contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
    ])
  }

  private func setupStackView() {
    // Add views to stack view in order
    contentStackView.addArrangedSubview(headerImageView)
    contentStackView.addArrangedSubview(titleLabel)
    contentStackView.addArrangedSubview(nameField)


    // Add some extra spacing before the save button
    let spacerView = UIView()
    spacerView.heightAnchor.constraint(equalToConstant: 20).isActive = true
    contentStackView.addArrangedSubview(spacerView)

    contentStackView.addArrangedSubview(saveButton)
  }
}

And when replacing the container UIView with a UIScrollView, it just fulling expand.

Image

qeude avatar Apr 17 '25 20:04 qeude

When using SwiftUI, is it needed to use PresentationHostingController? or should UIHostingController

If you don't use PresentationHostingController, then if your View height changes after its been presented, the UISheetPresentationController won't know and so it won't update the detent height.

It seems to work well unless I use a true UIKit UIViewController

The .ideal height is calculated based on the view's systemLayoutSizeFitting, which by default calculates based on autolayout constraints.

Here without a UIScrollView, the grow animation looks weird, maybe just because of my UIStackView distribution though

Yea, you'll probably need to account for stretching, with some kind of flexible spacer view or perhaps changing the alignment/distribution

nathantannar4 avatar Apr 18 '25 01:04 nathantannar4