20200920のSwiftに関する記事は10件です。

Swiftで複数の戻り値をreturnする。

戻り値を宣言する

メソッド宣言時の戻り値部でメソッド名()->(Data型1,Data型2,Data型3...)の様に記述をすると
複数のデータを1つのメソッドで返すことができる。

//日付取得メソッド
func getDate() -> (String, String, String, String){
    //操作日取得(yyyy-MM-dd)
    let date = Date()
    let formatter = DateFormatter()
    formatter.setLocalizedDateFormatFromTemplate("yMd")
    //日付計算
    let dayFromToday =  Calendar.current.date(byAdding: .day, value: -7, to: date)!
    let weekFromToday =  Calendar.current.date(byAdding: .weekday, value: -7, to: date)!
    let monthFromToday =  Calendar.current.date(byAdding: .month, value: -7, to: date)!

    let today = formatter.string(from: date);
    let DayFromToday = formatter.string(from: dayFromToday);
    let WeekFromToday = formatter.string(from: weekFromToday);
    let MonthFromToday = formatter.string(from: monthFromToday);

    return (today,DayFromToday,WeekFromToday,MonthFromToday)
}

//日付呼び出しメソッド
func viewDatePeriod(today:String,DayFromToday:String,WeekFromToday:String,MonthFromToday:String) {
    //処理〜〜〜
}

また、呼び出す際には以下のように取得したデータを割り振る必要がある。

var date = getDate()
viewDatePeriod(today: date.0,DayFromToday: date.1,WeekFromToday: date.2,MonthFromToday: date.3)

データに名前をつける

上記では、data.0など分かりづらいので以下の様にして宣言をすると呼び出し時に迷うことなくコーディングすることができる。

//データ選択時の呼び出しメソッド
func viewDatePeriod(today:String,DayFromToday:String,WeekFromToday:String,MonthFromToday:String) {

//日付呼び出しメソッド
viewDatePeriod(today: date.today,DayFromToday: date.DayFromToday,WeekFromToday: date.WeekFromToday,MonthFromToday: date.MonthFromToday)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] protocol制約をかけた型で"Cannot use mutating member on immutable value"と言うエラーが出る場合の対処。

swift5で次のコードを実行すると

Cannot use mutating member on immutable value: 'hoge' is a 'let' constant

と言うエラーが出ます。これにしばらくハマってしまったので解決方法を書いておきます。

protocol Hoge{
    var items:[String]{get set}
}

class Fuga:Hoge{
    var items: [String]
    init(_ items: [String]){
        self.items = items
    }
}

class Piyo:Hoge{
    var items: [String]
    init(_ items: [String]){
        self.items = items
    }
}

var hoges:[Hoge] = [Fuga(["a"]), Piyo(["a"]), Fuga(["a"])]
hoges.forEach{ (hoge: Hoge) in
    hoge.items.append("b")          //エラー:Cannot use mutating member on immutable value: 'hoge' is a 'let' constant
}

print(hoges.map{$0.items})

items{get set}で宣言しており、問題はないように見えますが、エラーではhogeが定数だと言っています。forEach内で変更しているのが問題なわけでもなく、実際次のコードはすんなり動きます。

var hoges:[Fuga] = [Fuga(["a"]), Fuga(["a"]), Fuga(["a"])]
hoges.forEach{ (hoge: Fuga) in
    hoge.items.append("b")
}

このエラーの原因はprotocolに準拠した型がclassstructか分からないことです。structは基本的に不変なので、もしもHogeに準拠した型がstructだった場合変更ができなくなってしまいます。

ということで、対応にはprotocolclassの制約をかけてあげることです。

protocol Hoge:class{           //ここが大事!!!!
    var items:[String]{get set}
}

class Fuga:Hoge{
    var items: [String]
    init(_ items: [String]){
        self.items = items
    }
}

class Piyo:Hoge{
    var items: [String]
    init(_ items: [String]){
        self.items = items
    }
}

var hoges:[Hoge] = [Fuga(["a"]), Piyo(["a"]), Fuga(["a"])]
hoges.forEach{ (hoge: Hoge) in
    hoge.items.append("b")
}

print(hoges.map{$0.items})

以上です。

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

[Swift] NSLogを安易に使ってしまうとセキュリティ的にマズい

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3

ログ出力とセキュリティ

printと比べてNSLogは日時を出力してくれるので便利そうに見えますが、、、
スクリーンショット 2020-09-15 9.06.22.png

端末をPC接続して、Apple Configurator 2などのツールを使うとコンソールに出力されてしまいます。
セキュリティ的に危うい状況です。
スクリーンショット 2020-09-15 9.08.38.png

一方、printはコンソールに出力されません。

printを自前で拡張するか、SwiftLog(Apple製のOSSライブラリ)などを利用すると良いかと思います。
https://github.com/apple/swift-log

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

大規模なiOSアプリ開発を2年以上継続して得られた知見の共有

前書き

それなりに大規模なアプリの新規開発〜機能追加を行う中で得られた知見をまとめます。

