Why does the UI freeze when dismissing ViewController with ARSCNView? - swift

I am running an ARKit Session where I place SceneKit nodes. With every node I am instantiating a new view controller and pass its view as the node's content like so:
func createTextNode(anchor: ARCardAnchor) -> SCNNode? {
let plane = SCNPlane()
plane.height = 0.5
plane.width = 0.5
let sb = UIStoryboard(name: "Main", bundle: nil)
let fCVC = sb.instantiateViewController(withIdentifier: "CardViewController") as! CardViewController
plane.firstMaterial?.diffuse.contents = fCVC.view
let cardNode = SCNNode(geometry: plane)
cardNode.constraints = [billboardConstraint]
return cardNode
}
I am adding the nodes to the scene using the following ARSCNViewDelegate method and my custom ARCardAnchor (a subclass of ARAnchor):
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if let fcanchor = anchor as? ARCardAnchor {
DispatchQueue.main.async {
guard let n = self.nodeCreator.createTextNode(anchor: fcanchor) else { return }
node.addChildNode(n)
}
}
}
So far everything works and the nodes are placed in 3D space. But when I navigate back to the previous View Controller the UI freezes and I can't do anything.
I've tried using an unwind segue like this
#IBAction func goBackToPrevious(_ sender: Any) {
sceneView.session.pause()
self.performSegue(withIdentifier: "unwindToPrevious", sender: self)
}
and a navigation controller where I pop the AR scene controller off the stack. Every time the previous view controllers are frozen. No errors in Xcode, the app keeps running .
If I wait for approx. 2 minutes I can use the screen again. If I don't add nodes with view controllers to my AR scene, everything works perfectly fine. My only explanation is that the UIThread is overwhelmed when adding nodes because it creates a massive memory leak somewhere (that I haven't found despite of 10 hours of debugging). Has anyone had a similar experience and can tell me how to resolve this? What can I do to debug this and ensure smooth navigation?

The problem was that I assigned my custom UIViewController views directly to the nodes' planes. My assumption is that this created reference cycles since the view controllers held references to objects that I used elsewhere in the scene. I resolved it by capturing an image of the view and assigning that one to the node.
Seems like it is generally a dangerous practice to assign UIViews directly to nodes because it invites reference cycles.
Maybe someone more qualified on memory issues and/or SceneKit can give an opinion on this.

/* I think whenever UIView are directly attached to SCNMaterial and
that SCNMaterial to SCNNode. SCNMaterial are still being used when
previous screen is visited. By resetting ARSCNView seems to resolve
the issue. */
//try this
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//reset ARSCNView before going back to previous screen
self.sceneView = ARSCNView()
}

Related

Simple NSPageController example throws an unknown subview warning and stops working

I'm trying to get a very basic NSPageController to work (in book mode, not history mode). It will successfully transition once, and then stop working.
I suspect I'm creating the NSImageViews I'm loading into it wrong, but I can't figure out how.
The storyboard has a the SamplePageController which holds in initial hard-coded NSImageView.
I suspect I'm missing something really obvious here, since all of the tutorial's I've found for NSPageController are in Objective C not swift, and tend to focus on the history view mode.
The code is:
import Cocoa
class SamplePageController: NSPageController, NSPageControllerDelegate {
private var images = [NSImage]()
#IBOutlet weak var Image: NSImageView!
//Gets an object from arranged objects
func pageController(pageController: NSPageController, identifierForObject object: AnyObject) -> String {
let image = object as! NSImage
let image_name = image.name()!
let temp = arrangedObjects.indexOf({$0.name == image_name})
return "\(temp!)"
}
func pageController(pageController: NSPageController, viewControllerForIdentifier identifier: String) -> NSViewController {
let controller = NSViewController()
let imageView = NSImageView(frame: Image.frame)
let intid = Int(identifier)
let intid_u = intid!
imageView.image = images[intid_u]
imageView.sizeToFit()
controller.view = imageView
return controller
// Does this eventually lose the frame since we're returning the new view and then not storing it and the original ImageView is long gone by then?
// Alternatively, are we not sizing the imageView appropriately?
}
override func viewDidLoad() {
super.viewDidLoad()
images.append(NSImage(named:"text")!)
images.append(NSImage(named:"text-2")!)
arrangedObjects = images
delegate = self
}
}
In this case your pageController.view is set to your window.contentView and that triggers the warning. What you need to do is add a subview in the window.contentView and have your pageController.view point to that instead.
The reason for the warning is that since NSPageController creates snapshots (views) of your content history, it will add them at the same level as your pageController.view to transition between them: that means it will try to add them to pageController.view.superview.
And if your pageController.view is set to window.contentView, you are adding subviews to the window.contentView.superview, which is not supported:
New since WWDC seed: NSWindow has never supported clients adding subviews to anything other than the contentView.
Some applications would add subviews to the contentView.superview (also known as the border view of the window). NSWindow will now log when it detects this scenario: "NSWindow warning: adding an unknown subview:".
Applications doing this will need to fix this problem, as it prevents new features on 10.10 from working properly. See titlebarAccessoryViewControllers for official API.

