20201016のiOSに関する記事は12件です。

for-in文を理解しよう!

今回は、for-in文について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください

for-in文とは?

for-in文は各要素を実行文に順次渡しながら、その要素の数だけ繰り返しを行います。

基本的な書き方は以下の通りです

qiita.rbvar
for 要素名 in シーケンス { 
要素ごとに繰り返し実行される文
 }

※シーケンス・・・連続(しているもの)、一続き(のもの)、順序、順番、並び、配列(する)、逐次、並べる、順序付ける、などの意味を持つ。

次は簡単な例を見ていきましょう。

qiita.rbvar
1.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.rbvar
let dictionary = ["a": 1,"b": 2]

for (key,value) in dictionary {

print("key:\(key),Value\(value)")
}
実行結果
key: a Value: 1
key: b Value: 2

break文

break文は、実行文を中断し、さらに、繰り返し文全体を終了します。例えば、これ以上繰り返しを行なう必要が奈君あた場合などに使用します。

簡単な例を見ていきましょう。

qiita.rbvar
var 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.rbvar
var 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文とは異なり後続の繰り返しは継続され、全ての要素にたいして処理が行われていることがわかります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ReactNative】iOS14に関する不具合等のまとめ

概要

iOS14が正式にリリースされ、ReactNativeで開発したアプリにも少なからず影響が出ています。
個々の事象については、既に有志の方が記事に起こしてくれていますが、それらを俯瞰して確認できるとより便利だと思ったので記事にしました。

以下、確認されている事象と対応まとめです。

確認されている事象

日付関連

DatePickerUIがホイールでなくなる

if (@available(iOS 14, *)) {
  UIDatePicker *picker = [UIDatePicker appearance];
  picker.preferredDatePickerStyle = UIDatePickerStyleWheels;
}

Date(string)の結果が異なる

画像関連

画像が表示されない

react-native+0.61.2.patch
diff --git a/node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m b/node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m
index 01aa75f..4ef8307 100644
--- a/node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m
+++ b/node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m
@@ -269,6 +269,8 @@ - (void)displayLayer:(CALayer *)layer
   if (_currentFrame) {
     layer.contentsScale = self.animatedImageScale;
     layer.contents = (__bridge id)_currentFrame.CGImage;
+  } else {
+    [super displayLayer:layer];
   }
 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

titleTextAttributes指定時のフォント崩れ対策

概要

iOS のナビゲーションバーの文字色を変更するためにtitleTextAttributesを指定してみたら、フォントが崩れたので対応。

環境

Xcode 11.3
iOS 14.0 & 13.3

事象と対策

ナビゲーションバータイトルにフォント指定しているばあい、
色だけ指定しても、フォント指定が外れる

  • フォントを(xibで)ヒラギノ角ゴシックにしている場合
    スクリーンショット 2020-10-16 16.30.05.png

  • 特定画面でフォントの色変えたいと思って、よくあるように色だけ指定するとフォント指定が外れる(システムデフォルトになっている?)

    self.navigationController?.navigationBar.titleTextAttributes = [
    // 文字の色
        .foregroundColor: UIColor.blue
    ]


スクリーンショット 2020-10-16 16.29.42.png

  • フォントの指定も加える必要がある
        self.navigationController?.navigationBar.titleTextAttributes = [
            NSAttributedString.Key.foregroundColor: UIColor.yellow,
            NSAttributedString.Key.font: UIFont(name: "HiraginoSans-W3", size: 16) as Any
        ]


スクリーンショット 2020-10-16 16.36.08.png

当たり前っちゃ当たり前ですが、気づかなかったので、備忘録として投稿します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

titleTextAttributes指定時のフォント指定忘れ対策

概要

iOS のナビゲーションバーの文字色を変更するためにtitleTextAttributesを指定してみたら、フォントが崩れたので対応。

環境

Xcode 11.3
iOS 14.0 & 13.3

事象

