Cannot request compass calibration from Core Location callback - swift

I am using Core Location in my app and I am trying to present a calibration alert when the device compass / heading values are too imprecise.
I am correctly reading the heading values from the didUpdateHeading delegate function.
I read the related posts and I the CLLocationManager documentation (https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate/1621457-locationmanagershoulddisplayhead).
In the locationManagerShouldDisplayHeadingCalibration(_:) delegate function I tried those two implementations:
func locationManagerShouldDisplayHeadingCalibration(_ manager: CLLocationManager) -> Bool {
return true
}
And:
func locationManagerShouldDisplayHeadingCalibration(_ manager: CLLocationManager) -> Bool {
print("locationManagerShouldDisplayHeadingCalibration")
if let h = manager.heading {
return h.headingAccuracy < 0 || h.headingAccuracy > 10
}
return true
}
In the documentation, it is stated "If you return false from this method or do not provide an implementation for it in your delegate, Core Location does not display the heading calibration alert. Even if the alert is not displayed", but I don't know how to "provide an implementation for it".
Moreover, I see that there is no way to present the system calibration alert manually, if the alert is not presented as often as I want, is it a good solution to poll the headingAccuracy value on the didUpdateHeading callback and to present a system-like alert ? Also as I never saw it is there any way I can trigger the system calibration alert ?

Related

How to request App Ratings with requestReview() correctly

