UIKitCore layoutGuides deadlock - swift

I am creating a popup menu. It has a UIPresentationController that calculates frameOfPresentedViewInContainerView based on presented view controller's view size before showing it.
Presented view controllers consist of the fixed height outer (navigation) view controller embedding some dynamic height inner (content) view controller;
Inner view controllers, under the hood, have UIStackView wrapped in a UIScrollView;
Before calculating size of inner view controller I am calling layoutIfNeeded() on it.
The problem occurred only on devices with the notch (I blame safeAreaLayout) and only with a UIStackView-based inner view controllers. When layoutIfNeeded() called on presented controller (e.x. when display orientation change, content size change, or presented second time) UIKitCore goes into an infinite loop calling -[UIView layoutGuides]. It doesn't crash the app, but use 100% of the main thread and freezes the UI(sometimes whole phone to the point you need make a hard reset), consuming about 10Mb of memory every second.
I was able to fix it by adding 1 extra point to a calculated height of the frameOfPresentedViewInContainerView. This sounds like an awful fix, so I am trying to better understand the problem.
I would be glad if someone with a deep understanding of UIKit could point me to a better strategy on how to debug/investigate the issue.
UPDATE
Seems like UIScrollView having hard time positioning content due to a safeArea. UIKitCore keeps repeating those 5 lines:
- [UIScrollView _layoutGuideOfType:createIfNecessary:]
- [NSISEngine(_UILayoutEngineStatistics)_UIKitPerformPendingChangeNotifications]
- [UIView layoutGuides]
- [_UIScrollViewScrollIndicator _layoutFillViewAnimated:]
- [UIView(AdditionalLayoutSupport) nsli_lowerAttribute:intoExpression:withCoefficient:forConstraint:onBehalfOfLayoutGuide:]
I also have
runtime: Layout Issues: Scrollable content size is ambiguous for UIScrollView.

I was able to fix my issue by specifically attaching UIScrollView to the bottom of safeAreaLayoutGuide.
var bottomConstraint = formView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
if #available(iOS 11.0, *) {
bottomConstraint = formView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
}

Related

insertSubVIew taking a "long" time

In my container controller, the user can pan the views to switch to different views. When the pan gesture begins, it's add the view of the new view controller to the view with: view.insertSubView(view:, atIndex:)
After researching a bit, I noticed this step takes about 0.03 sec (while the other things are all 0.001-0.002 sec). This causes the transition to stotter a bit which is kind of annoying.
The view controller is created at the beginning of the app as a global, using the storyboard.
Also, this only happens when the view is loaded for the first time. The transitions are all fluently after.
What can I do to preload the views so it won't take so "long" when its loaded for the first time?
EDIT:
SURROUNDING CONTEXT:
var pendingViewController: UIViewController! {
didSet {
if let pending = pendingViewController {
addChildViewController(pending)
let index = view.subviews.count - 1
NSLog("start insert view test")
view.insertSubview(pending.view, atIndex: index)
NSLog("end insert view test")
pending.didMoveToParentViewController(self)
}
}
}
Because it only happens when the view is loaded for the first time I was thinking the problem could be somewhere with the viewDidLoad and viewWillAppear. The results are shown below. Only viewDidLoad took a small amount of time (0.005 seconds). There's a gap of 0.02 sec before getting to viewDidLoad though, but I have no idea what it could be.
2015-12-17 15:15:57.116 Valor[777:232799] start insertView view test
2015-12-17 15:15:57.136 Valor[777:232799] start viewDidLoad test
2015-12-17 15:15:57.141 Valor[777:232799] end viewDidLoad test
2015-12-17 15:15:57.142 Valor[777:232799] start viewWillAppear test
2015-12-17 15:15:57.144 Valor[777:232799] end viewWillAppear test
2015-12-17 15:15:57.146 Valor[777:232799] end insertView view test
Use instruments to find out where slow code is happening, not log statements with timestamps. This will show you (including system calls) exactly where the time is being spent.
Inserting subviews can be slow because of layout. However, your trace (such as it is) suggests the time is being spent creating and loading the view of the view controller. You say this view comes from a storyboard. What is in there? How many other things get triggered when this view loads? Use the time profiler and you will be able to tell. It could be something as simple as a property you're giving a default value to that could instead be a lazy value.
The view controller is created at the beginning of the app as a global, using the storyboard.
If this is the case then you can force loading of the view controller's view by doing something like
let hack = viewcontroller.view
Accessing the view property of the view controller causes it to load up the view from the storyboard.
At first sight, I see one strange thing: the index for inserting pending.view. If the view count on your view is 5 for example, that means that the indices of its subviews vary from 0 to 4. And then, you want to put the new subview at index 4 (= 5 - 1) while there's one already on your view which has index = 4. AFAICS, you meant 5 instead of 4. So, you should ether stick to let index = view.subviews.count and then insert the new view at that index, or just use view.addSubview(pending.view). I hope this will help you out.

