Solución de persistencia de configuración local de la aplicación

descripción general

Durante el proceso de desarrollo de la aplicación, encontrará muchos requisitos de persistencia para elementos de configuración simples. Por ejemplo, la hora en que se inició la aplicación por última vez, la ID de usuario del último inicio de sesión en la aplicación y las condiciones de juicio para que el usuario use la función por primera vez. Y a medida que el negocio se expande, la configuración fragmentada seguirá aumentando.

Valores predeterminados de usuario

Apple proporciona el marco UserDefault para ayudarnos a almacenar configuraciones discretas, y UserDefaults se almacenará en el entorno sandbox en forma de archivos plist. Esta es la solución más recomendada sin introducir la base de datos NoSql.

Precauciones

Para mejorar la velocidad de lectura, la aplicación cargará la plist correspondiente a UserDefaults Standard en la memoria al inicio. Si el archivo es demasiado grande, aumentará el tiempo de carga de la aplicación al inicio y aumentará el consumo de memoria a un cierto medida.

Entonces, en Estándar, debemos almacenar información que debe obtenerse inmediatamente durante la fase de inicio de la aplicación, como la ID de inicio de sesión más reciente del usuario y la versión del caché de configuración remota de la aplicación.

Podemos reducir la cantidad de datos estándar dividiendo tablas. Utilice el modo suiteName de UserDefaults para crear diferentes tablas de configuración, de modo que los elementos de configuración se almacenen en sus propios archivos plist y estos plist independientes no se carguen automáticamente al inicio.

Preguntas frecuentes sobre la gestión de la configuración

  1. Utilice la clave de cadena codificada para almacenar la configuración en UserDefaults y acceda a los datos copiando y pegando la cadena de la clave.

  2. El uso disperso de UserDefaults carece de una solución de gestión centralizada. Por ejemplo, si necesita almacenar la configuración de "habilitar la función de notificación", la clave generalmente se mantiene directamente en el código relacionado con el negocio.

Esquema 1.0

Administrar valores predeterminados de usuario

Cree una clase de gestión UserDefault, el objetivo principal es cerrar el marco UserDefault y unificar la estrategia de uso.

public class UserDefaultsManager {
    public static let shared = UserDefaultsManager()
    private init() {}
    public var suiteName:String? {
        didSet {
            /**
             根据传入的 suiteName的不同会产生四种情况:
             传入 nil:跟使用UserDefaults.standard效果相同;
             传入 bundle id:无效,返回 nil;
             传入 App Groups 配置中 Group ID:会操作 APP 的共享目录中创建的以Group ID命名的 plist 文件,方便宿主应用与扩展应用之间共享数据;
             传入其他值:操作的是沙箱中 Library/Preferences 目录下以 suiteName 命名的  plist 文件。
             */
            userDefault = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
        }
    }
    public var userDefault = UserDefaults.standard
}

复制代码

crear tabla constante

  1. Registro y mantenimiento centralizado de la clave del elemento de configuración
struct UserDefaultsKey {
    static let appLanguageCode              = "appLanguageCode"
    static let lastLaunchSaleDate           = "resetLastLaunchSaleDate"
    static let lastSaleDate                 = "lastSaleDate"
    static let lastSaveRateDate             = "lastSaveRateDate"
    static let lastVibrateTime              = "lastVibrateTime"
    static let exportedImageSaveCount       = "exportedImageSaveCount"
  
    static let onceFirstLaunchDate          = "onceFirstLaunchDate"
    static let onceServerUserIdStr          = "onceServerUserIdStr"
    static let onceDidClickCanvasButton     = "onceDidClickCanvasButton"
    static let onceDidClickCanvasTips       = "onceDidClickCanvasTips"
    static let onceDidClickEditBarGuide     = "onceDidClickEditBarGuide"
    static let onceDidClickEditFreestyleGuide   = "onceDidClickEditFreestyleGuide"
    static let onceDidClickManualCutoutGuide    = "onceDidClickManualCutoutGuide"
    static let onceDidClickBackgroundBlurGuide  = "onceDidClickBackgroundBlurGuide"
    static let onceDidTapCustomStickerBubble    = "onceDidTapCustomStickerBubble"
    static let onceDidRequestHomeTemplatesFromAPI = "onceDidRequestHomeTemplatesFromAPI"
  