I am currently learning how to ask the user for an AppStore rating in the app. I have read through numerous documentations and reports from Apple and other bloggers, but I still have questions:
I thought that I want to display my request after the user has completed a sequence of actions 3 times. But if the user doesn't want to rate my app right now, can I ask him to rate my app again later? I also read that Apple controls when and how often the review request is displayed (up to 3x / year)
My app has different functions. Therefore it is not guaranteed that the user accesses exactly this function, where I want to show the rating view. Is it therefore possible to call up the rating request at different points in the app and simply leave it up to Apple whether the rating request is also displayed?
Best regards
Edit for #K bakalov :
enum AppReviewRequest {
#AppStorage("runCountSinceLastRequest") static var runCountSinceLastRequest = 0
#AppStorage("lastVersion") static var lastVersion = ""
static let threshold = 4
static let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
static func countUpRequestReview() {
if currentVersion != lastVersion {
if runCountSinceLastRequest < threshold - 1 {
runCountSinceLastRequest += 1
print(runCountSinceLastRequest)
}
}
}
static func requestReviewIfNeeded() {
if currentVersion != lastVersion {
if runCountSinceLastRequest == threshold {
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
lastVersion = currentVersion
runCountSinceLastRequest = 0
}
}
}
}
}
I assume you already read that but if you haven't I am leaving a link for you to check it out: https://developer.apple.com/documentation/storekit/requesting_app_store_reviews
It contains helpful information and guides when and how to prompt the user to leave a review.
Back to your questions.
Yes, you can ask again for review but there is no guarantee that the Review pop-up (alert) will be presented again. It is usually delayed in time and sometimes requires a new update of the app to trigger a new review prompt; That is correct, Apple has control over it but I don't know if its limited to only 3 times a year.
Yes, you can call it as many times as you want and wherever you find it suitable. Although, I highly encourage you to read (if still haven't) the link above. You need to think of a non-intrusive way to ask for review, otherwise you risk the user to dismiss the prompt, even if they like the app.
Just an advice, many apps use an approach with a custom popup "Do you like the app? Yes/No", if "Yes" then request a review using requestReview() call. And/or as mentioned in the article above, you can always use a manual review by redirecting to the AppStore, if you need it as a result of a CTA (tap on a button for example).

Changing Badge Notification count with Pushy in Capacitor/Cordova on iOS

I have a Capacitor/Cordova app using Pushy (https://pushy.me) to handle push notifications. Sending push notifications to devices seems to work fine, and as part of that transaction I can set the app's notification count on the icon when the app is closed.
However, Pushy only seems to have an JS option to clear the counter Pushy.clearBadge(); rather than change it to a specific number from within the app when it's running. The scenario being if a user has read one message, but leaves two unread and then closes the app, I want the counter to be correct.
Digging through the PushyPlugin.swift code, the function for Pushy.clearBadge(); looks like this:
#objc(clearBadge:)
func clearBadge(command: CDVInvokedUrlCommand) {
// Clear app badge
UIApplication.shared.applicationIconBadgeNumber = 0;
// Always success
self.commandDelegate!.send(
CDVPluginResult(
status: CDVCommandStatus_OK
),
callbackId: command.callbackId
)
}
Which is tantalisingly close to giving me what I need, if only I could pass an integer other than zero in that line UIApplication.shared.applicationIconBadgeNumber = 0;
My knowledge of Swift is zilch, other than recognising familiar programming syntax. I thought I'd have a hack at it and tried adding this to the PushyPlugin.swift file:
#objc(setBadge:)
func setBadge(command: CDVInvokedUrlCommand) {
// Clear app badge
UIApplication.shared.applicationIconBadgeNumber = 100;
// Always success
self.commandDelegate!.send(
CDVPluginResult(
status: CDVCommandStatus_OK
),
callbackId: command.callbackId
)
}
But the app coughed up Pushy.setBadge is not a function when I tried to give that a spin (note, I was testing with 100 just to see what would happen, ideally I'd want to pass an integer to that newly hacked in function).
So what I've learnt so far is I know nothing about Swift.
Am I sort of on the right lines with this, or is there a better way to set the badge counter?
Ok, so I spotted in the pushy-cordova/www folder a Pushy.js file which I needed to add my new function to
{
name: 'setBadge',
noError: true,
noCallback: true,
platforms: ['ios']
},
Then, a bit of trial and error with the PushyPlugin.swift function I added to this:
#objc(setBadge:)
func setBadge(command: CDVInvokedUrlCommand) {
UIApplication.shared.applicationIconBadgeNumber = command.arguments[0] as! Int;
// Always success
self.commandDelegate!.send(
CDVPluginResult(
status: CDVCommandStatus_OK
),
callbackId: command.callbackId
)
}
Then this can be called in my Capacitor project with Pushy.setBadge(X); (X being the int value you want to display on your icon).

MacOS Quartz Event Tap listening to wrong events

I am trying to intercept mouse move events using the CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) method as shown below:
let cfMachPort = CGEvent.tapCreate(tap: CGEventTapLocation.cghidEventTap,
place: CGEventTapPlacement.headInsertEventTap,
options: CGEventTapOptions.defaultTap,
eventsOfInterest:CGEventMask(CGEventType.mouseMoved.rawValue),
callback: {(eventTapProxy, eventType, event, mutablePointer) -> Unmanaged<CGEvent>? in event
print(event.type.rawValue) //Breakpoint
return nil
}, userInfo: nil)
let runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, cfMachPort!, 0)
let runLoop = RunLoop.current
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopAddSource(cfRunLoop, runloopSource, CFRunLoopMode.defaultMode)
I pass as event type eventsOfInterest mouseMoved events with a raw value of 5 as seen in the documentation. But for some reason my print() is not executed unless I click with the mouse. Inspecting the send mouse event in the debugger gives me a raw value of 2, which according to the documentation is a leftMouseUp event.
In the documentation for CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) it says:
Event taps receive key up and key down events [...]
So it seems like the method ignores mouseMoved events in general?! But how am I supposed to listen to mouseMoved events? I am trying to prevent my cursor (custom cursor) from being replaced (for example when I hover over the application dock at the bottom of the screen).
You need to bitshift the CGEventType value used to create the CGEventMask parameter. In Objective-C, there is a macro to do this: CGEventMaskBit.
From the CGEventMask documentation:
to form the bit mask, use the CGEventMaskBit macro to convert each constant into an event mask and then OR the individual masks together
I don't know the equivalent mechanism in swift; but the macro itself looks like this:
*/ #define CGEventMaskBit(eventType) ((CGEventMask)1 << (eventType))
In your example, it's sufficient to just manually shift the argument; e.g.
eventsOfInterest:CGEventMask(1 << CGEventType.mouseMoved.rawValue),
I would point out that the code example given in the question is a little dangerous; as it creates a default event tap and then drops the events rather than allowing them to be processed. This messes up mouse click handling and it was tricky to actually terminate the application using the mouse. Anyone running the example could set the event tap type to CGEventTapOptions.listenOnly to prevent that.
Here is a way to listen for mouseMove global events (tested with Xcode 11.2+, macOS 10.15)
// ... say in AppDelegate
var globalObserver: Any!
var localObserver: Any!
func applicationDidFinishLaunching(_ aNotification: Notification) {
globalObserver = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("in background: {\(location.x), \(location.y)}")
}
localObserver = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("active: {\(location.x), \(location.y)}")
return event
}
...
There's another thing incorrect in your code, although you might be lucky and it isn't normally causing a problem.
As documented for the mode parameter to CFRunLoopAddSource: "Use the constant kCFRunLoopCommonModes to add source to the set of objects monitored by all the common modes."
That third parameter should instead be CFRunLoopMode.commonModes.
What you have, CFRunLoopMode.defaultMode aka kCFRunLoopDefaultMode, is instead for use when calling CFRunLoopRun.

