20210125のiOSに関する記事は6件です。

エラー「Application tried to present modally a view controller」の時

環境

・Xcode: 12.3
・Swift5

以下のようなエラーログがでてクラッシュしてしまった時

libc++abi.dylib: terminating with uncaught exception of type NSException

*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: 'Application tried to present modally a view controller <ProjectName.ViewController: 0x158331780> 
.that has a parent view controller <UINavigationController: 0x159877a00>.'
terminating with uncaught exception of type NSException

こんなパターンがあるよ

スクリーンショット 2021-01-25 23.26.29.png

上記のようにViewControllerに対してNavigationControllerがいる場合に、
ViewControllerを表示しようとしてしまっている可能性

        let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
        if let navigationVc = storyboard.instantiateInitialViewController() as? UINavigationController,
           let viewController = navigationVc.topViewController as? ViewController {
            // これは×
            // present(viewController, animated: true, completion: nil)
            // これは○
            present(navigationVc, animated: true, completion: nil)
        }

これに限らないかと思いますが、誰かの役に立てば。

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

inputViewを使ってカスタムキーボードをつくろう!(Swift)

Xcode-12 Swift-5.3 iOS-14

はじめに

UITextFieldUITextView で入力する際はキーボードの入力タイプが色々用意されていますが実はこのキーボード色々カスタムができます!

たぶん実用的なのは UIPickerView とか UIDatePicker を表示したりとかです(なんか昔半角カナ用キーボードを作った記憶があるけどあんまよくなかった:innocent:)。

これからはもう UIKit を使うことも少なくなっていくのかもしれませんが備忘録として。。。

*注:「つくろう」って書きましたがとくにカスタムキーボードを薦めるものではありません。

UITextFieldをカスタマイズ

試しに UITextField のキーボードをカスタムしてみます。やり方は簡単で inputView に任意の View を設定するだけで OK です:v:

// こんなのとか
let keyboard = CustomKeyboardView(frame: .init(origin: .zero, size: .init(width: 284, height: 284)))
keyboard.delegate = self
textField.inputView = keyboard

// こんなのとか
let picker = UIPickerView()
picker.delegate = self
picker.dataSource = self
textField.inputView = picker

あとはデリゲートとかで入力文字を受け取って UITextField に表示するだけです。

こんな感じ。

カスタム View ピッカー
custom picker

カスタム View はこんな感じです。

xib

xib

protocol CustomKeyboardViewDelegate: AnyObject {
    func customKeyboardView(_ customKeyboardView: CustomKeyboardView, didSelectKey key: String)
}

final class CustomKeyboardView: UIView {

    weak var delegate: CustomKeyboardViewDelegate?
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    private func loadNib() {
        let view = Bundle.main.loadNibNamed("CustomKeyboardView", owner: self, options: nil)?.first as! UIView
        view.frame = bounds
        view.translatesAutoresizingMaskIntoConstraints = true
        view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addSubview(view)
    }

    @IBAction private func selectKey(_ sender: UIButton) {
        delegate?.customKeyboardView(self, didSelectKey: sender.titleLabel!.text!)
    }
}

キーボードの高さはわりと自由みたいです:raised_hands:

custom_2

サクッとカスタムキーボードが表示できました!ただカスタムキーボードを入力制限目的で利用するにはいくつか問題があるのでオススメしません:no_good:コピペとか外部キーボード接続とか。。。

コピペに関しては UITextField のカスタムクラスをつくって canPerformAction ごにょごにょすればいける気がしますが外部キーボードに関しては防げないと思われます(そもそもあんまり UITextField で入力制限なんてしない方がいいと思います)。

UIControlをカスタマイズ

とくに UITextFieldinputView に設定する!でも問題ないのですが UITextField を使うとなるとレイアウトの自由度が低いのでせっかくなんでカスタムクラスをつくった方が自由度が高いのでおすすめです:thumbsup:

下記のように UIControl を継承したクラスをつくって canBecomeFirstResponderinputView を設定してやるとカスタムキーボードが表示できます。UIView じゃないのは addAction がしたかったからです。フォーカス時がわかりにくいので枠線の色を変更しています。

protocol PickerInputControlDelegate: AnyObject {
    func pickerInputControl(_ pickerInputControl: PickerInputControl, didSelectValue value: String)
}

final class PickerInputControl: UIControl {

    var items = [String]()
    weak var delegate: PickerInputControlDelegate?

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        commonInit()
    }

    private func commonInit() {
        layer.cornerRadius = 8.0
        layer.borderWidth = 1.0
        layer.borderColor = UIColor.systemGray.cgColor
        addAction(.init(handler: { [weak self] _ in
            self?.isSelected.toggle()
            if self?.isSelected == true {
                self?.becomeFirstResponder()
            } else {
                self?.resignFirstResponder()
            }
        }), for: .touchUpInside)
    }

    override var inputView: UIView? {
        let picker = UIPickerView()
        picker.delegate = self
        picker.dataSource = self
        return picker
    }

    override var canBecomeFirstResponder: Bool {
        return true
    }

    override var isSelected: Bool {
        didSet {
            layer.borderColor = isSelected ? UIColor.systemBlue.cgColor : UIColor.systemGray.cgColor
        }
    }

    @discardableResult
    override func becomeFirstResponder() -> Bool {
        let value = super.becomeFirstResponder()
        isSelected = isFirstResponder
        return value
    }

    @discardableResult
    override func resignFirstResponder() -> Bool {
        let value = super.resignFirstResponder()
        isSelected = isFirstResponder
        return value
    }
}

extension PickerInputControl: UIPickerViewDelegate {

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        delegate?.pickerInputControl(self, didSelectValue: items[row])
    }
}

extension PickerInputControl: UIPickerViewDataSource {

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return items.count
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return items[row]
    }
}

こんな感じで UIControl なので中に View 置き放題です。

control

UIPickerViewDelegate, UIPickerViewDataSource, items は外に出した方が汎用性が高いかも:innocent:

static cellと組み合わせる

おまけで UITableViewController の static cell と組み合わせるとフォーカス時に自動スクロールしてくれるので複数の入力項目がある場合はおすすめです:thumbsup:

こんな感じ。

static_cell

おわりに

カスタムキーボードを作る際はタップ領域をちゃんと確保してあげると素敵です。ちょっと古い記事ですが下記の記事はキーボードのタップ領域とか詳しく調べていておもしろいです。

iPhone の当たり判定を検証した

わりとお世話になった手法だけどこれからはあんまり使わなくなるのかな UIKit。。。

調べてて気になったけどこいつ知らねえ:sweat:

inputviewcontroller

参考

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

iPhoneで快適な音楽ライフを 〜ショートカット×Ever Play×FLAC〜

サマリー

iPhoneのショートカットアプリは様々な一連の操作の簡略化に一役買っている。中でもミュージックアプリとの相性は良く、連携させる事でAirPods接続時や帰宅時をトリガーとしたBGMやプレイリストなどの自動再生ができるなど便利な点が多い。しかし、この連携は純正のミュージックアプリ間に限られており、またミュージックアプリおよびiTunesはFLACファイルの管理・再生を行うことができないことが著者の長年の不満となっている。
今回私はショートカットに対応した非純正アプリ"Ever Play"を発見した。このアプリは純正のミュージックアプリやiTunesで対応していないFLACファイルの再生に対応しているため、iPhoneでより高音質な音楽を楽しむことができる。また、ショートカットアプリとの連携が可能になったことで、より便利にミュージックアプリを扱える環境を整備することができた。

イントロ 〜ショートカット×ミュージック〜

iPhoneの様々な煩雑な作業をまとめて効率化してくれる便利アプリ、ショートカット。このアプリはミュージックアプリと連携することが可能である。例えば、iPhoneがAirPods(orその他のBTイヤホン)に接続することをトリガーとしてお気に入りのプレイリストを自動再生させたり、帰宅して宅内Wi-Fiに接続したことをトリガーとしてAirPlayを介したBGMの自動再生させたり、することができる。

