Swift: dispatch_async is effect is inconsistent for animation - ios

I have a globalstate in my app. Depending on the state the GUI is different.
When I go from the start View A to View B I have globalstate 3
It should show an information screen, but it doesn't. BUT: When the View B has loaded only once and I jump from View C/D/E back to View B, then the code work perfectly. (You have to be in View A to get in View B.)
I use a lot dispatch_async(dispatch_get_main_queue.. that isn't good style, is it?
Why is my animation not loading at the beginning? What is good style? Thank you for answers and sorry for mistakes (english isn't my mothertongue)
override func viewDidLoad() {
super.viewDidLoad()
dispatch_async(dispatch_get_main_queue(), {
self.animateTheInformationViewWhenGlobalStateIsThree()
})
}
func animateTheInformationViewWhenGlobalStateIsThree() {
print("GLOGBALSTATE \(globalState)") //it is 3
if globalState == 3 {
setGlobalState(3)
dispatch_async(dispatch_get_main_queue(), {
GUITools.animateTheInformationView(self.tableView, animateBottomLayout: self.animationBottomConstraint, value: self.negativValue)
})
print("THE POSITIV VALUE THE NEGATIV")
}
//GUITools-Static-Class:
class func animateTheInformationView(tableView: UITableView, animateBottomLayout: NSLayoutConstraint, value: CGFloat) {
dispatch_async(dispatch_get_main_queue(), {
animateBottomLayout.constant += value
UIView.animateWithDuration(Constants.animationTime, animations: { () -> Void in
tableView.layoutIfNeeded()
},completion: {
(value: Bool) in
})
})
}
EDIT
With viewDidAppear it works. But the animation isn't a real animation. The tableView "jumps". So there is no sliding/animation.
I deleted all dispatch_async..
override func viewDidAppear(animated: Bool) {
self.animateTheInformationViewWhenGlobalStateIsSeven()
}

viewDidLoad() does not mean that your view is already visible. Since it's not visible yet you cannot apply animations to it.
viewDidLoad() is only meant to configure your view controller's view and set up your view hierarchy - i.e. to add subviews.
What you want to use is viewWillAppear() (or viewDidAppear()) to start your animation as soon as the view becomes (or became) visible.
Also all the dispatch_async calls are most likely unnecessary. You usually only need them when you are not on the main (= UI) thread. Simply remove them.

Related

When using scrollView.zoom I get different results from pressing a button to calling a function

I am trying to zoom to a button on a UIScrollView. The first function is one that I run if I segue to the vc, the second is run from a button in the view.
When running the second function it goes perfectly and centres correctly
On the first however, despite being identical code, it has an offset in the x of about 40-50 pixels (Shows a point left of the button, not any part of it) although this offset changes dependent on the button.
func moveScrollviewToRoom(){
scrollView.zoom(to: artButton.frame, animated: true)
}
#IBAction func moveScrollview(_ sender: Any) {
scrollView.zoom(to: artButton.frame, animated: true)
}
The function was called here:
override func viewDidLoad() {
super.viewDidLoad()
if moveToRoom != "" {
moveScrollviewToRoom()
}
Any help would be greatly appreciated
Cheers!
Two points...
1 - Views and subviews have not yet been positioned by auto-layout in viewDidLoad(), so the artButton.frame will not be correct.
2 - Calling any function that animates a view from viewDidLoad() won't give the desired results, because the view is not even visible yet.
Move your call to viewDidAppear():
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if moveToRoom != "" {
moveScrollviewToRoom()
}
}

UITableViewAlertForLayoutOutsideViewHierarchy error: Warning once only (iOS 13 GM)

I am getting a strange error with iOS13 when performing a Segue and I can't figure out what it means, nor can I find any documentation for this error. The problem is that this seems to cause a lot of lag (a few seconds) until the segue is performed.
2019-09-11 22:45:38.861982+0100 Thrive[2324:414597] [TableView] Warning once only: UITableView was told to layout its visible cells
and other contents without being in the view hierarchy (the table view
or one of its superviews has not been added to a window). This may
cause bugs by forcing views inside the table view to load and perform
layout without accurate information (e.g. table view bounds, trait
collection, layout margins, safe area insets, etc), and will also
cause unnecessary performance overhead due to extra layout passes.
Make a symbolic breakpoint at
UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the
debugger and see what caused this to occur, so you can avoid this
action altogether if possible, or defer it until the table view has
been added to a window. Table view: ; layer = ; contentOffset: {0, 0}; contentSize: {315, 118};
adjustedContentInset: {0, 0, 0, 0}; dataSource: >
I am using Hero but I tried disabling it and using a regular Segue and this hasn't stopped the lag.
The code to initiate the segue is didSelectRowAt
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.section == 0 {
selectedCell = realIndexFor(activeGoalAt: indexPath)
performSegue(withIdentifier: "toGoalDetails", sender: nil)
} else if indexPath.section == 1 {
selectedCell = indexPath.row
performSegue(withIdentifier: "toIdeaDetails", sender: nil)
} else {
selectedDecision = indexPath.row
hero(destination: "DecisionDetails", type: .zoom)
}
}
And then none of the code in viewDidLoad or viewWillAppear from the destination VC affects this in any way (I tried commenting it all out with no difference.
Any idea what's causing this? I can share whatever other details are needed.
Thank you.
It happened to me because I registered the device for change orientation notification in the viewWillAppear(:) method.
I moved the registration in the viewDidAppear(:) and Xcode it's not stopping at the breakpoint anymore.
What I can say is that layout changes might be run when the view is already visible...
For people using DiffableDataSource, set animatingDifferences to false and warning will be gone.
dataSource.apply(snapshot, animatingDifferences: false)
Like #joe-h, I was getting this error and was also surprised as the unwind approach he shows is one used by lots of developers + is in some significant Apple iOS sample code.
The triggering line in my code (#joe-h, I'm guessing likely in yours, too) is a tableView.reloadRows at the selectedIndexPath (which is an unwrapped tableView.indexPathForSelectedRow):
tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
Unfortunately commenting out the row isn't an option if you are unwinding after updating the value in an existing tableView row (which is an approach in the Apple FoodTracker tutorial mentioned above, as well as one used in Apple's Everyone Can Code series). If you don't reload the row(s) then your change won't show in the tableView. After commenting out the reload in the unwind, I added a viewDidAppear with the following code and this seems to fix things:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
}
}
I'd welcome comments on whether this is a sound approach, but for now, this seems to be working.
I had the same error on my Project; A tableView with a diffable datasource. Been bugging on it for hours. Problem lies in updating the snapshot, more specifically on a background thread (default). Forcing the update of the datasource on the main thread got rid of the problem! Hope this helps someone out there!
func updateData(on annotations: [Annotation]) {
var snapshot = NSDiffableDataSourceSnapshot<AnnotationType, Annotation>()
//Append available sections
AnnotationType.allCases.forEach { snapshot.appendSections([$0]) }
//Append annotations to their corresponding sections
annotations.forEach { (annotation) in
snapshot.appendItems([annotation], toSection: annotation.type as AnnotationType)
}
//Force the update on the main thread to silence a warning about tableview not being in the hierarchy!
DispatchQueue.main.async {
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
This warning can happen du to updating table view or collection view while it is not visible, for example when it is on the parent view controller. To solve that, first, I created a property in the view controller, containing the table view to check if the view controller is visible or not, as bellow:
var isVisible: Bool = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.isVisible = true
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidAppear(animated)
self.isVisible = false
}
Then in the data source delegate, before reacting to changes, first check if the view controller is visible. If it was not, do not do any updates. For example
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard isVisible else { return }
tableView.beginUpdates()
}
You should check that visibility before doing any changes in the tableView. For example, in case of NSFetchedResultsController, it must be done in all delegate callbacks which we have implemented.
UPDATE
I recently found that if you update the table view with animation false, even when it is not visible, there won't be any warnings.
I'm new to Xcode/Swift so this may or may not help anyone. I started getting this error after updating to iOS 13 and Xcode 11 within the app when going back to a list from a detail view.
I found that I was doing a tableView.reloadRows and tableView.insertRows in the unwind(as suggested by Apple in one of their tutorials)
#IBAction func unwindToMealList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? MealViewController, let meal = sourceViewController.meal {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing meal.
meals[selectedIndexPath.row] = meal
tableView.reloadRows(at: [selectedIndexPath], with: .none)
}
else {
// Add a new meal.
let newIndexPath = IndexPath(row: meals.count, section: 0)
meals.append(meal)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
}
)
I commented out that section of code and it went away.
Oddly enough leaving the sort and self.tableView.reloadData() didn't give me the error.
In viewDidDisappear method I declare tableView.setContentOffset(CGPoint(x: 0, y: 0), animated: false) function. Some of you says it's not important but it affected tableView delegate methods. For example viewForHeader function is not called when I get this warning.
I found the most robust and safe way is to wait for the didMoveToWindow of the table view / collection view
as even in viewWillAppear the view may NOT be attached to a window and puting your code in viewDidAppear may cause unwanted graphical glitches
class MyTableViewOrCollectionView: UITableView {
var didMoveToWindowCallback: (()->())? = nil
override func didMoveToWindow() {
super.didMoveToWindow()
didMoveToWindowCallback?()
didMoveToWindowCallback = nil
}
}
and than you can
override func viewDidLoad() {
super.viewDidLoad()
tableView.didMoveToWindowCallback = { [weak self] in
self?.setupInitialContent()
}
}
iPadOS 13.2.3 swift 5.2 Xcode 11.2.1
Just ran into this issue only when starting the app while the device was landscape.
I was calling the detail seque in the viewDidLoad func of the master controller to make sure the detail view was setup correctly.
override func viewDidLoad() {
super.viewDidLoad()
...
self.performSegue(withIdentifier: "showDetail", sender: self)
}
When I removed the performSeque the warning not longer appeared, however,
the left bar buttons on the detail controller no longer worked properly, again only when starting the app while the device was landscape. The left most button would activate the next button to the right instead of what the first button was suppose to do.
The fix for the bar buttons was to add to the viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
...
self.splitViewController?.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
}
Then execute
override func viewWillAppear(_ animated: Bool) {
self.splitViewController?.preferredDisplayMode = UISplitViewController.DisplayMode.automatic
super.viewWillAppear(animated)
}
I have no explanation why this worked!
This app had worked flawlessly until iPados 13 was loaded.
I am getting similar breakpoint with SwiftUI, without even dealing with viewDidLoad or viewDidappear
//
// ContentView.swift
// DD
//
// Created by Roman Emperor on 3/29/20.
// Copyright © 2020 Emperors. All rights reserved.
//
import Combine
import SwiftUI
// Defining a class Booking of type Bindable Object [changed to ObservableObject]
class Booking: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
// Array of types to work with
static let types = ["Consultation", "Tooth Pain", "Cleaning", "Brases", "Dental Implant" ]
// Setting instance varibale type
var type = 0 { didSet { update() } }
func update () {
didChange.send(())
}
}
struct ContentView: View {
#ObservedObject var booking = Booking() //bindableObject in old swift version
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $booking.type, label: Text("Select a Booking Type")) {
ForEach(0 ..< Booking.types.count){
Text(Booking.types[$0]).tag($0)
}
}
}
}
.navigationBarTitle(Text("Darpan Dental Home"))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The Complete output Log is here:
*> 2020-03-29 09:22:09.626082+0545 DD[1840:76404] [TableView] Warning
once only: UITableView was told to layout its visible cells and other
contents without being in the view hierarchy (the table view or one of
its superviews has not been added to a window). This may cause bugs by
forcing views inside the table view to load and perform layout without
accurate information (e.g. table view bounds, trait collection, layout
margins, safe area insets, etc), and will also cause unnecessary
performance overhead due to extra layout passes. Make a symbolic
breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch
this in the debugger and see what caused this to occur, so you can
avoid this action altogether if possible, or defer it until the table
view has been added to a window.*
**where is this UITableViewAlertForLayoutOutsideViewHierarchy in SwiftUI ? **
extension UIView {
func rootView() -> UIView {
var view = self
while view.superview.isNotNil {
view = view.superview!
}
return view
}
var isOnWindow: Bool {
return self.rootView() is UIWindow
}
}
then you just need to check if your tableView isOnWindow like...
if self.tableView.isOnWindow {
/// do stuff
}
Disclaimer: as the documentation explains, you may need to defer the call which means that there is no warranty your method will be called again so it's your responsibility to perform your update when isOnWindow is true.
Had the same issue, removing tableView.reloadSections fixed it. This was the line of code causing the warning:
iOS 13:
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
in iOS 14, removing tableView.reloadSections did not fix the warning.
Or maybe your code (like mine) has nothing wrong with it and this message just randomly starts popping up. In that case, do a clean on your project, restart Xcode and watch the message magically go away!
Please check following function
override func viewWillLayoutSubviews()
For anyone that has this issue with a UISplitViewController and a UITableView inside the detail view controller, you can try subclassing and override layoutSubviews like this (From this thread):
class CustomTableView: UITableView {
override func layoutSubviews() {
if (self.window == nil) {
return
}
super.layoutSubviews()
}
}
Instead of reloading the rows inside viewDidAppear, this is what it worked for me:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.tableView.reloadRows(at: [indexPath], with: .none)
}
Also if you are using DiffableDataSource and you are selecting an indexPath manually for example, you need to do it on the completion block of the apply snapshot method:
dataSource.apply(snapshot, to: section, animatingDifferences: false, completion: {
// select the indexPath programmatically or do UITableView UI stuff here.
})
... the table view or one of its superviews has not been added to a window ...
To resolve the issue we need to check tableView.window property:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
updateTableViewRows()
}
func dataChanged() {
updateTableViewRows()
}
func updateTableViewRows() {
if tableView.window == nil {
// TODO: just update data source
} else {
tableView.performBatchUpdates {
// TODO: update data source
// TODO: update table view cells
}
}
}
}
The idea is to not call performBatchUpdates and related functions while tableView.window is nil.

