Swift MacOS array of buttons call function with argument - arrays

I have a playing field for a board game similar to chess with 60 fields. Instead of connecting them manually in storyboard I want to do it with a for...in.
My code is like this:
let feld = NSButton()
feld.frame = CGRect(x: 1300+30*h, y: 20, width: 22, height: 22)
feld.target = self
feld.action = #selector(ButtonTest)
self.view.addSubview(feld)
ButtonArray.append(feld)
And the function like this (I dont understand why I need #objc before the function but only this way it works.
#objc func ButtonTest() {
ButtonArray[2].isEnabled=false
}
The code work fine. But I want to pass an argument to the function so I can deactivate the button just being pressed and not only [2] like in this example.
(and I want to add other calculations to this function).
I searched around and it seems that one cannot pass arguments. If that is true how else can I solve my problem?
EDIT:
Great answer and explanation, thanks. I sadly asked the question imprecisely: I dont only want to deactivate the pressed button but also
do calculations like this:
var score = 5+/NumberOfButtonPressed/
So if the first button is pressed var score=6, if the 10th button is pressed score=15.

You need #objc because otherwise, the Swift compiler will not create a selector for the method. A selector is an Objective-C value that identifies a method, and it is necessary to call a method through Objective-C. (Cocoa is written in Objective-C.) You can get a method's selector using #selector, as you have already found.
On macOS, actions called through Cocoa take 0 or 1 additional parameters. The optional parameter is the "sender", which is the object that triggered the action (the button on which you clicked). Try this:
#objc
func ButtonTest(sender: NSButton?) {
sender?.isEnabled = false
}
You don't need to change the #selector() expression. The Cocoa runtime will call your method with that parameter just by virtue of it existing.

Related

Export Struct-like objects for arrays