ナビゲーションバータイトルにフォント指定している場合、
色だけ指定しても、フォント指定が外れる

  • フォントを(xibで)ヒラギノ角ゴシックにしている場合
    スクリーンショット 2020-10-16 16.30.05.png

  • 特定の画面でタイトルの色変えたいと思って、よくあるように色だけ指定するとフォント指定が外れる(システムデフォルトになっている?)

    self.navigationController?.navigationBar.titleTextAttributes = [
      // 文字の色
       .foregroundColor: UIColor.yellow
    ]


スクリーンショット 2020-10-16 16.29.42.png

対策

  • フォントの指定も加える必要がある
    self.navigationController?.navigationBar.titleTextAttributes = [
        NSAttributedString.Key.foregroundColor: UIColor.yellow,
        NSAttributedString.Key.font: UIFont(name: "HiraginoSans-W3", size: 16) as Any
    ]


スクリーンショット 2020-10-16 16.36.08.png

当たり前っちゃ当たり前ですが、気づかなかったので、備忘録として投稿します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS]AutoLayoutでviewをコピペしたらAuthLayoutが効かなくなってしまう問題への対応方法

長いこと謎だったんですが、UIStoryboard内のUIViewController直下のviewをコピーして貼り付けると、AutoLayoutができない状態になりますよね。

イメージ:ここの選択してるviewをコピペすると、上のようにSafeAreaがくっついてきて

スクリーンショット 2020-10-16 16.15.17.png

Autolayoutができない状態に!

スクリーンショット 2020-10-16 16.16.57.png

解決するには

どうやらLayoutがAutoresizingMask(AutoLayout切ってる旧レイアウト状態)になってるのでInferredに変更します。

スクリーンショット 2020-10-16 16.16.09.png

あと、一番下のSafe Area Layout Guideをオフにします。

解決できてスッキリ

CreatedAt: 2020/10/16 iOS14, Xcode12

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

switch文を理解しよう!

今回は、switch文について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください

switch文とは?

switch文を一言でいうと、パターンを利用して制御式の値に応じて実行文を切り替える制御構文です。

基本的な書き方は以下の通りです

qiita.rbvar
switch 制御式 {
case パターン1:
制御式がパターン1にマッチした場合に実行される文

case パターン2:
制御式がパターン2にマッチした場合に実行される文

default:
制御式がいずれのパターンにもマッチしなかった場合に実行される文
}

switch文は、一度マッチして実行文を実行するとマッチングを終了し、それ以降のパターンはスキップするという特徴を持ちます。
if文やguard文は成立するか否かの2つのケースへの分岐でしたが、switch文はさらに多くのケースに分岐できます。

次は簡単な例を見ていきましょう

qiita.rbvar
let a = -2
switch a {

case Int.min..<0:
print("aの負の値です")

case 1..<Int.max:
print("aは正の値です")

default:
print("aは0です")

}

実行結果:aは負の値です

whereキーワード

whereキーワードを利用すると、ケースにマッチする条件を追加できます。
これだけと、なんのことかさっぱりなので、早速、基本構文を見ていきましょう!

qiita.rbvar
switch 制御式 {
case パターン 制御式:
制御式がパターン1にマッチし,かつ,条件式を満たす場合に実行される文

default:
制御式がいずれのパターンにもマッチしなかった場合に実行される文

次は、簡単な例です。

qiita.rbvar
let 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.rbvar
let 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.rbvar
import 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.rbvar
import 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.rbvar
let a = 1
switch a{
case 1: 
print("ログインに成功しました")
fallthrough
case 2:
print("ログインに失敗しました")
default:
print("default")
}

実行結果:
ログインに成功しました
ログインに失敗しました

上記の例では、fallthrough文によって、実行が次のケースであるcase2に移ります。したがって、case1とcase2が出力されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発:タイマーアプリ(まとめ)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載していきます。タイマーだけで寂しいので、複数人のプレゼンテーションでの進行にも役立つような機能も。

タイマーアプリの主な機能

  • タイマーをセットする
  • タイマーを表示する
  • スタート/ストップ/リセットボタン
  • プログレスバー
  • アラーム・バイブレーションを発動する
  • 設定画面
  • プレゼンテーションモード
  • 発表者リスト
  • 発表者設定画面

開発環境

  • OS: macOS 10.15.x
  • エディタ: Xcode 11.x
  • 言語: Swift

手順

  1. iOSアプリ開発:タイマーアプリ(1.タイマーの時間設定)
  2. iOSアプリ開発:タイマーアプリ(2.タイマーの表示)
  3. iOSアプリ開発:タイマーアプリ(3.スタート/ストップボタン、リセットボタン)
  4. 以降順次アップ予定

補足

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)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発:タイマーアプリ(1.タイマーセット)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。

