How to hide game's state when going to background - ios

I am writing a logic puzzle game in SpriteKit that plays on the iPad and is against the clock, and am struggling to hide the puzzle neatly when the app goes into the background.
The issue is that user shouldn't be able to double-tap on the home button and see the full puzzle in the App Switcher, as this would allow them to work through it without the clock running.
This is the solution I have come up with:
In the Singleton GameManager there is a variable that is a SKTexture(), to hold a screenshot texture, and in my AppDelegate I have:
func applicationWillResignActive(_ application: UIApplication) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "resigning"), object: self)
let tempBackground = UIImageView(image: UIImage(cgImage: GameManager.shared.puzzleImage.cgImage()))
tempBackground.frame = CGRect(x: 0, y: 0, width: 1024, height: 768)
tempBackground.tag = 1000
self.window?.addSubview(tempBackground)
self.window?.bringSubviewToFront(tempBackground)
}
This sends out a notification when it's about to resign the focus, which is picked up by my GameScene, which then creates a screenshot and stores it to the variable in my GameManager. This is then added as a subview in the AppDelegate.
class GameScene: SKScene {
override func didMove(to view: SKView) {
// Layout puzzle here
NotificationCenter.default.addObserver(self, selector: #selector(appToBackground), name: NSNotification.Name(rawValue: "resigning") , object: nil)
}
#objc private func appToBackground() {
// Save state of puzzle and hide it
GameManager.shared.puzzleImage = SKView().texture(from: self)!
}
}
This all works. But not brilliantly. There is a noticeable time-lag between double-tapping and the tempBackground being added - the App Switcher shows the puzzle in detail very briefly and then changes the image to the hidden puzzle.
When the app comes back into focus, the following is called in AppDelegate:
func applicationDidBecomeActive(_ application: UIApplication) {
if let tempBackground = self.window?.viewWithTag(1000) {
tempBackground.removeFromSuperview()
}
}
But when the app returns to focus, the tempBackground is shown, then there is a very brief glimpse of the puzzle in all its detail, before once again showing the hidden puzzle.
I may well have gone about this completely the wrong way, but after reading various archive questions and articles on the internet, this seemed to be way to go.
What I'd like to know is: is there any way that I can have the hidden puzzle shown in the App Switcher immediately and avoid the flash of puzzle detail when returning to the puzzle?
Thanks.

There is no issue in adding subview logic, but the issue is in where are you triggering it, as per apple docs, you should add subview in applicationDidEnterBackground and should remove you subview and prepare your app to display in applicationWillEnterForeground
As I have created Xcode project in Xcode 12, my project has scene delegate, here is the code I used and O/P is shown in gif below
func sceneWillResignActive(_ scene: UIScene) {
guard let view = Bundle.main.loadNibNamed("HiddenView", owner: nil, options: [:])?[0] as? HiddenView else { return }
view.tag = 1000
view.frame = UIApplication.shared.windows[0].frame
UIApplication.shared.windows[0].addSubview(view)
UIApplication.shared.windows[0].bringSubviewToFront(view)
}
func sceneWillEnterForeground(_ scene: UIScene) {
if let tempBackground = UIApplication.shared.windows[0].viewWithTag(1000) {
tempBackground.removeFromSuperview()
}
}
EDIT 1:
As OP has mentioned, that he is not using Scene Delegate I am updating the answer for AppDelegate
func applicationWillResignActive(_ application: UIApplication) {
guard let view = Bundle.main.loadNibNamed("HiddenView", owner: nil, options: [:])?[0] as? HiddenView else { return }
view.tag = 1000
view.frame = UIApplication.shared.windows[0].frame
UIApplication.shared.windows[0].addSubview(view)
UIApplication.shared.windows[0].bringSubviewToFront(view)
}
func applicationDidBecomeActive(_ application: UIApplication) {
if let tempBackground = UIApplication.shared.windows[0].viewWithTag(1000) {
tempBackground.removeFromSuperview()
}
}