I have been bashing my head about this and can't seem to figure it out. In another engine, I could make a struct, then make an array of that struct that I could then edit in the inspector. There seems to be no way of doing this that I can find in Godot.
I want to have a Resource that holds the starting Value and Type of multiple faces on a dice. For example, one side could have "2 Damage" while another has "Heal 3." (this is a first-time godot experiment inspired by Slice&Dice). Every tutorial I watch however makes it seem like, if I want to do so, I'd have to make a completely new Resource for each combination of Value and Type (Damage 1 Resource, Damage 2 Resource, etc.)
class_name DiceResource extends Resource
class DiceFaceData:
export var BaseValue = 0
export(Resource) var Type = preload("Resources/DiceFaceTypes/Damage.tres")
func _init():
Type = 2
BaseValue = preload("Resources/DiceFaceTypes/Damage.tres")
export(Array) var Faces = [DiceFaceData.new()]
I cannot get DiceFaceData to show up in the Inspector's array, or be on the list of object types for an array. Extending Object doesn't work. Extending Node means I have to instantiate it, which I don't want to do for an editor-only Resource.
I find it hard to imagine Godot doesn't have anything like this available. Is there anything I can load in the inspector as just data and not have to instantiate it? Another option is create two arrays, one with int and another Resource, but that seems inconvenient to fill out. Or should I just give up with Resources and make everything a Node attached to a Node attached to a Node? Thanks!
Godot version 3.4.3
EDIT: If you're someone coming from Unity or Unreal, what you're looking for is Resource. While compared to ScriptableObjects or DataAssets from those other engines, that's not the complete answer. You would think, because of the way those game engines handle it, you can only create custom SO or DA as assets in the filesystem/content browser, but you can also use Resources as instanced classes. Instead of creating a new Resource in the filesystem, you can use
export(Resource) var n = preload("res://MyResourceScript.gd").new()
In the inspector, you can choose from the list New MyResourceScript and create it. You won't be referencing an externally made Reference file, you'll be creating a custom one right there. And look at the below answer as well on good tips for using Resources in cool ways.
First of all, I want to say that I sympathize. Custom resources and the inspector do not work well. There is a solution on the work… However that does not mean that the only thing we can do is keep Waiting For Godot.
Observations on your code
About your code, I want to point out that DiceFaceData is not a resource type. You could write it like this:
class DiceFaceData extends Resource:
export var BaseValue = 0
export(Resource) var Type = preload("Resources/DiceFaceTypes/Damage.tres")
func _init():
Type = 2
BaseValue = preload("Resources/DiceFaceTypes/Damage.tres")
And… That solves nothing.
And, also, by the way, I remind you can put it on its own file:
class_name DiceFaceData
extends Resource:
export var BaseValue = 0
export(Resource) var Type = preload("Resources/DiceFaceTypes/Damage.tres")
func _init():
Type = 2
BaseValue = preload("Resources/DiceFaceTypes/Damage.tres")
And… That is not the solution either.
Something else I want to point out is that GDScript has types. See Static typing in GDScript. Use them. To illustrate…
This is a Variant with an ìnt value
var BaseValue = 0
This is an int, typed explicitly:
var BaseValue:int = 0
And this is an int, typed implicitly with type inference:
var BaseValue := 0
And if you were using types Godot would tell you that this is an error:
BaseValue = preload("Resources/DiceFaceTypes/Damage.tres")
Because BaseValue is an int, and you setting a resource to it.
The Array of Resources problem
First of all, this is a Variant that happens to have an Array value, and it is exported as an Array:
export(Array) var Faces = []
Let us type it as an Array:
export(Array) var Faces := []
And sadly we cannot specify the type of the elements of the arrays in Godot 3.x (we need Godot 4.0 for that feature). However we can specify how we export it.
So, this is an Array exported as an Array of Resource:
export(Array, Resource) var Faces := []
See Exporting arrays.
Before you could not get your custom resource type to show up. And now you have the opposite problem: all the resource types show up. And this includes your custom resource type, if it in its own file.
You would guess that we need to specify the resource type we want:
export(Array, DiceFaceData) var Faces = []
And that would be correct if it were a build-in resource type. But it is a custom one. We are expecting this to be fixed in a future version. Meanwhile we will have to leave it with export(Array, Resource).
Mitigating the problem with an addon
To alleviate the pain of having all the possible resource types, consider using the addon "Improved resource picker" by MakovWait. You can find it on itch, or on github.
A proper solution
Anyway, we can do better. But you are going to need to make your script a tool script (you do that by putting tool on the top of the script, and it means that the code from the script can and will run on the editor).
We are going to define a setter with setget, and in there we are going to make sure the elements are of the correct type:
export(Array, Resource) var Faces = [] setget set_faces
func set_faces(new_value:Array) -> void:
Faces = []
for element in new_value:
element = element as DiceFaceData
if element == null:
element = DiceFaceData.new()
Faces.append(element)
Now, in the inspector panel when you increase the size of the array, Godot will insert a new null element to the array, which makes the setter we defined run, which will find that null and convert it to a new instance of your custom resource type, so you don't have to pick the resource type in the inspector panel at all.
A "hacky" solution
As you know, this does not work:
export(Array, DiceFaceData) var Faces = []
However, we can replace an export with _get_property_list. What happens is that Godot asks the object what properties it has to show up in the inspector panel. Godot does this by calling get_property_list And it will statically report the ones it found while parsing (the ones with export). However, Godot also defines a function _get_property_list where we can add more at run time.
See also Advanced exports.
Which begs the question, could we possibly make it work with _get_property_list? Kind of. The The code like this:
var Faces := []
func _get_property_list() -> Array:
return [
{
name = "Faces",
type = TYPE_ARRAY,
hint = 24,
hint_string = "17/17:DiceFaceData"
}
]
It will show up on the inspector as an array where the elements can only be of your custom resource type.
The issue is that it causes some error spam. Which you might or might not be OK with. It is your project, so it is up to you.
I know it looks like voodoo magic in part because we are using some undocumented stuff. If you want an explanation of that 24 and that 17/17: see How to add Array with hint and hint_string?.
About the sub-resources
Every tutorial I watch however makes it seem like, if I want to do so, I'd have to make a completely new Resource for each combination of Value and Type (Damage 1 Resource, Damage 2 Resource, etc.)
I'm not sure what you are getting to with "a completely new Resource", but yes. A resource is an instance of a resource type. And each of those combination would be a resource.
Perhaps "Damage", "Heal" and so on are resources too. Let us see… I'm guessing that is what the Type is for:
export(Resource) var Type = preload("Resources/DiceFaceTypes/Damage.tres")
Godot would be showing all the resource types it is aware of, which is a pain. I'm going to suggest a different approach than those above for this: Make an String enumeration.
export(String, "Damage", "Heal") var Type:String
That will show up as a drop down list on the inspector panel, with the options you specified.
Why String and not int? Ah, because you can then do this if you so desire:
var type_resource := load("Resources/DiceFaceTypes/" + Type + ".tres")
I'm assuming that those have the code that actually does damage or heal or whatever.
Alright, but when you add a new type of dice face, you would have to come here and update it… Or do you? With the power of tool scripts we are going to update that list to reflect the files that actually exist!
First of all, we are not going to use export, so it will be just:
var Type:String
And now we can export it from _get_property_list. There we can query the files. But before we do that, so we are clear what we have to do, the following code is equivalent to the export we had before:
func _get_property_list() -> Array:
return [
{
name = "Type",
type = TYPE_STRING,
hint = PROPERTY_HINT_ENUM,
hint_string = "Damage,Heal"
}
]
No undocumented stuff here.
Our task is to build that hint_string with the names of the files. And that looks like this:
const path := "res://"
func _get_property_list() -> Array:
var hint_string := ""
var directory := Directory.new()
if OK != directory.open(path) or OK != directory.list_dir_begin(true):
push_error("Unable to read path: " + path)
return []
var file_name := directory.get_next()
while file_name != "":
if not directory.current_is_dir() and file_name.get_extension() == "tres":
if hint_string != "":
hint_string += ","
hint_string += file_name
file_name = directory.get_next()
directory.list_dir_end()
return [
{
name = "Type",
type = TYPE_STRING,
hint = PROPERTY_HINT_ENUM,
hint_string = hint_string
}
]
Ah, yes, set the path constant to the path of the folder where the resources types you have are.
Addendum post edit
I want to elaborate on this example:
export(Resource) var n = preload("res://MyResourceScript.gd").new()
Here we are exporting a variable n as a Resource, which will appear in the Inspector panel. The variable is currently a Variant, we could type it Resource:
export(Resource) var n:Resource = preload("res://MyResourceScript.gd").new()
And then we don't need to tell Godot to export it as a Resource, because it is a Resource:
export var n:Resource = preload("res://MyResourceScript.gd").new()
Something else we can do is preload into a const. To be clear, preloads are resolved at parse time. Like this:
const MyResourceScript := preload("res://MyResourceScript.gd")
export var n:Resource = MyResourceScript.new()
This way, if you need to use the same script in multiple places, you don't need to repeat the path.
However, you might not need the path at all. If in the script res://MyResourceScript.gd we add a class_name (at the top of the script):
class_name MyResourceScript
Then we don't need to use preload at all. That name will be available everywhere, and you can just use it:
export var n:Resource = MyResourceScript.new()
Where is that resource stored?
Potentially nowhere. Above we are telling Godot to create a new one when our it initializes our object (e.g. which could be a Node, or another Resource - because, yes, Resources can have Resources) and those would only exist in RAM.
However, if you modify the Resource from the Inspector panel, Godot needs to store those changes somewhere. Now, if you are editing a Node, by default they go to the scene file. If you are editing another Resource, then it goes to wherever that Resource is stored. To be clear, scenes are resources too (PackedScene). And, yes, that means a file can have multiple Resources (A main resurce and sub-resources). You could also tell Godot to store the Resource in its own file from the Inspector panel. The advantage of giving a file to a Resource is in reusing it in multiple places (multiple scenes, for example).
So, a Resource could be stored in a file, or not stored at all. And a resource file could have a Resource alone, or it could also have sub-resources as well.
I'll take a moment to remind you that scenes can have instances of other scenes inside. So, there is no line between scenes and the so called "prefabs" in Godot.
… Did you know?
You can save the resources you created in runtime, using ResourceSaver. Which could be a way to save player progress, for example. You can also load them using load or ResourceLoader (in fact, load is a shorthand for ResourceLoader.load).
In fact, if you can use load or preload on something, it is a Resource. Wait a minute, we did this above:
const MyResourceScript := preload("res://MyResourceScript.gd")
Yep. The Script is a Resource. And yes, you can create that kind of resources in runtime too. Create a GDScript object (GDScript.new()), set its source_code, and reload it. Then you can attach it to an Object (e.g. a Node) with set_script. You can now start thinking of meta-programming, or modding support.

