XCUIElementQuery in SwiftUI testing returns inconsistent count - unit-testing

I'm doing UI testing of a SwiftUI view, and I have the following code to fetch a List element to check the count of staticTexts in it:
let pred = NSPredicate(format: "identifier == 'detailViewTable'")
let detailViewTable = app.descendants(matching: .any).matching(pred).firstMatch
let arrOfTexts = detailViewTable.staticTexts
let ct = arrOfTexts.count
print("Count is: \(ct)") // This prints 0
print("Count is now: \(detailViewTable.staticTexts.count)") // Also prints 0
The count always prints as 0.
The SwiftUI view basically looks like this:
List {
Section(header: Text("Item list")) {
HStack {
Text("Number")
Spacer()
Text(rec.recNum)
}
}
// Lots more Sections/HStacks/Texts/etc here
// ...
// ...
}
.accessibility(identifier: "detailViewTable")
.listStyle(.grouped)
When I put a breakpoint as shown,
and use po in the Debug Console, I get 0 for ct, but 21 if calling count directly:
(lldb) po ct
0
(lldb) po detailViewTable.staticTexts.count
21
Pic here:
Why is the var ct set to 0, but calling the count directly gives me the correct number 21?
It makes me wonder if the XCUIElementQuery takes a moment to run and returns the answer in some kind of implicit callback, and the data isn't ready at the moment the variable ct is set, but will return the correct answer in the debug console because it waits for the response. However, I don't see any discussion of a callback in the Apple documentation:
https://developer.apple.com/documentation/xctest/xcuielementquery

This answer
https://stackoverflow.com/a/37320452
pointed me in the right direction. The fix is to put a pause in the form of sleep(1), to give the query time to do its fetch, I assume.
let pred = NSPredicate(format: "identifier == 'detailViewTable'")
let detailViewTable = app.descendants(matching: .any).matching(pred).firstMatch
let arrOfTexts = detailViewTable.staticTexts
sleep(1) // <-- Add this
let ct = arrOfTexts.count
print("Count is: \(ct)") // Now prints 21
print("Count is now: \(detailViewTable.staticTexts.count)") // Also prints 21

Related

How do I save the results of a MKLocalSearch to an Array?

I am experiencing a bit of trouble, while working on my app in SwiftUI.
I want to append relevant data, summarized in an object, to an array and return this.
While returning the array, I could see by debugging, that it is empty. Debugging in the for loop showed me, that location objects are created and appended, but are not being "saved" in the array. The "mapItems" array on the other hand has lots of members.
What am I missing?
Here is the method I came up with:
func searchForLocation(searchTerm: String) -> Array<Location>{
var locations = [Location]
let searchReqeust = MKLocalSearch.Request()
searchRequest.region = region //region is a published variable and is determined before
searchRequest.naturalLanguageQuery = searchTerm
let search = MKLocalSearch(request: searchRequest)
search.start{response, error in
//error handling
.
.
.
//empty response.mapItems handling
.
.
.
for item in response!mapItems{
let location = createLocationFromItem(item: item)
locations.append(location)
}
}
return locations
}
My locations class if following:
class Location: Identifiable{
var id= UUID()
var coordinates: CLLocationCoordinate2d
//And its proper init method
}
Your searchForLocation has an asynchronous function inside (search.start{...}),
and your code returns before it has finished getting the results.
To "wait" for the results use a completion/closure handler,
something like this:
func searchForLocation(searchTerm: String, completion: #escaping ([Location]) -> ()) {
var locations = [Location]() // <-- here
// ....
search.start{response, error in // <-- here asynchronous function
//... todo deal with errors, eg return completion([])
for item in response!mapItems {
let location = createLocationFromItem(item: item)
locations.append(location)
}
completion(locations) // <- here return when finished
}
}
and call the function like this:
searchForLocation(searchTerm: "Tokyo") { results in
print("\(results)") // <-- here results available, not before
}
I suggest you read-up on how to create and use asynchronous functions, these are important concepts to master to code effectively in Swift.

