Xcode UI Test How to handle notification permissions generated by UNUserNotificationCenter - swift

In my app, I ask for notification permissions like this:
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
Now while testing, I need to handle this permission popup but it is not working, I tried these code:
XCUIApplication().alerts["“AppName” Would Like to Send You Notifications"].buttons["Allow"].tap() //didn't work.
addUIInterruptionMonitorWithDescription("Notification Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
addUIInterruptionMonitorWithDescription("“AppName” Would Like to Send You Notifications") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
addUIInterruptionMonitorWithDescription("Notifications may include alerts, sounds, and icon badges. These can be configured in Settings.") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
But nothing works.

Try this
let app2 = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let button = app2.alerts.firstMatch.buttons["Allow"]
button.waitForExistence(timeout: 10)
button.tap()

1/ Try recording the path with XCode test recorder to alert button and make sure, you have the right one.
XCUIApplication().alerts["“AppName” Would Like to Send You Notifications"].buttons["Allow"].tap() //didn't work.
might not be correct.
(note: Using test recorder for things, like finding the path to element is ok and very newbie-friendly.)
2/ You don't have to use addUIInterruptionMonitorWithDescription in case, you have a system alert that will happen every time or in case, you know that it will happen.
Just use waitForExistance and tap it if it exists.
(note: I've found that waiting & tapping on system notification is better than addUIInterruptionMonitorWithDescription, which is unstable/complicated sometimes)
3/ alert.buttons["Allow"].tap() doesn't seem to be correct to me, shouldn't it be XCUIApplication().alerts.buttons["Allow"]? Alternatively, you can use XCUIApplication().alerts.buttons.element(boundBy: 0 to tap on first button.

import XCTest
// For multiple permission requests
var isPhotoPermissionAvailable: Bool?
var isLocationPermissionAvailable: Bool?
var isNotificationsPermissionAvailable: Bool?
extension XCTestCase {
func allowPhotos(_ enabled: Bool = true) {
addUIInterruptionMonitor(withDescription: "Photo Permissions") { (alert) -> Bool in
isPhotoPermissionAvailable = enabled
var alertButton = alert.buttons["Don't Allow"]
if enabled == true {
alertButton = alert.buttons["OK"]
}
if alertButton.exists {
alertButton.tap()
return true
}
XCUIApplication().tap()
return false
}
if isPhotoPermissionAvailable == nil {
XCUIApplication().swipeUp() //with this code, alert.buttons["OK"] exsists
}
}
func allowLocation(_ enabled: Bool = true) {
addUIInterruptionMonitor(withDescription: "Location Dialog") { (alert) -> Bool in
isLocationPermissionAvailable = enabled
var alertButton = alert.buttons["Don't Allow"]
if enabled == true {
alertButton = alert.buttons["Always Allow"]
}
if alertButton.exists {
alertButton.tap()
return true
}
XCUIApplication().tap()
return false
}
if isLocationPermissionAvailable == nil {
XCUIApplication().swipeUp()
}
}
func allowNotifications(_ enabled: Bool = true) {
addUIInterruptionMonitor(withDescription: "Notification Dialog") { (alert) -> Bool in
isNotificationsPermissionAvailable = enabled
var alertButton = alert.buttons["Don't Allow"]
if enabled == true {
alertButton = alert.buttons["Allow"]
}
if alertButton.exists {
alertButton.tap()
return true
}
XCUIApplication().tap()
return false
}
if isNotificationsPermissionAvailable == nil {
XCUIApplication().swipeUp()
}
}
}

Related

Add haptic feedback to button in SwiftUI

I am able to create my custom style of button with a ButtonStyle but I would like to add haptic feedback when the button is touched down and let go of. I know there is a configuration.isPressed variable available in ButtonStyle, but how do I give haptic feedback when it actually changes.
I know how to give haptic feedback but need help knowing when the user touches the button and let's go.
I have tried this extension as a button:
struct TouchGestureViewModifier: ViewModifier {
let touchBegan: () -> Void
let touchEnd: (Bool) -> Void
let abort: (Bool) -> Void
#State private var hasBegun = false
#State private var hasEnded = false
#State private var hasAborted = false
private func isTooFar(_ translation: CGSize) -> Bool {
let distance = sqrt(pow(translation.width, 2) + pow(translation.height, 2))
return distance >= 20.0
}
func body(content: Content) -> some View {
content.gesture(DragGesture(minimumDistance: 0)
.onChanged { event in
guard !self.hasEnded else { return }
if self.hasBegun == false {
self.hasBegun = true
self.touchBegan()
} else if self.isTooFar(event.translation) {
self.hasAborted = true
self.abort(true)
}
}
.onEnded { event in
print("ended")
if !self.hasEnded {
if self.isTooFar(event.translation) {
self.hasAborted = true
self.abort(true)
} else {
self.hasEnded = true
self.touchEnd(true)
}
}
self.hasBegun = false
self.hasEnded = false
})
}
}
// add above so we can use it
extension View {
func onTouchGesture(touchBegan: #escaping () -> Void = {},
touchEnd: #escaping (Bool) -> Void = { _ in },
abort: #escaping (Bool) -> Void = { _ in }) -> some View {
modifier(TouchGestureViewModifier(touchBegan: touchBegan, touchEnd: touchEnd, abort: abort))
}
}
Then I do this on a view:
.onTouchGesture(
// what to do when the touch began
touchBegan: {
// tell the view that the button is being pressed
self.isPressed = true
// play click in sound
playSound(sound: "clickin-1", type: "wav")
// check if haptic feedback is possible
if self.engine != nil {
// if it is - give some haptic feedback
let pattern = try? CHHapticPattern(events: [self.event], parameters: [])
let player = try? self.engine?.makePlayer(with: pattern!)
try? self.engine!.start()
try? player?.start(atTime: 0)
}
},
// what to do when the touch ends. sometimes this doesnt work if you hold it too long :(
touchEnd: { _ in
// tell the view that the user lifted their finger
self.isPressed = false
playSound(sound: "clickout-1", type: "wav")
// check if haptic feedback is possible
if self.engine != nil {
// if it is - give some haptic feedback
let pattern = try? CHHapticPattern(events: [self.event], parameters: [])
let player = try? self.engine?.makePlayer(with: pattern!)
try? self.engine!.start()
try? player?.start(atTime: 0)
}
},
// if the user drags their finger away, abort the action
abort: { _ in
self.isPressed = false
}
)
But it gets stuck halfway through sometimes which is not very useable for users. How do I do it on the more reliable Button with ButtonStyle?

Swift: How to activate and unhide window of ANY application?

I see pretty strange behaviour of NSRunningApplicationInstance.activate() method.
Let's imagine that I have found app named "Finder". This app at the moment is:
Not active and/or hidden
I have really simple code:
let activeOptions: NSApplication.ActivationOptions = [.activateAllWindows, .activateIgnoringOtherApps]
print("1. isActive: \(app.isActive); isHidden: \(app.isHidden)")
if (!app.isActive)
{
app.activate(options: activeOptions)
}
if ( app.isHidden )
{
app.unhide()
}
print("2. isActive: \(app.isActive); isHidden: \(app.isHidden)")
for finding needed app you can use the following code:
let app = NSWorkspace.shared.runningApplications.filter{ $0.localizedName = "NameOfApp"}
2 times runned code result:
isActive: false; isHidden: false
isActive: false; isHidden: false
isActive: true; isHidden: false
isActive: true; isHidden: false
If you will try it... :
Code is will show me menu of the app:
BUT only on second code run! (Why?)
Will not show me app window! (Why?)
And I see similar behaviour with a lot of applications, not only with Finder.
As example SourceTree app.
Can somebody explain the logic and how do display window of the ANY of runned app by some code IN ANY CASE?
Here is working Playground module. The approach is to use KVO for observable properties to be informed when exactly desired state for target application occurs. Hope it would be helpful somehow.
import Cocoa
class AppActivator: NSObject {
private var application: NSRunningApplication!
private let filterName: String
init(appName: String) {
filterName = appName
}
func activate() {
guard let app = NSWorkspace.shared.runningApplications.filter ({
return $0.localizedName == self.filterName || $0.bundleIdentifier?.contains(self.filterName) ?? false
}).first else {
print("Application \(self.filterName) not found")
return
}
guard app.activationPolicy != .prohibited else {
print("Application \(self.filterName) prohibits activation")
return
}
self.application = app
self.unhideAppIfNeeded()
self.activateAppIfNeeded()
}
private func unhideAppIfNeeded() {
if application.isHidden {
application.addObserver(self, forKeyPath: "isHidden", options: .new, context: nil)
application.unhide()
}
}
private func activateAppIfNeeded() {
if !application.isHidden && !application.isActive {
application.addObserver(self, forKeyPath: "isActive", options: .new, context: nil)
application.activate(options: .activateIgnoringOtherApps)
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "isHidden" {
application.removeObserver(self, forKeyPath: "isHidden")
activateAppIfNeeded()
} else if keyPath == "isActive" {
application.removeObserver(self, forKeyPath: "isActive")
print("Application \(application.localizedName) - ACTIVATED!")
}
}
}
let activator = AppActivator(appName: "Finder")
activator.activate()

What is the best way to wrap `purchaserInfo` for easy comparisons throughout your code using RevenueCat

What would be the best way to wrap Purchases.shared.purchaserInfo to just return true or false when doing multiple comparisons in an If statement.
What I want to be able to do is to quickly check if the user has access to PRO features by having a method or a property that return true or false since I need to make multiple checks in an if statement, something like... if userHasAccess && someOtherCheck { // do something}
Is this a valid solution?
Singleton Class:
class IAPHelper {
static let shared = IAPHelper()
func hasAccessToProFeatures()->Bool{
var hasAccess:Bool?
Purchases.shared.purchaserInfo { (purchaserInfo, error) in
if let purchaserInfo = purchaserInfo {
if purchaserInfo.activeEntitlements.contains("myEntitlement") {
hasAccess = true
}else{
hasAccess = false
}
}
}
return hasAccess!
}
}
Usage:
if IAPHelper.shared.hasAccessToProFeatures() && someOtherCheck{
// unlock PRO features
}
Shared instances are not bad, however for such a small class you could declare hasAccessToProFeatures as a static variable instead.
class IAPHelper {
static var hasAccessToProFeatures: Bool {
var hasAccess:Bool?
Purchases.shared.purchaserInfo { (purchaserInfo, error) in
if let purchaserInfo = purchaserInfo {
if purchaserInfo.activeEntitlements.contains("myEntitlement") {
hasAccess = true
}else{
hasAccess = false
}
}
}
guard let isProMember = hasAccess else {
return false
}
return isProMember
}
}
Then you could call it like:
if IAPHelper.hasAccessToProFeatures && someOtherCheck {
// unlock PRO features
}

Button Click Twice

As we all know, to avoid clicking twice, we can set the code bellow on the tap method and add a HUD such as SVProgress.show().
isUserInteractionEnabled = false
After the network request, set it to true and SVProgress.dismiss().
I wonder if there is a method to extract the function for those button which needs to send a request. I have thought to use method swizzling. Add this to the button extension, the codes is bellow. It seems not good. Do you guys have some good ways to extract the function? Using inheritance, protocol or something else?
extension UIButton {
private struct AssociatedKeys {
static var cp_submitComplete = "cp_submitComplete"
static var cp_defaultMessage:String = NSLocalizedString("Loading", comment: "prompt")
static var cp_customMessage = "cp_customMessage"
}
var submitNotComplete: Bool {
get {
let objc_Get = objc_getAssociatedObject(self, &AssociatedKeys.cp_submitComplete)
if objc_Get != nil {
if let objc_Get = objc_Get as? Bool, objc_Get == true {
return true
}
return false
} else {
return false
}
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.cp_submitComplete, newValue as Bool, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
if !newValue {
isUserInteractionEnabled = true
SVProgressHUD.dismiss()
}
}
}
var customMessage: String {
get {
let cp_customMessage = objc_getAssociatedObject(self, &AssociatedKeys.cp_customMessage)
if let message = cp_customMessage {
return message as! String
} else {
return AssociatedKeys.cp_defaultMessage
}
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.cp_customMessage, newValue as String, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
override open class func initialize() {
if self == UIButton.self {
DispatchQueue.once(NSUUID().uuidString, block: {
let systemSel = #selector(UIButton.sendAction(_:to:for:))
let swizzSel = #selector(UIButton.cpSendAction(_:to:for:))
let systemMethod = class_getInstanceMethod(self, systemSel)
let swizzMethod = class_getInstanceMethod(self, swizzSel)
let isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod))
if isAdd {
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
} else {
method_exchangeImplementations(systemMethod, swizzMethod);
}
})
}
}
private dynamic func cpSendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
cpSendAction(action, to: target, for: event)
if submitNotComplete {
//begin submit
isUserInteractionEnabled = false
SVProgressHUD.show(withStatus: customMessage)
}
}
}
I think it's a bad idea to handle this kind of logic in UIButton. I would rather make the view controller responsible for enabling/disabling the button.
func handleTap(_ sender: UIButton) {
sender.isEnabled = false
SVProgressHUD.show(withStatus: customMessage)
doSomeTaskAsync(withCompletion: {
sender.isEnabled = true
SVProgressHUD.dismiss()
})
}

Login Item - cocoa

Is there a way to check if the login item already exists (with bundleIdentifier of the app?) I want to be able to see if there is a login item and if it is enable.
I was trying to check my checkbox in applicationDidFinishLuanching when the login item is enabled using this:
if (SMLoginItemSetEnabled(("bundleIDOfMyApp" as CFStringRef), true)) {
self.startAtLoginButton.state = 1
} else {
self.startAtLoginButton.state = 0
}
It does its thing, but it also launches my helper application.
Another thing is this:
#IBAction func startAtLoginButtonChecked(sender: NSButton) {
var enabled = false
if sender.state == 0 { enabled = false }
if sender.state == 1 { enabled = true }
if !SMLoginItemSetEnabled(("bundleIDOfMyApp" as CFStringRef), enabled) {
print("Login was not successful")
}
}
As far as I am concerned this is the way you implement checkbox to enable/disable login item.
What it does in my app is every time I check the box it launches the helper app (which launches my app again).
Although the method SMCopyAllJobDictionaries() is deprecated this is the usual way to check if the job is enabled, SMLoginItemSetEnabled is only be used to set the value
import ServiceManagement
let jobDicts = SMCopyAllJobDictionaries( kSMDomainUserLaunchd ).takeRetainedValue() as NSArray as! [[String:AnyObject]]
let label = "bundleIDOfMyApp"
let jobEnabled = jobDicts.filter { $0["Label"] as! String == label }.isEmpty == false
The double casting is needed to cast CFArray to NSArray and then to Array<String,AnyObject>
Also usually the checkbox is bound to a property via KVC. The lines above are the getter and SMLoginItemSetEnabled is the setter for example
let helperBundleIdentifier = "bundleIDOfMyApp"
#available(OSX, deprecated=10.10) // this line suppresses the 'deprecated' warning
dynamic var startAtLogin : Bool {
get {
guard let jobDicts = SMCopyAllJobDictionaries( kSMDomainUserLaunchd ).takeRetainedValue() as NSArray as? [[String:AnyObject]] else { return false }
return jobDicts.filter { $0["Label"] as! String == helperBundleIdentifier }.isEmpty == false
} set {
if !SMLoginItemSetEnabled(helperBundleIdentifier, newValue) {
print("SMLoginItemSetEnabled failed.")
}
}
}
Swift 3:
#available(OSX, deprecated: 10.10)
dynamic var startAtLogin : Bool {
get {
guard let jobDicts = SMCopyAllJobDictionaries( kSMDomainUserLaunchd ).takeRetainedValue() as? [[String:Any]] else { return false }
return jobDicts.first(where: { $0["Label"] as! String == helperBundleIdentifier }) != nil
} set {
if !SMLoginItemSetEnabled(helperBundleIdentifier as CFString, newValue) {
print("SMLoginItemSetEnabled failed.")
}
}
}
Side note: A launchd job requires the key Label so it's 100% safe to unwrap the optional in the filter function.