Displaying text from an array into text box

First off, I'd like to apologize for the newbie question, I'm trying to learn as I go with this. I've made a couple basic iOS apps but this is my first venture into macOS Storyboard apps with no formal training in programming.
I'm trying to create a program to help my partner (a writer by profession) with their creative blocks by displaying character, setting, and action attributes that can be used in a story. (Screenshots attached for a better representation)
I believe I have the basic window formatting down but I'm getting stuck on how to link the "Attribute" buttons to the text fields to display elements in the array. I just have placeholder elements until I get it working but ideally you would click the attribute button and it would display a random attribute from the array into the text box.
I've included what I was able to piece together so far but it's failing to build at the line to output to the text box and I can't seem to figure it out.
The error is:
Cannot assign value of type () to type String
Any assistance is appreciated!
import Cocoa
class SecondViewController: NSViewController {
#IBOutlet weak var CharAtt1: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func CharAttBut1(_ sender: NSButton) {
let array = ["Swift", "SwiftUI", "UIKit"]
let randomElement = array.randomElement()!
CharAtt1.stringValue = print("\(randomElement)")
}
}
Obviously, the offending line of code is:
CharAtt1.stringValue = print("\(randomElement)")
It's got a fair number of issues. The most prudent is that print has a type of (approximately, I'm simplifying) (Any) -> Void. You're calling it, passing it a string ("\(randomElement)"). This will print the string to the console, but it will also return back a () (a.k.a. the empty Tuple, of type Void). As the error message suggests, this can't be assigned to CharAtt1.stringValue, which is expecting a String.
The fix is simple, don't call print:
// If you do want to print it, do it in a separate expression
print("\(randomElement)")
CharAtt1.stringValue = "\(randomElement)"
But there's another issue: "\(randomElement)" is useless. randomElement is already a String. You could just write:
print(randomElement)
CharAtt1.stringValue = randomElement
I'd say that "\(anything)" is kind of anti-pattern. If you need to convert something to a string, I think it's better to do so in a way that's more explicit about the conversion you want. E.g. using String(anything) (if such an initializer exists), or String(describing: anything), or String(reflecting: anything) (depending on your usecase)

