トップ 追記

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|03|04|05|06|07|08|09|10|11|12|
2025|01|02|03|

2025-03-14 [Node.js] Docker環境構築

_ Dockerとは

  • アプリケーションの構築や共有、実行のためのコンテナ基盤管理

_ インストール

以下の公式サイトからDocker Desktopをダウンロードし、手順にしたがってインストールします。

_ 簡単な動作確認

$ docker version
Client:
 Version:           27.5.1
 API version:       1.47
 Go version:        go1.22.11
 Git commit:        9f9e405
 Built:             Wed Jan 22 13:37:19 2025
 OS/Arch:           darwin/arm64
 Context:           desktop-linux

_ Server: Docker Desktop 4.38.0 (181591)

Engine: Version: 27.5.1 API version: 1.47 (minimum version 1.24) Go version: go1.22.11 Git commit: 4c9b3b0 Built: Wed Jan 22 13:41:25 2025 OS/Arch: linux/arm64 Experimental: false containerd: Version: 1.7.25 GitCommit: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb runc: Version: 1.1.12 GitCommit: v1.1.12-0-g51d5e946 docker-init: Version: 0.19.0 GitCommit: de40ad0

インストールできていることが確認できた。


2025-02-09 [Node.js] Node.js環境構築

_ Node.jsとは

  • Webブラウザに組み込まれているJavScript実行環境を取り出して利用できるようにしたもの

_ インストール

macOSでHomebrewを使ってのインストールの手順を示します。

# nodebrewのインストール
$ brew install nodebrew
$ nodebrew setup
# .zprofile または .zshrc または .zshenv に設定を追加
export PATH=$HOME/.nodebrew/current/bin:$PATH
# 最新の安定版のNode.jsをインストール
$ nodebrew install-binary stable
# インストールされているNode.jsを確認する
$ nodebrew ls
# 使用するNode.jsのバージョンを設定
$ nodebrew use v22.14.0
# Node.jsのバージョンを確認する
$ node -v
v22.14.0

_ 簡単な動作確認

$ node
Welcome to Node.js v22.14.0.
Type ".help" for more information.
> 24 * 60 * 60
86400
> console.log(24 * 60 * 60)
86400
undefined

Ctrl + Dで終了。


2025-01-01 [Flutter] Flutter開発環境

_ Flutterとは

  • 同一のコードベースからAndroidやiOS、macOSなど向けのアプリケーションを生成するSDK
  • Dart言語で開発する

_ 開発環境の構築

macOSでの開発環境の構築の手順を示します。

# Rosetta 2のインストール
$ sudo softwareupdate --install-rosetta --agree-to-license
# Homebrewのインストール
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# .zprofile または .zshrc または .zshenv に設定を追加
eval "$(/opt/homebrew/bin/brew shellenv)"
# rbenvのインストール
$ brew install rbenv
# .zprofile または .zshrc または .zshenv に設定を追加
if which rbenv > /dev/null; then
        eval "$(rbenv init -)"
fi
# Ruby 3.3.6をインストールして選択
$ rbenv install 3.3.6
$ rbenv global 3.3.6
# Bundlerのインストール
$ rbenv exec gem install bundler
$ rbenv rehash
# .zprofile または .zshrc または .zshenv に設定を追加
export PATH=$HOME/.gem/bin:$PATH
# CocoaPodsのインストール
$ sudo gem install cocoapods
$ pod setup
Visual Studio Codeをダウンロード
- https://code.visualstudio.com/download
Xcodeを入手
- https://developer.apple.com/jp/xcode/ Command Line Toolsもインストールする # Xcodeの選択 $ sudo sh -c 'xcode-select -s /Applications/Xcode.app/Contents/Developer && xcodebuild -runFirstLaunch' # ライセンスを確認 $ sudo xcodebuild -license
Android Studioを入手
- https://developer.android.com/studio/install?hl=ja Android SDK Command-line Toolsなどのインストールが必要。
Flutter SDKをインストール
- https://docs.flutter.dev/get-started/install Flutter SDKは公式サイトで説明されている ~/development ディレクトリの配置する。 unzip ~/Downloads/flutter_macos_*-stable.zip -d ~/development/ # .zprofile または .zshrc または .zshenv に設定を追加 export PATH=$HOME/development/flutter/bin:$PATH
# Flutter doctorを実行して確認する
$ flutter doctor

エラーになった項目の対応を行う。