Swift animations in a new view happen instantly

One my primary view, animations execute according to the delay and durations set. However, when I segue to a new view, the animations are all complete instantly. How is this? This only happens when this animations told to execute from code in the viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
setProgress()
}
// the function below annimates a circular progress view
func setProgress() {
var to:Double = ((360.0 / 4) * increment)
progressCircle.animateFromAngle(360.0, toAngle: to, duration: 5) { completed in
if completed {
print("animation stopped, completed")
} else {
print("animation stopped, was interrupted")
}
}
}
viewDidLoad occurs before the view is presented on screen, if you want to run animations you should probably run them in viewDidAppear so that the view is loaded on screen and the components are visible.
call setProgress() in viewDiDAppear
override public func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
setProgress()
}
Maybe you can add delay and keep it running on a timer after viewdidload event of controller. Therefore you may reach your goal and run the animations in the time that you want.

Animating changes to presentingViewController during interactive dismissal

I'm working on a UIPresentationController subclass similar to Mail.app's open draft behavior. When a view controller is presented, it doesn't go all the way to the top and the presenting view controller scales down as if it were falling back.
The basic gist of it is below:
class CustomPresentationController : UIPresentationController {
// Create a 40pt space above the view.
override func frameOfPresentedViewInContainerView() -> CGRect {
let frame = super.frameOfPresentedViewInContainerView()
let insets = UIEdgeInsets(top: 40, left: 0, bottom: 0, right: 0)
return UIEdgeInsetsInsetRect(frame, insets)
}
// Scale down when expanded is true, otherwise identity.
private func setScale(expanded expanded: Bool) {
if expanded {
let fromMeasurement = presentingViewController.view.bounds.width
let fromScale = (fromMeasurement - 30) / fromMeasurement
presentingViewController.view.transform = CGAffineTransformMakeScale(fromScale, fromScale)
} else {
presentingViewController.view.transform = CGAffineTransformIdentity
}
}
// Scale down alongside the presentation.
override func presentationTransitionWillBegin() {
presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ context in
self.setScale(expanded: true)
}, completion: { context in
self.setScale(expanded: !context.isCancelled())
})
}
// Scale up alongside the dismissal.
override func dismissalTransitionWillBegin() {
presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ context in
self.setScale(expanded: false)
}, completion: { context in
self.setScale(expanded: context.isCancelled())
})
}
// Fix the scaled view's frame on orientation change.
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
guard let bounds = containerView?.bounds else { return }
presentingViewController.view.bounds = bounds
}
}
This works fine for a non-interactive presentation or dismissal. When performing an interactive dismissal, however, all animations on presentingViewController.view run non-interactively. That is, the scaling will happen in the ~300ms it normally takes to dismiss rather than staying at 3% complete when 3% dismissed.
You can see this in a sample project is available on GitHub. and a video of the issue is on YouTube.
I've tried the following approaches but they all yield the same result:
A parallel animation as seen above.
Animating in the UIViewControllerAnimatedTransitioning.
Using a CABasicAnimation an manually adjusting the timing of the container view's layer.
The problem was that presentingViewController is not a descendent of the presentation's containerView. UIPercentDrivenInteractiveTransition works by setting containerView.layer.speed to zero and setting containerView.layer.timeOffset to reflect the percent complete. Since the view in question was not part of the hierarchy, its speed stayed at 1 and it completed as normal.
This is explicitly stated in the documentation for animateAlongsideTransition(_:,completion:):
Use this method to perform animations that are not handled by the animator objects themselves. All of the animations you specify must occur inside the animation context’s container view (or one of its descendants). Use the containerView property of the context object to get the container view. To perform animations in a view that does not descend from the container view, use the animateAlongsideTransitionInView:animation:completion: method instead.
As the documentation indicates, switching to animateAlongsideTransitionInView(_:,animation:,completion:) fixes the problem:
// Scale up alongside the dismissal.
override func dismissalTransitionWillBegin() {
presentingViewController.transitionCoordinator()?.animateAlongsideTransitionInView(presentingViewController.view, animation: { context in
self.setScale(expanded: false)
}, completion: { context in
self.setScale(expanded: context.isCancelled())
})
}
The comment on that method in the header is a lot more direct about this than the documentation:
// This alternative API is needed if the view is not a descendent of the container view AND you require this animation to be driven by a UIPercentDrivenInteractiveTransition interaction controller.
Thank you for the answer, had this issue on iOS 9.x and 10.x devices - using animateAlongsideTransition(in:animation:completion:) did the job.
Interestingly, on iOS 11.x animate(alongsideTransition:completion:) works correctly for presentingViewController too (there is no need to use animateAlongsideTransition(in:animation:completion:)) - looks like Apple lifted something under the hood in the latest iOS.

