Skip to content

Latest commit

 

History

History
2052 lines (2026 loc) · 71.2 KB

Porting Your iOS App to macOS.md

File metadata and controls

2052 lines (2026 loc) · 71.2 KB

把你的iOS app接入到macOS上去

Porting Your iOS App to macOS

如果你是一个iOS的开发者,事实上你早已有了相当一部分的为另一个平台 - macOS开发app的能力!

如果你和大多数的开发者一样,不想仅仅为了把你的app迁移到一个新的平台,就把它重写一遍,因为这会浪费太多的时间和金钱。只需一点小小的努力,你就可以学到如何把你的iOS app接入到macOS上,复用你已存在的iOS app中的大部分内容,仅需把平台特定的那一部分重写就可以了。

在本教程中,你会学到如何创建一个同时支持iOS和macOS的项目,如何在两个平台上复用你的代码,以及什么时候适合去写平台指定的代码。

要搞懂本教程中的主要内容,你需要熟悉 To get the most out of this tutorial you should be familiar with NSTableView 。如果你需要的话,我们有一个很不错的 介绍 可以帮助你来学习。

入门

为学习本教程,你需要从 这里 下载初始项目。

这个样本工程就是在之前教程中用到的BeerTracker app。你可以用它来你尝试过的啤酒,以及啤酒的说明,评分和图片。运行项目,来熟悉一下它的工作方式。

Beer Tracker iOS

由于这个app目前只是在iOS上可用,因此要把这个app迁移到macOS上,第一件事就是创建一个新的target。target简单来说,就是一组告诉Xcode如何来构建app的指令集。当前,你只有一个iOS的target,它就包含了所有为iPhone构建你的app所需的信息。

在顶部的 Project Navigator 中选择 BeerTracker 。在 Project and Targets 列表的底部,点击 + 按钮。

这时会弹出一个窗口,让你为项目创建一个新的target。在窗口的顶部,你会看到表示所支持的不同的平台的tab。选择macOS,然后向下滚动到 Application 这里并选择 Cocoa App 。将新的target命名为 BeerTracker-mac

添加Assets

在你刚下载的起始项目中,你会发现一个名为 BeerTracker Mac Icons 的目录。你需要将其中的App Icon添加到 BeerTracker-mac 组中的 Assets.xcassets AppIcon 中。并将 beerMug.pdf 也添加到 Assets.xcassets 中。选择 beerMug ,打开 Attributes Inspector 并将 Scales 改为 Single Scale 。这样就确保了你不需要为这个asset使用不同scale的图。

当你完成之后,你的asset看起来应该是下面这个样子:

Assets for Mac

Xcode 窗口顶部的左侧,在scheme的弹出窗口中选择 BeerTracker-mac scheme。运行项目,你会看到一个空空的窗口。在你添加UI之前,你需要确保你的代码在iOS的框架 UIKit 和macOS的框架 AppKit 之间没有任何的冲突。

Mac starting window

能力的分离

Foundation 框架让你的app可以共享大量的代码,因为它对于两个平台是通用的。然而,你的UI无法是通用的。事实上,由于你的第二个平台将会呈现你初始app的UI,苹果建议多平台的app不要去分享UI代码。

iOS有着相当严格的 人机交互指南 以确保你的用户能够在他的触摸屏幕设备上读取和选择元素。然而,macOS上有着不同的要求。笔记本和台式机都有一个鼠标指针来点击和选择,这就使得屏幕上的元素可以比在手机上的更小。

确定了UI在两个平台上无法相同之后,理解你代码中其它哪些部分可以被复用,哪些需要重写就变得非常重要。记住,在大多数的情况下,并没有绝对正确或错误的答案,应当确定的是怎样的工作方式才对你的app最好。永远记得被共享的代码越多,需要测试和debug的代码就越少。