2024-12-04 [php][laravel] laravel入門

_ Laravelとは

  • PHPで実装されてWebフレームワーク
  • PHPのライブラリ管理ツールComposerを利用

_ インストール

macOSでHomebrewを使ってインストールの手順を示します。

# PHPのインストール
$ brew install php
 
# Composerのインストール
$ brew install composer
 
# Composerを使ってLaravelインストーラをインストールする
$ composer global require laravel/installer

.zprofileや.zshrcにパスを通す

# php composer
export PATH="$HOME/.composer/vendor/bin:$PATH"

_ アプリケーションを作成する

開発を行うディレクトリに移動して、以下のコマンドを叩く。

$ cd 作業ディレクトリ
$ laravel new example-app

お試しなのでデータベースはSQLite、npmは利用を選択した。

_ 動かしてみる。

$ cd example-app
$ npm install && npm run build
$ composer run dev

Webブラウザで"http://127.0.0.1:8000"を開いて動作を確認する。

_ ルーティングを設定する

example-app/routes/web.phpにルーティング情報を追加する。

Route::get('/hello', function () {
    return 'hello world!';
});
Route::get('/hello/{param}', function ($param) {
    return view('hello', ['param' => $param]);
});

_ ビューを追加する

example-app/resources/views/hello.blade.phpを作成する。

<!DOCTYPE html>
<html>
    <head>
        <title>Hello</title>
    </head>
    <body>
        <h1>Hello, {{ $param }}!</h1>
    </body>
</html>

"http://127.0.0.1:8000/hello"で"hello world!”と表示されることを確認。

"http://127.0.0.1:8000/hello/junk"で"Hello, junk!”と表示されることを確認。


2024-11-27 [SwiftUI] Thermostat

ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルThermostatをSwiftUIで実装してみた。

Thermostat

このサンプルは、KVCとKVO、Bindingsが題材になったものだが、SwiftUIの仕組み自体がこの機能を活用したものなので、今まで学習した内容で対応できた。

struct ContentView: View {
    @State private var internalTemperature: Double = 68.0
    @State private var isOn = true
    var body: some View {
        HStack {
            VStack {
                Slider(value: $internalTemperature, in: 0 ... 212, step: 1.0)
                    .disabled(!isOn)
                Text(String(format: "%.0lf", internalTemperature))
            }
            .padding()
            VStack {
                Spacer()
                Button("Warmer") {
                    internalTemperature += 1.0
                }
                .disabled(!isOn)
                Spacer()
                Button("Cooler") {
                    internalTemperature += -1.0
                }
                .disabled(!isOn)
                Spacer()
                Button("Power") {
                    isOn = !isOn
                }
                .buttonStyle(.borderless)
            }
            .padding()
        }.padding()
    }
}

サンプルは縦方向のスライダーだったが、SwiftUIのSliderは横方向のみなので、ここは諦めて横方向のスライダーとした。

_ 【ソースコード】

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/multiplatform/Thermostat - GitHub

2024-10-10 [SwiftUI] TableView

ヒレガス本『Cocoa Programming for OS X (5th Edition)』のTableViewのサンプルをSwiftUIで実装してみた。

TableView

前回のSpeakLineはテキスト読み上げに利用する声をコードで求めていたが、今回のサンプルでは現在利用できる言語に対応した声から選択できるものとなる。

以下が、現在の言語で利用できる声のリストを返す関数。

func getVoices() -> [AVSpeechSynthesisVoice] {
    let locale = Locale(identifier:Locale.preferredLanguages[0])
    let voices = AVSpeechSynthesisVoice.speechVoices().filter { $0.language == locale.identifier }
    return voices
}

得られた声のリストでListを作成する。

struct ContentView: View {
    @State private var text = ""
    @ObservedObject private var speaker: Speaker = .init()
    private let voices = getVoices()
    @State var selectedVoice: AVSpeechSynthesisVoice?
    
