How to Implement shake gestures in an Apple Watch application? - ios

The WatchKit reference seems to make no mention about it. Have I missed something? Or is it really not possible to implement a shake gesture in an Apple Watch application?
The following is a typical example of a shake gesture implementation on iOS:
// MARK: Gestures
override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent) {
if(event.subtype == UIEventSubtype.MotionShake) {
// do something
}
}

No, it is not possible to do anything having to do with UIEvents in WatchKit right now, with the current solution's "remoted UI" approach where you mostly just get to tell the watch how to use the pre-arranged UI from the storyboard and react to actions like tapping a button or a table row. There will be support for a lot more code running on the watch later this year, according to Apple.
Update: Native apps are now possible for watchOS 2. This functionality may be present.

This is now possible in Watch OS2 with CMMotionManager a part of CoreMotion.
You can have this workaround.
let motionManager = CMMotionManager()
if (motionManager.accelerometerAvailable) {
let handler:CMAccelerometerHandler = {(data: CMAccelerometerData?, error: NSError?) -> Void in
if (data!.acceleration.z > 0) {
// User did shake
}
}
motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue()!, withHandler: handler)
}

How Roger say, an workaround is use the CoreMotion.
You can use coreMotion to get movement.
I build this simple wrapper on my Github.

Related

Detecting physical keyboard keypress in a catalyst program

I have a small iOS app that I wanted to compile for macOS with Catalyst.
The app is working properly on the Mac, but it's a calculator app, so I wanted to be able to enter the numbers with the keyboard in addition to clicking on the buttons.
I searched and found that the override of pressesBegan was the good way to do it. But if I implement Apple's example at https://developer.apple.com/documentation/uikit/mac_catalyst/handling_key_presses_made_on_a_physical_keyboard?changes=_8
like this
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
// Run backward or forward when the user presses a left or right arrow key.
var didHandleEvent = false
for press in presses {
guard let key = press.key else { continue }
if key.charactersIgnoringModifiers == UIKeyCommand.inputLeftArrow {
//runBackward()
didHandleEvent = true
}
if key.charactersIgnoringModifiers == UIKeyCommand.inputRightArrow {
//runForward()
didHandleEvent = true
}
}
if didHandleEvent == false {
// Didn't handle this key press, so pass the event to the next responder.
super.pressesBegan(presses, with: event)
}
}
in my Appdelegate class, I've got an error for the instruction key = press.key:
Value of type 'UIPress' has no member 'key'
Reading the documentation of the class UIPress, I don't see any key member in XCode documentation (UIKit>Touches, Presses and Gesture>UIPress), but I see it on
https://developer.apple.com/documentation/uikit/uipress
??
I didn't find any report of the message "Value of type 'UIPress' has no member 'key'" on Internet
I figured out my problem.
I had an old version of XCode (11.3.1) which didn't know UIPress.key.
After upgrading to XCode 11.4.1, all is OK.
I don't really understand the reason, because I can compile the code for iOS 12.4, before the availability of the property key (Apple's documentation claims iOS 13.4+). But it works now !

Is it okay to override pressesBegan() to minimize a fullscreen AVPlayer in tvOS?

A part of my tvOS App UI is a minimized (400px width) AVPlayer and a button that sets that resizes the AVPlayer to fullscreen view by setting its frame to the window boundaries:
playerController.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height).
I was not able to add a 'close fullscreen' button to the fullscreen avplayer (that would be the best solution) that's why I'm overriding the pressesBegan()-method with:
override func pressesBegan(presses: Set<UIPress>, withEvent event: UIPressesEvent?)
{
guard presses.first?.type == UIPressType.Menu else
{
super.pressesBegan(presses, withEvent: event)
return
}
// If not minimized, minimize it
if playerController.view.frame.size.width != 400
{
playerController.view.frame = minimizedVideoBounds
}
}
The question
Is this a possible, safe and clean why to achieve my UX goal or is a a dirty hack that should be avoided at all costs?
In short: No it's not. I got weird side effects. I think we should use the AVPlayer or the AVPlayerController as full screen element and we should avoid subclassing this stuff.
If somebody is smart enough to handle this: Please tell me how.