開発環境

  • OS: macOS 10.15.x
  • エディタ: Xcode 11.x
  • 言語: Swift

手順概要

  1. TimeManagerというClassを用意
  2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
  3. PickerViewというViewを用意
  4. PickerViewにコンポーネントとしてPickerを配置する
  5. TimeManagerに、画面上で設定した時間をプロパティに反映するためのメソッドを作成
  6. TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成

1. TimeManagerというClassを用意

TimeManager.swiftというファイルを作成し、同名のClassを作成します。
のちのちのことを想定してObservableObjectというプロトコルを適用します。ObservableObjectとは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに@Publishedというキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値は常に監視され、他のViewから常に参照することができます。

TimeManager.swift
import SwiftUI

class TimeManager: ObservableObject {

}

2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成

カウントダウン時間の設定として、以下の5つのプロパティを用意します。

  • 時間単位の設定値を格納するプロパティ
  • 分単位の設定値を格納するプロパティ
  • 秒単位の設定値を格納するプロパティ
  • 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
  • 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ

各プロパティの値は他のViewから常に参照できるようにしたいので、キーワード@Publishedをvarの前につけておきます。

時間、分、秒はPickerからInt型で取得するためIntをデータ型に指定しておきます。一方、残り時間と最大時間はDouble型を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。

それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0にしておきます。

TimeManager.swift
class 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.swift
import SwiftUI

enum TimeFormat {
    case hr
    case min
    case sec
}

そして、TimeManagerのクラスに作成したTimeFormatのenumをタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず.minにしておきます。

TimeManager.swift
class TimeManager: ObservableObject {
    //(省略)

    //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式
    @Published var displayedTimeFormat: TimeFormat = .min
}

3. PickerViewというViewタイプのStructを用意

PickerView.swiftという新しいファイルを作成し、同名のViewタイプのStruct(構造体)を作成します。

PickerView.swift
import SwiftUI

struct PickerView: View {
    var body: some View {
    }
}

まずは必要なプロパティを作成します。
ここで、先に作成したTimeManagerクラスのインスタンスを作成しておきます。
@EnvironmentObjectのキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObjectプロトコル適用のClass内の@PublishedプロパティとViewプロトコル適用のStruct内の@EnvironmentObjectプロパティは対になり常に同期されます。

PickerView.swift
struct 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.swift
struct 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.swift
class 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.swift
struct 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.swift
class 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)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発:タイマーアプリ(1.タイマーをセット)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。

開発環境

  • OS: macOS 10.15.x
  • エディタ: Xcode 11.x
  • 言語: Swift

手順概要

  1. TimeManagerというClassを用意
  2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
  3. PickerViewというViewを用意
  4. PickerViewにコンポーネントとしてPickerを配置する
  5. TimeManagerに、画面上で設定した時間をプロパティに反映するためのメソッドを作成
  6. TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成

1. TimeManagerというClassを用意

TimeManager.swiftというファイルを作成し、同名のClassを作成します。
のちのちのことを想定してObservableObjectというプロトコルを適用します。ObservableObjectとは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに@Publishedというキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値は常に監視され、他のViewから常に参照することができます。

TimeManager.swift
import SwiftUI

class TimeManager: ObservableObject {

}

2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成

カウントダウン時間の設定として、以下の5つのプロパティを用意します。

  • 時間単位の設定値を格納するプロパティ
  • 分単位の設定値を格納するプロパティ
  • 秒単位の設定値を格納するプロパティ
  • 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
  • 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ

各プロパティの値は他のViewから常に参照できるようにしたいので、キーワード@Publishedをvarの前につけておきます。

