Where should I place Callkit Reload Extension code in my IOS project - ios

I am making an app like TrueCaller. I am able to block a number at first but whenever I am updating/adding more number then I need to manually disable and enable call directory plugin in Settings -> Phone -> Call Blocking & Identification to get list updated. I have seen many answers on StackOverflow where people say to call CXCallDirectoryManager.reloadExtension to reload extension via code. But I don't know where to write this code or from where should I call this reloadExtension from my project.
Update -1
Here is how I approached.
Below is my code of view controller.
#IBAction func add(_ sender: Any) {
//let defaults = UserDefaults.standard
let userDefaults = UserDefaults(suiteName: "group.com.number1")
var finArray = userDefaults!.object(forKey: "attribute") as? [Int64] ?? [Int64]()
let number1 = number.text!
finArray.append(Int64(number1)!)
print("from main add")
print(finArray)
//let userDefaults = UserDefaults(suiteName: "group.com.number1")
userDefaults!.set(finArray, forKey: "attribute")
CXCallDirectoryManager.sharedInstance.reloadExtension(withIdentifier: "com.akshat.Blocking-array.Callblockarray", completionHandler: {(error) -> Void in if let error = error {
print("akshat"+error.localizedDescription)
}})
}
Here is my code of callkit extension
// CallDirectoryHandler.swift
import Foundation
import CallKit
import CoreData
class CallDirectoryHandler: CXCallDirectoryProvider {
override func beginRequest(with context: CXCallDirectoryExtensionContext) {
context.delegate = self
print("inside beginrequest")
// let defaults = UserDefaults(suiteName: "group.com.number1")
// //(suiteName: "group.tag.number")
// let array = defaults!.object(forKey: "attribute") as? [Int64] ?? [Int64]()
// print(array)
// Check whether this is an "incremental" data request. If so, only provide the set of phone number blocking
// and identification entries which have been added or removed since the last time this extension's data was loaded.
// But the extension must still be prepared to provide the full set of data at any time, so add all blocking
// and identification phone numbers if the request is not incremental.
if context.isIncremental {
print("insideif")
addOrRemoveIncrementalBlockingPhoneNumbers(to: context)
print("insideif")
addOrRemoveIncrementalIdentificationPhoneNumbers(to: context)
} else {
addAllBlockingPhoneNumbers(to: context)
print("inside else")
addAllIdentificationPhoneNumbers(to: context)
}
context.completeRequest()
}
private func addAllBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
// Retrieve all phone numbers to block from data store. For optimal performance and memory usage when there are many phone numbers,
// consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
//
// Numbers must be provided in numerically ascending order.
print("func addAllBlockingPhoneNumbers")
let defaults = UserDefaults(suiteName: "group.com.number1")
//(suiteName: "group.tag.number")
var array = defaults!.object(forKey: "attribute") as? [Int64] ?? [Int64]()
array.sort()
print(array)
let allPhoneNumbers: [CXCallDirectoryPhoneNumber] = array
for phoneNumber in allPhoneNumbers {
context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
}
}
private func addOrRemoveIncrementalBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
// Retrieve any changes to the set of phone numbers to block from data store. For optimal performance and memory usage when there are many phone numbers,
// consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
print("addOrRemoveIncrementalBlockingPhoneNumbers")
let defaults = UserDefaults(suiteName: "group.com.number1")
//(suiteName: "group.tag.number")
var array = defaults!.object(forKey: "attribute") as? [Int64] ?? [Int64]()
array.sort()
print(array)
let phoneNumbersToAdd: [CXCallDirectoryPhoneNumber] = array
for phoneNumber in phoneNumbersToAdd {
print(phoneNumber)
context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
}
let phoneNumbersToRemove: [CXCallDirectoryPhoneNumber] = [ 1_877_555_5555 ]
for phoneNumber in phoneNumbersToRemove {
context.removeBlockingEntry(withPhoneNumber: phoneNumber)
}
// Record the most-recently loaded set of blocking entries in data store for the next incremental load...
}
private func addAllIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
// Retrieve phone numbers to identify and their identification labels from data store. For optimal performance and memory usage when there are many phone numbers,
// consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
//
// Numbers must be provided in numerically ascending order.
print("addAllIdentificationPhoneNumbers")
let allPhoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1_877_555_5555, 1_888_555_5555 ]
let labels = [ "Telemarketer", "Local business" ]
for (phoneNumber, label) in zip(allPhoneNumbers, labels) {
context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
}
}
private func addOrRemoveIncrementalIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
print(addOrRemoveIncrementalIdentificationPhoneNumbers)
// Retrieve any changes to the set of phone numbers to identify (and their identification labels) from data store. For optimal performance and memory usage when there are many phone numbers,
// consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
let phoneNumbersToAdd: [CXCallDirectoryPhoneNumber] = [ 1_408_555_5678 ]
let labelsToAdd = [ "New local business" ]
for (phoneNumber, label) in zip(phoneNumbersToAdd, labelsToAdd) {
context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
}
let phoneNumbersToRemove: [CXCallDirectoryPhoneNumber] = [ 1_888_555_5555 ]
for phoneNumber in phoneNumbersToRemove {
context.removeIdentificationEntry(withPhoneNumber: phoneNumber)
}
// Record the most-recently loaded set of identification entries in data store for the next incremental load...
}
}
extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {
func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) {
print("CXCallDirectoryExtensionContext")
// An error occurred while adding blocking or identification entries, check the NSError for details.
// For Call Directory error codes, see the CXErrorCodeCallDirectoryManagerError enum in <CallKit/CXError.h>.
//
// This may be used to store the error details in a location accessible by the extension's containing app, so that the
// app may be notified about errors which occured while loading data even if the request to load data was initiated by
// the user in Settings instead of via the app itself.
}
}
But still, this is only working sometimes. Call Directory Extension is taking too long to update new numbers whenever I add them. For eg: If I write number 'A' and press add button then this add function will be called and number 'A' will get blocked. Now if I add one more number let say 'B' then call directory extension is not able to add it and B will not get blocked. And now if add one more number 'C' then all will get blocked. I am really not getting this. I hope someone could help me.

