Is it possible to use Xcode UI Testing on an app using SpriteKit? - swift

I want to use UI testing for a game using SKSpriteKit.
As my first tries did not work I wonder if it possible to use Xcode UI Testing with SpriteKit.

The main idea is to create the accessibility material for elements that you want to UI test. That's mean:
List all accessible elements contained in the scene
Configure settings for each of these elements, especially framedata.
Step by Step
This answer is for Swift 3 and is mainly based on Accessibility (Voice Over) with Sprite Kit
Let's say I want to make the SpriteKit button named tapMe accessible.
List of accessible elements.
Add an array of UIAccessibilityElementto the Scene.
var accessibleElements: [UIAccessibilityElement] = []
Scene's cycle life
I need to update two methods: didMove(to:)and willMove(from:).
override func didMove(to view: SKView) {
isAccessibilityElement = false
tapMe.isAccessibilityElement = true
}
As scene is the accessibility controller, documentation stated it must return False to isAccessibilityElement.
And:
override func willMove(from view: SKView) {
accessibleElements.removeAll()
}
Override UIAccessibilityContainer methods
3 methods are involved: accessibilityElementCount(), accessibilityElement(at index:) and index(ofAccessibilityElement. Please allow me to introduce an initAccessibility() method I'll describe later.
override func accessibilityElementCount() -> Int {
initAccessibility()
return accessibleElements.count
}
override func accessibilityElement(at index: Int) -> Any? {
initAccessibility()
if (index < accessibleElements.count) {
return accessibleElements[index]
} else {
return nil
}
}
override func index(ofAccessibilityElement element: Any) -> Int {
initAccessibility()
return accessibleElements.index(of: element as! UIAccessibilityElement)!
}
Initialize accessibility for the Scene
func initAccessibility() {
if accessibleElements.count == 0 {
// 1.
let elementForTapMe = UIAccessibilityElement(accessibilityContainer: self.view!)
// 2.
var frameForTapMe = tapMe.frame
// From Scene to View
frameForTapMe.origin = (view?.convert(frameForTapMe.origin, from: self))!
// Don't forget origins are different for SpriteKit and UIKit:
// - SpriteKit is bottom/left
// - UIKit is top/left
// y
// ┌────┐ ▲
// │ │ │ x
// ◉────┘ └──▶
//
// x
// ◉────┐ ┌──▶
// │ │ │
// └────┘ y ▼
//
// Thus before the following conversion, origin value indicate the bottom/left edge of the frame.
// We then need to move it to top/left by retrieving the height of the frame.
//
frameForTapMe.origin.y = frameForTapMe.origin.y - frameForTapMe.size.height
// 3.
elementForTapMe.accessibilityLabel = "tap Me"
elementForTapMe.accessibilityFrame = frameForTapMe
elementForTapMe.accessibilityTraits = UIAccessibilityTraitButton
// 4.
accessibleElements.append(elementForTapMe)
}
}
Create UIAccessibilityElement for tapMe
Compute frame data on device's coordinates. Don't forget that frame's origin is the top/left corner for UIKit
Set data for UIAccessibilityElement
Add this UIAccessibilityElement to list of all accessible elements in scene.
Now tapMe is accessible from UI testing perspective.
References
Session 406, UI Testing in Xcode, WWDC 2015
eyes off eyes on — Voiceover accessibility in SpriteKit
How do I support VoiceOver in a SpriteKit game? | Apple Developer Forums
swift - Accessibility (Voice Over) with Sprite Kit - Stack Overflow

According to Apple developer forum discussion, Integrate UITest with SpriteKit is not currently possible:
This is likely not currently possible, but it probably could be
Update 2017-02-19
According to comment by #ChrisLivdahl this may be achieved by using UIAccessibility — Session 406, UI Testing in Xcode, WWDC 2015.
The idea is to make the element needed UI Testable.

Update early 2021
A much shorter solution tested with SpriteKit, SwiftUI App and Swift 5.4
The earlier approaches didn't seem to work anymore when using XCUI Tests. The basis of my app is a SwiftUI app that has a SKScene as its main view. In order for it to work it was at the end quite simple actually and required much less steps for me to work.
1. Deactivate accessibility of the scene by adding only one line to the didMove() method
override func didMove(to view: SKView) {
isAccessibilityElement = false
}
As mentioned in Dominique Vial's answer
2. Conforming your nodes to UIAccessibilityIdentification protocol
class YourNode: SKSpriteNode, UIAccessibilityIdentification {
var accessibilityIdentifier: String?
//...
}
Mentioned here
Any node that needs to be accessible by the UI Test needs to conform to this protocol. Update Extension, instead of subclassing every node I'm using now the extension below.
3. Assign the accessibilityIdentifier and activate accessibility of your nodes objects.
let yourNode = YourNode()
yourNode.isAccessibilityElement = true
yourNode.accessibilityIdentifier = "nodeID"
4. That's it! Run your tests!
func testingNodes() throws {
app = XCUIApplication()
app.launch()
let node = app.otherElements["nodeID"]
XCTAssert(node.waitForExistence(timeout: 1))
}
5. Optional: set accessibilityTraits
yourNode.accessibilityTraits = [.button, .updatesFrequently]
let nodeAsButton = app.buttons["nodeID"]
You can set specific trait like .button to tell the accessibility API what your node is. This is useful to differentiate your nodes better during testing but also if you're planning to implement actual accessibility features for the user than this should be set correctly for it to work.
Update 1 - Using Extensions:
Instead of subclassing the nodes I'm using now the following extension on SKNode. I now only set the accessibilityLabel an no longer the accessibilityIdentifier
extension SKNode: UIAccessibilityIdentification {
public var accessibilityIdentifier: String? {
get {
super.accessibilityLabel
}
set(accessibilityIdentifier) {
super.accessibilityLabel = accessibilityIdentifier
}
}
}
Update 2 - Making all descendants of SKScene accessible
To allow for nodes and all of their descendants to be accessible I've ended up using the following extension on SKNode. This adds each node to the scene's accessibilityElements.
extension SKNode: UIAccessibilityIdentification {
public var accessibilityIdentifier: String? {
get {
super.accessibilityLabel
}
set(accessibilityIdentifier) {
super.accessibilityLabel = accessibilityIdentifier
}
}
func makeUITestAccessible(label: String, traits: UIAccessibilityTraits) {
accessibilityLabel = label
isAccessibilityElement = true
accessibilityTraits = traits
if let scene = scene {
if scene.accessibilityElements == nil {
scene.accessibilityElements = [self]
} else {
scene.accessibilityElements?.append(self)
}
}
}
}

I've implemented an example project based on your (#Domsware's) awesome answer, and I've confirmed this trick works well for both Xcode UI Testing Framework and KIF.
Hope this example helps for anyone who is interested in this topic :)

