20201019のiOSに関する記事は9件です。

iOSアプリ開発:タイマーアプリ(5.アラーム、バイブレーションの実装)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーのカウントダウンが0に到達したときに発動するアラームやバイブレーションの実装について掲載します。

開発環境

  • OS: macOS 10.15.x (Catalina)
  • エディタ: Xcode 12.x
  • 言語: Swift
  • 主な使用ライブラリ: SwiftUI

手順概要

  1. TimeManagerでAudioToolboxライブラリをインポートする
  2. TimeManagerにデフォルトのアラーム音名とアラームIDのプロパティを追加する
  3. MainViewでAudioToolboxライブラリをインポートする
  4. MainViewのZStackの.onReceiveモディファイアにアラームの発動を実装する
  5. MainViewのZStackの.onReceiveモディファイアにバイブレーションの発動を実装する

手順詳細

1. TimeManagerでAudioToolboxライブラリをインポートする

TimeManagerに新たにAudioToolboxというライブラリをインポートします。
AudioToolboxライブラリには、主に効果音的サウンドに関するクラス、プロパティ、メソッドが含まれますので、タイマーのカウントダウンが0に達したときにアラーム音を慣らすためにこれを利用します。

TimeManager.swift
import SwiftUI
import AudioToolbox //追加でインポート

class TimeManager: ObservableObject {
    //(プロパティ、メソッド省略)
}

2. TimeManagerにデフォルトのアラーム音名とアラームIDのプロパティを追加する

AudioToolboxに含まれる音源を利用するには、その音源のSystemSoundIDを指定する必要があります。SystemSoundIDはひとつのデータ型になっており、UInt32のtypealiasです。

ですので、TimeManagerクラスにこのデータ型のプロパティを作成し、@Publishedのプロパティラッパーをつけて、デフォルトで指定したい音源のIDを値として代入します。

どのIDがどんな音なのかは、以下のリソースが参考になります。
https://github.com/TUNER88/iOSSystemSoundsLibrary

このリソースの項目をひとつずつ確認してみましたが、iOSデバイスのかなり古い機種で利用されていた通知音のように思います。最近の機種でデフォルトで採用されている通知音の情報は見つかりませんでした。ご了承ください。(ご存知の方がいればコメントいただけると幸いです)

プロパティ名はsoundIDとし、ここでは、個人的に心地よい音源だと感じたので、1151をIDとして代入しました。

TimeManager.swift
import SwiftUI
import AudioToolbox //追加でインポート

class TimeManager: ObservableObject {
    //(他のプロパティ省略)

    //AudioToolboxに格納された音源を利用するためのデータ型でデフォルトのサウンドIDを格納
    @Published var soundID: SystemSoundID = 1151

    //(メソッド省略)
}

3. MainViewでAudioToolboxライブラリをインポートする

手順 1 のTimeManagerと同様に、MainViewのほうにも AudioToolbox ライブラリをインポートします。

こちらでAudioToolboxをインポートする理由は、実際にカウントダウンタイマーが0に達したタイミングでアラート音を鳴らすメソッドをMainViewに記述する必要があり、そのメソッドがAudioToolboxに含まれるからです。

MainView.swift
import SwiftUI
import AudioToolbox //追加インポート

struct MainView: View {
    //(プロパティ、メソッド省略)
}

4. MainViewのZStackの.onReceiveモディファイアにアラームの発動を実装する

MainViewのZStackの.onReceiveモディファイア内で、すでに残り時間が0より大きいか、0以下の場合でif-else文による条件分岐をさせていました。そのelse文(残り時間が0以下の場合)の中にアラーム音発動用のメソッドを追加します。

メソッドは以下のように記述します。第一引数に、手順2で先に用意したTimeManagerクラスのsoundIDプロパティを指定します。第二引数は nil で大丈夫です。

AudioServicesPlayAlertSoundWithCompletion(self.timeManager.soundID, nil)

MainView.swift
struct MainView: View {
    //(プロパティ省略)

    var body: some View {
        ZStack {
            //(省略)
        }
        //指定した時間(1秒)ごとに発動するtimerをトリガーにしてクロージャ内のコードを実行
        .onReceive(timeManager.timer) { _ in
            //タイマーステータスが.running以外の場合何も実行しない
            guard self.timeManager.timerStatus == .running else { return }
            //残り時間が0より大きい場合
            if self.timeManager.duration > 0 {
                //残り時間から -1 する
                self.timeManager.duration -= 1
            //残り時間が0以下の場合
            } else {
                //タイマーステータスを.stoppedに変更する
                self.timeManager.timerStatus = .stopped
                //アラーム音を鳴らす
                AudioServicesPlayAlertSoundWithCompletion(self.timeManager.soundID, nil)
            }
        }
    }
}

5. MainViewのZStackの.onReceiveモディファイアにバイブレーションの発動を実装する