時間、分、秒はPickerからInt型で取得するためIntをデータ型に指定しておきます。一方、残り時間と最大時間はDouble型を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。

それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0にしておきます。

TimeManager.swift
class 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.swift
import SwiftUI

enum TimeFormat {
    case hr
    case min
    case sec
}

そして、TimeManagerのクラスに作成したTimeFormatのenumをタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず.minにしておきます。

TimeManager.swift
class TimeManager: ObservableObject {
    //(省略)

    //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式
    @Published var displayedTimeFormat: TimeFormat = .min
}

3. PickerViewというViewタイプのStructを用意

PickerView.swiftという新しいファイルを作成し、同名のViewタイプのStruct(構造体)を作成します。

PickerView.swift
import SwiftUI

struct PickerView: View {
    var body: some View {
    }
}

まずは必要なプロパティを作成します。
ここで、先に作成したTimeManagerクラスのインスタンスを作成しておきます。
@EnvironmentObjectのキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObjectプロトコル適用のClass内の@PublishedプロパティとViewプロトコル適用のStruct内の@EnvironmentObjectプロパティは対になり常に同期されます。

PickerView.swift
struct 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.swift
struct 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.swift
class 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.swift
struct 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.swift
class 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)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発:タイマーアプリ(1.タイマーの時間設定)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。

開発環境

  • OS: macOS 10.15.x
  • エディタ: Xcode 11.x
  • 言語: Swift

手順概要

  1. TimeManagerというClassを用意
  2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
  3. PickerViewというViewを用意
  4. PickerViewにコンポーネントとしてPickerを配置する
  5. TimeManagerに、画面上で設定した時間をプロパティに反映するためのメソッドを作成
  6. TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成

1. TimeManagerというClassを用意

TimeManager.swiftというファイルを作成し、同名のClassを作成します。
のちのちのことを想定してObservableObjectというプロトコルを適用します。ObservableObjectとは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに@Publishedというキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値は常に監視され、他のViewから常に参照することができます。

TimeManager.swift
import SwiftUI

class TimeManager: ObservableObject {

}

2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成

カウントダウン時間の設定として、以下の5つのプロパティを用意します。

  • 時間単位の設定値を格納するプロパティ
  • 分単位の設定値を格納するプロパティ
  • 秒単位の設定値を格納するプロパティ
  • 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
  • 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ

各プロパティの値は他のViewから常に参照できるようにしたいので、キーワード@Publishedをvarの前につけておきます。

時間、分、秒はPickerからInt型で取得するためIntをデータ型に指定しておきます。一方、残り時間と最大時間はDouble型を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。

それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0にしておきます。

TimeManager.swift
class 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.swift
import SwiftUI

enum TimeFormat {
    case hr
    case min
    case sec
}

そして、TimeManagerのクラスに作成したTimeFormatのenumをタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず.minにしておきます。

TimeManager.swift
class TimeManager: ObservableObject {
    //(先に作成したプロパティ省略)
    //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式
    @Published var displayedTimeFormat: TimeFormat = .min
}

3. PickerViewというViewタイプのStructを用意

PickerView.swiftという新しいファイルを作成し、同名のViewタイプのStruct(構造体)を作成します。

PickerView.swift
import SwiftUI

struct PickerView: View {
    var body: some View {
    }
}

まずは必要なプロパティを作成します。
ここで、先に作成したTimeManagerクラスのインスタンスを作成しておきます。
@EnvironmentObjectのキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObjectプロトコル適用のClass内の@PublishedプロパティとViewプロトコル適用のStruct内の@EnvironmentObjectプロパティは対になり常に同期されます。

PickerView.swift
struct 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.swift
struct 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.swift
class 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.swift
struct 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.swift
class 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)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発:タイマーアプリ(タイマーセット)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。

開発環境

  • OS: macOS 10.15.x
  • エディタ: Xcode 11.x
  • 言語: Swift

手順概要

  1. TimeManagerというClassを用意
  2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成
  3. PickerViewというViewを用意
  4. PickerViewにコンポーネントとしてPickerを配置する
  5. TimeManagerに、画面上で設定した時間をプロパティに反映するためのメソッドを作成
  6. TimeManagerに、カウントダウン中の残り時間を表示するためのメソッドを作成