How can I assert that a value of a string is equal to one of any string values in an array using XCTAssert in Swift?

I'm new to Swift and followed a simple tutorial to make a magic 8 ball Cocoa App that every time I click the ball it shows a different piece of advice. I am now trying to practice my UI automated tests by asserting (XCTAssert) that the "Piece of Advice" label is equal to one of the string values in my array.
My array looks like this and is in my ViewController.swift:
var adviceList = [
"Yes",
"No",
"Tom says 'do it!'",
"Maybe",
"Try again later",
"How can I know?",
"Totally",
"Never",
]
How can I make an assertion in my UITests.swift file that asserts that the string that is shown is equal to one of the string values in the array above?
It's possible that you're asking how to access application state from a UI test, or just in general UI testing.
I think it's a pretty interesting question so I'm going to answer because it's something that I don't know a lot about and hopefully will prompt other people to chime in and correct.
Background: A basic Magic 8 Ball project
I set up a basic project with a view controller that contains two views: a label and a button. Tapping the button updates the label text with a random message:
import UIKit
struct EightBall {
static let messages = ["Yes", "No", "It's not certain"]
var newMessage: String {
let randomIndex = Int(arc4random_uniform(UInt32(EightBall.messages.count)))
return EightBall.messages[randomIndex]
}
}
class ViewController: UIViewController {
let ball = EightBall()
#IBOutlet weak var messageLabel: UILabel!
#IBAction func shakeBall(_ sender: Any) {
messageLabel.text = ball.newMessage
}
}
A basic UI test
Here's a commented UI test showing how to automate tapping on the button, and grabbing the value of the label, and then checking that the value of the label is a valid message.
import XCTest
class MagicUITests: XCTestCase {
// This method is called before the invocation of each test method in the class.
override func setUp() {
super.setUp()
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = true
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication().launch()
}
func testValidMessage() {
// Grab reference to the application
let app = XCUIApplication()
// #1
// Grab reference to the label with the accesability identifier 'MessageLabel'
let messagelabelStaticText = app.staticTexts["MessageLabel"]
// Tap the button with the text 'Shake'
app.buttons["Shake"].tap()
// get the text of the label
let messageLabelText = messagelabelStaticText.label
// #2
// check if the text in the label matches one of the allowed messages
let isValidMessage = EightBall.messages.contains(messageLabelText)
// test will fail if the message is not valid
XCTAssert(isValidMessage)
}
}
At #1 The approach that I'm using to get the label is to access the labels accessibilityIdentifier property. For this project I entered this through storyboard, but if you're setting your views up in code you can directly set the accessibilityIdentifier property yourself.
The other thing that's confusing here is that to get access to elements in the view you're not navigating the view hierarchy, but a proxy of the hierarchy, which is why the syntax to get a label is the odd 'staticTexts' (The references at the bottom of the post explain this in more detail).
For #2 I'm inspecting the structure defined in my project. In a unit test you could access this my importing #testable import ProjectName but unfortunately this approach doesn't work for UI Test.
Instead, you'll have to make sure that any source file you want to access from the UI test is included as a target. You can do this in Xcode from this panel by checking the name of your UI test:
More UI testing references:
UI Testing Intro: http://www.mokacoding.com/blog/xcode-7-ui-testing/
UI Testing Cheat Sheet: http://masilotti.com/ui-testing-cheat-sheet/

