In my app I save the user's phone numbers they give me so others can find them and they can find their contacts on the app. But I am having trouble parsing through the phone numbers because users can save phone numbers in different ways:
– Country code, area code, number: +1 (area code) xxx-xxxx
– Country code, number: +1 xxx-xxxx
– Number: xxx-xxxx
The way I save the users number is by adding the area code and number as one long number and save the country code separately. I tried parsing through the telephone numbers taken from Contacts to mimic how I saved the numbers but it does not work. Is there something I am missing? I have copied the code at the bottom:
if accessGranted {
let contactStore = CNContactStore()
let keys = [CNContactPhoneNumbersKey]
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
do {
try contactStore.enumerateContacts(with: request, usingBlock: { (contact, error) in
let phoneNumberRaw = contact.phoneNumbers.first?.value.stringValue ?? ""
var phoneNumberRawArray = phoneNumberRaw.components(separatedBy: " ")
var phoneString = ""
if phoneNumberRawArray.count >= 3 { //probably has country code in the front
//remove country code
phoneNumberRawArray.removeFirst()
}
//add to phone string
for phone in phoneNumberRawArray {
phoneString.append(phone)
}
phoneString = phoneString.replacingOccurrences(of: "(", with: "")
phoneString = phoneString.replacingOccurrences(of: " ", with: "")
phoneString = phoneString.replacingOccurrences(of: ")", with: "")
phoneString = phoneString.replacingOccurrences(of: "-", with: "")
self.contactsPhoneNumbers.append(phoneString)
//get country code
for ContactNumber:CNLabeledValue in contact.phoneNumbers
{
let fullNumber = ContactNumber.value
let countryCode = fullNumber.value(forKey: "countryCode") as? String
self.contactsCountryCode.append(countryCode!)
}
})
}
catch {
print("Unable to get contacts")
}
}
Related
I'm trying to find a solution for a little problem I have with my app. It's a chess app and it works with Firebase and is written in swift. I use the database for user authentication, user information and uploading moves to play against each other online. All the userInfo is saved in a document in the collection "allUsers".
Everything is working fine, but I have a user screen where you can press a refresh button to update the current online users, with the .getDocuments() function. The problem is that every time a user refreshes, they query through all of the registered accounts, check if they have the flag "isOnline = true" and then list only those users in a TableView. I believe this counts in firebase as 1 read for every registered account, even though the user is not online.
The app is already live in the AppStore and I have quite a few people registered already. About 300. But, to be honest a lot of people just try it once or a few times and then leave again and never use the app again. But every time someone wants to update the online users, they cycle through 300 users and this gives me 300 reads with firebase. Right now it's not a big problem, but once people really start to use the app, I will reach my quotum quite quickly.
I could try .addSnapshotListener , but this will refresh the user screen everytime something happens in the userlist. It will be too chaotic. I've read about reading data from cache, but I'm not sure how to go about this. I could also get a fresh userlist when the app starts, save it locally and check every now and then if there are new online users, but I want the list to be updated whenever the user wants to.
Is there a way to compare a locally saved list to the online database list and only read/get the documents that are changed / new?
Sorry for the long text. Hopefully anyone can help me out!
Thanks.
Below is my code to load the users. It's a bit messy sorry.. Basically it retrieves all users and sorts them by online and offline. If a user searches for another user, it takes a String "query" as input.
Code :
func loadAllAvailableUsers(query : String) {
availableEmails = []
availableUsers = []
onlineUsers = []
isInGameIndex = []
var av : [String] = []
var ae : [String] = []
var wins : [Int] = []
var losses : [Int] = []
var draw : [Int] = []
var matches : [Int] = []
let collection = db.collection("allUsers")
collection.getDocuments { (snapshot, err) in
if let e = err {
print(e.localizedDescription)
} else {
if let documents = snapshot?.documents {
print("found users")
for doc in documents {
print(doc.documentID)
if doc.documentID != currentEmail && query == "" {
print(doc.data()["isOnline"] as! Bool)
if let name = doc.data()["username"] as? String, let w = doc.data()["wins"] as? Int, let l = doc.data()["losses"] as? Int, let d = doc.data()["draw"] as? Int, let numOfMatches = doc.data()["numberOfMatches"] as? Int, let online = doc.data()["isOnline"] as? Bool {
print("adding user : \(name) to list")
if online {
matches.append(numOfMatches)
av.append(name)
ae.append(doc.documentID)
wins.append(w)
losses.append(l)
draw.append(d)
onlineUsers.append(name)
if doc.data()["isInGame"] as! Bool == true {
print(i)
self.delegate?.addToInGameIndex(name: name)
}
}
}
} else if query != "" {
if let name = doc.data()["username"] as? String, let w = doc.data()["wins"] as? Int, let l = doc.data()["losses"] as? Int, let d = doc.data()["draw"] as? Int, let online = doc.data()["isOnline"] as? Bool, let numOfMatches = doc.data()["numberOfMatches"] as? Int {
print("Searched : adding user : \(name) to list")
if doc.documentID == currentEmail {
continue
}
let lowerName = name.lowercased()
let lowerQuery = query.lowercased()
if lowerName.contains(lowerQuery) && online {
av.append(name)
ae.append(doc.documentID)
wins.append(w)
losses.append(l)
draw.append(d)
matches.append(numOfMatches)
if doc.data()["isInGame"] as! Bool == true {
print(i)
self.delegate?.addToInGameIndex(name: name)
}
onlineUsers.append(name)
} else if lowerName.contains(lowerQuery) && !online {
av.append(name)
ae.append(doc.documentID)
wins.append(w)
losses.append(l)
draw.append(d)
matches.append(numOfMatches)
}
}
}
}
}
if ae.count > 0 {
self.delegate?.reloadTheTable(wins: wins, losses: losses, draw: draw, ae: ae, au: av, matches: matches)
}
print(availableUsers)
}
}
}
I am using multiline string as follows. there is a line where I display submittedPerson, either can be his Id or email, but also can be nil as well. I wonder how do you hide this line if it returns nil
var submittedPerson = ""
if let Id = User[index].Id {
submittedPerson = Id
} else if let email = User[index].email {
submittedPerson = email
}
let displayStr = """
\(department)
\"submittedBy" : \(submittedPerson)
\(submittedDate)
"""
I'll assume the hidden requirement here is that you still want to keep the multiline string literal readable, and not have code duplication :)
One way you could do this is to move one of the two line feed characters to when you assign to submittedPerson:
var submittedPerson: String? = ""
if let Id = User[index].Id {
submittedPerson = "\n\(Id)\n" // note the lines feeds
} else if let email = User[index].email {
submittedPerson = "\n\(email)\n"
}
let displayStr = """
\(department)
\(submittedPerson ?? "")
\(submittedDate)
"""
In my iOS application, I have a bunch of alertController messages containing text and one embedded phone number, and I wanted to offer the user the possibility from making the call from an alerControllerAction, and for that I need to be able to extract the phone number from the string dynamically, turn it into a phone number URL and let the old swift guy do its work, and so that's what I did after following about dozen of tuto around NSDataDetector, I came up with this function that for some reason always returns nil in my phoneNumberURL object. Could you guys check it out and tell me if something seems off ?
Here goes nothing :
private func showsHelpMessage()
{
let title = Bundle.main.localizedString(forKey: "account.help.popup.title",
value: "",
table: AFPConfig.sharedInstance.kLocalizableTable)
let message = Bundle.main.localizedString(forKey: "account.help.popup.message",
value: "",
table: AFPConfig.sharedInstance.kLocalizableTable)
var phoneNumber : String = ""
let detectorType: NSTextCheckingResult.CheckingType = [.phoneNumber]
do
{
let detector = try NSDataDetector(types: detectorType.rawValue)
let phoneNumberDetected = detector.firstMatch(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count))
phoneNumber = (phoneNumberDetected?.phoneNumber)!
phoneNumber = phoneNumber.removeWhitespace() // added this because i noticed the NSURL kept crashing because of the whitespaces between numbers
}
catch
{
phoneNumber = "+33969390215"
}
if let phoneURL = NSURL(string: ("tel://" + phoneNumber))
{
let alertAccessibility = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
alertAccessibility.addAction(UIAlertAction(title: "Appeler ?", style: .destructive, handler: { (action) in
UIApplication.shared.open(phoneURL as URL, options: [:], completionHandler: nil)
}))
alertAccessibility.addAction(UIAlertAction(title: "Annuler", style: UIAlertAction.Style.cancel, handler: nil))
self.present(alertAccessibility, animated: true, completion: nil)
}
}
Thank you in advance, and cheers!
Addressing the problem of extracting numbers which can't be identified as definitely phone numbers (see comment on my other answer):
Rather than try to extract a number from the message and hope that it's a phone number and not a distance or a house number, introduce a placeholder (%d) into the localised string and insert the phone number into the message:
enum LocalPhoneNumbers {
case reception = 1000
case helpdesk = 4567
// etc.
}
private function showHelpMessage() {
// "Call the helpdesk on %d"
let format = Bundle.main.localizedString(forKey: "account.help.popup.message",
value: "",
table: AFPConfig.sharedInstance.kLocalizableTable)
let number = LocalPhoneNumbers.helpdesk.rawValue
let message = String(format: format, number)
let url = URL(string: "tel://\(number)")
// Code to show alert here...
}
Your approach seems ok, but I suspect maybe something about your input data is the real problem. Try experimenting with this in a playground:
import Foundation
enum PhoneNumberDetectionError: Error {
case nothingDetected
case noNumberFound
}
func extractPhoneURL(from string: String) throws -> URL? {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.phoneNumber.rawValue)
guard let detected = detector.firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) else {
throw PhoneNumberDetectionError.nothingDetected
}
guard let number = detected.phoneNumber else {
throw PhoneNumberDetectionError.noNumberFound
}
let noWhiteSpaces = number.filter { !$0.isWhitespace }
return URL(string: "tel://\(noWhiteSpaces)")
}
let strings = [
"This 555–692–7753 is a phone number",
"This 1 555 692 7753 is a phone number",
"This 123 is an incomplete phone number",
"This does not have a phone number",
"This +1 555 692 7753 is a phone number",
]
strings.forEach {
do {
guard let url = try extractPhoneURL(from: $0) else {
print("❌ '\($0)' failed to make URL")
return
}
print("✅ '\($0)' -> \(url.absoluteString)")
} catch {
print("❌ '\($0)' : \(error)")
}
}
If you get a ❌ for anything that you think should be valid, that's your problem.
Also, you've got some odd ( ) in a couple of places:
// Odd combination of optional unwrap and force-unwrap
phoneNumber = (phoneNumberDetected?.phoneNumber)!
// Concise equivalent
phoneNumber = phoneNumberDetected!.phoneNumber
And:
// The brackets around the string concatenation don't achieve anything
if let phoneURL = NSURL(string: ("tel://" + phoneNumber))
// Better
if let phoneURL = NSURL(string: "tel://" + phoneNumber)
I am consuming an API that gives me the next page in the Header inside a field called Link. (For example Github does the same, so it isn't weird.Github Doc)
The service that I am consuming retrieve me the pagination data in the following way:
As we can see in the "Link" gives me the next page,
With $0.response?.allHeaderFields["Link"]: I get </api/games?page=1&size=20>; rel="next",</api/games?page=25&size=20>; rel="last",</api/games?page=0&size=20>; rel="first".
I have found the following code to read the page, but it is very dirty... And I would like if anyone has dealt with the same problem or if there is a standard way of face with it. (I have also searched if alamofire supports any kind of feature for this but I haven't found it)
// MARK: - Pagination
private func getNextPageFromHeaders(response: NSHTTPURLResponse?) -> String? {
if let linkHeader = response?.allHeaderFields["Link"] as? String {
/* looks like:
<https://api.github.com/user/20267/gists?page=2>; rel="next", <https://api.github.com/user/20267/gists?page=6>; rel="last"
*/
// so split on "," the on ";"
let components = linkHeader.characters.split {$0 == ","}.map { String($0) }
// now we have 2 lines like '<https://api.github.com/user/20267/gists?page=2>; rel="next"'
// So let's get the URL out of there:
for item in components {
// see if it's "next"
let rangeOfNext = item.rangeOfString("rel=\"next\"", options: [])
if rangeOfNext != nil {
let rangeOfPaddedURL = item.rangeOfString("<(.*)>;", options: .RegularExpressionSearch)
if let range = rangeOfPaddedURL {
let nextURL = item.substringWithRange(range)
// strip off the < and >;
let startIndex = nextURL.startIndex.advancedBy(1) //advance as much as you like
let endIndex = nextURL.endIndex.advancedBy(-2)
let urlRange = startIndex..<endIndex
return nextURL.substringWithRange(urlRange)
}
}
}
}
return nil
}
I think that the forEach() could have a better solution, but here is what I got:
let linkHeader = "</api/games?page=1&size=20>; rel=\"next\",</api/games?page=25&size=20>; rel=\"last\",</api/games?page=0&size=20>; rel=\"first\""
let links = linkHeader.components(separatedBy: ",")
var dictionary: [String: String] = [:]
links.forEach({
let components = $0.components(separatedBy:"; ")
let cleanPath = components[0].trimmingCharacters(in: CharacterSet(charactersIn: "<>"))
dictionary[components[1]] = cleanPath
})
if let nextPagePath = dictionary["rel=\"next\""] {
print("nextPagePath: \(nextPagePath)")
}
//Bonus
if let lastPagePath = dictionary["rel=\"last\""] {
print("lastPagePath: \(lastPagePath)")
}
if let firstPagePath = dictionary["rel=\"first\""] {
print("firstPagePath: \(firstPagePath)")
}
Console output:
$> nextPagePath: /api/games?page=1&size=20
$> lastPagePath: /api/games?page=25&size=20
$> firstPagePath: /api/games?page=0&size=20
I used components(separatedBy:) instead of split() to avoid the String() conversion at the end.
I created a Dictionary for the values to hold and removed the < and > with a trim.
I want to get only first letter from last name for privacy of users. Example: "John D."
extension String
{
public func getAcronyms(separator: String = "") -> String
{
let acronyms = self.components(separatedBy: " ").map({ String($0.characters.first!) }).joined(separator: separator);
return acronyms;
}
}
For proper naming, you have to use PersonNameComponentsFormatter.
let name = "Joe Singh"
let nameFormatter = PersonNameComponentsFormatter()
if let nameComps = nameFormatter.personNameComponents(from: name), let firstLetter = nameComps.givenName?.first, let lastName = nameComps.familyName {
let sortName = "\(firstLetter). \(lastName)" // J. Singh
}
You can also find:
nameComps.middleName
nameComps.familyName
nameComps.nameSuffix
nameComps.namePrefix
And also can configured the format of your names
Default
short
long
abbreviated