1. TimeManagerというClassを用意

TimeManager.swiftというファイルを作成し、同名のClassを作成します。
のちのちのことを想定してObservableObjectというプロトコルを適用します。ObservableObjectとは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに@Publishedというキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値は常に監視され、他のViewから常に参照することができます。

TimeManager.swift
import SwiftUI

class TimeManager: ObservableObject {

}

2. TimeManagerに、カウントダウンの時間を格納するためのプロパティを作成

カウントダウン時間の設定として、以下の5つのプロパティを用意します。

  • 時間単位の設定値を格納するプロパティ
  • 分単位の設定値を格納するプロパティ
  • 秒単位の設定値を格納するプロパティ
  • 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
  • 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ

各プロパティの値は他のViewから常に参照できるようにしたいので、キーワード@Publishedをvarの前につけておきます。

時間、分、秒はPickerからInt型で取得するためIntをデータ型に指定しておきます。一方、残り時間と最大時間はDouble型を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。

それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0にしておきます。

TimeManager.swift
class 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.swift
import SwiftUI

enum TimeFormat {
    case hr
    case min
    case sec
}

そして、TimeManagerのクラスに作成したTimeFormatのenumをタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず.minにしておきます。

TimeManager.swift
class TimeManager: ObservableObject {
    //(省略)

    //設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式
    @Published var displayedTimeFormat: TimeFormat = .min
}

3. PickerViewというViewタイプのStructを用意

PickerView.swiftという新しいファイルを作成し、同名のViewタイプのStruct(構造体)を作成します。

PickerView.swift
import SwiftUI

struct PickerView: View {
    var body: some View {
    }
}

まずは必要なプロパティを作成します。
ここで、先に作成したTimeManagerクラスのインスタンスを作成しておきます。
@EnvironmentObjectのキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObjectプロトコル適用のClass内の@PublishedプロパティとViewプロトコル適用のStruct内の@EnvironmentObjectプロパティは対になり常に同期されます。

PickerView.swift
struct 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.swift
struct 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.swift
class 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.swift
struct 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.swift
class 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)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】 iOS端末のミュージックライブラリから曲を取得して再生させる

UnityでiOS端末のミュージックライブラリの曲が必要になったのですが、参考になるサイトの情報が不足してエラーまみれになったり、記事自体が古かったので備忘録としてまとめます。

Objective-Cは、触って4日間の超初心者クオリティなので無駄なところがあるかもしれません。

AppleMusicの曲やDRMで保護がかかった曲は再生できないのでご了承ください。
(iTunesで購入した曲やCDからインポートした曲の再生は確認済み)

取得までの流れ

  1. 直接はUnityからアクセス出来ないのでObjective-Cでミュージックライブラリにアクセス
  2. Objective-Cでアクセスできても曲自体は直接取ることができないのでアプリのDocumentsフォルダ以下に曲をwav形式でエクスポート
  3. DocumentsフォルダならUnityから直接アクセス出来るのでエクスポートしたwavファイルをWWWを用いてAudioClipに変換
  4. 取得したAudioClipをAudioSourceにセットして再生!

今回はミュージックライブラリから1曲だけランダムで取得して再生するまでやっていきたいと思います。

ミュージックライブラリにアクセスしてエクスポートするObjective-Cのファイルを準備

まずは、Unityのプロジェクトを作成して、「Asset」フォルダの中に「Plugins」フォルダを作成し、その中に「iOS」フォルダを作成、そしてその中に今回は「MusicLibraryMediaPicker.mm」と言う名前のファイルを作成します。
(.mmファイルは直接作成できないと思うので一旦外部のテキストエディタでファイルを作成してドラッグ&ドロップしてインポートする方がいいと思います。)
image.png

Objective-Cファイルを操作するC#のファイルを作成

そのままでは直接Objective-Cのファイルは操作できないので今回は操作する用のC#スクリプトを「Asset」フォルダ直下に「MediaController.cs」と言う名前で作成します。