アラーム音の次はバイブレーションの実装です。これもメソッドはアラーム音発動と同じものを使います。引数が変わります。

引数には kSystemSoundID_Vibrate プロパティによって得られたバイブレーション用のsoundIDの値をSystemSoundIDデータ型に変換して渡しています。

AudioServicesPlayAlertSoundWithCompletion(SystemSoundID(kSystemSoundID_Vibrate)) {}

MainView.swift
struct MainView: View {
    //(プロパティ省略)

    var body: some View {
        ZStack {
            //(省略)
        }
        .onReceive(timeManager.timer) { _ in
            guard self.timeManager.timerStatus == .running else { return }
            if self.timeManager.duration > 0 {
            //(省略)
            } else {
                //(省略)
                //アラーム音を鳴らす
                AudioServicesPlayAlertSoundWithCompletion(self.timeManager.soundID, nil)
                //バイブレーションを作動させる
                AudioServicesPlayAlertSoundWithCompletion(SystemSoundID(kSystemSoundID_Vibrate)) {}
            }
        }
    }
}

次回は、アラームとバイブレーションのオン/オフなどを含む設定画面を作成していきます。

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

iOSのビルド時に"warning: double-quoted include"が出るときの対処法(Cocoapods 1.9.3)

FlutterをiOSでビルドしようとしたとき、エラーが出た。
とりあえず現時点(2020/10/19)での解決法をメモしておく。

初期環境

  • Xcode 12.1
  • Cocoapods 1.9.3
  • Dart 2.10.0
  • Flutter 1.22.0
  • Bundler 2.1.4

以下エラー。

Xcode build done.                                           115.3s
Failed to build iOS app
Error output from Xcode build:
↳
    ** BUILD FAILED **
Xcode's output:
↳
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBWrappers.pbobjc.m:11:
    /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBProtocolBuffers_RuntimeSupport.h:34:9: warning: double-quoted include "GPBBootstrap.h" in framework header, expected angle-bracketed instead [-Wquoted-include-in-framework-header]
    #import "GPBBootstrap.h"
            ^~~~~~~~~~~~~~~~
            <GPBBootstrap.h>
    /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBProtocolBuffers_RuntimeSupport.h:36:9: warning: double-quoted include "GPBDescriptor_PackagePrivate.h" in framework header, expected angle-bracketed instead [-Wquoted-include-in-framework-header]
    #import "GPBDescriptor_PackagePrivate.h"
            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            <GPBDescriptor_PackagePrivate.h>
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBWrappers.pbobjc.m:11:
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBProtocolBuffers_RuntimeSupport.h:36:
    /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBDescriptor_PackagePrivate.h:35:9: warning: double-quoted include "GPBDescriptor.h" in framework header, expected angle-bracketed instead [-Wquoted-include-in-framework-header]
    #import "GPBDescriptor.h"
            ^~~~~~~~~~~~~~~~~
            <GPBDescriptor.h>
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBWrappers.pbobjc.m:11:
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBProtocolBuffers_RuntimeSupport.h:36:
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBDescriptor_PackagePrivate.h:35:
    /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBDescriptor.h:33:9: warning: double-quoted include "GPBRuntimeTypes.h" in framework header, expected angle-bracketed instead [-Wquoted-include-in-framework-header]
    #import "GPBRuntimeTypes.h"
            ^~~~~~~~~~~~~~~~~~~
            <GPBRuntimeTypes.h>
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBWrappers.pbobjc.m:11:
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBProtocolBuffers_RuntimeSupport.h:36:
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBDescriptor_PackagePrivate.h:35:
    In file included from /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBDescriptor.h:33:

どうも、Xcode12からはimportにdouble-quoteは使えなくなった様。
https://developer.apple.com/forums/thread/651941

ただ、ライブラリ側が対応できていないものが多いので、Xcode上で無効にする。
https://developer.apple.com/forums/thread/651941?answerId=617655022#617655022

スクリーンショット 2020-10-19 15.50.23.png

この後、AndroidStudioでiOS向けにビルドすると、再度エラーが起こったので File -> Invalidate Caches / Restart ... でAndroidStudioを再起動した。

CI上での設定

ローカルでビルドすることはできたが、CI上でビルドしようとすると同様のエラーが発生した。

CIを使うときは、Preview版のCocoapodsを使うとできたぞと書かれていたので設定する。

If you're getting this locally, upgrade CocoaPods. If your CI is getting this error, force the CocoaPods version to 1.10.0.rc.1.

https://developer.apple.com/forums/thread/651941?answerId=640552022#640552022

// 前略
      before_install:
        - gem uninstall -ax cocoapods             # <- 追加
        - gem install cocoapods -v 1.10.0.rc.1    # <- 追加
        - bundle install
        - npm install -g firebase-tools
// 後略

ビルドできなかった。

Xcode build done.                                           113.7s
Failed to build iOS app
Error output from Xcode build:
↳
    ** BUILD FAILED **
