App Auth crash when unarchiving AuthState - nskeyedunarchiver

I'm implementing the App Auth library in my iOS app, and trying to clean up a couple warnings when archiving the Auth State.
'unarchiveObject(with:)' was deprecated in iOS 12.0: Use +unarchivedObjectOfClass:fromData:error: instead
and
'archivedData(withRootObject:)' was deprecated in iOS 12.0: Use +archivedDataWithRootObject:requiringSecureCoding:error: instead
The following works fine but with the warnings:
private func loadAuthState() {
guard let data = UserDefaults.standard.object(forKey: kAppAuthAuthStateKey) as? Data else {
return
}
if let authState = NSKeyedUnarchiver.unarchiveObject(with: data) as? OIDAuthState {
print("AuthState loaded. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")")
self.authState = authState
self.signedIn.update(with: .signedIn)
}
}
private func saveAuthState() {
if let authState = self.authState {
let data = NSKeyedArchiver.archivedData(withRootObject: authState)
UserDefaults.standard.set(data, forKey: kAppAuthAuthStateKey)
UserDefaults.standard.synchronize()
}
}
After changing to the following I get a crash on the unarchive (Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The response_type "(null)" isn't supported. AppAuth only supports the "code" or "code id_token" response_type.'):
private func loadAuthState() {
guard let data = UserDefaults.standard.object(forKey: kAppAuthAuthStateKey) as? Data else {
return
}
if let authState = try? NSKeyedUnarchiver.unarchivedObject(ofClass: OIDAuthState.self, from: data) {
print("AuthState loaded. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")")
self.authState = authState
self.signedIn.update(with: .signedIn)
}
}
private func saveAuthState() {
if let authState = self.authState {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: authState, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: kAppAuthAuthStateKey)
UserDefaults.standard.synchronize()
}
}

Related

How to get around 'unarchiveObject(with:)' was deprecated in iOS 12.0 when setting an HKQueryAnchor?

I was using the code below to set an HKQueryAnchor when making a HKAnchoredObjectQuery however 'unarchiveObject(with:)' has been deprecated and I can't figure out how to write it with the new API?
private func getAnchor() -> HKQueryAnchor? {
let encoded = UserDefaults.standard.data(forKey: AnchorKey)
if(encoded == nil){
return nil
}
let anchor = NSKeyedUnarchiver.unarchiveObject(with: encoded!) as? HKQueryAnchor
return anchor
}
private func saveAnchor(anchor : HKQueryAnchor) {
let encoded = NSKeyedArchiver.archivedData(withRootObject: anchor)
defaults.setValue(encoded, forKey: AnchorKey)
defaults.synchronize()
}
This is what I came up with based on Martin R's link, look ok?
private func getAnchor() -> HKQueryAnchor? {
let encoded = UserDefaults.standard.data(forKey: AnchorKey)
guard let unwrappedEncoded = encoded else { return nil }
guard let anchor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unwrappedEncoded as Data) as? HKQueryAnchor
else {
return nil
}
return anchor
}
private func saveAnchor(anchor : HKQueryAnchor) {
do {
let encoded = try NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: false)
defaults.setValue(encoded, forKey: AnchorKey)
defaults.synchronize()
} catch {
return
}
}
Try using JSONDecoder and JSONEncoder to get the data to and from HKQueryAnchor instance, i.e.
private func getAnchor() -> HKQueryAnchor? {
guard let encoded = UserDefaults.standard.data(forKey: AnchorKey) else {
return nil
}
let anchor = try? JSONDecoder().decode(HKQueryAnchor.self, from: encoded)
return anchor
}
private func saveAnchor(anchor : HKQueryAnchor) {
if let encoded = try? JSONEncoder().encode(anchor) {
defaults.setValue(encoded, forKey: AnchorKey)
defaults.synchronize()
}
}

Share Data between Project and Share Extension

