I have been trying to debug this for several days now but I can't seem to understand how it happens. I might be missing some information on how child view controllers are called.
I have a map that opens a child view controller on longpress. This works most of the time. Sometimes though, the app crashes. The console tells me:
Fatal error: Attempted to read an unowned reference but object 0x283f3cf80 was already deallocated
Note that this does not happen most of the time. It is rare and sometimes I need to trigger the long-press many times until it comes up. This is why I cannot use breakpoints.
To help me debug, I have added many prints starting from "CHECK 1" to "CHECK 16". These print in the correct order when everything works well. When it breaks, it breaks between "CHECK 14" and "CHECK 15".
I use this code to call my ChildVC:
let slideVC = TripOverviewVC(customer: myCustomer, route: myRoute, panelController: panelController!, annotationManager: annotationManager!, mapview: mapView!)
slideVC.view.roundCorners([.topLeft, .topRight], radius: 22)
slideVC.view.layer.zPosition = 15
print("CHECK 8")
add(slideVC)
print("CHECK 13")
let height = view.frame.height
let width = view.frame.width
slideVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
print("CHECK 14")
And this is my add(_ child) function:
func add(_ child: UIViewController) {
addChild(child)
print("CHECK 9")
view.addSubview(child.view)
print("CHECK 11")
child.didMove(toParent: self)
print("CHECK 12")
}
On my child VC, the viewWillAppear prints "CHECK 10" which is called correctly. However, after the child has been added using my add function, I guess that viewDidAppear should be called. But it never does. This is my viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("CHECK 15")
UIView.animate(withDuration: 0.3) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.main.bounds.height - self!.view.frame.height*0.5
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
}
tmpOrigin = self.view.frame.origin
print("CHECK 16")
}
This means that something breaks between adding the child as a subview, and before it is displayed on the screen, which is exactly what I can see on my app. What I don't understand is: What is being called between the two steps? Have I misunderstood the view lifecycle?
Here the force unwrapping is the problem which may lead to crash when the self is released before the execution of the animation which is delayed by 3 sec
let yComponent = UIScreen.main.bounds.height - self!.view.frame.height*0.5
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
Instead it can be updated as
UIView.animate(withDuration: 0.3) { [weak self] in
guard let this = self else { return }
let frame = this.view.frame
let yComponent = UIScreen.main.bounds.height - this.view.frame.height *0.5
this.view.frame = CGRectMake(0, yComponent, frame.width, frame.height)
}
Related
I have seen variations of the following code all over StackOverflow:
import UIKit
class segueFromLeft: UIStoryboardSegue {
override func perform() {
// Assign the source and destination views to local variables.
let src = self.source.view as UIView!
let dst = self.destination.view as UIView!
// Get the screen width and height.
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
// Specify the initial position of the destination view.
dst?.frame = CGRect(x: screenWidth, y: 0, width: screenWidth,
height: screenHeight)
// Access the app's key window and insert the destination view
above the current (source) one.
let window = UIApplication.shared.keyWindow
window?.insertSubview(dst!, aboveSubview: src!)
// Animate the transition.
UIView.animate(withDuration: 0.5, animations: { () -> Void in
src?.frame = (src?.frame.offsetBy(dx: -screenWidth, dy: 0))!
dst?.frame = (dst?.frame.offsetBy(dx: -screenWidth, dy: 0))!
}) { (Finished) -> Void in
self.source.present(self.destination, animated: false, completion: nil) {
}
}
}
}
At first, the code operates as a nice way of transitioning from one view to another. But with continued use, most of the problems that have been listed on this website as a result from it relate to memory. Every time the segue is used, the destination view is initialized and the source view remains in memory. With continued use, the memory use continues to grow and grow.
A simple dismissal of the source view did not function for me, the screen just went black.
My question is, how can we fix this problem?
iOS 13 seems to use a new UIPresentationController for presenting modal view controllers, but one that does not rely on taking snapshots of the presenting view controller (as most / all libraries out there do). The presenting view controller is 'live' and continues to display animations / changes while the modal view controller is showing above a transparent / tinted background.
I'm able to replicate this easily (as the aim is to make a backward compatible version for iOS 10 / 11 / 12 etc) by using a CGAffineTransform on the presenting view controller's view, however frequently while rotating the device, the presenting view begins to de-shape and grow out of bounds seemingly because the system updates its frame while there's an active transform applied to it.
According to the documentation, frame is undefined when there's a transform applied to the view. Given the system seems to be modifying the frame and not me, how do I solve this without ending up with hacky solutions where I'm updating the presenting view's bounds? I need this presentation controller to remain generic since the presenting controller could be any shape or form, and won't necessarily be a full-screen view.
Here's what I have so far - it's a simple UIPresentationController subclass, which seems to work fine, however rotating the device and then dismissing the presented view controller seems to de-shape the presenting view controller's bounds (becomes too wide or shrinks, depending on whether you presented the modal controller while in landscape / portrait)
class SheetPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return CGRect(x: 40, y: containerView!.bounds.height / 2, width: containerView!.bounds.width-80, height: containerView!.bounds.height / 2)
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
if let _ = presentingViewController.transitionCoordinator {
// We're transitioning - don't touch the frame yet as it'll
// clash with our transform
} else {
self.presentedView?.frame = self.frameOfPresentedViewInContainerView
}
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
containerView?.backgroundColor = .clear
if let coordinator = presentingViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.3)
// Scale the presenting view
self?.presentingViewController.view.layer.cornerRadius = 16
self?.presentingViewController.view.transform = CGAffineTransform.init(scaleX: 0.9, y: 0.9)
}, completion: nil)
}
}
override func dismissalTransitionWillBegin() {
if let coordinator = presentingViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.containerView?.backgroundColor = .clear
self?.presentingViewController.view.layer.cornerRadius = 0
self?.presentingViewController.view.transform = .identity
}, completion: nil)
}
}
}
And the Presenting Animation controller:
import UIKit
final class PresentingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
return
}
let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)
let containerView = transitionContext.containerView
containerView.addSubview(presentedViewController.view)
let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
presentedViewController.view.frame = finalFrameForPresentedView
// Move it below the screen so it slides up
presentedViewController.view.frame.origin.y = containerView.bounds.height
animator.addAnimations {
presentedViewController.view.frame = finalFrameForPresentedView
}
animator.addCompletion { (animationPosition) in
if animationPosition == .end {
transitionContext.completeTransition(true)
}
}
animator.startAnimation()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
}
As well as the dismissing animation controller:
import UIKit
final class DismissingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedViewController = transitionContext.viewController(forKey: .from) else {
return
}
guard let presentingViewController = transitionContext.viewController(forKey: .to) else {
return
}
let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
let containerView = transitionContext.containerView
let offscreenFrame = CGRect(x: finalFrameForPresentedView.minX, y: containerView.bounds.height, width: finalFrameForPresentedView.width, height: finalFrameForPresentedView.height)
let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)
animator.addAnimations {
presentedViewController.view.frame = offscreenFrame
}
animator.addCompletion { (position) in
if position == .end {
// Complete transition
transitionContext.completeTransition(true)
}
}
animator.startAnimation()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
}
Okay I figured it out. It seems iOS 13 does NOT use a scale transform. The moment you do that, as explained, rotating the device will modify the frame of the presenting view and since you've got a transform applied to the view already, the view will resize in unexpected ways and the transform will no longer be valid.
The solution is to instead use a z-axis perspective, which will give you the exact same result, but doing so will survive rotations etc since all you're doing is moving the view back into 3D space (Z-axis), thus effectively zooming it out. Here's the transform that did this for me (Swift):
func calculatePerspectiveTransform() -> CATransform3D {
let eyePosition:Float = 10.0;
var contentTransform:CATransform3D = CATransform3DIdentity
contentTransform.m34 = CGFloat(-1/eyePosition)
contentTransform = CATransform3DTranslate(contentTransform, 0, 0, -2)
return contentTransform
}
Here's an article explaining how this works: https://whackylabs.com/uikit/2014/10/29/add-some-perspective-to-your-uiviews/
In your UIPresenterController, you would need to do the following too in order to handle this transform across rotations properly:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// Reset transform before we rotate and then apply it again during rotation
if let presentingView = presentingViewController.view {
presentingView.layer.transform = CATransform3DIdentity
}
coordinator.animate(alongsideTransition: { [weak self] (context) in
if let presentingView = self?.presentingViewController.view {
presentingView.layer.transform = self?.calculatePerspectiveTransform() ?? CATransform3DIdentity
}
})
}
Custom presentations are a tricky part of UIKit. Here's what comes to mind, no guarantees ;-)
I would suggest you either try to "commit" the animation on the presenting view - so in the presentationTransitionDidEnd(Bool) callback remove the transform and set appropriate constraints on the presenting view that match what the transform did. Or you could also just animate the constraint changes to mimic a transform.
Presumably you will get a viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) call back to manage the ongoing presentation if a rotation occurs.
I'm using a custom UIPresentationController to present a view modally. After presenting the view, the first textfield in the presented view becomes the first responder and the keyboard shows up. To ensure that the view is still visible, I move it up. However, when I do this the frameOfPresentedViewInContainerView is not matching the actual frame of the view anymore. Because of this, when I tap on the view it's being dismissed, because there's a tapGestureRecogziner on the backgroundView which is on top of the presentingView. How to notify the presentingController that the frame/position of the presentedView has changed?
In the UIPresentationController:
override var frameOfPresentedViewInContainerView: CGRect {
var frame = CGRect.zero
let safeAreaBottom = self.presentingViewController.view.safeAreaInsets.bottom
guard let height = presentedView?.frame.height else { return frame }
if let containerBounds = containerView?.bounds {
frame = CGRect(x: 0,
y: containerBounds.height - height - safeAreaBottom,
width: containerBounds.width,
height: height + safeAreaBottom)
}
return frame
}
override func presentationTransitionWillBegin() {
if let containerView = self.containerView, let coordinator = presentingViewController.transitionCoordinator {
containerView.addSubview(self.dimmedBackgroundView)
self.dimmedBackgroundView.backgroundColor = .black
self.dimmedBackgroundView.frame = containerView.bounds
self.dimmedBackgroundView.alpha = 0
coordinator.animate(alongsideTransition: { _ in
self.dimmedBackgroundView.alpha = 0.5
}, completion: nil)
}
}
Presenting the view modally:
let overlayVC = CreateEventViewController()
overlayVC.transitioningDelegate = self.transitioningDelegate
overlayVC.modalPresentationStyle = .custom
self.present(overlayVC, animated: true, completion: nil)
Animation when keyboard appears (in the presented view):
#objc func animateWithKeyboard(notification: NSNotification) {
let userInfo = notification.userInfo!
guard let keyboardHeight = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
return
}
// bottomContraint is the constraint that pins content to the bottom of the superview.
let moveUp = (notification.name == UIResponder.keyboardWillShowNotification)
bottomConstraint.constant = moveUp ? (keyboardHeight) : originalBottomValue
let options = UIView.AnimationOptions(rawValue: curve << 16)
UIView.animate(withDuration: duration, delay: 0,
options: options,
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
From the Apple documentation:
UIKit calls this method multiple times during the course of a
presentation, so your implementation should return the same frame
rectangle each time. Do not use this method to make changes to your
view hierarchy or perform other one-time tasks.
AFAIK, if you specify frame through this variable, it's advised not to change it throughout the course of presentation. If you plan to play around with the frames, don't specify this variable and handle all the changes manually in your animator
I add an image to my view by the following code if the count is zero and remove it otherwise:
var coverImageView = UIImageView()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if count == 0 {
let coverImage = UIImage(named: "AddFirstRecord")!
coverImageView = UIImageView(image: coverImage)
coverImageView.frame = CGRect(x: 20, y: 5, width: tableView.frame.width-20, height: 100)
view.addSubview(coverImageView)
} else {
DispatchQueue.main.async {
self.coverImageView.removeFromSuperview()
}
}
}
The problem is that it adds the image to the view, but removeFromSuperview does not work. (I made sure that it reaches to the else condition by debugging). I did the process in the main queue as well to be sure that the problem does not relate to threads. I wonder where is the origin of the issue?
In viewWillAppear the view still is not prepared completely to view. So removingFromSuperview does not have any effects. Instead, we should do the action inside viewDidLayoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if fetchedResultsController.fetchedObjects?.count == 0 {
let coverImage = UIImage(named: "AddFirstRecord")!
coverImageView.image = coverImage
coverImageView.frame = CGRect(x: 20, y: 5, width: tableView.frame.width-20, height: 100)
view.addSubview(coverImageView)
} else {
coverImageView.removeFromSuperview()
}
}
From Apple Documentation :
viewDidLayoutSubviews()
Called to notify the view controller that its
view has just laid out its subviews.
Your view controller can override this method to make changes after
the view lays out its subviews. The default implementation of this
method does nothing.
I'm trying to make a loading bar as I complete a long function can could take several seconds. However I'm not sure how I can figure out how far along in the functions execution I am and how to trigger the changes in the UI. Below is what I have which works ok, but I won't behave exactly as desired. Any suggestions?
func build(_ sender: UIButton) {
let myMutableString = NSMutableAttributedString(string: "Compiling Latest Clean Build", attributes: [NSFontAttributeName: buildLbl.font])
buildLbl.attributedText = myMutableString
let greenStrip = UIView()
greenStrip.backgroundColor = Colors().green
greenStrip.frame = CGRect(x: 0, y: buildStatus.frame.height - 2.5, width: 10, height: 2.5)
buildStatus.addSubview(greenStrip)
UIView.animate(withDuration: 2.0, animations: {
// compile can take a while
compile(structureContent: structuredContent)
greenStrip.frame = CGRect(x: 0, y: self.buildStatus.frame.height - 2.5, width: self.buildStatus.frame.width, height: 2.5)
}, completion: {
(value: Bool) in
greenStrip.removeFromSuperview()
let dvc : PreviewViewController = self.storyboard?.instantiateViewController(withIdentifier: "PreviewViewController") as! PreviewViewController
self.navigationController?.pushViewController(dvc, animated: false)
})
}
You want to keep track of progress made from your compile() function that is looping through a data structure and do stuff. First, you need a delegate function from your compile() function that reports the progress as percentage so that you can later display that on UI. You can find a complete tutorial about delegate here and your protocol will look something like
protocol ProgressDelegate: class {
func didFinishTask(progress: Double)
}
Now here is a sample way of reporting progress. At the beginning of your compile() function, you want to get a count from your data structure as total jobs. Then in each round of your for loop, you divide the current loop count by total count and then report this progress by the delegate. So your code will look something like
func compile(){
...
let totalJobs = jobs.count
let counter = 0.0
for eachJob in jobs {
counter += 1.0
...
didFinishTask(progress: counter/totalJobs)
}
}
Finally, in your UI view controller that receives this delegate. Update UI base one the percentage finished of your task.
func didFinishTask(progress: Double){
// set up status bar here
}