Xcode's output:
↳
    error: the following command failed with exit code 0 but produced no further output
    CompileC /Users/travis/Library/Developer/Xcode/DerivedData/Runner-hdibzqvbombrjjhkjascueuffcef/Build/Intermediates.noindex/Pods.build/Release-Develop-iphoneos/Protobuf.build/Objects-normal/armv7/GPBWrappers.pbobjc.o /Users/travis/build/Cookbiz/wakumin-app/ios/Pods/Protobuf/objectivec/GPBWrappers.pbobjc.m normal armv7 objective-c com.apple.compilers.llvm.clang.1_0.compiler

cocoapodsのバージョンを上げたのになぁと思って、上の方を見ると、、、

$ bundle install
Fetching gem metadata from https://rubygems.org/........

...中略...

Fetching cocoapods 1.9.3
Installing cocoapods 1.9.3
Using colored 1.2

あー・・・・・・。bundleか・・・・・・。
このプロジェクトはfastlaneを使っていて、bundle install をしているのでした。
ビルドに使われるpodは多分こっちになったのかな。

ということで、

source "https://rubygems.org"

gem "fastlane"
gem 'cocoapods', "1.10.0.rc.1"  <= ここを指定

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

としてもう一度走らせると、、、

Running pod install...                                            214.8s (!)
Running Xcode build...                                          
 └─Compiling, linking and signing...                        60.2s
Xcode build done.                                           561.4s

無事CIビルドができた :tada: :tada:

補記

それはそうとこのビルド時間なんとかならないのか・・・・・・。
FlutterビルドとFastlaneビルド合わせて25分くらいビルドしている・・・・・。

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

iOSアプリ開発:タイマーアプリ(4.カウントダウンの表示)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、カウントダウンタイマーで実際に残り時間の表示を1秒毎に更新する手順について掲載します。

開発環境

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

手順概要

  1. TimeManagerクラスにTimerクラスpublishメソッドを追加する
  2. MainViewの残り時間の表示を1秒毎に更新する

1. TimeManagerクラスにTimerクラスpublishメソッドを追加する

現時点では、スタートボタンを押しても、タイマーステータスは.runningに変わりますが、画面上はまだ残り時間がカウントダウンされず、設定した時間のままです。

そこで、このタイマーアプリの肝となるプログラミングを施します。
SwiftUIライブラリにも含まれますが、一番ベースとなるFoundationライブラリに用意されているTimerというクラスのpublishというメソッドを利用します。

以下のように記述することで、引数everyで指定した時間(1秒)ごとに発動するタイマーを変数に格納できます。これがView側で何らかのアクションを1秒毎に発動したいときにトリガーになってくれます。この解説ではタイマーアプリが残り時間を1秒毎に更新する際に利用します。

TimeManager.swift
class TimeManager: ObservableObject {
    //(他のプロパティ省略)

    //1秒ごとに発動するTimerクラスのpublishメソッド
    var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    //(メソッド省略)
}

2. MainViewの残り時間の表示を1秒毎に更新する

MainViewでTimerViewを含んでいる一番外側のZStackに .onReceiveモディファイアを追加します。ZStack内のすべてのViewに反映されます。

この.onReceiveモディファイアがTimeManagerクラスのtimerの1秒毎の発動を受けとります。そして、そのあとのクロージャにはtimer発動ごとに実行するコードを記述します。

guard let ~ else 構文で、タイマーステータスが.running以外の時は何も実行しないようにクロージャにはreturnを記述します。
タイマーステータスが.runningであれば、それ以下のif文へ進みます。

if文では、残り時間が0より大きい時は、残り時間から1秒引き算し、残り時間が0以下の場合は、タイマーステータスを.stoppedに変更するように記述します。

MainView.swift
struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        ZStack {
            if timeManager.timerStatus == .stopped {
                PickerView()
            } else {
                TimerView()
            }

            VStack {
                Spacer()
                ButtonsView()
                    .padding(.bottom)
            }
        }
        //指定した時間(1秒)ごとに発動するtimerをトリガーにしてクロージャ内のコードを実行
        .onReceive(timeManager.timer) { _ in
            //タイマーステータスが.running以外の場合何も実行しない
            guard self.timeManager.timerStatus == .running else { return }
            //残り時間が0より大きい場合
            if self.timeManager.duration > 0 {
                //残り時間から -1 する
                self.timeManager.duration -= 1
                //残り時間が0以下の場合
            } else {
                //タイマーステータスを.stoppedに変更する
                self.timeManager.timerStatus = .stopped
            }
        }
    }
}

これでようやくカウントダウンタイマーらしくなってきました。
次回は、アラームの実装をしていきます。

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

iOSアプリ開発:タイマーアプリ(4.カウントダウンの実装)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、カウントダウンタイマーで実際に残り時間の表示を1秒毎に更新する手順について掲載します。

