I am a complete newbie to programming and I am trying to learn how to make a simple iOS game using Spritekit and Swift 4.
So far, I have achieved some mild success, but I would like to add some further details to the game to make it a little more playable.
I have added some actions to my GameScene so that when the user taps the screen, a Sprite execute an action. It works fine, but now I want to keep repeating that action if the user holds its finger on the screen.
I have read some posts about it, but they all seem to point to Objective-C or earlier versions of Swift that just pop a bunch of errors when testing and I am unable to get them working for me.
I know I am supposed to be using some instance of UILongPressGestureRecognizer but Apple's documentation seems rather confusing about how to initialise it or what to declare on action: Selector?
As I understand it, in my viewDidLoad I must include something like:
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: "handleLongPress:")
self.addGestureRecognizer(longTapRecognizer)
And then write a function (I am not sure if inside viewDidLoad as well) that handles the action:
func handleLongPress(recognizer: UIGestureRecognizer) {
if recognizer.state == .began {
print("Long press")
}
}
As easy as this may sound, I simply cannot seem to understand how the action: is supposed to be declared or how to solve this.
Any guidance will be greatly appreciated!
The syntax for the action in swift is #selector(methodName(params:))
(See https://developer.apple.com/documentation/swift/using_objective_c_runtime_features_in_swift)
Your gesture recognizer would be written as such:
let longTapRecognizer = UILongPressGestureRecognizer(
target: self,
action: #selector(handleLongPress(recognizer:)))
Related
Apple states in https://developer.apple.com/documentation/spritekit/skscenedelegate :
Modifying SpriteKit objects outside of the ordained callbacks (a
background queue or anything else non-main-thread) can result in
concurrency related problems. Even dispatching work on the main thread
asynchronously or at a later time is risky because the closure is
likely to be done outside of the timeframe SpriteKit expects. If
you're experiencing a segmentation fault or other type of crash
occurring deep within the SpriteKit framework, there's a good chance
your code is modifying a SpriteKit object outside of the normal
callbacks.
I'm using gesture recognizers to interact with my sprite kit objects. A simple example would be to add a SKAction to a node when the user tapped an object:
func tapAction(gr:UITapGestureRecognizer) {
scene.childNode(withName: "balloon")!.run(SKAction.fadeOut(withDuration: 2))
}
Despite the fact that this "just works" for the moment, I'm afraid that this does not work in more complicated cases.
Is there any hint from Apple that this is allowed? Or do I really have to defer the modification of the SpritKit object from the gesture action to an ordained callback?
It looks like you are safe, you are just assigning an action. That is going to run during the normal sprite kit updates
if you were manipulating the actual object, or removing a node, you would come into problems. Let's say you tap to remove a node. This tap happens right before didContactBegin. didContactBegin would expect a node, but alas, you removed it, so it will crash.
If you want to feel safe about this, then set up a queue to fire at the beginning of your update.
class GameScene : SKScene
{
public typealias Closure = ()->()
public var processOnUpdate = [Closure]()
override func update(_ currentTime: TimeInterval) {
proceseOnUpdate.forEach{$0()}
processOnUpdate = [Closure]()
....//do other stuff
}
}
//SKView Code
func tapAction(gr:UITapGestureRecognizer) {
scene.processOnUpdate.append(
{
scene.childNode(withName: "balloon")!.run(SKAction.fadeOut(withDuration: 2))
}}
}
My apologies if this does not run the first time, I am not on a Mac now to test this.
There are 2 ways to get CADisplayLink in iOS. The direct one is to use initializer:
let displaylink = CADisplayLink(target: self,
selector: #selector(step))
Returns a new display link.
This way is used in Apple's example: Listing 1.
But there are other way to get it from UIScreen:
let displayLink = UIScreen.main.displayLink(withTarget: self,
selector: #selector(step))
Returns a display link object for the current screen.
You use display link objects to synchronize your drawing code to the screen’s refresh rate. The newly constructed display link retains the target.
Documentation is very poor for details, but the second way looks a little more optimised. May be someone who has experience with CADisplayLink can tell which way to create it is preferred.
I am new to iOS /swift programming and I am working on an app developed by someone else, fixing some bugs.
The app is essentially a music player and the music has to be played also in background, giving the possibility to play/pause/skip from the lock screen. The app has several views, one of them, the main one, contains all the code related to the player itself (player.swift), the other ones containing other additional pages/features.
The commands from the lock screen works only when I lock the screen starting from the main view, if I do it starting from another view (e.g. the help view, which is just a page which is displayed over the player when the help link is tapped) they don't work. Reading several articles here I've realized that the reason is that the related code is in player.swift:
override func remoteControlReceivedWithEvent(event: UIEvent) {
if (event.type == UIEventType.RemoteControl) {
switch (event.subtype) {
case UIEventSubtype.RemoteControlPlay:
self.onPlayPause(self);
case UIEventSubtype.RemoteControlPause:
self.onPlayPause(self);
case UIEventSubtype.RemoteControlTogglePlayPause:
self.onPlayPause(self);
case UIEventSubtype.RemoteControlNextTrack:
onNext(nil)
default:
break
}
}
}
so I have understood the problem, but even if I've read several related articles (including remoteControlReceivedWithEvent called on iOS 7.0 device but not iOS 8.0, Using lock screen for my app?, Swift. Receive remote control events to work with MPNowPLayingInfoCenter) I can't figure out where do I need to move this code and if I need to move something else or make modifications.
EDIT. I moved the code in AppDelegate.swift (deleting the code in player.swift) , as suggested. It seems it now intercepts commands even if I lock the device from a view different than player.swift. I have two problems, though:
1) It seems it works just once, If I click on "next" from the lock screen I can see from a debug string that the command is intercepted, If I do it a second time nothing happens
2) I need to call the methods (onPlayPause and onNext) in player.swift from AppDelegate.swift, I guess those methods expect to have a player object set and/or they refer to variables declared in player.swift and I don't know how to handle this. For example the onNext method is declared as
#IBAction func onNext(sender: AnyObject?) {
oldImage = iAlbumArt.image
.......
and if I call the method as a new instance from AppDelegate
player().onNext(nil)
I get an error because iAlbumArt.image is NIL. iAlbumArt is a variable declared in the Player class as
#IBOutlet weak var iAlbumArt: UIImageView!
Sorry for the naive questions but I've been looking into iOS development just since a couple of weeks ago.
Try adding it to your App Delegate class.
Edit:
To forward remote control events to the view controller, add this code to the app delegate (assuming your player view controller is called PlayerViewController):
let vcs = (self.window!.rootViewController as! UINavigationController).viewControllers
let indexOfPlayer = (vcs as! NSArray).indexOfObjectPassingTest { (vc, idx, stop) in
return (vc.isKindOfClass(PlayerViewController))
}
let playerVC = vcs[indexOfPlayer];
Edit 2:
Place the override func remoteControlReceivedWithEvent method in your App Delegate Class.
At the top of that method, place the code snippet shown above.
In that method, replace self with playerVC.
In your player view controller, add code to respond to onPlayPause and onNext functions.
Note:
The reason why this code:
player().onNext(nil)
was throwing an error was because player() creates a brand-new instance of your player class. You want to use the existing instance so the changes get reflected on the screen.
I have created a custom subclass of UITextField, CustomTextView.
I created a
private var tapGesture = UITapGestureRecognizer()
in the customTextView class
In the initInView, I have the following code
tapGesture.addTarget(self, action: "tapTextField:")
self.addGestureRecognizer(tapGestureRecognizer)
CustomTextView implements UIGestureRecognizerDelgate
CustomTextView has a private func named tapTextField
Now when I use iOS simulator and click on the text field, the function tapTextField never gets called.
What am I missing here? I saw similar posts but none of them answer my question. I could not comment on those as I don't have reputation yet. So asking as a new question.
PS: Does this have to do with firstResponder being set? This is someone else's code I am working on, so I might have missed something. Let me know what I should look for
Related stack overflow questions:
Add UITapGestureRecognizer to UITextView without blocking textView touches
UITextview: Place focus on becomefirstresponder
I don't know swift, but try changing this "tapTextField:" to this "tapTextField" and make sure you don't have any arguments/parameters/whatever swift calls them in your "tapTextField" function.
Also, it looks like
self.addGestureRecognizer(tapGestureRecognizer) should be self.addGestureRecognizer(tapGesture)
You can try few things to debug the problem.
1> Make sure there is no mistake of tapTextField and tapTextField: i.e. you are adding selector with argument but you have implemented same method without any argument.
2> Make sure no any other transparent view obscuring your custom uitextfield.
3> print po yourTextFieldName in the xcode console to see whether actually your textfield has any gesture recognizer added in it or not.
Hope this helps
I am new to Xcode but have been developing for a 15 years. I am trying to understand how event declaration works in Xcode. Could it possibly be that you can only declare when you Ctr drag it to the code? And then only view it via the dialog box in the Connection Viewer? That would be terribly annoying. What am I missing? Surely the event should appear in the code somewhere.
Here is a sample function that is supposedly declaring a "Did End on Exit" code.
#IBAction func helloAction(sender: UITextField) {
nameLabel.text = "Hi \(sender.text)"
}
Thanks for your help.
The helloAction method is not declaring a "Did End on Exit" code, unless I'm misunderstanding what you mean by declaring. helloAction is a target that's called when the control it's bound to in Interface Builder receives a Did End on Exit event.
This uses a mechanism known as the Target-Action pattern in Cocoa. A control receives an event, and then calls any methods that have registered to be notified when the control receives that event.
This relationship can be created programmatically as well. Consider the following.
let button = UIButton()
button.addTarget(self, action: "tapButton:", forControlEvents:UIControlEvents.TouchUpInside)
When the button is tapped and released with the touch still inside the buttons bounds, self's tapButton: method is called.
If you want to learn more about Target-Action in Cocoa, check out a high level overview or the definitive discussion, both from Apple.