viewDidAppear & viewDidLoad Are Are Initializing Positions Too Late

I am looking to update a UIView thats in a storyboard (and instantiated from the storyboard) when it loads in the app.
I need to position a few icons in a dynamic way (that the interface builder doesn't let me do quite yet). However, if I put my code in viewDidAppear or viewDidLoad, it seems to be getting called too late.
Here is what happens the first few second or two when the view loads:
And then a bit later it goes to the right position (as the code was called).
My question is where do I need to initialize the positions of these objects so they dont snap over a second later. viewDidLoad and viewDidAppear are too late? Why!? :)
override func viewDidAppear(animated: Bool)
{
initView()
_gridLines = false
}
func initView()
{
_cameraFeed.initAfterLoad()
//center the buttons on half distance between middle button and screen edge
var middleDistance:CGFloat = _swapButton.frame.origin.x + _swapButton.frame.width/2
_linesButton.frame.origin.x = middleDistance/2 - _linesButton.frame.width/2
_flashButton.frame.origin.x = middleDistance + middleDistance/2 - _flashButton.frame.width/2
_selectPhotos.frame.origin.x = middleDistance/2 - _selectPhotos.frame.width/2
}
Swift and objc answers welcome!
Try putting the code setting the frame in viewWillAppear instead of viewDidAppear.
Also, you should call the super.
override func viewWillAppear(animated: Bool) {
initView()
_gridLines = false
super.viewWillAppear(animated)
}

Resources