I've got a swimming workout set up.
let configuration = HKWorkoutConfiguration()
configuration.activityType = .swimming
configuration.locationType = .outdoor
do {
workoutSession = try HKWorkoutSession(configuration: configuration)
workoutSession!.delegate = self
healthStore.start(workoutSession!)
} catch let error as NSError {
// Perform proper error handling here...
fatalError("*** Unable to create the workout session: \(error.localizedDescription) ***")
}
But my client is saying that the watch doesn't go into waterproof mode unless he manually makes it.
For context, "waterproof lock" is a mode that the Watch Series 2 is supposed to enter into when starting a swimming workout. From an Apple Support article:
When you start a swimming workout, your Apple Watch automatically locks the screen with Water Lock to avoid accidental taps. When you're done, turn the Digital Crown to unlock the screen and clear any water from your Apple Watch. You hear sounds and may feel some water on your wrist.
This functionality does not appear to be working with my app.
I only have a Series 0 to test on, so I can't confirm what's happening with my code with regards to the waterproof lock.
Am I missing something to make the workout force the watch into waterproof mode?
Available since watchOS 4:
https://developer.apple.com/documentation/watchkit/wkextension/2868458-enablewaterlock
// Only an application which is in an active workout or location session and is foreground is allowed to enable water lock
#available(watchOS 4.0, *)
open func enableWaterLock()
Call WKExtension.shared().enableWaterLock() when starting your swim / water workout.
There is currently no "waterproof mode". My client was under the impression that there was.
The native Apple Exercise app has a button that locks the watch and ignores interaction until the digital crown is rotated. This is not currently reproducible in third party apps.
The solution I have found is to disable all buttons on the watch app. This way, buttons are not accidentally tapped while in the water. The user has to force touch the watch to re-enable the buttons.
Related
After reading about it, I have some mess in my head.
This function is being called while user swipe his finger on some UI element :
func wasDragged() { signal here }
I would like to make small Haptic signals every time it's being called ( like a dates picker wheel )
How would I setup first time and make the signals of the Haptic Engine on call ?
Do I have to check for device kind ? I want it only on iPhone 7 and up.
Using latest Swift.
The documentation oh Haptic feedback is really descriptive.
But if you want some quick solution here it is.
var hapticGenerator: UISelectionFeedbackGenerator?
func wasDragged() {
hapticGenerator = UISelectionFeedbackGenerator()
haptiGenerator.selectionChanged()
hapticGeneraor = nil
}
Alternatively depending on the logic of the screen, you can initialize generator outside of wasDragged function and inside of it just call hapticGenerator.prepare() and selectionChanged(). In that case you should not assign nil to it after the dragging is complete because it won't get triggered again. As per documentation you have to release generator when no longer needed as Taptic Engine will wait and therefore consume system resources for another call.
Note that calling these methods does not play haptics directly.
Instead, it informs the system of the event. The system then
determines whether to play the haptics based on the device, the
application’s state, the amount of battery power remaining, and other
factors.
For example, haptic feedback is currently played only:
On a device with a supported Taptic Engine
When the app is running in the foreground
When the System Haptics setting is enabled
Documentation:
https://developer.apple.com/documentation/uikit/uifeedbackgenerator
I want to update my watch app state in background from iPhone, using session.updateApplicationContext(applicationContext).
Sending an application contact while the app on the watch is active does work properly.
When I activate the home button on the watch, the watch app goes to the background, handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) is called, and a WKSnapshotRefreshBackgroundTask is provided.
So I don’t understand why a WKSnapshotRefreshBackgroundTask is triggered properly, but not a WKWatchConnectivityRefreshBackgroundTask.
Apple’s docs say „When you receive background data from the paired iPhone, the system launches your app in the background, instantiates a WKWatchConnectivityRefreshBackgroundTask object, and passes the task object to your extension delegate’s handleBackgroundTasks: method.“.
But this does not happen, neither on a device, nor on the simulator. What could be wrong?
Edit:
To check what might be wrong, I downloaded Apple’s demo project „QuickSwitch“ that can be downloaded here. Here is the code that should handle background tasks:
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for backgroundTask in backgroundTasks {
if let wcBackgroundTask = backgroundTask as? WKWatchConnectivityRefreshBackgroundTask {
// store a reference to the task objects as we might have to wait to complete them
self.wcBackgroundTasks.append(wcBackgroundTask)
} else {
// immediately complete all other task types as we have not added support for them
backgroundTask.setTaskCompleted()
}
}
completeAllTasksIfReady()
}
There, the same happens:
I did set a breakponint in the line of the if statement and executed the app.
When the home button on the watch simulator is pressed, the breakpoint is reached with a WKSnapshotRefreshBackgroundTask. This is OK (see above).
However, if a different line is selected on the iPhone simulator, watchOS does not schedule a WKWatchConnectivityRefreshBackgroundTask, as expected. After all, this demo project should demo exactly this point.
Maybe somebody could try the demo project and confirm this problem or not.
What is wrong?
Update my answer
Conclusion first
Currently WKWatchConnectivityRefreshBackgroundTask is only called for sure on a watchOS Simulator when the watchOS extension's WCSession is in notActivated state and the extension is not running in foreground (in background or terminated).
In real devices, it won't be called in my tests. But Apple docs says it may. So you shouldn't rely on it won't be called until Apple changes its docs.
WCSession Cores
For WCSession, when it is activated, you can transfer userInfo, and when the counterpart is active, it can get the userInfo. The counterpart won't need to be in foreground to be activated, it can be in a high priority background.
Testing Results
Here are my testing results.
How to make WCSession notActivated?
Using Xcode terminate your watchOS Extension. Xcode will send a kill signal to your WKExtension.
Or don't run WCSession.activate() in your code on watchOS Extension side. As WCSession is notActivated by default.
-------------below are old post, you can ignore safely if you don't want to read.-------------------
Theory
Please watch the picture first then I will explain.
Because of the history of watchOS, there are both WCSessionDelegate receiving functions (start from watchOS 2.0) and WKExtensionDelegate.handle(_:) function (start from watchOS 3.0).
Although they all claims to be background dealing, the former only works immediately when your app is in foreground. The data will be queued if your app is not in foreground (in background or being terminated) and executed immediately later when your app becomes in foreground again.
WKExtensionDelegate.handle(_:) is really working in background. However, the WKExtensionDelegate.handle(_:) is optional although it is recommended and well-prepared if you use Xcode.
If you don't implement WKExtensionDelegate.handle(_:) by commenting it. You app works in a watchOS 2.0 way.
If you implement WKExtensionDelegate.handle(_:) but you don't have a WCSession in your watchOS app. The result is tricky. You won't get any data when you watchOS app is in foreground, as you don't has a WCSession. When your app is in background, it will be waken when data comes, but you can't get the data as you don't have a session.
If you implemented them both, which are in most situations, data comes will be dealt depending on the state of your watchOS app and never be queued.
How to proving it?
Create a new watchOS project. In iOS part, add a button, each time you clicked the button, send a userInfo to watchOS
session.transferUserInfo(["send test":""])
In your watchOS app, add a label in interface.storyboard, and drag it to viewController as #IBOutlet var label: WKInterfaceLabel!, and implement both WKExtensionDelegate.handle(_:) and func session(WCSession, didReceiveUserInfo: [String : Any] = [:]) appDelegate.
var total = 0
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once you’re done.
backgroundTask.setTaskCompleted()
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once you’re done.
total += 1
DispatchQueue.main.async {
if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
viewController.label.setText(String(self.total))
}
}
connectivityTask.setTaskCompleted()
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompleted()
default:
// make sure to complete unhandled task types
task.setTaskCompleted()
}
}
}
public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
total += 4
DispatchQueue.main.async {
if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
viewController.label.setText(String(self.total))
}
}
}
If WKExtensionDelegate.handle(_:) runs, we add total by 1. If func session(WCSession, didReceiveUserInfo: [String : Any] = [:]) runs, we add total by 4.
Debug
In Xcode, choose product->scheme as WatchKit app so we can terminate the watchOS app in Xcode.
run the project.
when watchOS app shows, open iOS app manually.
clicked the button in iOS app. You can see the label in watchOS changes by 4.
in Xcode, click product->stop(or cmd+.). watchOS app will disappear.
click one or more times on iOS app's button. Then manually open the watchOS app. You will see this time the label changes by 1 multiply your clicks.
The step will be 4 again when watchOS app is in foreground.
To all who have the same problem:
I submitted the problem to Apple Developer Technical Support, and they confirmed (# 652471299) the problem in watchOS 3, and suggested to file a bug report, what I did (# 29284559).
So, one has to wait for a bug fix by Apple.
Update:
They answered my bug report, only 2 days later:
Well we get a ton of issues like this, usually it is some misunderstanding about timings or the app not suspending because it is being debugged or not in the dock so it won’t get discretionary tasks.
In this case, reading the description above I’m guessing the user is debugging via xcode while testing. Two task types: Watch Connectivity and URLSession only arrive as “launch” events. When debugging, xcode keeps the app running so it will never get these tasks. The best way to test this is to disconnect from xcode and test, make sure your app is in the dock as well — only docked apps will get discretionary tasks.
If you see this not working after trying that we’ll need a sysdiagnose to go further.
I think this statement is wrong. My reply was:
Thanks for the quick answer. However, something is wrong anyway:
The function
handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>)
should handle all background tasks, including WKWatchConnectivityRefreshBackgroundTask.
To check that this is not the case is easy:
Just let the app crash when such a background task is scheduled, i.e. insert in Apple’s QuickSwitch demo project an assert statement that is always false:
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for backgroundTask in backgroundTasks {
if let wcBackgroundTask = backgroundTask as? WKWatchConnectivityRefreshBackgroundTask {
assert(false) // If the app comes here, it will crash
// store a reference to the task objects as we might have to wait to complete them
self.wcBackgroundTasks.append(wcBackgroundTask)
} else {
// immediately complete all other task types as we have not added support for them
backgroundTask.setTaskCompleted()
}
}
completeAllTasksIfReady()
}
Then run the app in foreground, in the dock, or in background, and select different codes on the iPhone.
The app will NOT crash, which proves that no WKWatchConnectivityRefreshBackgroundTask is scheduled.
Please do this test without Xcode control. Just run it on iPhone & watch devices.
Now, 1 week later, I did not get any more reply.
Maybe I am wrong, and somebody can give me a hint how to do it right.
I am using the following code to suspend the app.
UIControl().sendAction(Selector("suspend"), to: UIApplication.sharedApplication(), forEvent: nil)
do {
print("suspended")
} catch _ {
print("unable")
}
I was wondering is it possible to reopen the app after a certain time delay. Following code works for performing an action after time delay. But I don't know how to reopen the app at that time interval
let seconds = 300.0
let delay = seconds * Double(NSEC_PER_SEC) // nanoseconds per seconds
let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
print("time delay")
})
It's is not possible and apps with such kind of "functionality" will be rejected by Appstore ;)
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/StartingStopping.html
Always Be Prepared to Stop
An iOS app never displays a Close or Quit option
When people switch away from your app, iOS multitasking transitions it to the background and replaces its UI with the UI of the new app. To prepare for this situation, your app should:
Save user data as soon as possible and as often as reasonable. Do
this because an app in the background can be told to exit or
terminate at any time.
Save the current state when stopping at the finest level of detail possible. In this way, people don’t lose their
context when they switch back to your app. For example, if your app
displays scrolling data, save the current scroll position. You can
learn more about efficient ways to preserve and restore your app’s
state in Preserving Your App’s Visual Appearance Across Launches.
Some apps may need to keep running in the background while users run another app in the foreground. For example, users might want to keep listening to the song that’s playing in one app while they’re using a different app to check their to-do list or play a game. Learn how to handle multitasking correctly and gracefully in Multitasking.
Never quit an iOS app programmatically. People tend to interpret this as a crash. If something prevents your app from functioning as intended, you need to tell users about the situation and explain what they can do about it.
I set the audio session category to kAudioSessionCategory_MediaPlayback, I active the session, which returns no errors, and still the iPod music stops when I lock the device. This happens on iOS 5 GM, so I guess this will happen in the final version. On iOS 4+ the current code works fine. Any ideas how to fix this? Huge thanks :)
It's not a bug. To save power locking the phone is now treated as if the user pressed the home button. The fact that applicationMusicPlayer stops now when locking is just a side effect of this change.
To work around this problem you should switch to AVPlayer and make use iOS 4's audio in background mode.
Fixed this issue for my particular problem - how to detect the difference between OS4 and OS5 behavior when device gets to the Lock screen.
In OS4 app does 'applicationWillResignActive' but on OS5 it goes all the way to 'applicationDidEnterBackground' which looks exactly the same as the user hitting the Home button.
It turns out that if you check the UIApplicationState of the application given in '- (void)applicationDidEnterBackground:(UIApplication *)application', it has 3 possible values:
typedef enum {
UIApplicationStateActive,
UIApplicationStateInactive,
UIApplicationStateBackground
} UIApplicationState;
When the user hits home on OS5, you get UIApplicationStateBackground, but when the user hits Lock, you get UIApplicationStateInactive.
Hope that helps.
Happy Holidays everyone.
In a game I am developing using GameCenter, I want to handle the following scenario:
the user starts up the game. He is shown the system alert that prompts him to log on GameCenter. He ignores it for now.
after a while, the user wants to log in to GameCenter and clicks on(for instance) the Leaderboards menu item. He choses cancel instead of Log in, for now.
the process repeats several times. Eventually the user DOES want to log in to GameCenter. He clicks the Leaderboard menu item one more time.
In my tests, I have found that the alert popup raised by the call to "authenticateWithCompletionHandler" (as invoked by Apple's sample GameCenterManager) which suggests to log in to GameCenter only appears a limited number of times(4 or 5). The last time it appears, it says "Game Center Disabled, sign in with the Game Center application to enable"Afterwards". Afterwards, calling authenticateWithCompletionHandler no longer does anything visible -no prompt at all.
Playing FruitNinja I tried to replicate this. However, in their case, the popup saying "Game Center Disabled" does appear every time I click on a GameCenter item(Achievements, for instance).
What I'd like to do is to duplicate the functionality: that is, if you are not logged in to GameCenter, to have the standard game center alert appear all the times you click on the Leaderboard menu item.
Is there a way to learn whether the standard 'log in to game center' alert has appeared, or to force it to appear at all times(and not just the first couple of tries)?
Here's an is an idea to workaround this issue:
No matter if a "GC authenticateWithCompletionHandler"-Request is cancelled
by the user tapping "Cancel" in the dialog
or due to the fact that
GC is disabled on the device (which happens after the user has cancelled the alert-dialog exactly 3 times (in iOS 5 at least))
you will always receive an NSError with code 2 saying "The requested operation has been cancelled.".
The only differentiator that i could find is the time passed between the authenticateWithCompletionHandler-Request and the the execution of the completion-Handler.
So when sending the request i am saving the time:
requestTime = [NSDate date];
and in my completion handler i measure the time lapsed:
NSDate* now = [NSDate date];
CFTimeInterval elapsedTimeSinceAuthenticationRequest = [now timeIntervalSinceDate:requestTime];
NSLog(#"time Elapsed: %f", elapsedTimeSinceAuthenticationRequest);
If the user cancelled the request, the time passed will be significantly longer compared to the time passed if GC cancelled the operation. In my tests, it took a user at least one second to cancel the dialog, whereas a GC-cancelled request took less than 0.1 seconds (on my iPhone 4)
Of course, these values may vary depending on the device the code runs on and on what else the processor is busy with at the moment. One pitfall i already examined is the application launch: If you are sending the authenticationRequest during applicationDidFinishLaunching as suggested by Apple, it took much longer for GC to cancel the request in my case, because the device is busy loading views and whatever is necessary to launch the app.
So let me know if you tried this solution and if it worked for you, as will i once i have done further testing...
The behavior is something to the effect of, after N unsuccessful attempts - disable GameCenter for the app. Restarting the app or going to login in gamecenter itself will get it back online.
I forget which doc I read this in, but there is an Apple doc out there that explains this behavior.
I couldn't find a good answer for this either, so I decided to just replicate the message once the I start getting the cancel error. This is still in development but it basically changes the button callback to display the error alert rather than display the leader-board.
Just a note, not sure if this will be approved or not since I am replicating an Apple error message.
-(void) gcLogin: (id) sender {
[[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:^(NSError *error) {
if(error) {
if([[error domain] isEqualToString:GKErrorDomain] && [error code] == GKErrorCancelled) {
[ResourceManager showAlertWithTitle:#"GameCenter Disabled" message:#"Sign in with Game Center application to enable"];
mGameCenterCancelled = YES;
}
NSLog(#"%#", [error description]);
} else {
[self updateMenu];
mGameCenterCancelled = NO;
}
}];
}
I am playing around with Game Center myself right now I have seen the very same behavior. There is nothing in the documentation saying anything about the dialog only showing up the first couple of times.
In my case I would like a way to tell beforehand if the user is already logged into Game Center, so that I can behave appropriately. But now I can not know this before the dialog is shown to the user.
Since we are running in the sandbox during development this behavior might of course be something that behaves differently during production but this is not an easy thing to find out.
May be the link will helpful (First three paragraphs):
http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/GameKit_Guide/Users/Users.html#//apple_ref/doc/uid/TP40008304-CH8-SW9
The main point is in a rectangle "Important".
I'm facing the same issue. Though I couldn't find a way to enforce poping up the same dialog for logging into Game Center, I did find a way to implement a warning message saying 'gamecenter is disabled' when user clicks on a leaderboard icon:
if([GKLocalPlayer localPlayer].authenticated == NO)
{
// Prompt a warning message alert saying game center is disabled
}
else
{
// Proceed with opening leaderboard
}
Hope this helps!
It appear that iOS will disable Game Center completely and prevent it from prompting after the user chooses to Disable Game Center (the option would appear on your fifth 5th Game Center cancel sign in).
To restore the device to the original state where the login prompt will appear again. Simply sign in using the Game Center app using a normal working Game Center account (non Tester). Once you are in, Sign Out. It should start prompting you again on within your app.