🦋

WindowsアプリをSwiftで作成する。

2022/11/30に公開

前回はこちらです。

方法に関して

Windows向けのSwiftツールチェーンがダウンロード可能になったことによりツールチェーンを使って様々な方法でアプリを作成することができると思います。

自分の場合はGUIが必要でしたので方法として情報の多いCMakeとninjaを使った方法で作成していくことにしました。

今回はCMakeとninjaを使った方法でのアプリのビルドを行います。
自身はCMake初心者なので設定内容を詳しく把握しておりません。そのため実行は自己責任でお願いします。基本的にはこちらのリポジトリの記述内容を参考にしています。

最低限の「Swiftファイルのコンパイルと実行ファイルの作成」のみを行いたい方はこちらの動画を参考にしていただきたいです。

環境

OS: Windows 11 Version 22H2
Tool Chain: Swift 5.7
Editor: Visual Studio Code(以下VSCode)

前回からWindowsバージョンを11に上げましたが準備としては前回と同じようにできます。

リポジトリのクローン

今回CMakeとninjaを使った方法でのアプリのビルドを行うにあたり、GUIを簡単に作成できる下記のリポジトリを利用することにしました。
https://github.com/compnerd/swift-win32

こちらのreadme通りに進めれば既に準備を行っている環境でしたら問題なくUICatalog.exeが生成され、実行もできます。
readmeの翻訳では少しつまらないので、新規でCMakeListにビルド対象をクローンしたリポジトリ内に加えてビルドを行おうと思います。

お好きなフォルダにクローンができたらVSCodeで「swift-win32」フォルダを開きます。

新規でCMakeListにビルド対象をクローンしたリポジトリ内に加える

VSCodeで「swift-win32/Examples/」を開きます。この配下に作りたいアプリ名でフォルダを追加します。ここではDemoAppという名前で作成していきます。

このフォルダ内に追加するファイルはCMakeLists.txt, Info.plist, DemoApp.exe.manifest, DemoApp.swift4つです。

CMakeLists.txt

CMakeLists.txt
add_executable(DemoApp
  DemoApp.swift)
add_custom_command(TARGET DemoApp POST_BUILD
  COMMAND
    mt -nologo -manifest ${CMAKE_CURRENT_SOURCE_DIR}/DemoApp.exe.manifest -outputresource:$<TARGET_FILE:DemoApp>
  COMMAND
    ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist $<TARGET_FILE_DIR:DemoApp>)
# FIXME(SR-12683) `@main` requires `-parse-as-library`
target_compile_options(DemoApp PRIVATE
  -parse-as-library)
target_link_libraries(DemoApp PRIVATE
  SwiftWin32)

Info.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>ApplicationSceneManifest</key>
  <dict>
    <key>ApplicationSupportsMultipleScenes</key>
    <false/>
    <key>SceneConfigurations</key>
    <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>SceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>SceneDelegateClassName</key>
        <string>DemoApp.DemoApp</string>
      </dict>
    </array>
    </dict>
  </dict>
</dict>
</plist>

DemoApp.exe.manifest

DemoApp.exe.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<asmv1:assembly
  manifestVersion="1.0"
  xmlns:asmv1="urn:schemas-microsoft-com:asm.v1"
  xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">

  <!-- assembly identity -->
  <asmv1:assemblyIdentity
    name="org.compnerd.swift-win32.DemoApp"
    processorArchitecture="*"
    type="win32"
    version="1.0.0.0"/>

  <!-- application specific settings -->
  <asmv3:application xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
    <windowsSettings>
      <dpiAwareness xmlns="">PerMonitorV2</dpiAwareness>
      <dpiAware>true</dpiAware>
    </windowsSettings>
  </asmv3:application>

  <asmv1:description>DemoApp</asmv1:description>

  <!-- Common Control Support -->
  <asmv1:dependency>
    <asmv1:dependentAssembly>
      <asmv1:assemblyIdentity
        language="*"
        name="Microsoft.Windows.Common-Controls"
        processorArchitecture="*"
        publicKeyToken="6595b64144ccf1df"
        type="win32"
        version="6.0.0.0"
      />
    </asmv1:dependentAssembly>
  </asmv1:dependency>
</asmv1:assembly>

DemoApp.swift

DemoApp.swift
import SwiftWin32
import Foundation

@main
final class DemoApp: ApplicationDelegate, SceneDelegate {
    var window: Window!

    lazy var label = Label(frame: Rect(x: 150.0, y: 220.0, width: 400.0, height: 80.0))
    func scene(_ scene: Scene, willConnectTo session: SceneSession, options: Scene.ConnectionOptions) {
        guard let windowScene = scene as? WindowScene else { return }

        let size: Size = Size(width: 500, height: 500)
        windowScene.sizeRestrictions?.minimumSize = size
        windowScene.sizeRestrictions?.maximumSize = size

        self.window = Window(windowScene: windowScene)

        window.rootViewController = ViewController()
        window.rootViewController?.title = "Demo App"

        window.addSubview(self.label)
        self.label.text = "Hello, World"
        if let consolasFont = Font(name: "Consolas", size: 24) {
            self.label.font = consolasFont
        } else {
            self.label.font = Font(name: "Cascadia Code", size: 24)
        }

        window.makeKeyAndVisible()
    }

    func sceneDidBecomeActive(_: Scene) {
        print("Good morning!")
    }

    func sceneWillResignActive(_: Scene) {
        print("Good night!")
    }

    func applicationWillTerminate(_: Application) {
        print("Goodbye cruel world!")
    }
}

swift-win32/Examples/CMakeLists.txt

「swift-win32/Examples/CMakeLists.txt」に下記を追加します。

swift-win32/Examples/CMakeLists.txt
add_subdirectory(Calculator)
add_subdirectory(UICatalog)
+ add_subdirectory(DemoApp)

ビルドする

スタートから「x64 Native Tools Command Prompt for VS 2022」を探し出しクリックで実行します。
ウィンドウが開いたらcdコマンドで「swift-win32」配下まで移動します。

CMakeのビルド

cmake -B build -D BUILD_SHARED_LIBS=YES -D CMAKE_BUILD_TYPE=Release -D CMAKE_Swift_FLAGS="-sdk %SDKROOT%" -G Ninja -S .

「Build files have been written to:」でビルドした内容が格納されたパスが表示されたら成功です。

Ninjaのビルド

ninja -C build SwiftWin32 DemoApp

作成中からコマンド入力可能状態になればビルドは完了です。もしエラーが出た場合はその指示に従ってファイルを修正します。

アプリの起動

CMakeのビルド時に「Build files have been written to:」で示されたパス内のbinの中に.exeがついた実行ファイル---ここではDemoApp.exe---が格納されているのでダブルクリックします。

中心あたりに「Hello, World」とあるウィンドウが開けば成功です。

次回はswift-win32で実現されているUIパーツについて書く予定です。

Discussion