image.png

曲を再生するAudioSourceと曲名を表示するTextを作成

AudioSourceとTextを作成します。
image.png

Objective-Cの中身を作成

次はミュージックライブラリからアプリのDocumentsフォルダにエクスポートする処理を先ほど作成したObjective-Cのファイル(MusicLibraryMediaPicker.mm)に下記のソースをコピペしましょう。

こちらのサイトを参考にして私がエラー&バグを修正+加筆したソースを載せます。記事が8年前とかなり古めですがとても参考になりました。
(そのままではエラーが出るかもしれないですが今は放置で大丈夫です)

MusicLibraryMediaPicker.mm
# import <Foundation/Foundation.h>
# import <MediaPlayer/MediaPlayer.h>
# import <AVFoundation/AVAudioFile.h>
# import <AVFoundation/AVAudioEngine.h>
# import <AVFoundation/AVFoundation.h>
# import <AVFoundation/AVAssetReader.h>
# import <AVFoundation/AVAssetWriter.h>

extern "C" {

    // プロパティ
    BOOL do_export;
    long song_id;
    NSString* song_name;

    // 関数のプロトタイプ宣言
    void exportRandomToItem();
    long getSongId();
    char* getSongName();
    BOOL getDoExport();


    /***************************************************
     * MPMediaItemをwav形式でDocumentフォルダに出力する関数
     * @param item 出力したい曲のMPMediaItem
     * @return 正しく出力できたらYESを返す
     ***************************************************/
    BOOL exportItem (MPMediaItem *item) {
        // エクスポートフラグを立てる
        do_export = YES;
        // エラー表示用の変数
        NSError *error = nil;
        // WAVEファイルのフォーマット
        NSDictionary *audioSetting = [NSDictionary dictionaryWithObjectsAndKeys:
                                      [NSNumber numberWithFloat:44100.0],AVSampleRateKey,
                                      [NSNumber numberWithInt:2],AVNumberOfChannelsKey,
                                      [NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,
                                      [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
                                      [NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey,
                                      [NSNumber numberWithBool:0], AVLinearPCMIsBigEndianKey,
                                      [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
                                      [NSData data], AVChannelLayoutKey, nil];
        // 指定ファイルまでのパス
        NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
        // ↑の*urlからメディアデータへのアクセス用リンクを作成
        AVURLAsset *URLAsset = [AVURLAsset URLAssetWithURL:url options:nil];
        if (!URLAsset) {
            do_export = NO;
            return NO;
        }
        // ↑で作ったリンクをもとに指定されたアセットからメディアデータを読み取るアセットリーダーを返します。
        AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:URLAsset error:&error];
        if (error) {
            do_export = NO;
            return NO;
        }
        // メディアタイプのコンポジショントラックの配列を返す。
        NSArray *tracks = [URLAsset tracksWithMediaType:AVMediaTypeAudio];
        if (![tracks count]) {
            do_export = NO;
            return NO;
        }
        // アセットトラックからミックスされたオーディオデータを読み取る。
        AVAssetReaderAudioMixOutput *audioMixOutput = [AVAssetReaderAudioMixOutput
                                                       assetReaderAudioMixOutputWithAudioTracks:tracks
                                                       audioSettings:audioSetting];
        if (![assetReader canAddOutput:audioMixOutput]) {
            do_export = NO;
            return NO;
        }
        // 実際にミュージックデータを読み込む
        [assetReader addOutput:audioMixOutput];
        if (![assetReader startReading]) {
            do_export = NO;
            return NO;
        }
        // パスを作成
        NSArray *docDirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *docDir = [docDirs objectAtIndex:0];
        NSString *outPath = [[docDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", [item valueForProperty:MPMediaItemPropertyPersistentID]]]
                             stringByAppendingPathExtension:@"wav"];
        // 書き込みファイルまでのパスまでのリンクを作成
        NSURL *outURL = [NSURL fileURLWithPath:outPath];
        // ↑で作ったリンクをもとに指定されたUTIで指定された形式で、指定されたURLで識別されるファイルに書き込むためのアセットライターを返します。
        AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:outURL
                                                              fileType:AVFileTypeWAVE
                                                                 error:&error];
        if (error) {
            do_export = NO;
            return NO;
        }
        //ファイルが存在している場合は削除する
        NSFileManager *manager = [NSFileManager defaultManager];
        if([manager fileExistsAtPath:outPath]) [manager removeItemAtPath:outPath error:&error];
        if (error) {
            do_export = NO;
            return NO;
        }
        // データを書き込みする際に利用する
        AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
                                                                                  outputSettings:audioSetting];
        // リアルタイムで入力するか
        assetWriterInput.expectsMediaDataInRealTime = NO;
        if (![assetWriter canAddInput:assetWriterInput]) {
            do_export = NO;
            return NO;
        }
        // 書き込む情報を追加する
        [assetWriter addInput:assetWriterInput];
        if (![assetWriter startWriting]) {
            do_export = NO;
            return NO;
        }
        // コピー処理
        // ARCをオフにしているので自分で参照カウントを+1する
        [assetReader retain];
        [assetWriter retain];
        // 設定した情報を実際に書き込みを開始する
        [assetWriter startSessionAtSourceTime:kCMTimeZero];
        // 非同期処理
        dispatch_queue_t queue = dispatch_queue_create("assetWriterQueue", NULL);
        [assetWriterInput requestMediaDataWhenReadyOnQueue:queue usingBlock:^{
            while ( 1 ) {
                // ファイルの書き込みが出来るか
                if ([assetWriterInput isReadyForMoreMediaData]) {
                    // サンプルバッファーを出力用にコピーする
                    CMSampleBufferRef sampleBuffer = [audioMixOutput copyNextSampleBuffer];
                    if (sampleBuffer) {
                        // サンプルバッファーを追加する
                        [assetWriterInput appendSampleBuffer:sampleBuffer];
                        // オブジェクトを解放する
                        CFRelease(sampleBuffer);
                    } else {
                        // バッファーを追加出来ないようにする
                        [assetWriterInput markAsFinished];
                        break;
                    }
                }
            }
            // ディスパッチオブジェクトの参照(保持)カウントを減少させます。
            [assetWriter finishWriting];
            // ARCをオフにしているので自分で参照カウントを-1する
            [assetReader release];
            [assetWriter release];
            do_export = NO;
        }];
        dispatch_release(queue);
        return YES;
    }


    /**************************************
     * ランダムで曲をエクスポートする
     * @return エクスポートが完了したらYESを返す
     **************************************/
    void exportRandomToItem() {

        /// 曲情報を取得する処理
        MPMediaQuery* songQuery = [MPMediaQuery songsQuery];

        // 使える曲の配列
        NSMutableArray<MPMediaItem*>* array = [[NSMutableArray<MPMediaItem*> alloc] init];

        // ここでiCloudにしかない曲を弾く
        [songQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithBool:NO] forProperty:MPMediaItemPropertyIsCloudItem]];
        NSArray *songlists = songQuery.collections;

        // 使える曲リストを作成
        for ( int i = 0; i < [songlists count]; i++ ) {
            MPMediaItemCollection* songlist = [songlists objectAtIndex:i];
            MPMediaItem* item = [songlist representativeItem];
            if ( ![item hasProtectedAsset] ) [array addObject:item];
        }

        // 曲をエクスポート
        NSUInteger index = arc4random_uniform([array count]);
        MPMediaItem* item = [array objectAtIndex:index];
        song_id = [[item valueForProperty:MPMediaItemPropertyPersistentID] longValue];
        song_name = [item valueForProperty:MPMediaItemPropertyTitle];
        exportItem(item);
    }


    /************************************
     * セットされている曲のIDを取得する関数
     * @return セットされている曲のIDを返す
     ************************************/
    long getSongId() {
        return song_id;
    }


    /****************************************
     * セットされている曲のタイトルを取得する関数
     * @return セットされている曲のタイトルを返す
     ****************************************/
    char* getSongName() {
        return strdup([song_name UTF8String]);
    }


    /*******************************
     * コピー中かどうか判定する関数
     * @return コピー中ならYESを返す
     *******************************/
    BOOL getDoExport() {
        return do_export;
    }
}

