I use this example to avoid keyboard frame when it appears. Now I want to make it protocol based to avoid extending all view controllers with this functionality. Now I am getting two errors which looks like conflict each other (one is want to add #objc and another one remove it):
My Code:
import UIKit
protocol KeyboardFrameIgnorable {
func listenForKeyboardFrameChange()
func stopListenKeyboardFrameChange()
}
extension KeyboardFrameIgnorable where Self: UIViewController {
func listenForKeyboardFrameChange()
{
NotificationCenter.default
.addObserver(self,
selector: #selector(onKeyboardFrameWillChangeNotificationReceived(_:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)
}
func stopListenKeyboardFrameChange()
{
NotificationCenter.default
.removeObserver(self,
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)
}
#objc
private func onKeyboardFrameWillChangeNotificationReceived(_ notification: Notification)
{
guard
let userInfo = notification.userInfo,
let keyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
else {
return
}
let keyboardFrameInView = view.convert(keyboardFrame, from: nil)
let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame.insetBy(dx: 0, dy: -additionalSafeAreaInsets.bottom)
let intersection = safeAreaFrame.intersection(keyboardFrameInView)
let keyboardAnimationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey]
let animationDuration: TimeInterval = (keyboardAnimationDuration as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue
let animationCurve = UIView.AnimationOptions(rawValue: animationCurveRaw)
UIView.animate(withDuration: animationDuration,
delay: 0,
options: animationCurve,
animations: {
self.additionalSafeAreaInsets.bottom = intersection.height + 10
self.view.layoutIfNeeded()
}, completion: nil)
}
}
Related
I have a SwiftUI application which has a carousel of videos. I'm using an AVPlayer with UIViewRepresentable and I'm creating the carousel with a ForEach loop of my custom UIViewRepresentable view. I want to have a "periodicTimeObserver" on the active AVPlayer, but it crashes and says
"An instance of AVPlayer cannot remove a time observer that was added
by a different instance of AVPlayer SwiftUI"
My question is how can I remove the periodicTimeObserver of an AVPlayer inside of a UIViewRepresentable inside of a UIView, without causing the app to crash?
Here is my code:
ForEach(videosArray.indices, id: \.self) { i in
let videoURL = videosArray[i]
ZStack {
VStack {
VideoView.init(viewModel: viewModel, videoURL: URL(string: videoURL)!, videoIndex: i)
}
}
}
struct VideoView: UIViewRepresentable {
#ObservedObject var viewModel = viewModel.init()
var videoURL:URL
var previewLength:Double?
var videoIndex: Int
func makeUIView(context: Context) -> UIView {
return PlayerView.init(frame: .zero, url: videoURL, previewLength: previewLength ?? 6)
}
func updateUIView(_ uiView: UIView, context: Context) {
if videoIndex == viewModel.currentIndexSelected {
if let playerView = uiView as? PlayerView {
if !viewModel.isPlaying {
playerView.pause()
} else {
playerView.play(customStartTime: viewModel.newStartTime, customEndTime: viewModel.newEndTime)
}
}
} else {
if let playerView = uiView as? PlayerView {
playerView.pause()
}
}
}
}
public class ViewModel: ObservableObject {
#Published public var currentIndexSelected: Int = 0
#Published public var isPlaying: Bool = true
#Published public var newStartTime = 0.0
#Published public var newEndTime = 30.0
}
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer:Timer?
var previewLength:Double
var player: AVPlayer?
var timeObserver: Any? = nil
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
player = AVPlayer(url: url)
player!.volume = 0
player!.play()
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
playerLayer.backgroundColor = UIColor.black.cgColor
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
self.previewLength = 15
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func pause() {
if let timeObserver = timeObserver {
self.player?.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
player?.pause()
}
#objc func replayFinishedItem(noti: NSNotification) {
print("REPLAY FINISHED NOTIIIII: \(noti)")
if let timeDict = noti.object as? [String: Any], let startTime = timeDict["startTime"] as? Double, let endTime = timeDict["endTime"] as? Double/*, let player = timeDict["player"] as? AVPlayer, let observer = timeDict["timeObserver"]*/ {
self.removeTheTimeObserver()
self.play(customStartTime: startTime, customEndTime: endTime)
}
}
#objc func removeTheTimeObserver() {
print("ATTEMPT TO REMOVE IT!")
if let timeObserver = timeObserver {
self.player?.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
}
func play(at playPosition: Double = 0.0, customStartTime: Double = 0.0, customEndTime: Double = 15.0) {
var startTime = customStartTime
var endTime = customEndTime
if customStartTime > customEndTime {
startTime = customEndTime
endTime = customStartTime
}
if playPosition != 0.0 {
player?.seek(to: CMTime(seconds: playPosition, preferredTimescale: CMTimeScale(1)))
} else {
player?.seek(to: CMTime(seconds: startTime, preferredTimescale: CMTimeScale(1)))
}
player?.play()
var timeDict: [String: Any] = ["startTime": startTime, "endTime": endTime]
NotificationCenter.default.addObserver(self, selector: #selector(self.replayFinishedItem(noti:)), name: .customAVPlayerShouldReplayNotification, object: nil)
self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime.init(value: 1, timescale: 100), queue: DispatchQueue.main, using: { [weak self] time in
guard let strongSelf = self else {
return
}
let currentTime = CMTimeGetSeconds(strongSelf.player!.currentTime())
let currentTimeStr = String(currentTime)
if let currentTimeDouble = Double(currentTimeStr) {
let userDefaults = UserDefaults.standard
userDefaults.set(currentTimeDouble, forKey: "currentTimeDouble")
NotificationCenter.default.post(name: .currentTimeDouble, object: currentTimeDouble)
if currentTimeDouble >= endTime {
if let timeObserver = strongSelf.timeObserver {
strongSelf.player?.removeTimeObserver(timeObserver)
strongSelf.timeObserver = nil
}
strongSelf.player?.pause()
NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
} else if let currentItem = strongSelf.player?.currentItem {
let seconds = currentItem.duration.seconds
if currentTimeDouble >= seconds {
if let timeObserver = strongSelf.timeObserver {
strongSelf.player?.removeTimeObserver(timeObserver)
strongSelf.timeObserver = nil
}
NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
}
}
}
})
}
}
As we all know from the iOS 13+ modal presentation style for normal UIViewController will default to .pageSheet. If you want to change it you can change it to your's desired style when presenting it. I am using MFMailComposeViewController and MFMessageComposeViewController in my app to share content. In case of MFMailComposeViewController when i choose modalPresentationStyle = .fullScreen it works perfectly fine...
but not in case of MFMessageComposeViewController. Please find the code snippet below
if (MFMessageComposeViewController.canSendText()) {
let controller = MFMessageComposeViewController()
controller.body = “Message Body”
controller.messageComposeDelegate = self
controller.modalPresentationStyle = .fullScreen
self.present(controller, animated: true, completion: nil)
self.trackEvent(shareType: "SMS")
}
}
While I can't answer your question on *why you cant use .fullScreen for MFMessageComposeViewController, I can tell you that if you implement your own custom transition you will get the desired behavior.
This is how you set custom transition for the view controller :
var customTransitionDelegate = CustomTransition(presentAnimation: CustomActionSheetPresentationTransition(), dismissAnimation: CustomActionSheetDismissalTransition())
if (MFMessageComposeViewController.canSendText()) {
let controller = MFMessageComposeViewController()
controller.body = "Message Body"
controller.messageComposeDelegate = self
controller.modalPresentationStyle = .custom
controller.transitioningDelegate = customTransitionDelegate
self.present(controller, animated: true, completion: nil)
}
You should also conform to the delegate and check the result to dismiss it after the user clicked cancel if he did :
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
switch result {
case .cancelled:
self.presentedViewController?.dismiss(animated: true, completion: nil)
default: break
}
}
The presentation logic
class CustomActionSheetPresentationTransition: NSObject, UIViewControllerAnimatedTransitioning {
private let duration = 0.6
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.viewController(forKey: .from) else { return }
guard let toView = transitionContext.viewController(forKey: .to) else { return }
var screenOffUp = CGAffineTransform()
let container = transitionContext.containerView
screenOffUp = CGAffineTransform(translationX: 0, y: -fromView.view.frame.height)
toView.view.frame = CGRect(x: 0, y: UIScreen.main.bounds.height, width: fromView.view.frame.width, height: fromView.view.frame.height)
toView.view.center.x = container.center.x
container.addSubview(toView.view)
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [], animations: {
toView.view.transform = screenOffUp
}, completion: { (success) in
transitionContext.completeTransition(success)
})
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
}
The dismissal logic
class CustomActionSheetDismissalTransition: NSObject, UIViewControllerAnimatedTransitioning {
private let duration = 0.8
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.viewController(forKey: .from) else { return }
let screenOffDown = CGAffineTransform(translationX: 0, y: fromView.view.frame.height)
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [], animations: {
fromView.view.transform = screenOffDown
}, completion: { (success) in
transitionContext.completeTransition(success)
})
}
}
As you can see I am setting the presentation style as .overCurrentContext.
extension SingleQuestionViewController: AddResponseDelegate {
func save(response text: String, questionID: Int) {
questionsWrapper.add(newResponse: text, questionID: questionID) { [weak self] successful in
if successful {
self?.responseTV?.safelyReload()
} else {
DispatchQueue.main.async {
let alertViewController = AlertViewController<Any>()
alertViewController.modalPresentationStyle = .overCurrentContext
let contentModel = RegularContentsModel(title: "controllerTitle", message: "message")
let authorizeButtonModel = SimpleButtonModel(title: "yes message", action: {
//action goes here
})
let doNothingButtonModel = SimpleButtonModel(title: "noMsg", action: {
//completion?()
})
alertViewController.styleRegular(regularContentsModel: contentModel,
models: [authorizeButtonModel, doNothingButtonModel])
self?.present(alertViewController, animated: false, completion: nil)
}
}
}
questionsWrapper.goToQuestion(with: questionID)
responseTV?.safelyReload()
}
}
Here is the result:
I don't think this is caused by it being on the background thread, because if I move it to viewDidLoad, then I get the same result:
override func viewDidLoad() {
super.viewDidLoad()
setUpTopBar()
setupSearchBar()
responseTV.showsVerticalScrollIndicator = false
setupArrows()
responseTV.register(SimpleCell.self, forCellReuseIdentifier: SimpleCell.reuseID)
setAccessibility()
let alertViewController = AlertViewController<Any>()
alertViewController.modalPresentationStyle = .overCurrentContext
let contentModel = RegularContentsModel(title: "controllerTitle", message: "message")
let authorizeButtonModel = SimpleButtonModel(title: "yes message", action: {
//action goes here
})
let doNothingButtonModel = SimpleButtonModel(title: "noMsg", action: {
//completion?()
})
alertViewController.styleRegular(regularContentsModel: contentModel,
models: [authorizeButtonModel, doNothingButtonModel])
self.present(alertViewController, animated: false, completion: nil)
}
Here is the implementation of my custom alert.
class AlertViewController<Payload>: AkinVC {
typealias FlagsAction = ([ReportFlag], Payload) -> Void
enum AlertStyle<Payload> {
case flag(FlagsAction)
}
let innerWholeAlertContainer = UIView()
let outerWholeAlertContainer = UIView()
let buttonStack = AlertButtonsStack()
var payload: Payload?
let transitionDuration: TimeInterval = 0.11
let containerWidth: CGFloat = 300
private var contentsView: UIView! {
didSet {
innerWholeAlertContainer.addSubview(contentsView)
contentsView.constraints(firstHorizontal: .distanceToLeading(innerWholeAlertContainer.leadingAnchor, 0),
secondHorizontal: .distanceToTrailing(innerWholeAlertContainer.trailingAnchor, 0),
vertical: .distanceToTop(innerWholeAlertContainer.topAnchor, 0),
secondVertical: .distanceToBottom(buttonStack.topAnchor, 0))
}
}
func styleNoButtons(regularContentsModel: RegularContentsModel) {
initialSetup()
let alertContentView = RegularContentsView()
alertContentView.model = regularContentsModel.forContainer(width: containerWidth)
contentsView = alertContentView
setButtonConstraints()
}
func styleAsFlagView(flagsAction: #escaping FlagsAction) {
initialSetup()
let stackView = FlagsStackView()
stackView.flagItemViews = [FlagItemView](ReportFlag.allCases)
contentsView = stackView
buttonStack.buttonModels(
ButtonModel(tekt: "Report", color: .romanceRed, tektColor: .white, action: { [weak stackView] in
guard let selectedFlags = stackView?.flagItemViews?.selectedFlags,
let payload = self.payload else { return }
flagsAction(selectedFlags, payload)
self.dismissAlert()
}),
ButtonModel(tekt: "Cancel", color: .white, tektColor: .black,
borders: BorderModel(color: UIColor.black.withAlphaComponent(0.16), width: 1, edges: [.top]),
action: { [weak self] in
self?.dismissAlert()
})
)
setButtonConstraints()
}
func styleAsOkayAlert(regularContentsModel: RegularContentsModel, action: Action? = nil) {
initialSetup()
let alertContentView = RegularContentsView()
alertContentView.model = regularContentsModel.forContainer(width: containerWidth)
contentsView = alertContentView
let okayModel = standardizeButtonsWithDismissAction(models: [SimpleButtonModel(title: "Okay, I got it.", action: action)])
buttonStack.buttonModels(okayModel)
setButtonConstraints()
}
func styleCancelAlert(regularContentsModel: RegularContentsModel, models: SimpleButtonModel...) {
initialSetup()
let alertContentView = RegularContentsView()
alertContentView.model = regularContentsModel.forContainer(width: containerWidth)
contentsView = alertContentView
var models = models
models.append(SimpleButtonModel(title: "Cancel"))
let newButtonModels = standardizeButtonsWithDismissAction(models: models)
buttonStack.buttonModels(newButtonModels)
setButtonConstraints()
}
func styleRegular(regularContentsModel: RegularContentsModel, models: SimpleButtonModel...) {
self.styleRegular(regularContentsModel: regularContentsModel, models: models)
}
func styleRegular(regularContentsModel: RegularContentsModel, models: [SimpleButtonModel]) {
initialSetup()
let alertContentView = RegularContentsView()
alertContentView.model = regularContentsModel.forContainer(width: containerWidth)
contentsView = alertContentView
let newButtonModels = standardizeButtonsWithDismissAction(models: models)
buttonStack.buttonModels(newButtonModels)
setButtonConstraints()
}
private func standardizeButtonsWithDismissAction(models: [SimpleButtonModel]) -> [ButtonModel] {
var buttonModelsToAdd: [ButtonModel] = []
let count = models.count
for (inde, model) in models.enumerated() {
var borders: [BorderModel] = []
if count > 2 || count == 1 {
borders.append(BorderModel(color: .lightGray, width: 1, edges: [.top]))
} else if count == 2 {
if inde == 0 {
borders.append(BorderModel(color: .lightGray, width: 1, edges: [.top]))
} else if inde == 1 {
borders.append(BorderModel(color: .lightGray, width: 1, edges: [.left, .top]))
}
}
buttonModelsToAdd.append(ButtonModel(tekt: model.title, color: .white, tektColor: .darkGray, borders: borders, action: {
model.action?()
self.dismissAlert()
}))
}
return buttonModelsToAdd
}
func dismissAlert() {
UIView.animate(withDuration: transitionDuration, animations: {
self.view.alpha = 0
}) { (completed) in
self.safelyDissmiss(animated: false)
}
}
fileprivate func initialSetup() {
self.view.alpha = 0
modalPresentationStyle = .currentContext
view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
view.addSubview(outerWholeAlertContainer)
outerWholeAlertContainer.addSubview(innerWholeAlertContainer)
outerWholeAlertContainer.pinToEdges(innerWholeAlertContainer)
innerWholeAlertContainer.backgroundColor = .white
innerWholeAlertContainer.addSubview(buttonStack)
outerWholeAlertContainer.constraints(.horizontal(.centeredHorizontallyWith(view)),
.vertical(.centeredVerticallyTo(view)),
.horizontal(.width(containerWidth)) )
}
func setButtonConstraints() {
buttonStack.constraints(.horizontal(.distanceToLeading(innerWholeAlertContainer.leadingAnchor, 0)),
.horizontal(.distanceToTrailing(innerWholeAlertContainer.trailingAnchor, 0)),
.vertical(.distanceToBottom(innerWholeAlertContainer.bottomAnchor, 0)))
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
outerWholeAlertContainer.layer.applySketchShadow()
innerWholeAlertContainer.roundCorners(constant: 15)
UIView.animate(withDuration: transitionDuration, animations: {
self.view.alpha = 1
})
}
}
Here is what the visual debugger shows:
The view controller:
creates the alert controller;
sets modalPresentationStyle to .overCurrentContext; and
and calls styleRegular.
But styleRegular:
calls initialSetup
which resets modalPresentationStyle to .currentContext
It's that last step that is discarding your previous .overCurrentContext setting.
Warning: Attempt to present on
whose view is not in the window
hierarchy!
I have a custom presented view controller and would like to present UIAlertController on top of it. When presenting without custom transition everything works fine.
Tried adding definesPresentationContext = true without luck
Maybe my custom transition should include something like addChildViewController() ?
First VC
let adVC = AdViewController(with: adView)
adVC.setupAd(with: index)
let adNav = UINavigationController(rootViewController: adVC)
adNav.modalPresentationStyle = .custom
adNav.transitioningDelegate = adVC.adCustomTransition
self.present(adNav, animated: true, completion: nil)
AdViewController
class AdViewController: UIViewController {
var adCustomTransition = AdCustomTransition()
override func viewDidLoad() {
super.viewDidLoad()
definesPresentationContext = true;
submitButton.addTarget(self, action: #selector(presentAlert), for: .touchUpInside)
}
func presentAlert() {
DispatchQueue.main.async {
let alertVC = UIAlertController(title: "Alert!", message: "test alert message", preferredStyle: .alert)
alertVC.view.tintColor = .main
alertVC.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alertVC, animated: true, completion: nil)
}
}
}
class AdCustomTransition : NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return AdAnimationController(withDuration: 0.4, forTransitionType: .present)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return AdAnimationController(withDuration: 0.4, forTransitionType: .dismiss)
}
}
class AdAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case present, dismiss
}
var duration: TimeInterval
var isPresenting: Bool
init(withDuration duration: TimeInterval, forTransitionType type: TransitionType) {
self.duration = duration
self.isPresenting = type == .present
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return self.duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toFrame = transitionContext.finalFrame(for: transitionContext.viewController(forKey: .to)!)
if isPresenting == true {
let toVC = (transitionContext.viewController(forKey: .to) as! MainNavigationController).viewControllers.first as! AdViewController
containerView.addSubview(toVC.view)
containerView.layoutIfNeeded()
let bottomHeight = toVC.getSafeBottomHeight()
let offset = toVC.containerView.bounds.height - 120 - bottomHeight
toVC.containerView.transform = CGAffineTransform(translationX: 0, y: offset)
UIView.animate(withDuration: self.duration, delay: 0, usingSpringWithDamping: 5.4, initialSpringVelocity: 0.8, options: .curveEaseInOut, animations: {
toVC.containerView.transform = CGAffineTransform(translationX: 0, y: -1 * bottomHeight)
}, completion: { (_) in
toVC.view.frame = toFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
} else {
let fromVC = (transitionContext.viewController(forKey: .from) as! MainNavigationController).viewControllers.first as! AdViewController
let toVC = transitionContext.viewController(forKey: .to)!
containerView.addSubview(toVC.view)
containerView.addSubview(fromVC.view)
containerView.layoutIfNeeded()
let bottomHeight = fromVC.getSafeBottomHeight()
let offset = fromVC.containerView.bounds.height - 110 - bottomHeight
fromVC.containerView.transform = .identity
UIView.animate(withDuration: self.duration, delay: 0, usingSpringWithDamping: 5.7, initialSpringVelocity: 0.8, options: .curveEaseInOut, animations: {
fromVC.containerView.transform = CGAffineTransform(translationX: 0, y: offset)
}, completion: { (_) in
toVC.view.frame = toFrame
UIApplication.shared.keyWindow?.addSubview(toVC.view)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}
everyone i've been tearing out my hair trying to find a solution to an interactive view controller transition where you use the pan gesture in the downward direction to bring a full screen view controller from the top to the bottom. Has anyone run across or created any code like this. Below is my code. I already have the dismiss gesture down but cant figure out how to present the view controller by swiping down on the screen. PLEASE HELP!!!
import UIKit
class ViewController: UIViewController {
let interactor = Interactor()
var interactors:Interactor? = nil
let Mview = ModalViewController()
let mViewT: ModalViewController? = nil
var presentedViewControllers: UIViewController?
override func viewDidLoad() {
Mview.transitioningDelegate = self
Mview.modalPresentationStyle = .FullScreen
}
#IBAction func cameraSlide(sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
// convert y-position to downward pull progress (percentage)
let translation = sender.translationInView(Mview.view)
let verticalMovement = translation.y / UIScreen.mainScreen().bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactors else { return }
switch sender.state {
case .Began:
interactor.hasStarted = true
self.presentViewController(Mview, animated: true, completion: nil)
case .Changed:
interactor.shouldFinish = progress > percentThreshold
interactor.updateInteractiveTransition(progress)
case .Cancelled:
interactor.hasStarted = false
interactor.cancelInteractiveTransition()
case .Ended:
interactor.hasStarted = false
if !interactor.shouldFinish {
interactor.cancelInteractiveTransition()
} else {
interactor.finishInteractiveTransition()
} default:
break
}
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimator()
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PresentAnimator()
}
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
}
class PresentAnimator: NSObject {
}
extension PresentAnimator: UIViewControllerAnimatedTransitioning
{
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 1.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC2 = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
let toVC2 = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
let containerView2 = transitionContext.containerView() else {return}
let initialFrame = transitionContext.initialFrameForViewController(fromVC2)
toVC2.view.frame = initialFrame
toVC2.view.frame.origin.y = -initialFrame.height * 2
containerView2.addSubview(fromVC2.view)
containerView2.addSubview(toVC2.view)
let screenbounds = UIScreen.mainScreen().bounds
let Stage = CGPoint(x: 0, y: 0)
let finalFrame = CGRect(origin: Stage, size: screenbounds.size)
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
toVC2.view.frame = finalFrame
}, completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}
class ModalViewController: UIViewController {
let interactors = Interactor()
var interactor:Interactor? = nil
#IBAction func close(sender: UIButton) {
dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func handleGesture(sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
// convert y-position to downward pull progress (percentage)
let translation = sender.translationInView(self.view)
let verticalMovement = translation.y / -view.bounds.height * 2
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactor else { return }
switch sender.state {
case .Began:
interactor.hasStarted = true
dismissViewControllerAnimated(true, completion: nil)
case .Changed:
interactor.shouldFinish = progress > percentThreshold
interactor.updateInteractiveTransition(progress)
case .Cancelled:
interactor.hasStarted = false
interactor.cancelInteractiveTransition()
case .Ended:
interactor.hasStarted = false
if !interactor.shouldFinish {
interactor.cancelInteractiveTransition()
} else {
interactor.finishInteractiveTransition()
} default:
break
}
}
}
import UIKit
class DismissAnimator: NSObject {
}
extension DismissAnimator : UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 1.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
let containerView = transitionContext.containerView()
else {
return
}
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
let screenBounds = UIScreen.mainScreen().bounds
let topLeftCorner = CGPoint(x: 0, y: -screenBounds.height * 2)
let finalFrame = CGRect(origin: topLeftCorner, size: screenBounds.size)
UIView.animateWithDuration(
transitionDuration(transitionContext),animations: {fromVC.view.frame = finalFrame},
completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}
If you want a simple Pan Gesture to switch between UIViewControllers, you can check out this:
http://www.appcoda.com/custom-segue-animations/
If you want it to be interactive, as in you can go back and forth between VCs without having to complete the whole transition, I suggest you check out this:
https://www.youtube.com/watch?v=3jAlg5BnYUU
If you want to go even further and have a custom dismissing animation, then look no further than this:
https://www.raywenderlich.com/110536/custom-uiviewcontroller-transitions