Can I disable custom keyboards (iOS8) for my app? - ios

EDIT: tl;dr - it is possible, see accepted answer below.
Is there any (not only programatic) way of preventing custom keyboards (iOS8) from being used for my application? I am mainly interested in "per-app" setting, so just my app is not allowed to use custom keyboards, but disabling custom keyboards system-wide is last resort.
So far I know that custom keyboards are system-wide and can be used by any application. The OS will fallback to stock keyboard only for secure text entry (text fields with secureTextEntry set to YES). Not much hope here.
I got an impression from App Extension Programming Guide that MDM (Mobile Device Management) can restrict device from using custom keyboards at all, but I didn't find that option in the new beta version of Apple Configurator.app for OS X Yosemite. Is 'Configurator' just missing that option?
Any ideas here? Should I file a radar to suggest that Apple should introduce such functionality?

Looks like you got what you wanted in beta seed 3. Line 440 of UIApplication.h:
// Applications may reject specific types of extensions based on the extension point identifier.
// Constants representing common extension point identifiers are provided further down.
// If unimplemented, the default behavior is to allow the extension point identifier.
- (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier NS_AVAILABLE_IOS(8_0);
It's not currently included in the docs, but sound like it will do exactly what you asked here.
I'm guessing these "extension point identifiers" are not unique identifiers of extensions, but of their types, as there is also this on line 545:
// Extension point identifier constants
UIKIT_EXTERN NSString *const UIApplicationKeyboardExtensionPointIdentifier NS_AVAILABLE_IOS(8_0);
TLDR: to disable custom keyboards you would include something like this in your app delegate:
- (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier {
if ([extensionPointIdentifier isEqualToString: UIApplicationKeyboardExtensionPointIdentifier]) {
return NO;
}
return YES;
}

Swift 3 :
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplicationExtensionPointIdentifier) -> Bool {
if extensionPointIdentifier == UIApplicationExtensionPointIdentifier.keyboard {
return false
}
return true
}

I just want to add this for those developers who want to implement this method in Xamarin iOS. The idea is to override theShouldAllowExtensionPointIdentifier method in your AppDelegate:
public override bool ShouldAllowExtensionPointIdentifier(UIApplication application, NSString extensionPointIdentifier)
{
if (extensionPointIdentifier == UIExtensionPointIdentifier.Keyboard)
{
return false;
}
return true;
}

In Swift 5, UIApplicationExtensionPointIdentifier was changed to UIApplication.ExtensionPointIdentifier.
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
if extensionPointIdentifier == UIApplication.ExtensionPointIdentifier.keyboard {
return false
}
return true
}

In Swift 5 using a switch:
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
switch extensionPointIdentifier {
case .keyboard:
return false
default:
return true
}
}

Related

iOS - Intents eligible for in-app handling not work

I have an application with a deployment target on iOS 14 and built entirely with swiftui.
I have added an intentdefinition file and added a couple of intents with their respective handlers, and I have added to my app delegate the function to handle them:
struct ...App: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
...
}
extension AppDelegate {
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? {
switch intent {
case is NewTaskIntent:
return NewTaskHandler()
case is TodayTasksIntent:
return TodayTasksHandler()
default:
return nil
}
}
}
The problem is that this function is never called and when I run the shortcut from its app it always launches my app.
The info Plist contains the keys correctly:
<key>INIntentsSupported</key>
<array>
<string>NewTaskIntent</string>
<string>TodayTasksIntent</string>
</array>
Any idea what is happening?
If you are using SwiftUI, try onContinueUserActivity(_:perform:).
https://developer.apple.com/documentation/swiftui/view/oncontinueuseractivity(_:perform:)
Note: its first argument activityType is defined in your main app's Custom iOS Target Properties with NSUserActivityTypes key. This property seems to be generated by Xcode auto-ly.
Inspired by the "Step Six" in https://toolboxpro.app/blog/adding-shortcuts-to-an-app-part-four
However, it's quite wired that my func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? triggered yesterday, but makes no sense right now. Is it a 'feature' 😂

How to clear NSUserDefaults programmatically in XCUITest using Simulator