I have a solution, although it feels more like a work-around than a proper solution.
The problem is that iOS takes a snapshot of the screen before sending the app into the background and uses this when the app returns to the foreground.
This snapshot can be replaced by using either a predefined view (see #Sandeep's solution) or creating a new snapshot of the screen once the puzzle has been hidden (my original attempted solution).
However, removing this dummy in the applicationDidBecomeActive means that the original snapshot taken by the iOS is shown briefly before updating the screen.
There is a function UIApplication.shared.ignoreSnapshotOnNextApplicationLaunch() which can be entered into the applicationWillResignActive() function, but it doesn't seem to be working all the time - just most of it. In fact, on the Apple Developer's forum there's a comment about how it's inconsistent and a bug was reported. But that was over a year ago and it's still inconsistent.
Instead, in this function add the dummy not to the self.window? directly, but to the view of the rootViewController, using
self.window?.rootViewController!.view.addSubview(dummyImage)
Then don't actually remove this when the app comes back into the foreground. Instead, use Notification to alert GameScene that the app is back in the foreground. This can then run a method which adds a UITapGestureRecognizer to the scene. This in turn can be linked to a method that fades out the dummy image, before removing both it and the UITapGestureRecognizer once it's completed.
Perhaps not the most elegant solution, but it seems to work.

Related

Move keyboard above TabViewController TabBar

Is it possible to move the keyboard up so it doesn't cover the UITabViewController's TabBar?
Update after being given more context in comments
If your main concern is letting the user dismiss the keyboard, there are some well known patterns that are commonly applied on the platform:
Assumption regarding UI (derived from your comment):
- UITableView as main content
To make the keyboard dismissible, you can utilise a property on UIScrollView called .keyboardDismissMode. (UITableView is derived from UIScrollView, so it inherits the property.)
The default value for this property is .none. Change that to either .onDrag or .interactive. Consult the documentation for differences between the latter two options.
Behind the scenes, UIKit sets up a connection between the UIScrollView instance and any incoming keyboard. This allows the user to "swipe away" the keyboard by interacting with the scroll view.
Note that in order for this feature to work, your UIScrollView needs to be scrollable. To understand what 'scrollable' means in this context, please see this gist.
If your tableView has very few or no rows, it is likely not natively scrollable. To account for that, set tableView.alwaysBounceVertical = true. This will make sure your users can dismiss the keyboard regardless of the number of rows in the table.
Most of the popular apps handling keyboard dismissal also make it possible to dismiss the keyboard simply by tapping the content partially overlapped by it (in your case, the tableView). To enable this, you would simply have to install a UITapGestureRecognizer on your view and dismiss the keyboard in its action method:
class MyViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapRecognizer)
}
}
//MARK: - Tap handling
fileprivate extension MyViewController {
#objc func handleTap() {
if searchBar.isFirstResponder {
searchBar.resignFirstResponder()
}
// Alternative
// view.endEditing(true)
}
}
// -
Old answer
Yes, you can actually do this without using private API.
Disclaimer
You should really think about whether you actually want to do this. Opening the keyboard in virtually every use case should create a new "context" of editing which modally "blocks" other contexts (such as the navigation context provided by UITabBarController and its UITabBar). I guess one could make the point that users are able to leave an editing context by interacting with a potentially present UINavigationBar which is usually not blocked by keyboards. However, this is a known interaction throughout the system. Not blocking a UITabBar or UIToolbar while showing the keyboard on the other hand, is not. That being said, use the code below to move the keyboard up, but critically review the UX you are creating. I'm not to say it does never make sense to move the keyboard up, but you should really know what you're doing here. To be honest, it also looks kind of iffy, having the keyboard float above the tab bar.
Code
extension Sequence {
func last(where predicate: (Element) throws -> Bool) rethrows -> Element? {
return try reversed().first(where: predicate)
}
}
// Using `UIViewController` as an example. You could and actually should factor this logic out.
class MyViewController: UIViewController {
deinit {
NotificationCenter.default.removeObserver(self)
}
func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: .UIKeyboardWillHide, object: nil)
}
}
//MARK: - Keyboard handling
extension MyViewController {
private var keyboardOffset: CGFloat {
// Using a fixed value of `49` here, since that's what `UITabBar`s height usually is.
// You should probably use something like `-tabBarController?.tabBar.frame.height`.
return -49
}
private var keyboardWindowPredicate: (UIWindow) -> Bool {
return { $0.windowLevel > UIWindowLevelNormal }
}
private var keyboardWindow: UIWindow? {
return UIApplication.shared.windows.last(where: keyboardWindowPredicate)
}
#objc fileprivate func keyboardWillShow(notification: Notification) {
if let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = keyboardOffset
}
}
#objc fileprivate func keyboardWillHide(notification: Notification) {
if let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = 0
}
}
}
// -
Caution
Note that if you are using the .UIKeyboardWillShow and .UIKeyboardWillHide notifications to account for the keyboard in your view (setting UIScrollView insets, for example), you would have to also account for any additional offset by which you move keyboard window.
This works and is tested with iOS 11. However, there is no guarantee that the UIKit team won't change the order of windows or something else that breaks this in future releases. Again, you are not using any private API, so AppStore review should not be in danger, but you are doing something that you're not really supposed to do with the framework, and that can always come around and bite you later on.