開発環境

  • OS: macOS 10.15.x (Catalina)
  • エディタ: Xcode 12.x
  • 言語: Swift
  • 主な使用ライブラリ: SwiftUI

手順概要

  1. TimeManagerクラスにTimerクラスpublishメソッドを追加する
  2. MainViewの残り時間の表示を1秒毎に更新する

手順詳細

1. TimeManagerクラスにTimerクラスpublishメソッドを追加する

現時点では、スタートボタンを押しても、タイマーステータスは.runningに変わりますが、画面上はまだ残り時間がカウントダウンされません。画面上はPickerで設定した時間のまま止まっています。

そこで、このタイマーアプリの肝となる残り時間のカウントダウン表示を実装していきます。

SwiftUIライブラリにも含まれますが、Swift言語の一番ベースとなるFoundationライブラリに用意されているTimerというクラスのpublishというメソッドを利用します。

以下のように記述することで、publishメソッドの引数everyで指定した時間(1秒)ごとに発動するタイマーを変数timerに格納します。これをView側で利用することで、何らかのアクションを1秒毎に発動したいときにトリガーになってくれます。ここでは、タイマーアプリが残り時間を1秒毎に更新する際にトリガーとして利用していきます。

TimeManager.swift
class TimeManager: ObservableObject {
    //(他のプロパティ省略)

    //1秒ごとに発動するTimerクラスのpublishメソッド
    var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    //(メソッド省略)
}

2. MainViewの残り時間の表示を1秒毎に更新する

MainViewでTimerViewを含んでいる一番外側のZStackに .onReceiveモディファイアを追加します。ZStack内のすべてのViewに反映されます。

この.onReceiveモディファイアがTimeManagerクラスのtimerの1秒毎の発動を受けとります。そして、そのあとのクロージャにはtimer発動ごとに実行するコードを記述します。

guard let ~ else 構文で、タイマーステータスが.running以外の時は何も実行しないようにクロージャにはreturnを記述します。
タイマーステータスが.runningであれば、それ以下のif文へ進みます。

if文では、残り時間が0より大きい時は、残り時間から1秒引き算し、残り時間が0以下の場合は、タイマーステータスを.stoppedに変更するように記述します。

MainView.swift
struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        ZStack {
            if timeManager.timerStatus == .stopped {
                PickerView()
            } else {
                TimerView()
            }

            VStack {
                Spacer()
                ButtonsView()
                    .padding(.bottom)
            }
        }
        //指定した時間(1秒)ごとに発動するtimerをトリガーにしてクロージャ内のコードを実行
        .onReceive(timeManager.timer) { _ in
            //タイマーステータスが.running以外の場合何も実行しない
            guard self.timeManager.timerStatus == .running else { return }
            //残り時間が0より大きい場合
            if self.timeManager.duration > 0 {
                //残り時間から -1 する
                self.timeManager.duration -= 1
                //残り時間が0以下の場合
            } else {
                //タイマーステータスを.stoppedに変更する
                self.timeManager.timerStatus = .stopped
            }
        }
    }
}

これでようやくカウントダウンタイマーらしくなってきました。
次回は、アラームの実装をしていきます。

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

UnityでiOS向けにC系の関数を扱う

概要

UnityからC言語を使う為の備忘録です。

参考

https://docs.unity3d.com/ja/2018.4/Manual/NativePlugins.html

実装

名前空間

using System.Runtime.InteropServices;

[DllImport("__Internal")] を使用するのに必要です。

属性

[DllImport("__Internal")] 

メソッドの上に書く。

サンプル

/// <summary>
/// 初期化
/// </summary>
[DllImport("__Internal")]
static extern void InitializeiOS(string hogehoge);

iOSでしか機能しないので「UNITY_IOS」などのシンボルを活用すると尚良い。

C

extern "C"
{
    void InitializeiOS(char *hoge)
    {
        // 処理
    }
}

stringはcharのポインタで。

設置方法

基本的には「Assets/Plugin/iOS/XXXXX.mm」としてファイル名をつけて保存する。
ビルドするとそのままXcodeプロジェクト内部に保存される。

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

iOS14(xcode12ビルド)で、WKWebView の contentInset.top を一定値以上セットすると webView の load 中になぜか最下部までスクロールされる バグ?

iOS14+xcode12によるビルドでバグのような挙動が発生しているので報告です

WKWebView の contentInset.top を一定値以上セットすると、
webView の load 中になぜか最下部にスクロールされる

というものになります。

一定値というのは700〜800あたりで発生することを確認してます
500とかだと発生しません

以下再現コードです
WKWebViewを全面に配置しただけのstoryboardもあらかじめ作ってあります

class ViewController: UIViewController {
    @IBOutlet weak var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let url = URL(string: "https://cookpad.com")!

        let request = URLRequest(url: url)

        webView.load(request)

        webView.scrollView.contentInset.top = 800.0

        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            self.webView.reload()
        }
    }
}