  	static let firSaveExportTemplateKey     = "firSaveExportTemplateKey"
    static let firSaveTemplateDateKey       = "firSaveTemplateDateKey"
    static let firShareExportTemplateKey    = "firShareExportTemplateKey"
    static let firShareTemplateDateKey      = "firShareTemplateDateKey"
}
复制代码
  1. Proporcionar API CURD
private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.config").userDefaults

var exportedImageSaveCount: Int { 
  return appConfigUserDefaults.integer(forKey: key) 
}

func increaseExportedImageSaveCount() {
		let key = UserDefaultsKey.exportedImageSaveCount
		var count = appConfigUserDefaults.integer(forKey: key)
		count += 1
		appConfigUserDefaults.setValue(count, forKey: key)
}
复制代码

Hemos encapsulado la fuente de datos UserDefaults y el registro de la clave de cadena también está unificado en el archivo de constantes. Cuando queremos buscar o modificar, podemos encontrar fácilmente la clave de cadena de la tabla de configuración.

A medida que el negocio se expande, habrá más y más elementos de configuración y necesitaremos reorganizar varias subtablas en función de la clasificación de las funciones comerciales.

Luego encontraremos algunos problemas:

  1. String Key的注册虽然不麻烦,但Key中无法体现出Key归属与哪个UserDefaults。

  2. CURD API的数量会膨胀的更快,需要更多的维护成本。那么能不能将配置的管理更加面向对象,实现类似ORM的方式来管理呢?

方案2.0

根据上述的问题,来演化下方案2.0,我们来创建一个协议,用来规范UserDefaults的使用类。

它将包含CURD API的默认实现,初始化关联UserDefaults,自动生成String Key。

/// UserDefaults存储协议,建议用String类型的枚举去实现该协议
public protocol UserDefaultPreference {

    var userDefaults: UserDefaults { get }
    var key: String { get }

    var bool: Bool { get }
    var int: Int { get }
    var float: Float { get }
    var double: Double { get }

    var string: String? { get }
    var stringValue: String { get }

    var dictionary: [String: Any]? { get }
    var dictionaryValue: [String: Any] { get }

    var array: [Any]? { get }
    var arrayValue: [Any] { get }

    var stringArray: [String]? { get }
    var stringArrayValue: [String] { get }

    var data: Data? { get }
    var dataValue: Data { get }

    var object: Any? { get }
    var url: URL? { get }

    func codableObject<T: Decodable>(_ as:T.Type) -> T?

    func save<T: Encodable>(codableObject: T) -> Bool

    func save(string: String)
    func save(object: Any?)
    func save(int: Int)
    func save(float: Float)
    func save(double: Double)
    func save(bool: Bool)
    func save(url: URL?)
    func remove()
}
复制代码

定义完协议后,我们再添加一些默认实现,降低使用成本。

// 生成默认的String Key
public extension UserDefaultPreference where Self: RawRepresentable, Self.RawValue == String {
    var key: String {  return "\(type(of: self)).\(rawValue)" }
}

public extension UserDefaultPreference {
		// 默认使用 standard UserDefaults,可以在实现类中配置
    var userDefaults: UserDefaults { return UserDefaultsManager.shared.userDefaults }

    func codableObject<T: Decodable>(_ as:T.Type) -> T? {
        return UserDefaultsManager.codableObject(`as`, key: key, userDefaults: userDefaults)
    }

    @discardableResult
    func save<T: Encodable>(codableObject: T) -> Bool {
        return UserDefaultsManager.save(codableObject: codableObject, key: key, userDefaults: userDefaults)
    }

    var object: Any? { return userDefaults.object(forKey: key) }

    func hasKey() -> Bool { return userDefaults.object(forKey: key) != nil }

    var url: URL? { return userDefaults.url(forKey: key) }

    var string: String? { return userDefaults.string(forKey: key) }
    var stringValue: String { return string ?? "" }

    var dictionary: [String: Any]? { return userDefaults.dictionary(forKey: key) }
    var dictionaryValue: [String: Any] { return dictionary ?? [String: Any]() }

    var array: [Any]? { return userDefaults.array(forKey: key) }
    var arrayValue: [Any] { return array ?? [Any]() }

    var stringArray: [String]? { return userDefaults.stringArray(forKey: key) }
    var stringArrayValue: [String] { return stringArray ?? [String]() }

    var data: Data? { return userDefaults.data(forKey: key) }
    var dataValue: Data { return userDefaults.data(forKey: key) ?? Data() }