iOS - keep layout in landscape but change controls

I'm done with the auto-layout stuff in my iOS universal App, and it's working perfectly in portrait. However, I want the user to be able to rotate the device and play the game in landscape mode. The problem I'm facing is that I don't want the layout to change at all, and only change the controls of the game (sliding up the screen should make the player go up in both orientations).
Thing is, I don't know how to prevent orientation from changing the layout and at the same time be able to change behaviour based on the orientation. Do you guys have any idea how I could manage that?
Did found a way to do, for future reference, when an orientation is disabled, we still can access device orientation (and not interface orientation), and register a notification to act upon change.
class ViewController: UIViewController {
var currentOrientation = 0
override func viewDidLoad() {
super.viewDidLoad()
// Register for notification about device orientation change
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate(notification:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
// Remove observer on window disappears
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self)
if UIDevice.current.isGeneratingDeviceOrientationNotifications {
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
}
// That part gets fired on orientation change, and I ignore states 0 - 5 - 6, respectively Unknown, flat up facing and down facing.
func deviceDidRotate(notification: NSNotification) {
if (UIDevice.current.orientation.rawValue < 5 && UIDevice.current.orientation.rawValue > 0) {
self.currentOrientation = UIDevice.current.orientation.rawValue
}
}
}

Keep SpriteKit scene paused after app becomes active

When returning to my App after closing it the applicationDidBecomeActive(application: UIApplication) automatically fires in AppDelegate.swift.
This fires a method that handles the paused status of the app:
GameViewController().pause(true)
The method looks like this:
func pause(paused: Bool) {
if paused == true {
scene?.paused = true
print("paused")
} else if paused == false {
scene?.paused = false
print("unparsed")
}
}
When first launching the app the Game is automatically paused which is exactly what should happen. When returning to the app it unpauses though. Still, the Console prints "paused".
I have also tried using scene?.view?.paused instead of scene?.paused. This does work, but leads to lag in the animations running on the scene.
Any help would be highly appreciated
EDIT
I managed to solve the problem by calling the pause() method in the update(currentTime: NSTimeInterval) function but I don't like this solution as it means the method is called once per frame. Other solutions would be highly appreciated
This code makes no sense
GameViewController().pause(true)
because you are creating a new instance of GameViewController rather than accessing the current one.
Rather than pausing the whole scene you should just pause the nodes that you would liked paused. Usually you create some kind of worldNode in your game scene (Apple also does this in DemoBots)
class GameScene: SKScene {
let worldNode = SKNode()
// state machine, simple bool example in this case
var isPaused = false
....
}
than add it to the scene in DidMoveToView
override func didMoveToView(view: SKView) {
addChild(worldNode)
}
Than all nodes that you need paused you add to the worldNode
worldNode.addChild(YOURNODE1)
worldNode.addChild(YOURNODE2)
Than your pause function should look like this
func pause() {
worldNode.paused = true
physicsWorld.speed = 0
isPaused = true
}
and resume like this
func resume() {
worldNode.paused = false
physicsWorld.speed = 1
isPaused = false
}
Lastly to make sure the game is always paused when in paused add this to your update method. This ensures that you game does not resume by accident e.g due to app delegate, iOS alerts etc.
override func update(currentTime: CFTimeInterval) {
if isPaused {
worldNode.paused = true
physicsWord.speed = 0
return
}
// Your update code
...
}
To call these from your AppDelegate you should use delegation or NSNotificationCenter as has been mentioned in one of the comments.
In gameScene create the NSNotifcationObserver in didMoveToView
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(pause), name: "Pause", object: nil) // in your app put the name string into a global constant property to avoid typos when posting
and in appDelegate post it at the correct spot
NSNotificationCenter.defaultCenter().postNotificationName("Pause", object: nil)
The main benefit with the worldNode approach is that you can easily add pause menu sprites etc while the actual game is paused. You also have more control over your game, e.g having the background still be animated while game is paused.
Hope this helps.

