1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[SwiftUI]月旅行計画

Last updated at Posted at 2022-01-12

Mac OS Xが発表された際に、開発者を増やすという目的だと思うが、O'Reillyから『入門Carbon』と『入門Cocoa』という書籍が出版された。今回のCocoa練習帳では、『入門Carbon』のサンプル・アプリケーションをSwiftUIで実装することに挑戦した。

Xcodeで、macOSアプリケーションのSwiftUIプロジェクトを生成する。

新規プロジェクト.png

以下のCarbonで実装された初期のMacOS Xのアプリの画面をSwiftUIで実装する。

月旅行計画.png

UI部品が縦に並んでいるので、ContentView.swift に縦で部品を配置するに記述する。

struct ContentView: View {
    var body: some View {
        VStack {
        }
        .padding(20.0)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

月旅行計画の絵を参考にVStack内に部品を配置する。最初は画像。

            Image("Moon")

次にラジオ・ボタンを配置するが、SwiftUIのPickerだとラベルの位置が自由にならないので、ラベルはテキストで、その下に、ラベルなしのPickerを配置する。

            Text("Mode of Transportation")
 
            Picker(selection: $selected, label: Text("Mode of Transportation")) {
                            Text("Foot").tag(1)
                            Text("Car").tag(2)
                            Text("Commercial Jet").tag(3)
                            Text("Apollo Spacecraft").tag(4)
                        }
                        .pickerStyle(.radioGroup)
                        .labelsHidden()

次に計算をするボタンを。まだ、中身は空で。

            Button("Compute Travel Time") {
            }

その下に、計算結果を。Carbonではラベルとテキスト・フィールドだったが、Textとした。

            HStack {
                Text("Travel Time in Days:")
                
                Text("\(text)")
            }

最後は終了ボタン。

            HStack {
                Spacer()
                
                Button("Quit") {
                    NSApplication.shared.terminate(self)
                }
            }

計算のコードは簡単なものなので細かくは説明しないが、Pickerの選択結果と計算結果を@Stateの変数に格納している。

    private let kMTPHoursPerDay: Double = 24.0
    private let kMTPDistanceToMoon: Double = 384467.0
    private let kMTPFootMode: Int = 1
    private let kMTPCarMode: Int = 2
    private let kMTPCommercialJetMode: Int = 3
    private let kMTPApolloSpacecraftMode: Int = 4
    @State private var selected = 1
    @State private var text: String = ""
            Button("Compute Travel Time") {
                var travelTime: Double = 0.0
                switch (selected) {
                case kMTPFootMode:
                    travelTime = (kMTPDistanceToMoon / (4.0 / 0.62)) / kMTPHoursPerDay
                case kMTPCarMode:
                    travelTime = (kMTPDistanceToMoon / (70.0 / 0.62)) / kMTPHoursPerDay
                case kMTPCommercialJetMode:
                    travelTime = (kMTPDistanceToMoon / (600.0 / 0.62)) / kMTPHoursPerDay
                case kMTPApolloSpacecraftMode:
                    travelTime = 4.0
                default:
                    travelTime = 0.0
                }
                text = String(format: "%2.1lf", travelTime)
            }

このままだとウィンドウサイズが可変となってしまうので、固定サイズになるよう、コンテンツより大きな幅と高さの最小値を設定する。

    var body: some View {
        :
        VStack {
            :
            HStack {
                :
            }
            .padding(20.0)
            .frame(minWidth: 342.0, minHeight: 50)
            
            HStack {
                :
            }
            .padding(20.0)
            .frame(minWidth: 342.0, minHeight: 25)
        }
        .padding(20.0)
        .frame(minWidth: 342.0, minHeight: 512.0)
    }

VStackの中にHStackを配置した場合、HStackの幅を設定しないと、ウィンドウの横幅が可変となってしまう。

次はウィンドウのCloseとZoomのボタンを無効にする。

struct ContentView: View {
    :
    var body: some View {
        HostingWindowFinder { window in
            guard let w = window else { return }
            w.standardWindowButton(.zoomButton)?.isEnabled = false
            w.standardWindowButton(.closeButton)?.isEnabled = false
            w.styleMask = w.styleMask.subtracting(.resizable)
        }
        VStack {
            :
        }
        .padding(20.0)
        .frame(minWidth: 342.0, minHeight: 512.0)
    }
}
 
struct HostingWindowFinder: NSViewRepresentable {
    var callback: (NSWindow?) -> ()
    func makeNSView(context: Self.Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {}
}

最終的な内容は以下となる。

struct ContentView: View {
    private let kMTPHoursPerDay: Double = 24.0
    private let kMTPDistanceToMoon: Double = 384467.0
    private let kMTPFootMode: Int = 1
    private let kMTPCarMode: Int = 2
    private let kMTPCommercialJetMode: Int = 3
    private let kMTPApolloSpacecraftMode: Int = 4
    @State private var selected = 1
    @State private var text: String = ""
    var body: some View {
        HostingWindowFinder { window in
            guard let w = window else { return }
            w.standardWindowButton(.zoomButton)?.isEnabled = false
            w.standardWindowButton(.closeButton)?.isEnabled = false
            w.styleMask = w.styleMask.subtracting(.resizable)
        }
        VStack {
            Image("Moon")
            
            Text("Mode of Transportation")
            
            Picker(selection: $selected, label: Text("Mode of Transportation")) {
                            Text("Foot").tag(1)
                            Text("Car").tag(2)
                            Text("Commercial Jet").tag(3)
                            Text("Apollo Spacecraft").tag(4)
                        }
                        .pickerStyle(.radioGroup)
                        .labelsHidden()
            
            Button("Compute Travel Time") {
                var travelTime: Double = 0.0
                switch (selected) {
                case kMTPFootMode:
                    travelTime = (kMTPDistanceToMoon / (4.0 / 0.62)) / kMTPHoursPerDay
                case kMTPCarMode:
                    travelTime = (kMTPDistanceToMoon / (70.0 / 0.62)) / kMTPHoursPerDay
                case kMTPCommercialJetMode:
                    travelTime = (kMTPDistanceToMoon / (600.0 / 0.62)) / kMTPHoursPerDay
                case kMTPApolloSpacecraftMode:
                    travelTime = 4.0
                default:
                    travelTime = 0.0
                }
                text = String(format: "%2.1lf", travelTime)
            }
            
            HStack {
                Text("Travel Time in Days:")
                
                Text("\(text)")
            }
            .padding(20.0)
            .frame(minWidth: 342.0, minHeight: 50)
            
            HStack {
                Spacer()
                
                Button("Quit") {
                    NSApplication.shared.terminate(self)
                }
            }
            .padding(20.0)
            .frame(minWidth: 342.0, minHeight: 25)
        }
        .padding(20.0)
        .frame(minWidth: 342.0, minHeight: 512.0)
    }
}
 
struct HostingWindowFinder: NSViewRepresentable {
    var callback: (NSWindow?) -> ()
    func makeNSView(context: Self.Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {}
}

このままだと、デフォルトで用意されている、このアプリで不要なメニューが表示されるので、NSApplicationDelegateでデフォルトのメニュー項目を非表示とした。

Appクラスにデリゲートを宣言して。

@main
struct MoonTravelPlannerApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

AppDelegate.swift の内容は以下となる。

import SwiftUI
 
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        NSApplication.shared.mainMenu?.items[1].isHidden = true
        NSApplication.shared.mainMenu?.items[2].isHidden = true
        NSApplication.shared.mainMenu?.items[3].isHidden = true
        NSApplication.shared.mainMenu?.items[4].isHidden = true
        NSApplication.shared.mainMenu?.items[5].isHidden = true
        NSApplication.shared.mainMenu?.items[6].isHidden = true
    }
}

実行結果。

アプリ.png

【関連情報】
SwiftUI 2.0 disable window's zoom button on macOS
入門Carbon
Cocoa.swift
Cocoa勉強会 関東
Cocoa練習帳

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?