    var bool: Bool { return userDefaults.bool(forKey: key) }
    var boolValue: Bool? {
        guard hasKey() else { return nil }
        return bool
    }

    var int: Int { return userDefaults.integer(forKey: key) }
    var intValue: Int? {
        guard hasKey() else { return nil }
        return int
    }

    var float: Float { return userDefaults.float(forKey: key) }
    var floatValue: Float? {
        guard hasKey() else { return nil }
        return float
    }

    var double: Double { return userDefaults.double(forKey: key) }
    var doubleValue: Double? {
        guard hasKey() else { return nil }
        return double
    }

    func save(object: Any?) { userDefaults.set(object, forKey: key) }
    func save(string: String) { userDefaults.set(string, forKey: key) }
    func save(int: Int) { userDefaults.set(int, forKey: key) }
    func save(float: Float) { userDefaults.set(float, forKey: key) }
    func save(double: Double) { userDefaults.set(double, forKey: key) }
    func save(bool: Bool) { userDefaults.set(bool, forKey: key) }
    func save(url: URL?) { userDefaults.set(url, forKey: key) }

    func remove() { userDefaults.removeObject(forKey: key) }
}
复制代码

OK,我们来看下使用的案例

// MARK: - Launch
enum LaunchEventKey: String {
    case didShowLaunchGuideOnThisLaunch
    case launchGuideIsAlreadyShow
}
extension LaunchEventKey: UserDefaultPreference { }

func checkIfNeedLaunchGuide() -> Bool {
  return !LaunchEventKey.launchGuideIsAlreadyShow.bool  
}
func launchContentView() {
  LaunchEventKey.launchGuideIsAlreadyShow.save(bool: true)  
}

// MARK: - Language
enum LanguageEventKey: String {
    case appLanguageCode
}
extension LanguageEventKey: UserDefaultPreference { }

static var appLanguageCode: String {
  get {
    let code = LanguageEventKey.appLanguageCode.string ?? ""
    return code
  }
  set {
    LanguageEventKey.appLanguageCode.save(codableObject: newValue)
  }
}

// MARK: - Purchase
enum PurchaseStatusKey: String {
    case iapSubscribeExpireDate
}
extension PurchaseStatusKey: UserDefaultPreference { }

func handle() {
  let expirationDate: Date = Entitlement.expirationDate
  PurchaseStatusKey.iapSubscribeExpireDate.save(object: expirationDate)
}

func getValues() {
  let subscribeExpireDate = PurchaseStatusKey.iapSubscribeExpireDate.object as? Date
}

// MARK: - GlobalConfig
enum AppConfig: String {
    case globalConfig
}

private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.AppConfig").userDefaults

extension AppConfig: UserDefaultPreference {
    var userDefaults: UserDefaults { return appConfigUserDefaults }
}

// 自定义类型
public class GlobalConfig: Codable {
    /// 配置版本号
    let configVersion: Int
    /// 用户初始试用次数
    let userInitialTrialCount: Int
    /// 生成时间 如:2022-09-19T02:58:31Z
    let createDate: String

    enum CodingKeys: String, CodingKey {
        case configVersion = "version"
        case userInitialTrialCount = "user_initial_trial_count"
        case createDate = "create_date"
    }
	  ...
}

lazy var globalConfig: GlobalConfig = {
  guard let config = AppConfig.globalConfig.codableObject(GlobalConfig.self) else {
    return GlobalConfig()
  }
  return config
}() {
  didSet { AppConfig.globalConfig.save(codableObject: globalConfig) }
}
复制代码

从上述案例可以看出,在配置项的注册和维护成本相对方案1.0有了大幅度的降低,对UserDefaults的使用进行了规范性的约束,提供了更方便的CURD API,使用方式也更加符合面向对象的习惯。

同时为了满足复杂结构体的存储需求,我们可以扩展实现Codable对象的存取逻辑。

总结

本方案的目的是解决乱象丛生的UserDefaults的使用情况,分析后向两个方向进行了优化:

  1. 提供中心化的配置方式,关联UserDefaults、维护String Key。
  2. 提供类ORM的管理方式,减少业务的接入成本。

针对更复杂的、类缓存集合的,或者有查询需求的配置项管理,请尽快用NoSQL替换,避免数据量上升带来的效率下降。

Supongo que te gusta

Origin juejin.im/post/7200945340122906681
Recomendado
Clasificación