ショートカットAppで操作できるミュージックアプリは純正アプリにしか対応していない

こんなにも便利なショートカットだが欠点がある。それは数あるミュージックアプリの中でもApple純正のミュージックアプリにしかショートカットが対応していないことである。そのため、純正以外に愛用している音楽アプリやSpotify、Amazon music、LINE musicなどの音楽配信アプリをショートカットでコントロールすることができない。

(Amazon musicのプレイリストの自動再生は対応させることに成功している。
ショートカットを利用したAmazon Musicのプレイリスト自動再生)。

"ショートカットとの連携が優先だったら大人しく純正アプリ使えばいいじゃん"と思うかもしれないが、もうひとつの難点がそれを阻んでいる。

純正アプリおよびiTunesはFLACに対応していない

音楽好きにとってiPhoneが好まれない仕様が様々あるが、中でも私が特に感じている事はApple純正アプリとiTunesで FLACと呼ばれるフォーマットの音楽ファイルのライブラリーへの追加・再生ができないことである。

FLAC(フラック、Free Lossless Audio Codec)は、オープンフォーマットの可逆圧縮音声ファイルフォーマットである。〜〜〜〜〜。可逆圧縮であるため、元の音声データからの音質の劣化が無い。
(Wikipediaより転載、FLACについてのページ)

CDをリッピングする際、CDと変わらないクオリティであるWAVフォーマットはファイルサイズが大きいことが難点であるが、FLACは無劣化の可逆圧縮が可能であるため、WAVとほぼ同等音質を保てる上に(異論は認める)、ファイルサイズが小さく、さらにジャケット写真などのタグをファイル内に保存できるのでとても優秀なフォーマットである。音楽配信サイトのレコチョクでは、FLACフォーマットでハイレゾ音源の配信を行なっており、私も全ての音源やレンタルしたCDはすべてこのFLACフォーマットで保存・管理している。
こんな便利なファイルにも関わらず、iTunesは頑なにこのフォーマットに対応しようとしてこなかった。iPhone側はiOS11以降FLACフォーマットの再生に対応しているのに、だ。

「iPhone X」「iPhone 8」はFLAC再生をサポートし、「iOS 11」で「iPhone 7」でも再生が可能

私は約4年ほどiTunesのFLAC対応を待っているが一向に対応する気配を見せない。これは意図的だと私は考える。


そのため、Appleが変わるのを待つのではなく、こちらがなんとか変わる他なくなってしまった。最も無難な解決策として、全てのFLACファイルをiTunesに対応しているALACフォーマット等に変換することが挙げられる。これは膨大な音楽データを変換することが大変手間である上に、管理しているストレージ内でのデータ量がほぼ倍に増えてしまう点で絶対にやりたくない。

iTunesはFLACを扱えないわけではない

いろいろ調べてみると、iTunesではミュージックライブラリにFLACを読み込むことができないだけで、ファイル共有機能を使用すればPC内部のFLACフォーマットの楽曲をiPhoneへ転送することが可能であることがわかった。さらに、いくつかのミュージックアプリではFLACファイルの再生が可能であることもわかった。やや希望の光が差し込んできた瞬間である。そこで、FLACファイルの再生に対応した音楽アプリを探す旅を始めた。

FLACファイル対応アプリ: Ever Play

長らくFLACファイル対応のミュージックアプリを探す旅をしたが、結論としてiPhoneに最適なアプリはEver Playというものに自分の中で落ち着いた。

Ever Play

Twitterでサーチした感じからして日本人のユーザーはほとんどいないものと思われる。他の有名どころとしてfoobar2000もあるが、ひらがなやカタカナ名のアーティストが重複して表示されてしまうバグがあったため却下した。
Ever Playが素晴らしいのはFLACに対応しているのはもちろんのこと、WAV・DSDといったハイレゾ音源に使用される音楽フォーマットの再生までカバーしていることである。それでいて動作はとても軽く、歌詞表示までできてしまう優れものである。気に食わないことはアルバムアーティストのカラムがないこと・プレイリストがインポートできないこと(これは事実か追って検証する必要がある)があるが、これは仕方なく目を瞑る(今後のアップデートによる追加を祈る)。

