Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement bottom sheet for SwiftUI #5392

Merged
merged 1 commit into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 140 additions & 25 deletions UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation
import UIKit

public struct ActionSheetConfiguration {
public var style: ActionStyleNew
public var tapToDismiss: Bool = true

public var ignoreInteractiveFalseMoving: Bool = true

public var coverBackgroundColor: UIColor = UIColor(white: 0, alpha: 0.5)

public var presentAnimationDuration: TimeInterval = 0.3
public var presentAnimationCurve: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: 5 << 16)
public var dismissAnimationDuration: TimeInterval = 0.2
public var dismissAnimationCurve: UIView.AnimationCurve = .easeIn

public var sideMargin: CGFloat
public var cornerRadius: CGFloat = 16

public init(style: ActionStyleNew) {
self.style = style

switch style {
case .alert:
sideMargin = 52
case .sheet:
sideMargin = 0
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
import UIKit
import SnapKit
import RxSwift
import RxCocoa
import UIExtensions

public class ActionSheetControllerNew: UIViewController, IDeinitDelegate {
private var disposeBag = DisposeBag()
public var onDeinit: (() -> ())?

private let content: UIViewController
private weak var viewDelegate: ActionSheetViewDelegate?
weak var interactiveTransitionDelegate: InteractiveTransitionDelegate?

private let configuration: ActionSheetConfiguration

private var keyboardHeightRelay = BehaviorRelay<CGFloat>(value: 0)
private var didAppear = false
private var dismissing = false

private var animator: ActionSheetAnimator?
private var ignoreByInteractivePresentingBreak = false

private var savedConstraints: [NSLayoutConstraint]?

public required init?(coder aDecoder: NSCoder) {
fatalError()
}

public init(content: UIViewController, configuration: ActionSheetConfiguration) {
self.content = content

if let viewDelegate = content as? ActionSheetViewDelegate {
self.viewDelegate = viewDelegate
}
self.configuration = configuration

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

let animator = ActionSheetAnimator(configuration: configuration)
self.animator = animator
transitioningDelegate = animator
animator.interactiveTransitionDelegate = self
viewDelegate?.actionSheetView = self

if let interactiveTransitionDelegate = content as? InteractiveTransitionDelegate {
self.interactiveTransitionDelegate = interactiveTransitionDelegate
}
modalPresentationStyle = .custom

NotificationCenter.default.addObserver(self,
selector: #selector(keyboardNotification(notification:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)
}

public override var shouldAutomaticallyForwardAppearanceMethods: Bool {
false
}

public override func viewDidLoad() {
super.viewDidLoad()

if configuration.tapToDismiss {
let tapView = ActionSheetTapView()
view.addSubview(tapView)
tapView.snp.makeConstraints { maker in
maker.edges.equalToSuperview()
}
tapView.handleTap = { [weak self] in
self?.dismissing = true
self?.dismiss(animated: true)
}
}

// add and setup content as child view controller
addChildController()
}

// lifecycle
public override func viewWillAppear(_ animated: Bool) {
if let savedConstraints = savedConstraints {
view.superview?.addConstraints(savedConstraints)
}

dismissing = false
super.viewWillAppear(animated)

if !ignoreByInteractivePresentingBreak {
content.beginAppearanceTransition(true, animated: animated)
}

disposeBag = DisposeBag()
keyboardHeightRelay
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] height in
self?.setContentViewPosition(animated: true)
})
.disposed(by: disposeBag)
}

public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !ignoreByInteractivePresentingBreak {
content.endAppearanceTransition()
}
ignoreByInteractivePresentingBreak = false
didAppear = true
}

public override func viewWillDisappear(_ animated: Bool) {
dismissing = true
savedConstraints = view.superview?.constraints

let interactiveTransitionStarted = animator?.interactiveTransitionStarted ?? false

if !(configuration.ignoreInteractiveFalseMoving && interactiveTransitionStarted) {
content.beginAppearanceTransition(false, animated: animated)
}
super.viewWillDisappear(animated)
}