How to let user customize LaunchScreen [duplicate]

Xcode allows to create launch screen in .xib files via Interface Builder. Is it possible to execute some code with the xib, just like in usual view controllers? It would be great if we can set different text/images/etc while app launching.
No, it's not possible.
When launch screen is being displayed your app will be in loading state.
Even the - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions will not be completely executed while the launch screen is displayed.
So it's clear that, you don't have any access to your app and so at this point you can't execute any code.
I was trying to do the same thing here. :)
I really liked some of the apps, in which they do a little dynamic greeting text and image each time the app is launched, such as "You look good today!", "Today is Friday, a wonderful day", etc, which is very cute.
I did some search, below is how to do it:
(My code is XCode 7, with a launchscreen.xib file)
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var customizedLaunchScreenView: UIView?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
application.statusBarHidden = true
// customized launch screen
if let window = self.window {
self.customizedLaunchScreenView = UIView(frame: window.bounds)
self.customizedLaunchScreenView?.backgroundColor = UIColor.greenColor()
self.window?.makeKeyAndVisible()
self.window?.addSubview(self.customizedLaunchScreenView!)
self.window?.bringSubviewToFront(self.customizedLaunchScreenView!)
UIView.animateWithDuration(1, delay: 2, options: .CurveEaseOut,
animations: { () -> Void in
self.customizedLaunchScreenView?.alpha = 0 },
completion: { _ in
self.customizedLaunchScreenView?.removeFromSuperview() })
}
return true
}
// other stuff ...
}
Just do what ever you wanted to show, text, images, animations, etc. inside the customizedLaunchScreenView here.
At the end of the launching, just fade out this customized UIView using alpha value change, then remove it completely.
How cool is that? I absolutely love it!
Hope it helps.
I was also trying to achieve this. I tried the following, it gives a delay of couple of seconds but works for me.
Create and set a launch screen in project settings.
Create a view controller with custom class (SplashViewController) and set it your starting view controller in storyboard.
Add a container view in it and set it to full screen.
Set embedded segue to a Storyboard Reference.
Select Storyboard Reference and set launch screen in StoryBoard property from Attribute inspector.
Do whatever you want in SplashViewController (play animation or session check etc) and perform segue when done.
Hope it helps!
Language: Swift 4
Hello, here's a solution for using xib for launch screen.
Adding new class named LaunchView to your project, then u'll have a new file LaunchView.swift in project.
Go to LauchImage.xib to set class to LaunchView (which it's your new class).
Adding some code, & reading some information show at the LaunchView.swift. Here's is my code for displaying version of app at launch screen.
class LaunchView: UIView {
lazy var versionLabel: UILabel = {
let label = Factory.label(style: LabelStyle.custom(font: .regular, size: 10, color: .other(ColorUtility.versionGray)), mutiLine: false, textAlignment: .center)
label.text = "ver \(Constants.appVersion)"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
nibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
nibSetup()
}
private func nibSetup() {
addSubview(versionLabel)
versionLabel.snp.makeConstraints { (make) in
make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).offset(-6)
make.centerX.equalToSuperview()
}
}
}

