macOS   SwiftUIプログラミング   初めの一歩

ホーム

【お知らせ】
SwiftUIで作った macOS Todo アプリ ToDone を100ダウンロードまで無料にしました。マニュアルページは、ToDone サポートページ です。

【本文】
コーディングだけで macOS アプリが作れたらな、と思っていましたが、3年前から SwiftUI を使って、コーディングだけで macOS アプリが作れるようになっていると知り、さっそく始めてみることにしました。

このコーナーでは、任意のテキストエディタでコードを記述し、 ターミナルを使ってビルドする方法で作業を進めています。Xcode で作業を進める場合は、Xcodeで作業する場合 をご一読ください。

なお、ターミナルで作業を進める場合も、Swift コンパイラや SwiftUI フレームワークなどを Mac にインストールするために Xcode をインストールして、一度起動させなければなりません。インストールが終われば Xcodeは終了しても大丈夫です。

また、作成したアプリケーションを App Storeに提出するためのファイルにするには、Xcode を使わなければならなかったかもしれません。どこかで Xcode を使わずに作る方法を見たような気もするのですが...

私の開発環境は次のとおりです。

  • MacBook Air 2018年モデル、メモリ8G
  • macOS Monterey 12.4
  • Xcode 13.4.1
  • Swift 5.6.1

更新履歴
2022/07/06
Xcodeで作業する場合」を別のページに分割しました。
2022/07/09
行単位の詳しいコード説明をつけました。
2022/07/11
AppDelegate を使わずに、最後のウィンドウを閉じたらアプリケーションが終了するようにする」を追加しました。
2022/07/12
AppDelegate を使わずに、最後のウィンドウを閉じたらアプリケーションが終了するようにする」にコード説明をつけました。

初めの一歩

任意のテキストエディタで次の二つのファイルを作成します。

App.swift

以前は Main.swift というファイル名でしたが、App.swift で統一することにしました。なお、ファイル名は自由ですが、大文字で始めなければなりません。Swift ファイルの拡張子は .swift です。Xcode で作業をする場合は、プロダクト名App(プロダクト名App.swift)に次のコードを記述してください。


import SwiftUI

// SwiftUI のプログラムは @main と書かれた所から始まる決まりになっています。
@main
struct FooApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

// 次のクラスは、アプリケーションのウィンドウが閉じたら、
// アプリケーションも終了するようにするためのコードです。
// macOS のデフォルトではウィドウを閉じてもアプリケーションは終了しません。
class AppDelegate: NSObject, NSApplicationDelegate {

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
   }
}
	