    var body: some View {
        VStack {
            HStack {
                ZStack(alignment: .topLeading) {
                    TextEditor(text: $text)
                    
                    // prompt
                    if text.isEmpty {
                        Text("Enter text to be spoken...") .foregroundColor(Color.gray)
                            .padding(.horizontal, 6)
                    }
                }
                List(voices, id: \.self, selection: $selectedVoice) { voice in
                    Text(voice.name)
                }
            }
            HStack {
                Spacer()
                Button("Stop") {
                    print("stop button clicked")
                    speaker.stop()
                }.disabled(!speaker.isSpeaking)
                Button("Speak") {
                    if text.isEmpty {
                        print("string from TextEditor is empty")
                    } else {
                        print("string is \(text)")
                        if let voice = selectedVoice {
                            print("voice is \(voice)")
                            speaker.speak(text, voice: voice)
                        } else {
                            speaker.speak(text)
                        }
                    }
                }.disabled(speaker.isSpeaking)
            }
        }
        .padding()
    }
}

読み上げクラスも、声が渡された場合に対応。

final class Speaker: NSObject, AVSpeechSynthesizerDelegate, ObservableObject {
    func speak(_ text: String, voice: AVSpeechSynthesisVoice) {
        let utterance = AVSpeechUtterance(string: text)
        utterance.voice = voice
        speechSynth.speak(utterance)
    }
}

_ 【ソースコード】

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/multiplatform/SpeakLine - GitHub

2024-09-12 [SwiftUI] Delegation

ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルSpeakLineをSwiftUIで実装してみた。

SpeakLine

テキスト領域に入力された文言を喋るという内容だ。今回のポイントは、マルチプラットフォームなテキスト領域の利用と、マルチプラットフォームなテキスト読み上げ機能の利用とデリゲートの仕組みをSwiftUIでどう利用するかだ。

テキスト領域は、TextFieldを利用するのが適切だが、固定した行数の領域を設定するのが難しかったため、TextEditorを利用した。ただ、TextEditorはプロンプトの機能がないので、それっぽく実装してみたが、テキストの入力が完了しないと消えないとイマイチな挙動となってしまった。

struct ContentView: View {
    @State private var text = ""
    
    var body: some View {
        VStack {
            ZStack(alignment: .topLeading) {
                TextEditor(text: $text)
                
                // prompt
                if text.isEmpty {
                    Text("Enter text to be spoken...") .foregroundColor(Color.gray)
                        .padding(.horizontal, 6)
                }
            }
            HStack {
                Spacer()
                Button("Stop") {
                    print("stop button clicked")
                }
                Button("Speak") {
                    if text.isEmpty {
                        print("string from TextEditor is empty")
                    } else {
                        print("string is \(text)")
                    }
                }
            }
        }
        .padding()
    }
}

2つのStopボタンとSpeakボタンで制御するのだが、喋っている時はSpeakボタンは押下できない。黙っている時はStopボタンは押下できないとしたのだが、SwiftUIは監視するステートに変化がないと描画が更新されないのと、テキストの読み上げ状況はAVSpeechSynthesizerDelegateで受け取るため、そのためのクラスを用意した。

final class Speaker: NSObject, AVSpeechSynthesizerDelegate, ObservableObject {
    @Published var isSpeaking: Bool = false
    private let speechSynth = AVSpeechSynthesizer()
 
    override init() {
        super.init()
        speechSynth.delegate = self
    }
 
    func speak(_ text: String) {
        let utterance = AVSpeechUtterance(string: text)
        let locale = Locale(identifier:Locale.preferredLanguages[0])
        print("locale is \(locale)")
        let code = locale.languageCode!
        print("code is \(code)")
        utterance.voice = AVSpeechSynthesisVoice(language: code)
        speechSynth.speak(utterance)
    }
 
    func stop() {
        speechSynth.stopSpeaking(at: AVSpeechBoundary.immediate)
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
        isSpeaking = true
    }
 
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        isSpeaking = false
    }
}

アプリケーションのローカライズを行っていないため、言語コードが"en-JP"とおかしなことになったため、少し工夫している。

let utterance = AVSpeechUtterance(string: text)
let locale = Locale(identifier:Locale.preferredLanguages[0])
let code = locale.languageCode!
utterance.voice = AVSpeechSynthesisVoice(language: code)

読み上げ状況に合わせてボタンの有効/無効が切り替わるようにした。

struct ContentView: View {
    @State private var text = ""
    @ObservedObject private var speaker: Speaker = .init()
 
