睿欣飞翔 發表於 2023-2-3 15:06:35

iOS数据持久化UserDefaults封装器使用详解

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>使用属性封装器来完美创建UserDefaults封装器</li><li>什么是属性封装器?</li><li>什么是UserDefault封装器</li><li>将属性封装器进行通用化处理</li><li>存储自定义对象</li></ul></div><p class="maodian"></p><h2>使用属性封装器来完美创建UserDefaults封装器</h2>
<div class="cros igoods"><div class="goodsin" data-img="https://img14.360buyimg.com/pop/jfs/t3565/8/693050743/449834/b47eff85/5810e6ccN5577dc8d.jpg" data-name="Swift编程之旅" data-owner="京东自营" data-price="33.9" data-tgid="38" data-url="https://union-click.jd.com/jdc?e=&amp;p=JF8BAMgJK1olXwUFU1xcAUoRA18IG1MdVAUAUm4ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYCXFZUC0kRHDZNRwYlPG1wDQ0NCC1yewkWWyl0FmMcMQIgXkcbM2gNHF4dXwMBZF5eDkwXAmoIK2sVXDZQOobrvpOysnPcsdTA1ZEyVW5dD00TBm4BGFoXWA4CZF5VDHtUVypcWBhdbTYyV25tOEsnAF9WdVpGWwUGV1xVZhZCXWhKSAxMMwEBUF1bDEMnAW4JGVklbQ"></div></div>
<p>想象一下,你有一个应用想实现自动登录功能。你用UserDefaults封装了关于UserDefaults的读与写逻辑。你会用UserDefaults封装来保持对自动登录&rdquo;On/Off&ldquo;状态、userName的跟踪。你可能会以下面这种方式来封装UserDefaults</p>
<div class="jb51code"><pre class="brush:java;">struct AppData {
    private static let enableAutoLoginKey = "enable_auto_login_key"
    private static let usernameKey = "username_key"
    static var enableAutoLogin: Bool {
      get {
            return UserDefaults.standard.bool(forKey: enableAutoLoginKey)
      }
      set {
            UserDefaults.standard.set(newValue, forKey: enableAutoLoginKey)
      }
    }
    static var username: String {
      get {
            return UserDefaults.standard.string
      }
      set {
            UserDefaults.standard.set(newValueds, forKey: usernameKey)
      }
    }
}
</pre></div>
<p>通过Swift5.1对于属性封装器的介绍,我们可以对上面的代码进行精简,如下</p>
<div class="jb51code"><pre class="brush:java;">struct AppData {
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
}
</pre></div>
<p>这样就很完美了吗?接着看</p>
<p class="maodian"></p><h2>什么是属性封装器?</h2>
<p>在我们进入详细讨论之前,我们先快速地了解一下什么是属性封装器 基本上来讲,属性封装器是一种通用数据结构,可以拦截属性的读写访问,从而允许在属性的读写期间添加自定义行为。</p>
<p>可以通过关键字<code>@propertyWrapper</code>来声明一个属性封装器。你想要有一个字符串类型的属性,每当这个属性被进行读写操作的时候,控制台就会输出。你可以创建一个名为<code>Printable</code>的属性封装器,如下:</p>
<div class="jb51code"><pre class="brush:java;">@propertyWrapper
struct Printable {
    private var value: String = ""
    var wrapperValue: String {
      get {
            print("get value:\(value)")
            return value
      }
      set {
            print("set value:\(newValue)")
            value = newValue
      }
    }
}
</pre></div>
<p>通过上述代码我们可以看出,属性封装跟其他<code>struct</code>一样。然而,当定义一个属性封装器的时候,必须要有一个<code>wrapppedValue</code>。 <code>wrapppedValue</code> <code>get</code> <code>set</code>代码块就是拦截和执行你想要的操作的地方。在这个例子中,添加了打印状态的代码来输出get和set的值</p>
<p>接下来,我们看看,如何使用Printable属性封装器</p>
<div class="jb51code"><pre class="brush:java;">struct Company {
    @Printable static var name: String
}
Company.name = "Adidas"
Company.name
</pre></div>
<p>需要注意的是,我们如何使用<code>@</code>符号来声明一个用属性封装器封装的&rdquo;name&ldquo;变量。如果你想要在Playground中尝试敲出上述代码的话,你会看到以下输出:</p>
<blockquote><p>Set Value: Adidas<br />Get Value: Adidas</p></blockquote>
<p class="maodian"></p><h2>什么是UserDefault封装器</h2>
<p>在理解了什么是属性封装器以及它是如何工作的之后,我们现在开始准备实现我们的<code>UserDefaults</code>封装器。总结一下,我们的属性封装器需要持续跟踪自动登录的&rdquo;On/Off&ldquo;状态以及用户的username。 通过使用我们上述讨论的概念,我们可以很轻松的将<code>Printable</code>属性封装器转化为在读写操作期间进行读写的属性封装器。</p>
<div class="jb51code"><pre class="brush:java;">import Foundation
@propertyWrapper
struct Storage {
    private let key: String
    private let defaultValue: String
    init(key: Stirng, defaultValue: String) {
      self.key = key
      self.defaultValue = defaultValue
    }
    var wrappedValue: String {
      get {
            return UserDefaults.standard.string(forKey: key) ?? defaultValue
      }
      set {
            UserDefaults.standard.set(newValue, forKey: key)
      }
    }
}
</pre></div>
<p>在这里,我们将我们的属性封装器命名为<code>Storage</code>。有两个属性,一个是<code>key</code>,一个是<code>defaultValue</code>。<code>key</code>将作为<code>UserDefaults</code>读写时的键,而<code>defaultValue</code>则作为<code>UserDefaults</code>无值时候的返回值。</p>
<p><code>Storage</code>属性封装器准备就绪后,我们就可以开始实现<code>UserDefaults</code>封装器了。直截了当,我们只需要创建一个被<code>Storage</code>属性封装器封装的&lsquo;username&rsquo;变量。这里要注意的是,你可以通过<code>key</code>和<code>defaultValue</code>来初始化<code>Storage</code>。</p>
<div class="jb51code"><pre class="brush:java;">struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
}
</pre></div>
<p>一切就绪之后,<code>UserDefaults</code>封装器就可以使用了</p>
<div class="jb51code"><pre class="brush:java;">AppData.username = "swift-senpai"
print(AppData.username)
</pre></div>
<p>同时,我们来添加<code>enableAutoLogin</code>变量到我们的<code>UserDefaults</code>封装器中</p>
<div class="jb51code"><pre class="brush:java;">struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var username: Bool
}
</pre></div>
<p>这个时候,会报下面两种错误:</p>
<blockquote><p>Cannot convert value of type &lsquo;Bool&rsquo; to expected argument type &lsquo;String&rsquo;</p>
<p>Property type &#39;Bool&#39; does not match that of lthe &#39;WrappedValue&#39; property of its wrapper type &#39;Storage&#39;</p></blockquote>
<p>这是因为我们的封装器目前只支持<code>String</code>类型。想要解决这两个错误,我们需要将我们的属性封装器进行通用化处理</p>
<p class="maodian"></p><h2>将属性封装器进行通用化处理</h2>
<p>我们必须改变属性封装器的<code>wrappedValue</code>的数据类型来进行封装器的通用化处理,将<code>String</code>类型改成泛型<code>T</code>。进而,我们必须使用通用方式从<code>UserDefaults</code>读取来更新<code>wrappedValue</code> <code>get</code>代码块</p>
<div class="jb51code"><pre class="brush:java;">@propertyWrapper
struct Storage&lt;T&gt; {
    private let key: String
    private let defaultValue: T
    init(key: String, defaultValue: T) {
      self.key = key
      self.defaultValue = defaultValue
    }
    var wrappedValue: T {
      get {
            // Read value from UserDefaults
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
      }
      set {
            // Set value to UserDefaults
            UserDefaults.standard.set(newValue, forKey: key)
      }
    }
}
</pre></div>
<p>好,有了通用属性封装器之后,我们的<code>UserDefaults</code>封装器就可以存储Bool类型的数据了</p>
<div class="jb51code"><pre class="brush:java;">// The UserDefaults wrapper
struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
}
AppData.enableAutoLogin = true
print(AppData.enableAutoLogin)// true
</pre></div>
<p class="maodian"></p><h2>存储自定义对象</h2>
<p>上面的操作都是用来基本数据类型的。但是如果我们想要存储自定义对象呢?接下来我们一起看看,如何能让<code>UserDefaults</code>支持自定义对象的存储</p>
<p>这里的内容很简单,我们将会存储一个自定义对象到<code>UserDefaults</code>中,为了达到这个目的,我们必须改造一下<code>Storage</code>属性封装器的类型<code>T</code>,使其遵循<code>Codable</code>协议</p>
<p>然后,在<code>wrappedValue``set</code>代码块中我们将使用<code>JSONEncoder</code>把自定义对象转化为Data,并将其写入<code>UserDefaults</code>中。同时,在<code>wrappedValue``get</code>代码块中,我们将使用<code>JSONDecoder</code>把从<code>UserDefaults</code>中读取的数据转化成对应的数据类型。 如下:</p>
<div class="jb51code"><pre class="brush:java;">@propertyWrapper
struct Storage&lt;T: Codable&gt; {
    private let key: String
    private let defaultValue: T
    init(key: String, defaultValue: T) {
      self.key = key
      self.defaultValue = defaultValue
    }
    var wrappedValue: T {
      get {
            // Read value from UserDefaults
            guard let data = UserDefaults.standard.object(forKey: key) as? Data else {
                // Return defaultValue when no data in UserDefaults
                return defaultValue
            }
            // Convert data to the desire data type
            let value = try? JSONDecoder().decode(T.self, from: data)
            return value ?? defaultValue
      }
      set {
            // Convert newValue to data
            let data = try? JSONEncoder().encode(newValue)
            // Set value to UserDefaults
            UserDefaults.standard.set(data, forKey: key)
      }
    }
}
</pre></div>
<p>为了让大家看到如何使用更新后的<code>Storage</code>属性封装器,我们来看一下接下来的例子。 想象一下,你需要存储用户登录成功后服务端返回的用户信息。首先,需要一个持有服务端返回的用户信息的struct。这个struct必须遵循<code>Codable</code>协议,以至于他能被转化为Data存储到<code>UserDefaults</code>中</p>
<div class="jb51code"><pre class="brush:java;">struct User: Codable {
    var firstName: String
    var lastName: String
    var lastLogin: Date?
}
</pre></div>
<p>接下来,在<code>UserDefaults</code>封装器中声明一个<code>User</code>对象</p>
<div class="jb51code"><pre class="brush:java;">struct AppData {
    @Storage(key: "username_key", defaultValue: "")
    static var username: String
    @Storage(key: "enable_auto_login_key", defaultValue: false)
    static var enableAutoLogin: Bool
    // Declare a User object
    @Storage(key: "user_key", defaultValue: User(firstName: "", lastName: "", lastLogin: nil))
    static var user: User
}
</pre></div>
<p>搞定了,<code>UserDefaults</code>封装器现在可以存储自定义对象了</p>
<div class="jb51code"><pre class="brush:java;">let johnWick = User(firstName: "John", lastName: "Wick", lastLogin: Date())
// Set custom object to UserDefaults wrapper
AppData.user = johnWick
print(AppData.user.firstName) // John
print(AppData.user.lastName) // Wick
print(AppData.user.lastLogin!) // 2019-10-06 09:40:26 +0000</pre></div>
<p>以上就是iOS数据持久化UserDefaults封装器使用详解的详细内容,更多关于iOS数据持久化UserDefaults的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>IOS开发自定义Button的外观和交互行为示例详解</li><li>IOS开发Objective-C Runtime使用示例详解</li><li>iOS数据持久化KeyChain数据操作详解</li><li>iOS13适配三指撤销和文案限长实例详解</li><li>iOS&nbsp;16&nbsp;CocoaAsyncSocket&nbsp;崩溃修复详解</li><li>iOS开发蓝牙技术应用增加无线连接功能</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: iOS数据持久化UserDefaults封装器使用详解