Can I add feedback (notch) to a gesture on iOS using 3D Touch?

With Force Touch on OS X applications can provide a feedback or a notch (haptics/taptics?) like Apple's example describes:
Map rotation: You'll feel a notch when you rotate the compass to north
in Maps.
https://support.apple.com/en-us/HT204352
Is the the same thing possible with 3D Touch beyond just the old audio API to vibrate the device (AudioServicesPlayAlertSound)?
Yes, using UIFeedbackGenerator in iOS 10+ on supported devices you could do something like this:
var feedbackGenerator: UISelectionFeedbackGenerator
func prepare() {
// Instantiate a new generator.
feedbackGenerator = UISelectionFeedbackGenerator()
// Prepare the generator when the gesture begins.
feedbackGenerator?.prepare()
}
func notch() {
// Trigger selection feedback.
feedbackGenerator?.selectionChanged()
// Release the current generator.
feedbackGenerator = nil
}

GameCenter Multiplayer Stuck on "Starting Game..."

I am currently working on my game and I have decided to enable multiplayer via GameCenter in the Game to allow users to play their friend. I have followed a tutorial by RayWinderLinch, but ran into a problem.
My problem is that when I load up the GKMatchMakingViewController and hit the big Play Now button on both devices it will find each other (which is meant to happen) and under the set game center user name it will say Ready.
This means that GameCenter has found each player and is ready to start the match which it should, but in my case the match never begins. It is stuck on a loop that says Starting Game... and nothing happens. It appears that the
func matchmakerViewController(viewController: GKMatchmakerViewController!, didFindMatch theMatch: GKMatch!)
and the
func match(theMatch: GKMatch!, player playerID: String!, didChangeState state: GKPlayerConnectionState)
method's are never ran. I am completely lost on what is going on. I have tried this many times over and over to fix the problem but nothing worked. I will attach an image that show's the screen of the application where my problem persists and I will also attach the code I am using.
I am using a framework based of of the GameKitHelper.h In the
mentioned tutorial above. It is written in swift and is called
GCHelper
Code
The code for GCHelper can be found using the GitHub link mention earlier
I have cut out code that is unnecessary for this problem
class GameScene : SKScene, GameKitHelper, MultiplayerNetworkingProtocol {
override func didMoveToView () {
GCHelper().authenticateLocalUser() //Authenticate GameCenter User
println("\n \n \n Authenticating local user \n \n \n")
}
func startMultiplayer () {
var vc = self.view?.window?.rootViewController
GameKitHelper().findMatchWithMinPlayers(2, maxPlayers: 2, viewController: vc!, delegate: self); //Find match and load GKMatchMakerViewController
}
func matchStarted() {
//Delegate method
println("match started")
}
func matchEnded() {
//Delegate method
println("match ended")
}
func match(match: GKMatch, didReceiveData: NSData, fromPlayer: String){
//Delegate Method
println("Did receive data")
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == multiplayer //SKSpriteNode {
//User clicked on multiplayer button, launch multiplayer now!
println("Loading multiplayer")
startMultiplayer()
}
}
Image
UPDATE
I have noticed that when I test using my iPhone and the simulator, on the iPhone the status will go from Ready to Disconnected but on the simulator the status is still Ready and then I will get the following message in the console for the iPhone
Warning matchmakerViewController:didFindMatch: delegate method not implemented`
Even though it is implemented in the GCHelper.swift file. This does not happen when I test on my iPhone and iPad Mini it just keeps on saying Starting Game...
Any help will be appreciated.
Prerequisites
Both players must be in the same environment (Sandbox for testing)
The authenticationChanged in GCHelper.swift must not be private. You may have to remove that keyword.
There are a few delegates involved, and in your example, there are a few competing protocols. My recommendation is to create a new App using minimalistic code to track down the startMultiplayer issue.
Gamekit Multi Player Step by Step Tutorial using GCHelper
Create a new project (Xcode > File > New > Project... > Single View Application > ... > Create) using the very same Product Name & Organization Name as your game, so that it matches both App Bundle Identifier and iTunes Game Center parameters. This will allow you to run tests without overhead.
Use this Podfile:
platform :ios, '8.0'
use_frameworks!
target 'SO-31699439' do
pod 'GCHelper'
end
Use a GCHelperDelegate
Create a UIViewController with just the bare minimum (a Start Multiplayer button), and connect it to this action:
#IBAction func startMultiplayerAction(_ sender: AnyObject) {
GCHelper.sharedInstance.findMatchWithMinPlayers(
2,
maxPlayers: 2,
viewController: self,
delegate: self);
}
Here is the crux: the delegate you pass must adopt GCHelperDelegate. It does not have to be the same class, but in your example above it is, and the present rule was not respected. For this example, ViewController adopts GCHelperDelegate:
import UIKit
import GCHelper
import GameKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
GCHelper.sharedInstance.authenticateLocalUser()
}
}
Implement required GCHelperDelegate methods in an extension
Since ViewController adopts GCHelperDelegate, the three methods below must be in that same class, and will be invoked:
extension ViewController: GCHelperDelegate {
func matchStarted() {
print("matchStarted")
}
func match(_ match: GKMatch, didReceiveData: Data, fromPlayer: String) {
print("match:\(match) didReceiveData: fromPlayer:\(fromPlayer)")
}
func matchEnded() {
print("matchEnded")
}
}
Execution
Tested: built, linked, ran, successful match.
Launch app, tap Start Multiplayer button, tap Play Now on both devices (or iPhone Simulator + real device).
Log:
Authenticating local user...
Authentication changed: player not authenticated
Ready to start match!
Found player: SandboxPlayer
matchStarted
► Find this solution on GitHub and additional details on Swift Recipes.

iOS Motion Detection: Motion Detection Sensitivity Levels

I have a simple question. I'm trying to detect when a user shakes the iPhone. I have the standard code in place to detect the motion and this works no problem. However, in testing this on my actual phone, I've realized that you have to shake the device quite hard to get the motion detection to trigger. I would like to know if there is a way to implement a level of sensitivity checking. For example, a way to detect if a user lightly shakes the device or somewhere between light and hard shake. This will be targeted towards iOS 7 so any tips or advice that is not deprecated from older iOS version would be greatly appreciated. I've done my googling but have yet to find any good solutions to this problem (If there are any.)
Thanks!
-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if(motion == UIEventSubtypeMotionShake)
{
//Detected motion, do something about it
//at this point.
}
}
-(BOOL)canBecomeFirstResponder
{
return YES;
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self becomeFirstResponder];
}
-(void)viewWillDisappear:(BOOL)animated
{
[self resignFirstResponder];
[super viewWillDisappear:animated];
}
Here is the solution I found. This works well but you do have to play with the deviceMotionUpdateInterval time value as well as the accelerationThreshold which can be tricky to get a fine balancing act for a actual "light shake" vs "picking up the phone and moving it closer to your face etc..." There might be better ways but here is one to start. Inside of my view didLoad I did something like this:
#import <CoreMotion/CoreMotion.h> //do not forget to link the CoreMotion framework to your project
#define accelerationThreshold 0.30 // or whatever is appropriate - play around with different values
-(void)viewDidLoad
{
CMMotionManager *motionManager;
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1;
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMDeviceMotion *motion, NSError *error)
{
[self motionMethod:motion];
}];
}
-(void)motionMethod:(CMDeviceMotion *)deviceMotion
{
CMAcceleration userAcceleration = deviceMotion.userAcceleration;
if (fabs(userAcceleration.x) > accelerationThreshold
|| fabs(userAcceleration.y) > accelerationThreshold
|| fabs(userAcceleration.z) > accelerationThreshold)
{
//Motion detected, handle it with method calls or additional
//logic here.
[self foo];
}
}
This is a swift version based on zic10's answer, with the addition of a flag that prevents getting a few extra calls to your motion handler even when the first line in that handler is motionManager.stopDeviceMotionUpdates().
Also, a value of around 3.0 can be useful if you want to ignore the shake, but detect a bump. I found 0.3 to be way too low as it ended up being more like "detect move". In my tests, the ranges were more like:
0.75 - 2.49 is a better range for shake sensitivity
2.5 - 5.0 is a good range for "ignore shake, detect bump"
Here is the complete view controller for an Xcode single VC template:
import UIKit
import CoreMotion
class ViewController: UIViewController {
lazy var motionManager: CMMotionManager = {
return CMMotionManager()
}()
let accelerationThreshold = 3.0
var handlingShake = false
override func viewWillAppear(animated: Bool) {
handlingShake = false
motionManager.startDeviceMotionUpdatesToQueue(NSOperationQueue.currentQueue()!) { [weak self] (motion, error) in
if
let userAcceleration = motion?.userAcceleration,
let _self = self {
print("\(userAcceleration.x) / \(userAcceleration.y)")
if (fabs(userAcceleration.x) > _self.accelerationThreshold
|| fabs(userAcceleration.y) > _self.accelerationThreshold
|| fabs(userAcceleration.z) > _self.accelerationThreshold)
{
if !_self.handlingShake {
_self.handlingShake = true
_self.handleShake();
}
}
} else {
print("Motion error: \(error)")
}
}
}
override func viewWillDisappear(animated: Bool) {
// or wherever appropriate
motionManager.stopDeviceMotionUpdates()
}
func handleShake() {
performSegueWithIdentifier("showShakeScreen", sender: nil)
}
}
And the storyboard I used for this test looks like this:
It's also worth noting that CoreMotion is not testable in the simulator. Because of this constraint you may still find it worthwhile to additionally implement the UIDevice method of detecting motion shake. This would allow you to manually test shake in the simulator or give UITests access to shake for testing or tools like fastlane's snapshot. Something like:
class ViewController: UIViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
}
override func canBecomeFirstResponder() -> Bool {
return true
}
override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent?) {
if TARGET_OS_SIMULATOR != 0 {
if event?.subtype == .MotionShake {
// do stuff
}
}
}
}
And then use Ctrl-Cmd-Z to test shake in the simulator.
Use core motion.
Link your binary with the CoreMotion framework.
Include
#import <CoreMotion/CoreMotion.h>
in your class.
Create an instance of CMMotionManager.
Set the deviceMotionUpdateInterval property to a suitable value.
Then call startDeviceMotionUpdatesToQueue.
You will get continuous updates inside the block, which include acceleration, magnetic field, rotation, etc.
You will get the data you require.
One thing to be taken care of is that the update shall be so rapid if
the interval is too small, and hence you will have to employ suitable
logic to handle the same.
Heres how I did this using Swift 3.
Import CoreMotion and create an instance
import CoreMotion
let motionManager = CMMotionManager()
On ViewDidLoad or wherever you want to start checking for updates:
motionManager.startDeviceMotionUpdates(to: OperationQueue.current!, withHandler:{
deviceManager, error in
if(error == nil){
if let mgr = deviceManager{
self.handleMotion(rate: mgr.rotationRate)
}
}
})
This function takes the rotation rate and gets a sum for the absolute values for x,y and z movements
func handleMotion(rate: CMRotationRate){
let totalRotation = abs(rate.x) + abs(rate.y) + abs(rate.z)
if(totalRotation > 20) {//Play around with the number 20 to find the optimal level for your case
start()
}else{
print(totalRotation)
}
}
func start(){
//The function you want to trigger when the device is rotated
}

Resources