本文由 简悦 SimpRead转码, 原文地址 rderik.com
随着我们用来开发软件的框架和工具的进步,创建一个新的应用程序似乎就像......。
随着我们用来开发软件的框架和工具的进步,创建一个新的应用程序似乎就像变魔术一样。我们只需点击几个按钮,一切就会为我们创造出来。我喜欢魔术,但我认为有时我们最终只能成为框架的 "使用者",而无法真正理解发生了什么。在本篇文章中,我将解释 macOS 应用程序如何工作的几个概念,希望我们能更好地理解这个生态系统。
我们不会使用 Xcode,所以请拿起你最喜欢的文本编辑器,让我们开始吧。要讲的内容很多,我们会讲得很快。如果您有任何问题,可以在 Reddit /u/rcderik、twitter @rderik 或 email 上给我发消息。
你可以从 GitHub 代码库 下载代码。
基于代理的应用程序
你可能对基于代理的应用程序并不陌生,但我还是要解释一下什么是基于代理的应用程序,这样我们就可以从相同的基础开始。基于代理的应用程序是一种提供服务但不提供广泛 GUI(图形用户界面)的应用程序,它们提供的界面通常是主工具栏上的一个图标和一个上下文菜单。一个有名的例子是 Dropbox 应用程序,它就在你的工具栏上,你可以要求同步、查看状态和更改配置。
我们的应用程序将非常简单,它只会跟踪事件。它将显示一个计数器,并提供增加或减少计数的选项。它很简单,所以我们不会被它的工作方式所干扰,可以专注于学习 macOS 应用程序的工作方式。
使用 Swift 软件包管理器创建可执行文件
我们将使用 Swift 软件包管理器生成基本的应用程序,然后从这里开始。
我们将把应用程序命名为 "Squirrel"(如果你读过我的博客,就会知道我在命名方面的问题)。
$ mkdir ~/Desktop/Squirrel
cd ~/Desktop/Squirrel
$ swift package init --type executable
这样就创建了 Swift 可执行文件的结构,我们可以运行它并看到 "Hello, world!
$ swift run
# it'll build our executable and run it.
#[2/2] Linking ./.build/x86_64-apple-macosx/debug/Squirrel
#Hello, world!
让我们看看为我们创建了什么。
Swift 软件包结构
当我们运行 init
命令时,它创建了一个如下所示的文件结构:
.
├── Package.swift # specifies our package targets, dependencies and metadata
├── README.md # Just a README where you can explain what your package is about
├── Sources # All of our source code goes here
│ └── Squirrel
│ └── main.swift # Our entry point, the main code file
└── Tests # Tests structure that we'll ignore for now.
├── LinuxMain.swift
└── SquirrelTests
├── SquirrelTests.swift
└── XCTestManifests.swift
4 directories, 6 files
如您所见,我们的入口点是 main.swift
。打开它,你会看到 "Hello, world!"的打印语句。
当我们编译代码时,它会将所有输出生成到 .build
目录中。我们感兴趣的是位于以下目录中的可执行文件:
./.build/x86_64-apple-macosx/debug/Squirrel
我们可以使用 swift run
命令或直接运行它:
$ ./.build/x86_64-apple-macosx/debug/Squirrel
# Hello, world!
好了,这些应该足够了,你可以开始使用 SPM(Swift 包管理器)创建可执行文件了。现在,我们将开始创建 macOS 应用程序。
基本 macOS 应用程序
我们将创建一个基于代理的应用程序,它们非常方便,而且不需要复杂的图形用户界面。工作流程很简单,它应该显示一个计数器和一个上下文菜单来增加或减少计数。
我们习惯于启动 Xcode 并创建一个新项目,但在这里,我们只需通过命令行操作即可。我们将尽可能简单。
最低限度是什么?
使用 Xcode 的便利性让我们变得有点懒惰,我们不必考虑应用程序的启动过程。我们只知道只要按下播放键,应用程序就会运行。我们不知道一切从何开始。在我们的例子中,我们已经有了!我们已经有了我们的 main.swift
文件。我们可以从这里启动一个程序,哪怕只是一个简单的程序。
现在,我们需要启动应用程序的生命周期,这样我们就知道它将涉及到 AppDelegate
。
macOS 应用程序的生命周期
在 macOS 和 iOS 中,应用程序的生命周期由 AppDelegate
处理。当应用程序完成启动或转入后台等操作时,"AppDelegate "会收到通知。所以我们肯定需要一个 AppDelagete
。
因此,让我们创建AppDelegate
。在你的 Sources/Squirrel/
文件夹中创建文件 AppDelegate.swift
。并添加以下内容:
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
print("Welcome to Squirrel App!")
}
}
这应该很简单,但我们如何运行它呢?
如果你检查过任何用 Xcode 创建的应用程序,就会发现 AppDelegate
包含一个有趣的注解,即 @NSApplicationMain
。这应该会给我们一个提示。
什么是 @NSApplicationMain 注解?
@NSApplicationMain
被替换为启动应用程序和为应用程序创建 RunLoop
所需的模板。RunLoop "是一个 "无休止 "运行(直到应用程序停止为止)的循环,用于处理输入,也是应用程序执行操作的地方。例如,如果我们的应用程序是一个简单的 REPL(Run Eval Print Loop,运行评估打印循环),我们就需要有一个循环来提示用户输入信息(可能是显示一个问题),然后等待用户回答,然后执行操作,并返回到提示用户输入更多信息的循环。这个简单的循环可以是一个 while
循环,也可以是我们知道的任何其他循环。对于更多的 macOS 应用程序来说也是类似的,但更为复杂,因此它可以响应用户的点击、敲击、温度变化、文件系统变化或应用程序接收到的任何其他输入,但想法仍然是一样的。
既然我们知道应用程序需要 "RunLoop",那么我们就调用 "NSApplicationMain "函数来启动自己的 "RunLoop"。
编辑 main.swift
文件并将其内容替换为以下内容:
import Foundation
import Cocoa
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
这应该足以运行我们的应用程序了,让我们看看它是否正常!
$ swift run
哦,出错了:
注意:这不再会引发错误。谢谢你指出来,蒂姆!(出于历史原因,我打算保留它,但我们仍然需要应用程序捆绑包中的 Info.plist
,所以请继续阅读)。
Squirrel[9045:2534023] No Info.plist file in application bundle or no NSPrincipalClass in the Info.plist file, exiting
还记得 Xcode 为您生成的 Info.plist
文件吗?好的,我们需要为应用程序创建一个。
应用程序属性列表(Plist)
让我们为支持文件创建一个目录。
在此,我们可以创建包含以下内容的 Info.plist
文件:
<?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>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
现在我们可以将其复制到我们的 .build
目录中。
$ cp SupportFiles/Info.plist .build/x86_64-apple-macosx/debug/
现在我们可以运行应用程序了。
$ swift run
Welcome to Squirrel App!
我们的欢迎信息将显示出来,由于我们仍在 "RunLoop"(运行循环)中,我们的应用程序将在前台保持激活状态。要停止运行,我们可以按 [Ctrl-c]
(Control 加 c)键。
恭喜,我们的应用程序已经正常运行了。现在,我们可以设置菜单中的图标和应用程序逻辑的其余部分了。
在菜单栏中显示我们的应用程序
我们的应用程序很简单,因此我们将在 AppDelegate 上完成所有工作。我们将创建三个函数:"increaseCounter"(增加计数器)、"decreaseCounter"(减少计数器)和 "quit"(退出)。我们将把这些函数链接到菜单中的项目。你的 AppDelegate.swift
应该如下所示:
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(
withLength: NSStatusItem.variableLength)
statusBarItem.button?.title = "🌰"
let statusBarMenu = NSMenu(title: "Counter Bar Menu")
statusBarItem.menu = statusBarMenu
statusBarMenu.addItem(
withTitle: "Increase",
action: #selector(AppDelegate.increaseCount),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Decrease",
action: #selector(AppDelegate.decreaseCount),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Quit",
action: #selector(AppDelegate.quit),
keyEquivalent: "")
}
@objc func increaseCount() {
print("Increasing")
}
@objc func decreaseCount() {
print("Decreasing")
}
@objc func quit() {
NSApplication.shared.terminate(self)
}
}
现在我们可以运行(也会构建)应用程序并查看其运行情况。
$ swift run
现在检查状态栏。如果看不到我们的图标,可能是因为当前的某些菜单项覆盖了它。切换到 "Finder",你会在工具栏上看到一个螺母图标,点击它就能与我们的应用程序进行交互。
很好,但它没有计数。让我们来解决这个问题。我们将添加一个 counter
变量来跟踪计数,然后更新按钮标题以显示计数。代码并不复杂,以下是更改后的整个 AppDelegate
代码。
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
var counter: Int = 0
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(
withLength: NSStatusItem.variableLength)
statusBarItem.button?.title = "🌰 \(counter)"
let statusBarMenu = NSMenu(title: "Counter Bar Menu")
statusBarItem.menu = statusBarMenu
statusBarMenu.addItem(
withTitle: "Increase",
action: #selector(AppDelegate.increaseCount),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Decrease",
action: #selector(AppDelegate.decreaseCount),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Quit",
action: #selector(AppDelegate.quit),
keyEquivalent: "")
}
@objc func increaseCount() {
counter += 1
statusBarItem.button?.title = "🌰 \(counter)"
}
@objc func decreaseCount() {
counter -= 1
statusBarItem.button?.title = "🌰 \(counter)"
}
@objc func quit() {
NSApplication.shared.terminate(self)
}
}
运行它:
现在你应该能看到图标旁边的计数了。这很酷!但这不是 macOS 应用程序。我们不会告诉用户:"嘿,你需要安装开发工具,运行 swift run
等待构建,然后你就能看到它了"。
让我们手动创建一个应用程序。在创建应用程序之前,我们先来看看 macOS 中的一个概念,它是应用程序和系统中许多其他元素如何工作的关键。
macOS 捆绑文件
当你使用 "查找器 "查看应用程序、插件甚至 Xcode 项目时,你会将其视为一个单独的元素。实际上,所有这些元素都是具有特定结构的目录。Finder
知道如何将这些元素显示为一个图标。Finder
使用目录的名称来识别软件包的类型。例如,如果软件包的后缀名是 .app
,Finder
就会认为它是一个应用程序。这些软件包被称为 "软件包"。macOS 和 iOS 中的许多服务都知道如何与这些软件包交互,但有时它们需要额外的信息才能成功运行这些软件包。存储和访问捆绑包信息的最常见方式是通过 "PList"(属性列表)文件。
捆绑模式是一个非常聪明的模式,它将所有内容都包含在自己的空间内。
让我们来看看 macOS 中的应用程序捆绑包。
应用程序捆绑
在 macOS 中,如果你 "cd"(更改目录)到你的"/Applications "文件夹中安装的任何应用程序,你就能看到其结构。例如,让我们看看 Chess.app
文件结构:
Chess.app
└── Contents
├── Info.plist
├── MacOS
├── PkgInfo
├── Resources
├── _CodeSignature
└── version.plist
如您所见,App 内部有许多文件和资源。我们需要最简单的结构。让我们来看看创建应用程序的最低要求是什么。
最低限度
最基本的文件结构如下
Squirrel.app
└── Contents
├── Info.plist
└── MacOS
我们的可执行文件将位于 Contents/MacOS
中,仅此而已。我们已经有了之前创建的 Info.plist
。让我们用最新版本手动创建应用程序。
// First, let's create the containing directory
$ mkdir -p Squirrel.app/Contents/MacOS
// Now let's coppy our plist file
$ cp SupportFiles/Info.plist Squirrel.app/Contents/
// Copy our executable to the MacOS folder
$ cp .build/x86_64-apple-macosx/debug/Squirrel Squirrel.app/Contents/MacOS/
现在我们可以运行应用程序,看看一切是否正常。让我们先使用 Finder
运行它。
// you can open the current directory in Finder by using the open command
$ open .
你会看到我们的应用程序 Squirrel
,点击它就可以运行了:)。你可以跳过打开 Finder
,直接使用 open 命令打开我们的应用程序。
这样就能正确运行我们的应用程序了:)。
但是,如果你切换任务或查看 "Dock",你会看到 "Squirrel "的通用图标,我们不希望这样,所以让我们来解决这个问题。
代理
我们计划创建一个代理。代理 "不会在 "Dock "或任务切换器中显示为图标。解决这个问题很简单,我们只需告诉启动系统我们的应用程序是一个代理,然后在我们的 Info.plist
中设置属性即可。你可以看到,在 Plist
文件中设置应用程序的信息是多么方便。
将这两行添加到您的 SupportFiles/Info.plist
中
<key>LSUIElement</key>
<true/>
完整的 Info.plist
应该是这样的:
<?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>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>
再次复制到我们的 Squirrel.app/Contents/
。
$ cp SupportFiles/Info.plist Squirrel.app/Contents/
然后重新运行应用程序。现在,你的应用程序将像真正的 Agent 一样运行,不会在任务切换器或 Dock
上显示任何图标。
很好,但手动创建应用程序容易出错,而且重复性很高。我们应该将创建应用程序的过程自动化。您可以使用任何一种语言创建脚本,以自动执行这些步骤。下面我将向大家展示一个基本的 "Makefile "来自动完成这一过程。
Makefile
在你的项目目录中创建一个Makefile
,内容如下:
SUPPORTFILES=./SupportFiles/
PLATFORM=x86_64-apple-macosx
BUILD_DIRECTORY = ./.build/${PLATFORM}/debug
APP_DIRECTORY=./Squirrel.app
CFBUNDLEEXECUTABLE=Squirrel
install: build copySupportFiles
build:
swift build
copySupportFiles:
mkdir -p ${APP_DIRECTORY}/Contents/MacOS/ && \
cp ${SUPPORTFILES}/Info.plist ${APP_DIRECTORY}/Contents && \
cp ${BUILD_DIRECTORY}/${CFBUNDLEEXECUTABLE} ${APP_DIRECTORY}/Contents/MacOS/
run: install
open ${APP_DIRECTORY}
clean:
rm -rf .build
rm -rf ${APP_DIRECTORY}
.PHONY: run build copySupportFiles clean
现在您可以运行
// clean anything we created
$ make clean
// Build and create our application
$ make install
恭喜我们创建了一个代理应用程序!
最后的思考
我们学到了很多:
- 如何使用 Swift 包管理器创建可执行文件。
- 了解 macOS 应用程序调用 "AppDelegate "所需的引导(即 "RunLoop",但我认为你应该深入了解更多)。
- 如何创建基于代理的菜单栏应用程序。
- 了解 macOS 捆绑程序和应用程序捆绑程序的结构。
希望这些内容对你有所帮助。要创建一个可投入生产的应用程序,还有很多事情要做,尤其是当你需要对它进行公证时。不过,现在很多事情都应该更清楚了。
了解事物的工作原理是通往创造性解决方案和创新的良好途径。如果我们的知识很肤浅,没有真正了解事物的工作原理,就很难知道什么是可能的,什么是不可能的。
好了,今天就到这里,欢迎大家一如既往地提出反馈意见。如果你有任何问题、技巧或发现任何有趣的东西,请告诉我。
相关主题/兴趣点
- 苹果公司的 NSApplicationMain 文档
- Swift 属性文档
- 解释如何使用 NSApplicationMain 的文章
- Info.plist 键和值参考
- 苹果公司关于 RunLoop 的文档
- 命令行参数
- 我们本可以在构建选项中嵌入 Plist,参见以下文章 RedSweater 和 This gist
- MakeFiles 简介
- 使用故事板开发基于代理的应用程序
- macOS 打开命令示例
- 关于如何制作基于 Agent 的应用程序的 Stack overflow 答案
- 您可以从 GitHub Squirrel 下载我们创建的应用程序的代码。