How to control scrollview overdraw with NSView's prepareContentInRect?

I am trying to understand its usage better in order to improve scrolling performance on OSX. More specifically, I am trying to eradicate the "flashing/flickering" on the side of the screen as you scroll fast and AppKit runs into non-overdrawn (non-prepared) portions of your view OR portions of your view that have been prepared incorrectly with background-only.
My understanding is that the preparedContentRect represents the overdrawn section of your scrollview - the area available for responsive scrolling.
Part One
I placed code within the preparedContentInRect for two subViews of NSScrollView - each are floating subViews of NSScrollView which behave the same as documentViews when scrolling.
The preparedContentInRect gets called several times just after program launch (as expected). I then scroll to the end of the prepared area BUT (unexpectedly) it doesn't get called again as I get nearer and nearer and even on the edge of the prepared area. Why not? Even with no user interaction with the UI (system idle) it does not call preparedContentInRect to enlarge the overdraw area dynamically.
Part Two
Since (as above) I could not rely on AppKit calling preparedContentInRect when needed; I reorganised my code to prepare the preparedContentRect after each notification from the clipview's boundsChangeNotification and then call prepareContentInRect manually to inform AppKit of the new overdraw area.
I have included a "manual" flag so that my custom override can differentiate between my manual calls to prepareContentInRect and internal calls from AppKit - so as not to let AppKit mess up the overdraw!!!
var manuallyPrepareContentInRect = false
override func prepareContentInRect(rect: NSRect) {
if manuallyPrepareContentInRect{
println("GRID manual prepareContentInRect call: \(rect)")
manuallyPrepareContentInRect = false
super.prepareContentInRect(rect)
return
}
println("GRID bypass AppKit prepareContentInRect call: \(self.preparedContentRect)")
super.prepareContentInRect(self.preparedContentRect)
}
Unfortunately, even though prepareContentRect is reporting my intended prepare rect, when it scrolls it does not seem to have prepared the rect as I have intended - it prepares a rect 3,500 points wide instead of 5,040 wide and hence I run into the dreaded background color while waiting for the rest of the view to render.
The only "special" thing about the view that isn't getting correct overdraw is that it has a whole lot of subviews that have been drawn into its layer using canDrawSubviewsIntoLayer.
What am I misunderstanding above with respect to prepareContentInRect and overdraw? Why is AppKit behaving like it is?

How to tell UICollectionView to preload a larger range of cells?

I have a UICollectionView which shows images retrieved from the web. They are downloaded asynchronous.
When user scrolls fast, they see placeholders until the cell loads. It seems UICollectionView only loads what is visible.
Is there a way to say "collection view, load 20 cells more above and below" so chance is higher that it loaded more cells while user was looking at content without scrolling?
The idea is to have the VC recognize when a remote load might be required and start it. The only tricky part is keeping the right state so you don't trigger too much.
Let's say your collection is vertical, the condition you want to know about is when:
BOOL topLoad = scrollView.contentOffset.y < M * scrollView.bounds.size.height
or when
BOOL bottomLoad = scrollView.contentOffset.y > scrollView.contentSize.height - M * scrollView.bounds.size.height
in other words, when we are M "pages" from the edge of the content. In practice though, this condition will be over-triggered, like when you're first loading, or if you're testing it on scrollViewDidScroll, you don't want to generate web requests for every pixel of user scrolling.
Getting it right, therefore, requires additional state in the view controller. The vc can have a pair of BOOLs, like topLoadEnabled, bottomLoadEnabled, that are NO until the view is ready. Then, scroll delegate code looks like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// compute topLoad and bottomLoad conditions
if (topLoad && self.topLoadEnabled) [self startTopLoad];
similarly for bottom. The load code looks like this, abstractly:
self.topLoadEnabled = NO; // don't trigger more loading until we're done
[self.model getMoreTopStuff:^(NSArray *newStuff, NSError *error) {
// do view inserts, e.g. tableView.beginUpdates
self.topLoadEnabled = YES;
}];
Same idea for bottom. We expect the model to fetch for itself (maybe the model has image urls) and cache the result (then the model has images). As the datasource for the view, the view controller gets called upon to configure view cells. I can just naively ask the model for images. The model should answer either fetched images or placeholders.
Hope that makes sense.
In my opinion you are making the wrong assumption: cells are just views so you shouldn't treat them as model objects. UICollectionView and UITableView are very efficient because they constantly recycle cells so you should think in therms of pre loading content in the business side of things. Create interactor or viewmodel objects and populate your data source with those, then you'll be able to ask those objects to preload images, if you still wish to do so.
A BOOL flag seldom is the answer. I'd rather go for estimating a reasonable page size and fetching images as needed from the cellForItemAtIndePath method.