I've read several answers related to this and they suggest doing one of the following, but these options are not working for me. I have an XCUITest and I'm trying to clear the standard user defaults before before running the rest of my XCUITest. Currently my test app has a button that calls this code. I've also tried calling this code directly from within the XCUITest (I'm not sure if this is expected to work or if it needs to be run from within the app).
NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain];
I've also tried removing each individually:
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"MyKey1"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"MyKey2"];
I also tried each of the above methods followed by a synchronize call:
[[NSUserDefaults standardUserDefaults] synchronize];
The next time I read #"MyKey1" from the NSUserDefaults its still has the old value and has not been deleted.
Is there any way to remove an object from the NSUserDefaults programmatically when running an XCUITest in the simulator? These are automated tests, so I can't always manually click on "Reset Contents and Settings" in xcode.
Thanks!
There are couple of ways to solve your issues by setting the UserDefaults values before your tests run the app (not after).
Solution #1
Mock UserDefaults values for certain keys using launchArguments:
func testExample() {
let app = XCUIApplication()
app.launchArguments += ["-keepScreenOnKey", "YES"]
app.launch()
}
Note the minus sign before the keepScreenOnKey key. The minus sign indicates that it should take the next launch argument value as a value for that UserDefaults key.
Solution #2 (if solution #1 doesn’t work)
The previous solution might not work if you’re using the SwiftyUserDefaults library. In this case, there is one elegant solution: if the application is running under UI tests, erase all UserDefaults data from the app. How does the app know if it is being run under UI tests? By passing the launch arguments message, like this:
override func setUp() {
super.setUp()
app.launchArguments += ["UI-Testing"]
}
Then, in AppDelegate.swift check if the app is running under UI-Testing and remove all UserDefaults data (like you do):
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
setStateForUITesting()
return true
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("UI-Testing")
}
}
private func setStateForUITesting() {
if AppDelegate.isUITestingEnabled {
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
}
}
}
Solution #3 (if solution #2 is not enough)
But what if the UI test expects states other than the default state set by solution #2? Bools default to false, Integers to 0 and Strings to "", but what if you need true for the Bool key?
Do something like this:
func testExample() {
app.launchEnvironment["UI-TestingKey_keepScreenOn"] = "YES"
app.launch()
}
And then in AppDelegate.swift file:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static let uiTestingKeyPrefix = "UI-TestingKey_"
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if AppDelegate.isUITestingEnabled {
setUserDefaults()
}
return true
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("UI-Testing")
}
}
private func setUserDefaults() {
for (key, value)
in ProcessInfo.processInfo.environment
where key.hasPrefix(AppDelegate.uiTestingKeyPrefix) {
// Truncate "UI-TestingKey_" part
let userDefaultsKey = key.truncateUITestingKey()
switch value {
case "YES":
UserDefaults.standard.set(true, forKey: userDefaultsKey)
case "NO":
UserDefaults.standard.set(false, forKey: userDefaultsKey)
default:
UserDefaults.standard.set(value, forKey: userDefaultsKey)
}
}
}
}
extension String {
func truncateUITestingKey() -> String {
if let range = self.range(of: AppDelegate.uiTestingKeyPrefix) {
let userDefaultsKey = self[range.upperBound...]
return String(userDefaultsKey)
}
return self
}
}
Please note that this example only works for Bool and String keys. If you need more scalability, the switch command should be modified to somehow check if the value is Integer or Double or Any other value, but the general idea is here.
EDIT: It looks like the reason for using solutions #2 and #3 is not valid anymore as of SwiftyUserDefaults version 4.0.0-beta.1 as they've added support for setting values through launch arguments. But, I have to admit that I have not tested SwiftyUserDefaults library from this version onward, so I'll keep both solutions here.
With UI tests, the app runs as a separate process. You would need to call the methods to clear NSUserDefaults from within the app itself.
We have our UI Test pass a resetNSUserDefaults flag to the app when it launches the app. The app then clears the NSUserDefaults early in the launch process.

How to properly check if user has enabled "Allow Full Access" to custom keyboard extension