アプリのジャンル/ライフサイクル、チームメンバーのスキルレベルなどによって適用できない事項もあるかと思いますが、一つの事例として参考にしていただければ幸いです。

<アプリ/チームの概要>

  • iOS版開発チームとしては最小4人〜最大9人。
    • その他、Androidチーム、バックエンドチーム、デザインチーム…などなど諸々合わせて総勢20人〜30人
  • アプリの累計ダウンロード数は数百万。
  • App Store上のカテゴリは「ファイナンス」。
  • リリースサイクル(計画リリース)は平均で2ヶ月に1回。
  • 開発環境はXcode + Swift。
  • Scrumで開発。

本文・コメント含め、これ以上に具体的なことは書けませんのでご了承ください。
本記事の内容は内容はあくまで私個人の見解であり、所属企業における立場、戦略、意見を代表するものではありません。

UI構築

ポリシー

  • 一時期はStoryboardと、コードベース・レイアウトが混在していた時期もありましたが、現在ではコードベース・レイアウトのみに統一しました。
  • 理由は以下のようなStoryboardのデメリットを感じることが多くなってきたためです。
    • 人数が多いとStoryboardのconflictが多く、その解消がとにかく面倒臭い。
    • Storyboardでは、画面修正のレビューが辛い。
    • アプリ規模が大きくなるにつれ、Xcode上でStoryboardを開く時の待ち時間が長くなってしまった。
    • デザイナーからのUI/UX要求レベルが上がってきて、画面の状態が複雑になってきた。
      • 例えば、ローディングプレースホルダを表示しつつ裏でAPI通信>取得成功時は通常View、エラー時はエラー用の特殊なView、など。。。
      • Storyboardでは静的な状態はプレビューできるが、上のような動的な状態変化が多いと意味がない。
    • Storyboardでは、フォント、色など、アプリの統一ポリシーを適用し忘れることがしばしばある。
  • ただし、Unwind Segueは便利なので、画面遷移はStoryboardを使っています。
  • コードベース・レイアウトを容易にするために、自作ライブラリを構築しました(productionコードなので公開はできません)。

参考リンク:
iOSアプリケーションでコードベースのレイアウトを積極利用する
[Xcode] [Swift] 実務的Tips: Unwind Segueで2つ以上前の画面に一気に戻って画面を再更新する

評価

  • コードベース・レイアウト統一に至るまでに紆余曲折はありましたが、それが生産性・品質・保守性ともにベストな方法である、と評価しています。
  • ただし、画面遷移のみStoryboardに依存している状態はあまり「見通し」がよくないので、改善の余地がありそうです。

アーキテクチャー・パターン

ポリシー

  • 当初は標準的なCocoa MVCで作っていました。当然FatViewController化しました。
  • アプリの規模が大きくなり、またエンハンスメントを重ねるごとに改修が困難になってきました。
  • このため、MVVMへのリアーキテクチャーを進めました。
    • 一気に行わず、新規機能や大規模改修に合わせて実施。
    • ライブラリは使わず、設計パターンだけを適用。
  • MVVMアーキテクチャーを選定した理由:
    • Clean Architectureなどの複雑なアーキテクチャーよりも、MVVMへの変更の方が改修規模が小さかったため。
    • メンバーのスキルレベルに差があり、MVVMより複雑なアーキテクチャーを全員が習熟することは困難であったため。

(具体的な実装例は追って別記事を投稿する予定です。)

評価

  • 保守性・拡張性が上がったと評価しており、今後も継続する予定です。

ライブラリ管理

ポリシー

評価

  • ライブラリに起因するトラブルは開発計画を大きく狂わせるので、今後も必要最小限に止める方針に変わりないと思います。
  • Carthageについては、2020年9月現在でXcode 12でビルドエラーになるというクリティカルな問題が発生しているので、CocoaPodsに近日中に乗り換えるかもしれません。。。
  • 大規模アプリにおいては、ライブラリ管理ツール、導入ライブラリ共に、その選定は慎重にならざるを得ないです。

コードレビュー

ポリシー

  • IBM Cloud上にGitLabを導入しました。
  • コードレビューは99.9%実施しています。
  • GitLabのマージリクエスト上でレビューコメントをやりとりするスタイルです。
  • レビューアーは原則として複数人としています。
    • 一次レビューはメンバー
    • 最終レビューはリーダー
  • コードレビューの目的は以下の通りと宣言しており、その観点で気になったことがあればレビューコメントを書くようにしています。
    • システムのメンテナンス性、リーダビリティ、理解のしやすさを改善すること。
    • ブラックボックステストでは検出しづらい不具合(例:メモリーリーク)を発見すること。
    • 知識を共有すること。
  • 一応、コーディングガイドラインはありますが項目は最低限です。
    • 「強制アンラップは理由がない限りは使わないようにしよう」「インデントは合わせよう」「Xcodeの設定を合わせよう」程度です。
    • 私見ですが、ガチガチに縛ろうとして規約を増やしたところで、読むことが大変になるだけで、完璧に守ることは困難です。
    • 規約で縛るよりも、上のような「目的」を共有することの方が大事では、と考えます。

