/*
|
See LICENSE folder for this sample’s licensing information.
|
|
Abstract:
|
A data manager that manages data conforming to `Codable` and stores it in `UserDefaults`.
|
*/
|
|
using System;
|
using Foundation;
|
using CoreFoundation;
|
|
namespace Other.Siri
|
{
|
public struct UserDefaultsStorageDescriptor
|
{
|
public string Key { get; set; }
|
public UserDefaultsStorageDescriptor(string key)
|
{
|
Key = key;
|
}
|
}
|
|
public static class NotificationKeys
|
{
|
// Clients of `DataManager` that want to know when the data changes can
|
// listen for this notification.
|
public const string DataChanged = "DataChangedNotification";
|
}
|
|
public class DataManager<ManagedDataType> : NSObject
|
where ManagedDataType : NSObject, INSCoding
|
{
|
// This sample uses App Groups to share a suite of data between the
|
// main app and the different extensions.
|
protected NSUserDefaults UserDefaults = NSUserDefaultsHelper.DataSuite;
|
|
// To prevent data races, all access to `UserDefaults` uses this queue.
|
protected DispatchQueue UserDefaultsAccessQueue = new DispatchQueue("User Defaults Access Queue");
|
|
// Storage and observation information.
|
protected UserDefaultsStorageDescriptor StorageDescriptor;
|
|
// A flag to avoid receiving notifications about data this instance just
|
// wrote to `UserDefaults`.
|
protected bool IgnoreLocalUserDefaultsChanges = false;
|
|
// The observer object handed back after registering to observe a
|
// property.
|
IDisposable UserDefaultsObserver;
|
|
// The data managed by this `DataManager`.
|
//此“DataManager”管理的数据。
|
protected ManagedDataType ManagedDataBackingInstance;
|
|
// Access to `managedDataBackingInstance` needs to occur on a dedicated
|
// queue to avoid data races.
|
protected DispatchQueue DataAccessQueue = new DispatchQueue("Data Access Queue");
|
|
// Public access to the managed data for clients of `DataManager`
|
public ManagedDataType ManagedData
|
{
|
get
|
{
|
ManagedDataType data = null;
|
DataAccessQueue.DispatchSync(() => data = ManagedDataBackingInstance);
|
return data;
|
}
|
}
|
|
// See note below about createInitialData and initialData
|
public DataManager(UserDefaultsStorageDescriptor storageDescriptor, ManagedDataType initialData)
|
{
|
StorageDescriptor = storageDescriptor;
|
LoadData();
|
if (ManagedDataBackingInstance is null)
|
{
|
ManagedDataBackingInstance = initialData;
|
WriteData();
|
}
|
ObserveChangesInUserDefaults();
|
}
|
|
// createInitialData
|
//
|
// The Swift version of this app has a createInitialData method.
|
// Each child class of the DataManager class overrides this method, and
|
// then the DataManager base class calls the derived versions to get
|
// the initial data. C# gives a compiler warning for this ("Virtual
|
// member call in constructor"). Since in C# the base class constructor
|
// is run before the child class constructor, having the base clas
|
// constructor call out to a method on the derived class is calling
|
// a method on an object that has not yet been fully constructed.
|
// The C# version of this sample works around this problem by passing
|
// in the initial data to the constructor.
|
|
void ObserveChangesInUserDefaults()
|
{
|
var weakThis = new WeakReference<DataManager<ManagedDataType>>(this);
|
Action<NSObservedChange> changeHandler = (change) =>
|
{
|
if (weakThis.TryGetTarget(out var dataManager))
|
{
|
// Ignore any change notifications coming from data this
|
// instance just saved to `NSUserDefaults`.
|
if (dataManager is null || dataManager.IgnoreLocalUserDefaultsChanges)
|
{
|
return;
|
}
|
|
// The underlying data changed in `NSUserDefaults`, so
|
// update this instance with the change and notify clients
|
// of the change.
|
dataManager.LoadData();
|
dataManager.NotifyClientsDataChanged();
|
}
|
};
|
UserDefaultsObserver = UserDefaults.AddObserver(
|
StorageDescriptor.Key,
|
NSKeyValueObservingOptions.Initial | NSKeyValueObservingOptions.New,
|
changeHandler
|
);
|
}
|
|
// Notifies clients the data changed by posting an `NSNotification` with
|
// the key `NotificationKeys.DataChanged`
|
void NotifyClientsDataChanged()
|
{
|
var notification = NSNotification.FromName(NotificationKeys.DataChanged, this);
|
NSNotificationCenter.DefaultCenter.PostNotification(notification);
|
}
|
|
protected virtual void FinishUnarchiving(NSObject unarchivedData)
|
{
|
throw new NotImplementedException();
|
}
|
|
// Loads the data from `NSUserDefaults`.
|
void LoadData()
|
{
|
UserDefaultsAccessQueue.DispatchSync(() =>
|
{
|
NSData archivedData = UserDefaults.DataForKey(StorageDescriptor.Key);
|
try
|
{
|
// Let the derived classes handle the specifics of
|
// putting the unarchived data in the correct format.
|
// This is necessary because the derived classes
|
// (SoupMenuManager, SoupOrderMenuManager) are using
|
// generic data formats (NSMutableSet<T> or NSMutableArray<T>)
|
// and these types cannot be casted directly from the
|
// deserialized data.
|
NSObject unarchivedData = NSKeyedUnarchiver.UnarchiveObject(archivedData);
|
FinishUnarchiving(unarchivedData);
|
}
|
catch (Exception e)
|
{
|
if (!(e is null))
|
{
|
Console.WriteLine($"Error: {e.Message}");
|
}
|
}
|
});
|
}
|
|
// Writes the data to `NSUserDefaults`
|
protected void WriteData()
|
{
|
UserDefaultsAccessQueue.DispatchAsync(() =>
|
{
|
try
|
{
|
NSData encodedData = NSKeyedArchiver.ArchivedDataWithRootObject(ManagedDataBackingInstance);
|
IgnoreLocalUserDefaultsChanges = true;
|
UserDefaults.SetValueForKey(encodedData, (NSString)StorageDescriptor.Key);
|
IgnoreLocalUserDefaultsChanges = false;
|
NotifyClientsDataChanged();
|
}
|
catch (Exception e)
|
{
|
throw new Exception($"Could not save data. Reason: {e.Message}");
|
}
|
});
|
}
|
|
|
|
}
|
}
|