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.
Related
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)
}
I have a strange problem: in my table view I want to update a single cell when its UISlider gets modified (value changed control event). At the moment the value of the slider changes, I want to animate a button into the cell (sliding in from the right). I set the constant of the button's constraint to -50 to make sure it's not visible by default, and when the slider's value gets changed, a method gets called which updates the table view, so cellForRowAtIndexPath gets called again for all cells in my table. Simplified it looks something like this:
func valueChanged(sender:UISlider) {
// Reload table view
self.myTableView.reloadData()
// Let table view know the value of the slider got modified
self.didChangeValueOfSlider = true
}
And in cellForRowAtIndexPath I'm keeping track of which cell's slider got changed by using a custom selectedCellIndexPath variable. When the table view gets to the cell that got modified: it runs the following code:
// Check if cell is selected
if indexPath == self.selectedCellIndexPath {
// Check if value of slider was changed
if self.didChangeValueOfSlider == true {
// Value was changed: set constraint back to default
saveButtonTrailingConstraint.constant = CGFloat(15)
self.view.setNeedsUpdateConstraints()
self.view.updateConstraintsIfNeeded()
// Reset slider update status
self.didChangeValueOfSlider = false
}
}
Calling those setNeedsUpdateConstraints() and updateConstraintsIfNeeded() might be overkill or unnecessary, but please note this is my 15th attempt or so to actually get the view to display the updated layout. I used breakpoints to confirm the constant actually changes, after the code above is finished running and everything works perfectly fine. The only thing I can't get working is the updating part. I've tried UIView.animateWithDuration to animate the change, and I've tried methods like layoutIfNeeded() and beginUpdates() and endUpdates of the table view: nothing works. What could be the reason the layout doesn't get updated? And what am I supposed to be calling the layout and update methods on? I've been calling them on self.view but I'm not even sure if you're supposed to be calling it on the view if you're trying to update the layout of a table view cell. Any help would be appreciated.
I figured it out. The problem turned out to be the constraint itself. as I was accessing the wrong one. I defined it as saveButtonTrailingConstraint but I was actually modifying was the saveButton's width constraint (which apparently can't be animated). Instead of accessing saveButton.constraints[indexOfTrailingConstraint] I should have defined saveButton.superview!.constraints[indexOfTrailingConstraint] as the constraint belongs to the superview and not to the button itself. I guess the best thing to take away from this clumsy mistake is to always double-check if you're modifying the correct constraint if you're doing it programmatically, because it doesn't always show on the first eye.
a have subclassed a SKScene and implemented didMoveToView which did nothing at the moment. Empty function body. Let's name the class TestScene for a moment.
I also have a WindowController and a var named testScene:
var testScene: TestScene?
To functions are implemented showand hide
The show function looks like this:
func show(){
testScene = TestScene(fileNamed:"TestScene")
}
fileNamed:"TestScene" is of course the .sks file with a animation.
The hide function looks like this:
func hide(){
if let scene = testScene {
scene.paused = true
}
testScene?.removeFromParent()
testScene = nil
self.close()
}
This SpriteKit scene does appear on circumstances (button click) in my application. And after the execution is disappears.
Everything is working fine so far.
If I'll press the button the animation comes into the view and close after hide() is triggered, BUT: Since the button is pressed and
testScene = TestScene(fileNamed:"TestScene")
is executed, i got 0.5% CPU load. Even after hide. I can in commend the TestScene() and the CPU falls down to 0% after a little peak.
( i do some more stuff in that buttonPressed: function). So I am pretty sure that the TestScene is not removed after hide is called and maybe do some crazy shit in the background.
The Problem is, that the whole FinalScene will produce 5% of CPU and after the first appearance this load won't go away. :(
My Question is: How to remove the SKScene from the execution loop?
Thanks a lot for any advice, i debug this since hours by now and can't figure out how to dismiss the ended animation.
ps.
The solution is to explizit remove the SKView-subview:
animationView.removeFromSuperview()
This means that we have to build the view from the xib on every event, because the view is lost. The other possibility is to add the SKView programatically on show().
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.
I am using storyboards for the 1st time, and I cant figure out what I am doing wrong here ... I have a button that transitions from one view controller to another using StoryBoards (the 2nd view is presented modally).
I am trying to use the "prepare for segue" in order to pass the value of a text field from view 1 to view 2, but it is not working. Can somebody tell me what I have wrong here ... ?
View 1:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if ([[segue identifier] isEqualToString:#"saveGame"]) {
statsViewController *svc = (statsViewController *)[segue destinationViewController];
[svc setStatsTextField:gameTextField];
}
}
If there is other code I can post to clarify please let me know.
(for the record there are no errors, the text field on view 2 just doesn't update.)
You cannot just assign a text field in one view controller to a property in another one. This achieves nothing for the text field that is actually in the second view controller's view. Instead, you have to assign a value to the text field's text property. (And ideally, this value should not come directly from another text field because you shouldn't use views to store your app's data. Whenever a text field updates, you should store the updated value in a variable in your view controller or model.)
Also, the statsTextField does not yet exist at the time this code is executed because the destination view controller's view is not yet loaded. You should declare a separate string property in statsViewController (class names should begin with a capital letter btw) and then assign the text field's value in viewDidLoad.
The text field is probably nil at that point since the view hasn't been loaded. You can force this (no problem doing so since its about to happen anyway!) by wrapping your code in an if statement:
if (svc.view)
svc.textfield.text = #"Hello";
Accessing the view property forces the view controller to load the view, if it is not already present.
I notice you seem to be passing a whole textfield object instead of a string to the text property - that doesn't seem like a good idea. It should be more like my example above.