Skscene transitioning in Swift - swift

In essence, I have a menu scene and level scene and I want to be able to transition back and forth. If you look here you can see the first present scene which starts off showing the menu.
if let view = self.view as! SKView? {
let testLevel = LevelMenu()
testLevel.size = self.view.frame.size
testLevel.initializeMenu(NumberOfLevels: 6, Restricted: true, MenuNumber: 1)
testLevel.scaleMode = .fill
view.presentScene(testLevel)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
(LevelMenu is a custom class which extends SKScene, initialize menu is a method which I use to set up the specifics of the scene.)
Now inside the LevelMenu class if a couple conditions are met the following code is called to go into a level.
if let view = self.view as SKView? {
view.presentScene(currentLevel)
}
(currentLevel is a variable which stores the current Level object.)
Also, when the levels are set up they have the same code as the menu when its set up.
let TempLevel = Level()
TempLevel.size = self.size
TempLevel.setUp(package: menuNumber, numberInPackage: i, locked:
restricted, menu: self)
TempLevel.name = "Package \(menuNumber), Level \(i)"
TempLevel.scaleMode = .fill
levels.append(TempLevel)
(Level is another custom SKScene, setUp() is a method which sets it up, and levels is an array which holds all the levels.)
To transition back to the menu I call this code again just with the menu which has been passed through to the class.
if let view = self.view as SKView? {
view.presentScene(currentLevel)
}
This is the issue: When I call the transition inside the menu to go to a level it works perfectly. Transitions animates works perfectly fine; however, when I call the presentScene() to go back, it will print out the text I put in the viewDidAppear() except it still shows the level Skscene and the level Skscene still has working buttons.
I tried removing scenes from their parents and it seems to not work and from what I have read be a bad idea. I theorize that the menu scene is loaded the whole time and just blocked and when I call presentScene it says "hey I'm here." but fails to realize I want it up front. If there could be some way to stop presenting the level scene and just have the menu show up that would be great since once you finish a level it shouldn't just sit around.
Thank you in advance for all help offered.

So though more investigation and debugging I found out that the scene was actually being loaded but for some reason was not being displayed. After playing around with the visual settings I thought about making sure the size of the scene was the proper size and I found that by setting the size of the menu before going back it now shows the menu.
I hope this question helps someone out there who ever encounters a similar issue, thanks internet for nothing.

Related

Swift UISplitViewController how to go from triple to double columns

I'm having a lot of trouble figuring out how to structure a UISplitViewController.
I want:
A sidebar in the primary view (always)
I want the 1st sidebar navigation item (animals) to show triple (sidebar, animal list, animal detail)
I want the 2nd sidebar navigation item (profile) to show double (sidebar, profile view)
I see other apps doing this (GitHub for example), but I've really got no idea how they're managing it. Resources are hard to find, and most tutorials I've seen just show one or the other column styles.
I'm mostly looking for answers on how to architecture this well, but any code would also be massively appreciated!
SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = ViewController(style: .tripleColumn)
window?.makeKeyAndVisible()
}
Root view controller
class ViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewControllers = [
SidebarViewController(),
AnimalsViewController(),
AnimalDetailViewController()
]
// Example attempt at removing the secondary view
setViewController(ProfileViewController(), for: .supplementary)
setViewController(nil, for: .secondary)
hide(.secondary)
}
}
Desired behaviour
Animals
Profile
Cheers!
There is no "official" way to do it but it is possible. As far as I can tell, one of the best ways so solve it is to have two instances of UISplitViewController in your root view controller and juggle between them when needed. Here is my approach (approximately):
Disclaimer: This code was consulted with Apple engineers during the last WWDC22 on UIKit Labs. They have confirmed that it is very unfortunate that they currently do not offer a convenient way of doing it, and that this approach is probably the best way to do it. Feedback was filed and its ID passed to the engineers so hopefully we get an official API in the iOS 17 :D
rdar://FB10140263
Step 1. Initialise the UISplitViewControllers
private lazy var doubleColumnSVC: UISplitViewController = {
$0.primaryBackgroundStyle = .sidebar
// setup your SVC here
$0.setViewController(doubleColumnPrimaryNC, for: .primary)
return $0
}(UISplitViewController(style: .doubleColumn))
private lazy var tripleColumnSVC: UISplitViewController = {
$0.primaryBackgroundStyle = .sidebar
// setup your SVC here
$0.setViewController(tripleColumnPrimaryNC, for: .primary)
return $0
}(UISplitViewController(style: .tripleColumn))
Step 2. Initialise your sidebar VC and two separate UINavigationControllers
I have found it to be the most reliable solution for swapping sidebar VC. With a single UINavigationController instance there was a bug that the sidebar would randomly not appear. Two instances solve this problem while still keeping a single SidebarVC with proper focus state and already laid out content.
// Sidebar is shared and swapped between two split views
private lazy var sideBar = YourSideBarViewController()
private lazy var doubleColumnPrimaryNC = UINavigationController(
rootViewController: UIViewController()
)
private lazy var tripleColumnPrimaryNC = UINavigationController(
rootViewController: UIViewController()
)
Step 3. Make a property to store currently displayed SVC
It will come in handy in the next step when toggling between the two instances.
private var current: UISplitViewController?
Step 4. Implement Toggling between two styles when needed
This function should be called every time you want to navigate to a different screen from sidebar.
private func toggleStyleIfNeeded(_ style: UISplitViewController.Style) {
switch style {
case .doubleColumn:
// skip if the desired column style is already set up
if current === doubleColumnSVC { return }
// reassign current
current = doubleColumnSVC
// here add doubleColumnSVC as child view controller
// here add doubleColumnSVC.view as subview
// swap the sidebar
doubleColumnPrimaryNC.setViewControllers([sideBar], animated: false)
// here remove tripleColumnSVC from parent
// here remove tripleColumnSVC.view from superview
case .tripleColumn:
// skip if the desired column style is already set up
if current === tripleColumnSVC { return }
// reassign current
current = tripleColumnSVC
// here add tripleColumnSVC as child view controller
// here add tripleColumnSVC.view as subview
// swap the sidebar
tripleColumnPrimaryNC.setViewControllers([sideBar], animated: false)
// here remove doubleColumnSVC from parent
// here remove doubleColumnSVC.view from superview
default:
return
}
// If you are using UITabBarController for your compact style, assign it here
current?.setViewController(tabBar, for: .compact)
}
In lines that start with "here add" you will need to write your own code. I have simplified the code sample to make it shorter.
Step 5. Enjoy your SVC with dynamic columns!
Now you are basically ready to go! With this simple helper method on your root VC (or whichever one that is handling the navigation and managing the SVCs) you will have all the power that you need to achieve what you wanted, which is a UISplitViewController with dynamic number of columns!
func setViewController(
_ viewController: UIViewController,
for column: UISplitViewController.Column,
style: UISplitViewController.Style
) {
toggleStyleIfNeeded(style)
current?.setViewController(viewController, for: column)
}
We are using this approach in production for a few months now and it works great. The app supports iOS, iPadOS and Mac Catalyst. There are some things like customising the status bar style and getting consistent sidebar button experience a bit tricky to work perfectly but with some adjustments and help from the UISplitViewControllerDelegate everything is possible.
Good luck!
P.S. If anyone have walked this path before and is able to share suggestions, please do! I would love to learn more on how one could improve this dynamic split view experience both for users and developers.
To switch from 3 to 2 columns you must simply reinitialise the UISplitViewController with two columns (UISplitViewController(style: .doubleColumn) and reassign it to the window.rootViewController.
When reinitialising the UISplitViewController, you can either assign existing view controller objects, to maintain the current state, or initialise new ones. In case you assign existing view controller objects, it's probably handy to store these in variables after creating them for the first time.

Update to Xcode 11.3.1 - navigationBar and half of the Views disappear after storyboard refactoring

Using Xcode 11.3.1, Simulator11.3.1, iPhoneX, Swift5.1.3, iOS13.3,
I am wondering why half of my app suddenly disappears !!
Could it be the update to Xcode 11.3.1 ???
The following shows a screenshot of the Xcode Debug View Hierarchy.
The left side is what the iPhone 11 Pro Simulator shows and the right side is the Debug View Hierarchy:
Clearly there are many more objects in the view hierarchy (such as the round buttons at the bottom) that are not shown on the Simulator (and also not on a physical iPhoneX). Also the NavigationBar is missing completely !!!!
The blue highlighted object is a custom navigationBar (consisting of a stackView). This worked before but not since the Xcode update. I am really not believing this. What could go wrong here ??
If it is not the Xcode-update, then my refactoring of the storyboard could also be a cause of this view-losses.
Before my refactoring, the VC at question was a ChildViewController of another ViewController. Now, it is the entry point of the App. Could this change bring the view-losses ? I want to see a NavigationController with largeTitle. But there is no NavigationController whatsoever now!
Here is the code that sets up the navigationBar:
override func viewDidLoad() {
// set up navigationItem and navigationController look and feeel
navigationItem.largeTitleDisplayMode = .always
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
navigationController?.set_iOS12_lookAndFeel()
navigationItem.title = "bluub"
}
And the needed NavigationController extension:
import UIKit
extension UINavigationController {
func set_iOS12_lookAndFeel() {
if #available(iOS 13.0, *) {
self.keep_iOS12_lookAndFeel()
} else {
let attrLargeTitle = AppConstants.FontAttributes.NavBar_LargeTitleTextAttributes
self.navigationBar.largeTitleTextAttributes = attrLargeTitle
let attrTitle = AppConstants.FontAttributes.NavBar_TitleTextAttributes
self.navigationBar.titleTextAttributes = attrTitle
}
}
private func keep_iOS12_lookAndFeel() {
if #available(iOS 13.0, *) {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithDefaultBackground()
navBarAppearance.backgroundEffect = .init(style: .systemThickMaterialDark)
navBarAppearance.titleTextAttributes = AppConstants.FontAttributes.NavBar_TitleTextAttributes
navBarAppearance.largeTitleTextAttributes = AppConstants.FontAttributes.NavBar_LargeTitleTextAttributes
navBarAppearance.buttonAppearance.normal.titleTextAttributes = AppConstants.FontAttributes.NavBar_ButtonAppearance_Normal
navBarAppearance.doneButtonAppearance.normal.titleTextAttributes = AppConstants.FontAttributes.NavBar_Done_ButtonAppearance_Normal
self.navigationBar.standardAppearance = navBarAppearance
self.navigationBar.scrollEdgeAppearance = navBarAppearance
}
}
}
.
---------------- more findings -----------------------------
After another storyboard refactoring, I could bring back the round menu buttons. However, the largeTitle-NavigationBar is still completely missing.
Frankly, the latest refactoring did not introduce any new constraints or other storyboard settings as before. The fact that I kicked out the NavigationController and replaced it by an identical new one, plus, re-assigned one or the other constraint of the menu-button-View, did bring the bottom menu back alive. As far as I can tell, no difference to the previous storyboard was introduced.
It is very annoying why a storyboard needs to be redrawn basically to render correctly. Something seems corrupt here as for the Xcode functionality with storyboard !
But lets leave this talk.
My remaining question:
How can I bring back a missing NavigationBar ?????????
.
---------------- another finding -----------------------------
If I reassign the "first-entry-ViewController" to the old ViewController that eventually adds the Menu-button-ViewController as a ChildViewController --> then everything works!
If I assign the "first-entry-ViewController" to be the Menu-button-ViewController directly, then the NavigationBar disappears !
Here is the overview:
I finally found a solution.
It indeed had to do with my login-architecture of this app.
The fact that only by setting the "first-entry-ViewController" as the old-Main-ViewController made a difference:
This old-Main-ViewController (that eventually adds the Menu-button-ViewController as its Child) did have the following line in its viewWillAppear method:
navigationController?.setNavigationBarHidden(true, animated: animated)
Its intention was actually to never show the navigationBar of its own. But instead load a ChildViewController that itself shows a navigationBar of its own.
The strange thing with storyboard: Even tough setting the Menu-button-ViewController as first-entry does somehow still consider the navigationController-hiding mechanism of the previous first-entry setting. This seems a bug to me inside storyboard. I would assume that visible navigationBar is the default behaviour. But having set it once to be hidden keeps it hidden, even tough the hiding-command is no longer executed. Anyway, very strange behaviour.
By eliminiting that line - or better - by adding it "with hidden = false" inside the Menu-Button-ViewController, makes the NavigationBar being shown again !!!
My learning is to keep an eye on all navigationController actions or mutations throughout the entire App hierarchy. The fact that a single ViewController might mutate something on its navigationController might not be enough. You have to check event parent-ViewControllers or segue-parents as well. And most annoying, applying a different first-entry to a VC does require you to overwrite default behaviours of your views to make sure your views are shown !

