CallKit - How to bring the CXCallController to the front - swift

I'm creating a VOIP App.
Most of the logic is similar to the Raywenderlich Tutorial.
My issue is that after accepting the call, the CXCallController is not on top of the App, but next to it in the "task manager":
The problem with that is, that once you have accepted the call, the CXCallController is not the top most controller, but the actual App.
So you can't tell that the call is taking place.
And to perform actions regarding the call - for example hang up, hold, mute you would have to go through the task manager to open the CXCallController. Not very user friendly.
How do I bring the CXCallController to the front after the user has accepted the call?
Provider delegate called after the user has answered the call:
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
guard let call = callManager.getCall(with: action.callUUID) else {
action.fail()
return
}
configureAudioSession()
self.incommingCall = call
action.fulfill()
}
Callback after the audio session has been created:
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
incommingCall?.answerCall(with: audioSession) { success in
if success {
self.incommingCall?.startAudio()
}
}
}
Start call logic using TokBox VOIP Service:
func answerCall(with audioSession: AVAudioSession, completion: ((_ success: Bool) -> Void)?) {
OTAudioDeviceManager.setAudioDevice(OTDefaultAudioDevice.sharedInstance(with: audioSession))
if session == nil {
session = OTSession(apiKey: self.apiKey, sessionId: self.sessionID, delegate: self)
}
answerCallCompletion = completion
var error: OTError?
hasStartedConnecting = true
session?.connect(withToken: self.token, error: &error)
if error != nil {
CoreServices.catchError(error: error, at: #function)
}
}
The call itself works fine. Two parties are able to communicate, start and end calls. The only issue is the described CXCallController behavior.
Is this expected behavior? Or how do I bring the CXCallController to the front?
Help is very appreciated.

This is the expected behaviour of the callkit. Callkit allows us to answer or decline the call when the app is receiving the incoming call. After answering the call, the app has to manage all the actions.
when the phone is locked, receive the incoming call, CXCallController will be visible, after answering the call, still you can see the CXCallController in the lock mode. After unlocking the phone, CXCallController will be invisible, the app has to manage the hangup, mute or hold options.
when the phone is not locked, receive the incoming call, CXCallController will be visible, after answering the call, CXCallController will be invisible, the app has to manage the hangup, mute or hold options.

Related

ATTrackingManager stopped working in iOS 15

ATTrackingManager.requestTrackingAuthorization stopped working on ios 15. Application rejected from Apple.
According to the discussion in Apple Developer Forum, you need to add delay for about one second when calling requestTrackingAuthorization.
https://developer.apple.com/forums/thread/690607
Example:
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
// Tracking authorization completed. Start loading ads here.
// loadAd()
})
})
P.S.
Also if you have requesting push notification permission, firstly you need request push notification then request tracking authorization with a delay =>
private func requestPushNotificationPermission() {
let center = UNUserNotificationCenter.current()
UNUserNotificationCenter.current().delegate = self
center.requestAuthorization(options: [.sound, .alert, .badge], completionHandler: { (granted, error) in
if #available(iOS 14.0, *) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
// Tracking authorization completed. Start loading ads here.
// loadAd()
})
})
}})
UIApplication.shared.registerForRemoteNotifications()
}
The problem has been solved, just call it in applicationDidBecomeActive:
https://developer.apple.com/forums/thread/690762
Follow by apple doc:
Calls to the API only prompt when the application state is UIApplicationStateActive.
So, we need to call ATTrackingManager.requestTrackingAuthorization on
applicationDidBecomeActive of AppDelegate.
But If you're using scenes (see Scenes), UIKit will not call this method. Use sceneDidBecomeActive(_:) instead to restart any tasks or refresh your app’s user interface. UIKit posts a didBecomeActiveNotification regardless of whether your app uses scenes.
So, my approach is to register on addObserver on didFinishLaunchingWithOptions such as:
NotificationCenter.default.addObserver(self, selector: #selector(handleRequestEvent), name: UIApplication.didBecomeActiveNotification, object: nil)
on handleRequestEvent:
requestPermission() // func call ATTrackingManager.requestTrackingAuthorization NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
Hope this helps. It's work for me.
Make sure your iPhone's Settings -> Privacy -> Tracking is enabled. Otherwise, it won't prompt for request Aurthorization.

didRegisterForRemoteNotificationsWithDeviceToken does not get triggered - Push Notifications not working

I've spent hours now trying to figure out why didRegisterForRemoteNotificationsWithDeviceToken is not being called.
It worked before. I didn't touch it in weeks. Now stopped working.
Here's my setup:
Got didRegisterForRemoteNotificationsWithDeviceToken sitting in SceneDelegate
Have declared SceneDelegate to be the UNUserNotificationCenterDelegate
I'm setting UNUserNotificationCenter.current().delegate = self in func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
I'm calling UNUserNotificationCenter.current().requestAuthorization { granted, error in... from one of the UIViewControllers in my App, specifically in viewDidLoad of that Controller - I get the Authorization Pop-up and when accepting I get a true back for granted
Within UNUserNotificationCenter.current().getNotificationSettings { settings in ... a check for settings.authorizationStatus == .authorized returns true
Push Notifications are added as a capability for the App in "Signing & Capabilities" and the entitlement file has been created. I've even already deleted and re-created the entitlement file - just to be sure...
I've made sure to run the app on a real device (both via Xcode as well as via TestFlight) - so the delegate not being called is not related to the App running in the Simulator.
Have checked if didFailToRegisterForRemoteNotificationsWithError gets called instead at least - but it doesn't.
I have tried with a second physical device which I have never used for testing before.
I've even checked the status of the APNS Sandbox here: https://developer.apple.com/system-status/
The didReceive delegate gets called though if I'm sending a test notification to simulator via terminal command xcrun simctl push... and I'm getting the notification.
Provisioning profile is managed by Xcode and the specified AppID is configured for Push Notifications. Certificates are set up in apple developer account.
Here's how I'm requesting Authorization from the user within viewDidLoad of one of my Apps UIViewControlles
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
DispatchQueue.main.async {
if (granted) {
//self.processInitialAPNSRegistration()
UIApplication.shared.registerForRemoteNotifications()
UserDefaults.standard.set(true, forKey: "pushNotificationsEnabled")
}
print("permission granted?: \(granted)")
}
}
And here's the didRegisterForRemoteNotificationsWithDeviceToken delegate method sitting inside SceneDelegate. Note: I'm also setting UNUserNotificationCenter.current().delegate = self in scene willConnectTo and declaredSceneDelegateto implement theUNUserNotificationCenterDelegate` protocol.
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format:"%02.2hhx", $0) }.joined()
print("didRegisterForRemoteNotificationsWithDeviceToken GOT CALLED - APNS TOKEN IS: \(token)")
self.apiService.setAPNSToken(apnsToken: token, completion: {result in
switch result {
case .success(let resultString):
DispatchQueue.main.async {
UserDefaults.standard.set(token, forKey: "apnsToken")
print(resultString, " TOKEN IS: \(token)")
}
case .failure(let error):
print("AN ERROR OCCURED: \(error.localizedDescription)")
}
})
}
Whatever I do, didRegisterForRemoteNotificationsWithDeviceToken is not getting triggered. I'm running out of ideas on what is going wrong.
Where is the error? How can I get didRegisterForRemoteNotificationsWithDeviceToken to execute again?
I think you have gone wrong in the second step didRegisterForRemoteNotificationsWithDeviceToken, didFailToRegisterForRemoteNotificationsWithError delegates are available in UIApplicationDelegate not in UNUserNotificationCenterDelegate
Add UIApplicationDelegate to your SceneDelegate class and in willConnectTo function set the delegate as:
UIApplication.shared.delegate = self
I hope this works... let me know if you still see an issue.
Have you made sure that you use the right provisioning profile which is using a push notification enabled AppId? This one is missing in your steps.
As I am not eligible to put comments yet, posting this in answer.

Background refresh task not triggered in watchos 6 independent app

I'm developing an independent watch app on XCode 11.0 beta 5. Everything works alright except background refresh. I'm using the following code to schedule the background refresh task when I open the app:
let fireDate = Date(timeIntervalSinceNow: 60.0 * 30.0)
// optional, any SecureCoding compliant data can be passed here
let userInfo = ["reason" : "update UI"] as NSDictionary
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: fireDate, userInfo: userInfo) { (error) in
if (error == nil) {
print("successfully scheduled background task, use the crown to send the app to the background and wait for handle:BackgroundTasks to fire.")
}
}
The func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) is never called.
If I use the Debug->Simulate background fetch option from XCode the method gets called.
I have exactly the same problem, even if I don't run the app as a standalone. The problem only occurred with watchOS 6.
Does anyone have an idea what the solution is?
Here is my source code:
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
print("applicationDidFinishLaunching")
self.reloadActiveComplications()
scheduleNextReload()
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
self.reloadActiveComplications()
scheduleNextReload()
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
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.
print("background")
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.
scheduleNextReload()
self.reloadActiveComplications()
backgroundTask.setTaskCompletedWithSnapshot(true)
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.
connectivityTask.setTaskCompletedWithSnapshot(true)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompletedWithSnapshot(true)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(true)
}
}
}
func reloadActiveComplications() {
let server = CLKComplicationServer.sharedInstance()
print("ExtensionDelegate: requesting reload of complications")
for complication in server.activeComplications ?? [] {
server.reloadTimeline(for: complication)
}
}
func scheduleNextReload() {
var targetDate:Date
let currentDate = Date()
let timezoneOffset = TimeZone.current.secondsFromGMT()
let epochDate = currentDate.timeIntervalSince1970
let timezoneEpochOffset = (epochDate + Double(timezoneOffset))
let timeZoneOffsetDate = Date(timeIntervalSince1970: timezoneEpochOffset)
targetDate = timeZoneOffsetDate.addingTimeInterval(120)
print("ExtensionDelegate: scheduling next update at %#", "\(timeZoneOffsetDate)")
print("ExtensionDelegate: scheduling next update at %#", "\(targetDate)")
WKExtension.shared().scheduleBackgroundRefresh(
withPreferredDate: targetDate,
userInfo: nil,
scheduledCompletion: { error in
// contrary to what the docs say, this is called when the task is scheduled, i.e. immediately
NSLog("ExtensionDelegate: background task %#",
error == nil ? "scheduled successfully" : "NOT scheduled: \(error!)")
}
)
}
}
Try scheduling it in applicationDidResignActive, rather than from a controller.
It only does it IF the application is in the background. It doesn't seem to think it needs to do it if the application isn't in the background.
You can make applicationDidResignActive fire by pressing the crown button.

how to integrate Callkit with Agora VOiP in swift 4 iOS?

I want to integrate apple Callkit with Agora VOiP in swift 4 iOS.
Please give any suggestions How can I do that.
To integrate voip, you will have to use both, callKit and PushKit.
CallKit will be used to show native call screen and handlers during in call transition while Pushkit will be used to invoke app, when app is killed.
Its easy to integrate:-
Enable background modes in info.plist and check option "App provides Voice over IP services".
Import Callkit in the viewcontroller viewdidload/ any init method of anyclass you would use to Implement CXProviderDelegate functions. Through this you will configure call objects, when to report in comming call, accept actions, reject action etc.
Implement the following functions:
func providerDidReset(_ provider: CXProvider) {
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
action.fulfill()
}
Now import Pushkit and implement PKPushRegistryDelegate functions.
a.)Configure pushkit like this
let registry = PKPushRegistry(queue: nil)
registry.delegate = self
registry.desiredPushTypes = [PKPushType.voIP]
b.) implement pushkit token function. You may have to update to server for delivering voip push notifications
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
print(pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined())
}
c. Now when you receive incomming notification, implement this fuction
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
let config = CXProviderConfiguration(localizedName: "App name")
config.iconTemplateImageData = UIImagePNGRepresentation(UIImage(named: "any image name")!)
config.supportsVideo = true;
let provider = CXProvider(configuration: config)
provider.setDelegate(self, queue: nil)
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: "Caller name")
update.hasVideo = true
provider.reportNewIncomingCall(with: UUID(), update: update, completion: { error in })
}
Go to developer portal and generate VoIP Services certificate and install it.
Enable push notifications under Capabilities.
This was a basic code over view. You will have to add cases to simulating incomming call and other customizations. I hope this will help you to move further.

Parse: Update user location upon app open from background/inactive state (Swift)

I have a location-based social networking iPhone app written in Swift and using the Parse SDK.
When the app launches, if the user is logged in, the PFQueryTableViewController is loaded. This is the home page that basically shows a live feed of posts by users within a x-mile radius where users can like or comment on those posts.
Currently, when the user moves to a new location (outside the set mile radius) then goes back into the app, the same feed shows, even if the user pulls-to-refresh the tableview. The new feed of posts only updates to the new location when the user quits the app and launches the app again.
My goal is to have the user's current location update after the user exits (not quits) the app by pressing the iPhone home button or multi-tasking to another app, then opens (not relaunches) the app again. In other words, the feed should update to the new location when the app becomes active again. I think I may have to somehow call some of the functions in the PFQueryTableViewController from applicationDidBecomeActive but I am not sure.
I greatly appreciate any help. Thank you so much.
In my PFQueryTableViewController:
override func viewDidLoad() {
...
queryForLocation()
queryForLikes()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.tableView.reloadData()
self.loadObjects()
}
func queryForLocation() {
/* From documentation:
When the following code is run, the following happens:
1) An internal CLLocationManager starts listening for location updates (via startsUpdatingLocation).
2) Once a location is received, the location manager stops listening for location updates (via stopsUpdatingLocation) and a PFGeoPoint is created from the new location. If the location manager errors out, it still stops listening for updates, and returns an NSError instead.
3) Your block is called with the PFGeoPoint
***Are we supposed to use `startsUpdatingLocation` somewhere? (So that it updates the location (currLocation) because it is used in queryForTable() to return the query***
*/
PFGeoPoint.geoPointForCurrentLocationInBackground {
(geoPoint: PFGeoPoint?, error: NSError?) -> Void in
if error == nil {
// store the new geoPoint in currLocation GeoPoint property
self.currLocation = geoPoint
// this should call queryForTable
self.loadObjects()
} else {
// Unable to fetch the current device's location.
}
}
}
override func queryForTable() -> PFQuery {
let query = PFQuery(className: "Posts")
// self.currLocation obtained from `geoPointForCurrentLocationInBackground` call in queryForLocation() but does not seem to be update to new location on app open (not launch).
if let userLocation = self.currLocation {
query.whereKey("location", nearGeoPoint: userLocation, withinMiles: 5)
query.includeKey("User")
query.cachePolicy = PFCachePolicy.NetworkOnly
// If no objects are loaded in memory, we look to the cache first to fill the table and then subsequently do a query against the network.
if (self.objects?.count == 0) {
query.cachePolicy = PFCachePolicy.CacheThenNetwork
}
query.limit = 200;
return query
} else {
/* How the application should react if there is no location available */
return PFQuery()
}
}
You are calling your queryForLocation() only in your viewDidLoad method but when the app goes background then foreground, viewDidLoad is not recalled, that's why you don't have a new position.
You might just register for the UIApplicationWillEnterForegroundNotification notification, so you will be notified when the app is brought back to the foreground.
Just add (if you are not using Xcode 7.3, you may have to use selector: "willEnterForeground") to your viewDidLoad method :
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(YourViewController.comeFromBackground), name: UIApplicationWillEnterForegroundNotification, object: nil)
func comeFromBackground() {
// This is where you should do your request for location again
}
So everytime your app goes background->foreground this function will be called.
Don't forget to remove the observer in the viewDidDisappear :
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIApplicationWillEnterForegroundNotification, object: nil)
Hope this will help you !