10.创建一个基于文档的应用程序¶
本书到目前为止,你已经建立了两个非常不同的Mac
应用程序。首先,你使用SwiftUI
制作了一个传统的基于窗口的应用程序。接下来,你使用AppKit
创建了一个菜单栏应用程序。
现在,你将了解另一类应用程序:基于文档的应用程序。在本节中,你将回到SwiftUI
,但与上一节的做法相反,你将在SwiftUI
应用中嵌入一个AppKit
视图。
本节的应用程序是一个Markdown
编辑器。Markdown
是一种标记语言,它允许你快速而轻松地编写格式化的文本。它可以转换为HTML
进行显示,但在编写和编辑时比HTML
要方便得多。
您将从Xcode
模板中创建一个基于文档的应用程序,看看它能免费提供多少功能。然后,你将继续定制用于保存和打开的文件类型,并添加一个HTML
预览。
如果你读过SwiftUI by Tutorials
,这个应用程序对你来说会很熟悉,尽管这个版本有一个不同的名字,以避免混淆设置。会有一些不同之处,特别是在下一章中,它详细地处理了菜单。如果你对这个应用程序已经很熟悉了,可以跳过这一章,继续学习下一章中提供的启动项目。
设置一个基于文档的应用程序¶
许多Mac
应用程序是基于文档的。想想像TextEdit
、Pages
、Numbers
或Photoshop
这样的应用程序。你一次只处理一个文件,每个文件都在自己的窗口中,而且你可以同时打开多个文件,每个文件都显示自己的内容。这类应用程序允许你编辑、保存和打开不同的文件,所有这些都基于文件的类型。
现在,你将制作你自己的基于文件的应用程序,它可以处理任何Markdown
文件,即使是由不同的编辑器创建的。
启动Xcode
并创建一个新项目。选择macOS
并选择Document App
。
确保界面是SwiftUI
,语言是Swift
。将该应用程序称为MarkDowner
。
一旦你保存了这个项目,建立并运行这个应用程序。点击New Document
,如果出现文件选择器,或者从File
菜单中选择New
。这将给你一个单一的窗口,显示一些默认文本。你可以编辑这个文本,并使用标准的Edit
菜单命令进行选择、剪切、复制和粘贴,以及撤销和重做。
在File
菜单中可以看到所有你希望在任何编辑器类型的应用程序中看到的菜单项。从File
菜单中选择Save
或按Command-S
。
Note
如果你在保存对话框中没有看到文件扩展名,进入Finder ▸ Preferences ▸ Advanced
并打开Show all filename extensions
。这将使你更容易理解本章的下一部分内容。
默认的应用程序使用的文件扩展名是.exampletext
,所以给它起个名字,用建议的扩展名保存你的文件。关闭窗口,用Command-N
创建一个新窗口。现在从File ▸ Open
中选择你保存的文件,打开它。
所以你已经有了一个可以编辑、保存和打开文档的应用程序。而你甚至还没有看过代码呢!
关闭所有的文档窗口,退出应用程序,回到Xcode
看看那里发生了什么。
默认的文档应用程序¶
在你的项目中,有三个.swift
文件
MarkDownerApp.swift
与你在其他SwiftUI
项目中看到的App.swift
文件类似,但它的body
不是包含一个WindowGroup
,而是包含一个DocumentGroup
。
你用一个文档类型的实例来初始化DocumentGroup
,在这里是MarkDownerDocument
。在闭包中,你提供了显示该文件数据的视图,并将其传递给文件的文档绑定,因此对文档的修改可以流回。
如果你的应用程序支持一个以上的文档类型,你可以在这里添加一个以上的DocumentGroup
。
视图像往常一样被设置为ContentView
,但有document
参数。
ContentView.swift
接收该文档并使用其text
属性来填充TextEditor
。这种类型的视图允许编辑长块的文本。
真正的魔法发生在MarkDownerDocument.swift
中。这是你配置文档类型的地方,也是保存和打开文档的地方。
首先看一下UTType
扩展名。UT
代表Uniform Type
,是macOS
处理文件类型、文件扩展名和确定哪些应用程序可以打开哪些文件的方式。当你定制应用程序来处理Markdown
文件时,你会很快了解到更多关于这个问题。
在MarkDownerDocument
中,你有一个text
属性,用来保存文件的内容。它的初始化器设置了你在运行应用程序时在每个新窗口看到的默认文本。readableContentTypes
属性决定了这个应用程序可以打开哪些文档类型,使用前面定义的UTType
。
init
和fileWrapper
方法处理所有打开和保存文档文件的工作。现在,他们使用的是.exampletext
文件扩展名,但现在是时候解决如何处理Markdown
文件了。
为Markdown
配置¶
当你在Mac
上双击一个文档文件时,Finder会使用默认的应用程序打开它。TextEdit
用于.txt
文件,Preview
用于.png
文件,等等。右键点击任何一个文档文件,看看Open With
菜单。你会看到Mac
上能够打开该类型文件的应用程序的列表。Finder
知道哪些应用程序可以打开该文件,因为应用程序开发人员已经指定了他们的应用程序可以打开哪些Uniform Types
。
要设置一个基于文档的应用程序来打开一个特定的文件类型,你需要关于该文件的三条信息:
- 统一类型标识符或`UTI'。
- 这符合什么标准的文件类型。
- 文件扩展名或扩展名。
苹果公司提供了一个系统声明的统一类型列表。在为一个应用程序确定文件类型时,你总是应该先检查这里。但在这种情况下,它没有帮助,因为Markdown
不在那里。
然而,在互联网上搜索markdown uniform type
可以找到John Gruber
,Markdown
的发明者。他说统一类型标识符应该是net.daringfireball.markdown
,而这符合public.plain-text
。
在FileInfo.com上搜索Markdown
告诉你,Markdown
最流行的文件扩展名是.markdown
和.md
。
这给了你所有的数据,你需要把你的应用程序从使用纯文本切换到使用Markdown
文本。
设置一个文件类型¶
在项目导航器列表的顶部选择项目。点击MarkDowner
目标,从上方的选择中选择Info
标签。
展开Document Types
部分,将Identifier
改为net.daringfireball.markdown
。
接下来,展开Imported Type Identifiers
部分,做如下修改:
Description:
Markdown
文本
Identifier:
net.daringfireball.markdown
Extensions:
markdown
, md
Note
如果你对你的Markdown
文件使用不同的扩展名,请参阅下面的Challenge 1
。
这里的所有其他设置可以保持不变,因为Conforms To
字段已经包含public.plain-text
。
这就是你的应用程序的配置;现在你必须改变MarkDownerDocument
以使用这些新设置。
回到MarkDownerDocument.swift
中,将UTType
一行替换为以下内容:
UTType(importedAs: "net.daringfireball.markdown")
接下来,右击exampleText
,选择Refactor ▸ Rename...
,将其重命名为markdownText
。
而且,为了让你知道它已经工作了,把init
中的默认文本改为# Hello, MarkDowner!
,这是1
级标题的Markdown
格式。
测试文件设置¶
建立并运行应用程序,并创建一个新的文件。现在的默认文本是# Hello MarkDowner!
。保存文档,并确认建议的文件名是使用.md
或.markdown
作为文件扩展名。
Note
它选择哪一个似乎是随机的,也许取决于你过去在其他应用程序中使用过什么。
保存并关闭你的新文件,然后在Finder
中找到该文件。鼠标右键单击它,显示其Open With
菜单。
你看到MarkDowner
列在那里,是因为你的设置告诉Finder
,你的应用程序可以打开Markdown
文件。如果你有其他应用程序创建的任何Markdown
文件,右键点击其中一个并在MarkDowner
中打开它。
你的应用程序还没有对Markdown
文本做任何处理,但它现在可以编辑、保存和打开任何Markdown
文件。伟大的工作! 现在来学习更多关于Markdown
的知识。
Markdown
和HTML
¶
Markdown
是一种标记语言,它使用快捷键将纯文本的格式化,以便于转换为HTML
。作为一个例子,请看下面这个HTML
:
<h1>Important Header</h1>
<h2>Less Important Header</h2>
<a href="https://www.raywenderlich.com">Ray Wenderlich</a>
<ul>
<li>List Item 1</li>
<li>List Item 2</li>
<li>List Item 3</li>
</ul>
如果要用Markdown
写同样的内容,你会用:
# Important Header
## Less Important Header
[Ray Wenderlich](https://www.raywenderlich.com)
- List Item 1
- List Item 2
- List Item 3
我相信你会同意,Markdown
版本更容易写,更容易读,而且更可能是准确的。
你可以从这个非常有用的小抄中了解到更多关于Markdown
的信息。
在MarkDowner
中,你用Markdown
写文本。这个应用会把它转换为HTML
,并在网页视图中显示在一边。
Swift
有能力将某些Markdown
元素转换为AttributedString
。这对于格式化你的用户界面的部分内容非常有用。这不是你在这里想要的,因为它不会创建HTML
。但有几个Swift
包可以。你在这个应用中要使用的是[Swift MarkdownKit](https://github.com/objecthub/swift-markdownkit)。
将Markdown
转换为HTML
¶
如果你通过上一节的工作,或者你在iOS
应用程序中使用过Swift
包管理器,那么你会熟悉这个过程。
在Xcode
中,在项目导航器中选择项目,这一次,点击MarkDowner
项目而不是目标。进入Package Dependencies
标签,点击加号按钮,添加一个新的依赖性。
将此网址复制到右上方的搜索栏中,以搜索该软件包。
https://github.com/objecthub/swift-markdownkit
当Xcode
找到软件包后,确保它被选中并点击Add Package
来下载它。
一旦下载完成,你会看到一个新的对话框,询问你想使用软件包的哪些部分。选择MarkdownKit Library
并点击Add Package
将其添加到你的项目中。
下一步是编辑MarkDownerDocument.swift
,使其能够创建HTML
版本的文档。为了使用你刚刚添加的包,你需要导入它。把它添加到文件顶部的其他导入文件中:
import MarkdownKit
在MarkDownerDocument
中,在text
属性下,定义一个html
属性:
var html: String {
let markdown = MarkdownParser.standard.parse(text)
return HtmlGenerator.standard.generate(doc: markdown)
}
这段代码创建了一个计算属性,它使用MarkdownKit
的MarkdownParser
来解析文本,并使用其HtmlGenerator
将其转换为HTML
。
你的文档现在有两个属性。一个是Markdown
文本,这是每一个文档文件保存的内容。另一个是这个文本的HTML
版本,它是使用MarkdownKit
包从文本中导出的。
嵌入一个AppKit
视图¶
现在你已经为MarkDownerDocument
设置了一个html
属性,你需要一个方法来显示它。显示HTML
的明显方法是在某种网络视图中。问题是,SwiftUI
没有网络视图--至少现在没有。但这提供了一个学习在SwiftUI
应用程序中嵌入WebKit
视图的绝佳机会。
如果你在iOS
应用中做过这个,你会使用UIViewRespresentable
来嵌入一个UIKit
视图。对于嵌入AppKit
视图,你使用NSViewRepresentable
,但它的工作方式完全相同,如果你用NS
替换每个UI
。
创建一个新的Swift
文件,名为WebView.swift
,并将其内容替换为以下代码:
// 1
import SwiftUI
import WebKit
// 2
struct WebView: NSViewRepresentable {
// 3
var html: String
init(html: String) {
self.html = html
}
// 4
func makeNSView(context: Context) -> WKWebView {
WKWebView()
}
// 5
func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.loadHTMLString(
html,
baseURL: Bundle.main.resourceURL)
}
}
踏踏实实地做这件事:
- 你需要
SwiftUI
库来使用NSViewRepresentable
,而你要嵌入的WKWebView
是在WebKit
框架中。 - 这个结构定义了一个名为
WebView
的SwiftUI
视图。它符合NSViewRepresentable
。 - 该结构有一个单一的属性来保存
HTML
文本。 NSViewRepresentable
有两个必要的方法:makeNSView(context:)
创建并返回NSView
,在这里是WKWebView
。- 第二个方法是
updateNSView(_:context:)
。每当属性发生变化,需要更新视图时,系统就会调用这个方法。在这种情况下,每当HTML
发生变化时,网络视图就会重新加载WKWebView
中的HTML
。
现在,你可以访问一个新的SwiftUI
视图,叫做WebView
,它包含一个WKWebView
。
显示HTML
¶
打开ContentView.swift
,用这个替换body
的内容:
HSplitView {
TextEditor(text: $document.text)
WebView(html: document.html)
}
你想在可调整大小的窗格中并排显示Markdown
和HTML
。MacOS
的SwiftUI
有一个专门为此设计的视图,叫做HSplitView
。如果你想垂直堆叠视图,也有一个VSplitView
,但水平分割更适合这个应用程序。
在HSplitView
中,TextEditor
和以前一样。新的部分是你刚刚创建的WebView
。你要把文档的HTML
版本的文本传递给这个视图。
先不要建立和运行。它可能看起来一切都设置好了,但它不会工作。
再谈Mac
沙盒¶
在第1节中,你发现你必须打开Outgoing Connections (Client)
以允许从互联网下载。你可能认为这个应用程序不需要任何这样的权限,因为它只处理本地数据,但是Mac
沙盒并不是这样工作的。
要把任何东西载入WKWebView
,甚至是本地的HTML
字符串,你需要以完全相同的方式打开沙盒。
点击项目导航器顶部的项目,选择MarkDowner
目标,然后选择Signing & Capabilities
标签。
勾选Outgoing Connections (Client)
,允许你的WebView
加载HTML
。
现在,构建并运行。测试一些Markdown
。如果你想做个入门者,可以复制上面的样本。试着调整窗口的大小,拖动分隔线来调整每个窗格的大小。
你可以让每个子视图变得很小,甚至让它消失。这并不理想,所以你需要解决这个问题。
限制框架¶
正如你在第二章"使用窗口"中所发现的,为你的窗口设置框架以限制其大小是很重要的。
在本例中,你希望TextEditor
填充在窗口的左边,WebView
填充在右边。当用户调整窗口大小和用户拖动它们之间的分隔线时,它们都应该调整大小。但是分隔线不应该让任何一个视图消失,而且窗口应该有一个最小尺寸。
回到ContentView.swift
中,将body
的内容替换成这样:
HSplitView {
TextEditor(text: $document.text)
// 1
.frame(minWidth: 200)
WebView(html: document.html)
// 2
.frame(minWidth: 200)
}
// 3
.frame(
minWidth: 400,
idealWidth: 600,
maxWidth: .infinity,
minHeight: 300,
idealHeight: 400,
maxHeight: .infinity)
增加的内容都是frame
修改器,但这里是它们的作用:
- 在
HSplitView
中,将TextEditor
的最小宽度设置为200
。 - 对
WebView
应用同样的宽度限制。 - 给
HSplitView
一个更完整的frame
,设置其最小、理想和最大尺寸。最小宽度足以容纳两个子视图的最小宽度。最大尺寸是infinity
,所以窗口可以达到用户想要的大小。
建立并再次运行,尝试调整每个窗格和窗口的大小。这样效果更好。:]
添加一个工具条¶
现在,该应用程序允许你编辑Markdown
文本,并在网页视图中呈现出相应的HTML
。但有时看到实际生成的HTML
代码是很有用的。而且,如果在一个较小的屏幕上空间紧张,能够完全关闭预览是很方便的。
所以现在,你要添加一个工具栏。在工具栏中,你将有控件在三种可能的预览模式之间切换:网页、HTML
代码和关闭。
首先,为预览模式定义一个枚举。把它添加到ContentView.swift
的末尾,在任何结构之外:
enum PreviewState {
case web
case code
case off
}
接下来,将这个属性添加到ContentView
中:
@State private var previewState = PreviewState.web
这就定义了一个@State
属性来保存所选状态,并默认设置为web
。
最后,在frame
修改器之后,将其添加到HSplitView
中:
// 1
.toolbar {
// 2
ToolbarItem {
// 3
Picker("", selection: $previewState) {
// 4
Image(systemName: "network")
.tag(PreviewState.web)
Image(systemName: "chevron.left.forwardslash.chevron.right")
.tag(PreviewState.code)
Image(systemName: "nosign")
.tag(PreviewState.off)
}
// 5
.pickerStyle(.segmented)
// 6
.help("Hide preview, show HTML or web view")
}
}
这一切有什么作用?
- 给
HSplitView
应用一个工具条修改器。 - 在工具栏中插入一个
ToolbarItem
。 ToolbarItem
包含一个Picker
,其选择与previewState
属性绑定。- 为每个
PreviewState
显示苹果公司SF
符号字体中的一个图像,并将标签设置为相应的情况。 - 将选择器设置为使用
segmented
风格。 - 使用
help
修改器应用可访问性文本和工具提示。
当你在第1节中制作一个工具条时,你把工具条的代码放在自己的文件中。如果你的视图很复杂或者工具栏包含很多按钮,这是个好主意。在这种情况下,直接应用它仍然使ContentView.swift
相当简短和可读。
构建并运行应用程序,可以看到工具栏的最右边有这三个选项。点击每一个,可以看到显示当前所选选项的视觉差异:
配置预览¶
你已经有了支配预览的控件,但是你的应用程序并没有对它们做出反应。现在,在HSplitView
中你有TextEditor
和WebView
。但是当你允许预览选项时,有三种可能的组合。
- 单独的
文本编辑器
。 TextEditor
加WebView
.TextEditor
加上新的东西来显示原始HTML
。
首先,为了让用户关闭WebView
,Command-click HSplitView
里面的WebView
,选择Make Conditional
。
Note
如果你的Xcode
偏好设置Command-click
为Jumps to Definition
,使用Command-Control-click
来显示菜单。
将true
占位符替换为:
previewState == .web
建立并运行该应用程序。点击工具条上的三个选项。只有当你在选取器中选择网络按钮时,WebView
才是可见的,当你点击其他任何一个时,它就会消失:
让WebView
有条件地出现,为它不应该出现的时候增加了一个EmptyView
。但这是你想检查previewState
是否被设置为code
的地方。
替换:
} else {
EmptyView()
}
为:
// 1
} else if previewState == .code {
// 2
ScrollView {
// 3
Text(document.html)
// 4
.frame(minWidth: 200)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading)
.padding()
// 5
.textSelection(.enabled)
}
}
下面是这段代码的作用:
- 检查
previewState
是否被设置为code
。 - 显示一个
ScrollView
,这样你就可以看到所有的文本,即使有超过适合的窗口。 - 在一个
Text
视图中显示HTML版本的文档文本。 - 为
Text
视图设置frame
和padding
,使其具有与其他视图相同的最小宽度,但要扩展到填补ScrollView
。 - 使文本可选择,所以你可以复制它。
现在建立并运行,并尝试三种预览状态。
挑战¶
挑战1:添加文件扩展名¶
当你设置文件类型时,你允许应用程序使用.markdown
或.md
作为文件扩展名。但有些人使用.mdown
来表示Markdown
文件。编辑项目,使之成为一个有效的扩展名。为了测试它,重命名你的一个文件,使用这个新的扩展名,看看你是否能在MarkDowner
中打开它。
挑战2:应用一个应用程序图标¶
打开下载材料中本章的assets
文件夹,你会发现一个叫markdown.png
的图像文件。回头看看第5章"设置偏好和图标",提醒自己如何创建一个应用程序图标集并将其添加到项目中。
你可以自己去实现这些,但如果你需要一些帮助,请查看challenge
文件夹。
关键点¶
- 苹果为基于文件的
Mac
应用程序提供了一个起始模板。这可以让你很快上手,但现在你知道如何定制这个模板以适应你自己的文件类型。 - 你使用
Uniform Types
来指定你的应用程序可以处理哪些文件类型。这些可以是苹果公司在系统中定义的类型,或者你可以创建你自己的自定义类型。 SwiftUI
和AppKit
协作良好。你可以使用NSViewRepresentable
在SwiftUI
应用程序中嵌入任何AppKit
视图。
何去何从?¶
你已经创建了一个编辑器应用程序,可以处理Markdown
文件,将其转换为HTML
,并以各种方式预览它们。
在下一章中,你将为这个应用程序添加菜单命令。它们会让你对HTML进行造型,调整字体大小,显示一些Markdown
的帮助,并将Markdown
片段添加到你的文档中。