Warning: Attempt to present error when trying to present UIAlertController on Custom - swift

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

Related

UIViewController + NSNotification extension protocol based #objc issue

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

Massive CPU overuse whenever I push ViewController - Swift - Programmatically

I have a high CPU level whenever I push a new ViewController from the navigationController.
I used the Time Profiler tool and it turns out that the issue that is causing this CPU overuse is related to a ViewController I embedded in the rootViewController.
The ViewController I'm talking about is as follows:
class QuoteGeneratorController : UIViewController {
let interactiveShowTextView : UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "avenir-black", size: 35)
textView.textColor = .white
textView.textAlignment = .left
textView.textContainer.maximumNumberOfLines = 3
textView.backgroundColor = .clear
textView.isEditable = false
textView.isSelectable = false
textView.isUserInteractionEnabled = false
return textView
}()
let progressBar : UIProgressView = {
let progressView = UIProgressView(progressViewStyle: .bar)
progressView.trackTintColor = UIColor.clear
progressView.tintColor = UIColor.white
return progressView
}()
let quotesArray = ["Hello", "Hello2", "Hello3"]
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("I'm called")
view.subviews.forEach { subview in
subview.layer.masksToBounds = true
subview.layer.cornerRadius = progressBar.frame.height / 2.0
}
}
override func viewDidAppear(_ animated: Bool) {
setUpAnimation()
}
override func viewDidLoad() {
setUpUI()
setUpAnimation()
}
fileprivate func setUpUI(){
view.addSubview(interactiveShowTextView)
view.addSubview(progressBar)
interactiveShowTextView.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor)
interactiveShowTextView.heightAnchor.constraint(equalToConstant: 150).isActive = true
progressBar.anchor(top: interactiveShowTextView.bottomAnchor, leading: interactiveShowTextView.leadingAnchor, bottom: nil, trailing: nil, padding: .init(top: 10, left: 0, bottom: 0, right: 0), size: .init(width: 70, height: 9))
}
var iterator : Int = 0
fileprivate func setUpAnimation(){
switch iterator {
case 0:
animateAndIterate()
case 1:
animateAndIterate()
case 2:
animateAndIterate()
default:
self.iterator = 0
self.setUpAnimation()
}
}
fileprivate func animateAndIterate(){
UIView.animate(withDuration: 0.0, animations: {
self.progressBar.layoutIfNeeded()
}, completion: { finished in
self.progressBar.progress = 1.0
self.interactiveShowTextView.text = self.quotesArray[self.iterator]
self.interactiveShowTextView.fadeIn()
UIView.animate(withDuration: 3, delay: 0.0, options: [.curveLinear], animations: {
self.progressBar.layoutIfNeeded()
self.perform(#selector(self.afterAnimation), with: nil, afterDelay: 2.5)
}, completion: { finished in
self.interactiveShowTextView.fadeOut()
self.progressBar.progress = 0
self.iterator = self.iterator + 1
self.setUpAnimation()
})
})
}
#objc func afterAnimation() {
self.interactiveShowTextView.fadeOut()
}
}
I actually don't know what could have caused the issue and since I'm new to time profiler I though that some of you have encountered the same process through development.
In Time Profiler I'm getting this indications:
When using completion blocks it's recommended to use weak references to the result. Otherwise the ARC will count every reference you used inside those block.
Use closures like this:
view.subviews.forEach { [weak self] subview in
subview.layer.masksToBounds = true
subview.layer.cornerRadius = progressBar.frame.height / 2.0
}
// (...)
fileprivate func animateAndIterate() {
UIView.animate(withDuration: 0.0,
animations: { [weak self] in
self?.progressBar.layoutIfNeeded()
}, completion: { [weak self] finished in
self?.progressBar.progress = 1.0
self?.interactiveShowTextView.text = self.quotesArray[self.iterator]
self?.interactiveShowTextView.fadeIn()
UIView.animate(withDuration: 3,
delay: 0.0,
options: [.curveLinear],
animations: { [weak self] in
self?.progressBar.layoutIfNeeded()
self?.perform(#selector(self.afterAnimation),
with: nil, afterDelay: 2.5)
}, completion: { finished in
self?.interactiveShowTextView.fadeOut()
self?.progressBar.progress = 0
self?.iterator = self.iterator + 1
self?.setUpAnimation()
})
})
}

iOS 13 modalPresentationStyle = UIModalPresentationFullScreen not working for MFMessageComposeViewController

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

MKAnnotationView image toggling's transition issue

I have 2 image states for the MKAnnotationView, which are selected and deselected. The issue is that the transition between these two states is poor. There isn't much online about this and I'm having trouble with transitions, generally.
Here is the MKAnnotationView I am using:
class CustomPinView: MKAnnotationView {
func updateImage() {
guard let mapAnnotation = annotation as? MapAnnotation else {return}
if let selectedImageName = mapAnnotation.selectedImageName, isSelected {
image = UIImage(inCurrentBundleWithName: selectedImageName)
} else if let imageName = mapAnnotation.imageName {
image = UIImage(inCurrentBundleWithName: imageName)
} else {
image = nil
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
updateImage()
}
override var annotation: MKAnnotation? {
didSet {
updateImage()
}
}
}
I updated the updateImage function to look like this:
private func updateImage() {
CATransaction.begin()
CATransaction.setAnimationDuration(0.2)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn, animations: {
guard let mapAnnotation = self.annotation as? MapAnnotation else {return}
if let selectedImageName = mapAnnotation.selectedImageName, self.isSelected {
self.image = UIImage(inCurrentBundleWithName: selectedImageName)
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.6)
} else if let imageName = mapAnnotation.imageName {
self.image = UIImage(inCurrentBundleWithName: imageName)
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
} else {
self.image = nil
}
self.centerOffset = CGPoint(x: 0.5, y: 0.5)
}, completion: nil)
CATransaction.commit()
}
This is extracted from this answer here: https://stackoverflow.com/a/54431257/4114335

Custom alert presenting over empty window instead of previous viewController

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.