- 投稿日:2020-10-16T23:12:50+09:00
for-in文を理解しよう!
今回は、for-in文について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承くださいfor-in文とは?
for-in文は各要素を実行文に順次渡しながら、その要素の数だけ繰り返しを行います。
基本的な書き方は以下の通りです
qiita.rbvarfor 要素名 in シーケンス { 要素ごとに繰り返し実行される文 }※シーケンス・・・連続(しているもの)、一続き(のもの)、順序、順番、並び、配列(する)、逐次、並べる、順序付ける、などの意味を持つ。
次は簡単な例を見ていきましょう。
qiita.rbvar1.let array = [1,2,3] 2.for element in array { print(element) } 実行結果 1 2 3上記の例では、配列array内の値に、定数elementを通じて1つずつアクセスしています。
Dictionary型の要素をfor-inで列挙する場合、要素の型は(key,value)型のタプルとなります。例えば、[String: Int]型の値を
for-in文に渡すと、要素は(String: Int)型となります。簡単な例を見ていきましょう。
qiita.rbvarlet dictionary = ["a": 1,"b": 2] for (key,value) in dictionary { print("key:\(key),Value\(value)") } 実行結果 key: a Value: 1 key: b Value: 2break文
break文は、実行文を中断し、さらに、繰り返し文全体を終了します。例えば、これ以上繰り返しを行なう必要が奈君あた場合などに使用します。
簡単な例を見ていきましょう。
qiita.rbvarvar containsThree = false let array = [1,2,3,4,5] for element in array { if element == 3 { containsThree = true break //3が見つかったら終了 } print("element:\(containsTwo)") } print("containsTwo: \(containsTwo)") 実行結果 element: 3 containsTwo: true上記の例では、配列の中に3が含まれているかを検査するプログラムです。
3が見つかった時点で後続の繰り返しを行なう必要はないので、break文を用いて繰り返し文を終了しています。continue文
continue文は、実行文を中断した後、後続の繰り返しを継続します。例えば、特定の場合だけ処理をスキップする場合などに使用します。
qiita.rbvarvar adds = [Int]() let array = [1,2,3,] for element in array { if element % 2 == 1 { adds.append(element) continue } print("even: \(element)") } print("odds:\(adds)") 実行結果 even: 2 adds: [1,3]上記の例では、break文とは異なり後続の繰り返しは継続され、全ての要素にたいして処理が行われていることがわかります。
- 投稿日:2020-10-16T20:10:36+09:00
[Swift] forとifを入れ子にするよりfor whereを使おう
随分前からある構文ですが、私は最近気づいたので改めて書きます。
Swiftを書いていると時折こういうコードを書く必要が出てきます。var count = 0 for i in 0..<n{ if condition(i){ count += 1 } }
for
の直下にif
があり、かつelse
がない場合です。でも
where
を使えばこう書けます。var count = 0 for i in 0..<n where condition(i){ count += 1 }意味は簡単で、
where
節が条件を指定しています。こう書くことでネストが1つ減るだけでなく、考えている条件もわかりやすくなります。
最適化のおかげでパフォーマンス上の影響は全くないので、積極的に使っていくべきだと思います。
- 投稿日:2020-10-16T16:39:42+09:00
titleTextAttributes指定時のフォント崩れ対策
概要
iOS のナビゲーションバーの文字色を変更するためにtitleTextAttributesを指定してみたら、フォントが崩れたので対応。
環境
Xcode 11.3
iOS 14.0 & 13.3事象と対策
ナビゲーションバータイトルにフォント指定しているばあい、
色だけ指定しても、フォント指定が外れるself.navigationController?.navigationBar.titleTextAttributes = [ // 文字の色 .foregroundColor: UIColor.blue ]
- フォントの指定も加える必要がある
self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.yellow, NSAttributedString.Key.font: UIFont(name: "HiraginoSans-W3", size: 16) as Any ]当たり前っちゃ当たり前ですが、気づかなかったので、備忘録として投稿します。
- 投稿日:2020-10-16T16:39:42+09:00
titleTextAttributes指定時のフォント指定忘れ対策
概要
iOS のナビゲーションバーの文字色を変更するためにtitleTextAttributesを指定してみたら、フォントが崩れたので対応。
環境
Xcode 11.3
iOS 14.0 & 13.3事象と対策
ナビゲーションバータイトルにフォント指定しているばあい、
色だけ指定しても、フォント指定が外れるself.navigationController?.navigationBar.titleTextAttributes = [ // 文字の色 .foregroundColor: UIColor.yellow ]
- フォントの指定も加える必要がある
self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.yellow, NSAttributedString.Key.font: UIFont(name: "HiraginoSans-W3", size: 16) as Any ]当たり前っちゃ当たり前ですが、気づかなかったので、備忘録として投稿します。
- 投稿日:2020-10-16T14:24:29+09:00
switch文を理解しよう!
今回は、switch文について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承くださいswitch文とは?
switch文を一言でいうと、パターンを利用して制御式の値に応じて実行文を切り替える制御構文です。
基本的な書き方は以下の通りです
qiita.rbvarswitch 制御式 { case パターン1: 制御式がパターン1にマッチした場合に実行される文 case パターン2: 制御式がパターン2にマッチした場合に実行される文 default: 制御式がいずれのパターンにもマッチしなかった場合に実行される文 }switch文は、一度マッチして実行文を実行するとマッチングを終了し、それ以降のパターンはスキップするという特徴を持ちます。
if文やguard文は成立するか否かの2つのケースへの分岐でしたが、switch文はさらに多くのケースに分岐できます。次は簡単な例を見ていきましょう
qiita.rbvarlet a = -2 switch a { case Int.min..<0: print("aの負の値です") case 1..<Int.max: print("aは正の値です") default: print("aは0です") } 実行結果:aは負の値ですwhereキーワード
whereキーワードを利用すると、ケースにマッチする条件を追加できます。
これだけと、なんのことかさっぱりなので、早速、基本構文を見ていきましょう!qiita.rbvarswitch 制御式 { case パターン 制御式: 制御式がパターン1にマッチし,かつ,条件式を満たす場合に実行される文 default: 制御式がいずれのパターンにもマッチしなかった場合に実行される文次は、簡単な例です。
qiita.rbvarlet optionalA: Int? = 5 switch optionalA { case .some(let a)where a > 10: print("10より大きい値\(a)が存在します") default: print("値が存在しない,もしくは10以下です") 実行結果: 値が存在しない,もしくは10以下です上記の例では、定数optionalAは値を持っているため、case .some(let a)の部分にはマッチしますが、where a > 10という条件を満たしません。
break文
break文は、switch文のケースの実行を中断する文です。
簡単な例を見ていきましょう。qiita.rbvarlet a = 1 switch a{ case 1: print("ログインに成功しました") break print("ログインに失敗しました") default: break } 実行結果:ログインに成功しました上記の例では、マッチするけーすcase1:内に2つのprint()関数が書かれていますが、2つ目のprint関数の前にbreak文が書かれているため、2つ目のprint関数は実行されません。
ラベル
ラベルはbreak文の制御対象を指定するための仕組みです。
switch文が入れ子になっている場合など、break文の対象となるswitch文を明示する必要があるケースで利用します。入れ子・・・プログラミング用語で、あるモノの中にそれと同じモノを入れた構造という意味。
基本構造を見ていきましょう!
qiita.rbvarラベル名: switch文 break ラベル名では、今度は、ラベルが必要となる例を見ていきましょう。
qiita.rbvarimport UIKit let value = 0 as Any outerSwitch: switch value{ case let int as Int: let description: String switch int { case 1,3,5,7,9: description = "奇数" case 2,4,6,8,10: description = "偶数" default: print("対象外の値です")//ここが読まれる break outerSwitch } print("値は\(description)です") default: print("値がInt型ではありません") } 実行結果:対象外の値です上記の例では、Any型の値が1から10までのInt型であれば、その値が奇数か偶数かを出力するプログラムです。
指定された値0なので、対象外の値ですと出力されています。では、今度はlet valueの指定を"aaa"に変更してみます。
qiita.rbvarimport UIKit let value = "aaa" as Any outerSwitch: switch value{ case let int as Int: let description: String switch int { case 1,3,5,7,9: description = "奇数" case 2,4,6,8,10: description = "偶数" default: print("対象外の値です") break outerSwitch } print("値は\(description)です") default: print("値がInt型ではありません")//ここが読まれる } 実行結果:値がInt型ではありませんこのように指定された値がInt型ではないため、2つ目のprint関数が呼ばれます。
fallthrough文
fallthrough文は、switch文のケースの実行を終了し、次のケースを実行する制御構文です。
qiita.rbvarlet a = 1 switch a{ case 1: print("ログインに成功しました") fallthrough case 2: print("ログインに失敗しました") default: print("default") } 実行結果: ログインに成功しました ログインに失敗しました上記の例では、fallthrough文によって、実行が次のケースであるcase2に移ります。したがって、case1とcase2が出力されます。
- 投稿日:2020-10-16T12:17:50+09:00
配列の要素の削除方法まとめ(自分用メモ)
いつも何回も検索をかけてしまうのでまとめておく。
(特にインデックスが不明の時)最初の要素削除
var strArray = ["A","B","C","D","E"] strArray.removeFirst() //["B", "C", "D", "E"]最後の要素削除
var strArray = ["A","B","C","D","E"] strArray.removeLast() //["A", "B", "C", "D"]インデックスを指定して要素削除
var strArray = ["A","B","C","D","E"] strArray.remove(at: 2) //["A", "C", "D", "E"]インデックスが不明な要素を削除
"C"を削除したい場合、"C"以外をfilterで指定する。
var strArray = ["A","B","C","D","E"] strArray = strArray.filter { $0 != "C" } // ["A", "B", "D", "E"]
- 投稿日:2020-10-16T12:17:50+09:00
[Swift]配列から要素を削除する方法まとめ(自分用メモ)
いつも何回も検索をかけてしまうのでまとめておく。
最初の要素削除
var strArray = ["A","B","C","D","E"] strArray.removeFirst() //["B", "C", "D", "E"]最後の要素削除
var strArray = ["A","B","C","D","E"] strArray.removeLast() //["A", "B", "C", "D"]全ての要素削除
var strArray = ["A","B","C","D","E"] strArray.removeAll() //[]指定した要素を削除
var strArray = ["A","B","C","D","E"] strArray.removeAll(where: {$0 == "C"}) // ["A", "B", "D", "E"]インデックスを指定して要素削除
var strArray = ["A","B","C","D","E"] strArray.remove(at: 2) //["A", "C", "D", "E"]インデックスが不明な要素を削除 (これまでのが使えない時)
"C"を削除したい場合、"C"以外をfilterで指定する。
var strArray = ["A","B","C","D","E"] strArray = strArray.filter { $0 != "C" } // ["A", "B", "D", "E"]
- 投稿日:2020-10-16T10:59:24+09:00
iOSアプリ開発:タイマーアプリ(まとめ)
記事内容
タイマーアプリを作成するためのポイントを複数記事に分けて掲載していきます。タイマーだけで寂しいので、複数人のプレゼンテーションでの進行にも役立つような機能も。
タイマーアプリの主な機能
- タイマーをセットする
- タイマーを表示する
- スタート/ストップ/リセットボタン
- プログレスバー
- アラーム・バイブレーションを発動する
- 設定画面
- プレゼンテーションモード
- 発表者リスト
- 発表者設定画面
開発環境
- OS: macOS 10.15.x
- エディタ: Xcode 11.x
- 言語: Swift
手順
補足
Xcode 12 ではCanvasの機能を使って、コーディングしながらリアルタイムにデバイスの画面がどのように表示されるのかを確認できます。
ただし、ObservableObjectプロトコルを適用したクラスの@Publishedプロパティラッパーがついたプロパティの値をViewから常に参照するような構成になっている場合、そのViewのプレビュー用Structは、以下のようにViewのインスタンスに、.environmentObject(ClassName())モディファイアをつけないとCanvasに表示されず、エラーになります。
struct TimerView_Previews: PreviewProvider { static var previews: some View { TimerView() .environmentObject(TimeManager()) } }また、画面上のコンポーネントをいくつかのViewに分けて作成していきますが、コンポーネント単体をフルサイズのデバイス大のCanvasに表示より、そのViewサイズのみで表示したい場合は、以下のように .previewLayout(.sizeThatFits) モディファイアをつけるとジャストサイズになります。
struct TimerView_Previews: PreviewProvider { static var previews: some View { TimerView() .environmentObject(TimeManager()) .previewLayout(.sizeThatFits) } }
- 投稿日:2020-10-16T08:56:53+09:00
iOSアプリ開発:タイマーアプリ(1.タイマーセット)
記事内容
タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。開発環境
- OS: macOS 10.15.x
- エディタ: Xcode 11.x
- 言語: Swift
手順概要
- TimeManagerというClassを用意
- TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
- PickerViewというViewを用意
- PickerViewにコンポーネントとしてPickerを配置する
- TimeManagerに、画面上で設定した時間をプロパティに反映するためのメソッドを作成
- TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成
1. TimeManagerというClassを用意
TimeManager.swiftというファイルを作成し、同名のClassを作成します。
のちのちのことを想定してObservableObjectというプロトコルを適用します。ObservableObjectとは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに@Publishedというキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値は常に監視され、他のViewから常に参照することができます。TimeManager.swiftimport SwiftUI class TimeManager: ObservableObject { }2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
カウントダウン時間の設定として、以下の5つのプロパティを用意します。
- 時間単位の設定値を格納するプロパティ
- 分単位の設定値を格納するプロパティ
- 秒単位の設定値を格納するプロパティ
- 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
- 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ
各プロパティの値は他のViewから常に参照できるようにしたいので、キーワード@Publishedをvarの前につけておきます。
時間、分、秒はPickerからInt型で取得するためIntをデータ型に指定しておきます。一方、残り時間と最大時間はDouble型を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。
それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0にしておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //Pickerで設定した"時間"を格納する変数 @Published var hourSelection: Int = 0 //Pickerで設定した"分"を格納する変数 @Published var minSelection: Int = 0 //Pickerで設定した"秒"を格納する変数 @Published var secSelection: Int = 0 //カウントダウン残り時間 @Published var duration: Double = 0 //カウントダウン開始前の最大時間 @Published var maxValue: Double = 0 }さらに、設定したカウントダウン時間の長さによって時間表示の形式を変えたい(例えば2時間30分に設定したら"02:30:00"、5分に設定したら"05:00"、30秒に設定したら"30"という表示にしたい)ので、enumを作ります。
Data.swiftというファイルを新たに作成し、TimeFormatというenumを作成します。
Data.swiftimport SwiftUI enum TimeFormat { case hr case min case sec }そして、TimeManagerのクラスに作成したTimeFormatのenumをタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず.minにしておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //(省略) //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式 @Published var displayedTimeFormat: TimeFormat = .min }3. PickerViewというViewタイプのStructを用意
PickerView.swiftという新しいファイルを作成し、同名のViewタイプのStruct(構造体)を作成します。
PickerView.swiftimport SwiftUI struct PickerView: View { var body: some View { } }まずは必要なプロパティを作成します。
ここで、先に作成したTimeManagerクラスのインスタンスを作成しておきます。
@EnvironmentObjectのキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObjectプロトコル適用のClass内の@PublishedプロパティとViewプロトコル適用のStruct内の@EnvironmentObjectプロパティは対になり常に同期されます。PickerView.swiftstruct PickerView: View { //TimeManagerのインスタンスを作成 @EnvironmentObject var timeManager: TimeManager //デバイスのスクリーンの幅 let screenWidth = UIScreen.main.bounds.width //デバイスのスクリーンの高さ let screenHeight = UIScreen.main.bounds.height //設定可能な時間単位の数値:0から23までの整数のArray var hours = [Int](0..<24) //設定可能な分単位の数値:0から59までの整数のArray var minutes = [Int](0..<60) //設定可能な秒単位の数値:0から59までの整数のArray var seconds = [Int](0..<60) struct PickerView: View { var body: some View { } }4. PickerViewにコンポーネントとしてPickerを配置する
PickerViewの中に、以下のコンポーネントを左から順番に横並びにします。
- 時間のPicker
- 時間単位のtext
- 分のPicker
- 分単位のtext
- 秒のPicker
- 秒単位のtext
時間Pickerの引数selectionには先に作ったインスタンスtimeManagerのプロパティhourSelectionを指定します。これにより、Pickerで選んだ値がTimeManagerクラスの同名プロパティへ代入されるようになります。
Pickerの{}内のクロージャでは、ForEachで0から23それぞれに対してPickerに数値を表示するようにし、.tagによって実際にPickerで時間を選択した時に取得する値(データ型含む)を指定します。取得する値がInt型、反映先のTimeManagerのプロパティhourSelectionもInt型で合わせています。
時間Pickerができたら、Textコンポーネントで"hour"という単位を表示するよう指定します。
同様にして、分、秒のPickerとTextも作成します。
PickerView.swiftstruct PickerView: View { //(プロパティ部分の記述省略) var body: some View { //時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに HStack { //時間単位のPicker Picker(selection: self.$timeManager.hourSelection, label: Text("hour")) { ForEach(0 ..< self.hours.count) { index in Text("\(self.hours[index])") .tag(index) } } //上下に回転するホイールスタイルを指定 .pickerStyle(WheelPickerStyle()) //ピッカーの幅をスクリーンサイズ x 0.1、高さをスクリーンサイズ x 0.4で指定 .frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4) //上のframeでクリップし、フレームからはみ出す部分は非表示にする .clipped() //時間単位を表すテキスト Text("hour") .font(.headline) //分単位のPicker Picker(selection: self.$timeManager.minSelection, label: Text("minute")) { ForEach(0 ..< self.minutes.count) { index in Text("\(self.minutes[index])") .tag(index) } } .pickerStyle(WheelPickerStyle()) .frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4) .clipped() //分単位を表すテキスト Text("min") .font(.headline) //秒単位のPicker Picker(selection: self.$timeManager.secSelection, label: Text("second")) { ForEach(0 ..< self.seconds.count) { index in Text("\(self.seconds[index])") .tag(index) } } .pickerStyle(WheelPickerStyle()) .frame(width:self.screenWidth * 0.1, height: self.screenWidth * 0.4) .clipped() //秒単位を表すテキスト Text("sec") .font(.headline) } } }5. TimeManagerに、Pickerで設定・取得した時間を最大時間、および残り時間のプロパティに反映するためのメソッドを作成
TimeManagerクラス内にsetTimerというメソッドを作成します。
PickerViewからTimeManagerの時間・分・秒のプロパティの値を取得できるようになっているので、それらの値を秒に換算して合計し、残り時間および最大時間のプロパティに反映します。併せて、時間表示形式も合計時間によって条件分岐するようにします。TimeManager.swiftclass TimeManager: ObservableObject { //Pickerで設定した"時間"を格納する変数 @Published var hourSelection: Int = 0 //Pickerで設定した"分"を格納する変数 @Published var minSelection: Int = 0 //Pickerで設定した"秒"を格納する変数 @Published var secSelection: Int = 0 //カウントダウン残り時間 @Published var duration: Double = 0 //カウントダウン開始前の最大時間 @Published var maxValue: Double = 0 //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式 @Published var displayedTimeFormat: TimeFormat = .min //Pickerで取得した値からカウントダウン残り時間とカウントダウン開始前の最大時間を計算し、 //その値によって時間表示形式も指定する func setTimer() { //残り時間をPickerから取得した時間・分・秒の値をすべて秒換算して合計して求める duration = Double(hourSelection * 3600 + minSelection * 60 + secSelection) //Pickerで時間を設定した時点=カウントダウン開始前のため、残り時間=最大時間とする maxValue = duration //時間表示形式を残り時間(最大時間)から指定する //60秒未満なら00形式、60秒以上3600秒未満なら00:00形式、3600秒以上なら00:00:00形式 if duration < 60 { displayedTimeFormat = .sec } else if duration < 3600 { displayedTimeFormat = .min } else { displayedTimeFormat = .hr } } }さらに、上記setTimerメソッドを、PickerViewで時間を設定したタイミングで発動できるように、PickerViewのほうにボタンを追加します。最大時間=残り時間という式が入っているため、カウントダウン前つまり時間設定操作の直後が理想的だからです。
ボタンのアイコンはApple純正SF Symbolsから"checkmark.circle.fill"を使います。
.offsetモディファイアでデフォルトの中央配置からやや下にずらして配置し、PickerViewと重ならないように調整しています。
.opacityモディファイアで、時間、分、秒、いずれも設定が0の場合は透明度を 10%(x 0.1)にして、今はタップできませんというのを視覚的に表現しています。
モディファイアの引数を入力する際、ifとelseを使う代わりに「△△ ? □□ : ○○」 と記載することで「もし △△ だったら □□ そうでなければ ○○」という意味に完結に条件分岐する引数を与えられて便利です。また「&&」は「かつ」という意味で複数の条件に当てはまる場合を表現する際に使います。
.onTapGestureモディファイアの{}のクロージャ内で、時間、分、秒いずれかのプロパティの値が0でなければ、タップしたときにsetTimerメソッドが発動するようにします。if構文内の「||」は「または」の意味です。
PickerView.swiftstruct PickerView: View { //TimeManagerのインスタンスを作成 @EnvironmentObject var timeManager: TimeManager //デバイスのスクリーンの幅 let screenWidth = UIScreen.main.bounds.width //デバイスのスクリーンの高さ let screenHeight = UIScreen.main.bounds.height //設定可能な時間単位の数値 var hours = [Int](0..<24) //設定可能な分単位の数値 var minutes = [Int](0..<60) //設定可能な秒単位の数値 var seconds = [Int](0..<60) var body: some View { //ZStackでPickerとレイヤーで重なるようにボタンを配置 ZStack{ //時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに HStack { //(Picker部分省略) } //タップして設定を確定するチェックマークアイコン Image(systemName: "checkmark.circle.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 35, height: 35) .offset(y: self.screenWidth * 0.32) .opacity(self.timeManager.hourSelection == 0 && self.timeManager.minSelection == 0 && self.timeManager.secSelection == 0 ? 0.1 : 1) .onTapGesture { if self.timeManager.hourSelection != 0 || self.timeManager.minSelection != 0 || self.timeManager.secSelection != 0 { self.timeManager.setTimer() } } } } }6. TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成
TimeManager.swiftclass TimeManager: ObservableObject { //(省略) //カウントダウン中の残り時間を表示するためのメソッド func displayTimer() -> String { //残り時間(時間単位)= 残り合計時間(秒)/3600秒 let hr = Int(duration) / 3600 //残り時間(分単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 let min = Int(duration) % 3600 / 60 //残り時間(秒単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 で割った余り let sec = Int(duration) % 3600 % 60 //setTimerメソッドの結果によって時間表示形式を条件分岐し、上の3つの定数を組み合わせて反映 switch displayedTimeFormat { case .hr: return String(format: "%02d:%02d:%02d", hr, min, sec) case .min: return String(format: "%02d:%02d", min, sec) case .sec: return String(format: "%02d", sec) } } }
- 投稿日:2020-10-16T08:56:53+09:00
iOSアプリ開発:タイマーアプリ(1.タイマーをセット)
記事内容
タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。開発環境
- OS: macOS 10.15.x
- エディタ: Xcode 11.x
- 言語: Swift
手順概要
- TimeManagerというClassを用意
- TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
- PickerViewというViewを用意
- PickerViewにコンポーネントとしてPickerを配置する
- TimeManagerに、画面上で設定した時間をプロパティに反映するためのメソッドを作成
- TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成
1. TimeManagerというClassを用意
TimeManager.swiftというファイルを作成し、同名のClassを作成します。
のちのちのことを想定してObservableObjectというプロトコルを適用します。ObservableObjectとは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに@Publishedというキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値は常に監視され、他のViewから常に参照することができます。TimeManager.swiftimport SwiftUI class TimeManager: ObservableObject { }2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
カウントダウン時間の設定として、以下の5つのプロパティを用意します。
- 時間単位の設定値を格納するプロパティ
- 分単位の設定値を格納するプロパティ
- 秒単位の設定値を格納するプロパティ
- 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
- 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ
各プロパティの値は他のViewから常に参照できるようにしたいので、キーワード@Publishedをvarの前につけておきます。
時間、分、秒はPickerからInt型で取得するためIntをデータ型に指定しておきます。一方、残り時間と最大時間はDouble型を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。
それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0にしておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //Pickerで設定した"時間"を格納する変数 @Published var hourSelection: Int = 0 //Pickerで設定した"分"を格納する変数 @Published var minSelection: Int = 0 //Pickerで設定した"秒"を格納する変数 @Published var secSelection: Int = 0 //カウントダウン残り時間 @Published var duration: Double = 0 //カウントダウン開始前の最大時間 @Published var maxValue: Double = 0 }さらに、設定したカウントダウン時間の長さによって時間表示の形式を変えたい(例えば2時間30分に設定したら"02:30:00"、5分に設定したら"05:00"、30秒に設定したら"30"という表示にしたい)ので、enumを作ります。
Data.swiftというファイルを新たに作成し、TimeFormatというenumを作成します。
Data.swiftimport SwiftUI enum TimeFormat { case hr case min case sec }そして、TimeManagerのクラスに作成したTimeFormatのenumをタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず.minにしておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //(省略) //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式 @Published var displayedTimeFormat: TimeFormat = .min }3. PickerViewというViewタイプのStructを用意
PickerView.swiftという新しいファイルを作成し、同名のViewタイプのStruct(構造体)を作成します。
PickerView.swiftimport SwiftUI struct PickerView: View { var body: some View { } }まずは必要なプロパティを作成します。
ここで、先に作成したTimeManagerクラスのインスタンスを作成しておきます。
@EnvironmentObjectのキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObjectプロトコル適用のClass内の@PublishedプロパティとViewプロトコル適用のStruct内の@EnvironmentObjectプロパティは対になり常に同期されます。PickerView.swiftstruct PickerView: View { //TimeManagerのインスタンスを作成 @EnvironmentObject var timeManager: TimeManager //デバイスのスクリーンの幅 let screenWidth = UIScreen.main.bounds.width //デバイスのスクリーンの高さ let screenHeight = UIScreen.main.bounds.height //設定可能な時間単位の数値:0から23までの整数のArray var hours = [Int](0..<24) //設定可能な分単位の数値:0から59までの整数のArray var minutes = [Int](0..<60) //設定可能な秒単位の数値:0から59までの整数のArray var seconds = [Int](0..<60) struct PickerView: View { var body: some View { } }4. PickerViewにコンポーネントとしてPickerを配置する
PickerViewの中に、以下のコンポーネントを左から順番に横並びにします。
- 時間のPicker
- 時間単位のtext
- 分のPicker
- 分単位のtext
- 秒のPicker
- 秒単位のtext
時間Pickerの引数selectionには先に作ったインスタンスtimeManagerのプロパティhourSelectionを指定します。これにより、Pickerで選んだ値がTimeManagerクラスの同名プロパティへ代入されるようになります。
Pickerの{}内のクロージャでは、ForEachで0から23それぞれに対してPickerに数値を表示するようにし、.tagによって実際にPickerで時間を選択した時に取得する値(データ型含む)を指定します。取得する値がInt型、反映先のTimeManagerのプロパティhourSelectionもInt型で合わせています。
時間Pickerができたら、Textコンポーネントで"hour"という単位を表示するよう指定します。
同様にして、分、秒のPickerとTextも作成します。
PickerView.swiftstruct PickerView: View { //(プロパティ部分の記述省略) var body: some View { //時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに HStack { //時間単位のPicker Picker(selection: self.$timeManager.hourSelection, label: Text("hour")) { ForEach(0 ..< self.hours.count) { index in Text("\(self.hours[index])") .tag(index) } } //上下に回転するホイールスタイルを指定 .pickerStyle(WheelPickerStyle()) //ピッカーの幅をスクリーンサイズ x 0.1、高さをスクリーンサイズ x 0.4で指定 .frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4) //上のframeでクリップし、フレームからはみ出す部分は非表示にする .clipped() //時間単位を表すテキスト Text("hour") .font(.headline) //分単位のPicker Picker(selection: self.$timeManager.minSelection, label: Text("minute")) { ForEach(0 ..< self.minutes.count) { index in Text("\(self.minutes[index])") .tag(index) } } .pickerStyle(WheelPickerStyle()) .frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4) .clipped() //分単位を表すテキスト Text("min") .font(.headline) //秒単位のPicker Picker(selection: self.$timeManager.secSelection, label: Text("second")) { ForEach(0 ..< self.seconds.count) { index in Text("\(self.seconds[index])") .tag(index) } } .pickerStyle(WheelPickerStyle()) .frame(width:self.screenWidth * 0.1, height: self.screenWidth * 0.4) .clipped() //秒単位を表すテキスト Text("sec") .font(.headline) } } }5. TimeManagerに、Pickerで設定・取得した時間を最大時間、および残り時間のプロパティに反映するためのメソッドを作成
TimeManagerクラス内にsetTimerというメソッドを作成します。
PickerViewからTimeManagerの時間・分・秒のプロパティの値を取得できるようになっているので、それらの値を秒に換算して合計し、残り時間および最大時間のプロパティに反映します。併せて、時間表示形式も合計時間によって条件分岐するようにします。TimeManager.swiftclass TimeManager: ObservableObject { //Pickerで設定した"時間"を格納する変数 @Published var hourSelection: Int = 0 //Pickerで設定した"分"を格納する変数 @Published var minSelection: Int = 0 //Pickerで設定した"秒"を格納する変数 @Published var secSelection: Int = 0 //カウントダウン残り時間 @Published var duration: Double = 0 //カウントダウン開始前の最大時間 @Published var maxValue: Double = 0 //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式 @Published var displayedTimeFormat: TimeFormat = .min //Pickerで取得した値からカウントダウン残り時間とカウントダウン開始前の最大時間を計算し、 //その値によって時間表示形式も指定する func setTimer() { //残り時間をPickerから取得した時間・分・秒の値をすべて秒換算して合計して求める duration = Double(hourSelection * 3600 + minSelection * 60 + secSelection) //Pickerで時間を設定した時点=カウントダウン開始前のため、残り時間=最大時間とする maxValue = duration //時間表示形式を残り時間(最大時間)から指定する //60秒未満なら00形式、60秒以上3600秒未満なら00:00形式、3600秒以上なら00:00:00形式 if duration < 60 { displayedTimeFormat = .sec } else if duration < 3600 { displayedTimeFormat = .min } else { displayedTimeFormat = .hr } } }さらに、上記setTimerメソッドを、PickerViewで時間を設定したタイミングで発動できるように、PickerViewのほうにボタンを追加します。最大時間=残り時間という式が入っているため、カウントダウン前つまり時間設定操作の直後が理想的だからです。
ボタンのアイコンはApple純正SF Symbolsから"checkmark.circle.fill"を使います。
.offsetモディファイアでデフォルトの中央配置からやや下にずらして配置し、PickerViewと重ならないように調整しています。
.opacityモディファイアで、時間、分、秒、いずれも設定が0の場合は透明度を 10%(x 0.1)にして、今はタップできませんというのを視覚的に表現しています。
モディファイアの引数を入力する際、ifとelseを使う代わりに「△△ ? □□ : ○○」 と記載することで「もし △△ だったら □□ そうでなければ ○○」という意味に完結に条件分岐する引数を与えられて便利です。また「&&」は「かつ」という意味で複数の条件に当てはまる場合を表現する際に使います。
.onTapGestureモディファイアの{}のクロージャ内で、時間、分、秒いずれかのプロパティの値が0でなければ、タップしたときにsetTimerメソッドが発動するようにします。if構文内の「||」は「または」の意味です。
PickerView.swiftstruct PickerView: View { //TimeManagerのインスタンスを作成 @EnvironmentObject var timeManager: TimeManager //デバイスのスクリーンの幅 let screenWidth = UIScreen.main.bounds.width //デバイスのスクリーンの高さ let screenHeight = UIScreen.main.bounds.height //設定可能な時間単位の数値 var hours = [Int](0..<24) //設定可能な分単位の数値 var minutes = [Int](0..<60) //設定可能な秒単位の数値 var seconds = [Int](0..<60) var body: some View { //ZStackでPickerとレイヤーで重なるようにボタンを配置 ZStack{ //時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに HStack { //(Picker部分省略) } //タップして設定を確定するチェックマークアイコン Image(systemName: "checkmark.circle.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 35, height: 35) .offset(y: self.screenWidth * 0.32) .opacity(self.timeManager.hourSelection == 0 && self.timeManager.minSelection == 0 && self.timeManager.secSelection == 0 ? 0.1 : 1) .onTapGesture { if self.timeManager.hourSelection != 0 || self.timeManager.minSelection != 0 || self.timeManager.secSelection != 0 { self.timeManager.setTimer() } } } } }6. TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成
TimeManager.swiftclass TimeManager: ObservableObject { //(省略) //カウントダウン中の残り時間を表示するためのメソッド func displayTimer() -> String { //残り時間(時間単位)= 残り合計時間(秒)/3600秒 let hr = Int(duration) / 3600 //残り時間(分単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 let min = Int(duration) % 3600 / 60 //残り時間(秒単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 で割った余り let sec = Int(duration) % 3600 % 60 //setTimerメソッドの結果によって時間表示形式を条件分岐し、上の3つの定数を組み合わせて反映 switch displayedTimeFormat { case .hr: return String(format: "%02d:%02d:%02d", hr, min, sec) case .min: return String(format: "%02d:%02d", min, sec) case .sec: return String(format: "%02d", sec) } } }
- 投稿日:2020-10-16T08:56:53+09:00
iOSアプリ開発:タイマーアプリ(1.タイマーの時間設定)
記事内容
タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。開発環境
- OS: macOS 10.15.x
- エディタ: Xcode 11.x
- 言語: Swift
手順概要
- TimeManagerというClassを用意
- TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
- PickerViewというViewを用意
- PickerViewにコンポーネントとしてPickerを配置する
- TimeManagerに、画面上で設定した時間をプロパティに反映するためのメソッドを作成
- TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成
1. TimeManagerというClassを用意
TimeManager.swiftというファイルを作成し、同名のClassを作成します。
のちのちのことを想定してObservableObjectというプロトコルを適用します。ObservableObjectとは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに@Publishedというキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値は常に監視され、他のViewから常に参照することができます。TimeManager.swiftimport SwiftUI class TimeManager: ObservableObject { }2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
カウントダウン時間の設定として、以下の5つのプロパティを用意します。
- 時間単位の設定値を格納するプロパティ
- 分単位の設定値を格納するプロパティ
- 秒単位の設定値を格納するプロパティ
- 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
- 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ
各プロパティの値は他のViewから常に参照できるようにしたいので、キーワード@Publishedをvarの前につけておきます。
時間、分、秒はPickerからInt型で取得するためIntをデータ型に指定しておきます。一方、残り時間と最大時間はDouble型を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。
それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0にしておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //Pickerで設定した"時間"を格納する変数 @Published var hourSelection: Int = 0 //Pickerで設定した"分"を格納する変数 @Published var minSelection: Int = 0 //Pickerで設定した"秒"を格納する変数 @Published var secSelection: Int = 0 //カウントダウン残り時間 @Published var duration: Double = 0 //カウントダウン開始前の最大時間 @Published var maxValue: Double = 0 }さらに、設定したカウントダウン時間の長さによって時間表示の形式を変えたい(例えば2時間30分に設定したら"02:30:00"、5分に設定したら"05:00"、30秒に設定したら"30"という表示にしたい)ので、enumを作ります。
Data.swiftというファイルを新たに作成し、TimeFormatというenumを作成します。
Data.swiftimport SwiftUI enum TimeFormat { case hr case min case sec }そして、TimeManagerのクラスに作成したTimeFormatのenumをタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず.minにしておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //(先に作成したプロパティ省略) //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式 @Published var displayedTimeFormat: TimeFormat = .min }3. PickerViewというViewタイプのStructを用意
PickerView.swiftという新しいファイルを作成し、同名のViewタイプのStruct(構造体)を作成します。
PickerView.swiftimport SwiftUI struct PickerView: View { var body: some View { } }まずは必要なプロパティを作成します。
ここで、先に作成したTimeManagerクラスのインスタンスを作成しておきます。
@EnvironmentObjectのキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObjectプロトコル適用のClass内の@PublishedプロパティとViewプロトコル適用のStruct内の@EnvironmentObjectプロパティは対になり常に同期されます。PickerView.swiftstruct PickerView: View { //TimeManagerのインスタンスを作成 @EnvironmentObject var timeManager: TimeManager //デバイスのスクリーンの幅 let screenWidth = UIScreen.main.bounds.width //デバイスのスクリーンの高さ let screenHeight = UIScreen.main.bounds.height //設定可能な時間単位の数値:0から23までの整数のArray var hours = [Int](0..<24) //設定可能な分単位の数値:0から59までの整数のArray var minutes = [Int](0..<60) //設定可能な秒単位の数値:0から59までの整数のArray var seconds = [Int](0..<60) struct PickerView: View { var body: some View { } }4. PickerViewにコンポーネントとしてPickerを配置する
PickerViewの中に、以下のコンポーネントをHStackを使って左から順番に水平方向に並べます。
- 時間のPicker
- 時間単位のtext
- 分のPicker
- 分単位のtext
- 秒のPicker
- 秒単位のtext
時間Pickerの引数selectionには先に作ったインスタンスtimeManagerのプロパティhourSelectionを指定します。これにより、Pickerで選んだ値がTimeManagerクラスの同名プロパティへ代入されるようになります。
Pickerの{}内のクロージャでは、ForEachで0から23それぞれに対してPickerに数値を表示するようにし、.tagモディファイアによって実際にPickerで時間を選択した時に取得する値(データ型含む)を指定します。取得する値がInt型、反映先のTimeManagerのプロパティhourSelectionもInt型で合わせています。
時間Pickerができたら、Textコンポーネントで"hour"という単位を表示するよう指定します。
同様にして、分、秒のPickerとTextも作成します。
PickerView.swiftstruct PickerView: View { //(プロパティ部分の記述省略) var body: some View { //時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに HStack { //時間単位のPicker Picker(selection: self.$timeManager.hourSelection, label: Text("hour")) { ForEach(0 ..< self.hours.count) { index in Text("\(self.hours[index])") .tag(index) } } //上下に回転するホイールスタイルを指定 .pickerStyle(WheelPickerStyle()) //ピッカーの幅をスクリーンサイズ x 0.1、高さをスクリーンサイズ x 0.4で指定 .frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4) //上のframeでクリップし、フレームからはみ出す部分は非表示にする .clipped() //時間単位を表すテキスト Text("hour") .font(.headline) //分単位のPicker Picker(selection: self.$timeManager.minSelection, label: Text("minute")) { ForEach(0 ..< self.minutes.count) { index in Text("\(self.minutes[index])") .tag(index) } } .pickerStyle(WheelPickerStyle()) .frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4) .clipped() //分単位を表すテキスト Text("min") .font(.headline) //秒単位のPicker Picker(selection: self.$timeManager.secSelection, label: Text("second")) { ForEach(0 ..< self.seconds.count) { index in Text("\(self.seconds[index])") .tag(index) } } .pickerStyle(WheelPickerStyle()) .frame(width:self.screenWidth * 0.1, height: self.screenWidth * 0.4) .clipped() //秒単位を表すテキスト Text("sec") .font(.headline) } } }5. TimeManagerに、Pickerで設定・取得した時間を最大時間、および残り時間のプロパティに反映するためのメソッドを作成
TimeManagerクラス内にsetTimerというメソッドを作成します。
PickerViewからTimeManagerの時間・分・秒のプロパティの値を取得できるようになっているので、それらの値を秒に換算して合計し、残り時間および最大時間のプロパティに反映します。併せて、時間表示形式も合計時間によって条件分岐するようにします。TimeManager.swiftclass TimeManager: ObservableObject { //Pickerで設定した"時間"を格納する変数 @Published var hourSelection: Int = 0 //Pickerで設定した"分"を格納する変数 @Published var minSelection: Int = 0 //Pickerで設定した"秒"を格納する変数 @Published var secSelection: Int = 0 //カウントダウン残り時間 @Published var duration: Double = 0 //カウントダウン開始前の最大時間 @Published var maxValue: Double = 0 //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式 @Published var displayedTimeFormat: TimeFormat = .min //Pickerで取得した値からカウントダウン残り時間とカウントダウン開始前の最大時間を計算し、 //その値によって時間表示形式も指定する func setTimer() { //残り時間をPickerから取得した時間・分・秒の値をすべて秒換算して合計して求める duration = Double(hourSelection * 3600 + minSelection * 60 + secSelection) //Pickerで時間を設定した時点=カウントダウン開始前のため、残り時間=最大時間とする maxValue = duration //時間表示形式を残り時間(最大時間)から指定する //60秒未満なら00形式、60秒以上3600秒未満なら00:00形式、3600秒以上なら00:00:00形式 if duration < 60 { displayedTimeFormat = .sec } else if duration < 3600 { displayedTimeFormat = .min } else { displayedTimeFormat = .hr } } }さらに、上記setTimerメソッドを、PickerViewで時間を設定したタイミングで発動できるように、PickerViewのほうにボタンを追加します。最大時間=残り時間という式が入っているため、カウントダウン前つまり時間設定操作の直後が理想的だからです。
ボタンのアイコンはApple純正SF Symbolsから"checkmark.circle.fill"を使います。
.offsetモディファイアでデフォルトの中央配置からやや下にずらして配置し、PickerViewと重ならないように調整しています。
.opacityモディファイアで、時間、分、秒、いずれも設定が0の場合は透明度を 10%(x 0.1)にして、今はタップできませんというのを視覚的に表現しています。
モディファイアの引数を入力する際、ifとelseを使う代わりに「△△ ? □□ : ○○」 と記載することで「もし △△ だったら □□ そうでなければ ○○」という意味に完結に条件分岐する引数を与えられて便利です。また「&&」は「かつ」という意味で複数の条件に当てはまる場合を表現する際に使います。
.onTapGestureモディファイアの{}のクロージャ内で、時間、分、秒いずれかのプロパティの値が0でなければ、タップしたときにsetTimerメソッドが発動するようにします。if構文内の「||」は「または」の意味です。
PickerView.swiftstruct PickerView: View { //TimeManagerのインスタンスを作成 @EnvironmentObject var timeManager: TimeManager //デバイスのスクリーンの幅 let screenWidth = UIScreen.main.bounds.width //デバイスのスクリーンの高さ let screenHeight = UIScreen.main.bounds.height //設定可能な時間単位の数値 var hours = [Int](0..<24) //設定可能な分単位の数値 var minutes = [Int](0..<60) //設定可能な秒単位の数値 var seconds = [Int](0..<60) var body: some View { //ZStackでPickerとレイヤーで重なるようにボタンを配置 ZStack{ //時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに HStack { //(Picker部分省略) } //タップして設定を確定するチェックマークアイコン Image(systemName: "checkmark.circle.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 35, height: 35) .offset(y: self.screenWidth * 0.32) .opacity(self.timeManager.hourSelection == 0 && self.timeManager.minSelection == 0 && self.timeManager.secSelection == 0 ? 0.1 : 1) .onTapGesture { if self.timeManager.hourSelection != 0 || self.timeManager.minSelection != 0 || self.timeManager.secSelection != 0 { self.timeManager.setTimer() } } } } }6. TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成
displayTimerという名前のメソッドにします。String型の戻り値を返します。
残り時間の計算は算数ですが以下のとおりです。
ちなみに、%は割り算の余りを計算できる演算子です。
- 合計残り時間(秒) / 3600(秒) = 残り時間(時間)
- 合計残り時間(秒) % 3600(秒)/60(秒) = 残り時間(分)
- 合計残り時間(秒) / 3600(秒) % 60(秒) = 残り時間(秒)
上記計算で得られた3つの数値を、文字列型データとして画面に横並びに表示します。
String(format: "%02d:%02d:%02d", hr, min, sec)
このコードは、String(format: “桁数指定”, 値) という表記です。特に "" 内の %02d は全桁数が2桁で、2桁に満たない場合は大きい桁から0で埋めるという意味になります。そして %02d の箇所にその後ろに記載した値がカンマ区切りで左から順番に入る形になっています。
例えば、以下のように画面に表示されます。
- 残り時間が4000秒だったら 01:06:40
- 残り時間が350秒だったら 05:50
- 残り時間が7秒だったら、07
TimeManager.swiftclass TimeManager: ObservableObject { //(省略) //カウントダウン中の残り時間を表示するためのメソッド func displayTimer() -> String { //残り時間(時間単位)= 残り合計時間(秒)/3600秒 let hr = Int(duration) / 3600 //残り時間(分単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 let min = Int(duration) % 3600 / 60 //残り時間(秒単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 で割った余り let sec = Int(duration) % 3600 % 60 //setTimerメソッドの結果によって時間表示形式を条件分岐し、上の3つの定数を組み合わせて反映 switch displayedTimeFormat { case .hr: return String(format: "%02d:%02d:%02d", hr, min, sec) case .min: return String(format: "%02d:%02d", min, sec) case .sec: return String(format: "%02d", sec) } } }
- 投稿日:2020-10-16T08:56:53+09:00
iOSアプリ開発:タイマーアプリ(タイマーセット)
記事内容
タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。開発環境
- OS: macOS 10.15.x
- エディタ: Xcode 11.x
- 言語: Swift
手順概要
- TimeManagerというClassを用意
- TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
- PickerViewというViewを用意
- PickerViewにコンポーネントとしてPickerを配置する
- TimeManagerに、画面上で設定した時間をプロパティに反映するためのメソッドを作成
- TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成
1. TimeManagerというClassを用意
TimeManager.swiftというファイルを作成し、同名のClassを作成します。
のちのちのことを想定してObservableObjectというプロトコルを適用します。ObservableObjectとは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに@Publishedというキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値は常に監視され、他のViewから常に参照することができます。TimeManager.swiftimport SwiftUI class TimeManager: ObservableObject { }2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
カウントダウン時間の設定として、以下の5つのプロパティを用意します。
- 時間単位の設定値を格納するプロパティ
- 分単位の設定値を格納するプロパティ
- 秒単位の設定値を格納するプロパティ
- 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
- 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ
各プロパティの値は他のViewから常に参照できるようにしたいので、キーワード@Publishedをvarの前につけておきます。
時間、分、秒はPickerからInt型で取得するためIntをデータ型に指定しておきます。一方、残り時間と最大時間はDouble型を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。
それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0にしておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //Pickerで設定した"時間"を格納する変数 @Published var hourSelection: Int = 0 //Pickerで設定した"分"を格納する変数 @Published var minSelection: Int = 0 //Pickerで設定した"秒"を格納する変数 @Published var secSelection: Int = 0 //カウントダウン残り時間 @Published var duration: Double = 0 //カウントダウン開始前の最大時間 @Published var maxValue: Double = 0 }さらに、設定したカウントダウン時間の長さによって時間表示の形式を変えたい(例えば2時間30分に設定したら"02:30:00"、5分に設定したら"05:00"、30秒に設定したら"30"という表示にしたい)ので、enumを作ります。
Data.swiftというファイルを新たに作成し、TimeFormatというenumを作成します。
Data.swiftimport SwiftUI enum TimeFormat { case hr case min case sec }そして、TimeManagerのクラスに作成したTimeFormatのenumをタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず.minにしておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //(省略) //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式 @Published var displayedTimeFormat: TimeFormat = .min }3. PickerViewというViewタイプのStructを用意
PickerView.swiftという新しいファイルを作成し、同名のViewタイプのStruct(構造体)を作成します。
PickerView.swiftimport SwiftUI struct PickerView: View { var body: some View { } }まずは必要なプロパティを作成します。
ここで、先に作成したTimeManagerクラスのインスタンスを作成しておきます。
@EnvironmentObjectのキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObjectプロトコル適用のClass内の@PublishedプロパティとViewプロトコル適用のStruct内の@EnvironmentObjectプロパティは対になり常に同期されます。PickerView.swiftstruct PickerView: View { //TimeManagerのインスタンスを作成 @EnvironmentObject var timeManager: TimeManager //デバイスのスクリーンの幅 let screenWidth = UIScreen.main.bounds.width //デバイスのスクリーンの高さ let screenHeight = UIScreen.main.bounds.height //設定可能な時間単位の数値:0から23までの整数のArray var hours = [Int](0..<24) //設定可能な分単位の数値:0から59までの整数のArray var minutes = [Int](0..<60) //設定可能な秒単位の数値:0から59までの整数のArray var seconds = [Int](0..<60) struct PickerView: View { var body: some View { } }4. PickerViewにコンポーネントとしてPickerを配置する
PickerViewの中に、以下のコンポーネントを左から順番に横並びにします。
- 時間のPicker
- 時間単位のtext
- 分のPicker
- 分単位のtext
- 秒のPicker
- 秒単位のtext
時間Pickerの引数selectionには先に作ったインスタンスtimeManagerのプロパティhourSelectionを指定します。これにより、Pickerで選んだ値がTimeManagerクラスの同名プロパティへ代入されるようになります。
Pickerの{}内のクロージャでは、ForEachで0から23それぞれに対してPickerに数値を表示するようにし、.tagによって実際にPickerで時間を選択した時に取得する値(データ型含む)を指定します。取得する値がInt型、反映先のTimeManagerのプロパティhourSelectionもInt型で合わせています。
時間Pickerができたら、Textコンポーネントで"hour"という単位を表示するよう指定します。
同様にして、分、秒のPickerとTextも作成します。
PickerView.swiftstruct PickerView: View { //(プロパティ部分の記述省略) var body: some View { //時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに HStack { //時間単位のPicker Picker(selection: self.$timeManager.hourSelection, label: Text("hour")) { ForEach(0 ..< self.hours.count) { index in Text("\(self.hours[index])") .tag(index) } } //上下に回転するホイールスタイルを指定 .pickerStyle(WheelPickerStyle()) //ピッカーの幅をスクリーンサイズ x 0.1、高さをスクリーンサイズ x 0.4で指定 .frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4) //上のframeでクリップし、フレームからはみ出す部分は非表示にする .clipped() //時間単位を表すテキスト Text("hour") .font(.headline) //分単位のPicker Picker(selection: self.$timeManager.minSelection, label: Text("minute")) { ForEach(0 ..< self.minutes.count) { index in Text("\(self.minutes[index])") .tag(index) } } .pickerStyle(WheelPickerStyle()) .frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4) .clipped() //分単位を表すテキスト Text("min") .font(.headline) //秒単位のPicker Picker(selection: self.$timeManager.secSelection, label: Text("second")) { ForEach(0 ..< self.seconds.count) { index in Text("\(self.seconds[index])") .tag(index) } } .pickerStyle(WheelPickerStyle()) .frame(width:self.screenWidth * 0.1, height: self.screenWidth * 0.4) .clipped() //秒単位を表すテキスト Text("sec") .font(.headline) } } }5. TimeManagerに、Pickerで設定・取得した時間を最大時間、および残り時間のプロパティに反映するためのメソッドを作成
TimeManagerクラス内にsetTimerというメソッドを作成します。
PickerViewからTimeManagerの時間・分・秒のプロパティの値を取得できるようになっているので、それらの値を秒に換算して合計し、残り時間および最大時間のプロパティに反映します。併せて、時間表示形式も合計時間によって条件分岐するようにします。TimeManager.swiftclass TimeManager: ObservableObject { //Pickerで設定した"時間"を格納する変数 @Published var hourSelection: Int = 0 //Pickerで設定した"分"を格納する変数 @Published var minSelection: Int = 0 //Pickerで設定した"秒"を格納する変数 @Published var secSelection: Int = 0 //カウントダウン残り時間 @Published var duration: Double = 0 //カウントダウン開始前の最大時間 @Published var maxValue: Double = 0 //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式 @Published var displayedTimeFormat: TimeFormat = .min //Pickerで取得した値からカウントダウン残り時間とカウントダウン開始前の最大時間を計算し、 //その値によって時間表示形式も指定する func setTimer() { //残り時間をPickerから取得した時間・分・秒の値をすべて秒換算して合計して求める duration = Double(hourSelection * 3600 + minSelection * 60 + secSelection) //Pickerで時間を設定した時点=カウントダウン開始前のため、残り時間=最大時間とする maxValue = duration //時間表示形式を残り時間(最大時間)から指定する //60秒未満なら00形式、60秒以上3600秒未満なら00:00形式、3600秒以上なら00:00:00形式 if duration < 60 { displayedTimeFormat = .sec } else if duration < 3600 { displayedTimeFormat = .min } else { displayedTimeFormat = .hr } } }さらに、上記setTimerメソッドを、PickerViewで時間を設定したタイミングで発動できるように、PickerViewのほうにボタンを追加します。最大時間=残り時間という式が入っているため、カウントダウン前つまり時間設定操作の直後が理想的だからです。
ボタンのアイコンはApple純正SF Symbolsから"checkmark.circle.fill"を使います。
.onTapGestureの{}内クロージャ部分にて、時間、分、秒いずれかのプロパティの値が0でなければ、タップしたときにsetTimerメソッドが発動するようにします。if構文内の || は「または」の意味です。似た表現で && がありますがこちらは「かつ」です。
PickerView.swiftstruct PickerView: View { //TimeManagerのインスタンスを作成 @EnvironmentObject var timeManager: TimeManager //デバイスのスクリーンの幅 let screenWidth = UIScreen.main.bounds.width //デバイスのスクリーンの高さ let screenHeight = UIScreen.main.bounds.height //設定可能な時間単位の数値 var hours = [Int](0..<24) //設定可能な分単位の数値 var minutes = [Int](0..<60) //設定可能な秒単位の数値 var seconds = [Int](0..<60) var body: some View { //ZStackでPickerとレイヤーで重なるようにボタンを配置 ZStack{ //時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに HStack { //(Picker部分省略) } //タップして設定を確定するチェックマークアイコン Image(systemName: "checkmark.circle.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 35, height: 35) .offset(y: self.screenWidth < self.screenHeight ? self.screenWidth * 0.32 : self.screenHeight * 0.3) .opacity(self.timeManager.hourSelection == 0 && self.timeManager.minSelection == 0 && self.timeManager.secSelection == 0 ? 0.1 : 1) .onTapGesture { if self.timeManager.hourSelection != 0 || self.timeManager.minSelection != 0 || self.timeManager.secSelection != 0 { self.timeManager.setTimer() } } } } }6. TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成
TimeManager.swiftclass TimeManager: ObservableObject { //(省略) //カウントダウン中の残り時間を表示するためのメソッド func displayTimer() -> String { //残り時間(時間単位)= 残り合計時間(秒)/3600秒 let hr = Int(duration) / 3600 //残り時間(分単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 let min = Int(duration) % 3600 / 60 //残り時間(秒単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 で割った余り let sec = Int(duration) % 3600 % 60 //setTimerメソッドの結果によって時間表示形式を条件分岐し、上の3つの定数を組み合わせて反映 switch displayedTimeFormat { case .hr: return String(format: "%02d:%02d:%02d", hr, min, sec) case .min: return String(format: "%02d:%02d", min, sec) case .sec: return String(format: "%02d", sec) } } }
- 投稿日:2020-10-16T03:59:45+09:00
[Swift] 初心者がCocoaPods導入でつまづいた事&導入手順
今回の題
初めまして、swiftの学習を初めて2週間ちょっとの人です。
利用したいライブラリがあり、パッケージ管理にCocoaPodsを導入しようとしたところ、何箇所か詰まった箇所があったのでそれをメモしておきます。
エラー解消しながら導入手順も載せてます。
何か間違い等あればお手数ですが、コメントにお願いいたします。環境
macOS Catalina 10.15.6
xcode 11.1CocoaPodsのインストール
ターミナルを起動して以下のコマンドでcocoapodsをインストールします。
sudo gem install cocoapods
が、早速ここで以下のようなエラーが出ました。
ERROR: Loading command: install (LoadError) dlopen(×××××××××/.rbenv/versions/2.5.0/lib/ruby/2.5.0/x86_64-darwin16/openssl.bundle, 9): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib Referenced from: ×××××××××/.rbenv/versions/2.5.0/lib/ruby/2.5.0/x86_64-darwin16/openssl.bundle Reason: image not found - ×××××××××/.rbenv/versions/2.5.0/lib/ruby/2.5.0/x86_64-darwin16/openssl.bundle ERundefined method `invoke_with_build_arこのエラーの解決策がこちらの記事にある、
$ rbenv uninstall 2.5.0 $ rbenv install 2.5.0を行う事で解決できました。
バージョンはエラー内容にある自分の環境のものに置き換えてください。
私の場合は、エラー内容に2.5.0
とあったのでそれを指定して行いました。そしたらもう一度インストールを実行。
sudo gem install cocoapods
今度は成功しました。
設定
ライブラリの情報をセットアップ。
pod setup
そうしたらプロジェクトまで移動します。
私はここで、
プロジェクトってどの階層だ?
ってなったのですが、プロジェクト名.xcodeproj
がある階層をさしているそうです。
間違えて更にその下の階層に行くとこでした汗Podfileの作成
以下のコマンドを実行すると、今いる階層(プロジェクト)に
Podfile
というファイルが作成されます。pod init
Podfileを編集
初期状態で以下のようになっています。
# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'UIMenuItem' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for UIMenuItem end今回私は、FloatingPanelというライブラリを使いたかったので、以下のように編集します。
# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'UIMenuItem' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for UIMenuItem pod 'FloatingPanel' # 追記 end
pod 'FloatingPanel'
を追記しました編集が終わったらファイルを保存し閉じます。
ライブラリのインストール
以下のコマンドを実行する事でPodfileに記述に従ってライブラリがインストールされます。
pod install
ライブラリを使ってみる
ここで少し勘違いをして詰まりました。
これまで、
プロジェクト名.xcodeproj
というファイルを開いて作業してきたのですが、そこだとライブラリが使用できませんでした。調べてみると、CocoaPods導入後は、
プロジェクト名.xcworkspace
というファイルで作業することになります。
同じ階層に作られているので間違えずにこちらを開いてください。ライブラリをimportします。
ViewControllerimport UIKit import FloatingPanel // ライブラリをimport class ViewController: UIViewController { // 略ここで終わっている記事が多かったのですが、私の場合、
No such module 'FloatingPanel'
というエラーが出ました。
このエラーの解決策がこちらです。
stackoverflowのかなり古い質問ですが、そのまま試したらスルッとエラーが消えました。手順を書いておきます。
まず、以下の画像を参考に
Manage Schemes...
を見つけ出してクリックしてください。
下記のようなメニューの中にライブラリ名があるのでチェックをつけてください。
これでエラーが消えます。
以上です。
- 投稿日:2020-10-16T02:01:21+09:00
【iOS開発】アプリ開発中に陥りがちな罠
はじめに
自分がiOSアプリ開発をしてきた中で「陥りがちだな」と思った罠をまとめてみました。
リリース申請前に確認するといいかも?レイアウト、UI系
端末によってレイアウトが崩れる
AutoLayoutを見直そう Previewを活用しよう
AutoLayoutというのは、LabelやButtonなどの部品に、部品の大きさや他の部品との距離といっった制約を設定することで、異なる大きさのiPhoneでも自動的にレイアウトを整えてくれる機能のことです。
iPhoneの画面サイズの種類はとても多いので、AutoLayoutで制約をつけておかないと、リリース申請時にリジェクトされると思います。AutoLayoutの分かりやすい記事
https://qiita.com/_ha1f/items/5c292bb6a4617da60d4fPreviewというのは、AutoLayoutで設定した制約がしっかり機能しているかどうかを、ビルドなしで確認することができる機能のことです。
Previewの分かりやすい記事
http://developers.goalist.co.jp/entry/2017/06/29/170000iPadのSplit View/Slide Overでレイアウトが崩れる
AutoLayoutを見直そう Requires full screenを設定しよう(非推奨)
Split View/Slide Overというのは、iPadの機能の1つで、1つの画面上に2つのアプリを起動させることができる機能のことです。
AutoLayoutを設定する時、この機能たちの存在を忘れてしまいがちです。
気をつけたいアプリのGeneralの項目にある、Requires full screenというチェックボックスをONにすると、Split ViewとSlide Overを無効にすることができます。
画面サイズ4インチ以下のiPhoneのみ、着信状態にレイアウトが崩れる
AutoLayoutを見直そう
これは本当に盲点中の盲点だと思います。
iPhoneSE(第1世代)などの4インチのiPhoneは、着信時にステータスバーが太くなります。
シュミレーターを起動中に⌘Yを押すと、着信時のレイアウトを確認することができます。ダークモード
ダークモードに対応しよう
iOS13からの新機能のダークモードは、アプリ全体を黒を基調にした配色に変更することができるモードです。
Color Setという機能を使えば簡単に実装できます。ダークモード対応の分かりやすい記事
https://qiita.com/hirothings/items/4834481d170332e173f5ランチスクリーン
Launch Screenを設定しよう
Launch Screenというのは、アプリの起動中に表示される画面のことです。
Launch Screenの分かりやすい記事
https://qiita.com/h1d3mun3/items/93d1e00e8f47a7ed6215モーダル遷移
遷移の設定を確認しよう
iOS13から、モーダル遷移のデフォルト設定が
.automatic
という、遷移元のViewの上に遷移先のViewが上に重なる遷移方法になっています。
しかし、この方法だとユーザーは自分の意思で前の画面に戻ることができるので、勝手に戻って欲しくない場合は.fullScreen
に設定しましょう。モーダル遷移をfullScreenにすることに関する分かりやすい記事
https://qiita.com/akatsuki174/items/fd74cb1e08b70da5e4fd旧バージョンとの互換
iOS12以下で起動すると画面が真っ暗になる
SceneDelegate、AppDelegateを見直そう
iOS13から、新しくUIWindowSceneDelegateが導入されました。
しかしiOS12まではそんな概念はなかったので、iOS13以上用に開発していたアプリをiOS12以下で動かすには、コードを少し追記する必要があります。
iOS12で実際にビルドするまで、画面が真っ暗かどうかはわからないので、気づかずに申請してしまいがちです。SceneDelegateなどの分かりやすい記事
https://qiita.com/sakuto0116/items/7caad22f041375810c9ciOS12以下だとSF Symbolsが消える
SF Symbolsを画像にする
SF Symbolsとは、iOS用のアイコンの素材集のようなもので、Appleの標準アプリのようなアイコンをすぐに使うことができます。
しかしiOS12まではそんなものなかったので、SF SymbolsをiOS12以下で使いたい時は、SF Symbolsを画像にする必要があります。
そんな時におすすめなのがHappyCoding
というソフトです。
https://happycoding.app/
このソフトは、SF Symbols以外にも、アプリの多言語化やプライバシー関係など様々な機能があるのでおすすめです。HappyCodingの使い方の分かりやすい記事
https://happycoding.app/
↑この記事にはSF Symbolsを画像化させる方法が載っていなかったので、今度自分が記事書いておきます。その他
iPadでポップオーバーしようとすると落ちる
sourceView
を設定するUIAlertControllerのActionSheetや、UIActivityViewControllerなどをiPadで使う場合は、
sourceView
を設定する必要があります。
iPhoneではsourceView
を設定しなくても問題なく動くので、忘れがちです。分かりやすい記事
なかったので今度自分で書いておきます。
簡単にいうとこういうことですactivityViewController.popoverPresentationController.sourceView = ButtonSign in with Appleを実装する
Sign in with Appleを実装する
サードパーティーのログイン連携をしているアプリは、Sign in with Appleの実装が必須です!
分かりやすい記事
https://qiita.com/shiz/items/5e094910f742c2ad72a4おわりに
今後、また罠を見つけたら追記していこうと思います
この記事が役に立ったと思ったらLGTMお願いします?
少しでも多くの人の役に立てたなら幸いです
「他にもこんな落とし穴あるよ!」というのがあれば是非コメント欄で教えてください