参考リンク:
[Xcode] 実務的Tips: チーム内でXcodeの設定項目を合わせて不要な修正差分が出ないようにする

評価

  • 品質・保守性には大きな効果が認められ、コードレビューを行わないという選択肢は今後もありません。
  • 以下の点は要改善ポイントです。
    • リーダーの負荷が高いこと(レビューに追われて自分の担当分の実装時間が削られる)。
    • リーダーがボトルネックになること。
      • リーダーが不在だとマージできない。
      • 先行タスクのマージリクエストがレビュー未完了だと後続作業が滞る。

テスト

ポリシー

  • 1年ぐらい、XCUITestによるテストの自動化に取り組みました。→詳細は「参考リンク」の記事を参照
  • しかしながら、機能改修時のメンテナンスコストがあまりにも大きかったです。
    • コード修正は1時間でも、既存のテストの修正に半日以上かかることもザラ。。。
  • このため、現在では以下のような手法を取っています。
    • ロジック部分のテストはXCTest。
    • UI部分のテストは実動作で確認。
    • APIについては、Node.jsベースのスタブツール(ローカルサーバー)を自作し、正常系/異常系レスポンスのテストを行う。
  • 以上は開発チームとしてのテストで、業務観点での総合テストは別途QAチームがテストシナリオに即して行う。

参考リンク:
XcodeのUIテストフレームワーク「XCUITest」のTips

評価

  • 私たちのプロダクトではUIの改変が多いため、XCUITestは費用対効果の観点で合いませんでした。
  • MVVMパターンへのリアーキテクチャーとの「合わせ技」で、なるべくView/ViewControllerには画面表示以外のロジックは書かないようにし、ViewModelのXCTestを増やすように取り組んでいるところです。

CI/CD

ポリシー

  • 以下の目的で、CI/CDを取り入れています。
    • Sprintの途中であっても、マージされた機能はいち早くQAエンジニアに渡してテストしてもらいたい。
    • あるいは、オーナーやデザイナーに動かしてもらい、フィードバックしてもらいたい。
  • 現状は、JenkinsでGitLabのブランチを監視して、fastlaneをキックしてビルドを走らせ、TestFlightにアップロードしています。
  • テストは自動で走らせるものの、失敗はSlackに自動postするだけで、デプロイは止めません。
    • 緊急のバグ修正時などはテストコードの修正は後回しにするケースもあるので…

評価

  • アプリの規模が大きくなるに応じてビルド時間が長くなるので、大規模アプリでは自動デプロイは必須です。

後書き

  • DX時代が到来し、各企業とも顧客とのエンゲージメント向上のためにスマホアプリを活用する例は多く、ライバルに遅れを取らないためには高・多機能なアプリを早いサイクルでリリースすることが求められています。
  • しかしながら、OSも開発環境も絶えず進化している中で、大規模なアプリを、迅速かつトラブルなくリリースし続けることは容易ではありません。
  • それを成し遂げるのためには「完璧な手法をじっくり作り上げること」ではなく「早いサイクルで小さい改善を積み重ねること」と考えています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大規模なiOSアプリ開発を2年以上継続して得られた知見

前書き

それなりに大規模なアプリの新規開発〜機能追加を行う中で得られた知見をまとめます。

アプリのジャンル/ライフサイクル、チームメンバーのスキルレベルなどによって適用できない事項もあるかと思いますが、一つの事例として参考にしていただければ幸いです。

<アプリ/チームの概要>

  • iOS版開発チームとしては最小4人〜最大9人。
    • その他、Androidチーム、バックエンドチーム、デザインチーム…などなど諸々合わせて総勢20人〜30人
  • アプリの累計ダウンロード数は数百万。
  • App Store上のカテゴリは「ファイナンス」。
  • リリースサイクル(計画リリース)は平均で2ヶ月に1回。
  • 開発環境はXcode + Swift。
  • Scrumで開発。

本文・コメント含め、これ以上に具体的なことは書けませんのでご了承ください。
本記事の内容は内容はあくまで私個人の見解であり、所属企業における立場、戦略、意見を代表するものではありません。

UI構築

ポリシー

  • 一時期はStoryboardと、コードベース・レイアウトが混在していた時期もありましたが、現在ではコードベース・レイアウトのみに統一しました。
  • 理由は以下のようなStoryboardのデメリットを感じることが多くなってきたためです。
    • 人数が多いとStoryboardのconflictが多く、その解消がとにかく面倒臭い。
    • Storyboardでは、画面修正のレビューが辛い。
    • アプリ規模が大きくなるにつれ、Xcode上でStoryboardを開く時の待ち時間が長くなってしまった。
    • デザイナーからのUI/UX要求レベルが上がってきて、画面の状態が複雑になってきた。
      • 例えば、ローディングプレースホルダを表示しつつ裏でAPI通信>取得成功時は通常View、エラー時は特殊なView、など。。。
      • Storyboardでは静的な状態はプレビューできるが、上のような動的な状態変化が多いと意味がない。
    • Storyboardでは、フォント、色など、アプリの統一ポリシーを適用し忘れることがしばしばある。
  • ただし、Unwind Segueは便利なので、画面遷移はStoryboardを使っています。
  • コードベース・レイアウトを容易にするために、自作ライブラリを構築しました(productionコードなので公開はできません)。