Is it possible to apply changes to all objects in Array without using "for each" or "for" in Swift 3?

for example
var imageViewArray:UIImageView = [imageView1,imageView2,imageView3]
I want to chage sameimageView.image = img or imageView.isUserInteractionEnabled = false to all Image View inside the array
1. It's an array
First of all it's not
var imageViewArray:UIImageView
but
var imageViewArray:[UIImageView]
because you want an array of UIImageView right?
2. Naming conventions
Secondly is Swift we don't name a variable after it's type so imageViewArray becomes imageViews.
3. map
Now if you really hate the for in and the foreach your can write
imageViews = imageViews.map { imageView in
imageView.isUserInteractionEnabled = true
return imageView
}
or as suggested by Bohdan Ivanov in the comments
imageViews.map { $0.isUserInteractionEnabled = true }
4. Wrap up
This answer shows you how to use the wrong construct (map) to do something that should be made with the right construct (for in).
That's the point of having several constructs, everything could be made with an IF THEN and a GOTO. But a good code uses the construct that best fits that specific scenario.
So, the best solution for this scenario is absolutely the for in or the for each
imageViews.forEach { $0.isUserInteractionEnabled = true }

How can I pass a Swift array as an argument to a thread?

I'm trying to spawn a new thread to do some background processing, based on a String that I've broken down into an array of characters. Here's what my code looks like:
var testString : String = NSString(data:data!, encoding:NSUTF8StringEncoding)
var testStringArray : Array<Character> = []
for character in testString
{
if(!(self.isCharacterStrippable(character)))
{
testStringArray.append(character)
}
}
NSThread.detachNewThreadSelector("fillKeysFromArray:", toTarget: self, withObject: testStringArray)
I get a compiler error telling me that "Array does not conform to protocol AnyObject".
Short of writing an object wrapper for my array, or setting it as an instance variable (both of which seem like overkill), is there any way I can get this array passed through to the new thread?
Using Grand Central Dispatch is going to be much easier in the long run. You can run your function on a background thread with something like:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
[weak self] in // This is so that we don't create a reference cycle
self?.fillKeysFromArray(testStringArray);
return
}
You can read up on Grand Central Dispatch and all the nice things it provides when it comes to threading and concurrency in Apple's Concurrency Programming Guide.
The root cause is that detachNewThreadSelector can only accept a NSMutableArray for the withObject parameter. If you make testStringArray a variable of type NSMutableArray you can get rid of the compiler error message.
Having said that, you should seriously consider the advices from Bryan Chen and Mike S to switch to GCD.

Resources