実行すると、なぜか最下部までスクロールした状態でロードされます。
リロード後も同じ。

例:

しかし、軽いページなのかなにか条件があるのか、一度目はちゃんと最上部のままロードされるページもあります
(google.co.jp等)
ただ、その場合も reload() 後は最下部にスクロールされた状態になります

iOS14SDKのバグっぽい挙動なのですが、報告です

もし回避策がありましたら教えていただけるとありがたいです!

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

iOSアプリ開発:タイマーアプリ(3.スタート/ストップボタン、リセットボタン)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、カウントダウンタイマーのメイン操作となるスタート、一時停止、リセット機能を実装するための手順を掲載します。

開発環境

  • OS: macOS 10.15.x (Catalina)
  • エディタ: Xcode 12.x
  • 言語: Swift
  • 主な使用ライブラリ: SwiftUI

手順概要

  1. TimeManagerにタイマーのステータスを示すプロパティを作成する
  2. TimeManagerにタイマーのステータスを変更するメソッドを作成する
  3. ButtonsViewを作成する
  4. ButtonsViewにスタート/ストップボタンを作成する
  5. ButtonsViewにリセットボタンを作成する
  6. MainViewにButtonsViewを配置する

手順詳細

1. TimeManagerにタイマーのステータスを示すプロパティを作成する

タイマーのステータスを表すプロパティをTimeManagerクラスに作成しておきます。ステータスは以下の3つとします。

  • running:タイマーがカウントダウン中の状態
  • pause:タイマーが一時停止中の状態、再開可能
  • stopped:タイマーがカウントダウン終了している状態

まず下ごしらえとして、Data.swiftファイルにタイマーのステータスを表す新しいenumを作成します。

Data.swift
//(他のenum省略)

enum TimerStatus {
    case running
    case pause
    case stopped
}

作成したenumをデータ型としてTimeManagerクラスにプロパティを作成します。タイマーは使用者がスタートボタンをタップするまでは作動していない状態にする必要があるので、このプロパティのデフォルトの値は.stoppedにしておきます。

TimeManager.swift
class TimeManager: ObservableObject {
    //(他のプロパティ省略)

    //タイマーのステータス
    @Published var timerStatus: TimerStatus = .stopped

    //(メソッド省略)

2. TimeManagerにタイマーのステータスを変更するメソッドを作成する

まだボタンを作成していませんが、先にそれぞれのボタンをタップしたときに、タイマーをスタートしたり、一時停止したり、完全に終了させるメソッドをそれぞれ作成します。言い換えると、これらのメソッドにより先に作成したタイマーステータスのプロパティの値が都度変更される形になります。

TimeManager.swift
class TimeManager: ObservableObject {
    //(プロパティ省略)

    //(他のメソッド省略)

    //スタートボタンをタップしたときに発動するメソッド
    func start() {
        //タイマーステータスを.runningにする
        timerStatus = .running
    }

    //一時停止ボタンをタップしたときに発動するメソッド
    func pause() {
        //タイマーステータスを.pauseにする
        timerStatus = .pause
    }

    //リセットボタンをタップしたときに発動するメソッド
    func reset() {
        //タイマーステータスを.stoppedにする
        timerStatus = .stopped
        //残り時間がまだ0でなくても強制的に0にする
        duration = 0
    }
}

3. ButtonsViewを作成する

ButtonsViewという名前のswiftファイルを新たに作成します。同名のstructが生成されます。

ボタン操作と手順1で作成したTimeManagerクラスのtimerStatusプロパティを連携する必要があるため、このViewでもTimeManagerクラスのインスタンスを作成しておきます。例によって、@EnvironmentObjectのプロパティラッパーもつけておきます。

ButtonsView.swift
import SwiftUI
    @EnvironmentObject var timeManager: TimeManager

struct ButtonsView: View {
    var body: some View {

    }
}

4. ButtonsViewにスタート/ストップボタンを作成する

スタートボタンとストップボタンは画面上同じ場所に表示することにします。
タイマーステータスによって、どちらのボタンを画面上に表示するかを条件分岐させます。

  • .runningの時は一時停止ボタンを表示
  • .pause または .stoppedの時はスタートボタンを表示

ボタンアイコンには、Apple純正SF Symbolsから以下2つを採用します(オーディオの再生、一時停止によく使われるアイコンです)。