Related

How do I deal with multiple widget instances accessing the same CoreData store?

Background Info
I have a main application, which writes a single entry to a database contained in an App Group (we'll call them "DB1", and "Group1", respectively). To the same project, I have added an iOS 14 Home Screen Widget extension. This extension is then added to Group1.
All three sizes (small, medium, large) show the same information from the DB1 entry, just rearranged, and for the smaller widgets, some parts omitted.
Problem
The problem is, if I have multiple instances of the widget (say a small and a medium), then when I rebuild the target, and it loads/runs, I get CoreData errors such as the following:
<NSPersistentStoreCoordinator: 0x---MEM--->: Attempting recovery from error encountered during addPersistentStore: Error Domain=NSCocoaErrorDomain Code=134081 "(null)" UserInfo={NSUnderlyingException=Can't add the same store twice}
Relevant Code
Here's the code for the Timeline function
public func timeline(with context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
//Set up widget to refresh every minute
let currentDate = Date()
let refreshDate = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)!
WidgetDataSource.shared.loadTimelineEntry { entry in
guard let e = entry else { return }
let entries: [TimelineEntry] = [e]
let timeline = Timeline(entries: entries, policy: .after(refreshDate))
completion(timeline)
}
}
And here is the loadTimelineEntry function
(persistentContainer gets initialized in the init() of WidgetDataSource, the class that holds loadTimelineEntry function)
func loadTimelineEntry(callback: #escaping (TimelineEntry?) -> Void) {
persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
print("Loading persistent stores")
var widgetData: [WidgetData]
if let error = error {
print("Unresolved error \(error)")
callback(nil)
} else {
let request = WidgetData.createFetchRequest()
do {
widgetData = try self.persistentContainer.viewContext.fetch(request)
guard let data = widgetData.first else { callback(nil); return }
print("Got \(widgetData.count) WidgetData records")
let entry = TimelineEntry(date: Date())
callback(entry)
} catch {
print("Fetch failed")
callback(nil)
}
}
})
}
What I've Tried
Honestly, not much. I had gotten this same error before I put the widget into the Group1. That time it was due to the main app already having DB1 created on it's first run, then on the subsequent widget run, it looked in it's own container, didn't find it, and attempted to create it's own DB1, and the OS didn't let it due to them having the same name.
Widgets being a fairly new feature, there are not many questions regarding this specific use case, but I'm sure someone out there has a similar setup. Any help or tips will be highly appreciated!
Thanks to Tom Harrington's comment, I was able to solve the warnings by adding a check at the top of my loadTimelineEntry function like so:
if !persistentContainer.persistentStoreCoordinator.persistentStores.isEmpty {
//Proceed with fetch requests, etc. w/o loading stores
} else {
//Original logic to load store, and fetch data
}