addUIInterruptionMonitor is not getting called on macOS

I want to test my macOS application. It uses your Macbook's camera, and want to handle this in my UITest. However I cannot get it working. Here is my NOT working code. This code triggers to notification, and I'm presented an alert to allow access to my camera, but the closure is not getting called. Thanks fo any help.
There are many solutions for iOS, but I need it on macOS.
let alertHandler = addUIInterruptionMonitor(withDescription: "Camera Permission Alert") { (alert) -> Bool in
if alert.buttons.matching(identifier: "OK").count > 0 {
alert.buttons["OK"].click()
self.app.click()
return true
} else {
return false
}
}
XCTAssertTrue(startButton.waitForExistence(timeout: 1.0))
startButton.click()
XCTAssertTrue(recordButton.waitForExistence(timeout: 20.0))
recordButton.click()
wait(for: 8)
recordButton.click()
removeUIInterruptionMonitor(alertHandler)
}
I managed to make interruption monitor work on macOS by adding an extra interaction after the interaction that triggers the system dialog (be it camera access or else). So in your example I would add an action after startButton.click() (if that is what triggers the camera access dialog).
Example:
func testCamera() {
let alertHandler = addUIInterruptionMonitor(withDescription: "Camera Permission Alert") { (alert) -> Bool in
if alert.buttons.matching(identifier: "OK").count > 0 {
alert.buttons["OK"].click()
self.app.click()
return true
} else {
return false
}
}
useCameraButton.click()
// try to interact with the app by clicking on the app's window
app.windows.first().click()
// at this point the handler should intercept the system interruption
// and blocks further execution until handler does return
// try to use the camera again
useCameraButton.click()
removeUIInterruptionMonitor(alertHandler)
}
Hint about this behaviour in Apple's documentation:
When an alert or other modal UI is an expected part of the
test workflow, don't write a UI interruption monitor. The test won’t
use the monitor because the modal UI isn’t blocking the test. A UI
test only tries its UI interruption monitors if the elements it needs
to interact with to complete the test are blocked by an interruption
from an unrelated UI.
https://developer.apple.com/documentation/xctest/xctestcase/handling_ui_interruptions

Detect if Google Earth Plugin is in Street View mode

I just want to know if there is any way to determine if the current Google earth plug-in is in street view mode or not.
To test if street view is enabled in the Google Earth plugin:
var navcontrol = ge.getNavigationControl();
var response = navcontrol.getStreetViewEnabled(); // default=false
// response: true -> enabled; false -> disabled
if (!response) {
// to enable street view mode
navcontrol.setStreetViewEnabled(true);
}
If street view is enabled by calling setStreetViewEnabled(true) then user will be able to interactively enter StreetView by manually zooming in (or using pegman).
Note the API docs documents calling ge.getPlugin().streetViewEnabled(true) but this is an error since the getPlugin() method does not exist. The correct method is calling getNavigationControl().
Here's how to test if street view mode is active:
var la = ge.getView().copyAsLookAt(ge.ALTITUDE_RELATIVE_TO_GROUND);
var viewerOptions = la.getViewerOptions();
if (viewerOptions) {
var state = viewerOptions.getOption(ge.OPTION_STREET_VIEW);
if (state == ge.OPTION_STATE_ENABLED) {
// street view mode is active
// do something here
}
}
// if viewerOptions is null then no modes are active
State can be one of three values:
GEPlugin.OPTION_STATE_DEFAULT (0)
GEPlugin.OPTION_STATE_ENABLED (1)
GEPlugin.OPTION_STATE_DISABLED (2)
Reference: Interface options and GE Plugin.