  • play.circle.fill:スタートボタン
  • pause.circle.fill:一時停止ボタン

残り時間の表示が0のときは、スタートも一時停止もできないことを示すため、透明度の.opacityモディファイアで条件分岐を作っておきます。

ボタンをタップしたときのアクションとして、PickerViewが表示(そして時間設定)されている場合、Startボタンをタップするとタイマーがセットされるように、setTimerメソッドを指定します。PickerViewが表示されている時というのは、タイマーステータスが.stoppedの時なので、これをonTapGestureモディファイアの中にif文で記述します。

ボタンをタップしてこのsetTimerメソッドが実行されると、残り時間のdurationプロパティ、最大時間のmaxValueプロパティに設定した時間が代入されます。このdurationが0以外の状態、かつタイマーステータスが.runningではない状態の時startメソッドも実行されるようにonTapGestureモディファイアの中にif文で記述します。

別の条件としてタイマーステータスが.runningの時のみ一時停止できるように、先のif文に今度はif else文で続けて記述していきます。

ButtonsView.swift
struct ButtonsView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        //running: 一時停止ボタン/pause or stopped: スタートボタン
        Image(systemName: self.timeManager.timerStatus == .running ? "pause.circle.fill" : "play.circle.fill")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 75, height: 75)
            //ボタンの右側とスクリーンの端にスペースをとる
            .padding(.trailing)
            //Pickerの時間、分、秒がいずれも0だったらボタンの透明度を0.1に、そうでなければ1(不透明)に
            .opacity(self.timeManager.hourSelection == 0 && self.timeManager.minSelection == 0 && self.timeManager.secSelection == 0 ? 0.1 : 1)
            //ボタンをタップした時のアクション
            .onTapGesture {
                if timeManager.timerStatus == .stopped {
                    self.timeManager.setTimer()
                }
                //残り時間が0以外かつタイマーステータスが.running以外の場合
                if timeManager.duration != 0 && timeManager.timerStatus != .running {
                    self.timeManager.start()
                //タイマーステータスが.runningの場合
                } else if timeManager.timerStatus == .running {
                    self.timeManager.pause()
                }
            }
    }
}

5. ButtonsViewにリセットボタンを作成する

ButtonsViewの中に、さらにリセットボタンを作成します。

このボタンをタップすると、手順2で作成したTimeManagerクラスのresetメソッドが発動します。

ボタンのアイコンには、SF Symbolsの"stop.circle.fill"を採用しました。

タイマーステータスが.stopped以外の場合にresetメソッドが発動するように、onTapGestureモディファイアの中にif文で記述します。

ボタンのレイアウトについて、リセットボタンを画面の左、先に作成したスタート/一時停止ボタンを画面の右に配置したいので、HStackの中に両方のボタンを配置します。

デフォルトのままだと、どちらのボタンも配置が画面中央になるため、ボタンとボタンの間にSpacerを入れ、ボタンがスクリーン両端にそれぞれ寄るようにします。

ボタンをスクリーン端に寄せすぎると見栄えが悪いので、paddingで調整しています。

ButtonsView.swift
struct ButtonsView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        //HStackで画面の左にリセットボタン、右にスタート/一時停止ボタン
        HStack {
            //リセットボタン
            Image(systemName: "stop.circle.fill")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 75, height: 75)
                //ボタンの左側とスクリーンの端にスペースをとる
                .padding(.leading)
                //タイマーステータスが終了なら透明度を0.1に、そうでなければ不透明に
                .opacity(self.timeManager.timerStatus == .stopped ? 0.1 : 1)
                //ボタンをタップしたときのアクション
                .onTapGesture {
                    //タイマーステータスが.stopped以外の場合
                    if timeManager.timerStatus != .stopped {
                    self.timeManager.reset()
                }

            //ボタンとボタンの間隔をあける
            Spacer()

            //running: 一時停止ボタン/pause or stopped: スタートボタン
            Image(systemName: self.timeManager.timerStatus == .running ? "pause.circle.fill" : "play.circle.fill")
                //(モディファイア省略)
        } 
    }
}

6. MainViewにButtonsViewを配置する

MainViewにはすでにPickerViewとTimerViewが追加してありました。

タイマーステータスが.stoppedかどうかで、どちらのViewが表示されるか変わるようにif-else文で記述しますします。

PickerViewとTimerViewの2つは、このタイマーアプリのもっとも重要なコンポーネントのため、画面中央に配置しておきます(配置を指定しなければデフォルトで水平、垂直方向で中央になります)。

iPhoneなどiOSデバイスの指での操作を考えると、今回追加したい ButtonsView は PickerView / TimerView より下、それもスクリーン下端に寄せる形で配置したいので、ZStack で PickerView / TimerView とはレイヤーを分ける形にし、VStack で ButtonsView の上に Spacer を配置することでButtonsViewを下端へ寄せます。ただしスクリーン端ギリギリだと見栄えが良くないため、padding(.bottom)モディファイアで微調整しておきます。

MainView.swift
struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager

     var body: some View {
        ZStack {
            if timeManager.timerStatus == .stopped {
                PickerView()
            } else {
                TimerView()
            }

            VStack {
                Spacer()
                ButtonsView()
                    .padding(.bottom)
            }
        }

    }
}

次回は残り時間のカウントダウン表示を実装していきます。

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

Flutter build error が起きたときにするべきこと

はじめに

