SwiftでiOSアプリを開発するために、まずは基本的な使い方を学びます。ここでは最低限のプログラミングとして、構造体やループ、条件分岐や配列の処理を行い、必要に応じてPythonとの対応についても記載します。Swift初心者の方は是非参考にしてみてください。
こんにちは。wat(@watlablog)です。今回はSwift言語の基礎文法をメモします!
Swift入門
動作環境
本記事のコードは以下の環境で動作確認しました。
Mac | OS | macOS Ventura 13.2.1 |
---|---|---|
CPU | 1.4[GHz] | |
メモリ | 8[GB] | |
Xcode | Version | 14.3(14E222b) |
Swift | Version | 5.8(swiftlang-5.8.0.124.2 clang-1403.0.22.11.100) |
iPhone SE2 | OS | iOS 16.3.1 |
初期コード
この記事では、Xcodeで初期的に作られる以下のHello Worldコードから始めます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import SwiftUI struct ContentView: View { var body: some View { VStack { Text("Hello World!") } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } |
struct(構造体)
struct(構造体)は複数の値をまとめて管理できるもので、データを一元管理する場合等に使用します。classと似ていますが、classはObjective-Cとの併用をする時に使用されることが多いとのことです\(^{[1]}\)。
データをUIに表示させる方法
以下のコードはUI(ユーザーインターフェース)であるTextに、あらかじめ定義しておいたstruct Personのプロパティを表示させるシンプルなコードです。
struct Personでオブジェクトのプロパティを定義し、各プロパティに値を設定したpersonインスタンスを生成して値を取得するという基本文法を理解できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import SwiftUI struct Person { // プロパティを定義 var name: String var age: Int } struct ContentView: View { // インスタンス化 let person = Person(name: "Taro", age: 20) var body: some View { VStack { // インスタンス化したオブジェクトから情報を取得 Text("Name:\(person.name)") Text("Age: \(person.age)") } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } |
これは過去に勉強したPythonのクラス(というかオブジェクト指向全般)と同じ\(^{[2]}\)ですね。
データをUIに表示させる(状態の更新:@State)
こちらの方が良く使うかも知れません。一度オブジェクトをインスタンス化した後にプロパティ値を更新する場合は以下のコードのように書きます。ボタンを押した時にageが更新されるような例としてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import SwiftUI struct Person { // プロパティを定義 var name: String var age: Int } struct ContentView: View { // インスタンス化 @State private var person = Person(name: "Taro", age: 20) var body: some View { VStack { // インスタンス化したオブジェクトから情報を取得 Text("Name:\(person.name)") Text("Age: \(person.age)") // ボタンをクリックすると値が更新される Button("Button") { person.age += 1 } } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } |
ここで、@StateはSwiftUIがビュー内の状態を管理するためのPropertyWrapperです。@Stateを使うことで、値の更新時にビューが自動更新されます。privateはなくても動きますが、これを書いておくことでこのインスタンスがContentViewの中だけで使われるようになり、他のメソッドからプロパティが更新されることを防ぎます。他にも詳細が記載されているサイト発見\(^{[3]}\)。
func(関数)
関数はfuncで定義します。引数には型を指定します。以下のコードはContentViewの中に数値を加算するaddNumbers関数を定義して、結果を文字列としてTextに表示するものです。
「-> Int」は返り値の型を指定しています。returnがない、つまり返り値がない場合は省略できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import SwiftUI struct ContentView: View { func addNumbers(num1: Int, num2: Int) -> Int { // 足し算をする関数 return num1 + num2 } @State var outputText = "Hello World" var body: some View { VStack { Text(outputText) // ボタンをクリックすると値が更新される Button("Button") { let result = addNumbers(num1: 1, num2: 2) outputText = String(result) } } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } |
forループ
Swiftにおけるforループの例をいくつかメモします。以下のコードはループ数を0から10までと明示してprint文でiを表示させるだけのシンプルなものです。
1 2 3 |
for i in 0...10 { print(i) } |
forループを使って配列の要素数をループの中で抽出するコードも以下に示します。配列はPythonと同じ書き方でいけました。
1 2 3 4 5 6 7 |
// 数値配列を定義 let intArray = [10, 20, 30, 40, 50] // forループのテスト for i in intArray { print(i) } |
以下のコードは上記配列をループ条件に指定する方法と比べやや冗長な書き方ですが、Pythonの「len()」に相当するcountメソッドを使って配列数をループ数にしている例です。「..<」は配列のインデックス範囲外を参照しないようにする文です。
1 2 3 4 5 6 7 |
// 数値配列を定義 let intArray = [10, 20, 30, 40, 50] // forループのテスト for i in 0..<intArray.count { print(intArray[i]) } |
whileループ
whileループもおさらいしておきます。「{}」がある以外はPythonと同じですね。
1 2 3 4 5 6 |
// whileループのテスト var counter = 0 while counter <= 10 { print(counter) counter += 1 } |
if文
if文の例です。先ほどのwhileループの中で符号による条件分岐をします。この辺は特に難しくないですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// whileループのテスト var counter = -5 while counter <= 5 { // if 文のテスト if counter < 0 { print("The number is negative.", counter) } else if counter == 0 { print("The number is zero.", counter) } else { print("The number is positive.", counter) } counter += 1 } |
配列
開始:終了:刻みで配列を作成する
データ系のプログラミングをする場合、最低限の配列処理を覚えておきたいものです。以下の例は開始と終了、そして刻みを指定して配列を作成するコードです。Pythonでいうnumpy.arange()ですね。
1 2 3 |
// 配列のテスト let arrayA = Array(stride(from: 0, to: 1, by: 0.1)) print(arrayA) |
スライス
Pythonのnumpyでは配列操作としてスライスが便利でした。Swiftでスライスを行う例を以下に示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 配列のテスト let array = Array(stride(from: 0, to: 10, by: 1)) print("Created array=", array) // 任意の位置から末尾までをスライス let arrayTail = array[3...] print("Sliced array=", arrayTail) // 最初から任意の位置までをスライス let arrayHead = array[..<3] print("Sliced array=", arrayHead) // 任意の範囲をスライス let arrayMid = array[2..<6] print("Sliced array=", arrayMid) |
基準配列に演算をかけて新しく配列を作成する
科学技術プログラミングをする場合、毎回必ずといって良いほど横軸xを使って縦軸yを計算するような処理を行います。Pythonではxを配列で作っておけば「y = 2 * x」といった簡単な文で作成できました。
Swiftではmapを使って以下のように書くそうです。
ここで「$0」とはxの現在の要素です。つまり、map関数はクロージャ「{}」で囲まれた処理を配列の各要素に対して内部ループで行い、最終的に得られた配列を返す関数ということです。
1 2 3 4 5 6 7 |
// 基準配列を作成 let x = Array(stride(from: 0, to: 10, by: 1)) print(x) // 基準配列に演算を行い新規配列を作成 let y = x.map {2 * $0} print(y) |
別ファイルに分ける
複雑なプログラムになるとclassやstructを多用する必要があります。1ファイルで全てを書くとファイル編集時のスクロールが大変になったり、可読性の低下となってしまいます。
そのような場合はファイルを分けましょう。
Swiftにおけるファイル分割は簡単で、「〜.swift」ファイルを新規作成し、単純に分割するだけでOKです。ファイルは「New File...」から作成可能です。
元ファイルの例
例として以下に1つのファイルで成立していたコードを分割してみましょう。まずは1ファイルの元ファイルを以下に示します。このファイルは「SwiftUI/iOSアプリ:録音データをChartsでグラフ化する」に記事で書いたSwiftファイルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
import SwiftUI import Charts import AVFoundation class Recorder: NSObject, ObservableObject { // 録音クラス // オプショナル型としてAVAudioRecoderを定義 var audioRecorder: AVAudioRecorder? // 録音の状態を管理するプロパティ @Published var isRecording = false // 録音データの変数を宣言 @Published var waveformData: [Float] = [] // サンプリングレート var sampleRate: Double = 4096 // カスタムクラスのコンストラクタを定義 override init() { super.init() setUpAudioRecorder() } private func setUpAudioRecorder() { // 録音の設定 let recordingSession = AVAudioSession.sharedInstance() // エラーを確認 do { try recordingSession.setCategory(.playAndRecord, mode: .default) try recordingSession.setActive(true) // 辞書型で設定値を変更 let settings: [String: Any] = [ AVFormatIDKey: Int(kAudioFormatLinearPCM), AVSampleRateKey: sampleRate, AVNumberOfChannelsKey: 1, AVLinearPCMBitDepthKey: 16, AVLinearPCMIsBigEndianKey: false, AVLinearPCMIsFloatKey: false, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] // wavファイルのパスを設定する(.wavはリアルタイムに書き込まれる) let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let audioFileURL = documentsPath.appendingPathComponent("recording.wav") audioRecorder = try AVAudioRecorder(url: audioFileURL, settings: settings) // audioRecorderがnilでない場合のみバッファ割当てや初期化、設定をする audioRecorder?.prepareToRecord() } // エラーの場合 catch { print("Error setting up audio recorder: \(error)") } } func startRecording() { // 録音するメソッド audioRecorder?.record() isRecording = true } func stopRecording() { // 録音停止するメソッド audioRecorder?.stop() isRecording = false // 録音停止時にwavファイルのパスをコンソールに表示する if let audioFileURL = audioRecorder?.url { print(audioFileURL)} // 配列データとして取得する getWaveformData { waveformData in print("wave length=", waveformData.count) self.waveformData = waveformData } } func getWaveformData(completion: @escaping ([Float]) -> Void) { // 録音結果の配列データを取得するメソッド guard let audioFileURL = audioRecorder?.url else { return } do { let audioFile = try AVAudioFile(forReading: audioFileURL) let audioFormat = AVAudioFormat(standardFormatWithSampleRate: audioFile.processingFormat.sampleRate, channels: audioFile.processingFormat.channelCount) let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat!, frameCapacity: UInt32(audioFile.length)) try audioFile.read(into: audioBuffer!) let floatArray = Array(UnsafeBufferPointer(start: audioBuffer!.floatChannelData![0], count: Int(audioBuffer!.frameLength))) completion(floatArray) } catch { print("Error getting waveform data: \(error)") } } } struct PointsData: Identifiable { // 点群データの構造体 var xValue: Float var yValue: Float var id = UUID() } struct ContentView: View { // データ構造関係の宣言 @State private var data: [PointsData] = [] @State private var x: [Float] = [] @State private var y: [Float] = [] // 録音関係の宣言 @StateObject private var recorder = Recorder() private var dt: Float { Float(1.0 / recorder.sampleRate) } @State private var isDisplayingData = false var body: some View { // UI VStack{ Chart { // データ構造からx, y値を取得して散布図プロット ForEach(data) { shape in // 折れ線グラフをプロット LineMark( x: .value("x", shape.xValue), y: .value("y", shape.yValue) ) } } .padding(.all, 10) if recorder.isRecording { // 録音している時 Button(action: { // 停止ボタンが押されたらデータをChartsに表示させる // 録音の実行 print("Stop") recorder.stopRecording() // バックグラウンドでデータ処理を行う DispatchQueue.global(qos: .userInitiated).async { // データ取得 y = recorder.waveformData let samplePoints = Float(y.count) x = Array(stride(from: 0.0, to: samplePoints * dt, by: dt)) // プロットデータの追加 data.removeAll() // メインと同期させる DispatchQueue.main.async { data = zip(x, y).map { PointsData(xValue: $0, yValue: $1) } isDisplayingData = false } } }) { Text("Stop Recording") .padding() .background(Color.red) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } } else { // 録音していない時 Button(action: { print("Start") isDisplayingData = true recorder.startRecording() }) { Text("Start Recording") .padding() .background(Color.green) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } .opacity(isDisplayingData ? 0.5 : 1.0) .disabled(isDisplayingData) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { // プレビュー ContentView() } } |
分割ファイルの例
こちらが「Audio.swift」としてRecorderクラス部分とデータ構造のstructのみを書いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
import Foundation import AVFoundation class Recorder: NSObject, ObservableObject { // 録音クラス // オプショナル型としてAVAudioRecoderを定義 var audioRecorder: AVAudioRecorder? // 録音の状態を管理するプロパティ @Published var isRecording = false // 録音データの変数を宣言 @Published var waveformData: [Float] = [] // サンプリングレート var sampleRate: Double = 4096 // カスタムクラスのコンストラクタを定義 override init() { super.init() setUpAudioRecorder() } private func setUpAudioRecorder() { // 録音の設定 let recordingSession = AVAudioSession.sharedInstance() // エラーを確認 do { try recordingSession.setCategory(.playAndRecord, mode: .default) try recordingSession.setActive(true) // 辞書型で設定値を変更 let settings: [String: Any] = [ AVFormatIDKey: Int(kAudioFormatLinearPCM), AVSampleRateKey: sampleRate, AVNumberOfChannelsKey: 1, AVLinearPCMBitDepthKey: 16, AVLinearPCMIsBigEndianKey: false, AVLinearPCMIsFloatKey: false, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] // wavファイルのパスを設定する(.wavはリアルタイムに書き込まれる) let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let audioFileURL = documentsPath.appendingPathComponent("recording.wav") audioRecorder = try AVAudioRecorder(url: audioFileURL, settings: settings) // audioRecorderがnilでない場合のみバッファ割当てや初期化、設定をする audioRecorder?.prepareToRecord() } // エラーの場合 catch { print("Error setting up audio recorder: \(error)") } } func startRecording() { // 録音するメソッド audioRecorder?.record() isRecording = true } func stopRecording() { // 録音停止するメソッド audioRecorder?.stop() isRecording = false // 録音停止時にwavファイルのパスをコンソールに表示する if let audioFileURL = audioRecorder?.url { print(audioFileURL)} // 配列データとして取得する getWaveformData { waveformData in print("wave length=", waveformData.count) self.waveformData = waveformData } } func getWaveformData(completion: @escaping ([Float]) -> Void) { // 録音結果の配列データを取得するメソッド guard let audioFileURL = audioRecorder?.url else { return } do { let audioFile = try AVAudioFile(forReading: audioFileURL) let audioFormat = AVAudioFormat(standardFormatWithSampleRate: audioFile.processingFormat.sampleRate, channels: audioFile.processingFormat.channelCount) let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat!, frameCapacity: UInt32(audioFile.length)) try audioFile.read(into: audioBuffer!) let floatArray = Array(UnsafeBufferPointer(start: audioBuffer!.floatChannelData![0], count: Int(audioBuffer!.frameLength))) completion(floatArray) } catch { print("Error getting waveform data: \(error)") } } } struct PointsData: Identifiable { // 点群データの構造体 var xValue: Float var yValue: Float var id = UUID() } |
そしてこちらがメインの「ContentVIew.swift」です。特にimport文を書かなくてもこれでコードは正常に動作します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
import SwiftUI import Charts struct ContentView: View { // データ構造関係の宣言 @State private var data: [PointsData] = [] @State private var x: [Float] = [] @State private var y: [Float] = [] // 録音関係の宣言 @StateObject private var recorder = Recorder() private var dt: Float { Float(1.0 / recorder.sampleRate) } @State private var isDisplayingData = false var body: some View { // UI VStack{ Chart { // データ構造からx, y値を取得して散布図プロット ForEach(data) { shape in // 折れ線グラフをプロット LineMark( x: .value("x", shape.xValue), y: .value("y", shape.yValue) ) } } .padding(.all, 10) if recorder.isRecording { // 録音している時 Button(action: { // 停止ボタンが押されたらデータをChartsに表示させる // 録音の実行 print("Stop") recorder.stopRecording() // バックグラウンドでデータ処理を行う DispatchQueue.global(qos: .userInitiated).async { // データ取得 y = recorder.waveformData let samplePoints = Float(y.count) x = Array(stride(from: 0.0, to: samplePoints * dt, by: dt)) // プロットデータの追加 data.removeAll() // メインと同期させる DispatchQueue.main.async { data = zip(x, y).map { PointsData(xValue: $0, yValue: $1) } isDisplayingData = false } } }) { Text("Stop Recording") .padding() .background(Color.red) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } } else { // 録音していない時 Button(action: { print("Start") isDisplayingData = true recorder.startRecording() }) { Text("Start Recording") .padding() .background(Color.green) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } .opacity(isDisplayingData ? 0.5 : 1.0) .disabled(isDisplayingData) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { // プレビュー ContentView() } } |
まとめ
今回はPythonしかまともに書けない筆者がSwiftを書く時にまず手をつけた内容を記載しました。
コード全体の構造、ループや条件分岐、配列処理ができれば、内容は薄いですが最低限のプログラミングはできそうです(あとは適宜調べつつ)。
まだまだforループの書き方にも種類があるそうですが、網羅するのではなくすぐにアプリ制作を開始するのが目的のためここまでにします。Swift関連で何か簡単なテクニックを見つけたらこちらに追記しようと思います。
ちなみにボタンイベントの書き方やビルドについては以下の記事にまとめていますので、この記事と合わせて読者の方もアプリ制作をしてみてください!
・SwiftでiOSデバイスのマイクを使って録音機能を追加する方法
それにしてもSwiftはGUIがリアルタイムに更新されるプログラミングなのでライブ感が楽しいですね。Pythonで技術的な計算の検証をしつつSwiftでフロントエンドに対応させていく…というのが自分には合っているのかも?自分のモバイル端末で技術計算アプリをリリースすることを目標に今後も進捗を報告します。
参考文献
[1]:iPhoneアプリ開発集中講座, 藤治仁・小林加奈子・小林由憲, ソシム株式会社, 2022, pp120[2]:Pythonのクラスの使い方とオブジェクト指向の考え方を理解する
[3]:カピ通信, 【SwiftUI】@Stateの使い方
PythonとSwiftの二刀流を目指します!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!