Memory leak using Firebase

I execute an API call in Firebase for retrieving the user profile information and storing it in a ViewController member variable.
The API is declared as a static function inside a class MyApi:
// Get User Profile
static func getUserProfile(byID userId:String,response:#escaping (_ result:[User]?,_ error:Error?)->()) {
// check ID is valid
guard userId.length > 0 else {
print("Error retrieving Creator data: invalid user id provided")
response(nil,ApiErrors.invalidParameters)
return
}
// retrieve profile
let profilesNode = Database.database().reference().child(MyAPI.profilesNodeKey)
profilesNode.child(userId).observe(.value, with: { (snapshot) in
// check if a valid data structure is returned
guard var dictionary = snapshot.value as? [String:AnyObject] else {
print("Get User Profile API: cannot find request")
response([],nil)
return
}
// data mapping
dictionary["key"] = userId as AnyObject
guard let user = User(data:dictionary) else {
print("Get User Profile API: error mapping User profile data")
response(nil,ApiErrors.mappingError)
return
}
response([user], nil)
}) { (error) in
response(nil,ApiErrors.FirebaseError(description: error.localizedDescription))
}
}
and I call it like that:
MyAPI.getUserProfile(byID: creatorId) { (profiles, error) in
guard let profiles = profiles, profiles.count > 0 else {
Utility.showErrorBanner(message: "Error retrieving Creator profile")
print("Error retrieving creator profile ID:[\(creatorId)] \(String(describing: error?.localizedDescription))")
return
}
self.currentProfile = profiles.first!
}
The ViewController is called in Modal mode so it should be deallocated every time I exit the screen.
Problem: a huge chunk of memory get allocated when I enter the screen, but it doesn't get freed up when I leave it. I'm sure about this because the problem doesn't appear if I remove the line self.currentProfile = profiles.first! (obviously)
How can I avoid this from happening?
NOTE: currentProfile is of type User, which was used to be a struct. I made it a class so I could use a weak reference for storing the information:
weak var currentCreator: User? {
didSet {
updateView()
}
}
but the problem still persists.
You are adding an observer:
profilesNode.child(userId).observe(...)
But you never remove it. As long as that observe is still added, it will hold on to memory from the entire set of results, and continually retrieve new updates. It's a really bad practice not to remove your observers.
If you want to read data just a single time, there is a different API for that using observeSingleEvent.

how to make getting data faster from Firebase Firestore?