    var body: some View {
        VStack {
            ZStack(alignment: .topLeading) {
                TextEditor(text: $text)
 
                // prompt
                if text.isEmpty {
                    Text("Enter text to be spoken...") .foregroundColor(Color.gray)
                        .padding(.horizontal, 6)
                }
            }
            HStack {
                Spacer()
                Button("Stop") {
                    print("stop button clicked")
                    speaker.stop()
                }.disabled(!speaker.isSpeaking)
                Button("Speak") {
                    if text.isEmpty {
                        print("string from TextEditor is empty")
                    } else {
                        print("string is \(text)")
                        speaker.speak(text)
                    }
                }.disabled(speaker.isSpeaking)
            }
        }
        .padding()
    }
}

_ 【ソースコード】

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/multiplatform/SpeakLine - GitHub

2024-08-09 [SwiftUI] UI部品

ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルRGBWellをSwiftUIで実装してみた。

RGBWell

NSColorWellの操作で得られた色のRGBをNSSliderの値に反映し、NSSliderの操作で変更したRGBをNSColorWellに変異するというサンプルだ。

AppKitとUIKitでUI部品のクラスに差異があるが、SwiftUIではColorPickerやSlider、Colorと共通のクラスとなっている。ただし、Colorの内部の色情報はmacOSではNSColor、iOSではUIColorと差異があるのでRGBを値として取り出す場合は、macOSとiOSで異なる型になるようにする必要があった。

#if os(iOS)
typealias MyColor = UIColor
#elseif os(macOS)
typealias MyColor = NSColor
#else
#error("your os is not supported")
#endif
 
extension MyColor {
    var rgba: (red: Double, green: Double, blue: Double, alpha: Double) {
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0
        getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return (Double(red), Double(green), Double(blue), Double(alpha))
    }
}
 
extension Color {
    var rgbValues:(red: Double, green: Double, blue: Double){
        let rgba = MyColor(self).rgba
        return (rgba.red, rgba.green, rgba.blue)
    }
}

ColorPickerで得られるのはColor構造体だ。ColorPickerによって変更が発生したイベントを受け取るために@Observableのクラスを用意した。

@Observable
final class MyData {
    var color = Color(.sRGB, red: 0.98, green: 0.9, blue: 0.2)
    var red = 0.98
    var green = 0.9
    var blue = 0.2
}

ただし、Color構造体は更新不可なので、RGBスライダーで値が変更されても、ColorPickerが利用しているColorの値は更新できないので、スライダーによる変更は諦めた。

struct ContentView: View {
    @State private var myData = MyData()
 
    var body: some View {
        HStack {
            ColorPicker("", selection: $myData.color)
            VStack {
                HStack {
                    Text("R")
                    Slider(
                        value: $myData.red,
                        in: 0.0...1.0
                    ).disabled(true)
                }
                HStack {
                    Text("G")
                    Slider(
                        value: $myData.green,
                        in: 0.0...1.0
                    ).disabled(true)
                }
                HStack {
                    Text("B")
                    Slider(
                        value: $myData.blue,
                        in: 0.0...1.0
                    ).disabled(true)
                }
            }
            .padding()
        }.padding().onAppear {
            trackingColor(myData)
        }
    }
    
    nonisolated private func trackingColor(_ myData: MyData) {
        withObservationTracking {
            _ = myData.color
        } onChange: {
            let rgb = myData.color.rgbValues
            myData.red = rgb.red
            myData.green = rgb.green
            myData.blue = rgb.blue
            trackingColor(myData)
        }
    }
}

ヒレガス本では、

let newColor = NSColor(calibratedRed: CGFloat(r),
                               green: CGFloat(g),
                               blue: CGFloat(b),
                               alpha: CGFloat(a))
colorWell.color = newColor 

とNSColorWellのcolorを更新することで値が変更していたが、これは少々トリッキーなコードだがサンプルだったので、と考えて、SwiftUIで同じことを行うことは諦めた。

_ 【ソースコード】

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/multiplatform/RGBWell - GitHub

2024-07-30 [SwiftUI] ヒレガス本に挑戦する

SwiftUIの学習のため、ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルをSwiftUIで実装していく。

第一弾はボタンと押すとテキスト領域にパスワードを生成して表示するアプリだ。

RandomPassword

書籍ではnibまたはStoryboardと独自のWindowControllerまたはViewControllerを関連づけて、ボタン押下イベントを受け取りテキスト領域にパスワードを表示されていたが、これをSwiftUIで組んだ。

"GeneratePassword.swift"。基本的に書籍のサンプルのまま。

import Foundation
 