I would like to share data between my Main Project and my Share Extension. This is what I did:
1. enable App Groups in both Project & Share Extension
2. save data in Project inside viewDidLoad (works fine, I tested it):
DataHandler.getWishlists { (success, dataArray, dropOptionsArray) in
if success && dataArray != nil {
self.shouldAnimateCells = true
self.dataSourceArray = dataArray as! [Wishlist]
self.theCollectionView.isHidden = false
self.theCollectionView.reloadData()
self.dropOptions = dropOptionsArray as! [DropDownOption]
self.addButton.isEnabled = true
self.activityIndicator.stopAnimating()
// save dataSourceArray in UserDefaults
if let defaults = UserDefaults(suiteName: UserDefaults.Keys.groupKey) {
defaults.setDataSourceArray(data: dataArray as! [Wishlist])
defaults.synchronize()
} else {
print("error Main")
}
}
}
3. retrive data in Share Extension (error 2 fires!)
if let defaults = UserDefaults(suiteName: UserDefaults.Keys.groupKey) {
if let data = defaults.getDataSourceArray() {
dataSourceArray = data
defaults.synchronize()
}else {
print("error 2")
}
} else {
print("error 1")
}
UserDefaults + Helpers
extension UserDefaults {
public struct Keys {
public static let groupKey = "group.wishlists-app.wishlists"
public static let dataSourceKey = "dataSourceKey"
}
func setDataSourceArray(data: [Wishlist]){
set(try? PropertyListEncoder().encode(data), forKey: Keys.dataSourceKey)
synchronize()
}
func getDataSourceArray() -> [Wishlist]? {
if let data = UserDefaults.standard.value(forKey: Keys.dataSourceKey) as? Data {
if let dataSourceArray = try? PropertyListDecoder().decode(Array<Wishlist>.self, from: data) as [Wishlist] {
return dataSourceArray
}
}
return nil
}
}
I can not retrieve the data inside my Share Extension but I have no idea why. Could anyone help me out here?
Your helper function getDataSourceArray() tries to access UserDefaults.standard which is not shared between your host app and the extension app. You need to use the shared container.
UserDefaults.standard -> not shared between host and extension
UserDefaults(suiteName:) -> shared between host and extension
Try to change your function to this:
func getDataSourceArray() - > [Wishlist] ? {
if let data = UserDefaults(suiteName: UserDefaults.Keys.groupKey).value(forKey: Keys.dataSourceKey) as ? Data {
if let dataSourceArray =
try ? PropertyListDecoder().decode(Array < Wishlist > .self, from: data) as[Wishlist] {
return dataSourceArray
}
}
return nil
}

CloudKit Public Records And Changes not Downloaded