AUDIOKIT - Why is the AKOscillatorBank RELASE, DECAY, SUSTAIN, and ATTACK being updated long after I change their values?

I have a simple AudioKit app in the works.
I initialize an AKOsicillatorBANK at runtime and allow you to press a button to access a new viewCONTROLLER which has SLIDERS that when changed change the values of the oscillatorBANK properties [releaseDURATION, attackDURATION, decayDURATION, sustainLEVEL].
At the end of the slider's PANGESTURE [state == ended], I call a function that sets the oscillatorBANKS properties to their new values, which are stored in class variables.
I do not know why, but the changes only take effect some time (a few minutes) after the function is called. Does anyone know why this is?
ALSO PLEASE DO NOT COMMENT ON MY CAPITALIZATION STYLE. I PREFER THIS AS IT ALLOWS ME TO EMPHASIZE WHAT I BELIEVE TO BE IS IMPORTANT, WHICH CAN THEN HELP YOU SEE MY TRAIN OF THOUGHTS BETTER.
HERE IS THE CODE. Please note that I ONLY included the code for DECAY within the panGESTURE, because attack, release, and sustain code is all the same design:
// MAIN VIEW CONTROLLER CLASS, GLOBAL SCOPE, INITIALIZED WITH VIEW DID LOAD //
let bank = AKOscillatorBank()
var bankATTACK: Double = 0
var bankDECAY: Double = 0
var bankSUSTAIN: Double = 1
var bankRELEASE: Double = 0
func updateBANKWAVE() {
self.bank.rampDuration = 1
self.bank.attackDuration = self.bankATTACK
self.bank.decayDuration = self.bankDECAY
self.bank.sustainLevel = self.bankSUSTAIN
self.bank.releaseDuration = self.bankRELEASE
print("NEW BANK RAMP [\(self.bank.rampDuration)]")
print("NEW BANK ATTACK [\(self.bank.attackDuration)]")
print("NEW BANK DEC [\(self.bank.decayDuration)]")
print("NEW BANK SUS [\(self.bank.sustainLevel)]")
print("NEW BANK REL [\(self.bank.releaseDuration)]")
}
func prepareforSEGUE() {
if let sliderCONTROLLER = segue.destination as? SLIDERVIEWCONTROLLER {
sliderCONTROLLER.mainCONTROLLER = self
}
}
// SLIDER VIEW CONTROLLER CLASS //
if panGESTURE.state == changed {
// ... update a UILabel to reflect the value to be set ...
decayTEXT = String(format: "%.3f", (self.decayTRANSLATION.truncatingRemainder(dividingBy: 100.0)))
// ... update the MAIN controller variables ...
self.mainCONTROLLER.bankDECAY = Double(self.decayTRANSLATION.truncatingRemainder(dividingBy: 100.0))
// ... protect against values above 10 or below 0 ...
if decayTRANSLATION > 10 { decayVALUE.text = "10.000" ; decayTRANSLATION = 10.01 ; self.mainCONTROLLER.bankDECAY = 10 }
if decayTRANSLATION < 0 { decayVALUE.text = "0.000" ; decayTRANSLATION = -0.01 ; self.mainCONTROLLER.bankDECAY = 0 }
}
if panGESTURE.state == ended {
self.mainCONTROLLER.updateBANKWAVE()
}
By changing the oscillatorBANK.rampDURATION property to 0 instead of 1, I get instantaneous results. However, even though the release is being set to 1, the note can still be heard after 4 or 5 seconds... so...

Swift: clear array in iteration function