※プレイリストのインポート-likeなことは一応できなくもない。iTunesのファイル共有で楽曲の転送を行う際に、プレイリスト単位で送信すると、転送後のフォルダがプレイリストに対応するため一応プレイリスト単位での曲の管理や再生ができる。

Ever Play、アップデートによるショートカット対応

アップデートに全く気づけなかったのだが、Ever Playがいつのまにかショートカットに対応していた。しかもこれがただ全曲を再生するだけではなく、ショートカットアプリでの制御と同様に、再生するリストの範囲を変えることができる点がとても良い。

試しにショートカットを作ってみる。
ホーム画面の右上のショートカットアプリのマークをタップすると、
Screenshot at Oct 27 18-00-47.png

ショートカットの編集画面のようなものが出てくる。
Screenshot at Oct 27 18-00-47.png

このまま"Add Shortcut to Siri"を押すと、ショートカットアプリに全曲再生のショートカットが登録されている。"Shuffle"をオンにすれば全曲シャッフルのショートカットを登録できる。
Screenshot at Oct 27 18-00-47.png

ここで注意点が1つ。シャッフルのオンオフはこの画面で作成した時のみに変更可能で、ショートカット登録後には変更できない。

ホーム画面の右上のショートカットアプリのマークをタップ後の画面で、青色で下線のある"All Songs"をタップすると、このように再生範囲を"Playlist"、"Album"、"Artist"、"Genre"から選び、どの範囲で再生を行うか選ぶことができる。
Screenshot at Oct 27 18-00-47.png

例えば、"Playlist"を選べばどのプレイリストを再生するかの選択が表示される。まぁ便利。一つ残念なのは"File"が選択できないこと。
Screenshot at Oct 27 18-00-47.png

試しにお気に入りの曲のプレイリストを作成し、シャッフル再生のショートカットを組んでみるとこんな感じになる。
Screenshot at Oct 27 18-00-47.png

ミュージック×ショートカットの使用例

これでようやくミュージックアプリをショートカットの支配下におけたので、いろいろとショートカットを作ってみることにした。

1. 帰宅時に自宅のBTスピーカーから自動でBGMが再生されるようにする

これが一番やりたかったこと。帰宅したら自然とジャズが流れている自室という環境が作ってみたかった。
以下のようにショートカットを組んでみた。
Screenshot at Oct 27 18-00-47.png
メディアの出力先をBTスピーカーに変更し、音量をちょうど良い大きさの45%に変更し、Ever Playでジャズのプレイリストを再生する、といった感じ。

これによって
玄関前でショートカット起動 -> 玄関の鍵解錠 -> リビングルームのシーリングライト点灯 -> デスクの照明点灯 -> ジャズが流れる
という一連の工程を全て自動化させることに成功した。

2. 複数の自動再生ショートカットから選択して自動再生する

ここで作成した複数の音楽再生ショートカットの中からどれか一つを選んで再生するショートカットを作成したいと思う。
構想としてはこんな感じ。
・プレイリストやアーティストの自動再生ショートカットをひたすら作る
・ショートカットアプリ内で1つのフォルダに集約する
・集約したフォルダ内から1つを選んで再生するようなショートカットを組む

まず、お気に入りのプレイリストなりアーティストなりを再生するためのショートカットを先ほど述べた手順で作成する。
次に、作成したショートカットを1つのグループにまとめる。Ever Playアプリのショートカット用のフォルダを作成し、そこへ先ほど作成したショートカットを登録する。
Screenshot at Oct 27 18-00-47.png

Screenshot at Oct 27 18-00-47.png

そして最後に、そのフォルダを参照して1つのショートカットを選択して起動させるショートカットを組む。
Screenshot at Oct 27 18-00-47.png