通常情况下,你可以共享model和 model controller。打开 Beer.swift ,并在 Xcode 中打开 Utilities 抽屉,并选择 File Inspector 。由于两个target都会使用这个model,因此在 Target Membership 下,勾选 BeerTracker-mac BeerTracker 仍然保持勾选。为 BeerManager.swift Utilities 组下的 Utilities 执行同样的操作。

如果你运行项目,你就会得到一个编译错误。这是因为 Beer.swift 导入了 UIKit 。这个model使用了一些平台特有的逻辑,来为啤酒加载和保存图片。

将文件顶部import的这行代码替换成下面这样:

import Foundation

尝试运行项目,你会看到,现在由于 UIImage 是已被移除的 UIKit 中的一部分,它不会再被编译。当这个文件中的model部分在两个target之间可共享时,平台指定部分的逻辑就需要被分离出来。因此在 Beer.swift 中,删除全部标记为 Image Saving 的全部extension。在 import 语句之后,添加下列的协议:

protocol BeerImage {
  associatedtype Image
  func beerImage() -> Image?
  func saveImage(_ image: Image)
}

由于每个模块仍然都需要访问啤酒的图片,并可以保存这些图片,这个protocol就提出了一个可以交给两个target来实现的协议。

Models

点击 File/New/File… 来创建一个新的文件,选择 Swift File ,并将其命名为 Beer_iOS.swift 。确保仅有 BeerTracker target被勾选。然后创建另一个名为 Beer_mac.swift 的文件,这次则只选中 BeerTracker-mac 作为target。

打开 Beer_iOS.swift ,删除文件全部的原始内容,并添加下列的代码:

import UIKit
// MARK: - Image Saving
extension Beer: BeerImage {
// 1.
typealias Image = UIImage
// 2.
func beerImage() -> Image? {
guard let imagePath = imagePath,
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
return #imageLiteral(resourceName: "beerMugPlaceholder")
}
// 3.
let pathName = (path as NSString).appendingPathComponent("BeerTracker/(imagePath)")
guard let image = Image(contentsOfFile: pathName) else { return #imageLiteral(resourceName: "beerMugPlaceholder") }
return image
}
// 4.
func saveImage(_ image: Image) {
guard let imgData = UIImageJPEGRepresentation(image, 0.5),
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
return
}
let appPath = (path as NSString).appendingPathComponent("/BeerTracker")
let fileName = "(UUID().uuidString).jpg"
let pathName = (appPath as NSString).appendingPathComponent(fileName)
var isDirectory: ObjCBool = false
if !FileManager.default.fileExists(atPath: appPath, isDirectory: &isDirectory) {
do {
try FileManager.default.createDirectory(atPath: pathName, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Failed to create directory: (error)")
}
}
if (try? imgData.write(to: URL(fileURLWithPath: pathName), options: [.atomic])) != nil {
imagePath = fileName
}
}
}

上述的代码:

  1. BeerImage 协议要求实现的类定义一个 关联类型 。它是基于对象的实际需要的,你真实想使用的对象类型名称的占位符。因此在这个文件中,你会使用 UIImage
  2. 实现协议的第一个方法。这里的 Image 类型就代表了 UIImage
  3. 此处是当初始化一个image的时候,如何使用类型别名的另一个例子。
  4. 实现第二个协议方法来保存image。

将你的scheme切换为 BeerTracker ,然后运行项目。app此时的表现应当如同之前一样。

现在既然你的iOS target已经可以正常地工作了,你就可以去添加macOS指定的代码了。打开 Beer_mac.swift ,删除全部的原始内容,并添加下列的代码:

import AppKit
// MARK: - Image Saving
extension Beer: BeerImage {
// 1.
typealias Image = NSImage
func beerImage() -> Image? {
// 2.
guard let imagePath = imagePath,
let path = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first else {
return #imageLiteral(resourceName: "beerMugPlaceholder")
}
let pathName = (path as NSString).appendingPathComponent(imagePath)
guard let image = Image(contentsOfFile: pathName) else { return #imageLiteral(resourceName: "beerMugPlaceholder") }
return image
}
func saveImage(_ image: Image) {
// 3.
guard let imgData = image.tiffRepresentation,
let path = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first else {
return
}
let fileName = "/BeerTracker/(UUID().uuidString).jpg"
let pathName = (path as NSString).appendingPathComponent(fileName)
if (try? imgData.write(to: URL(fileURLWithPath: pathName), options: [.atomic])) != nil {
imagePath = fileName
}
}
}

上述的代码和之前的一篇几乎如出一辙,只有几点的不同:

  1. 这里使用 AppKit 指定的类 NSImage 来替换 UIImage
  2. 在iOS中,通常会把文件保存到Documents目录下。你通常是不需要担心会把这个目录搞乱的,因为他是指定app下的目录,并且是对用户隐藏的。但在macOS中,你是不可以弄乱用户的Documents目录的,因此你要把文件保存到App的支持目录下。
  3. 由于 NSImage 并没有和 UIImage 一样的获取图片data的方法,因此你需要使用 tiffRepresentation 来代替。

将你的target切换为 BeerTracker_mac ,然后运行项目。由于你的model已包含了标准功能的集合,app现在可以同时在两个平台上进行编译了。

创建用户界面

你空空的Mac app并没有什么用处,因此我们要来构建UI。在 BeerTracker-mac 组中,打开 Main.storyboard 。拖拽一个 Table View 到你空空的view上。现在在Document Outline中选择 Table View
Adding a table view

macOS的storyboards有时会要求你深入地挖掘视图层级。这点和iOS有所不同,你看到过iOS所有的模板view都处在顶层。

配置Table View

选中 Table View ,然后在Attributes Inspector中做出下面的改变:

  • Columns 设为1
  • 不勾选 Reordering
  • 不勾选 Resizing

在Document Outline中选择 Table Column ,并将它的Title设置为 Beer Name

Selecting the table view

在Document Outline中,选择 Bordered Scroll View (它包含着 Table View ),然后在Size Inspector中找到View这一部分,并将View的位置尺寸设置为:

  • x :0
  • y :17
  • width :185
  • height :253

坐标的设置在这里也有一点的不同。在macOS中,UI的原点并非位于左上侧,而在左下侧。这里你将 y 坐标设置为17,就意味着距离底部是17个像素点。

添加Delegate和Data Source

接下来你需要连接 Table View 的delegate,data source和property。你需要再次在Document Outline中选择 Table View ,然后拖拽到Document Outline中的 View Controller 并点击 delegate 。为 dataSource 执行同样的操作。

在Assistant Editor中打开 ViewController.swift ,把 Table View 拖拽 到这里,并创建一个新的名为 tableView 的outlet。

在你完成 Table View 之前,还有最后一件事你需要去做。返回Document Outline,找到名为 Table Cell View 的item。选中它,打开Identity Inspector,并将 Identifier 设置为 NameCell

图片和文本

设置 Table View 完毕后,接下来就是设置UI的“form”部分了。

首先,你需要添加一个 Image Well 到table的右侧。将frame设置为如下的数值:

  • x :190
  • y :188
  • width :75
  • height :75

Image Well 是一个方便的用来展示图片的对象,但也允许用户把一张图片拖拽到它的上面。为了实现这点, Image Well 拥有将动作连接到你代码上的能力!

在Assistant Editor中打开BeerTracker-mac的 ViewController.swift ,并为 Image Well 创建一个名为 imageView 的outlet。然后为 Image View 创建一个名为 imageChanged 的动作。确保将 Type 设置为 NSImageView ,就像下面这样:

Adding the imageView action.

拖拽尽管非常得棒,有时用户却更希望看到一个 打开对话框 由它们自己来搜索文件。拖拽一个 Click Gesture Recognizer Image Well 的上面。在Document Outline中,将 Click Gesture Recognizer 连接到 ViewController.swift 中并命名为 selectImage

Image Well 的右侧添加一个 Text Field 。在Attributes Inspector中,将 Placeholder 改为 Enter Name 。将frame设置成下面的样子:

  • x :270
  • y :223
  • width :190
  • height :22

ViewController.swift 中为 Text Field 创建一个名为 nameField 的outlet。

给啤酒评分

下面,添加一个 Level Indicator 到name field的下面。它将用来控制你的啤酒的评分。在Attributes Inspector中进行如下的设置:

  • Style :Rating
  • State :Editable
  • Minimum :0
  • Maximum :5
  • Warning :0
  • Critical :0
  • Current :5
  • Image :beerMug

将frame设置为下面的样子:

  • x :270
  • y :176
  • width :115

Level Indicator 创建一个名为 ratingIndicator 的outlet。

在rating indicator的下面添加一个
Text View 。将它的frame设置为:

  • x :193
  • y :37
  • width :267
  • height :134

要为 Text View 创建一个outlet,你需要确保在Document Outline中选择的是 Text View ,就像你在 Table View 中做的一样。将outlet命名为 noteView 。你还需要将 Text View 的delegate设置为 ViewController

在note view的下方,放置一个 Push Button 。将它的title改为 Update ,并将它的frame设置为:

  • x :284
  • y :3
  • width :85

从按钮连接一个名为 updateBeer 的动作到 ViewController 上。

添加和删除啤酒

到目前为止,你已经有了所有用来编辑和查看啤酒必须的控件。然而,现在还无法添加和删除啤酒。这将使得app很难使用,即使你的用户还没有任何要喝的酒:]

添加一个 Gradient Button 到屏幕底部的左侧。在Attributes Inspector中,将 Image 改为 NSAddTemplate

在Size Inspector中,将frame设置为:

  • x :0
  • y :-1
  • width :24
  • height :20

为新的按钮添加一个名为 addBeer 的动作。

在macOS中有一件很棒的事情,是你可以访问像 + 号这样的模板图片。这样当你有任何标准动作的按钮,但没有时间或能力创建你自己的图片,就能够让你的生活变得更加的容易。

接下来,你需要添加移除的按钮。直接添加另一个 Gradient Button 到前一个按钮的右边,并将 Image 改为 NSRemoveTemplate 。将它的frame设置为:

  • x :23
  • y :-1
  • width :24
  • height :20

最后,为这个按钮添加一个名为 removeBeer 的动作。

完成UI

你几乎已经完成了构建UI!你只需要添加几个label让它变得更棒。

添加下列的label:

  • 在name field的上方,添加文本为 Name 的label。
  • 在rating indicator的上方,添加文本为 Rating 的label。
  • 在notes view的上方,添加文本为 Notes 的label。
  • 在table view的下方,添加文本为 Beer Count: 的label。
  • 在beer count label的下方,添加文本为 0 beer count label

对于上述添加的每一个label,在Attributes Inspector中,将它们的font设置为 Other – Label ,size设置为10。

为最后一个label,创建一个名为 beerCountField 的outlet到 ViewController.swift 上。

确保你的所有label看起来像是这样:

Final UI

点击解决 Auto Layout Issues 的按钮,并在 All Views in View Controller 的部分点击 Reset to Suggested Constraints

添加代码

好的!现在你要准备开始coding了。打开 ViewController.swift 并删除名为 representedObject 的property。在 viewDidLoad() 下面添加下列的方法:

private func setFieldsEnabled(enabled: Bool) {
imageView.isEditable = enabled
nameField.isEnabled = enabled
ratingIndicator.isEnabled = enabled
noteView.isEditable = enabled
}
private func updateBeerCountLabel() {
beerCountField.stringValue = "(BeerManager.sharedInstance.beers.count)"
}

这里有两个方法来帮助你控制UI:

  1. setFieldsEnabled(_:) 可以帮助你快速地打开或关闭使用form控件的能力。
  2. updateBeerCountLabel() 帮助你快速地在 beerCountField 中设置啤酒的数量。

在你所有的outlet下面,添加下列的property:

var selectedBeer: Beer? {
didSet {
guard let selectedBeer = selectedBeer else {
setFieldsEnabled(enabled: false)
imageView.image = nil
nameField.stringValue = ""
ratingIndicator.integerValue = 0
noteView.string = ""
return
}
setFieldsEnabled(enabled: true)
imageView.image = selectedBeer.beerImage()
nameField.stringValue = selectedBeer.name
ratingIndicator.integerValue = selectedBeer.rating
noteView.string = selectedBeer.note!
}
}

这个property将会跟踪从table view选中的啤酒。如果当前没有啤酒被选择,setter会负责清空所有field中的值,并禁用所有当前无法使用的UI组件。

viewDidLoad() 替换为下列的代码:

override func viewDidLoad() {
super.viewDidLoad()
if BeerManager.sharedInstance.beers.count == 0 {
setFieldsEnabled(enabled: false)
} else {
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
}
updateBeerCountLabel()
}

就像在iOS中一样,你希望在一启动的时候去做一些事情。然而,在macOS的版本中,你需要一上来就将form填充好,以便用户能够及时看到他们的数据。

添加数据到Table View上

现在table view还不可以实际地展示任何数据,但 selectRowIndexes(_:byExtendingSelection:) 会选中列表中的第一瓶啤酒。delegate中的代码将会为你处理剩余的事情。

为了让table view去展示你的啤酒列表,添加下列的代码到 ViewController.swift 尾部,要在 ViewController 类之外:

extension ViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return BeerManager.sharedInstance.beers.count
}
}
extension ViewController: NSTableViewDelegate {
// MARK: - CellIdentifiers
fileprivate enum CellIdentifier {
static let NameCell = "NameCell"
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
<span class="hljs-keyword">let</span> beer = <span class="hljs-type">BeerManager</span>.sharedInstance.beers[row]

<span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> cell = tableView.makeView(withIdentifier: <span class="hljs-type">NSUserInterfaceItemIdentifier</span>(rawValue: <span class="hljs-type">CellIdentifier</span>.<span class="hljs-type">NameCell</span>), owner: <span class="hljs-literal">nil</span>) <span class="hljs-keyword">as</span>? <span class="hljs-type">NSTableCellView</span> {
  cell.textField?.stringValue = beer.name
  <span class="hljs-keyword">if</span> beer.name.characters.<span class="hljs-built_in">count</span> == <span class="hljs-number">0</span> {
    cell.textField?.stringValue = <span class="hljs-string">"New Beer"</span>
  }
  <span class="hljs-keyword">return</span> cell
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>

}
func tableViewSelectionDidChange(_ notification: Notification) {
if tableView.selectedRow >= 0 {
selectedBeer = BeerManager.sharedInstance.beers[tableView.selectedRow]
}
}
}

上述代码负责用来自data source的数据填充table view中的行。

仔细地看一下它,你会发现它和iOS中相应的部分 BeersTableViewController.swift 并没有太大的差别。一个值得注意的区别是,当table view选择的行发生变化时,NSTableView会发送一个 Notification NSTableViewDelegate 上。

记住,你的新macOS app现在有了多个的输入源 - 不仅仅依靠一根手指来操作。使用鼠标或键盘都可以改变table view的选择,这导致处理这些改变时,将和iOS有一点点不同。

现在添加一个啤酒。将 addBeer() 修改为:

@IBAction func addBeer(_ sender: Any) {
// 1.
let beer = Beer()
beer.name = ""
beer.rating = 1
beer.note = ""
selectedBeer = beer
// 2.
BeerManager.sharedInstance.beers.insert(beer, at: 0)
BeerManager.sharedInstance.saveBeers()
// 3.
let indexSet = IndexSet(integer: 0)
tableView.beginUpdates()
tableView.insertRows(at: indexSet, withAnimation: .slideDown)
tableView.endUpdates()
updateBeerCountLabel()
// 4.
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
}

这里没有太多累赘的东西。你只需去完成下列的事情:

  1. 创建一个新的啤酒。
  2. 将啤酒插入到model中。
  3. 为table插入一个新的行。
  4. 选择新啤酒所在的这行。

你货主注意到了这点,就像在iOS中一样,你需要在插入新的行之前调用 beginUpdates() endUpdates() 。所以说,你其实早已懂得了关于macOS的很多的内容!

移除条目

为了移除一瓶啤酒,添加下列的代码到 removeBeer(_:) 中:

@IBAction func removeBeer(_ sender: Any) {
guard let beer = selectedBeer,
let index = BeerManager.sharedInstance.beers.index(of: beer) else {
return
}
// 1.
BeerManager.sharedInstance.beers.remove(at: index)
BeerManager.sharedInstance.saveBeers()
// 2
tableView.reloadData()
updateBeerCountLabel()
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
if BeerManager.sharedInstance.beers.count == 0 {
selectedBeer = nil
}
}

依然是非常直接的代码:

  1. 如果已选中一个啤酒,就从model中移除它。
  2. 重载table view,选择第一瓶可用的啤酒。

处理图片

记得 Image Wells 拥有接收拖拽到它上面的图片的能力么?将 imageChanged(_:) 的方法修改为:

@IBAction func imageChanged( sender: NSImageView) {
guard let image = sender.image else { return }
selectedBeer?.saveImage(image)
}

你以为这可能会很难!但苹果早已为你负责处理了所有繁重的工作,并将接收拖拽来的图片的能力赐予了你。

但另一方面,你需要做很多工作,来方便用户从你的app中选取图片。将 selectImage() 方法替换为:

@IBAction func selectImage( sender: Any) {
guard let window = view.window else { return }
// 1.
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = true
// 2.
openPanel.allowedFileTypes = ["jpg", "png", "tiff"]
// 3.
openPanel.beginSheetModal(for: window) { (result) in
if result == NSApplication.ModalResponse.OK {
// 4.
if let panelURL = openPanel.url,
let beerImage = NSImage(contentsOf: panelURL) {
self.selectedBeer?.saveImage(beerImage)
self.imageView.image = beerImage
}
}
}
}

上述代码实现了你使用 NSOpenPanel 来选取一个文件的过程。以下是详细步骤:

  1. 创建一个 NSOpenPanel ,并配置它的设置。
  2. 为了让用户只可以选择图片,你将允许的文件类型设置为你所需的文件格式。
  3. 展示这个sheet给用户。
  4. 如果用户选择了一张图片的话,保存它。

最后,在 updateBeer(_:) 中添加保存数据model的代码:

@IBAction func updateBeer(_ sender: Any) {
// 1.
guard let beer = selectedBeer,
let index = BeerManager.sharedInstance.beers.index(of: beer) else { return }
beer.name = nameField.stringValue
beer.rating = ratingIndicator.integerValue
beer.note = noteView.string
// 2.
let indexSet = IndexSet(integer: index)
tableView.beginUpdates()
tableView.reloadData(forRowIndexes: indexSet, columnIndexes: IndexSet(integer: 0))
tableView.endUpdates()
// 3.
BeerManager.sharedInstance.saveBeers()
}

上述代码:

  1. 确认啤酒是存在的,并更新它的property。
  2. 更新table view以在table上反映任何名称的变化。
  3. 保存数据到磁盘上。

你已经全部设定完毕!运行app,然后添加啤酒。记住你需要选择 Update 来更新你的数据。

Final UI with some beers added.

最后的接触

你已经学到了大量关于iOS和macOS开发的不同之处。但这里还有一个你需要熟悉的概念: Settings/Preferences 。在iOS中,你可能已经习惯了前往Setting,找到你的app,然后修改你可用的设置。在macOS中,这些却需要你在app内部的 Preferences 中来完成。

运行 BeerTracker target,在模拟器中的Settings app中找到BeerTracker的设置。这里你会发现一个让用户限制他的笔记长度的设置,以避免他们日后会进行投诉。

Settings - iOS

为了在你的mac app中获得相同的特性,你要为你的用户创建一个Preferences窗口。在 BeerTracker-mac 中,打开 Main.storyboard ,并拖拽一个新的 Window Controller 。选择这个 Window ,打开Size Inspector,并进行如下的修改:

  1. Content Size 的width设置为380,height设置为55。
  2. Minimum Content Size 中,将width设置为380,height设置为55。
  3. Maximum Content Size 中,将width设置为380,height设置为55。
  4. Full Screen Minimum Content Size 中,将width设置为380,height设置为55。
  5. Full Screen Maximum Content Size 中,将width设置为380,height设置为55。
  6. Initial Position 中,选择 Center Horizontally Center Vertically

接下来,选择这个空空的View Controller的view,然后将它的size修改至和上面设置中相匹配的380 x 55。

以上操作可以让你的Preferences窗口始终保持在相同的尺寸下,以在合乎逻辑的位置打开给用户。当你完成之后,你的新窗口看起来在storyboard中像这个样子:

Preferences - Mac

现在,用户还没有办法来打开你的新窗口。由于它应当被绑定到 Preferences 的菜单项上,因此要在storyboard中找到菜单栏场景。你可以把它拖动到靠近Preferences窗口的地方,这样以下部分的操作就可以方便一些。当它足够接近之后,执行如下的操作:

  1. 在storyboard的菜单栏中,点击 BeerTracker-mac 来打开菜单。

  2. Preferences 菜单项 拖拽 到新Window Controller上
  3. 在弹出的对话框中选择 Show

添加一个 Check Box Button 到空空的View Controller上。将它的text修改为 Restrict Note Length to 1,024 Characters

选中 Checkbox Button 后,打开Bindings Inspector,执行下列的操作:

  1. 展开 Value ,并勾选 Bind to
  2. 在弹出的菜单中选择 Shared User Defaults Controller
  3. Model Key Path 中,输入 BT_Restrict_Note_Length

Utilities 组中创建一个新的名为 StringValidator.swift 的Swift文件。确定为这个文件同时勾选两个target。

打开 StringValidator.swift ,并使用下列的代码来替换其中的内容:

import Foundation
extension String {
private static let noteLimit = 1024
func isValidLength() -> Bool {
let limitLength = UserDefaults.standard.bool(forKey: "BT_Restrict_Note_Length")
if limitLength {
return self.characters.count <= String.noteLimit
}
return true
}
}

这个类为两个target提供了检查一个字符串的长度是否合法的能力,只要用户默认的 BT_Restrict_Note_Length 值为true。

ViewController.swift 中添加下列的代码到文件的尾部:

extension ViewController: NSTextViewDelegate {
func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
guard let replacementString = replacementString else { return true }
let currentText = textView.string
let proposed = (currentText as NSString).replacingCharacters(in: affectedCharRange, with: replacementString)
return proposed.isValidLength()
}
}

最后,在 Main.storyboard 中改变每个 Window 的名称已匹配他们的目标,让用户更加的清晰。选择initial的 Window Controller ,并在 Attributes Inspector 中将它的title改为 BeerTracker 。从Preferences窗口中选择 Window Controller ,并将它的title修改为 Preferences

运行你的app。选择Preferences菜单项,现在你应当能够看到带有偏好项的新Preferences窗口了。勾选这里,然后找到一些较长的文本来粘贴到 Text View 中,如果超过了1024个字符的话,就无法做到,就像在iOS app中一样。

Build and Run with Preferences

从这儿去向哪里?

你可以从 这里 下载最终完成的项目。

在本教程中,你已学到了:

  • 如何将一个现有的iOS项目迁移到macOS上去。
  • 如何根据你的平台所需来拆分代码。
  • 如何在项目之间复用已存在的代码。
  • Xcode在两个平台上的一些不同的表现。

关于更多移植的你的app到macOS中的内容,请访问 Apple's Migrating from Cocoa Touch Overview