I've tried prowling stackoverflow, and I've found some answers where you:
1) update UIPasteboard, which I don't want to be altering for obvious reasons (Allow Full Access check in keyboards iOS10)
2) check NSUserdefaults, drawbacks are the keyboard must be opened, and allow full access be enabled, and if it is later disabled, there will be the false value (https://forums.developer.apple.com/thread/28690)
Looking for iOS 10, Swift 3 solution. Thank you!
this is what I use in my code in swift
var hasAccess: Bool {
get{
if #available(iOSApplicationExtension 11.0, *) {
return self.hasFullAccess
} else {
return UIDevice.current.identifierForVendor != nil
}
}
}

How to use requestReview (SKStore​Review​Controller) to show review popup in the current viewController after a random period of time

I've read about this new feature available in iOS 10.3 and thought it will be more flexible and out of the box. But after I read the docs I found out that you need to decide the time to show it and the viewController who calls it. Is there any way I can make it trigger after a random period of time in any viewController is showing at that moment?
In your AppDelegate:
Swift:
import StoreKit
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let shortestTime: UInt32 = 50
let longestTime: UInt32 = 500
guard let timeInterval = TimeInterval(exactly: arc4random_uniform(longestTime - shortestTime) + shortestTime) else { return true }
Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(AppDelegate.requestReview), userInfo: nil, repeats: false)
}
#objc func requestReview() {
SKStoreReviewController.requestReview()
}
Objective-C:
#import <StoreKit/StoreKit.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
int shortestTime = 50;
int longestTime = 500;
int timeInterval = arc4random_uniform(longestTime - shortestTime) + shortestTime;
[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:#selector(requestReview) userInfo:nil repeats:NO];
}
- (void)requestReview {
[SKStoreReviewController requestReview];
}
The code above will ask Apple to prompt the user to rate the app at a random time between 50 and 500 seconds after the app finishes launching.
Remember that according to Apple's docs, there is no guarantee that the rating prompt will be presented when the requestReview is called.
For Objective - C:
Add StoreKit.framework
Then in your viewController.h
#import <StoreKit/StoreKit.h>
Then in your function call :
[SKStoreReviewController requestReview];
For Swift
Add StoreKit.framework
In your ViewController.swift
import StoreKit
Then in your function call :
if #available(iOS 10.3, *) {
SKStoreReviewController.requestReview()
} else {
// Open App Store with OpenURL method
}
That's it ! Apple will take care of when it would show the rating (randomly).
When in development it will get called every time you call it.
Edited : No need to check OS version, StoreKit won't popup if the OS is less than 10.3, thank Zakaria.
Popping up at a random time is not a good way to use that routine, and is not only in contravention of Apple's advice, but will give you less-than-great results.
Annoying the user with a pop up at a random time will never be as successful as prompting them at an appropriate time- such as when they have just completed a level or created a document, and have that warm fuzzy feeling of achievement.
Taking Peter Johnson's advice, I created a simple class where you can just stick the method in at the desired spot in your code and it'll pop up at a spot where the user's just had a success.
struct DefaultKeys {
static let uses = "uses"
}
class ReviewUtility {
// Default Keys stored in Structs.swift
static let sharedInstance = ReviewUtility()
private init() {}
func recordLaunch() {
let defaults = UserDefaults.standard
// if there's no value set when the app launches, create one
guard defaults.value(forKey: DefaultKeys.uses) != nil else { defaults.set(1, forKey: DefaultKeys.uses); return }
// read the value
var totalLaunches: Int = defaults.value(forKey: DefaultKeys.uses) as! Int
// increment it
totalLaunches += 1
// write the new value
UserDefaults.standard.set(totalLaunches, forKey: DefaultKeys.uses)
// pick whatever interval you want
if totalLaunches % 20 == 0 {
// not sure if necessary, but being neurotic
if #available(iOS 10.3, *) {
// do storekit review here
SKStoreReviewController.requestReview()
}
}
}
}
To use it, stick this where you want it to be called and hopefully you won't tick off users with randomness.
ReviewUtility.sharedInstance.recordLaunch()
Showing the dialog at random time is not probably a good idea. Please see the Apple guideline which mentions: Don’t interrupt the user, especially when they’re performing a time-sensitive or stressful task.
This is what Apple suggests:
Ask for a rating only after the user has demonstrated engagement with your app. For example, prompt the user upon the completion of a game level or productivity task. Never ask for a rating on first launch or during onboarding. Allow ample time to form an opinion.
Don’t be a pest. Repeated rating prompts can be irritating, and may even negatively influence the user’s opinion of your app. Allow at least a week or two between rating requests and only prompt again after the user has demonstrated additional engagement with your app.
This post is also quite interesting...
I cant add comments yet but if you are using Appirater you might want to check the version to see if its lower than 10.3 so the other Appirater review message box pops up.