参考リンク:
iOSアプリケーションでコードベースのレイアウトを積極利用する
[Xcode] [Swift] 実務的Tips: Unwind Segueで2つ以上前の画面に一気に戻って画面を再更新する

評価

  • コードベース・レイアウト統一に至るまでに紆余曲折はありましたが、私たちのアプリではコードベース・レイアウトが生産性・品質・保守性ともにベストな方法である、と評価しています。
  • ただし、画面遷移のみStoryboardに依存している状態はあまり「見通し」がよくないので、改善の余地がありそうです。

アーキテクチャー・パターン

ポリシー

  • 当初は標準的なCocoa MVCで作っていました。当然FatViewController化しました。
  • アプリの規模が大きくなり、またエンハンスメントを重ねるごとに改修が困難になってきました。
  • このため、MVVMへのリアーキテクチャーを進めました。
    • 一気に行わず、新規機能や大規模改修に合わせて実施。
    • ライブラリは使わず、設計パターンだけを適用。
  • MVVMアーキテクチャーを選定した理由:
    • Clean Architectureなどの複雑なアーキテクチャーよりも、MVVMへの変更の方が改修規模が小さかったため。
    • メンバーのスキルレベルに差があり、MVVMより複雑なアーキテクチャーを全員が習熟することは困難であったため。

(具体的な実装例は追って別記事を投稿する予定です。)

評価

  • 保守性・拡張性が上がったと評価しており、今後も継続する予定です。

ライブラリ管理

ポリシー

評価

  • ライブラリに起因するトラブルは開発計画を大きく狂わせるので、今後も必要最小限に止める方針に変わりないと思います。
  • Carthageについては、2020年9月現在でXcode 12でビルドエラーになるというクリティカルな問題が発生しているので、CocoaPodsに近日中に乗り換えるかもしれません。。。
  • 大規模アプリにおいては、ライブラリ管理ツール、導入ライブラリ共に、その選定は慎重にならざるを得ないです。

コードレビュー

ポリシー

  • IBM Cloud上にGitLabを導入しました。
  • コードレビューは99.9%実施しています。
  • GitLabのマージリクエスト上でレビューコメントをやりとりするスタイルです。
  • レビューアーは原則として複数人としています。
    • 一次レビューはメンバー
    • 最終レビューはリーダー
  • コードレビューの目的は以下の通りと宣言しており、その観点で気になったことがあればレビューコメントを書くようにしています。
    • システムのメンテナンス性、リーダビリティ、理解のしやすさを改善すること。
    • ブラックボックステストでは検出しづらい不具合(例:メモリーリーク)を発見すること。
    • 知識を共有すること。
  • 一応、コーディングガイドラインはありますが項目は最低限です。
    • 「強制アンラップは理由がない限りは使わないようにしよう」「インデントは合わせよう」「Xcodeの設定を合わせよう」程度です。
    • 私見ですが、ガチガチに縛ろうとして規約を増やしたところで、読むことが大変になるだけで、完璧に守ることは困難です。
    • 規約で縛るよりも、上のような「目的」を共有することの方が大事では、と考えます。

参考リンク:
[Xcode] 実務的Tips: チーム内でXcodeの設定項目を合わせて不要な修正差分が出ないようにする

評価

  • 品質・保守性には大きな効果が認められ、コードレビューを行わないという選択肢は今後もありません。
  • 以下の点は要改善ポイントです。
    • リーダーの負荷が高いこと(レビューに追われて自分の担当分の実装時間が削られる)。
    • リーダーがボトルネックになること。
      • リーダーが不在だとマージできない。
      • 先行タスクのマージリクエストがレビュー未完了だと後続作業が滞る。

テスト

ポリシー

  • 1年ぐらい、XCUITestによるテストの自動化に取り組みました。→詳細は「参考リンク」の記事を参照
  • しかしながら、機能改修時のメンテナンスコストがあまりにも大きかったです。
    • コード修正は1時間でも、既存のテストの修正に半日以上かかることもザラ。。。
  • このため、現在では以下のような手法を取っています。
    • ロジック部分のテストはXCTest。
    • UI部分のテストは実動作で確認。
    • APIについては、Node.jsベースのスタブツール(ローカルサーバー)を自作し、正常系/異常系レスポンスのテストを行う。
  • 以上は開発チームとしてのテストで、業務観点での総合テストは別途QAチームがテストシナリオに即して行う。

参考リンク:
XcodeのUIテストフレームワーク「XCUITest」のTips

評価

  • 私たちのプロダクトではUIの改変が多いため、XCUITestは費用対効果の観点で合いませんでした。
  • MVVMパターンへのリアーキテクチャーとの「合わせ技」で、なるべくView/ViewControllerには画面表示以外のロジックは書かないようにし、ViewModelのXCTestを増やすように取り組んでいるところです。

