I am using AWS AppSync for creating my iOS application. I want to leverage the offline mutation as well as query caching provided by AppSync. But when I am turning my internet off, I am not getting any response. Rather its showing an error as "The Internet connection appears to be offline.". This seems to be rather an Alamofire exception than an AppSync exception. This is because the query is not getting cached inside my device. Following is my code snippet to initialize the client.
do {
let appSyncClientConfig = try AWSAppSyncClientConfiguration.init(url: AWSConstants.APP_SYNC_ENDPOINT, serviceRegion: AWSConstants.AWS_REGION, userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider())
AppSyncHelper.shared.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncClientConfig)
AppSyncHelper.shared.appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error in initializing the AppSync Client")
print("Error: \(error)")
UserDefaults.standard.set(nil, forKey: DeviceConstants.ID_TOKEN)
}
I am caching the token in the UserDefaults at the time of fetching the session, and then whenever the AppSyncClient is called, it fetches the latest token by calling the getLatestAuthToken() method of my MyCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider. This is returning the token stored in the UserDefaults -
// background thread - asynchronous
func getLatestAuthToken() -> String {
print("Inside getLatestAuthToken")
var token: String? = nil
if let tokenString = UserDefaults.standard.string(forKey: DeviceConstants.ID_TOKEN) {
token = tokenString
return token!
}
return token!
}
My query pattern is the following
public func getUserProfile(userId: String, success: #escaping (ProfileModel) -> Void, failure: #escaping (NSError) -> Void) {
let getQuery = GetUserProfileQuery(id: userId)
print("getQuery.id: \(getQuery.id)")
if appSyncClient != nil {
print("AppSyncClient is not nil")
appSyncClient?.fetch(query: getQuery, cachePolicy: CachePolicy.returnCacheDataElseFetch, queue: DispatchQueue.global(qos: .background), resultHandler: { (result, error) in
if error != nil {
failure(error! as NSError)
} else {
var profileModel = ProfileModel()
print("result: \(result)")
if let data = result?.data {
print("data: \(data)")
if let userProfile = data.snapshot["getUserProfile"] as? [String: Any?] {
profileModel = ProfileModel(id: UserDefaults.standard.string(forKey: DeviceConstants.USER_ID), username: userProfile["username"] as? String, mobileNumber: userProfile["mobileNumber"] as? String, name: userProfile["name"] as? String, gender: (userProfile["gender"] as? Gender).map { $0.rawValue }, dob: userProfile["dob"] as? String, profilePicUrl: userProfile["profilePicUrl"] as? String)
} else {
print("data snapshot is nil")
}
}
success(profileModel)
}
})
} else {
APPUtilites.displayErrorSnackbar(message: "Error in the user session. Please login again")
}
}
I have used all the 4 CachePolicy objects provided by AppSync, i.e,
CachePolicy.returnCacheDataElseFetch
CachePolicy.fetchIgnoringCacheData
CachePolicy.returnCacheDataDontFetch
CachePolicy.returnCacheDataAndFetch.
Can someone help me in implementing the cache properly for my iOS app so that I can do queries without the internet also?
Okay so I found the answer myself. The databaseUrl is an optional argument. It does not come in the suggestions when we are initializing the AWSAppSyncClientConfiguration object.
So the new way in which I initialized the client is the following
let databaseURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(AWSConstants.DATABASE_NAME, isDirectory: false)
do {
let appSyncClientConfig = try AWSAppSyncClientConfiguration.init(url: AWSConstants.APP_SYNC_ENDPOINT,
serviceRegion: AWSConstants.AWS_REGION,
userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider(),
urlSessionConfiguration: URLSessionConfiguration.default,
databaseURL: databaseURL)
AppSyncHelper.shared.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncClientConfig)
AppSyncHelper.shared.appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error in initializing the AppSync Client")
print("Error: \(error)")
}
Hope it helps.
Related
I was trying to learn the basics of networking with openweather api.
Implemented a very basic struct like this.
protocol WeatherManagerDelegate {
func didUpdateWeather(_ weatherManager : WeatherManager, weather : WeatherModel)
func didFailWithError(error: Error)
}
struct WeatherManager {
var delegate : WeatherManagerDelegate?
var temp : Double = 0.0
let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=40ca58efce193db0fc801564afb08283&units=metric"
func fetchWheather(cityName : String){
let urlString = "\(weatherURL)&q=\(cityName)"
performRequest(with: urlString)
}
func performRequest(with urlString: String){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url){ (data, response, error) in
if error != nil{
delegate?.didFailWithError(error: error!)
return
}
if let safedata = data {
if let weather = self.parseJSON(weatherData: safedata){
// let WeatherVC = WeatherViewController()
self.delegate!.didUpdateWeather(self, weather: weather)
}
print("Data is \(safedata)")
}
}
task.resume()
}
}
func parseJSON(weatherData:Data)-> WeatherModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
let id = decodedData.weather[0].id
let temp = decodedData.main.temp
let name = decodedData.name
let weather = WeatherModel(conditionID: id, cityName: name, temperature: temp)
return weather
} catch{
print("error is \(error)")
delegate?.didFailWithError(error: error)
return nil
}
}
}
But the issue is none of the print statements inside the requests are giving any outputs , nor i am able to update the UI. But the URL is giving the correct JSON Response when tried in browser
I am using the latest XCode on iOS 15 (iPhone 11 Device) with M1 Pro chip mac.
Found some threads which mentions to use "open with rosetta" but none of which worked.
also, not getting any errors on the console
Any solution?
Edit : Called in VC like this:
func textFieldDidEndEditing(_ textField: UITextField) {
if let city = searchTextField.text {
weatherManager.fetchWeather(cityName: city)
}
searchTextField.text = ""
}
Please try using:
let session = URLSession.shared
β¦instead of creating local session variable within the function scope
And if error != nil then don't return from that block. Simply use if-else for error handling.
I'm trying to create a "Profile" collection in Firestore in order to store more data on my users than just their email/name.
I'm stuck with creating this document and uploading the profile picture they choose (as an URL).
Here is the function called when they click on the "Register" button:
func register() {
Auth.auth().createUser(withEmail: self.email, password: self.pass) { (res, err) in
if err != nil {
self.error = err!.localizedDescription
self.alert.toggle()
return
}
// Success registering a new user
guard let userUID = res?.user.uid else { return }
uploadImageToFirestore(id: userUID, image: myImage) {
print("SUCCESS")
self.imageURL = downloadImageFromFirestore(id: userUID)
self.createUserDocument(id: userUID, imgURL: self.imageURL)
}
}
First step is uploading picture on Firebase Storage using the uploadImageToFirestore function and I tried using a completion handler to wait before calling the next 2 functions:
func uploadImageToFirestore(id: String, image: UIImage, completion: () -> Void) {
let storageRef = storage.reference().child("images/\(id)/image.jpg").putData(image.jpegData(compressionQuality: 0.35)!, metadata: nil) { (_, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
print("Success uploading picture to Firestore")
}
}
Second step is downloading the newly uploaded image on Firebase Storage to get the URL:
func downloadImageFromFirestore(id: String) -> String {
let storageRef = storage.reference().child("images/\(id)/image.jpg")
storageRef.downloadURL { url, error in
if error != nil {
print("DEBUG: \((error?.localizedDescription)!)")
return
}
print("Success downloading picture from Firestore")
self.imageURL = "\(url!)"
}
return self.imageURL
}
Third step is creating the Profile collection in Firestore with the ImageURL:
func createUserDocument(id: String, imgURL: String) {
db.collection("profiles").document(id).setData([
"name": name,
"surname": surname,
"email": email,
"shelter": isMember == true ? shelters[selectedShelter] : shelters[0],
"photoURL": imgURL,
"reputation": 0,
"uuid": id
])
{ err in
if let error = err {
print("Error ading document: \(error)")
} else {
print("New profile document created in Firestore")
}
}
}
THE PROBLEM
The problem I face is that in my "Register" function, the completion block of uploadImageToFirestore is never called, thus the function createUserDocument neither.
Is this the best way to achieve what I want (a.k.a. creating a profile document with the imageURL of the picture they just choose while registering)? Why is my completion block not called? (I don't see the "SUCCESS" printed in my console).
Thank you for your help!
Kind regards,
Jihaysse
You need to call the completion handler closure that you are passing to uploadImageToFirestore.
You should probably also pass the error, if any, so that you can handle it.
func uploadImageToFirestore(id: String, image: UIImage, completion: (Error?) -> Void) {
let storageRef = storage.reference().child("images/\(id)/image.jpg").putData(image.jpegData(compressionQuality: 0.35)!, metadata: nil) { (_, err) in
completion(err)
}
}
UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("πππππππIn closure function to update articlesπππππππ")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
I am currently checking my app version. My apps are notified if there is a new version and should the App Store screen, press the OK. I am checking the app version to do it, but it always shows an error.
func isUpdateAvailable(completion: #escaping (Bool?, Error?) -> Void) throws -> URLSessionDataTask {
guard let info = Bundle.main.infoDictionary,
let currentVersion = info["CFBundleShortVersionString"] as? String,
let identifier = info["CFBundleIdentifier"] as? String,
let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
throw IXError.invalidBundleInfo
}
Log.Debug(currentVersion)
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
if let error = error { throw error }
guard let data = data else { throw IXError.invalidResponse }
let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any]
guard let result = (json?["results"] as? [Any])?.first as? [String: Any], let version = result["version"] as? String else {
throw IXError.invalidResponse
}
completion(version != currentVersion, nil)
} catch {
completion(nil, error)
}
}
task.resume()
return task
}
Usage
_ = try? isUpdateAvailable { (update, error) in
if let error = error {
Log.Error(error)
} else if let update = update {
Log.Info(update)
}
}
Is this because my app doesn't have an app store?
If I have an app store, What response can I get to know if I have a version to update?
How can I go to the App Store?
Please help me a lot.
Yes, the method you use must be an already published app.
If you use an unpublished app, you will get results = []
Go to the App Store like this
let appId = "1454358806" // Replace with your appId
let appURL = URL.init(string: "itms-apps://itunes.apple.com/cn/app/id" + appId + "?mt=8") //Replace cn for your current country
UIApplication.shared.open(appURL!, options:[.universalLinksOnly : false]) { (success) in
}
Note:
This method will not be very timely, meaning that the application you just released, even if it can be searched in the App store, but results will not be updated immediately. Update information will be available after approximately 1 hour, or longer
I have done this via a completion handler
func appStoreVersion(callback: #escaping (Bool,String)->Void) {
let bundleId = Bundle.main.infoDictionary!["CFBundleIdentifier"] as! String
Alamofire.request("https://itunes.apple.com/lookup?bundleId=\(bundleId)").responseJSON { response in
if let json = response.result.value as? NSDictionary, let results = json["results"] as? NSArray, let entry = results.firstObject as? NSDictionary, let appStoreVersion = entry["version"] as? String{
callback(true,appStoreVersion)
}else{
callback(false, "-")
}
}
}
appStoreVersion contains your app version on app store. Remember: When your app goes live on app store, it may take up to 24 hours until you see the latest version.
Here is how to use it:
appStoreVersion { (success,version) in
appVersion = version
self.VersionLabel.text = "App version \(appVersion)"
}
You can use the success version to do a different things it version cannot be retrieved. i.e. you are not connected or.
You can check how it is used in this app(Settings Tab):
https://apps.apple.com/us/app/group-expenses-light/id1285557503
I have used the completion handler too.
private func getAppInfo(completion: #escaping (AppInfo?, Error?) -> Void) -> URLSessionDataTask? {
guard let identifier = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String,
let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
DispatchQueue.main.async {
completion(nil, VersionError.invalidBundleInfo)
}
return nil
}
private func getVersion() {
_ = getAppInfo { info, error in
if let appStoreAppVersion = info?.version {
let new = appStoreAppVersion.components(separatedBy: ".")
if let error = error {
print("error getting app store version: \(error)")
} else {
print(new)
}
}
We retrieve any saved passwords through the function:
SecRequestSharedWebCredential(NULL, NULL, ^(CFArrayRef credentials, CFErrorRef error) {
if (!error && CFArrayGetCount(credentials)) {
CFDictionaryRef credential = CFArrayGetValueAtIndex(credentials, 0);
if (credential > 0) {
CFDictionaryRef credential = CFArrayGetValueAtIndex(credentials, 0);
NSString *username = CFDictionaryGetValue(credential, kSecAttrAccount);
NSString *password = CFDictionaryGetValue(credential, kSecSharedPassword);
dispatch_async(dispatch_get_main_queue(), ^{
//Updates the UI here.
});
}
}
});
The issue is that on IOS 9.3.3 iPhone 6 A1524, we get the prompt with an entry called 'Passwords not saved'. There is no error message to suggest the no passwords have been found. Because the array > 0, it completes the form with the entry.
Why is this the case? We thought the prompt does not appear if no passwords are stored under your entitled domains.
Any suggestions?
Thank you.
I'm checking for this in viewDidLoad() for my Auth view controller. The code is a bit different than above, gleaned from several other SO answers.
Swift 3:
SecRequestSharedWebCredential(Configuration.webBaseFQDN as CFString, nil, { (credentials, error) in
if let error = error {
print("ERROR: credentials")
print(error)
}
guard let credentials = credentials, CFArrayGetCount(credentials) > 0 else {
// Did not find a shared web credential.
return
}
guard CFArrayGetCount(credentials) == 1 else {
// There should be exactly one credential.
return
}
let unsafeCredential = CFArrayGetValueAtIndex(credentials, 0)
let credential = unsafeBitCast(unsafeCredential, to: CFDictionary.self)
let unsafeEmail = CFDictionaryGetValue(credential, Unmanaged.passUnretained(kSecAttrAccount).toOpaque())
let email = unsafeBitCast(unsafeEmail, to: CFString.self) as String
let unsafePassword = CFDictionaryGetValue(credential, Unmanaged.passUnretained(kSecSharedPassword).toOpaque())
let password = unsafeBitCast(unsafePassword, to: CFString.self) as String
if self.isValidEmail(email) && self.isValidPassword(password) {
self.usedSharedWebCredentials = true
self.doSignIn(email: email, password: password)
}
})
The extra check at the end for isValidEmail(_:) and isValidPassword(_:) handles the case where SecRequeestSharedWebCredential() returns "Passwords not saved" in the first credential (email).
Hopefully someone can explain why this is happening, but if not, at least there's a way to trap this scenario.
I'd also like to add that I've seen this up to iOS 10.2.1
I bumped into the same issue and wanted to add that the spaces in "PasswordsΒ notΒ saved" are not real spaces. Not sure why, maybe something odd when converting from CFString.
Either way, since Apple documentation is still in ObjC and their Security framework is still very much CoreFoundation-heavy, I thought it'd be nice to post the whole Swift 5 code I've written for the Shared Web Credentials wrapper.
It has nice error management logic (to adjust since you might not have the same ErrorBuilder API). About the weird spaces, when copied from Xcode to StackOverflow, they turn into real spaces, hence the extra logic in the String extension.
There is nothing better online from what I've seen.
//
// CredentialsRepository.swift
// Created by Alberto De Bortoli on 26/07/2019.
//
import Foundation
public typealias Username = String
public typealias Password = String
public struct Credentials {
public let username: Username
public let password: Password
}
public enum GetCredentialsResult {
case success(Credentials)
case cancelled
case failure(Error)
}
public enum SaveCredentialsResult {
case success
case failure(Error)
}
protocol CredentialsRepository {
func getCredentials(completion: #escaping (GetCredentialsResult) -> Void)
func saveCredentials(_ credentials: Credentials, completion: #escaping (SaveCredentialsResult) -> Void)
}
//
// SharedWebCredentialsController.swift
// Created by Alberto De Bortoli on 26/07/2019.
//
class SharedWebCredentialsController {
let domain: String
init(domain: String) {
self.domain = domain
}
}
extension SharedWebCredentialsController: CredentialsRepository {
func getCredentials(completion: #escaping (GetCredentialsResult) -> Void) {
SecRequestSharedWebCredential(domain as CFString, .none) { cfArrayCredentials, cfError in
switch (cfArrayCredentials, cfError) {
case (_, .some(let cfError)):
let underlyingError = NSError(domain: CFErrorGetDomain(cfError) as String,
code: CFErrorGetCode(cfError),
userInfo: (CFErrorCopyUserInfo(cfError) as? Dictionary))
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsFetchFailure, underlyingError: underlyingError)
DispatchQueue.main.async {
completion(.failure(error))
}
case (.some(let cfArrayCredentials), _):
if let credentials = cfArrayCredentials as? [[String: String]], credentials.count > 0,
let entry = credentials.first,
// let domain = entry[kSecAttrServer as String]
let username = entry[kSecAttrAccount as String],
let password = entry[kSecSharedPassword as String] {
DispatchQueue.main.async {
if username.isValidUsername() {
completion(.success(Credentials(username: username, password: password)))
}
else {
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsFetchFailure, underlyingError: nil)
completion(.failure(error))
}
}
}
else {
DispatchQueue.main.async {
completion(.cancelled)
}
}
case (.none, .none):
DispatchQueue.main.async {
completion(.cancelled)
}
}
}
}
func saveCredentials(_ credentials: Credentials, completion: #escaping (SaveCredentialsResult) -> Void) {
SecAddSharedWebCredential(domain as CFString, credentials.username as CFString, credentials.password as CFString) { cfError in
switch cfError {
case .some(let cfError):
let underlyingError = NSError(domain: CFErrorGetDomain(cfError) as String,
code: CFErrorGetCode(cfError),
userInfo: (CFErrorCopyUserInfo(cfError) as? Dictionary))
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsSaveFailure, underlyingError: underlyingError)
DispatchQueue.main.async {
completion(.failure(error))
}
case .none:
DispatchQueue.main.async {
completion(.success)
}
}
}
}
}
extension String {
fileprivate func isValidUsername() -> Bool {
// https://stackoverflow.com/questions/38698565/secrequestsharedwebcredential-credentials-contains-passwords-not-saved
// don't touch the 'Passwords not saved', the spaces are not what they seem (value copied from debugger)
guard self != "PasswordsΒ notΒ saved" else { return false }
let containsAllInvalidWords = contains("Passwords") && contains("not") && contains("saved")
return !containsAllInvalidWords
}
}