private let characters = Array("0123456789abcdefghijklmnopqrstuvwxyz" +
                               "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
 
func generateRandomString(length: Int) -> String {
    var string = ""
    for index in 0.. Character {
    let index = Int(arc4random_uniform(UInt32(characters.count)))
    let character = characters[index]
    return character
}

”ContentView”。ここに画面の宣言とイベントに対する操作を実装する。

import SwiftUI
 
struct ContentView: View {
    @State private var text: String = ""
    var body: some View {
        VStack {
            Text(text)
            
            Button("Generate Password") {
                let length = 8
                let password = generateRandomString(length: length)
                text = password
            }
        }
        .padding()
    }
}
 
#Preview {
    ContentView()
}

生成したパスワードを表示するテキスト領域とパスワード生成ボタンを縦に並べている。

テキスト領域のクラスは Cocoa (AppKit) と CocoaTouch (UIKIt) で異なっているが、SwiftUIは両者のどちらかに引きずられるのではなくで、新たの機能に合わせて用意しているようだ。

_ 【ソースコード】

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/multiplatform/RandomPassword - GitHub

2024-06-21 [macOS] MACアドレスを印字する

アプリ内課金のレシートを自分で解析する方法を調べていたら、公式ドキュメントにIO Kitを使ったMACアドレスを取り出すコードが説明されていたので、面白いので、Swiftで実装したコマンドラインのプログラムとして動かしてみた。

import IOKit
import Foundation
 
// Returns an object with a +1 retain count; the caller needs to release.
func io_service(named name: String, wantBuiltIn: Bool) -> io_service_t? {
    let default_port = kIOMainPortDefault
    var iterator = io_iterator_t()
    defer {
        if iterator != IO_OBJECT_NULL {
            IOObjectRelease(iterator)
        }
    }
 
    // CFMutableDictionaryRef IOBSDNameMatching(mach_port_t mainPort, uint32_t options, const char *bsdName);
    guard let matchingDict = IOBSDNameMatching(default_port, 0, name),
        // kern_return_t IOServiceGetMatchingServices(mach_port_t mainPort, CFDictionaryRef matching, io_iterator_t *existing);
        IOServiceGetMatchingServices(default_port,
                                     matchingDict as CFDictionary,
                                     &iterator) == KERN_SUCCESS,
        iterator != IO_OBJECT_NULL
    else {
        return nil
    }
 
    // io_object_t IOIteratorNext(io_iterator_t iterator);
    var candidate = IOIteratorNext(iterator)
    while candidate != IO_OBJECT_NULL {
        // CFTypeRef IORegistryEntryCreateCFProperty(io_registry_entry_t entry, CFStringRef key, CFAllocatorRef allocator, IOOptionBits options);
        if let cftype = IORegistryEntryCreateCFProperty(candidate,
                                                        "IOBuiltin" as CFString,
                                                        kCFAllocatorDefault,
                                                        0) {
            let isBuiltIn = cftype.takeRetainedValue() as! CFBoolean
            if wantBuiltIn == CFBooleanGetValue(isBuiltIn) {
                return candidate
            }
        }
 
        IOObjectRelease(candidate)
        candidate = IOIteratorNext(iterator)
    }
 
    return nil
}
 
func copy_mac_address() -> CFData? {
    // Prefer built-in network interfaces.
    // For example, an external Ethernet adaptor can displace
    // the built-in Wi-Fi as en0.
    guard let service = io_service(named: "en0", wantBuiltIn: true)
            ?? io_service(named: "en1", wantBuiltIn: true)
            ?? io_service(named: "en0", wantBuiltIn: false)
        else { return nil }
    defer { IOObjectRelease(service) }
 
    // CFTypeRef IORegistryEntrySearchCFProperty(io_registry_entry_t entry, const io_name_t plane, CFStringRef key, CFAllocatorRef allocator, IOOptionBits options);
    if let cftype = IORegistryEntrySearchCFProperty(
        service,
        kIOServicePlane,
        "IOMACAddress" as CFString,
        kCFAllocatorDefault,
        IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) {
            return (cftype as! CFData)
    }
 
    return nil
}
 
func hexaDecimalString(_ cfData: CFData) -> String {
    let data: Data = cfData as Data;
    let value = data.map {
        String(format: "%.2hhx", $0)
    }
    .joined(separator: ":")
    return value
}
 
if let cfData = copy_mac_address() {
    print("\(cfData)")
    print("\(hexaDecimalString(cfData))")
}

トップ 追記