How to Remove windows from UIApplication in Swift

While clicking on the button , i am moving to another view controller using the following code.
var window: UIWindow?
window = UIWindow.init(frame: UIScreen.main.bounds)
window?.autoresizesSubviews = true
window?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let trackingViewController = LoginCameraViewController.init(screen:
.main)
window?.rootViewController = trackingViewController
window?.addSubview((trackingViewController?.view)!)
window?.makeKeyAndVisible()
window?.layoutSubviews()
For every button click, a new window is added to the application.I want to remove the latest window added.
The number of windows present in the application can be known by using following code.
let windowz = UIApplication.shared.windows
print("subviews",windowz)
I think you get the wrong concept of navigation in iOS. Window is like a root object in which ViewControllers appear. So probably the solution you're looking in a first place is UINavigationController.
Apple Documentation on Navigation
For iOS 13 i was able to do it this way
I created array which contains the window using which this new viewController is being presented,
var arrWindow = [UIWindow]()
arrWindow.append(yourNewWindow)
// Note: This will be stored as strong reference so need to remove it.
Also store your original window in variable
let originalWindow = yourOriginalWindow
// Note: Same goes for this as well ,this will be stored as strong reference so need to remove it.
At the time of removing there are many ways to do it but this was the most suited way for me,
func removeAppendedWindow() {
for window in arrWindow {
if window != originalWindow {
if let index = arrWindow.index(of: window) {
window.isHidden = true
arrWindow.remove(at: index)
}
}
}
}
In the below code windowz is normal array.
let windowz = UIApplication.shared.windows
You can remove last by using
windowz.removeLast()
You should use View Controller instead of adding windows and pop it instead where you are removing the window.
Window is only one object for app and will contain the views.
Please correct your understanding and use View controllers.

