SpriteKit scene deformed after modal presentation swift - swift

Could use some help troubleshooting an issue with a SpriteKit scene.
I have a scene that displays some coins in the main section of the app.
When I present a viewcontroller from the bottom I have no issue. Same for tab bar navigation, no issues.
Here is the view as it should always be displayed.
The issue comes only when I present a viewcontroller from the side.
When the new viewcontroller is dismissed, the scene works, but is distorted.
this is how it is displayed after a viewcontroller is displayed modally and later on dismissed.
EDIT: I forgot to mention that if I swipe vertically on the distorted scene, the distortion is fixed and all is good.
Here is some of the code in viewDidAppear of the viewcontroller.
Thanks for the help.
EDIT 2:
I just tested the app on a iPhone 5 using iOS 10 and the issue doesn't happen. Any chance this might be iOS 11 related?
func configureScene(_ completion: () -> Void) {
defer { completion() }
guard wScene == nil else { return }
let skView = SKView(frame: self.view.frame)
skView.isUserInteractionEnabled = false
skView.backgroundColor = .clear
wScene = WScene(size: view.frame.size)
wScene.backgroundColor = .clear
skView.presentScene(wScene)
view.insertSubview(skView, belowSubview: collectionView)
if let buttonsObstacle = doubleButton?.buttonsView {
let obstacleSize = CGSize.init(width: buttonsObstacle.frame.width, height: buttonsObstacle.frame.height)
obstacle = SKSpriteNode.init(color: .clear, size: obstacleSize)
guard let obstacle = obstacle else { return }
obstacle.name = WScene.obstacleNodeName
let convertedOrigin = view.convert(buttonsObstacle.center, from: buttonsObstacle.superview)
let skConvertedOrigin = skView.convert(convertedOrigin, to: wScene)
obstacle.position = skConvertedOrigin
obstacle.physicsBody = SKPhysicsBody(rectangleOf: obstacleSize)
obstacle.physicsBody?.allowsRotation = false
obstacle.physicsBody?.isDynamic = false
source.scrollHandler = { [weak self] (scrollView) in
guard let strongSelf = self else { return }
strongSelf.buttonsMoved(inView: skView, withScroll: scrollView)
}
wScene.addChild(obstacle)
presenter.loadData()
}
}

I solved my issue.
It was related to the new iOS 11 adjustedContentInset property.
My Coin SK scene was being moved by the scroll handler when the view appeared after a modal transition.
My solution is to disable the scrolling for the first 0.1 second after the view appears. In this way iOS 11 doesn't touch the coins anymore while users are able to scroll correctly because they interact with the view most of the time after at least 0.1 seconds.

Related

Add alpha to parentVC.view

I am trying to add alpha to the background view when tapped on a button. So far achieved adding blur but alpha not so much.
How can I add alpha to the background so that when the bottom sheet appears background will be darker and disabled.
let maxDimmedAlpha: CGFloat = 0.2
lazy var dimmedView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.alpha = maxDimmedAlpha
return view
}()
#objc func shareBtnClick() {
dimmedView.frame = self.parentVC.view.bounds
dimmedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.parentVC.view.addSubview(dimmedView)
if self.parentVC.navigationController != nil {
if self.parentVC.navigationController?.viewControllers.count == 1 {
showBottomSheet()
} else {
NotificationCenter.default.post(name: NSNotification.Name("ShowBottomSheet"), object: nil, userInfo: ["itemId": modalSheet(), "delegate": self])
}
} else {
showBottomSheet()
}
}
func showBottomSheet() {
let modalSheet = MainBottomSheet()
modalSheet.data = self.modalSheet()
modalSheet.delegate = self
modalSheet.modalPresentationStyle = .overCurrentContext
self.parentVC.present(modalSheet, animated: true)
}
I was able to produce the dimmed effect using this code in XCode, I'm not sure why it won't work in your project but there is an easy way to debug this.
I suggest using Debug View Hierarchy, one of XCode's best tools in my opinion. This allows you to separate every single layer of the user interface. This way, you can see if your dimmedView is actually being added to the parent view and that its frame is matching the parent view's bounds.
Keep in mind if your background is dark, you won't see this dimmedView because its backgroundColor is set to UIColor.black.
Debug View Hierarchy button