Related

Xcode warning: 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead

when updating my App's Deployment target to 15.0, i receive this warning:
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a
relevant window scene instead
I have tried to look on the net what could be done to remediate this, but couldn't find much info on this. Hope you could share some advice.
The line of code i am using where this alert occurred is:
let window = UIApplication.shared.windows[0]
followed by in my ViewDidLoad():
DispatchQueue.main.async { [weak self] in
if defaults.bool(forKey: "darkModeBoolSwitch") == true {
self?.window.overrideUserInterfaceStyle = .dark
} else if defaults.bool(forKey: "darkModeBoolSwitch") == false {
self?.window.overrideUserInterfaceStyle = .light
}
An alternative to #DuncanC's solution that may also work for you: UIApplication has a connectedScenes property, which lists all of the currently-active scenes doing work in your application (for most applications, this is just the one main scene).
Of those scenes, you can filter for scenes which are UIWindowScenes (ignoring scenes which are not currently active and in the foreground), and of those, find the first scene which has a window which is key:
extension UIApplication {
static var firstKeyWindowForConnectedScenes: UIWindow? {
UIApplication.shared
// Of all connected scenes...
.connectedScenes.lazy
// ... grab all foreground active window scenes ...
.compactMap { $0.activationState == .foregroundActive ? ($0 as? UIWindowScene) : nil }
// ... finding the first one which has a key window ...
.first(where: { $0.keyWindow != nil })?
// ... and return that window.
.keyWindow
}
}
I hesitate to call this extension something like UIApplication.keyWindow because the reason for the deprecation of these APIs is because of the generalization to multi-scene applications, each of which may have its own key window... But this should work.
If you still need to support iOS 14, which does not have UIWindowScene.keyWindow, you can replace the firstWhere(...)?.keyWindow with: flatMap(\.windows).first(where: \.isKeyWindow).
I am out-of-date with Apple's recent changes to implement scenes.
I did a little digging, and found a protocol UIWindowSceneDelegate
It looks like you are supposed to add an "Application Scene Manifest" to your app's info.plist file that tells the system the class that serves as the app's window scene delegate.
Then in that class you want to implement the method scene(_:willConnectTo:options:). When that method is called you sould try to cast the UIScene that's passed to you to to a UIWindowScene, and if that cast succeeds, you can ask the window scene for it's window and save it to an instance property.
That should allow you to save a pointer to your app's window and use it when needed.

How to unit test a textView using a spy

I have a cell that contains a textView and i would like to test that the properties of that textView are set correctly using unit tests. However i seem to have hit a blocker when it comes to having access to the textView in the test since its private.
Is there a way I can test my textView:-
Here is my code
class MyCell {
private let myText: UITextView = {
let textView = UITextView()
textView.isScrollEnabled = false
textView.isEditable = false
return textView
}()
func setup(viewModel: MYViewModel) {
if viewModel.someValue {
myText.backgroundColor = UIColor.red
} else {
myText.backgroundColor = .clear
}
}
}
Is it possible to test something like the testView's background color is set to clear ?
Unless you want to relax the access level of myText from private to internal (the default if you don't specify it), there is no direct way to test it.
The only suggestion I have to test this indirectly would be to use snapshot testing.
You could write two snapshot tests, one for each value of .someValue from your MYViewModel.
Another option to make the testing –and maintainability– of your view straightforward is to introduce a ViewConfiguration value type, following the humble view pattern.
Basically, you can have a struct in between MYViewModel and MyCell that describes each of the view properties for MyCell.
struct MyCellViewConfiguration {
let textFieldBackgroundColor: UIColor
}
extension MYViewModel {
var viewConfiguration: MyCellViewConfiguration = {
return MyCellViewConfiguration(
textFieldBackgroundColor: someValue ? .red : .clear
)
}
}
extension MyCell {
func setup(with configuration: MyCellViewConfiguration) {
myText.backgroundColor = configuration.textFieldBackgroundColor
}
}
The code in setup(with configuration: MyCellViewConfiguration) is so simple –just a 1-to-1 assignment– that you can get away without testing it.
You can then write test for how MyCellViewConfiguration is computed from MYViewModel.
Simply remove private from your declaration. Then the access control will be the default internal. In the test code, make sure to #testable import so gain access to internal features.
Unit testing a few attributes isn't hard. But if you want a test that records the appearance, look into snapshot testing. This can be done without XCUITestCase. It's an order of magnitude slower than a regular unit test, but probably another order of magnitude faster than a UI test.

Could not cast value of type 'SKSpriteNode' to myApp.CustomSprite

I'm new to Swift and SpriteKit so please bear with me.
I'm trying to creating a simple game, I made a custom class (CustomSprite):
import UIKit
import SpriteKit
class CustomSprite: SKSpriteNode {
var spriteColor: String = ""
}
CustomSprite has spriteColor for checking reasons only (later in the game)
I'm trying to make a new sprite in the GameScene:
var mPlayer: CustomSprite = CustomSprite()
override func didMove(to view: SKView) {
//crashes here (mPlayer = self. xxxxxx)
mPlayer = self.childNode(withName: "mainPlayer") as! CustomSprite
mPlayer.spriteColor = "red"
}
I get this error when running the game and getting to didMove:
Could not cast value of type 'SKSpriteNode' to 'myApp.CustomSprite'
I don't understand what I have to do in order to fix it, and why this is happening.
I did set the Custom Class in the .sks file to CustomSprite and saved, but it didn't changed or fixed the error.
You could try to substitute your code with an optional binding:
if let sprite = self.childNode(withName: "//mainPlayer") as? CustomSprite {
mPlayer = sprite
// do whatever you want with your mPlayer
}
The "//" before mainPlayer specifies that the search should begin at the root node and be performed recursively across the entire node tree. Otherwise, it performs a recursive search from its current position. You can find more details here

How do I use a switch on Xcode viewcontroller?

I have read a book on Swift logic but I don't know how to use it with an interface to make an app in xCode 8. I'm making a very basic app where you have a currency (coins) and when you click a button (goldPan) it checks to see what upgrade the goldPan is on and depending on that upgrade the user gets so many coins, basically like the game cookie clicker. Instead of writing a ton of "if" statements I decided I have built a "switch" to check what upgrade the goldPan is on. Also, the goldPan has its own class. Im more of asking the question of "is Switch the way to go to check the upgrade? and if so how do I implement it with my code. Like I said, Im not sure if it's the most efficient way but when I do use this, Im getting the error of
Use of instance member 'checkUpgradeNumber' on type 'ViewController.goldPan'; did you mean to use a value of type 'ViewController.goldPan' instead?
and
Instance member 'production' cannot be used on type 'ViewController.goldPan'
Please help me, Im not sure if the switch is the best way to go or something else. Sorry for the very noob question. My code is below:
import UIKit
class ViewController: UIViewController {
#IBOutlet var topLabel: UILabel!
var coins = 0;
class goldPan {
var upgradeNumber = 0
var production = 1
func checkUpgradeNumber() {
switch upgradeNumber {
case 0:
production = 1;
case 1:
production = 2;
case 2:
production = 3;
default:
production = 1;
}
}
}
#IBAction func goldPanButton(_ sender: Any) {
goldPan.checkUpgradeNumber();
coins = coins + goldPan.production
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
First of all please consider the naming convention that class names start with capital letter (GoldPan).
You are calling a method and accessing a property on the class which is not possible with that code.
Either make the method and the properties static
...
static var upgradeNumber = 0
static var production = 1
class func checkUpgradeNumber() { ...
or create an instance of the class
let goldPan = Goldpan()
goldPan.checkUpgradeNumber()
coins = coins + goldPan.production
But basically I suppose you don't need to use a separate class for that. Remove the inner class block and use the method and the properties in the ViewController class.
Change class goldPan to func goldPan
I don't think you can have a class within a class. This may not fix the whole problem, but it might fix part of it. I'm not completely sure on this, but I just noticed that you are using a class instead of a function.
You can replace:
switch upgradeNumber {
case 0:
production = 1;
case 1:
production = 2;
case 2:
production = 3;
default:
production = 1;
}
With:
production = upgradeNumber + 1

basic spritebuilder and swift

I m trying to create a game using sprite builder and swift. In the game the player can change, for example the shoes of a baby in center of the screen and in the left of the screen there are different kinds of shoes wich the player can choose from.
In spritebuilder i put 3 color nodes with names of colorNode , colornode1, centroNode.
I would like to put the colornode in the position of centronode, one by one.
Thanks
Here there is the code i've Tried to write :
class MainScene: CCNode {
let colorNode : CCNodeColor!
let colorNode1 : CCNodeColor!
weak var centroNode : CCNodeColor!
init (centroNode:CCNodeColor){
self.centroNode=centroNode
}
override func onEnter() {
super.onEnter()
self.userInteractionEnabled = true
}
override func touchBegan(touch: CCTouch!, withEvent event: CCTouchEvent!) {
let touchPosition = touch.locationInNode(self)
if(CGRectContainsPoint(colorNode.boundingBox(), touchPosition))
{
print("entratooo")
centroNode=colorNode
print("centroooo")
let move = CCActionMoveTo(duration:1.0, position:ccp(20, 100))
centroNode.runAction(move)
}
else {print ("toccooo")}
if(CGRectContainsPoint(colorNode1.boundingBox(), touchPosition))
{
print("entratooo22")
Given the provided error message
MIOGIOCO43.spritebuilder/Source/MainScene.swift: 3: 7: fatal error: use of unimplemented initializer 'init()' for class 'MIOGIOCO43.MainScene' (lldb)
It looks like you just need to add an initializer with 0 params to your class
init () {
// init your properties here
}