CI/CD

ポリシー

  • 以下の目的で、CI/CDを取り入れています。
    • Sprintの途中であっても、マージされた機能はいち早くQAエンジニアに渡してテストしてもらいたい。
    • あるいは、オーナーやデザイナーに動かしてもらい、フィードバックしてもらいたい。
  • 現状は、JenkinsでGitLabのブランチを監視して、fastlaneをキックしてビルドを走らせ、TestFlightにアップロードしています。
  • テストは自動で走らせるものの、失敗はSlackに自動postするだけで、デプロイは止めません。
    • 緊急のバグ修正時などはテストコードの修正は後回しにするケースもあるので…

評価

  • アプリの規模が大きくなるに応じてビルド時間が長くなるので、大規模アプリでは自動デプロイは必須です。

後書き

  • OSも開発環境も絶えず進化している中で、大規模なアプリを、迅速かつトラブルなくリリースし続けることは容易ではない、と感じます。
  • 「早いサイクルで小さい改善を積み重ねる」をモットーに、まだまだ模索を続けて行こうと考えています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ARCoachingOverlayViewでユーザーにデバイスをうごかしてもらって、ARKitに世界情報をあたえる

ARKitがWorldTrackingを確立するには、デバイスを動かして、ARKitに視点を与える必要があります。

ARCoachingOverlayViewでユーザーにガイダンスを出して、デバイスをうごかしてもらいます。

Sep-20-2020 14-12-53.gif

1、ARCoachingOverlayViewを初期化します。

let coachingOverlay = ARCoachingOverlayView()

ARCoachingOverlayViewの設定

coachingOverlay.goal = .anyPlane //情報を取る目標設定
// 可能な値
    case anyPlane
    case horizontalPlane
    case tracking
    case verticalPlane

coachingOverlay.activatesAutomatically = true //Tracking状態が不十分になると、再度コーチングするか

// セッションとデリゲートを設定
coachingOverlay.session = sceneView.session
coachingOverlay.delegate = self

// Viewとして扱う
coachingOverlay.frame = sceneView.bounds
sceneView.addSubview(coachingOverlay)

2、ARCoachingOverlayViewDelegateを設定

コーチングの状態によって呼ばれるDelegate functionは以下

func coachingOverlayViewWillActivate(_ coachingOverlayView: ARCoachingOverlayView) {
    // コーチングが表示される前に呼ばれる。ここでコーチング中操作できないButtonなどを非表示にする。
}

func coachingOverlayViewDidDeactivate(_ coachingOverlayView: ARCoachingOverlayView) {
    // コーチングが終了したあとに呼ばれる。ここでコーチング中操作できないButtonなどを再表示する。

}

これでARSessionをrunすると動くはずです。
必要な情報が得られたら、コーチングは消えます。
このステップをやったほうが、正確なワールドトラッキングが得られます。


Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

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

XCTSkipの使い方まとめ

はじめに

Xcode11.4からXCTestフレームワークに新しく導入されたプロパティの一つに「XCTSkip」があります。

XCTSkipを使用することで明示的に「スキップ」の結果を出力することができます。

特に統合テストにおいては、必要条件や依存関係が必ずと言っていいほどあるかと思います。
例えばアプリケーションがiPad特有の機能を持っている場合はiPhoneではテストできませんし古いOSでは利用できないAPIを用いる場合テスト対象は必然と絞られます。

そんな条件付きの実行が必要なテストを行う際に「XCTSkip」が活躍します。

中の実装は「こちら」をご参照ください。

なぜ「スキップ」があると良いのか?

以前まで(Xcode11.4より前)は下記の2択しかありませんでした。

  • テストを合格にする
  • テストを不合格にする

もし一部の条件下では必ずテストが成功するようにしていた場合、検証に合格していないプログラムが機能するということになりますし、テスト失敗とした場合は何が悪いのかの判断ができず無駄に時間を消費してしまいます。

XCTSkipを利用することでResult Bundlesを見た際にテスト結果のより詳細な情報がわかるようになりテスト全体の把握が容易になります。

Xcode11よりResult Bundlesへのデータの集約方法も変更されたのですが、そこら辺の詳細に関しては別の機会に記事を書きたいと思います。

使い方

使い方を紹介するために簡単なサンプルを作成しました。
テキストを音声で読み上げるとてもシンプルなものです(あまり良い例が思い付かず優しい目で見てください)

import AVFoundation

class Speech {

    private var nonMaleJPVoices: [AVSpeechSynthesisVoice] {
        return AVSpeechSynthesisVoice.speechVoices()
            .filter { $0.language == "ja-JP" && $0.gender != .male }
    }

    private func letsSpeaking() {
        let synthesizer = AVSpeechSynthesizer()
        let utterance = AVSpeechUtterance(string:"こんにちは")
        utterance.voice = nonMaleJPVoices.first
        synthesizer.speak(utterance)
    }

}

