在 Swift 中使用 SETTINGS BUNDLES

原文:USING SETTINGS BUNDLES WITH SWIFT
作者:STEVEN LIPTON
译者:kmyhy

你是否知道怎样将用户定义的设置放系统设置程序中?Xcode 通过创建一个特殊的 plist 文件 settings.bunlde 将多个值以 NSUserDafulats 的方式添加到设置程序中。在本教程中你将学习如何在 app 中创建 settings.bundle ,从而在设置程序中访问 xml。在后续的教程中,我们会通过 plist 编辑器深入讨论 settings.bundle。

本教程假设你已经知道如何使用 plist 和 NSUserDefaults。如果你不知道,你可以在这里找到我之前发的关于 Property ListsNSUserDefaults 的帖子。

创建 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,
            selector: "defaultsChanged",
            name: NSUserDefaultsDidChangeNotification,
            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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值