lodev09/react-native-true-sheet

View on GitHub
ios/TrueSheetViewController.swift

Summary

Maintainability
A
0 mins
Test Coverage
//
//  Created by Jovanni Lo (@lodev09)
//  Copyright (c) 2024-present. All rights reserved.
//
//  This source code is licensed under the MIT license found in the
//  LICENSE file in the root directory of this source tree.
//

// MARK: - SizeInfo

struct SizeInfo {
  var index: Int
  var value: CGFloat
}

// MARK: - TrueSheetViewControllerDelegate

protocol TrueSheetViewControllerDelegate: AnyObject {
  func viewControllerDidChangeWidth(_ width: CGFloat)
  func viewControllerDidDismiss()
  func viewControllerSheetDidChangeSize(_ sizeInfo: SizeInfo)
  func viewControllerWillAppear()
  func viewControllerKeyboardWillShow(_ keyboardHeight: CGFloat)
  func viewControllerKeyboardWillHide()
}

// MARK: - TrueSheetViewController

class TrueSheetViewController: UIViewController, UISheetPresentationControllerDelegate {
  // MARK: - Properties

  weak var delegate: TrueSheetViewControllerDelegate?

  var blurView: UIVisualEffectView
  var lastViewWidth: CGFloat = 0
  var detentValues: [String: SizeInfo] = [:]

  var sizes: [Any] = ["medium", "large"]

  var maxHeight: CGFloat?
  var contentHeight: CGFloat = 0
  var footerHeight: CGFloat = 0

  var cornerRadius: CGFloat?
  var grabber = true
  var dimmed = true
  var dimmedIndex: Int? = 0

  // MARK: - Setup

  init() {
    blurView = UIVisualEffectView()

    super.init(nibName: nil, bundle: nil)

    blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    blurView.frame = view.bounds

    view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
    view.insertSubview(blurView, at: 0)
  }

  deinit {
    NotificationCenter.default.removeObserver(self)
  }

  @available(*, unavailable)
  required init?(coder _: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  @available(iOS 15.0, *)
  func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheet: UISheetPresentationController) {
    if let rawValue = sheet.selectedDetentIdentifier?.rawValue,
       let sizeInfo = detentValues[rawValue] {
      delegate?.viewControllerSheetDidChangeSize(sizeInfo)
    }
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default.addObserver(
      self, selector: #selector(keyboardWillShow(_:)),
      name: UIResponder.keyboardWillShowNotification,
      object: nil
    )

    NotificationCenter.default.addObserver(
      self, selector: #selector(keyboardWillHide(_:)),
      name: UIResponder.keyboardWillHideNotification,
      object: nil
    )
  }

  @objc
  private func keyboardWillShow(_ notification: Notification) {
    guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
      return
    }

    delegate?.viewControllerKeyboardWillShow(keyboardSize.height)
  }

  @objc
  private func keyboardWillHide(_: Notification) {
    delegate?.viewControllerKeyboardWillHide()
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    delegate?.viewControllerWillAppear()
  }

  override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    delegate?.viewControllerDidDismiss()
  }

  /// This is called multiple times while sheet is being dragged.
  /// let's try to minimize size update by comparing last known width
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if lastViewWidth != view.frame.width {
      delegate?.viewControllerDidChangeWidth(view.bounds.width)
      lastViewWidth = view.frame.width
    }
  }

  /// Setup dimmed sheet.
  /// `dimmedIndex` will further customize the dimming behavior.
  @available(iOS 15.0, *)
  func setupDimmedBackground(for sheet: UISheetPresentationController) {
    if dimmed, dimmedIndex == 0 {
      sheet.largestUndimmedDetentIdentifier = nil
    } else {
      sheet.largestUndimmedDetentIdentifier = .large

      if #available(iOS 16.0, *) {
        if dimmed, let dimmedIndex, sheet.detents.indices.contains(dimmedIndex - 1) {
          sheet.largestUndimmedDetentIdentifier = sheet.detents[dimmedIndex - 1].identifier
        } else if let lastIdentifier = sheet.detents.last?.identifier {
          sheet.largestUndimmedDetentIdentifier = lastIdentifier
        }
      }
    }
  }

  /// Prepares the view controller for sheet presentation
  func configureSheet(at index: Int = 0, _ completion: ((SizeInfo) -> Void)?) {
    let defaultSizeInfo = SizeInfo(index: index, value: view.bounds.height)

    guard #available(iOS 15.0, *), let sheet = sheetPresentationController else {
      completion?(defaultSizeInfo)
      return
    }

    detentValues = [:]

    var detents: [UISheetPresentationController.Detent] = []

    for (index, size) in sizes.enumerated() {
      let detent = detentFor(size, with: contentHeight + footerHeight, with: maxHeight) { id, value in
        self.detentValues[id] = SizeInfo(index: index, value: value)
      }

      detents.append(detent)
    }

    sheet.animateChanges {
      sheet.detents = detents
      sheet.prefersEdgeAttachedInCompactHeight = true
      sheet.prefersGrabberVisible = grabber
      sheet.preferredCornerRadius = cornerRadius
      sheet.delegate = self

      var identifier: UISheetPresentationController.Detent.Identifier = .medium

      if sheet.detents.indices.contains(index) {
        let detent = sheet.detents[index]
        if #available(iOS 16.0, *) {
          identifier = detent.identifier
        } else if detent == .large() {
          identifier = .large
        }
      }

      setupDimmedBackground(for: sheet)

      sheet.selectedDetentIdentifier = identifier
      completion?(detentValues[identifier.rawValue] ?? defaultSizeInfo)
    }
  }
}