Background is not filling the whole view SpriteKit

For some reason my code will not fill the whole SKScene. Here is the code that I am using on Xcode 12 Beta 5.
GameScene.swift
class GameScene: SKScene {
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "space")
background.zPosition = 0
background.anchorPoint = CGPoint(x: 0.5, y: 0.5) // default
background.position = CGPoint(x: frame.midX, y: frame.midY)
print("frame.size \(frame.size)")
print("self.size \(self.size)")
print("view \(view.frame.size)")
background.size = CGSize(width: self.size.width, height: self.size.height)
self.addChild(background)
}
}
GameViewController.swift
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GKScene(fileNamed: "GameScene") {
if let sceneNode = scene.rootNode as! GameScene? {
// Present the scene
if let view = self.view as! SKView? {
sceneNode.size = view.bounds.size
sceneNode.anchorPoint = CGPoint.zero
sceneNode.scaleMode = .aspectFit
print("view.bounds.size \(view.bounds.size)")
view.presentScene(sceneNode)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override var prefersStatusBarHidden: Bool {
return true
}
}
Also for some reason, my view size is reporting ->
frame.size (320.0, 480.0) self.size (320.0, 480.0) view (320.0,
480.0)
But my GameScene.sks is set to -> w750, h1336
Why is my code cutting off the tops and the bottoms of the background?
This is going to sound dumb, but do you have a launch Screen? I was having the same problem which would only happen in Xcode 12 and not 11, but the main difference I found was Xcode 11 has a launch screen. Once I added the launch screen storyboard and added it to my plist my SCNView would fill the screen. I am not sure why this would cause the view not to follow the constraints, but after adding it to my other projects it fixes the issue.
Edit:
You do not need to have a launch screen storyboard, you can just add your main storyboard that displays you scene in the plist under "Launch screen interface file base name".
This is definitely caused by not having a launch screen assigned. It's very odd that Xcode 12 has this behavior for SpriteKit by default. I'm sure many many people will be stumped and confused. I noticed that the main visible difference to earlier versions was the lack of a launch screen.
You can either create a launch screen and assign it or define Main.storyboard as the launch screen, as proposed earlier. Probably the easiest thing to just get it working is to go to the project target, then General and choose "Main" where it says "Launch Screen File". This will make it work as expected.
Update: This is still happening in Xcode 12.4 (12D4e). Surprisingly, the launch screen will be missing in a brand-new iOS-only project, whereas it's there in a multi-platform project. It seems like this is an oversight on Apple's part.
How to select Main as your project's Launch Screen File:

Rotating animation shown at final position only iOS swift

I am trying to implement custom activity indicator which is simple rotating UIImageView. I have next code
import Foundation
import UIKit
class CatSpinnerAnimation: UIView {
let catImageView: UIImageView = {
let catImageView = UIImageView()
catImageView.image = UIImage(named: "CatDateIcon-1024")
catImageView.translatesAutoresizingMaskIntoConstraints = false
return catImageView
}()
func startRotating(superView: UIView) {
superView.addSubview(catImageView)
UIApplication.shared.beginIgnoringInteractionEvents()
catImageView.centerXAnchor.constraint(equalTo: superView.centerXAnchor).isActive = true
catImageView.centerYAnchor.constraint(equalTo: superView.centerYAnchor).isActive = true
catImageView.widthAnchor.constraint(equalToConstant: 50).isActive = true
catImageView.heightAnchor.constraint(equalToConstant: 50).isActive = true
let rotate = CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 5.0) {
self.catImageView.transform = rotate
}
}
func stopRotating(superView: UIView) {
UIApplication.shared.endIgnoringInteractionEvents()
catImageView.removeFromSuperview()
}
}
In my ViewControllers I instantiate an instance of this class and call its function
let catSpinnerAnimation = CatSpinnerAnimation()
catSpinnerAnimation.startRotating(superView: view)
But this doesn't show rotating animation at all, just my ImageView in final position (rotated for 189 degrees). I tried many types of animation but still don't have necessary result. I would appreciate any explanation of correct behavior of rotating animations
I think you might need to let the view be added to the view hierarchy first before you can animate it. Try wrapping your call to UIView.animate() in a call to DispatchQueue.main.asyncAfter(deadline: .now() + .01) {}
That would let your view be added to the view hierarchy and get rendered before you submit your animation.

Terminated due to memory issue - ViewControllers taking up memory

I'm developing a game using sprite kit and have various view controllers for different scenes. Like story mode, multiplayer etc...
I've noticed a problem where every time I open up a view controller with a SpriteKit scene, the memory usage for my app increases. And when the view is dismissed, that memory isn't allocated back?
Is there something I need to be doing in my code to return the memory the ViewController or SpriteKit scene used up upon loading?
EDIT: As requested, here is some code from when the VC loads up with a SpriteKit scene & the code used to dismiss the VC:
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
if let scene = SKScene(fileNamed: "GameScene.sks") {
scene.scaleMode = .aspectFill
view.presentScene(scene)
(scene as! GameScene).parentVC = self
}
}
}
For the view dismissal, I first present a loading screen over the UIApplication.shared.keyWindow and then dismiss:
dismiss(animated: true) {
LoadingScreen?.removeFromSuperview()
}
The loading screen class is handled in the AppDelegate.
I think it's an issue of retain cycles
1 if let view = self.view as! SKView? {
2 if let scene = SKScene(fileNamed: "GameScene.sks") {
3 scene.scaleMode = .aspectFill
4 view.presentScene(scene)
5 (scene as! GameScene).parentVC = self
}
}
if line 5, inside the closure, you're referencing self, so when the viewcontroller should be deallocated, the reference on line 5 is stopping the deallocation, Can you try this :
if let view = self.view as! SKView? {
if let scene = SKScene(fileNamed: "GameScene.sks") {
scene.scaleMode = .aspectFill
view.presentScene(scene)
guard let scene = scene as? GameScene else { return }
guard let scencePVC = scene.parentVC else { return }
scencePVC = self
}
}
I'm trying to fix the issue without knowing the object hierarchy but it's hard. Basically the issue is that you have 2 objects( viewcontrollers) referencing each other that would stop the deallocation.
You can read more about this issue here & here