Going to the previous scene SpriteKit

I'm making a game. On the main menu I have a button for "Start Game" "High Scores" "Options" "Store".
I have various scenes for the different levels. Each of those scenes has a "pause button" which takes you to the "Options" scene. The Options scene has a "BACK" button.
How do I get that "BACK" button to return to what have scene called it?
Right now the "BACK" button only has the ability to return to the menu ie SceneSkip in my code, regardless of what scene called it:
func back(){
let sceneSkip = SceneSkip(fileNamed: "SceneSkip")
sceneSkip?.scaleMode = .aspectFill
self.view?.presentScene(sceneSkip!, transition: SKTransition.fade(withDuration: 0.5))
}
So I've been working on a game in my spare time and I have solved pretty much exactly what you're doing. Here are a couple things I learned in the process.
1) When you present a scene, your scene's didMove(to: view) method is going to be called. Usually you do your initialization in that method so if you are just pausing, and you present the scene again, you're likely going to mess up the game state.
2) Because of number 1, you can't really present a new scene each time the user wants to pause.
So... the solution I've been using, which is the result of a number of google searches, experimenting and etc, is to actually use a separate view for all my popup/pause type scenes. I set it up like this in my GameViewController viewWillLayoutSubviews where skView is the main view:
let psize = view.bounds
popup = SKView.init(frame: psize)
skView.addSubview(popup!)
popup?.allowsTransparency=true
popup?.isHidden=true
Then, later when any scene anywhere in the game wants to add a popup, I added the following functions:
func showPopupScene(_ scene : SKScene) {
(self.view as? SKView)?.scene?.isPaused = true
popup?.isHidden=false
popup?.presentScene(scene)
}
func closePopup() {
popup?.isHidden=true
if let v=view as? SKView {
(v.scene as? PopupNotify)?.onPopupClosed()
v.scene?.isPaused=false
}
}
Now, any scene can create a scene, and show it as a popup with showPopupScene. The popup scene then needs to call closePopup and the game returns to the scene where it left off.
Other items:
I wanted my game scenes to behave correctly with pausing popups as well as when they are paused from coming out of background etc... so I override isPaused:
override var isPaused : Bool {
get {
guard let v = self.view?.window?.rootViewController as? GameViewController else {
return super.isPaused
}
guard let p = v.popup else { return super.isPaused }
return super.isPaused || !p.isHidden
}
set(newPaused) {
super.isPaused = newPaused
physicsWorld.speed = newPaused ? 0 : 1.0
}
}
Also, the PopupNotify protocol for my scenes helped where I wanted scenes to be aware that the popup was closed in case they needed to make any changes according to whatever the popup was showing.
Hope I'm not forgetting anything, but the combination of these additions has provided pretty easy popup/pause management.
I think that your are going about "levels" in a difficult way.
What I suggest you do is...
Have a scene for your menu which transitions to your GameScene.
In your GameScene you load all generic objects that are common to the game regardless of the level (such as gameHud, score labels, pause buttons, options buttons etc.)
you then load your level scene into GameScene based on a parameter (levelID).
this way none of your generic objects have to be created in multiple scenes greatly reducing the chance of redundancy errors.
Completely optional portion
I don't like to switch scenes in the middle of a game for pausing or options. it creates to many opportunities for something to go wrong (failing to load data, failing to save locations, score, animations etc.).
I create my pause and option dialogs as SKSpriteNodes and pause the game and display the dialog over top of the GameScene. That way when I am done with the dialog I can just remove it, and unpause the game and everything goes back to how it was without having to load the scene all over.