CloudKit Public Records And Changes not Downloaded
I have a CloudKit app with records for both a Public and a Custom Private
Zone. I seem to have the change token process working for the custom
private zone but am unable to get the public data to work. The code I am
using is identical for both databases except for the public/private
names and using the default zone for the public. I understand that
subscriptions do not work on default zones, but I could not find any
references to limitations on change tokens for public data. Xcode 10.1, iOS 12.0
I create my PubicData class and initialize it:
var publicDatabase : CKDatabase!
init() {
let kAppDelegate = UIApplication.shared.delegate as! AppDelegate
context = kAppDelegate.context
let container = CKContainer.default()
publicDatabase = container.publicCloudDatabase
}//init
the download function that is called from the app entry scene - a tableview:
func downloadPublicUpdates(finishClosure : # escaping(UIBackgroundFetchResult) -> Void) {
var listRecordsUpdated : [CKRecord] = []
var listRecordsDeleted : [String : String] = [:]
var publicChangeToken : CKServerChangeToken!
var publicChangeZoneToken : CKServerChangeToken!
let userSettings = UserDefaults.standard
if let data = userSettings.value(forKey: "publicChangeToken") as? Data {
if let token = try? NSKeyedUnarchiver.unarchivedObject(ofClass : CKServerChangeToken.self, from : data) {
publicChangeToken = token
print("publicChangeToken exists")
}
} else {
print("userSettings entry for publicChangeToken does not exist")
}//if let data
if let data = userSettings.value(forKey: "publicChangeZoneToken") as? Data {
if let token = try? NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: data) {
publicChangeZoneToken = token
}
}//if let data
let zone = CKRecordZone.default()
var zonesIDs : [CKRecordZone.ID] = [zone.zoneID]
let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: publicChangeToken)
operation.recordZoneWithIDChangedBlock = {(zoneID) in
zonesIDs.append(zoneID)
}
operation.changeTokenUpdatedBlock = {(token) in
publicChangeToken = token
}
operation.fetchDatabaseChangesCompletionBlock = {(token, more, error) in
if error != nil{
finishClosure(UIBackgroundFetchResult.failed)
} else if !zonesIDs.isEmpty {
publicChangeToken = token
let configuration = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
configuration.previousServerChangeToken = publicChangeZoneToken
let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zonesIDs, configurationsByRecordZoneID: [zonesIDs[0] : configuration])
fetchOperation.recordChangedBlock = {(record) in
listRecordsUpdated.append(record)
}//fetchOperation.recordChangedBlock
fetchOperation.recordWithIDWasDeletedBlock = {(recordID, recordType) in
listRecordsDeleted[recordID.recordName] = recordType
}//fetchOperation.recordWithIDWasDeletedBlock
fetchOperation.recordZoneChangeTokensUpdatedBlock = {(zoneID, token, data) in
publicChangeZoneToken = token
}//fetchOperation.recordZoneChangeTokensUpdatedBlock
fetchOperation.recordZoneFetchCompletionBlock = {(zoneID, token, data, more, error) in
if let ckerror = error as? CKError {
self.processErrors(error: ckerror)
} else {
publicChangeZoneToken = token
self.updateLocalRecords(listRecordsUpdated : listRecordsUpdated)
self.deleteLocalRecords(listRecordsDeleted : listRecordsDeleted)
listRecordsUpdated.removeAll()
listRecordsDeleted.removeAll()
}//if else
}//fetchOperation.recordZoneFetchCompletionBlock
fetchOperation.fetchRecordZoneChangesCompletionBlock = {(error) in
if error != nil {
print("Error fetchRecordZoneChangesCompletionBlock")
finishClosure(UIBackgroundFetchResult.failed)
} else {
if publicChangeToken != nil {
if let data = try? NSKeyedArchiver.archivedData(withRootObject: publicChangeToken, requiringSecureCoding: false) {
userSettings.set(data, forKey : "publicChangeToken")
}
}//if changeToken != nil
if publicChangeZoneToken != nil {
if let data = try? NSKeyedArchiver.archivedData(withRootObject: publicChangeZoneToken, requiringSecureCoding: false) {
userSettings.set(data, forKey : "publicChangeZoneToken")
}
}
//self.updateInterface()
self.updateLocalReferences()
finishClosure(UIBackgroundFetchResult.newData)
}
}//fetchOperation.fetchRecordZoneChangesCompletionBlock
self.publicDatabase.add(fetchOperation)
} else {//else if !zonesIDs.isEmpty
finishClosure(UIBackgroundFetchResult.noData)
}//if zoneid not empty
}//fetchDatabaseChangesCompletionBlock
print("listRecordsUpdated.count is \(listRecordsUpdated.count)")
publicDatabase.add(operation)
}//downloadPublicUpdates
Outside of class: var PD = PDData()
I call the download method in viewDidLoad from the initial TableViewController:
PD.downloadPublicUpdates { (result) in
print("in ctvc viewDidLoad and downloadPublicUpdates")
switch result {
case .noData:
print("no data")
case .newData:
print("new data")
case .failed:
print("failed to get data")
}//switch
}//downloadPublicUpdates
The console output is always:
userSettings entry for publicChangeToken does not exist
listRecordsUpdated.count is 0
in ctvc viewDidLoad and downloadPublicUpdates
failed to get data
Any guidance would be appreciated.
There are no change tokens available in a public database. Those only exist in private and shared databases.
To keep things in sync, you typically have to keep a modification date on records locally, and then query for stuff that is newer on the CloudKit server using a CKQueryOperation.
Good luck!

Enabling cache for AWS AppSync Client iOS Swift

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.

Unarchive Array with NSKeyedUnarchiver unarchivedObject(ofClass:from:)