I am new in programming and in iOS development. I am trying to make an app using Firestore database from Firebase. I don't know if it is normal or not, but when I am trying to get a data from firestore database, it seems too long for me. I don't know if I make a mistake or not
here is my code to get all city data from firestore
reference :
import Foundation
import FirebaseFirestore
import Firebase
enum FirestoreCollectionReference {
case users
case events
case cities
private var path : String {
switch self {
case .users : return "users"
case .events : return "events"
case .cities : return "cities"
}
}
func reference () -> CollectionReference {
return Firestore.firestore().collection(path)
}
}
I use getAllCitiesDataFromFirestore method in CityKM class to get the city data that stored in firestore
class CityKM {
var name : String
var coordinate : GeoPoint
init (name: String , coordinate: GeoPoint ) {
self.name = name
self.coordinate = coordinate
}
init (dictionary: [String:Any]) {
// this init will be used if we get data from firebase observation to construct an event object
name = dictionary["name"] as! String
coordinate = dictionary["coordinate"] as! GeoPoint
}
static func getAllCitiesDataFromFirestore (completion: #escaping ( [CityKM]? )->Void) {
// to retrieve all cities data from Firebase database by one read only, not using realtime fetching listener
let startTime = CFAbsoluteTimeGetCurrent() // to track time consumption of this method
FirestoreCollectionReference.cities.reference().getDocuments { (snapshot, error) in
if let error = error {
print("Failed to retrieve all cities data: \(error.localizedDescription)")
} else {
print("Sucessfully get all cities data from firestore")
guard let documentsSnapshot = snapshot, !documentsSnapshot.isEmpty else {
completion(nil)
return
}
let citiesDocuments = documentsSnapshot.documents
var cityArray = [CityKM]()
for document in citiesDocuments {
guard let cityName = document.data()["name"] as? String,
let cityCoordinate = document.data()["coordinate"] as? GeoPoint else {return}
let theCity = CityKM(name: cityName, coordinate: cityCoordinate)
cityArray.append(theCity)
}
completion(cityArray)
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime // to track time consumption of this method
print("Time needed to get all cities data from Firestore : \(timeElapsed) s.") // to track time consumption of this method
}
}
}
}
extension CityKM {
// MARK: - User Helper Methods
func toDictionary() -> [String:Any]{
return [
"name" : name,
"coordinate" : coordinate
]
}
}
from my debugging area, it is printed
"Time needed to get all cities data from Firestore : 1.8787678903 s."
is it possible to make it faster? Is 1.8s normal? am i make a mistake in my code that make the request data takes too long time ? I hope that I can make request time is below one second
I don't think the internet speed is the problem, since I can open video on youtube without buffering
That performance sounds a bit worse than what I see, but nothing excessive. Loading data from the cloud simply takes time. A quick approach to hide that latency is by making use of Firebase's built-in caching.
When you call getDocuments, the Firebase client needs to check on the server what the document's value is before it can call your code, which then shows the value to the user. As said: there is no way to speed up this reading in your code, so it'll always take at least 1.8s before the user sees a document.
If instead you listen for realtime updates from the database with addSnapshotListener, the Firebase client may be able to immediately call your code with values from its local cache, and then later re-invoke your code in case there has been an update to the data on the server.

How to create autocomplete text field in Swift