letsSpeaking()をコールすると「こんにちは」と音声が発せられます。
nonMaleJPVoicesでは使用可能な音声オブジェクトのうち「言語が日本語」かつ「男性以外」の音声情報を渡す実装になっています。

今回はnonMaleJPVoicesの正当性をテストしてみたいと思います。
nonMaleJPVoicesの要件は以下の2つです。

  • 言語が日本語
  • 男性以外

実際に書いたテストは下記です。

class SpeechTests: XCTestCase {

    func testNonMaleJPVoicesContainOnlyJapaneseLanguage() throws {
        XCTAssertFalse(Speech().nonMaleJPVoices.contains{ $0.language != "ja-JP" }, "nonMaleJPVoices must not contain any language other than Japanese.")
    }

    func testNonMaleJPVoicesContainOnlyNonMaleGender() throws {
        XCTAssertFalse(Speech().nonMaleJPVoices.contains{ $0.gender == .male }, "nonMaleJPVoices must not contain a male voice.")
    }

}

上から順に

  • nonMaleJPVoicesは言語が日本語の要素を持つAVSpeechSynthesisVoiceの配列であること
  • nonMaleJPVoicesは音声の性別が男性以外の要素を持つAVSpeechSynthesisVoiceの配列であること

を確かめるためのテストです。
良さそうに見えるのですが、一つ注意が必要です。

AVSpeechSynthesisVoiceGenderはiOS13より追加されたプロパティなのでそれより前のOSバージョンでは取得ができません。
iOS12ではgenderへのアクセスができないのです。

その場合テストはどうなるでしょう?

まず、機能の実装にはOSバージョンの条件分岐が入ります。

internal var nonMaleJPVoices: [AVSpeechSynthesisVoice] {
        if #available(iOS 13.0, *) {
            return AVSpeechSynthesisVoice.speechVoices()
                .filter { $0.language == "ja-JP" && $0.gender != .male }
        }
        return AVSpeechSynthesisVoice.speechVoices()
            .filter { $0.language == "ja-JP" }
    }

すると男性以外という条件を含むのはiOS13以上となります。

先ほどのテストに戻りますがこのままではテストとしては不十分なので修正をする必要があります。

class SpeechTests: XCTestCase {

    /// 修正が必要    
    func testNonMaleJPVoicesContainOnlyNonMaleGender() throws {
        XCTAssertFalse(Speech().nonMaleJPVoices.contains{ $0.gender == .male }, "nonMaleJPVoices must not contain a male voice.")
    }

}

どのように修正するのがよいでしょうか?
iOS13未満では必ずテストが成功するようにしますか?

ここで活躍するのがXCTSkipです。

性別に関してテストすべきなのはiOS13以上の時だけでありそれより下のOSではテスト不要なのでスキップするのが適切です。

早速修正しましょう!

class SpeechTests: XCTestCase {

    func testNonMaleJPVoicesContainOnlyNonMaleGender() throws {
        guard #available(iOS 13.0, *) else {
            throw XCTSkip("AVSpeechSynthesisVoiceGender tests can only run on iOS 13.0+")
        }

        XCTAssertFalse(Speech().nonMaleJPVoices.contains{ $0.gender == .male }, "nonMaleJPVoices must not contain a male voice.")
    }

}

実際にiOS12系の端末でテストを実行してみるとこうなります。
スクリーンショット 2020-09-19 20.45.57.png
何やら見慣れないマークが表示されていますがこれがスキップしたことを示すマークです。

Report Navigatorよりスキップの詳細を見ることができます。
スクリーンショット 2020-09-19 20.51.01.png

使い方はとても簡単ですね...!

スキップの種類

XCTSkipを開始する関数は2種類あります。

XCTSkipIf(::file:line:)

条件式が真の場合にスキップします。

func testExample() throws
    try XCTSkipIf(UIDevice.current.userInterfaceIdiom == .pad, "this tests are not for iPad only")

    // test for other than iPad ...
}

XCTSkipUnless(::file:line:)

条件式が偽の場合にスキップします。

func testExample() throws
    try XCTSkipUnless(UIDevice.current.userInterfaceIdiom == .pad, "this tests are for iPad only")

    // test for iPad only...
}

XCTSkip

先に関数は2種類と説明しましたが、XCTSkip構造体を直接投げることも可能です。

その場合はガードと組み合わせる使い方がおすすめでWWDC20の「XCTSkip your tests」のセッションでも同じように紹介されていました。

func testNonMaleJPVoicesContainOnlyJapaneseLanguage() throws {
        guard #available(iOS 13.0, *) else {
            throw XCTSkip("this tests can only run on iOS 13.0+")
        }

        // test for iOS13+ ...
    }

まとめ

テストを書いていく中で特定の条件下では実行できないテストというのが必ずあると思います(特に統合テスト)

そんなときにXCTSkipを使うことでテストの実行結果をより正確にモデル化することが可能となります。

テストの結果についてより具体的に詳細を把握するためにもぜひスキップしてみてはいかがでしょうか!!

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

