[翻]OS X和Swift入门指南:(3/3)

本片翻译自:http://www.raywenderlich.com/87004/getting-started-with-os-x-and-swift-tutorial-part-3

欢迎回到第三部分和最后一部分关于如何创建简单的MAC应用程序的教程。
第一部分教程,你创建了一个Mac应用程序显示了一个Scary Bugs列表。
第二部分教程中,你学习到如何显示bugs的详细信息,同样包含了如何添加、删除、修改bugs。
在第三部分和最后一部分教程,你将会包装你的OS X和Swift introduction,通过雕琢你的app,并且提供更好的用户体验。
在完成这个教程后,你将会创建完整的OS X应用程序-同时希望你会创造自己的一些MAC应用程序!
如果你没有跟随先前的部分,这里是final project from part 2,你可以在Xcode中打开它,并且从这里开始。

这个app有什么问题?

到目前为止一切工作顺利。你可以看到一堆scary bugs,添加或者删除bugs,并且你甚至可以修改bug的任何信息。
它的功能齐全。但是,你没有提供一个很好的用户体验。
例如,如果你调整窗口大小,并且使它变得很大,看看可怜的控件发生了什么!
osx-tutorial-app3-1-them-problem
他们没有调整大小,并且他们完全不对齐,使app看起来很丑陋很不专业。
如果你让你的窗口变小,它甚至更糟:
osx-tutorial-app3-2-them-problem2
确认!在这种情况下,你甚至不能看到你需要的全部信息。很明显,你需要设置窗口的最小大小,使应用程序可以使用。
另一个问题是,bug数据不会在应用程序会话之间持久存储。每次应用程序重启后任何用户添加或修改的数据都会丢失。你将稍后在教程的这个部分添加bug数据的持久化存储。
所以,在首先添加一些UI元素后,让我们通过设置窗体的最小大小来修正调整大小的问题。
打开MasterViewController.xib。调整view的大小,排列控件的位置到你觉得很好看,这样,你可以看到你需要的最小尺寸的信息。也许是这样的:
osx-tutorial-mastersize
在示例截图中,控件也是排列好的,所以所有的按钮都是垂直对齐的,并且在detail view中的所有控件水平对齐,并且都有同样的宽度(除了Change Picture按钮)。
现在让我们添加一个小细节使界面更好看,并且使列表和detail section清楚的愤慨。在Objects Library中选择Vertical Line,拖拽它到view中。放置在列表和detail控件之前,在空白区域的中间。
osx-tutorial-verticalline
这看起来好多了!

重置

同样,如果有一个按钮可以重置bug的数据到app提供的原始数据是很好的。拖拽一个push button到table view下面,修改它的标题为Reset All,如同在第二部分做的一样,在Assistant Editor中,control-drag从按钮到MasterViewController.swift,添加一个anction,叫做resetData
osx-toturial-resetbutton
在resetData()中添加如下代码:

setupSampleBugs()
updateDetailInfo(nil)
bugsTableView.reloadData()

这个方法调用setupSampleBugs()来重置app的原始数据。带有nil参数的updateDetailInfo函数将会清楚details区域,然后,你只需要重新加载table view。
编译运行应用程序,然后,添加、删除、或者修改bug数据。然后,点击Reset按钮来确保它工作正确。

调整大小

在这些修改后,跳到size inspector,记下MasterViewController.xib中主Custom View的大小。在我的示例中,它是540×400像素,但是你的可能有所不同。这是没有问题的,只是纪录你的view大小。
osx-tutorial-customviewsize
这将会是你的应用程序窗口的最小大小。现在,打开MainMenu.xib,然后选择app的窗口。在size inspector中,对于Minimum Size选中Constraint,并且把宽高设置为你刚才纪录的数据。
osx-tutorial-windowminsize
编译运行应用程序,尝试修改窗体大小:
osx-tutorial-windowminsize2
你可以看大你仍然可以修改窗体大小,但是窗口不会小于你定义的最小大小。通过这个改变,你的bug信息总是会正常显示,w00t!
现在是时候处理大小改变了,这需要一些考量。你的窗体有两个不同的部分:table view和detail section。它们在你窗口改变大小时应该有不同的行为。
首先,你需要确保MasterViewController view本身在app窗口缩放时可以正常调整大小。记得吧,窗体和view controller的view事两个不同的事物!你将通过Auto Layout Visual Format Language来实现。打开AppDelegate.swift,添加如下代码在applicationDidFinishLaunching()后面:

// 3. Set constraints on masterViewController.view
masterViewController.view.translatesAutoresizingMaskIntoConstraints = false
let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[subView]|",
  options: NSLayoutFormatOptions(0),
  metrics: nil,
  views: ["subView" : masterViewController.view])
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[subView]|",
  options: NSLayoutFormatOptions(0),
  metrics: nil,
  views: ["subView" : masterViewController.view])
 
NSLayoutConstraint.activateConstraints(verticalConstraints + horizontalConstraints)

通过编程添加这些约束来确保MasterViewController view在窗口缩放时会同样缩放。对于水平和垂直方向,master view controller都会挨着窗体的边框。
在这个view中,你想要在窗体缩放时table view垂直增长,但是你想要它的宽度保持不变。然而,你想要detail section在窗体增大时大小扩大。你将会通过interface builder的auto layout约束views来处理缩放。
打开MasterViewController.xib,并且选择table view。在Interface Builder的编辑窗口右下角的点击Pin constraint弹窗,并且添加top,left,bottom约束,并且添加宽度约束,在下拉菜单中,确保设置bottom约束相对于custom view,然后点击“Add 4 Constraints”:
osx-tutorial-constraints-tableview
注意你的约束值也许和这里图片显示的不同,因为间距也许不同。
下一步,选择Reset按钮,添加相对于table view的top约束和相对于main view的left约束:
osx-tutorial-constraints-reset
下一步,选择你添加的separator line。设置相对于main view的top和bottom约束,并且添加相对于table view的left约束,确保在左侧的下拉约束选择了“Bordered Scroll View – Table View” :
osx-tutorial-constraints-verticalline
现在你需要对“Add”和“Delete”按钮配置约束。你不想它们改变大小,但是你需要保持他们相对于table view底部的距离。独立的对每个按钮添加top,left,width和height约束。例如,对于“Add”按钮:
osx-tutorial-constraints-addbutton
对于“Delete”按钮重复同样的操作。
编译运行应用程序,并且尝试修改窗体大小。巨大的成功!
osx-tutorial-app3-3-masterresize
现在,你可以修改窗体的大小,并且table view垂直增长来适配它。按钮也修改他们的位置来保持在table view下面。
但是detail section显示还不是很啊后。在detail section中,你需要在窗口宽度增加时水平修改大小。detail view的Auto Layout约束和table view采用类似的方式设置。在Interface Builder中对detail views添加如下约束,采用如下顺序:

  • Name Label添加top和left约束
  • bugTitleView text filed添加top,left和right约束
  • Rating label添加top和left约束
  • bugRating自定义控件增加Top, left, right, 和height约束。
  • bugImageView控件增加Top, left, right, 和height约束。
  • 移动Change Picture按钮,因此他的右侧边框和bugImageView对齐,然后对按钮添加right和bottom约束。
  • 在设置这些约束后,你也许注意到了bugImageView的一些auto layout警告,但是警告会在设置完Change Picture按钮后解决。
    在完成列表的操作后,detail views的约束会如下所示:
    osx-tutorial-constraints-detailviews
    编译并运行应用程序,并且再次尝试修改大小。
    osx-tutorial-app3-4-resize
    如果你修改app大小,你会看到控件增长来填充剩余的空间,并且适当修改它们的位置。现在,它看起来好多了!
    你可以在bugImageView尝试不同的缩放设置。在Interface Builder选择Image Well,设置它的不同缩放属性选项,例如“Proportionally Up or Down”或者“Axes Independently”,并且看看当你运行app并且缩放窗体时会发生什么。

    注意:如果你觉得你的app窗体变大时显示的不好,你也可以采用你修改窗体最小大小的方式修改窗体的最大大小。只要到主窗体的Size Inspector面板中,设置maximum size选项。
    

    注重细节

    现在,app工作正常,并且他可以适配窗体大小改变。用户界面相对于以前那个看起来更好更专业。
    这里仍然有一些小的细节可以使用户体验更好。例如-编译运行应用程序,并且不选择任何事情,点击“Delete”或者“Change Picture”按钮。你可以点击他们,但是没有任何反应,对不对?
    osx-tutorial-onestar
    因为你是app的开发者,你知道,这些按钮在不选中table cell时是不做任何事情的。但是用户也许不知道这些,所以,所以下面的情况可能会发生:
    这个情况你需要通过启用和禁用按钮来实现,因此你的用户有更好的体验。这些小细节加起来,并修复它们会让你的应用程序看起来更光鲜。
    这里是当选择改变时应该发生什么:

  • 如果一行被选中,你需要启用“Delete”按钮,“Change picture”按钮,text filed和评分view。
  • 如果table没有任何选中,你只需要禁用他们,使得用户不能和这些控件交互。
  • 在Interface Builder中,你将要设置按钮和text filed默认为禁用的。打开MasterViewController.xib选择 “Delete” 按钮,打开Attributes Inspector。向下滚动知道你找到“Enabled”属性,并且取消选中它。
    osx-tutorial-deletebutton
    对Picture按钮和text filed重复这个操作。
    这种方式,这些控件在应用程序启动时默认是禁用的。你需要在用户选择table view的行时启用他们。
    为了启用这些控件,你需要在view controller中添加outlets。让我们首先对”Delete”按钮做这个操作。切换到Assistant Editor,确保它显示MasterViewController.swift
    选择“Delete”按钮,control-drag从按钮到MasterViewController.swift
    osx-tutorial-deletebutton2
    一个弹窗会显示出来,允许你关联NSButton作为你的类的属性。确保Connection属性是“Outlet”,命名为deleteButton,点击Connect。
    对”Change Picture”按钮做同样的操作,并且命名为changePictureButton
    osx-tutorial-deletebutton3
    打开MasterViewController.swift,并且在tableViewSelectionDidChange(_:)添加以下代码。在updateDetailInfo(selectedDoc)行下面:

    // Enable/disable buttons based on the selection
    let buttonsEnabled = (selectedDoc != nil)
    deleteButton.enabled = buttonsEnabled
    changePictureButton.enabled = buttonsEnabled
    bugRating.editable = buttonsEnabled
    bugTitleView.enabled = buttonsEnabled
    

    在这个代码中,你基于用户选择判断控件是否需要启用还是禁用。如果selectedDocnil,这意味着没有行被选中,所以控件应该是禁用的。
    同样地,如果用户选择了一个bug,控件应该重新启用。
    还有一个步骤。评分view默认是启用的,你也同样希望当应用程序启动时,它是禁用的。因为它是custom view,不能通过Interface Builder启用/禁用。
    你需要在应用程序启动时,通过编程方式禁用它。找到loadView() ,并且修改这行:

    self.bugRating.editable = true
    

    为:

    self.bugRating.editable = false
    

    通过这个修改,你设置了控件默认是不可编辑的。控件可以显示,但是评分不可以修改,直到用户选择了一个bug。
    编译并且运行应用程序。
    当应用程序启动后,你可以看到控件默认是禁用的。当你选择了一个bug,它们都启用了。并且当行被取消或删除时,它们被禁用了,因为没有bug被选择。
    注意:你也可以通过隐藏detail view来解决这个问题,直到用户选择了一个bug。根据你个人喜好和什么最适合你的应用程序。

    保存Bugs

    你的app功能很好,正确的处理了窗口大小改变,并且给用户一致的用户界面。然而,如果用户添加,移除或者编辑app的bug数据,然后退出程序,这些修改都将在会话之间丢失,并且用户必须从头再来。
    为了解决这个问题,bug数据的修改必须存储到磁盘上。有许多技术用来保存数据,并且我们会考虑一些对于这个简单的应用程序有效的方法。
    一种选择是对于每一个bug在磁盘上保存一个文件。当app启动时,读取所有的bug文件来创建bug数据。另一种方式是保存数据到cloud,使用CloudKit或者三方API。
    如果app拥有大量的bug数据,你应该考虑例如Core Data来存储bug信息。
    因为这个app相对简单,并且仅仅包含少量的bugs,你将会采取更基本的方法,类似于iOS,Mac应用程序可以使用NSUserDefaults,所以你用它来bug存储数据。
    在这样做之前,你需要配置bug model类符合NSCoding协议。打开ScaryBugData.swift,在文件结尾添加如下扩展:

    // MARK: - NSCoding
     
    extension ScaryBugData: NSCoding {
      func encodeWithCoder(coder: NSCoder) {
        coder.encodeObject(self.title, forKey: "title")
        coder.encodeObject(Double(self.rating), forKey: "rating")
      }
    }
    

    这里,你设置协议的一致性并且实现encodeWithCoder,它将对象的重要数据写入coder对象。
    你也需要对应的初始化。然而,你不可以把初始化放在类的扩展中,所以,添加它到main class定义:
    required convenience init(coder decoder: NSCoder) {
    self.init()
    self.title = decoder.decodeObjectForKey(“title”) as! String
    self.rating = decoder.decodeObjectForKey(“rating”) as! Double
    }
    init(coder:)将会做encodeWithCoder相反的操作,并且从coder中读取对象。
    同样,在ScaryBugDoc.swift文件中添加NSCoding协议的如下扩展:

    // MARK: - NSCoding
    extension ScaryBugDoc: NSCoding {
      func encodeWithCoder(coder: NSCoder) {
        coder.encodeObject(self.data, forKey: "data")
        coder.encodeObject(self.thumbImage, forKey: "thumbImage")
        coder.encodeObject(self.fullImage, forKey: "fullImage")
      }
    }
    

    然后,在main class中添加必要的初始化:

    required convenience init(coder decoder: NSCoder) {
      self.init()
      self.data = decoder.decodeObjectForKey("data") as! ScaryBugData
      self.thumbImage = decoder.decodeObjectForKey("thumbImage") as! NSImage?
      self.fullImage = decoder.decodeObjectForKey("fullImage") as! NSImage?
    }
    

    所以,你已经设置了model对象的编码解码。现在你需要实现保存他们到NSUserDefaults。从在MasterViewController.swift添加如下帮助方法开始:

    func saveBugs() {
      let data = NSKeyedArchiver.archivedDataWithRootObject(self.bugs)
      NSUserDefaults.standardUserDefaults().setObject(data, forKey: "bugs")
      NSUserDefaults.standardUserDefaults().synchronize()
    }
    

    这个方法从bugs数组创建了NSData对象,然后保存对象到NSUserDefaultsNSKeyedArchiver可以处理在bug数组中的的模型对象,因为它们符合NSCoding
    切换到AppDelegate.swift,在applicationWillTerminate()添加如下代码:

    masterViewController.saveBugs()
    

    在应用程序终止之前,MasterViewController将保存bug列表到NSUserDefaults中。

    加载Bugs

    现在,你有保存bug数据的方式,你必须在应用程序启动时读取这些数据。仍然在AppDelegate.swift中,找到applicationDidFinishLaunching,在其中添加如下代码:

    masterViewController.setupSampleBugs()
    

    用以下内容替换该行:

    if let data = NSUserDefaults.standardUserDefaults().objectForKey("bugs") as? NSData {
      masterViewController.bugs = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [ScaryBugDoc]
    } else {
      masterViewController.setupSampleBugs()
    }
    

    你首先判断NSUserDefaults中是否有bug数据存在。如果有,即使是空数组(也许用户讨厌bugs!),恢复数据到bugs数据。否则加载示例bug数据。
    编译并且运行应用程序,添加/编辑/删除bugs,然后通过Cmd-Q退出应用程序。重新启动app,你会看到所有的bug修改被保存了!

    注意:如果你不是正常退出app,saveBugs()也许没有调用-你需要点击Command-Q而不是通过Xcode杀死app。为了解决这个问题,你可以在在MasterViewController添加更多的saveBugs()调用-只要有一个新的bug或者更改现有的bug。
    

    何去何从?

    这里是最后的工程,包含了你在这个系列教程中开发的全部代码。
    在这里,我建议你阅读Apple’s Mac App Programming Guide,或有一个看一些由苹果公司提供的samples。
    你也可以尝试对应用程序添加不同的控件或者新的功能。例如,如何将模型写入到文件,并使用file save panel询问用户在哪里保存它?或者对bug列表添加搜索功能,使用search field控件?
    我希望你喜欢使用swift构建Mac版本的ScaryBugs app!