Since upgrading to Swift 4.2 I've found that many of the NSKeyedUnarchiver and NSKeyedArchiver methods have been deprecated and we must now use the type method static func unarchivedObject<DecodedObjectType>(ofClass: DecodedObjectType.Type, from: Data) -> DecodedObjectType? to unarchive data.
I have managed to successfully archive an Array of my bespoke class WidgetData, which is an NSObject subclass:
private static func archiveWidgetDataArray(widgetDataArray : [WidgetData]) -> NSData {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: widgetDataArray as Array, requiringSecureCoding: false) as NSData
else { fatalError("Can't encode data") }
return data
}
The problem comes when I try to unarchive this data:
static func loadWidgetDataArray() -> [WidgetData]? {
if isKeyPresentInUserDefaults(key: USER_DEFAULTS_KEY_WIDGET_DATA) {
if let unarchivedObject = UserDefaults.standard.object(forKey: USER_DEFAULTS_KEY_WIDGET_DATA) as? Data {
//THIS FUNCTION HAS NOW BEEN DEPRECATED:
//return NSKeyedUnarchiver.unarchiveObject(with: unarchivedObject as Data) as? [WidgetData]
guard let nsArray = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: unarchivedObject as Data) else {
fatalError("loadWidgetDataArray - Can't encode data")
}
guard let array = nsArray as? Array<WidgetData> else {
fatalError("loadWidgetDataArray - Can't get Array")
}
return array
}
}
return nil
}
But this fails, as using Array.self instead of NSArray.self is disallowed. What am I doing wrong and how can I fix this to unarchive my Array?
You can use unarchiveTopLevelObjectWithData(_:) to unarchive the data archived by archivedData(withRootObject:requiringSecureCoding:). (I believe this is not deprecated yet.)
But before showing some code, you should better:
Avoid using NSData, use Data instead
Avoid using try? which disposes error info useful for debugging
Remove all unneeded casts
Try this:
private static func archiveWidgetDataArray(widgetDataArray : [WidgetData]) -> Data {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: widgetDataArray, requiringSecureCoding: false)
return data
} catch {
fatalError("Can't encode data: \(error)")
}
}
static func loadWidgetDataArray() -> [WidgetData]? {
guard
isKeyPresentInUserDefaults(key: USER_DEFAULTS_KEY_WIDGET_DATA), //<- Do you really need this line?
let unarchivedObject = UserDefaults.standard.data(forKey: USER_DEFAULTS_KEY_WIDGET_DATA)
else {
return nil
}
do {
guard let array = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) as? [WidgetData] else {
fatalError("loadWidgetDataArray - Can't get Array")
}
return array
} catch {
fatalError("loadWidgetDataArray - Can't encode data: \(error)")
}
}
But if you are making a new app, you should better consider using Codable.
unarchiveTopLevelObjectWithData(_:)
is deprecated as well. So to unarchive data without secure coding you need to:
Create NSKeyedUnarchiver with init(forReadingFrom: Data)
Set requiresSecureCoding of created unarchiver to false.
Call decodeObject(of: [AnyClass]?, forKey: String) -> Any? to get your object, just use proper class and NSKeyedArchiveRootObjectKeyas key.
As unarchiveTopLevelObjectWithData is also deprecated after iOS 14.3 only the Hopreeeenjust's answer is correct now.
But if you don't need NSSecureCoding you also can use answer of Maciej S
It is very easy to use it, by adding extension to NSCoding protocol:
extension NSCoding where Self: NSObject {
static func unsecureUnarchived(from data: Data) -> Self? {
do {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false
let obj = unarchiver.decodeObject(of: self, forKey: NSKeyedArchiveRootObjectKey)
if let error = unarchiver.error {
print("Error:\(error)")
}
return obj
} catch {
print("Error:\(error)")
}
return nil
}
}
With this extension to unarchive e.g. NSArray you only need:
let myArray = NSArray.unsecureUnarchived(from: data)
For Objective C use NSObject category:
+ (instancetype)unsecureUnarchivedFromData:(NSData *)data {
NSError * err = nil;
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData: data error: &err];
unarchiver.requiresSecureCoding = NO;
id res = [unarchiver decodeObjectOfClass:self forKey:NSKeyedArchiveRootObjectKey];
err = err ?: unarchiver.error;
if (err != nil) {
NSLog(#"NSKeyedUnarchiver unarchivedObject error: %#", err);
}
return res;
}
Note that if the requiresSecureCoding is false, class of unarchived object is not actually checked and objective c code returns valid result even if it is called from wrong class.
And swift code when called from wrong class returns nil (because of optional casting), but without error.
Swift 5- IOS 13
guard let mainData = UserDefaults.standard.object(forKey: "eventDetail") as? NSData
else {
print(" data not found in UserDefaults")
return
}
do {
guard let finalArray =
try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(mainData as Data) as? [EventDetail]
else {
return
}
self.eventDetail = finalArray
}
You are likely looking for this:
if let widgetsData = UserDefaults.standard.data(forKey: USER_DEFAULTS_KEY_WIDGET_DATA) {
if let widgets = (try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, WidgetData.self], from: widgetsData)) as? [WidgetData] {
// your code
}
}
if #available(iOS 12.0, *) {
guard let unarchivedFavorites = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(favoritesData!)
else {
return
}
self.channelFavorites = unarchivedFavorites as! [ChannelFavorite]
} else {
if let unarchivedFavorites = NSKeyedUnarchiver.unarchiveObject(with: favoritesData!) as? [ChannelFavorite] {
self.channelFavorites = unarchivedFavorites
}
// Achieving data
if #available(iOS 12.0, *) {
// use iOS 12-only feature
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: channelFavorites, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "channelFavorites")
} catch {
return
}
} else {
// handle older versions
let data = NSKeyedArchiver.archivedData(withRootObject: channelFavorites)
UserDefaults.standard.set(data, forKey: "channelFavorites")
}
This is the way I have updated my code and its working for me

Resources