What I want to be able to create is an auto complete text field in iOS.
I have a form for selecting a client, wherein the user must select a client once using a text field . What I want to happen is when the user writes the first three letters on the text field, I want some service to run a remote web service query using the entered text and present the query results as auto complete suggestions.
Below is my current code for my app (iPad only).
import UIKit
class AddClientViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var clientTextField: UITextField!
var foundList = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let listUrlString = "http://bla.com/myTextField.php?field=\(clientTextField)"
let myUrl = NSURL(string: listUrlString);
let request = NSMutableURLRequest(URL:myUrl!);
request.HTTPMethod = "GET";
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil {
print(error!.localizedDescription)
dispatch_sync(dispatch_get_main_queue(),{
AWLoader.hide()
})
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? NSArray
if let parseJSON = json {
self.foundList = parseJSON as! [String]
}
} catch {
print(error)
}
}
task.resume()
}
Here is the json output that my web service provides.
["123,John", "343,Smith", "345,April"]
Separated by commas, the first parameter is the client ID and the second parameter is the name of the client. John is the name so it should be presented in the auto complete suggestions, which if selected will set the text of the clientTextField to John.
The current text content of the clientTextField is passed as a GET parameter to my webservice.
I don't know how to do this. The user could be typing and not yet finished, while multiple queries could already have been sent.
I did something like this in my app for looking up contacts. I will pseudo code this out for you to understand the concept:
1) Capture the characters entered into the textfield by the enduser
2) At some character count entered decide to query the server to return all entries that match - choose the character count you are comfortable with (I chose around 3-4 characters). Fewer returns more, more returns less obviously...up to you, perf and UX considerations.
3) Put the results of this server query into an array on the client. This will be your superset from which you will offer the suggestions to the user.
4) After each subsequent character entered into the text field you will now filter the array (array.filter()) by character string entered to this point.
5) tableView.reloadData() against the filtered array at each character entered.
6) I use a dataFlag variable to determine what datasource to show in the tableview depending on what the user is doing.
Note: You only query the server once to minimize perf impact
// this function is called automatically when the search control get user focus
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
if searchBar.text?.range(of: "#") != nil {
self.getUserByEmail(searchBar.text!)
}
if searchController.searchBar.text?.characters.count == 0 && dataFlag != "showParticipants" {
dataFlag = "showInitSearchData"
self.contacts.removeAll()
self.participantTableView.reloadData()
}
if dataFlag == "showInitSearchData" && searchController.searchBar.text?.characters.count == 2 {
self.loadInitialDataSet() {
self.dataFlag = "showFilteredSearchData"
}
}
if dataFlag == "showFilteredSearchData" {
self.filterDataForSearchString()
}
}
// filter results by textfield string
func filterDataForSearchString() {
let searchString = searchController.searchBar.text
self.filteredContacts = self.contacts.filter({
(contact) -> Bool in
let contactText: NSString = "\(contact.givenName) \(contact.familyName)" as NSString
return (contactText.range(of: searchString!, options: NSString.CompareOptions.caseInsensitive).location) != NSNotFound
})
DispatchQueue.main.async {
self.participantTableView.reloadData()
}
}
Using Trie like structure will be a better option here. Based on entered string, trie will return top keywords (lets say 10) starting with the entered string. Implementing this trie on server side is better. When UI makes http call, calculations will be done on server side and server will send top results to UI. Then, UI will update TableView with new data.
You can also do this with hashmap/dictionary but performance will be worse. Using trie/prefix tree approach will give you the best performance when you have thousands or millions of strings to check.

Background process listening to variable changes?

I'm new to background operations in iOS, so I'm wondering what is the best way to solve such problem:
I have data, coming from one webservice#1 that needed to be parsed and sent to webservice#2 in background.
I need a background thread, which will be listening for changes in array that stores data from webserivce#1 and when it's not empty, the thread will start uploadOperation, which will process the array and send processed data to webservice#2
Literally, how I see it:
I have DataManager class, presented by a Singleton sharedInstance.
let sharedInstance = DataManager()
It has
public var data: [String]? {
didSet {
processData()
}
}
private var uploadToWebSerivice2Queue: NSOperationQueue?
private override init() {
uploadToWebSerivice2Queue = NSOperationQueue()
uploadToWebSerivice2Queue.maxConcurentOperations = 1
getCachedAndNotSentDataFromDatabase()
}
private func getCachedAndNotSentDataFromDatabase() {
data = ("string 1", "string 2", "string 3", "string 4")
}
private func processData() {
while let lastElement = data!.removeLast {
let processedData = process(lastElement)
let uploadOperation = UploadOperation(processedData) // Data upload opeation
uploadToWebSerivice2Queue!.addOperation(uploadOperation)
}
}
Some data may come from Database where they are cached if they couldn't be send to webservice#2 during the last try. And some data may come from webservice#1 in runtime.
So in other class, let's call it Webservice1DataHandler, I'd do:
DataManager.sharedInstance.data.append("New string to process and upload to webservice#1")
To sum up,
var data will be set after first init() and uploadQueue will start to process that data. Then new string will be appended to var data, that means that processData() method will be invoked and concurecny problems with data array access may occur.
I'm not sure if my algorithm is OK.

Resources