起動させた様子がこちら。
Screenshot at Oct 27 18-00-47.png

余談

iTunesとミュージックアプリはいい加減FLACフォーマットの再生に対応しろ、以上。

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

Xcode12以降でシミュレータビルドできない時の対処法

Xcode12以降でシミュレータビルドできない時の対処法

  • こちらはメモ的なものになります
  • いろいろ調べて対応したので残しておきます。

対応方法

project -> architectures -> Excluded Architectures

ここに

  • arm64を記入
  • Any SDKのままでもビルドできたけど、AnyiOS SDKの方が良いと思う

スクリーンショット 2021-01-25 11.49.30.png

Pods関係

  • これを記入する
post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
    end
  end
end

参考

https://developer.apple.com/forums/thread/656509
https://stackoverflow.com/questions/63391793/xcode-12-build-target-in-wrong-order-for-simulator/63405201#63405201

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

【ionic】面倒臭がりでもできる、アラーム機能

時計やスケジュール張のアプリはもちろんですが、漫画アプリやコミュニケーションツールなどなど。通知機能はいろんなスマホアプリで活用されています。

今回はその中のアラーム通知をスマホアプリで実装する方法を「ionic」というフレームワークを用いて紹介させていただきたいと思います。

アラーム設定方法

基本的にはアラームの方法は2種類あります。

  • プッシュ通知(リモート通知)
    オンラインでの通知をすることができます。例えば、アプリ提供側から新着情報をユーザに提供したい場合はこれを使って通知を送ることができます。

  • ローカル通知
    プッシュ通知とは逆にオフラインの機能になります。ユーザが自分で時間を設定して、その時間に通知を受け取りたい場合に使えます。

一般的にアラームとなるとローカル通知をイメージされる方が多いと思います。今回はそのローカルでの通知を使ったアラーム機能を紹介します。

ionic公式より

ionicの公式ページにLocal Notificationという機能があります。これを使えば簡単に実装できます。

import { LocalNotifications } from '@ionic-native/local-notifications/ngx';


constructor(private localNotifications: LocalNotifications) { }

...

// Schedule delayed notification
this.localNotifications.schedule({
   text: 'Delayed ILocalNotification',
   //現在時刻の10秒後にアラーム。1秒=1000。
   trigger: {at: new Date(new Date().getTime() + 10000)},
   led: 'FF0000',
   sound: null
});

工夫を加えて実装

やりたいこと:複数のアラームを同時にかけたい。

タスクリストなどを作成し、それぞれに時間を設定。その時間になったらそれぞれのアラームが起動し、通知がくるようなアプリを想定してコードを書いていきます。

//以下のように配列にユーザが入力した時間とタイトル(通知が来たときのメッセージ)をもつような状態にする
todoList = [
  {
    time: [15,30],
    schedule:'おやつ'
  },
  {
    time: [18,50],
    schedule:'夜ご飯'
  },
]
todoAlerm() {
    const dateHour = new Date().getHours();
    const dateMinutes = new Date().getMinutes();
    //現在時間を「分」に置き換える
    const dateTime = dateHour * 60 + dateMinutes;

    for (let i = 0; i < this.todoList.length; i++) {
      let timeHour = this.todoList[i].time[0];
      let timeMinute = this.todoList[i].time[1];
    //設定した時間を「分」に置き換える
      const setAlerm = parseInt(timeHour, 10) * 60 + parseInt(timeMinute, 10);

      //設定した時間から現在時刻の差をとり、「秒」にする
      const disTime = (setAlerm - dateTime) * 60;

      ///ionic notification のtriggerに合う形に直していく
      const datedistime = new Date().getTime() + disTime * 1000;
      const alermsettime = new Date(datedistime);

      //音声の設定(AndroidとiOSで指定する音が異なるためどちらの端末か特定している)
      //ちなみにionicのplatform機能を利用
      if (this.platform.is('android')) {
        this.isAndroid = true;
      }

      //表示するメッセージを設定
      const alermtext = this.todoList[i].schedule + 'の時間になりました!';

      //現在時刻と設定時刻との差(disTime)が0以上だったら発動、そうでなかったらアラートを出す
      if (disTime >= 0) {
        this.localNotifications.schedule([
          {
       //ここがミソ!!アラームをいくつか設定する時はidを連番にする必要がある。
            id: i + 1,
            text: alermtext,
            trigger: {at: alermsettime},
            led: 'FF0000',
       //Androidなら'file://sound.mp3'、そうでない(iOS)なら'file://beep.caf'
            sound: this.isAndroid ? 'file://sound.mp3': 'file://beep.caf'
          }
        ]);
        this.alertSetting = true;
      } else  {
        this.alertSetting = false;
        alert(this.todoList[i].imageName.kanji + 'の時間を現在時刻より後に設定してください!。');
      }
    }
  }
}