このままではUnityから呼び出す事ができないので次はC#にObjective-Cと連携させる処理を作成します。

C#の中身を作成

ここでは、Objective-Cのコードを実行させたいので先ほど作成したC#のファイル(MediaController.cs)に下記のソースをコピペして実行できるようにしましょう。

MusicLibraryMediaPicker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
using UnityEngine.UI;
using System.IO;

public class MediaController : MonoBehaviour {

    private AudioSource audio;
    private Text text;

    #if UNITY_IOS
        [DllImport("__Internal")]
        public static extern void exportRandomToItem();

        [DllImport("__Internal")]
        public static extern long getSongId();

        [DllImport("__Internal")]
        public static extern string getSongName();

        [DllImport("__Internal")]
        public static extern bool getDoExport();
    #endif

    // Use this for initialization
    void Start () {

        // プロパティを取得
        audio = GameObject.Find("Audio Source").GetComponent<AudioSource>();
        text = GameObject.Find("Text").GetComponent<Text>();

        // ループ再生するようにする
        audio.loop = true;

        // コルーチンを開始
        StartCoroutine(MusicImport());
    }

    IEnumerator MusicImport() {

        text.text = "楽曲エクスポート中";

        // 曲エクスポートを開始
        exportRandomToItem();

        yield return new WaitForSeconds(0.25f);

        // 曲エクスポート完了まで待つ
        while ( getDoExport() ) yield return new WaitForSeconds(0.25f);

        text.text = "楽曲インポート中";

        // Documentsにある曲を取得
        string path = Application.persistentDataPath + "/" + getSongId() + ".wav";
        WWW www = new WWW("file://" + path);

        // インポートが完了するまで待つ
        while ( !www.isDone ) yield return new WaitForSeconds(0.25f);

        audio.clip = www.GetAudioClip(false, false);

    text.text = "再生します!";

        audio.Play();

        text.text = getSongName();

        // wavファイルを削除
        System.IO.File.Delete(path);
    }
}