How to call a function in viewController from GameScene

I am wanting to call a share function during game play. I have some code I can use in my GameViewController
func showTweetSheet() {
let tweetSheet = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
tweetSheet.completionHandler = {
result in
switch result {
case SLComposeViewControllerResult.Cancelled:
//Add code to deal with it being cancelled
break
case SLComposeViewControllerResult.Done:
//Add code here to deal with it being completed
//Remember that dimissing the view is done for you, and sending the tweet to social media is automatic too. You could use this to give in game rewards?
break
}
}
tweetSheet.setInitialText("Test Twitter") //The default text in the tweet
tweetSheet.addImage(UIImage(named: "TestImage.png")) //Add an image if you like?
tweetSheet.addURL(NSURL(string: "http://twitter.com")) //A url which takes you into safari if tapped on
self.presentViewController(tweetSheet, animated: false, completion: {
//Optional completion statement
})
}
I have also set the ViewController in my GameScene Class...
var viewController: GameViewController!
... and set the scene.viewController to self in my ViewController
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
scene.viewController? = self
}
}
However, When I call the function like so...
viewController.showTweetSheet()
... from my GameScene it gives me a "found nil when unwrapping an optional value" error.
I think I may need to set the scene.viewController to self later on, but I don't know how to do that in the viewController.
Any help would be much appreciated.
First of all, you don't need the question mark after scene.viewController.
Second, scene.viewController = self should come before skView.presentScene(scene). This will probably fix your problem.
Lastly, it's considered bad design (or at least sloppy) to make an SKScene have a property that is a UIViewController. The scene class is now tied to using a UIViewController, and if you want to extend your code to something that doesn't use UIViewController to control views (e.g. you want to make a Mac version of your game), it won't work right away because it's hard-coded to work with UIViewController.
The "pure" way to do this would be a technique called "delegation" by iOS programmers. You create a protocol that will be your delegate, and make your view controller implement that protocol. Then the SKScene uses the protocol, not UIViewController.
All that said, you might want to leave out this complexity.

In swift, how to get memory back to normal after an SKScene is removed?

I created a simple game with SpriteKit, however every time I run the game, the memory usage in simulator increases about 30mb, but never decreases when the game is finished.
When I run the game over ten times the simulator gets slower and slower and eventually crashes.
In this simple game I have two controllers and a gamescene:
MainController calls GameViewController via a button triggered
In GameViewController, gamescene is initialised in this way:
class GameViewController: UIViewController
{
var skView:SKView!
var scene:GameScene!
override func viewDidLoad() {
super.viewDidLoad()
scene = GameScene(size: view.bounds.size)
skView = view as SKView
skView.ignoresSiblingOrder = true
scene.scaleMode = .ResizeFill
scene.viewController = self
skView.presentScene(scene)
}
//with a prepareForSegue deinitialises the scene and skview:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "GameFinished"{
scene.removeAllActions()
scene.removeAllChildren()
scene.removeFromParent()
scene = nil
skView.presentScene(nil)
skView = nil
let target = segue.destinationViewController as MainController
}
}
}
In the GameScene, viewController is a property
var viewController:GameViewController? = GameViewController()
the segue is triggered with this:
self.viewController!.performSegueWithIdentifier("GameFinished", sender: nil)
I've also tried putting remove methods into deinit in GameScene:
deinit{
self.removeAllActions()
self.removeAllChildren()
}
Still wouldn't work
Your GameViewController has a strong reference to your GameScene. And your GameScene had a strong reference to your GameViewController. This leads to a strong reference cycle, which means that neither objects will be deallocated.
You need to declare your viewController property in your GameScene as weak.
weak var viewController:GameViewController? = GameViewController()
Using Swift 3, Xcode 8 and iOS 10.
After avoiding strong references, taken care of SKTextures, etc. memory level didn't recover after dismissing the scene and returning to the "menu" viewController.
I was using:
override func sceneDidLoad() {...}
This is available in iOS 10 but I wanted iOS 8&9 compatibility that's why I've
changed to the older:
override func didMove(to view: SKView) {...}
Beside getting compatible with older iOS versions, it turns out that the memory level drops after dismissing the scene. This was a surprise. I'm missing probably some leaks but it's working for me.
I hope it helps someone.