ViewController slide animation

I want to create an animation like the iOS app facebook at tabswitch[1]. I have already tried to develop some kind of animation, the problem that occurs is that the old view controller becomes invisible directly on the switch, instead of fading out slowly while the new controller is sliding in fast.
I've found this SO question How to animate Tab bar tab switch with a CrossDissolve slide transition? but the as correct marked solution does not really work for me (it is not a slide it is a fade transition). What I'd also like to get is the function to make slide left or right to switch the tabs. Like it was on a older version of facebook.
What I've got so far is this:
extension TabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let fromView = selectedViewController?.view,
let toView = viewController.view else { return false }
if fromView != toView {
toView.transform = CGAffineTransform(translationX: -90, y: 0)
UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
toView.transform = CGAffineTransform(translationX: 0, y: 0)
})
}; return true
}
}
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
How to fix this?
[1]
I would very much like to add a gif from the Facebook app. The problem is that I don't want to censor the video and just reveal too much of my data. (Even if fb already has them). Also on youtube I didn't find a suitable recording. Please try it yourself in the fb app in iOS.
You can use the following idea: https://samwize.com/2016/04/27/making-tab-bar-slide-when-selected/
Also, here's the code updated to Swift 4.1 and I also removed the force unwrappings:
import UIKit
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let tabViewControllers = tabBarController.viewControllers, let toIndex = tabViewControllers.index(of: viewController) else {
return false
}
animateToTab(toIndex: toIndex)
return true
}
func animateToTab(toIndex: Int) {
guard let tabViewControllers = viewControllers,
let selectedVC = selectedViewController else { return }
guard let fromView = selectedVC.view,
let toView = tabViewControllers[toIndex].view,
let fromIndex = tabViewControllers.index(of: selectedVC),
fromIndex != toIndex else { return }
// Add the toView to the tab bar view
fromView.superview?.addSubview(toView)
// Position toView off screen (to the left/right of fromView)
let screenWidth = UIScreen.main.bounds.size.width
let scrollRight = toIndex > fromIndex
let offset = (scrollRight ? screenWidth : -screenWidth)
toView.center = CGPoint(x: fromView.center.x + offset, y: toView.center.y)
// Disable interaction during animation
view.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.3,
delay: 0.0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: .curveEaseOut,
animations: {
// Slide the views by -offset
fromView.center = CGPoint(x: fromView.center.x - offset, y: fromView.center.y)
toView.center = CGPoint(x: toView.center.x - offset, y: toView.center.y)
}, completion: { finished in
// Remove the old view from the tabbar view.
fromView.removeFromSuperview()
self.selectedIndex = toIndex
self.view.isUserInteractionEnabled = true
})
}
}
So, you need to subclass UITabBarController and you also have to write the animation part, you can tweak the animation options (delay, duration, etc).
I hope it helps, cheers!
I've never seen Facebook so I don't know what the animation is. But you can have any animation you like when a tab bar controller changes its tab (child view controller), coherently and without any hacks, using the built-in mechanism that Apple provides for adding custom animation to a transition between view controllers. It's called custom transition animation.
Apple first introduced this mechanism in 2013. Here's a link to their video about it: https://developer.apple.com/videos/play/wwdc2013/218/
I immediately adopted this in my apps, and I think it makes them look a lot spiffier. Here's a demo of a tab bar controller custom transition that I like:
The really cool thing is that once you've decided what animation you want, making the transition interactive (i.e. drive it with a gesture instead of a button click) is easy:
Now, you might be saying: Okay, but that's not quite the animation I had in mind. No problem! Once you've got the hang of the custom transition architecture, changing the animation to anything you like is easy. In this variant, I just commented out one line so that the "old" view controller doesn't slide away:
So let your imagination run wild! Adopt custom transition animations, the way that iOS intends.
If you want something for pushViewController navigation, you can try this.
However, when switching between tabs on a TabBarController, this will not work. For that, I'd go with #mihai-erős 's solution
Change the Animation duration as per your liking, and assign this class to your navigation segues, for a Slide Animation.
class CustomPushSegue: UIStoryboardSegue {
override func perform() {
// first get the source and destination view controllers as UIviews so that they can placed in navigation stack
let sourceVCView = self.source.view as UIView!
let destinationVCView = self.destination.view as UIView!
let screenWidth = UIScreen.main.bounds.size.width
//create the destination view's rectangular frame i.e starting at 0,0 and equal to screenwidth by screenheight
destinationVCView?.transform = CGAffineTransform(translationX: screenWidth, y: 0)
//the destinationview needs to be placed on top(aboveSubView) of source view in the app window stack before being accessed by nav stack
// get the window and insert destination View
let window = UIApplication.shared.keyWindow
window?.insertSubview(destinationVCView!, aboveSubview: sourceVCView!)
// the animation: first remove the source out of screen by moving it at the left side of it and at the same time place the destination to source's position
// Animate the transition.
UIView.animate(withDuration: 0.3, animations: { () -> Void in
sourceVCView?.transform = CGAffineTransform(translationX: -screenWidth,y: 0)
destinationVCView?.transform = CGAffineTransform.identity
}, completion: { (Finished) -> Void in
self.source.present(self.destination, animated: false, completion: nil)
})
}
}