コード説明

  1. import SwiftUI
  2. SwiftUI に定義されている各種構造体や型や関数。そして SwiftUI プログラミングに必要なその他の定義や設定を読み込んでいます。これがテキストファイル形式なのか、すでにバイナリ形式になっているかは分かりません。ファイルのある場所は処理系(コンパイラやリンカ)がすでに知っていることになっています。
  3. @main
    SwiftUI のプログラムは、この記述の位置から始まる決まりになっています。
  4. struct FooApp: App {
    App プロトコル(protocol、議定書)を採用する FooApp という構造体(struct)を定義しています。プロトコルとはそれを採用すると、そのプロトコルに定義されている、すべてのプロパティとメソッドを実装しなければならないことになっています。プロトコルを採用することによって、その構造体の役目がはっきりとすることになります。
  5. @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    Apple はアプリケーションのライフサイクル(起動から終了までの各種イベントを拾う仕組み)をAppDelegate というものに任せてきました。delegate(デリゲート、代理人)は、そこに定義されているメソッドの必要なものだけを実装すれば良い仕組みです。
    しかし、最近になってライフサイクルを App プロトコルに任せようという動きになってきています。私が使っている Xcode 13.4.1 では、ライフサイクルに何を使うかを選択できる項目はすでになく、自動的に App プロトコルを使わなければならない仕組みになっています。このように App ライフサイクルしか選択できない場合に、このコードを記述すれば、AppDelegate も併用することができるようになります。
  6. var body: some Scene {
    App プロトコルに定義されているのは body プロパティだけです。some Scene は、「Scene プロトコルを採用した何か」という意味になるそうで、必ず一つの型だけを指定しているわけではないみたいです。Scene は2020年に新しく導入された概念で、 iPhone や iWatch などでは、この Sceneがデバイスの画面のすべてを支配し、macOS や iPad では一つのウィンドウに一つの Scene が割り当てられるとのことらしいです。
  7. WindowGroup {
    Scene の次に WindowGroup というのを設定します。一見この部分は省略して、すぐに ContentView() を記述できそうですが、実際には省略できないみたいです。ここまでのコードで、ウィンドウグループシーンというのを設定したことになり、ウィンドウグループシーンの実際の取り扱いは、iOS、iWatch、iPad、macOS で変わることになります。macOSでは、ここまでの一連のコードで、アプリケーションに新規メニューが付いたり、複数のウィンドウをタブとして表示できるらしいです。
  8. ContentView()
    ここでは前述の WindowGroup に、次の Sub.swift で定義する ContentView を1つだけ設定しています。ここに ContentView() を2つ記述すれば、macOS の場合は、ウィンドウが2つ表示されそうですが、実際には ContentView の内容を2つ持つ、1つのウィンドウが表示されます。あくまでも Scene から続く一連のコードで一つのウィンドウということみたいです。
  9. some Scene から続くコードを長々と説明しましたが、要するに some Scene から ContentView() まで一の連のコードが画面の表示やウィンドウの表示を担っている。どのように表示されるかは、プラットフォーム(platfome、OS)によって違うということみたいです。
  10. class AppDelegate: NSObject, NSApplicationDelegate {
    アプリケーションのライフサイクルを管理するために AppDelegate クラスを作っています。Swift では、クラスに特に継承するスーパークラスがない場合は NSObject を継承する決まりになっています。次の NSApplecationDelegate は先ほど説明したプロトコルです。
  11. func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    NSApplecationDelegate で定義されているメソッドの一つです。このメソッドで true を返せば、アプリケーションの最後のウィンドウを閉じた時にアプリケーションが終了するようになります。App ライフサイクルだけでは、ウィンドウを閉じてもアプリケーションを終了することはまだできないみたいです。


View.swift

以前は Sub.swift というファイル名でしたが、View.swift というファイル名で統一することにしました。なお、ファイル名は自由ですが、大文字で始めなければなりません。Swift ファイルの拡張子は .swift です。Xcode で作業を進める場合は、ContentView(ContentView.swift)に次のコードを記述してください。


import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
            .font(.largeTitle)
            .fontWeight(.thin)
        }
        .frame(minWidth: 300, maxWidth:.infinity, minHeight: 200, maxHeight: .infinity)
    }
}
	

コード説明

  1. struct ContentView: View {
    View プロトコルを採用した ContentView 構造体(struct)を定義しています。
  2. var body: some View {
    View プロトコルで定義しなければいけないのは、body プロパティだけです。some View は「View プロトコルを採用した何か」という意味なり、何型であるかをはっきりさせる必要がなくなります。
  3. VStack {
    VStack はコンテナー(container)と呼ばれる、他の View をその中配置できる View です。この中に10個までの子 View を縦一列に配置できます。もともと bar に設定できるのは一つの View だけです。まずコンテナーを一つ設定すれば、その中に子 View を複数配置できるようになります。
  4. Text("Hello, world!")
    Text 構造体は、import SwiftUI で読み込んだファイルの中に定義されている View プロトコルを採用した構造体です。引数で指定されている文字列を表示します。
  5. .font(.largeTitle).fontWeight(.thin)
  6. SwiftUI のそれぞれの View はモディファイア(modifier、修飾するもの、変更するもの)というものを持っており.(ドット)を繋いて呼び出していくことができます。
    .font で文字の大きさを決めて .fontWeight で文字の太さを決めています。
  7. .frame(minWidth: 300, maxWidth:.infinity, minHeight: 200, maxHeight: .infinity)
  8. ここでは VStack の frame モディファイアを呼び出して、縦横最小サイズと縦横最大サイズを指定しています。.infinity は無限大を表しています。
    もともとこのサンプルでは、実際に表示される View は Text("Hello, world!")だけです。一つだけなので、body プロパティにこれだけを設定すれば良いのですが、それだとウィンドウの大きさが文字列の大きさになってしまいます。なので VStack で囲んで、その VStack のサイズを設定することによって、アプリケーションウィンドウの大きさを決めています。


ビルド

ターミナルを起動して、前述の2つのファイルがあるディレクトリに移動します。


// コンパイル
swiftc Appn.swift View.swift -o foo
// アプリケーションバンドルの作成
mkdir Foo.app
// ビルドファイルをアプリケーションバンドルへ移動
mv foo Foo.app
	


Xcode で作業を進めている場合は、Product メニューの Run をクリックするか、 Xcode の左上の右三角 ▶︎ をクリックします。

実行

作成した Foo アプリ(アプリケーションバンドル)をダブルクリックします。
Xcode で作業を進めている場合は何もする必要はありません。 しばらく待つとアプリケーションが起動します。

  1. ウィンドウはリサイズ・最大化・最小化ができます。
  2. 簡易的な方法で作りましたので アプリケーションバンドルの名前がアプリケーションの名前になります。
  3. Xcode で作業を進めている場合は、 プロダクト名がアプリケーションの名前になります。
  4. Quit メニューだけでなく、ウィンドウを閉じても、 アプリケーションが終了します。


AppDelegate を使わずに、最後のウィンドウを閉じたら アプリケーションが終了するようにする

Apple が AppDelegate から App へ移行しようとしているのだから、AppDelegate を使わない方法を考えてみました。@ObservedObject というバインディングの技術を使っています。

App.swift

以前は、Main.swift というファイル名でしたが、App.swift というファイル名で統一することにしました。Xcode で作業している場合は、プロダクト名App(プロダクト名App.swift)に次のコードを記述してください。


/************************************
    App.swift
    copyright    :   vivacocoa.jp
    last modified:   Jul. 12, 2022
************************************/

import SwiftUI

class Count: ObservableObject {
    @Published var count = 0
}

@main
struct FooApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
    

コード説明

  1. class Count: ObservableObject {
    View 間でデータを共有するためのクラスを定義しています。ObsevableObject はプロトコルになります。Google で翻訳すると「観察可能なオブジェクト」となりました。SwiftUI の class には、NSObject などのスーパークラスの記述は必要ないみたいです。逆に記述しても問題ありません。また、このクラスは必ずここに記述しなければならないということはなくて、例えば、View.swift の import から struct ContentView の間に記述しても問題ありません。
  2. @Published var count = 0
    Published 属性を付けて count プロパティを宣言しています。published をグーグルで翻訳すると「公開済み」になりました。いろいろなドキュメントには、データを共有するプロパティには @Published 属性を付けなければならないとなっていますが、実際には付けなくても問題なく動作します。SwiftUI はどんどん進化しているのでしょう。


View.swift

以前は Sub.swift というファイル名でしたが、View.swift というファイル名で統一することにしました。Xcode で作業している場合は、ContentView(ContentView.swift)に次のコードを記述してください。


/************************************
    View.swift
    copyright    :   vivacocoa.jp
    last modified:   Jul. 12, 2022
************************************/

import SwiftUI

struct ContentView: View {
    @ObservedObject var count = Count()
    var body: some View {
        VStack {
            Text("Hello, world!")
            .font(.largeTitle)
            .fontWeight(.thin)
        }
        .frame(minWidth: 300, maxWidth:.infinity, minHeight: 200, maxHeight: .infinity)
        .onAppear {
            count.count += 1
        }
        .onDisappear{
            count.count -= 1
            if count.count < 1 {
                NSApplication.shared.terminate(self)
            }
        }
    }
}
    
h3>コード説明
  1. @ObservedObject var count = Count()
    @ObservedObject 属性を付けて count プロパティを宣言して、Count クラスのインスタンスを代入しています。Observed object をグーグルで翻訳すると「観察対象」になりました。ここで大事なことは、View 構造体(ContentView)の中だけでデータをバインディングさせる場合は @State 属性を使い、違う View 構造体の間でデータをバインディング(共有といっても良いかも知れません)する場合は、@ObservedObject を使うということです。
    「あれ? 1つの ContentView じゃないの?」と思われた方もいると思います。しかし、New メニューアイテムを選択すると、この ContentView が複数作られるのです。
  2. .onAppear {
    「現れる時・表示する時」という意味になると思います。通常 GUI ではウィンドウが作成されるのと実際に表示されるのは別のイベントです。しかし、ここでは、表示 = 作成ぐらいで考えて大丈夫みたいです。
  3. count.count += 1
    Count クラスの count プロパティの値を1つ増やしています。
  4. .onDisappear{
    「消える時・非表示になる時」という意味になると思います。通常 GUI ではウィンドウが非表示になるのとウィンドウが実際になくなるのは別のイベントです。しかし、ここでは、非表示 = 破棄ぐらいで考えて大丈夫みたいです。
  5. count.count -= 1
    まず Count クラスの count プロパティの値を1つ減らします。
  6. if count.count < 1 {
    前の行で、まずカウントを減らしてから、if 文で、count プロパティの値が 0 以下かどうか調べています。
  7. NSApplication.shared.terminate(self)
  8. アプリケーションを終了するコードです。しかしこのコードは、Objective-C の頃から使われてきたコードです。もしかするともっと新しいコードがあるのかも知れません。
  9. 最後に、Swift のクラスのインスタンスは参照型として渡されます。参照型ということは、通常は、参照先(参照元?)のメモリーは自動的に解放されません。しかし、SwiftUI では、明示的にメモリーを解放するコードはないみたいです。Swift のように nil を代入するという手段も使えないみたいです。ARC(Auto Reference Counting)でメモリ管理されていることを期待します。




5971 visits
Posted: Jun. 22, 2022
Update: Jul. 12, 2022

ホーム   目次