Int型を拡張して3の倍数か判定するメソッドを作成する

この記事について

この記事は「Swiftである程度のことはできるようになったけど、extensionはコピペまたはUITableViewくらいでしか使ったことない〜。けど可読性も上がるし拡張が使えるとかっこいいよな!」という人が書いています。例として3の倍数か判定するメソッドを作成していますが、型の拡張を使いこなせることを目標としています。ご意見やアドバイス等があればコメントよろしくお願いします。

extension(拡張)とは

extensionとは既に存在するクラス,構造体,列挙型およびプロトコルに新しいメソッドやコンピューテッドプロパティを追加するできる機能のことです。Xcodeでは可読性を上げるためにクラスを拡張していることがあります。

ソースコード

extension Int {
    var isMultipleOf3: Bool {
        if self%3 == 0 {
            return true
        } else {
            return false
        }
    }
}
11.isMultipleOf3 //false
12.isMultipleOf3 //true

Bool型を返すコンビューテッドプロパティとしてisMultipleOf3を定義しています。

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

【Swift】循環参照 丸わかり?

循環参照

循環参照とは?

お互いのインスタンスを参照しあい、メモリが解放されない現象。
「解放」...もうここ使わないから、あとはご自由にどうぞ、というイメージ。?

image.png

なぜNG?

メモリ リークを引き起こす。
最終的にメモリが足りなくなり、アプリが落ちてしまう。

メモリ リーク

メモリの解放忘れが原因で、永久にメモリが消費され続けること。
再現性が低く 見つけるのが困難で、悪質なバグの一つ。

循環参照 を 防ぐには?

弱参照をする。
weakunownedを使う。(-> unknowned ではない...!)

unowned: 所有されていない
unknown: 知られていない、不明な
unknowned -> こんな言葉ない

なぜ防げるのか

弱参照を行うと、参照カウントは変わらないため。

weakは「このインスタンスへの参照は、カウントしないよ」
という制限を、メモリ管理機能(= Automatic Reference Counting) に伝える。

参照カウント

「プロパティや変数、定数からの インスタンスへの参照」が いくつあるかをカウントするもの。

参照の種類

  • 強参照: strong -> デフォルト
  • 弱参照: weak
  • 非所有参照: unowned

強参照されたインスタンスは、参照カウントが 1 増やされます
弱参照非所有参照を行うと、参照カウントは変わりません

weak と unowned の違い

インスタンスが破棄された場合、弱参照(= weak) 元にはnilが代入されます。

非所有参照(= unowned) では、変数にnilが代入されません。
( -> インスタンス解放後に変数にアクセスすると エラー)

改めまして、「循環参照」とは??

  • 循環参照とは、インスタンス間でお互いを強参照しあった場合に参照カウントが0にならず、メモリ上にインスタンスが残り続けてしまう状態のこと。

  • 永久にメモリが消費され続ける、「メモリリーク」という現象を引き起こす恐れがある。

  • それを防ぐために、weakunownedを使用して弱参照を行う。

 Swiftのメモリ管理を知る

おしまい。

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

【Swift】AudioKitを使ってみた(2020.9)

はじめに

【2020.9.21】 Oscillators and the Physics of Sound に書き漏れがあったので追記しました。

サウンドファイルの再生じゃなくて、音そのものを発信(発振?)させるのってどうすればいいんだろうとググっていて、AudioKitというライブラリを見つけました。

で、AudioKit Tutorial: Getting Startedというチュートリアルが、順を追って丁寧に解説されてるので動かしてみました。

ただページそのものがかなり古い(2016/バージョンは3.6)ので、公式ページにある最新版のAudioKit Playgrounds (zip)(バージョンは4.9.5)で動かしてみたときの変更点を備忘録としてまとめておきます。DLはこちらから。

実行環境

  • Xcode 11.6
  • macOS Catalina(10.15.6)
  • 公式ページのAudioKit Playgrounds (zip)(バージョンは4.9.5)

Getting Started

まず AudioKit Playgrounds (zip)は最新版を使うので、チュートリアルページのリンクではなく、公式サイトのDLページから入手します。ファイル名は「AudioKitPlaygrounds-4.9.5.zip」になります。

これを解凍してXcodeで開いた後に・・・

Click the + button in the bottom left-hand corner of the Navigator view. Select the New Playground… option, name it Journey, and save it in the Playgrounds folder the other playground files are stored in.

ナビゲータービューの左下隅にある+ボタンをクリックします。 [新しいプレイグラウンド…]オプションを選択し、「ジャーニー」という名前を付けて、他のプレイグラウンドファイルが格納されているプレイグラウンドフォルダに保存します。

と書いてありますが、ナビゲータービューの左下隅にある+ボタンでは「New Playground…」なんて出てこないので、ナビゲータービュー上で右クリックすると「New Playground Page」が出てきますのでここから追加できます。
スクリーンショット 2020-09-20 0.18.44.png
Xcodeのバージョンの問題かしらん?Playgroundってあんまり使ってないんで。。。

さらに以下のような注釈がありますが・・・