Flutterとはネイティブアプリを開発するためのフレームワークとして2018年にGoogleが発表しました。今回はFlutterでbuildしているときにエラーが発生したときにするべきことを書こうと思います。

最初に、、 【エラー状況を知る】

実際にどういうエラーなのかなどの詳細を知るためには以下のコマンドを入力してください。これでflutter runコマンド実行中の詳細ログを表示します。

$ flutter run --verbose

手段1 【flutter clean】

Flutter build errorが発生したときに一番最初にやるべきことはflutter cleanです。

$ flutter clean

30%のエラーはこの方法で解決します。手段2以降はこの方法でもうまくいかなかった人向けです。

手段2 【Pod関連を再インストール】

$ flutter clean
$ rm -Rf ios/Pods
$ rm -Rf ios/.symlinks
$ rm -Rf ios/Flutter/Flutter.framework
$ rm -Rf ios/Flutter/Flutter.podspec
$ rm ios/Podfile

手段3 【Flutterのバージョンを更新】

次はFlutterのバージョンを更新します。

# flutter upgrade
$ flutter clean

手段4 【Pod update】

次はFlutterのバージョンを更新します。

# flutter upgrade
$ flutter clean

手段5 【FlutterのChannelを変更】

次はFlutterのChannelを変更します。FlutterのChannelはmaster、beta、stableなどがありますが基本的にstableにしていた方がバグやエラーが少ないです。

# flutter channel
$ flutter channel stable
$ flutter clean

手段6 【Xcodeのキャッシュ削除】

Tip: Xcodeがフリーズする場合などに解決策として使われるコマンドのようです。

$ rm -rf ~/Library/Caches/com.apple.dt.Xcode

手段7 【DerivedData(中間生成ファイル)の削除】

DerivedDataとはXcodeで生成される中間生成ファイルが保存されるディレクトリの名前です。

$ rm -rf ~/Library/Developer/Xcode/DerivedData/

手段8 【Xcode Toolsによるキャッシュ削除】

DerivedDataとはXcodeで生成される中間生成ファイルが保存されるディレクトリの名前です。

$ xcrun --kill-cache

手段9 【シミュレータにインストールしたアプリ、設定を削除】

$ xxcrun simctl erase all

手段10 【CocoaPodsのキャッシュ削除】

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

Swift Optionalの研究

Optional型

Optional型に関していろいろ書籍等でもう一度勉強し直したのでついでに書きます。
自分で勉強したものをそのまま載せますので、見辛いかもしれませんがご了承ください
使い方というより、そもそも的な物なので、使い方的なものに関しては、他の記事を読んだ方が良いです。

ざっくり Optionalとは

  • 値があるか空かいずれかを表す型
  • 基本的にはnilは許容しないが許容する場合はOptionalを使う
  • Wrappedはプレースホルダー型という
  • Wrappedを具体的な型に置き換えて使用
// Optionalはenumで定義されている
enum Optional<Wrapped> {
    case uone
    case some(Wrapped)
}

let none = Optional<Int>.none
print(".none: \(String(describing: none))")
let some = Optional<Int>.some(1)
print(".some: \(String(describing: some))")

* 型推論
let some2 = Optional.some(1) // Optional<Int>
let none2: Int? = Optional.none // Optional<Int> nil
// .someは型推論できる .noneは型指定しないとダメ Error

var a: Int?
a = nil         // nilリテラル代入による.noneの生成
a = Optional(1) // イニシャライザによる.someの生成
a = 1           // 値代入による.someの生成
let opInt: Int? = nil
let opString: String? = nil

print(type(of: opInt), String(describing: opInt))
print(type(of: opString), String(describing: opString))
// Optional<Int> nil
// Optional<String> nil

*イニシャライザによる.someの生成*

let opInt2 = Optional(1)
let opString2 = Optional("a")
print(type(of: opInt2), String(describing: opInt2))
print(type(of: opString2), String(describing: opString2))
// Optional<Int> Optional(1)
// Optional<String> Optional("a")

*値代入による.someの生成*

let opInt3: Int? = 1
print(type(of: opInt3), String(describing: opInt3))
// Optional<Int> Optional(1)
// アンラップ
// Optional<Wrapped>型は値を持っていない可能性があるため
// Wrapped型の変数や定数と同じように扱うことができない
// Int?型どうしの演算はerrorになる
let aa: Int? = 1
let bb: Int? = 1
// aa + bb これだとerror
/*
 Optional<Wrapped>型の値が持つWrapped型の値に対する操作を行うには、
 Optional<Wrapped>型の値からWrapped型に取り出す必要がある
 Wrapped型の値を取り出す操作をアンラップと言う
 */

* 大事なところ

// オプショナルバインディング
// ??演算子
// 強制アンラップ

* オプショナルバインディング
/*
 条件分岐や繰り返し文の条件にOptional<Wrapped>型の値をしていする
 値の存在が保証させている分岐内では、Wrapped型の値に直接アクセスすることができる
 if-let文
 if let 定数名 Optional<Wrapped>型の値 {
  値が存在する場合に実行される文
 }
 */