Programmatic beginRefreshing() on iOS11 has problems with largeTitles mode

We have found what seems to be a bug in UIKit but wanted to post here to see if anyone else has this problem or found a solution.
We're trying to use the new iOS11 large titles and hoisted search bar/refreshcontrol. We seemed to have found a problem where the root viewController of the navigation stack shows a minor display issue (problem A) but once another viewcontroller is pushed onto the navigation stack, the display goes nuts (problem B):
Things to note:
The problem is worse on the 2nd VC in the stack rather than the 1st
The refreshControl is not the green color the code sets it to the 1st time you see it on each sceen
The refreshControl slides down as you pull to refresh, it shouldn't do this
This odd behavior seems to only be a problem when we programmatically do a "pull to refresh" in viewDidLoad so that the user can see that the data is loading when they enter the screen. If we remove the lines that invoke refreshControl?.beginRefreshing() the display is clean. I've recreated this problem in a sample vanilla app. This is the entirety of the viewcontroller that shows the problem:
import UIKit
class ViewController: UITableViewController {
var tableHeaderSearchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.navigationController?.navigationBar.prefersLargeTitles = true
self.navigationController?.navigationItem.largeTitleDisplayMode = .always
tableHeaderSearchController = UISearchController(searchResultsController: UITableViewController())
navigationItem.searchController = tableHeaderSearchController
refreshControl?.tintColor = UIColor.green
refreshControl?.backgroundColor = UIColor.clear
refreshControl?.attributedTitle = NSAttributedString(string: "Loading Stuff...", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17)])
refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
// Commenting out these 2 lines makes it work fine but you can't see the initial refresh spinner
refreshControl?.beginRefreshing()
refreshPulled()
}
#objc func refreshPulled() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [refreshControl] in
refreshControl?.endRefreshing()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here's the storyboard. It's just a vanilla tableviewcontroller wrapped in a navigationController. 3 static cells, the 2nd one traverses to another instance of the same controller type.
Any ideas would be greatly appreciated. We'd really like to adopt the new look but this stuff is making it very hard to do so.
First, it is absolutely crucial that the table view extend up underneath the navigation bar and that is iOS 11 offset behavior be correct:
self.edgesForExtendedLayout = .all
self.tableView.contentInsetAdjustmentBehavior = .always
Second, scrolling to show the refresh control when you refresh manually is up to you, and calculating the amount is not at all simple:
self.refreshControl!.sizeToFit()
let top = self.tableView.adjustedContentInset.top
let y = self.refreshControl!.frame.maxY + top
self.tableView.setContentOffset(CGPoint(0, -y), animated:true)
self.refreshControl!.beginRefreshing()
The bar still stays too big during the refresh, but I don't see what can be done about that. Basically Apple has implemented large titles and shown the refresh control in the nav bar without thinking through the effects or dealing with the resulting bugs.