How to detect whether custom keyboard is activated from the keyboard's container app?

I was wondering if there is a method that would allow me to detect from the keyboard container app whether the associated keyboard has been activated in the the device's Settings app.
For example, I am interested in adding a simple "steps" feature inside the container app where step 1 would be "activate the keyboard", and step 2 would be contingent on step 1's completion. As such, I am interested in figuring out whether there is a way to detect whether the keyboard extension is activated?
Thanks!
Here is a method I have used in one of my projects. I think it is what you asked for, hope it helps you.
- (BOOL)isCustomKeyboardEnabled {
NSString *bundleID = #"com.company.app.customkeyboard"; // Replace this string with your custom keyboard's bundle ID
NSArray *keyboards = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] objectForKey:#"AppleKeyboards"]; // Array of all active keyboards
for (NSString *keyboard in keyboards) {
if ([keyboard isEqualToString:bundleID])
return YES;
}
return NO;
}
Just in case here is Swift version of Kurt's brilliant and awesome answer:
func isKeyboardExtensionEnabled() -> Bool {
guard let appBundleIdentifier = Bundle.main.bundleIdentifier else {
fatalError("isKeyboardExtensionEnabled(): Cannot retrieve bundle identifier.")
}
guard let keyboards = UserDefaults.standard.dictionaryRepresentation()["AppleKeyboards"] as? [String] else {
// There is no key `AppleKeyboards` in NSUserDefaults. That happens sometimes.
return false
}
let keyboardExtensionBundleIdentifierPrefix = appBundleIdentifier + "."
for keyboard in keyboards {
if keyboard.hasPrefix(keyboardExtensionBundleIdentifierPrefix) {
return true
}
}
return false
}
The current documentation states By default, your extension and its containing app have no direct access to each other’s containers.
It is also stating that the container app can share data with the keyboard in the following fashion:
// Create and share access to an NSUserDefaults object.
NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc]
initWithSuiteName:#"com.example.domain.MyShareExtension"];
// Use the shared user defaults object to update the user's account.
[mySharedDefaults setObject:theAccountName forKey:#"lastAccountName"];
Read more on this: Communicating and persisting data between apps with App Groups
Obstacle no 1: According to the documentation, for this to work, the RequestsOpenAccess in the plist needs to be set to YES as it would gain the following capability:
Option to use a shared container with the keyboard’s containing app,
which enables features such as providing a custom lexicon management
UI in the containing app
Requesting full access for a simple case like this is definitely not preferred on my side.
Obstacle no 2: Using this knowledge of setting a NSUserDefault, leaves me to think of a method where this can be set in place. But there's no public method indicating an extension is installed. So this is a dead end for now.
--
[Update 1]
Not super relevant but still worth stating: the shouldAllowExtensionPointIdentifier app delegate method in combination with the constant UIApplicationKeyboardExtensionPointIdentifier can deal with disallowing custom keyboards. The extension point identifiers are not unique identifiers of the extension but of their type.
Read more on this: Can I disable custom keyboards (iOS8) for my app?
--
[Update 2]
Another question with same issue, but w/o solution: How to detect an app extension is enabled in containing app on iOS 8?
--
This is a work-in-progress answer stating my findings so far which I hope to be updating coming days should I find a solution.
You can use this function (Swift 3 and 4) to check your custom keyboard extension have open access or not:
func isOpenAccessGranted() -> Bool{
if #available(iOS 10.0, *) {
let originalString = UIPasteboard.general.string
UIPasteboard.general.string = "Sour LeangChhean"
if UIPasteboard.general.hasStrings {
UIPasteboard.general.string = originalString ?? ""
return true
}else{
UIPasteboard.general.string = ""
return false
}
} else {
// Fallback on earlier versions
if UIPasteboard.general.isKind(of: UIPasteboard.self) {
return true
}else{
return false
}
}
}

Resources