UIView Content Clipping Not Working/UIView Resize Not Working

I ran into an issue where clipping of subviews is not working consistently (or potentially, the UIView resize isn't working). Here's the scenario:
I have a UIView (section above the line). The actual size is larger to accommodate search bar. I basically resize it on viewDidLoad.
When clicking on the text field, UIView expands and show a search bar
After search, I collapse the UIView, but found that the UIView is not collapsed in certain case (see first image). If I were to not hide the search bar, it will remain also.
Is there any reason why this issue occurs? I'm still trying to debug and see if there is anything that could have caused this issue (as something may reset to original size). It definitely does seem like everything is resized correctly since even the result table is moved correctly also. Something else must have triggered it after the expected resize. Any pointer is appreciated. I've only done iOS development for 5 days so I'm still not aware of a lot of things.
- (void)showAddressField
{
CGRect headerFrame = self.searchHeaderView.frame;
headerFrame.size.height = headerHeight + adjustmentSize;
self.searchHeaderView.frame = headerFrame;
CGRect tableFrame = resultTableView.frame;
tableFrame.size.height = tableHeight - adjustmentSize;
tableFrame.origin.y = headerFrame.size.height + statusBarHeight;
resultTableView.frame = tableFrame;
[self renderHeaderBorder:headerFrame.size.height - 1];
}
- (void)hideAddressField
{
CGRect headerFrame = self.searchHeaderView.frame;
headerFrame.size.height = headerHeight;
self.searchHeaderView.frame = headerFrame;
CGRect tableFrame = resultTableView.frame;
tableFrame.size.height = tableHeight;
tableFrame.origin.y = headerHeight + statusBarHeight;
resultTableView.frame = tableFrame;
[self renderHeaderBorder:headerHeight - 1];
}
EDITED
SearchHeaderView is just a UIView that is a subview of the main view. It's not a table header. I found that if I put a search bar in the table header, it behaves very unpredictably so I have a UIView containing the search portion and have a UITableView right below it.
Sorry, since I only have just over a week to get a rather massive app out from scratch, I didn't have time to wait. I already changed my approach a little bit, but I will still award the points even after the bounty has expired. I'm trying to understand everything to do with layout since that's pretty much the only thing that I can't quite figure out with iOS app development.
If the search bar is in the table view header, try reassigning the header view to the table view:
[tableView setTableHeaderView:self.searchHeaderView];
If it is a section header, be sure to update the value for the delegate's
– tableView:heightForHeaderInSection:
– tableView:viewForHeaderInSection:
Otherwise, please post more code on how the searchHeaderView is initialized and added to the table.
Hope this helps!
Firstly, frames are already relative to the enclosing window. You should not be taking into account the height of the status bar (assuming that refers to the 20px area at the top of the screen).
I have found in the past that you may need to implement "layoutSubviews" in your view controller and compute the frames of the views there. It depends on your resizing mask / if you auto-layout enabled.
As the others have stated, more code would be helpful... or at least the relevant portions of the nib/xib.

UIScrollView mysteriously changing frame and content size

I have a scroll view in my app, initialized through storyboard, and I am attempting to change the frame. Everything seems to work, but if the frame and content size are accessed just a short time after they are set, but after my method returns, they are different! Here is the code:
CGFloat inputHeight = inputView.frame.size.height;
originalContentHeight = self.scrollableContentView.frame.size.height;
NSLog(#"%f", self.scrollableContentView.frame.size.height-inputHeight);
[self.scrollableContentView setFrame:CGRectMake(self.scrollableContentView.frame.origin.x, self.scrollableContentView.frame.origin.y, self.scrollableContentView.frame.size.width, self.scrollableContentView.frame.size.height-inputHeight)];
NSLog(#"%f", self.scrollableContentView.frame.size.height);
NSLog(#"%#", self.scrollableContentView);
The output of these logs are all as expected, that is:
<UIScrollView: 0x808de00; frame = (0 0; 320 81); ... >
However, if I set a breakpoint in another method, by the time that is called, logging the scrollView shows this:
<UIScrollView: 0x808de00; frame = (0 0; 320 416); ... >
Additionally, the content size changes as such:
(width=320, height=416)
to
(width=320, height=504)
Both of the values seem to be reverting automatically to the values they have when the view is first laid out. I have set breakpoints in the getter and setter of my scroll view, and cannot find any unexpected accesses, so I have concluded that the change must come internally. Is this a justifiable conclusion? What could be going wrong?
If you're using autolayout you should be changing the constraints, not setting the scrollview's frame.
See my answer to a similar question here. It has some links to examples of how to set constraints programmatically.
Also, in which method are you setting the frame and contentSize? Since you are using a storyboard, if it is in viewDidLoad or viewWillAppear: it would be better to move this code to viewDidLayoutSubviews instead.