I want to use this iteration function to check a array, and store the result with new array, and then check the new array again and again...however I don't know how to clear the the old useless arrays so that the memory can be released.
func checkEnvironment(environment: [MGLPolygon], seed: MGLPolygon) -> [MGLPolygon]?{
var newEnv: [MGLPolygon]?
var alien: MGLPolygon?
var check = false
for i in 0..<environment.count{
if detectIntersect(poly1: seed, poly2: environment[i]) && check == false{
check = true
alien = seed
newEnv?.append(alien!)
}else{
newEnv?.append(environment[i])
}
}
if check == false{
// error occurs on here
environment.removeAll()
return newEnv!
}else{
checkEnvironment(environment: newEnv!, seed: alien!)
}
return nil
}
in the code
environment.removeAll()
gets a error that says environment is a let static so I can't change it. My question is how do I release the memory form those array?
Any help would be appreciate.
In swift3, function parameter are immutable so in order to change the value you need to add a var and assign into it like below.
func checkEnvironment(environment: inout [MGLPolygon], seed: MGLPolygon) -> [MGLPolygon]?{
if check == false{
environment.removeAll()
return newEnv!
}
}

Swift 2 to 3 conversion error with Value of type '[Any]' has no member

I'm having a problem with a Swift 2 to 3 conversion piece of work and some of the remains syntax giving: Value of type '[Any]' has no member errors.
I was hoping someone could point me at a good solution.
Swift 2 code
Swift 2 code
func search() {
epsonPrinters = [Printer]()
starPrinters = [Printer]()
epson_startSearching()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { [unowned self] in
let devices = SMPort.searchPrinter()
self.starPrinters = devices.map { portInfo -> Printer in
let p = Printer(
id: portInfo.modelName,
make: "Star Micronics",
model: portInfo.modelName,
portName: portInfo.portName)
if let name = portInfo.modelName as? String {
p.emulation = name.containsString("TSP143") ? "StarGraphics" : "StarLine"
}
return p
}
}
}
Swift 3 Code (I've added comments above areas with errors)
func search() {
epsonPrinters = [Printer]()
starPrinters = [Printer]()
epson_startSearching()
DispatchQueue.global(qos: .background).async { [unowned self] in
let devices = SMPort.searchPrinter()
self.starPrinters = [devices.map { portInfo -> Printer in
// id, model and portName in below fails with messages like:
// Value of type '[Any]' has no member 'modelName'
let p = Printer(
id: portInfo.modelName,
make: "Star Micronics",
model: portInfo.modelName,
portName: portInfo.portName)
// error on portInfo.modelName
// Value of type '[Any]' has no member 'modelName'
if let name = portInfo.modelName as? String {
p.emulation = name.containsString("TSP143") ? "StarGraphics" : "StarLine"
}
return p
}!]
}
}
I know that I can replace the 'id:...' part with the likes of:
id: ((portInfo[0] as AnyObject).modelName) ?? "",
But this isn't correct because PortInfo can have none, 1 or multiples depending on the number of printers we find.
I'd appreciate any suggestions for refactoring this in an elegant way that is good Swift 3 syntax and likely to survive into Swift 4.
I'm working in Xcode 8.3.2
When you get some errors about types, you'd better check what type each variable has. When you select devices in the line let devices = ..., Quick Help of Xcode will show you something like this:
Declaration let devices: [Any]?
First, it's an Optional and you need to unwrap it, before using the actual content.
Second, the type of the elements in devices is Any, to which you cannot apply any methods (including property accessors). You need to cast it to an appropriate type at an appropriate place.
To solve the two things above, you can write something like this:
guard let devices = SMPort.searchPrinter() as? [PortInfo] else {
fatalError("This may never happen")
}
With guard statement above, Quick Help will show you:
Declaration let devices: [PortInfo]
Non-Optional, simple Array of PortInfo, so you can use any methods of PortInfo for the elements of this devices.
I would translate your Swift 2 code into Swift 3 like this:
func search() {
epsonPrinters = []
starPrinters = []
epson_startSearching()
DispatchQueue.global(qos: .default).async {
guard let devices = SMPort.searchPrinter() as? [PortInfo] else {
fatalError("This may never happen")
}
self.starPrinters = devices.map { portInfo -> Printer in
let p = Printer(
id: portInfo.modelName,
make: "Star Micronics",
model: portInfo.modelName,
portName: portInfo.portName)
if let name = portInfo.modelName {
p.emulation = name.contains("TSP143") ? "StarGraphics" : "StarLine"
}
return p
}
}
}
You may need some fixes (not many, I believe) to use this code, as you are not showing all relevant things such as the definition of Printer.

Restrict TextField to act like a numeric stepper

I am making a numeric stepper from scratch, so I want my text field to only accept numbers in this format: xx.x, x.x, x, or xx where x is a number. For example:
Acceptable numbers:
1
22
15.5
3.5
None Acceptable numbers:
213
33.15
4332
1.65
Maybe this will help some how:
http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/text/TextField.html#restrict
This is what I got so far:
var tx:TextField = new TextField();
tx.restrict="0-9."; //Maybe there is a regular expression string for this?
tx.type=TextFieldType.INPUT;
tx.border=true;
You can copy past this in flash and it should work.
Thank you very much for your help good sirs.
Very similar to TheDarklins answer, but a little more elegant. And actually renders _tf.restrict obsolete, but I would still recommend using it.
_tf.addEventListener(TextEvent.TEXT_INPUT, _onTextInput_validate);
Both of these event listeners here do the EXACT same function identically. One is written in a one line for those who like smaller code. The other is for those who like to see what's going on line by line.
private function _onTextInput_validate(__e:TextEvent):void
{
if ( !/^\d{1,2}(?:\.(?:\d)?)?$/.test(TextField(__e.currentTarget).text.substring(0, TextField(__e.currentTarget).selectionBeginIndex) + __e.text + TextField(__e.currentTarget).text.substring(TextField(__e.currentTarget).selectionEndIndex)) ) __e.preventDefault();
}
for a more broken down version of the event listener
private function _onTextInput_validate(__e:TextEvent):void
{
var __reg:RegExp;
var __tf:TextField;
var __text:String;
// set the textfield thats causing the event.
__tf = TextField(__e.currentTarget);
// Set the regular expression.
__reg = new RegExp("\\d{1,2}(?:\\.(?:\\d)?)?$");
// or depending on how you like to write it.
__reg = /^\d{1,2}(?:\.(?:\d)?)?$/;
// Set all text before the selection.
__text = __tf.text.substring(0, __tf.selectionBeginIndex);
// Set the text entered.
__text += __e.text;
// Set the text After the selection, since the entered text will replace any selected text that may be entered
__text += __tf.text.substring(__tf.selectionEndIndex);
// If test fails, prevent default
if ( !__reg.test(__text) )
{
__e.preventDefault();
}
}
I have had to allow xx. as a valid response otherwise you would need to type 123 then go back a space and type . for 12.3. That is JUST NOT NICE. So 12. is now technically valid.
package
{
import flash.display.Sprite;
import flash.text.TextField;
import flash.text.TextFieldType;
import flash.events.TextEvent;
public class DecimalPlaces extends Sprite
{
public function DecimalPlaces()
{
var tf:TextField = new TextField();
tf.type = TextFieldType.INPUT;
tf.border = true;
tf.width = 200;
tf.height = 16;
tf.x = tf.y = 20;
tf.restrict = ".0-9"
tf.addEventListener(TextEvent.TEXT_INPUT, restrictDecimalPlaces);
addChild(tf);
}
function restrictDecimalPlaces(evt:TextEvent):void
{
var matches:Array = evt.currentTarget.text.match(/\./g);
var allowedDecimalPlaces:uint = 1;
if ((evt.text == "." && matches.length >= 1) ||
(matches.length == 1 && (evt.currentTarget.text.lastIndexOf(".") + allowedDecimalPlaces < evt.currentTarget.text.length)))
evt.preventDefault();
}
}
}