Execute code in Launch Screen

Xcode allows to create launch screen in .xib files via Interface Builder. Is it possible to execute some code with the xib, just like in usual view controllers? It would be great if we can set different text/images/etc while app launching.
No, it's not possible.
When launch screen is being displayed your app will be in loading state.
Even the - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions will not be completely executed while the launch screen is displayed.
So it's clear that, you don't have any access to your app and so at this point you can't execute any code.
I was trying to do the same thing here. :)
I really liked some of the apps, in which they do a little dynamic greeting text and image each time the app is launched, such as "You look good today!", "Today is Friday, a wonderful day", etc, which is very cute.
I did some search, below is how to do it:
(My code is XCode 7, with a launchscreen.xib file)
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var customizedLaunchScreenView: UIView?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
application.statusBarHidden = true
// customized launch screen
if let window = self.window {
self.customizedLaunchScreenView = UIView(frame: window.bounds)
self.customizedLaunchScreenView?.backgroundColor = UIColor.greenColor()
self.window?.makeKeyAndVisible()
self.window?.addSubview(self.customizedLaunchScreenView!)
self.window?.bringSubviewToFront(self.customizedLaunchScreenView!)
UIView.animateWithDuration(1, delay: 2, options: .CurveEaseOut,
animations: { () -> Void in
self.customizedLaunchScreenView?.alpha = 0 },
completion: { _ in
self.customizedLaunchScreenView?.removeFromSuperview() })
}
return true
}
// other stuff ...
}
Just do what ever you wanted to show, text, images, animations, etc. inside the customizedLaunchScreenView here.
At the end of the launching, just fade out this customized UIView using alpha value change, then remove it completely.
How cool is that? I absolutely love it!
Hope it helps.
I was also trying to achieve this. I tried the following, it gives a delay of couple of seconds but works for me.
Create and set a launch screen in project settings.
Create a view controller with custom class (SplashViewController) and set it your starting view controller in storyboard.
Add a container view in it and set it to full screen.
Set embedded segue to a Storyboard Reference.
Select Storyboard Reference and set launch screen in StoryBoard property from Attribute inspector.
Do whatever you want in SplashViewController (play animation or session check etc) and perform segue when done.
Hope it helps!
Language: Swift 4
Hello, here's a solution for using xib for launch screen.
Adding new class named LaunchView to your project, then u'll have a new file LaunchView.swift in project.
Go to LauchImage.xib to set class to LaunchView (which it's your new class).
Adding some code, & reading some information show at the LaunchView.swift. Here's is my code for displaying version of app at launch screen.
class LaunchView: UIView {
lazy var versionLabel: UILabel = {
let label = Factory.label(style: LabelStyle.custom(font: .regular, size: 10, color: .other(ColorUtility.versionGray)), mutiLine: false, textAlignment: .center)
label.text = "ver \(Constants.appVersion)"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
nibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
nibSetup()
}
private func nibSetup() {
addSubview(versionLabel)
versionLabel.snp.makeConstraints { (make) in
make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).offset(-6)
make.centerX.equalToSuperview()
}
}
}

Resources