原文:USING SETTINGS BUNDLES WITH SWIFT
作者:STEVEN LIPTON
译者:kmyhy
你是否知道怎样将用户定义的设置放系统设置程序中?Xcode 通过创建一个特殊的 plist 文件 settings.bunlde 将多个值以 NSUserDafulats 的方式添加到设置程序中。在本教程中你将学习如何在 app 中创建 settings.bundle ,从而在设置程序中访问 xml。在后续的教程中,我们会通过 plist 编辑器深入讨论 settings.bundle。
本教程假设你已经知道如何使用 plist 和 NSUserDefaults。如果你不知道,你可以在这里找到我之前发的关于 Property Lists 和 NSUserDefaults 的帖子。
创建 Settings.bundle
新建 Single view application 项目,名为 SettingsBundleDemo ,勾选 Swift 和 Universal device。
在 SettingsBundleDemo 文件夹上右键,选择 New file。然后选择 Resources 类下的 Settings Bundle 再点击 Next。
文件会以 settings 命名。在保存窗口中,确保 settings.bundle 位于 SettingsBundleDemo 文件夹(注:这点很重要)。点击 Create。
系统将打开 settings.bundle。它看起来像一个编译过的包。在导航窗口中,点击箭头展开 settings.bundle。
点击 Root.plist 文件。你会看到一个 plist 文件:
如果 Preference Items 项是收起的,请展开它。这个里面有一些实例数据。和别的 Plist 不同,这里的数据不会直接将值存储为字典,而是一个字典的数组,每个字典都表示一个控件。你可以在这个 plist 文件中描述你想要的控件,然后在设置程序中会显示这个控件。当我们将 settings.bundle 和 NSUserDefaults.standardUserDefaults 关联起来时,系统会在 NSUserDefaults 中创建 key 和 value。
下面列出你将用到的控件:
每个控件都有许多属性。如果 Group Control 当前未展开,请展开它,在 Root.plist 中,它是 Item 0。
Group 控件通常用于将其他控件组合在一起。它只有 title 和 Type 属性。展开 Item 3(Slider)。
slider 有更多的属性,都列在了这里。很多控件都会有一个 Default Value 属性,这个值会在设置 app 中显示,它并不会在 defaults 中保存。这个值会在第一次使用时显示这个值,但只是用于显示,而不是一个真正的值。
译者注:也就是说,虽然你给控件指定了 Default Value,但第一次运行 APP 时,这个控件所存储的值仍然是 nil。Default Value 仅用于显示,不会被存储。
每种控件都有不同的属性,如上表所示。这里列出了 Root.plist 中的所有属性:
加入自己的设置
删除所有控件。在 Root.plist 中删除 plist 比较麻烦。最简单的办法是将 item 剪切掉。选中 Item 0 (Group),在 group 上右键,选中快捷菜单中的 Cut。对 Item 3(Slider) 和其他控件进行同样的操作。现在你的 Plist 将是这个样子:
选中 Preferencce Items,确保它被展开。此时箭头应当是向下。按它旁边的 Add(+) 按钮。会创建一个新的项:
这时会显示一个弹出式菜单。如果你不小心点击了别的地方,这个菜单又会消失,这时你可以点击它右边的下箭头(),菜单又会出现,列出一个控件列表:
选择其中的 Toggle Switch。我们用它来存储 Bool 值。展开 Toogle Switch 会呈现如下属性:
为每个属性赋值,将 Title 设置为 Room for cream,Identifier 设置为 coffee_cream。这个 Identifier 属性就是值存在 NSUserDefaults 后的 key 。将 Default Value 设置为 YES。最终是这个样子:
关闭 switch 控件的属性,悬着 Item 0。点击右键,然后选择 Add Row。在类型菜单中选择 Text Field,其实这也是默认的类型。展开 Item 1(Text Field),查看它的属性:
将 Title 设置为 Beverage of choice,Identifer 设为 coffee_type。右键点击 Identifier 然后选择 Add Row。这会创建一个新行,并让你选择合适的属性:
选择 Default Value。并设为 Coffee。
关闭 Text 的属性。选中 Preference items,右键点击并选择 Add Row。新的行会插入到其他行之上,变成 Item 0。类型选择 Multi-Value。Multi-Value 有两个数组属性,用于显示一个选项列表。展开 multi-value ,将 title 设置为 Size,Default Value 设置为 0,Identifier 设置为 coffe_size。
这个控件不会自动提供值数组。我们必须手动提供。选中最下面的属性,右键点击,选择 Add Row。行类型选择 Values。展开 Values 数组,现在它是空的。通过 Add row 或者 + 按钮,在 Values 中添加 4 个子项。第一行的值设置为 0,类型修改为 Number。类似地将其他行设置为 1,2,3。
关闭 Values。为 multi-value 控件添加另一个属性 Titles。在 Titles 数组下添加 4 个子项,将值分别设置为字符串的 Small、Medium、Large 和 Extra Large。
现在来看看我们的设置界面。为了醒目起见,我们将 Launchscreen.storyboard 背景色设置为成色(#ff8000)。编译运行,当背景色显示出红色时,切换到设置程序。如果使用真机测试,请点击 Home 键,模拟器请按 command+shift+H 键。打开设置 app。
向下滚动,你会看到你的 app 图标。点击你的 app 图标,进入 app 的设置页面。
编辑故事板
停止 app,打开 Main.storyboard,在故事板中拖入一个 switch,一个 label,一个 text field 和一个 segmented control。将这些控件设置成这个样子:
选择 switch,在 size 面板中,设置 Compression Resistance (别挤我)和 Content Hugging (别拉我)的水平和垂直都设置为 1000。这样 switch 的尺寸永远不会改变,设置这两项主要是起这个作用(系数越高,则越晚被压缩和拉伸)。
点击 stack view 按钮,创建一个水平布局的 stack view。然后选择中故事板所有 UIView。再次点击 stack view 按钮创建一个垂直布局的 stack view,设置它的属性为:
Pin 这个 stack view 为:上 71,左 5,右 5。 Update frames 选择为 items of new constraints。
打开助手编辑器,为这些控件创建合适的 UIoUtlet 连接:
@IBOutlet weak var roomForCream: UISwitch!
@IBOutlet weak var drinkText: UITextField!
@IBOutlet weak var sizeSegment: UISegmentedControl!
读取 settings.bundle
NSUserDefaults 并不知道我们创建了一个 settings.bundle。我们第一件事情就是向 NSUserDefaults 注册我们的 settings.bundle。
func registerSettingsBundle(){
let appDefaults = [String:AnyObject]()
NSUserDefaults.standardUserDefaults().registerDefaults(appDefaults)
}
这个方法向 NSUserDefaults.standardUserDefaults 注册 settings.bundle。regusterDefaults 方法在资源目录中搜索 plist 文件并将字典中存放的键值类型修改为 [String:AnyObject]。这个方法只需要在我们的代码中执行一次。
新增一个方法 updateDisplayFromDefaults 用于读取我们的 defaults:
func updateDisplayFromDefaults(){
//获得 defaults 引用
let defaults = NSUserDefaults.standardUserDefaults()
}
然后,读取键值对并赋给我们的 IBOutlet 组件。继续在声明的方法中添加如下代码:
//将控件的默认值设置为 default 中存储的值
roomForCream.on = defaults.boolForKey("coffee_cream")
if let drink = defaults.stringForKey("coffee_type"){
drinkText.text = drink
} else{
drinkText.text = ""
}
sizeSegment.selectedSegmentIndex = defaults.integerForKey("coffee_size")
stringForKey 方法返回的是一个可空类型。我们的代码需要判断值是否为空并针对性地处理。在 view DidLoad 方法中调用新方法:
override func viewDidLoad() {
super.viewDidLoad()
registerSettingsBundle()
updateDisplayFromDefaults()
}
我们想在 app 启动时就开始刷新。回到模拟器或真机上。删除 App。这时 app 和 app 的设置数据都会被删除。我们的 app 偏好设置又被清空了,因为它们并没有被写到 app 中。而且 plist 中的 Default Value 只是一个显示值,它没有真实反应出用户选择。
按下 command+shift+H,切到设置程序。设置你的偏好设置:
按下 command+shift+HH,回到示例 app。我们的 app 仍然没有任何变化。
关闭 app ,回到 Xcode。重新运行 app。现在读到我们的 defaults 了。
用观察者模式同步 defaults
通过重启来刷新偏好设置不是很友好。问题是,我们需要告诉 app 有东西被改变了。并且需要自动进行刷新。在 app 中,无论是 settings.bundle 还是 NSUserDefaults,都面临着这个问题。你修改了一个设置,然后想到处刷新这个新值。这种改变需要通知观察者。
NSUserDefaults 类会产生一种通知,叫做 NSUserDefaultsDidChangeNotification。我们可以让观察者监听这种通知,当改变发生时调用某些代码。修改 viewDidLoad 方法:
override func viewDidLoad() {
super.viewDidLoad()
registerSettingsBundle()
updateDisplayFromDefaults()
NSNotificationCenter.defaultCenter().addObserver(self,selector: "defaultsChanged",name: NSUserDefaultsDidChangeNotification,object: nil)
}
}
我们让通知中心记住我们已经注册了一个观察者。我们为 NSUserDefaultsDidChangeNotification 注册了一个观察者,告诉通知中心,当收到这个通知时,执行观察者的 defaultsChanged 方法。
另外,你可能想将 registerSettingsBundle 的代码合并到 updateDisplayFromDefaults 中。这样,每执行一次 registerSettingsBundle,都会改变 NSUserDefaults,增加一份 defaults。而每增加一个值我们会收到一个通知,又会触发一次递归调用,导致内存耗尽。因此,请保持 registerSettingsBundle 作为一个独立的方法。
当收到通知时,我们需要运行 defaultsChanged 方法。这个方法实现如下:
func defaultsChanged(){
updateDisplayFromDefaults()
}
还需要写一些常规的代码。在 iOS9 之前,为了保持良好的内存管理,我们需要注销观察者。从 iOS9 开始,ARC 会自动为你注销。对于 iOS 8 设备,则需要用以下代码来注销观察者:
deinit { //在 iOS9 以后不再需要,ARC 自动会移除观察者。
NSNotificationCenter.defaultCenter().removeObserver(self)
}
这些代码放在 Swift 类的 deinit 方法里,将观察者从通知中心移除。
模拟器中的一个 Bug
编译运行。按下 Command+shift+H,切换到设置程序。选择设置->SettingsBundleDemo … 出现一片空白。
在真机上不会这样。只有在模拟器中,这才会发生。似乎模拟器仍然在 app 的上一次运行中。当你点击 Xcode 中的 Stop 按钮,它不会终止设置程序。点击 command+shift+HH 或者双击 Home 按钮。向上滑动设置程序,将它终止进程。重新启动设置程序,设置程序才会刷新。你又可以看到 app 的设置页面了。将设置修改为:
回到 SettingsBundleDemo,这次是这个样子:
关于 settings.bundle 的基本介绍就到这里。另外还有一些高级技巧,比如子设置页,以及直接以代码操作设置中的 XML。下面列出了 Root.plist 的代码供你参考
完整代码
//
// ViewController.swift
// SetttingsBundleDemo
//
// Created by Steven Lipton on 3/11/16.
// Copyright © 2016 MakeAppPie.Com. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var roomForCream: UISwitch!
@IBOutlet weak var drinkText: UITextField!
@IBOutlet weak var sizeSegment: UISegmentedControl!
deinit { //Not needed for iOS9 and above. ARC deals with the observer.
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func registerSettingsBundle(){
let appDefaults = [String:AnyObject]()
NSUserDefaults.standardUserDefaults().registerDefaults(appDefaults)
//NSUserDefaults.standardUserDefaults().synchronize()
}
func updateDisplayFromDefaults(){
//Get the defaults
let defaults = NSUserDefaults.standardUserDefaults()
//Set the controls to the default values.
roomForCream.on = defaults.boolForKey("coffee_cream")
if let drink = defaults.stringForKey("coffee_type"){
drinkText.text = drink
} else{
drinkText.text = ""
}
sizeSegment.selectedSegmentIndex = defaults.integerForKey("coffee_size")
}
func defaultsChanged(){
updateDisplayFromDefaults()
}
@IBAction func updateDefaults(sender: AnyObject) {
updateDisplayFromDefaults()
}
override func viewDidLoad() {
super.viewDidLoad()
registerSettingsBundle()
updateDisplayFromDefaults()
NSNotificationCenter.defaultCenter().addObserver(self,object: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Root.plist
你可以以 XML 方式来编辑你的 plist 文件,因为种种原因,我们不展开讨论。这里仅列出本示例中使用的 plist 的 XML 源代码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StringsTable</key>
<string>Root</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Size</string>
<key>Key</key>
<string>coffee_size</string>
<key>DefaultValue</key>
<string>0</string>
<key>Values</key>
<array>
<integer>0</integer>
<integer>1</integer>
<integer>2</integer>
<integer>3</integer>
</array>
<key>Titles</key>
<array>
<string>Small</string>
<string>Medium</string>
<string>Large</string>
<string>Extra Large</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Room for cream</string>
<key>Key</key>
<string>coffee_cream</string>
<key>DefaultValue</key>
<true/>
</dict>
<dict>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
<key>Title</key>
<string>Beverage of Choice</string>
<key>Key</key>
<string>coffee_type</string>
<key>DefaultValue</key>
<string>Coffee</string>
</dict>
</array>
</dict>
</plist>