How to push a view in iOS8 and Swift

I am updating an app that pre-dates iOS5 - consequently much needs to change.
In the existing app we push a view (created in a nib) onto a screen that contains a UIPickerView/UIDatePicker to allow the user to make selections.
I this should be an easy thing to migrate to iOS8/Swift but I have spent the past 24h trying to work out the best way to do this with Storyboard/Segue, Container views, viewControllers, UIPopoverPresentationControler etc. with little consensus (even apple's sample code pushes the UIDatePicker in a table cell)
This seems to be a common requirement and I would appreciate any advice/comments on how others have solved this.
Many thanks
Try this
self.performSegueWithIdentifier("your segue identifier", sender: self)
Thanks for the answers and questions seeking clarification.
After a weekend of investigation I realize that I have been stuck in an old iOS paradigm.
With iOS8 everything has moved to ViewController presentation, allowing the developer to control the animation and define behavior on a range of devices (in different orientations).
The definitive answer to my question (I believe) is as follows:
Use UIPresentationController to present the desired UIViewController:
The view controller is the view to display (use code, nib or storyboard to create the picker view)
The presentation controller manages how it is presented (e.g. size, presentationStyle)
This is also the place to handled transparent/blurred views to cover existing content
check out:
func sizeForChildContentContainer(container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize
func frameOfPresentedViewInContainerView() -> CGRect
The UIPresentationController needs a transitioning delegate to manage the presentation (conforms to UIViewControllerTransitioningDelegate)
{
// We need to create a presentation controller to manage the presentation of VC
func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController!, sourceViewController source: UIViewController) -> UIPresentationController? {
let opc = OverlayPresentationController(presentedViewController: presented, presentingViewController: source)
return opc
}
// Get hold of an animation controller for presentation
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animationController = OverlayAnimatedTransitioning()
return animationController
}
// Get hold of an animation controller for dismissal
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animationController = OverlayAnimatedTransitioning()
return animationController
}
which in turn requires an animation controller (UIViewControllerAnimatedTransitioning) to handle the animation (slide, dissolve or something more adventurous):
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let containerView = transitionContext.containerView()
let animationDuration = self .transitionDuration(transitionContext)
containerView.addSubview(toViewController.view)
UIView.animateWithDuration(animationDuration, animations: { () -> Void in
<<< Animation Here>>>)
}) { (finished: Bool) in
transitionContext.completeTransition(true)
}
With the building blocks in place, the desired UIViewController may then be called (using an action/selector/segue) and by setting the transitioningDelegate property of the desired view the desired effect will be invoked:
#IBAction func showOVC(sender: AnyObject) {
let ovc = OverlayViewController()
let overlayTransitioningDelegate = OverlayTransitioningDelegate()
ovc.transitioningDelegate = overlayTransitioningDelegate
self.presentViewController(ovc, animated: true, completion: nil)
}
Dismissal is handled in the same way, simply by calling:
dismissViewControllerAnimated(true, completion: nil)
More information can be obtained via the docs and the excellent WWDC video which made everything clear for me: A look inside presentation controllers
or see apple's example code: LookInside
Bottom line - not as simple as before (assuming a single device in one orientation), but for a little more complexity there is far greater control of views and animation, with the ability to scale to multiple devices.
I hope that this helps people in a similar position
Thanks

Present View from SKScene in Xcode 6 with Swift

I have a basic game running exclusively as SKScene. I would like to transition to a view called testview.xib but I cannot find any Swift examples of how to do so.
Here's my attempt based on other answers
let vc = UIViewController(nibName: "testview", bundle: nil) as UIViewController
self.view.window.rootViewController.presentViewController(vc, animated: true, completion: nil)
but the application fails with an error "Thread1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode 0x0)"
The code for moving between SKScenes is
var transition:SKTransition = SKTransition.flipHorizontalWithDuration(1)
var scene:SKScene = otherScene(size: self.size)
self.view.presentScene(scene, transition: transition)
This turned out to be an issue with calling presentViewController() from within the init() method of the class. Basically, you can't call presentViewController() before the view/controller is setup; instead, you'll typically want to call it when some action occurs (button press, touch event, etc.).
The issue that was starting to be discussed in the comments and then moved in to chat was solved by loading the view controller from a storyboard instead of a xib. NOTE: An xib would work, but it's trickier to setup than a storyboard is.