これでソースコードの準備は完了です!!

C#とゲームオブジェクトを連携

先ほどObjective-Cを動かす為のC#ソースコードを作成しましたが、そのままでは動いてくれないのでUnityのオブジェクトにアタッチしてアプリ起動時に実行されるようにします。
なので今回は「Main Camera」のゲームオブジェクトにC#のスクリプトをアタッチしましょう!
image.png
Main CameraにMediaController.csをアタッチ

ビルド!

これでもうUnityでの準備は完了なので、「PlayerSettings」を各自の環境に設定してビルドしましょう!!
(PlatformがiOSになってない方は先にiOSにSwitch Platformで切り替えてからビルドしてください)
image.png

実行!!

そのままでは実行できないので、いくつか設定する必要があります。

  • Info.plistにミュージックライブラリにアクセスする為の設定を追加
    1. 左にあるInfo.plistを選択して、「Information Property List」の右にある「+」を押す
    2. すると入力ボックスが現れるのでそこに「Privacy – Media Library Usage Description」を入力して右側に適当な文字列を入力します。

これで、アプリ起動時に端末のミュージックライブラリへアクセス許可を選択させるポップアップを表示させる事ができます。
image.png

Info.plistに設定を追加する

  • 作成したObjective-CファイルのARCを無効にする

次は設定がちょっと厄介で画像のように、左の「Unity-iPhone」を押し真ん中上部付近の「Build Phases」を押して「Compile Sources」のボタンを押しましょう。
image.png

下にスクロールすると先ほど作成した「MusicLibraryMediaPicker.mm」があるのでダブルクリックして画像のように「-fno-objc-arc」と入力しましょう。
image.png

これで、準備は完了であとは自分の開発者アカウントでSigningして実行しましょう!!

動かした様子

IMAGE ALT TEXT HERE

作成したソースコード&プロジェクト

作成したソースコードとプロジェクトは、GitHubにアップので良かったらどうぞ。

https://github.com/reishisu/ImportiOSMusicLibrary

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む