This project will fail unless you build the project at least once using Product / Build, or ⌘-B. Once built, run the playground again and you’ll hear your playground emit 10 seconds of a beeping sound. You can use the Play/Stop button at the bottom left of the playground window within the Debug Area to stop or repeat the playground.

このプロジェクトは、Product / Buildまたは⌘-Bを使用してプロジェクトを少なくとも1回ビルドしない限り失敗します。 作成したら、遊び場をもう一度実行すると、遊び場から10秒間のビープ音が聞こえます。 デバッグ領域内のプレイグラウンドウィンドウの左下にある[再生/停止]ボタンを使用して、プレイグラウンドを停止または繰り返します。

Note: If the playground fails to execute, and you see errors in the Debug Area, try restarting Xcode. Unfortunately, using playgrounds in combination with frameworks can be a little error-prone and unpredictable. :[

注:プレイグラウンドの実行に失敗し、デバッグ領域にエラーが表示された場合は、Xcodeを再起動してみてください。 残念ながら、フレームワークと組み合わせてプレイグラウンドを使用すると、エラーが発生しやすくなり、予測できなくなります。 :[

私の環境でも結構出たので、新規のPlaygoundファイルを作ったら「⌘-B」を押し、実行してエラーが出たときは、Xcodeを再起動したり、AudioKitPlaygrounds.xcodeprojに既に作成済みのPlaygroudファイルを実行してから、再度自分で書いたPlaygroundを実行したりしてました。

Oscillators and the Physics of Sound

このコードをそのまま実行すると、以下のエラーが出ます。

error: 02Oscillators.xcplaygroundpage:21:1: error: use of unresolved identifier 'AKPlaygroundLoop'
AKPlaygroundLoop(every: 0.5) {
^~~~~~~~~~~~~~~~

最新バージョンでは、AudioKitとAudioKitUI(おそらくUI系)と二つのフレームワークに分割されていて、'AKPlaygroundLoop'は AudioKitUI にあるので、

import AudioKitUI

を追加すればOKです。以降、'AKPlaygroundLoop'が出てくる箇所ではimportを忘れずに。

【参考】AudioKit/AudioKit/iOS/AudioKit/User Interface/AKPlaygroundLoop.swift

【2020.9.21追記】

rampTimeもエラーになります。

error: 02Oscillators.xcplaygroundpage:18:12: error: value of type 'AKOscillator' has no member 'rampTime'
oscillator.rampTime = 0.2
~~~~~~~~~~ ^~~~~~~~

これは rampDuration に変更されていうようなので、以下のように変更します。

//---rampTimeは、rampDurationに変更
oscillator.rampDuration = 0.2

【参考】AudioKit Reference AKOscillator Class Reference

Sound Envelopes

import AudioKitUI

を追加すればOKです。

Additive Sound Synthesis

1.まず以下を追加。

import AudioKitUI

2.Live ViewにUIを表示するので、Live Viewを表示する必要があります。「Alt+Cmd+Return」で表示/非表示を切り替えます。

【参考】How to create live playgrounds in Xcode

  1. AKPlaygroundView クラスを使ってUIを作成していますが、これがエラーになって動かない。

AudioKitPlaygrounds.xcodeprojに既存の他のファイルを見てみると、AKLiveViewController を使っているようなので、こちらで書き換えました。クラス名を含めた変更点は3つ。

・AKPlaygroundView >> AKLiveViewController
・override func setup() >> override func viewDidLoad()
・addSubview() >> addView()

GitHubで、AudioKit/AudioKit/iOS/AudioKit/User Interface/を見ると、どちらのクラスも存在しているので AKPlaygroundView で何故ダメなのかは不明。

4.Live View にUIが表示されるようになるが、音が鳴らない。

オシレーターを生成するための「createAndStartOscillator()」という関数があって、サンプルコードでは生成時に「oscillator.start()」を実行しているけど、「AudioKit.start()」の後に実行しないと音が鳴らないようです。

なので、createAndStartOscillator()にある「oscillator.start()」をコメントにして、「AudioKit.start()」のあとに以下のコードを追加すればOK。

//AKOscillator.start()は、AudioKit.start()の後でないと音が鳴らない
oscillators.forEach {
    $0.start()
}

Polyphony

1.まず以下を追加。

import AudioKitUI

2.AKPlaygroundView を AKLiveViewController に変更。Additive Sound Synthesis の項を参照。

Sampling

コードの変更点は、特になし。ただサンプルコードで使っている.wavファイルは、自分でDL(リンクあり)して該当Playground配下の「Resources」フォルダに入れておく必要があります。

まとめ

AudoKit、面白そうだけど、入門的な記事は古いものしか見当たらなかったので、まとめてみました。参考になれば幸いです。自分で書いたコードではないので、あえてコード全文は載せていません。

他にいい参考記事あったら教えてください。

シンセサイザーの音声合成的な知識が必要なので使いこなすにはハードル高そうですが、画面のキャラクタタップしたら「どう森」みたいにインチキ言語を喋るとかさせたら面白いかも(笑)。

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