XCUITest application shortcuts - swift

Is there any way of testing the UIApplication shortcuts within XCUITests?
I know that in order to test 3d shortcuts in a simulator you need a trackpad with force touch, but I was wondering if I could write tests that test my shortcuts.

Looks like yes! You'll need to expose a private API, though. Exported XCUIElement header from Facebook's WebDriverAgent here.
Or, if that's the only thing you need, just expose it ala:
#interface XCUIElement (Private)
- (void)forcePress;
#end
And then to force press your icon by going to the springboard and getting your icon element see my answer here.
class Springboard {
// this is another private method you'll need to expose (see linked other answer)
static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")
/// Open the 3d touch menu
class func open3dTouchMenu() {
XCUIApplication().terminate() // this may or may not be necessary depending on your desired function / app state
// Resolve the query for the springboard rather than launching it
springboard.resolve()
// Force delete the app from the springboard
let icon = springboard.icons["MyAppName"]
if icon.exists {
icon.forcePress()
// do something with the menu that comes up
}
}
}
* I have not tested this yet.

Related

Accessibility Permissions for Development in macOS and XCode

Is there a way to give an app I am developing in XCode accessibility permissions by default during development. The idea is that I could hit the run key and test the new code without having to jump through the hoops in the settings. For deployment obviously it wouldn't work, but for development is there a way to basically whitelist the app?
EDIT: This is a new method I've found/created that is working and has the added benefit of prompting the user first too, improving the onboarding experience of your app. The original method (which I've left at the bottom of this post) still prompted for accessibility on each new build of the app unfortunately.
I had to get this working in my macOS app and had another idea and way to approach it, given the user needs to action it anyway, I just have the app present the required dialogue on each run and during the app build, I run a script to clear the existing permissions.
For reference, see v1.3.0 of my Auto Clicker macOS app, specifically:
Services/PermissionsService.swift (whole class)
Init/AppDelegate.swift (line 21)
Init/AutoClickerApp.swift (line 32 & 40)
auto-clicker.xcodeproj/project.pbxproj (line 435)
The links are links to the tagged version, so should never change or break. There is quite a bit of code, I'll try to summarise here.
Firstly, I created a new class to manage permissions related functionality to keep things contextual:
//
// PermissionsService.swift
// auto-clicker
//
// Created by Ben on 10/04/2022.
//
import Cocoa
final class PermissionsService: ObservableObject {
// Store the active trust state of the app.
#Published var isTrusted: Bool = AXIsProcessTrusted()
// Poll the accessibility state every 1 second to check
// and update the trust status.
func pollAccessibilityPrivileges() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.isTrusted = AXIsProcessTrusted()
if !self.isTrusted {
self.pollAccessibilityPrivileges()
}
}
}
// Request accessibility permissions, this should prompt
// macOS to open and present the required dialogue open
// to the correct page for the user to just hit the add
// button.
static func acquireAccessibilityPrivileges() {
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
let enabled = AXIsProcessTrustedWithOptions(options)
}
}
Then, I added the following to my AppDelegate (baring in mind this was in Swift UI):
//
// AppDelegate.swift
// auto-clicker
//
// Created by Ben on 30/03/2022.
//
import Foundation
import Cocoa
final class AppDelegate: NSObject, NSApplicationDelegate {
// When the application finishes launching, request the
// accessibility permissions from the service class we
// made earlier.
func applicationDidFinishLaunching(_ notification: Notification) {
PermissionsService.acquireAccessibilityPrivileges()
}
}
Then finally, in our Swift UI app init, lets add some UI feedback to the user that we are waiting for permissions and listen to that isTrusted published property we setup earlier in the permissions service class that gets polled every second to unlock the UI when the user has granted the required permissions:
//
// AutoClickerApp.swift
// auto-clicker
//
// Created by Ben on 12/05/2021.
//
import Foundation
import SwiftUI
import Defaults
#main
struct AutoClickerApp: App {
// Create an instance of the permissions service class that we
// can observe for the trusted state change.
#StateObject private var permissionsService = PermissionsService()
var body: some Scene {
// Wait for trust permissions being granted from the user,
// displaying a blocking permissions view telling the user
// what to do and then presenting the main application view
// automatically when the required trust permissions are granted.
WindowGroup {
if self.permissionsService.isTrusted {
MainView()
} else {
PermissionsView()
}
.onAppear(perform: self.permissionsService.pollAccessibilityPrivileges)
}
}
}
You can see the blocking view I made in the app above, Views/Main/PermissionsView.swift.
Then in order to automatically clear the permissions during the app build, I added a new build script to the project that runs the following against /bin/sh:
tccutil reset Accessibility $PRODUCT_BUNDLE_IDENTIFIER
As seen in auto-clicker.xcodeproj/project.pbxproj (line 435).
This means I'll be prompted with the system dialogue on each app build to just press the + button and add the app.
Unfortunately this is the most frictionless way I've found to develop the app with these permissions being required.
Outdated answer:
Found a way to do this after some trial and error, navigating through Xcode's (>11, currently 13) new build system.
With Xcode open and having it as the foreground app (so it takes over the menu bar with its menu items), do the following:
From the menu bar, select 'Xcode'
If your project doesn't yet have a workspace, click 'Save as Workspace...' near the bottom of the list and save the Workspace alongside your *.xcodeproj so they should be in the same directory. From now on you'll open your project via the new *.xcworkspace Workspace instead of your *.xcodeproj Project.
From the menu bar, select 'Xcode' again
Click 'Workspace Settings...' near the bottom of the list
Under 'Derived Data' select 'Workspace-relative Location', if you want to customise the path do so now
Click 'Done' in the bottom right
This puts the build location of our *.app binary within the project so its easy to find, along with also allowing us to check the change into source control as we now have the Workspace settings stored in the *.xcworkspace file.
Next we need to now point the permissions at the above build binaries location, so:
Open System Preferences
Click 'Security & Privacy'
Click the padlock in the bottom right to make changes
Select 'Accessibility' from the left side list
Click the plus button at the bottom left of the list and find the *.app file to add it to the list that we put within the project directory, this should be something like $PROJECT_DIR/DerivedData/$PROJECT/Build/Products/Debug/*.app
Click the checkbox to the left of the app to check it if its not already checked
Restart the app
Any builds should now have the relevant permissions.
Just to note though, this will overwrite the permissions of any archived/prod builds as the app names and binaries are the same. They are easily added back though, just delete the permission for the build app and instead point it back at your prod app, usually in /Applications.

Programmatically minimize all windows or besides my Cocoa application?

I haven't been able to find anything about this in Swift. Is there a way to programmatically make my application minimize all other windows open in the background, or even just minimize Safari? I basically want my application to run against the desktop, without any clutter in the background. Is there a way to programmatically do this for a Cocoa app? I'm pretty new to swift, so any help would be appreciated.
You can use api on NSWorkspace which allows you to hide all visible app in the background.
You can find more about NSWorkspace here: link
Hides all applications other than the sender. This method must be called from your app’s main thread.
NSWorkspace.shared().hideOtherApplications()
If you only want to hide Safari,
let appPath = NSWorkspace.shared().fullPath(forApplication: "Safari")
let identifier = Bundle.init(path: appPath!)?.bundleIdentifier
if let bundleID = identifier {
let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier:bundleID )
if !runningApps.isEmpty {
runningApps.first?.hide()
}
}

How to reload an application from a 'play again?' button

Using Xcode 7.3, writing Swift program; searched for this topic but could not find an answer. Apologies, in advance. Last program language was COBOL.
Application background:
It is a single view application, User keys in data and at some point the User 'wins' - at that point, two buttons are available: 'Quit?' and 'Play Again?'
For the 'Quit?' button I have:
// MARK: C.This IBAction used to quit the game
#IBAction func quitIt(sender: AnyObject)
{
exit(0)
}
Hopefully, the above is the most efficient way.
What I need help on is the 'Play Again?' button:
// TODO: B. This IBAction used to play another game
#IBAction func Playagain(sender: AnyObject)
{
}
What code do I put in?
Move the startup code for your game to it's own func(tion).
Call that function both from [wherever you moved it from] and also in PlayAgain().
I hope this is for OS X rather than iOS - Apple will probably reject your iOS app if it exits. They consider that "crashing".
https://developer.apple.com/library/ios/qa/qa1561/_index.html

NSApplication keyWindow is nil during applicationDidFinishLaunching

Starting with a blank OS X application project, I add the following code to applicationDidFinishLaunching.
func applicationDidFinishLaunching(aNotification: NSNotification) {
let app = NSApplication.sharedApplication()
guard let window = app.keyWindow else {
fatalError("No keyWindow\n")
}
print(window)
}
At launch I hit the error case because my local window variable is nil. Yet when I show the contents of the app variable, I see a valid value for _keyWindow. Also notice that the blank GUI Window is being displayed on the screen next to the stack dump.
Why does the keyWindow: NSWindow? property return nil in this case?
Thanks
NSApplication's keyWindow property will be nil whenever the app is not active, since no window is focused for keyboard events. You can't rely on it being active when it finished launching because the user may have activated some other app between when they launched your and when it finished launching, and Cocoa is designed to not steal focus.
To some extent, you may be seeing that happen more when launching from Xcode because app activation is a bit strange in that case. But, still, you must not write your applicationDidFinishLaunching() method to assume that your app is active.
What you're seeing in terms of the app's _keyWindow instance variable is, of course, an implementation detail. One can't be certain about what it signifies and you definitely should not rely on it. However, I believe it's basically the app's "latent" key window. It's the window that will be made key again when the app is activated (unless it is activated by clicking on another of its windows).

Change language in app - how to restart?

I want to give the user the possibility to change the language in my app.
The way to do this is described here, in monotouch code to set the preferred language to Dutch and alternate language to English:
NSUserDefaults.StandardUserDefaults.SetValueForKey
(NSArray.FromStrings("nl", "en"), new NSString("AppleLanguages"));
You have to restart the application before this will take effect. But on the iPhone 4 the app does not restart when you close it, it is just hidden. Is there a way to force an app to restart after closing?
Thanks Dimitris. So changing the language at runtime is not that simple.
I found a solution which works in my case:
When the user changes the language I use the solution described by Mauro Delrio in "How to force NSLocalizedString to use a specific language". In monotouch:
string newLanguage = "nl";
myBundle = NSBundle.FromPath(NSBundle.MainBundle.PathForResource(newLanguage, "lproj"));
All strings will now be loaded in the selected language with myBundle.LocalizedString(...). Of course, everything which was already printed on a view is not yet translated. But I found an easy way to reset all views. In my app I use a MainTabController which looks like this:
public class MainTabBarController : UITabBarController
{
public override void ViewDidLoad()
{
Reset();
SelectedIndex = 2;
}
public void Reset()
{
ViewControllers = new UIViewController[]
{
new ViewControllerTab1(),
new ViewControllerTab2(),
new ViewControllerTab3(),
new ViewControllerTab4(),
new ViewControllerTab5()
};
}
}
So all I have to do is call Reset like:
((AppDelegate)UIApplication.SharedApplication.Delegate).MainTabBarController.Reset();
All current views are disposed and re-created in the correct language. Seems like a trick, but it is perfectly legal and documented, see Apple documentation for MainTabBarController viewControllers property. It even activates the same tab index as the one which was active, so for the user it seems that nothing but the language is changed.
Of course, any unsaved data in all views is lost, so if this is a problem, you have to find a way to save this before resetting.
No there is no way to restart an app. You can only force it to terminate when the users presses the home button by setting the property "UIApplicationExitsOnSuspend" in your Info.plist file to true.
The trick to use specific language by selecting it from the app is to force the NSLocalizedString to use specific bundle depending on the selected language ,
here is the post i have written for this http://learning-ios.blogspot.com/2011/04/advance-localization-in-ios-apps.html
and here is the code of one sample app https://github.com/object2dot0/Advance-Localization-in-ios-apps
As an additional note, to force the UIBarButtonSystemItem to change their locale, you have to add
<key>CFBundleDevelopmentRegion</key>
<string>nl</string>
to your info.plist. Just fire up TextEdit and place it somewhere. Hope this helps!