drshapeless


Writing Swift in Emacs

Tags: macos | swift | emacs

Create: 2022-11-22, Update: 2022-11-24

I have been using Emacs for almost everything. I stubbornly want to use Emacs in some inappropriate situations, like writing iOS apps. This blog talks about how to write Swift within Emacs, and more importantly, SwiftUI, which is a lot more complicated.

Swift-mode

In Emacs, there is a swift-mode, which provides syntax highlighting and indentation.

Install it with straight.el. (I don't use use-package anymore.)

(straight-use-package 'swift-mode)
(require 'swift-mode)

Sourcekit-lsp

To have lsp support, I use eglot.

(add-to-list 'eglot-server-programs
             '(swift-mode . ("sourcekit-lsp")))

(add-hook 'swift-mode-hook #'eglot-ensure)

Format on save

I use apheleia to format any code in any languages. Unfortunately, it does not support Swift out-of-the-box. I have opened a pull request for add this features, however, the owner requires me to perform a test to install it on ubuntu. I have no way to do that other than compiling from source, because the debian repo has so little packages and so out-of-date.

In order to format code, we need to install swift-format. Interestingly, there are two formatter for Swift, one is swift-format, one is SwiftFormat. The first one is from Apple, the second one is from community. I do not know their difference, so I selected the official one because it is the only one in the AUR :P.

To install on Arch Linux.

paru -S swift-format

To install on macOS. Be careful, there are two packages of similar name, swift-format and swiftformat, we are using the first one.

brew install swift-format

By default, the swift-format uses a 2 space indentation. I personally do prefer the 4 space indentation for Swift.

(straight-use-package 'apheleia)
(require 'apheleia)

(add-to-list 'apheleia-mode-alist
             '(swift-mode . swift-format))

(add-to-list 'apheleia-formatters
             '(swift-format "swift-format" (buffer-file-name)))

(apheleia-global-mode 1)

In .swift-format, this is a json file. I changed the spaces into 4 from 2, the lineLength from 100 to 80. Put it into the project root directory.

{
  "fileScopedDeclarationPrivacy" : {
    "accessLevel" : "private"
  },
  "indentConditionalCompilationBlocks" : true,
  "indentSwitchCaseLabels" : false,
  "indentation" : {
    "spaces" : 4
  },
  "lineBreakAroundMultilineExpressionChainComponents" : false,
  "lineBreakBeforeControlFlowKeywords" : false,
  "lineBreakBeforeEachArgument" : false,
  "lineBreakBeforeEachGenericRequirement" : false,
  "lineLength" : 80,
  "maximumBlankLines" : 1,
  "prioritizeKeepingFunctionOutputTogether" : false,
  "respectsExistingLineBreaks" : true,
  "rules" : {
    "AllPublicDeclarationsHaveDocumentation" : false,
    "AlwaysUseLowerCamelCase" : true,
    "AmbiguousTrailingClosureOverload" : true,
    "BeginDocumentationCommentWithOneLineSummary" : false,
    "DoNotUseSemicolons" : true,
    "DontRepeatTypeInStaticProperties" : true,
    "FileScopedDeclarationPrivacy" : true,
    "FullyIndirectEnum" : true,
    "GroupNumericLiterals" : true,
    "IdentifiersMustBeASCII" : true,
    "NeverForceUnwrap" : false,
    "NeverUseForceTry" : false,
    "NeverUseImplicitlyUnwrappedOptionals" : false,
    "NoAccessLevelOnExtensionDeclaration" : true,
    "NoBlockComments" : true,
    "NoCasesWithOnlyFallthrough" : true,
    "NoEmptyTrailingClosureParentheses" : true,
    "NoLabelsInCasePatterns" : true,
    "NoLeadingUnderscores" : false,
    "NoParensAroundConditions" : true,
    "NoVoidReturnOnFunctionSignature" : true,
    "OneCasePerLine" : true,
    "OneVariableDeclarationPerLine" : true,
    "OnlyOneTrailingClosureArgument" : true,
    "OrderedImports" : true,
    "ReturnVoidInsteadOfEmptyTuple" : true,
    "UseEarlyExits" : false,
    "UseLetInEveryBoundCaseVariable" : true,
    "UseShorthandTypeNames" : true,
    "UseSingleLinePropertyGetter" : true,
    "UseSynthesizedInitializer" : true,
    "UseTripleSlashForDocumentationComments" : true,
    "UseWhereClausesInForLoops" : false,
    "ValidateDocumentationComments" : false
  },
  "tabWidth" : 8,
  "version" : 1
}

However, the indentation is a bit fuck up with standard Swift. When I first learn about Swift a few years ago, I was told that dot function should indent 2 space, and other things should indent 4 spaces. The worst part is that, the swift-mode in Emacs use the first style, and swift-format uses the second style. (See below.)

// This was when I prefers.
if foo {
    bar()
      .bello()
}

// This is what swift-format gives.
if foo {
    bar()
        .bello()
}

UPDATE: 2024-01-04

After watching some videos and reading some code of SwiftUI lately, I discovered that everyone is just using an indentation of 4 everywhere, the two spaces indentation is just a myth in the past. Just keep everything 4-space indented.

Creating SwiftUI project

All the above steps are just for editing code. Just like every other modern languages, Swift has its own toolchain for creating project.

We have two approaches to achieve this goal.

Using xcode GUI

This is probably the easiest way to create a Swift project. Just open xcode, click click click and that's it. After you have created the project, you can use whatever editor you like to edit the code.

Using the command line

As a hardcore programmer, I would like to automate everything via a script, (or even better, with an Emacs keybinding).

The swift package can be used to create a project. The --type flag is necessary, otherwise, you are just creating a library.

swift package init --type executable

Build the project. Because of the stupidity of the sourcekit-lsp, we need to build the project in order to index the symbols for lsp auto-completion. When a big chunk of code is added, build it afterwards.

swift build

To test the project, run it.

swift run

Using xcode with Apple Script

There are two ways of doing this. One is running xcode in the background, and use Apple Script to tell xcode to do things. Another is to use the Swift toolchain, which is purely command line utility. I lean towards the latter one, as xcode takes up a lot of resources. (It actually does not affect performance even if I keep xcode running 24/7 on my M1 machine, not even the battery life.)

Lets take a look at how to use Apple Script with xcode.

When you google "swift emacs", it shows a very great blog post, Using Emacs for Swift development. This blog shows the template of interacting with xcode via osascript. Here are some of the snippets.

(defun xcode-build()
  (interactive)
  (shell-command-to-string
    "osascript -e 'tell application \"Xcode\"' -e 'set targetProject to active workspace document' -e 'build targetProject' -e 'end tell'"))
(defun xcode-run()
  (interactive)
  (shell-command-to-string
    "osascript -e 'tell application \"Xcode\"' -e 'set targetProject to active workspace document' -e 'stop targetProject' -e 'run targetProject' -e 'end tell'"))
(defun xcode-test()
  (interactive)
  (shell-command-to-string
    "osascript -e 'tell application \"Xcode\"' -e 'set targetProject to active workspace document' -e 'stop targetProject' -e 'test targetProject' -e 'end tell'"))

Conclusion

To be honest, if you want a braindead simple way to develop iOS apps, you should use xcode. If you really want to develop mobile apps with your beloved text editor, you should use flutter. However, the temptation of writing native SwiftUI apps and using Emacs have led me to a non-stop journey of Googling.