let optionalA = Optional("a") // String?型
if let a = optionalA {
    print(type(of: a)) // optionalAに値があるときのみ実行される
}
// String

* ??演算子
// 値が存在しない場合 defaultの値を表示
let optionalInt: Int? = 1
// let optionalInt: Int? = nil この場合 3が表示させる
let int = optionalInt ?? 3 // 1

* 強制アンラップ
/*
 Optional<Wrapped>型からWrapped型の値を強制的に取り出す方法
 強制的というのは、値が存在しない場合実行Errorになることを意味する
 !演算子を使用
 */

let num1: Int? = 1
let num2: Int? = 1
// 強制アンラップのやりかた
num1! + num2! // 2

/*
 強制アンラップは値がないケースを無視しているので、errorの危険性がある
 多用は避ける
 値の存在がよほど明らかな場合や、
 値が存在しない時はプログラムを終了させたい箇所以外は基本的に使用を避ける
 */

* オプショナルチェイン
// Optional<Double>型からDouble型の isInfiniteプロパティにアクセスするために
// オプショナルバインディングをしている

let optionalDouble = Optional(1.0)
let optionalIsInfinite: Bool?

if let double = optionalDouble {
    optionalIsInfinite = double.isInfinite
} else {
    optionalIsInfinite = nil
}
print(String(describing: optionalIsInfinite))

/*
 オプショナルチェインを使えばアンラップをしないでもWrapped型のプロパティやメソッドにアクセスできます。
 オプショナルチェインを利用する場合はOptional<Wrapped>の四季の後に?に続けて
 Wrapped型のプロパティやメソッド名を記述する
 Optional<Wrapped>の型の変数や定数がnilだった場合?以降に記述されたプロパティやメソッドへのアクセスは行わずに nilが返却される
 元のOptional<Wrapped>型の式が値を持ってないということは
 アクセス対象のプロパティやメソッドも存在しないということであり返すべき値も存在しないためです
 下記例では上のCodeをオプショナルチェインを使って書き換えたもの 結果はBool?型
 */

let opDouble = Optional(1.0)
let opIsInfinite = opDouble?.isInfinite
print(String(describing: opIsInfinite))

// 下記例はcontainsを呼び出し
// CountableRange<Int>?型の定数optionalRangeの範囲に指定した値が含まれているかどうかを
// 判定しています 結果はBool?値

let optionalRange = Optional(0..<10)
let containsSeven = optionalRange?.contains(7)
print(String(describing: containsSeven))

* map flatMap

// アンラップしないで値変換するメソッド
// Int?型の定数num3に対して値を2倍にするクロージャーを実行して、
// 結果としてInt?型の値Optinal(34)を受け取っている

let num3 = Optional(17)
let num4 = num3.map { value in
    value * 2
} // 34
type(of: num4) // Optional<Int>.Type
/*
また map を使って別の型に変換できる
 Int?型 num5に対してIntをStringに変換するクロージャーを実行して、結果としてString?の17を受け取る
 */

let num5 = Optional(17)
let num6 = num5.map { val in
    String(val)
} // "17"
type(of: num6)

// flat(Map
// クロージャーの戻り値はOptionalになる
let num7 = Optional("17")
let num8 = num7.flatMap { val in
    Int(val)
}
type(of: num8) // Optional<Int>.Type
// ポイント
/*
 値の有無が不確かな定数に対し、さらに値を返すか定かではない操作を行なっている点です
 ここがflatMapではなくMapの場合
 最終結果は二重にラップ型になってしまいInt??となる
 */

let num9 = Optional("17")
let num10 = num9.map { val in
    Int(val)
}

type(of: num10) // Optional<Optional<Int>>.Type
// この 二重に 不確かな状態を一つにまとめてくれるのがFlatMapである
// 暗黙的アンラップ
// Wrapped!に関して

var aaa: String? = "aaa"
var bbb: String! = "bbb"

print(type(of: aaa))
print(type(of: bbb))
// Optional<String>
// Optional<String>
var ccc: String! = aaa
var ddd: String? = bbb

// 暗黙的アンラップはnilの場合errorになる
let eee: Int! = 1
eee + 1 // Int型ど同様に演算が可能
// var fff: Int! = nil
// fff + 1 // 値が入っていないため実行時Errorが起きる
/*
 Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: file __lldb_expr_84/Optional.playground, line 224
 Playground execution failed:
 error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
 The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.
 */

まとめ

  • 通常は バインディング、?? map flatMap を組み合わせて使うのがベター
  • 強制アンラップや暗黙的アンラップは実行時にErrorになるので避ける
  • !を多用しない
  • 常に考える

せっかく勉強したので、とりあえず記事にしました。
今回は他の記事と違って質低いので、読んでくれただけでありがたやです。

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