I'm using ARKit with SpriteKit. My AR feature is not a core feature in my app, users may optionally navigate to a viewController with an ARSKView where I configure and set an SKScene in the ARsession.
I pause the session when the user navigates back:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
then the viewController is pop from the navigationController where it was pushed. But I'm seeing that the func update(_ currentTime: TimeInterval) in my SKScene subclass keeps being called... which is the appropriate way to also stop that? I thought that pausing the ARSession would also stop the scene.
EDIT: when I navigate back from the view controller that holds the ARSKView and it is deinitialized, I see that the SKScene is still in memory and a new one is created every time I navigate again to the AR related view controller. So, if I navigate there N times, I see I have N SKScenes in memory. How could I appropiately remove the scene when navigating back? The scene property of the ARsession is read only.
that sounds like a memory retain issue. Make sure that any node in you SKScene subclass is properly removed and deinitialized.
Set your scene delegate to nil, set any node or array of nodes referenced in your VC to nil.
I ran a simple test with 2 VC embedded in a navigation controller, the first one has a view with a start button, the second one is the ARVC.
I run the session, set my custom scene delegate to ARVC, when I hit the back button everything stops by itself and my ARVC is being popped. Nothing in memory.
So make sure that you manually remove all your nodes and their animations, constraints, actions and anything that relates to you custom SKScene subclass.
Also don't be afraid to call sceneView.presentScene(nil) if that can help.
I couldn't find any official documentation about this, apparently there is not proper method to stop a session yet. What I found that was similar to your scenario is this work around.
It suggests you create ARSKView as disposable and before leaving your controller, pause the session, remove the view from superview and assign it to nil.
Related
I have a small problem I sadly couldn't solve. I have a few ViewControllers. I need to call a function when going back from a ViewController to the previous ViewController (which does not reload, so viewDidLoad / Appear is not called).
Mustn't there be anything like viewDidLoad, but gets called every time the ViewController is visible?
This is how I open the ViewControllers:
let vc = storyboard?.instantiateViewController(identifier: "levels_vc") as! LevelsViewController
present(vc, animated: true)
This is the way I try to get the event, but it only gets called the first time (when the ViewController loads the first time), but not when I open a new ViewController from this one and close the new one.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("VC appeared!")
}
You can use viewWillAppear or viewDidAppear for that purpose!.
viewWillAppear() is called every time the view will appear on the screen.
This can be useful for views that need to be updated every time they’re scrolled to. For example, the Now Playing section in a music app or any view that is updated when it is scrolled to.
viewDidAppear is called when the view has appeared on the users screen.
This is a good place to “lazy-load” any other elements onto your screen. If your user interface is very complicated, you don’t want to stall or cause a delay in the previous mentioned methods.
It’s better to load something onto the screen and then call some methods to update the views for some large applications.
This is also a create place to start any animations.
That would be ViewDidAppear() ViewDidAppear() - Apple Developer
Below I have two and a half different ways to set up my game using view controllers and SKScene. The half I haven’t put so much time into yet as I’m hoping one of these other solutions will work.
described in more detail below are:
MainViewController -> GameViewController(holds the SKScene)
GameViewController(holds the SKScene) - Using Protocols
0.5 Two SKScenes/Two swift custom classes.
Number 1
MainViewController -> GameViewController(holds the SKScene)
when the game is over in the SKScene - I want to dismiss GameViewController and return to MainViewController.
I’ve had no luck in getting that to work. I’ve tried dismissing the view controller and the closest I’ve come is dismissing the SKScene using something like(in SKScene:
self.view?presentScene(nil)
or
self.dismiss(animated: false, completion: nil)
self.presentingViewController?.dismiss(animated: false, completion: nil)
but I’m unable to go back to the MainViewController. I’ve tried creating a reference to the GameViewController to pop it like:
weak var gameViewController:GameViewController? = GameViewController()
Unfortunatly, I’m unable to find the right functions to dismiss it properly.
After a lot of searching without finding an answer I thought I would try a different method.
Number 2
GameViewController(holds the SKView) then have the GVC hold the buttons and labels that I wanted and show the scene on top. this was a fail as The buttons and labels don’t have a zPositions and appeared on top of the scene.
I tried doing .isHidden = true on them before calling the scene and that seemed to work but when I dismiss the scene and attempt to change them back to .isHidden = false (tried using protocols) I was unable to get it to work. (was going off of what stvn describes How to reference the current view controller from a sprite kit scene)
I suspect this second way could be a good way of doing it but maybe I’m just not approaching it correctly.
Number 0.5
then finally what I’m thinking about now is just making the second scene to “flip” back and forth between. I believe this is the recommended way of doing it but I’m not really sure how to approach it. If I did this version. I would like to have a gameScene.swift/gameScene.sks for each view (perhaps using something like presenting a uiviewcontroller for skscene.
I would prefer the first way with having two view controllers as I think that the layout for my game would be much easier to do using a few view controllers.
My least preferred method is multiple scenes as I’m just not that great with sprite kit and math for laying out everything.
Is the first method even possible? If so any suggestions would be great. If not what would be the recommended way of doing this?
In method 1 if your MainViewController is a UINavigationController you should have no issues pushing and popping whatever other ViewControllers you need to.
e.g. in GameViewController:
self.navigationController!.popViewController(animated: true)
My app uses standard UINavigationViewController.
At some point, user has navigated to a ViewController which shows a SceneKit Scene.
This scene is very heavy in memory, since it has around 6000 geometries and each geometry has its own Material (this needs to be this way).
When user clicks the back button at the top bar in order to return to the previous viewController, the app correctly pops the current View Controller and shows the one below.
However, for around 4 seconds, the UI of the new ViewController shown freezes.
I have used Instruments Time Profiler, and I can see that big part of those 4 seconds are taken by these SceneKit methods:
-[NSConcreteMapTable countByEnumeratingWithState:objects:count:]
-[SCNMetalResourceManager _geometryWillDie:]
-[SCNMetalResourceManager _materialWillDie:]
It makes sense because there are so many geometries and materials.
How can I solve this situation, so that UI is not blocked while the heavy Scene is being released, either from a SceneKit perspective (making deallocation quicker) or from a UINavigationViewController perspective (maybe forcing deallocation of the Scene to happen in a separate thread?).
My understanding is that SceneKit uses it's own background thread to do work, so it shouldn't be blocking the main thread.
It is possible that the blocking of the main thread has something to do with UINavigationController animating the content away. I've had issues when trying to animate SCNViews/SCNScenes and it looks like SceneKit and CoreAnimation do not collaborate very well.
I don't have the full context here, but I can suggest couple of things for you to test:
You can try setting the scene propery of the SCNView with the heavy scene scnView.scene = [SCNScene scene]; to a new empty scene just before the View Controller with the scene is popped off, so that the heavy scene is not involved in the animation, and SceneKit properly deallocates it in it's background threat.
You can try keeping a strong reference to the heavy scene in the View Controller that prepares the problematic View Controller with the heavy scene.
Then when the latter is popped off and you return to the one below it, the popped off View Controller and all of its content should have deallocated, except the heavy scene and you'll have a reference to it.
Here you can try setting to nil (after the animation has completed) and possibly it will deallocate properly on SceneKit background thread.
If not, you can first remove all heavyScene.rootNode child nodes, actions, etc and then deallocate.
If this still does not work, you can first try using a recursive function to traverse all your nodes one by one and nil their geometry/material first and then deallocate the scene itself.
Using the rendering loop -renderer:updateAtTime: method to remove the nodes over several frames (less than a second). Then you can be sure that SceneKit will deal with them the way it is designed to.
You will have to detect when the user presses UINavigationController "Back button" and the VC with the heavy scene is being popped off. Then over, say 20 frames (1/3 of a second at 60fps), remove 5% of nodes at each frame, so there will still be a short delay but 1/3 of a second and hopefully unnoticeable.
The detection of the VC being popped off can happen by overriding its didMoveToParentViewController: method and checking if nil is passed as the parent parameter.
E.g.
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (!parent) {
// work that should be done when VC is being removed from parent
// maybe set a counter in the VC being popped off to
// count down from 20 to 0, -1 at each -update call
}
}
I'm making an app that takes pictures. It consists of two view controllers connected via a tabBarController. On one of the viewControllers is the camera screen. It has a UIView which has the sole purpose of displaying the contents of AVCaptureVideoPreviewLayer.
I've hooked up the inputs and outputs, and have a live feed of the camera's view displayed on my UIView. Since I have 2 view controllers connected by a tabBarController, I make sure I call stopRunning whenever I'm not on my camera's viewController - when the user tabs to the other viewController.
Currently, I've implemented it as follows:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
session.startRunning()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
session.stopRunning()
}
Whenever I'm on the camera screen with the session running, my iPhone 5 heats up - something I expected - probably due to the AVCaptureVideoPreviewLayer object's demand for resources. My above attempt is to stop the session wherever it's not needed. However, I feel like I haven't covered all the grounds.
If my app goes to background, will the session continue? I used some println statements to track my viewController life - cycle methods, and found that viewWillDisappear is not called when app is retired to the background.
Does that mean my session continues to run and my camera is still trying to provide the feed? I'm suspect this may be an issue, because the phone still feels warm after a long while.
I'm trying to load a UIView and then right away detect touches on that new view. Currently, overriding touchesBegan gives a delay of around a second.
So, I load up the UIView and immediately keep tapping on the screen. It takes around a second for touchesBegan to be called. From that point on, all is well. However, I can't afford the ~second wait initially.
I have stripped back all the code in the UIView to just the barebones incase anything was clogging up the main thread, but the delay still persists.
How can I go about getting immediate feedback of touches from a newly presented UIView? Thanks.
-- EDIT BELOW --
I've been playing around with this for the past few hours. Even when creating a custom UIWindow and overriding sendEvent, the UITouchPhase gets halted when the new view is displayed. To begin receiving events again I have to take my finger off the screen and place it back on the screen. (I don't want to have to do this).
The problem seems to lie with the segue to the new view controller. When it segues, the touch phase is ended. If I simply add a subview to the current view controller, I see the desired functionality (i.e. instant responding to touch).
Given my newly presented view contains a lot of logic, I wanted to wrap it all up in it's own view controller rather than add it to the presenter view controller. Is there a way for me to do this and use 'addSubview` to present it? This should hopefully achieve the desired effect.
In the end, I created a custom view controller with it's own xib. Where I would have segued, I now instantiate that custom view controller and append it's view. This has eliminated the touch lag.
Have you disabled multi-touch? There's an inherent delay while the controller waits to see if there's a follow up touch (on all single touches). The initial sluggishness might be from loading up the multi-touch code and deciding what to do about it.
myViewController.view.multipleTouchEnabled=NO;
As to your final question, look into view controller containment. Since iOS 5 Apple has provided the hooks officially and safely to present one view controller as a sub view of another.
Sadly I've no insight as to the greater issue.
I found an answer that worked for me from a similar question asked here:
iOS: Why touchesBegan has some delay in some specific area in UIView
This solution isn't check-marked on that thread, so I'll copy it here to make it easier to find.
override func viewDidAppear(animated: Bool) {
let window = view.window!
let gr0 = window.gestureRecognizers![0] as UIGestureRecognizer
let gr1 = window.gestureRecognizers![1] as UIGestureRecognizer
gr0.delaysTouchesBegan = false
gr1.delaysTouchesBegan = false
}