トップ 最新 追記

Cocoa練習帳

iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど

2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|

2022-01-12 [SwiftUI]月旅行計画

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

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

新規プロジェクト

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

月旅行計画

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
    }
}

実行結果。

アプリ

トップ 最新 追記