[macOS翻译]通过构建基于代理(菜单栏)的应用程序了解 macOS 应用程序的几个概念

30 阅读10分钟

本文由 简悦 SimpRead转码, 原文地址 rderik.com

随着我们用来开发软件的框架和工具的进步,创建一个新的应用程序似乎就像......。

随着我们用来开发软件的框架和工具的进步,创建一个新的应用程序似乎就像变魔术一样。我们只需点击几个按钮,一切就会为我们创造出来。我喜欢魔术,但我认为有时我们最终只能成为框架的 "使用者",而无法真正理解发生了什么。在本篇文章中,我将解释 macOS 应用程序如何工作的几个概念,希望我们能更好地理解这个生态系统。

我们不会使用 Xcode,所以请拿起你最喜欢的文本编辑器,让我们开始吧。要讲的内容很多,我们会讲得很快。如果您有任何问题,可以在 Reddit /u/rcderiktwitter @rderikemail 上给我发消息。

你可以从 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 使用目录的名称来识别软件包的类型。例如,如果软件包的后缀名是 .appFinder 就会认为它是一个应用程序。这些软件包被称为 "软件包"。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 捆绑程序和应用程序捆绑程序的结构。

希望这些内容对你有所帮助。要创建一个可投入生产的应用程序,还有很多事情要做,尤其是当你需要对它进行公证时。不过,现在很多事情都应该更清楚了。

了解事物的工作原理是通往创造性解决方案和创新的良好途径。如果我们的知识很肤浅,没有真正了解事物的工作原理,就很难知道什么是可能的,什么是不可能的。

好了,今天就到这里,欢迎大家一如既往地提出反馈意见。如果你有任何问题、技巧或发现任何有趣的东西,请告诉我。

相关主题/兴趣点


www.deepl.com 翻译