public override func viewDidDisappear(_ animated: Bool) {
content.endAppearanceTransition()
super.viewDidDisappear(animated)

didAppear = false
}

@objc private func keyboardNotification(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }

let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let endFrameY = endFrame?.origin.y ?? 0

if endFrameY >= UIScreen.main.bounds.size.height {
keyboardHeightRelay.accept(0)
} else {
keyboardHeightRelay.accept(endFrame?.size.height ?? 0.0)
}
}

deinit {
onDeinit?()
removeChildController()
NotificationCenter.default.removeObserver(self)
}

}

// Child management
extension ActionSheetControllerNew {

private func addChildController() {
addChild(content)
view.addSubview(content.view)
setContentViewPosition(animated: false)
content.view.clipsToBounds = true
content.view.cornerRadius = configuration.cornerRadius
}

private func removeChildController() {
content.removeFromParent()
content.view.removeFromSuperview()
}

func setContentViewPosition(animated: Bool) {
guard !dismissing, content.view.superview != nil else {
return
}

content.view.snp.remakeConstraints { maker in
maker.leading.trailing.equalToSuperview().inset(configuration.sideMargin)
if configuration.style == .sheet { // content controller from bottom of superview
maker.top.equalToSuperview()
maker.bottom.equalToSuperview().inset(configuration.sideMargin + keyboardHeightRelay.value).priority(.required)
} else { // content controller by center of superview
maker.centerX.equalToSuperview()
maker.centerY.equalToSuperview().priority(.low)
maker.bottom.lessThanOrEqualTo(view.snp.bottom).inset(keyboardHeightRelay.value + 16)
}
if let height = viewDelegate?.height {
maker.height.equalTo(height)
}
}
if let superview = view.superview {
if animated && didAppear {
UIView.animate(withDuration: configuration.presentAnimationDuration) { () -> Void in
superview.layoutIfNeeded()
}
} else {
view.layoutIfNeeded()
}
}
}

}

extension ActionSheetControllerNew: ActionSheetView {

public func contentWillDismissed() {
dismissing = true
}

public func dismissView(animated: Bool) {
DispatchQueue.main.async {
self.dismiss(animated: animated)
}
}

public func didChangeHeight() {
setContentViewPosition(animated: true)
}

}

extension ActionSheetControllerNew: InteractiveTransitionDelegate {

public func start(direction: TransitionDirection) {
interactiveTransitionDelegate?.start(direction: direction)
dismissing = direction == .dismiss
}

public func move(direction: TransitionDirection, percent: CGFloat) {
interactiveTransitionDelegate?.move(direction: direction, percent: percent)
}

public func end(direction: TransitionDirection, cancelled: Bool) {
if direction == .dismiss, cancelled {
dismissing = false
}

interactiveTransitionDelegate?.end(direction: direction, cancelled: cancelled)
guard configuration.ignoreInteractiveFalseMoving else {
return
}
if cancelled {
ignoreByInteractivePresentingBreak = true
} else {
content.beginAppearanceTransition(false, animated: true)
viewDelegate?.didInteractiveDismissed()
}
}

public func fail(direction: TransitionDirection) {
interactiveTransitionDelegate?.fail(direction: direction)
}

}

extension ActionSheetControllerNew {

override open var childForStatusBarStyle: UIViewController? {
content
}

override open var childForStatusBarHidden: UIViewController? {
content
}

}

@available(iOS 13.0, *)
extension ActionSheetControllerNew: UIAdaptivePresentationControllerDelegate {
public func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
self.presentationController?.delegate?.presentationControllerShouldDismiss?(presentationController) ?? false
}

public func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
dismissing = true
self.presentationController?.delegate?.presentationControllerWillDismiss?(presentationController)
}

public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
dismissing = false
self.presentationController?.delegate?.presentationControllerDidDismiss?(presentationController)
}

public func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
self.presentationController?.delegate?.presentationControllerDidAttemptToDismiss?(presentationController)
}
}
Loading