以上です!!
ポイントは主に2つです。

  1. triggerに設定する時間
    時間の形式はどんな形でも良いわけではなく、フォーマットがあります。最初は少しここでつまづきました。1秒を1000とする決まりもあるので初心者の方は注意してください。

  2. いくつか同時に仕掛けたいならidを連番にする
    for文等を利用してidを連番にしないと上手く通知が表示されないので注意しましょう。

以上に注意すれば簡単にアラームを機能をつけることができます。
ionicフレームワークは他にも機能をたくさん揃えています。ほとんどが公式サイトにコードごと載っています。それを自分なりにカスタムしてより良いアプリやサービスを作っていけるといいですね!
機会があれば是非試してみてください!

ちなみに、以下のQRコードは実際に初学者である開発者が上記のアラーム機能を使って作ったスマホアプリです。「やることリスト」という機能に使われています。
image.png

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

【swift】文字列を一文字ずつに分割する時のArray("hoge")の型について

文字列を1文字ずつに分割したい!
競技プログラミングを解いているとよく出くわしますね。

解決方法としては

let str = "hoge"

let separatedStr = Array(str)
print(separatedStr)
 // ["h", "o", "g", "e"]

という感じでArray()の()の中に文字列を入れれば、一文字ずつ分割してくれます。

でも、これで分割された文字列の配列はString型ではなくCharacter型なのです。

Character型とは

developer.apple.comによると、
"A single extended grapheme cluster that approximates a user-perceived character."
(google翻訳: ユーザーが知覚する文字を近似する単一の拡張書記素クラスター。)

なんか難しく書いてますがとりあえず、
「人間が自然に認識できる最小の"1文字の型"」
と認識していただければ大丈夫です。

問題なのは、String型とは明確に違うってことです。
特にInt型に変換するときに問題になってきます。

Int型への変換(例: ABC083B - Some Sums

2桁以上の数字に対して、各桁の和を算出したいです。
(例: "13"だったら1+3 = 4、"624"だったら6+2+4 = 12)

まず、与えられた数字列を1文字ずつに分解します。

let digits = "13" //String型

let separatedDigits = Array(digits) //Character型の配列
print(separatedDigits)  //["1","3"]

さて、当然Int型に直さないと計算できないので、
この後["1","3"][1,3]に直します。

ただString型の感覚で

let intDigits = separatedDigits.map({Int($0)!})

ってやったら
Cannot convert value of type 'String.Element' (aka 'Character') to expected argument type 'String'
というエラーが出ます。
swift5のVersion 12.3ではCharacter型から直接Int型には変換できないためです。

なので、

let intDigits = separatedDigits.map({Int(String($0))!})

として一旦String型を経由させてください。

その後はいつも通りreduceで配列内の足し算を行えばいいので、

let result = intDigits.reduce(0, +)

print(result) //4

で導出完了です。

コード全文

let digits = "13"

let separatedDigits = Array(digits)
let intDigits = separatedDigits.map({Int(String($0))!}) //数字列の分割

let result = intDigits.reduce(0, +)
print(result) //4

参考文献

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