Swift 4: getter и setter непосредственно в структуре или классе

Я пытаюсь найти способ доступа к UserDefaults с использованием свойств. Тем не менее, я застрял, потому что не могу понять, как сделать свойства динамическими, так сказать.

Это общая идея, которую я хочу получить API:

class AppUserDefaults {
  var username = DefaultsKey<String>("username", defaultValue: "Unknown")
  var age = DefaultsKey<Int?>("age", defaultValue: nil)
}

let props = AppUserDefaults()
props.username = "bla"
print("username: \(props.username)")

Но это (конечно) не работает, так как тип DefaultsKey<String>, а не String. Поэтому я должен добавить свойство value в реализацию DefaultsKey чтобы добавить getter и setter, например:

struct DefaultsKey<ValueType> {
  private let key: String
  private let defaultValue: ValueType

  public init(_ key: String, defaultValue: ValueType) {
    self.key = key
    self.defaultValue = defaultValue
  }

  var value: ValueType {
    get {
      let value = UserDefaults.standard.object(forKey: key)
      return value as? ValueType ?? defaultValue
    }
    set {
      UserDefaults.standard.setValue(newValue, forKey: key)
      UserDefaults.standard.synchronize()
    }
  }
}

и затем используйте его следующим образом:

let props = AppUserDefaults()
props.username.value = "bla"
print("username: \(props.username.value)")

Но я нахожу это довольно уродливым. Я также попробовал подстрочный метод, но тогда вам все равно нужно добавить [] вместо .value:

struct DefaultsKey<ValueType> {
  ...
  subscript() -> ValueType {
    get {
      let value = UserDefaults.standard.object(forKey: key)
      return value as? ValueType ?? defaultValue
    }
    set {
      UserDefaults.standard.setValue(newValue, forKey: key)
      UserDefaults.standard.synchronize()
    }
  }
}

let props = AppUserDefaults()
props.username[] = "bla"
print("username: \(props.username[])")

В принципе, я хочу, чтобы я мог определить getter и setter непосредственно на DefaultsKey вместо того, чтобы пройти через это свойство value. Это просто невозможно с Swift? Есть ли другой способ получить поведение, которое я хочу, где свойства, определенные в AppUserDefaults являются "динамическими" и проходят через getter и setter, без необходимости определять его в объявлении свойства внутри AppUserDefaults?

Надеюсь, я использую здесь правильные условия и поставил вопрос каждому.

Ответ 1

Единственное, о чем я могу думать, это:

struct DefaultsKey<ValueType> {
  private let key: String
  private let defaultValue: ValueType

  public init(_ key: String, defaultValue: ValueType) {
    self.key = key
    self.defaultValue = defaultValue
  }

  var value: ValueType {
    get {
      let value = UserDefaults.standard.object(forKey: key)
      return value as? ValueType ?? defaultValue
    }
    set {
      UserDefaults.standard.setValue(newValue, forKey: key)
      UserDefaults.standard.synchronize()
    }
  }
}

class AppUserDefaults {
  private var _username = DefaultsKey<String>("username", defaultValue: "Unknown")
  var username: String {
    set {
      _username.value = newValue
    },
    get {
      return _username.value
    }
  }
}

let props = AppUserDefaults()
props.username = "bla"
print("username: \(props.username)")

Ответ 2

Помимо всех предлагаемых вариантов вы также можете определить свой пользовательский оператор для назначения значения структуре DefaultsKey.

Для этого ваша структура DefaultsKey должна выглядеть так:

struct DefaultsKey<ValueType> {
    private let key: String
    private let defaultValue: ValueType

    public init(_ key: String, defaultValue: ValueType) {
        self.key = key
        self.defaultValue = defaultValue
    }
    public private(set) var value: ValueType {
        get {
            let value = UserDefaults.standard.object(forKey: key)
            return value as? ValueType ?? defaultValue
        }
        set {
            UserDefaults.standard.setValue(newValue, forKey: key)
        }
    }
}

Объяснение для блока кода DefaultsKey:

  1. private(set) var означает, что вы можете установить значение этого свойства только там, где вы можете получить доступ к нему с помощью private уровня доступа (также вы можете написать internal(set) или fileprivate(set) чтобы иметь возможность устанавливать его из internal и fileprivate уровней доступа соответственно).

    Вам нужно будет установить value свойства позже. Чтобы получить доступ к этому значению, getter определяется как public (путем записи public до private(set)).

  2. Вам не нужно использовать метод synchronize() ("этот метод не нужен и не должен использоваться", ссылка: https://developer.apple.com/documentation/foundation/userdefaults/1414005-synchronize).

Теперь пришло время определить пользовательский оператор (его можно назвать так, как вы хотите, это только пример):

infix operator <<<
extension DefaultsKey {
    static func <<<(left: inout DefaultsKey<ValueType>, right: ValueType) {
        left.value = right
    }
}

С помощью этого оператора вы не можете установить значение неправильного типа, чтобы он был безопасным.

Чтобы проверить, вы можете немного изменить свой код:

class AppUserDefaults {
    var username = DefaultsKey<String>("username", defaultValue: "Unknown")
    var age = DefaultsKey<Int?>("age", defaultValue: nil)
}

let props = AppUserDefaults()
props.username <<< "bla"
props.age <<< 21
props.username <<< "Yo man"
print("username: \(props.username.value)")
print("username: \(props.age.value)")

Надеюсь, поможет.

Ответ 3

Существует очень хорошая запись в блоге из radex.io о статически типизированных NSUserDefaults, которые могут оказаться полезными, если я понимаю, что вы пытаетесь сделать.

Я обновил его для последней версии Swift (так как теперь мы имеем условное соответствие) и добавил значение по умолчанию для моей реализации, которое я изложил ниже.

class DefaultsKeys {}

class DefaultsKey<T>: DefaultsKeys {
    let key: String
    let defaultResult: T

    init (_ key: String, default defaultResult: T) {
        self.key = key
        self.defaultResult = defaultResult
    }
}

extension UserDefaults {
    subscript<T>(key: DefaultsKey<T>) -> T {
        get {
            return object(forKey: key.key) as? T ?? key.defaultResult
        }
        set {
            set(newValue, forKey: key.key)
        }
    }
}

// usage - setting up the keys
extension DefaultsKeys {
    static let baseCurrencyKey = DefaultsKey<String>("Base Currency", default: Locale.current.currencyCode!)
    static let archiveFrequencyKey = DefaultsKey<Int>("Archive Frequency", default: 30)
    static let usePasswordKey = DefaultsKey<Bool>("Use Password", default: false)
}

// usage - getting and setting a value
let uDefaults = UserDefaults.standard
uDefaults[.baseCurrencyKey] = "GBP"
let currency = uDefaults[.baseCurrencyKey]

Ответ 4

Вот как мы это делаем в моем текущем проекте. Это похоже на некоторые другие ответы, но я думаю, что еще меньше плита котла. В качестве примера можно использовать isFirstLaunch.

enum UserDafaultsKeys: String {
    case isFirstLaunch
}

extension UserDefaults {

    var isFirstLaunch: Bool {
        set {
            set(!newValue, forKey: UserDafaultsKeys.isFirstLaunch.rawValue)
            synchronize()
        }
        get { return !bool(forKey: UserDafaultsKeys.isFirstLaunch.rawValue) }
    }

}

Затем он используется так.

UserDefaults.standard.isFirstLaunch = true

Он имеет то преимущество, что вам не нужно изучать отдельный объект для использования и очень ясно, где хранится переменная.