20200812のiOSに関する記事は11件です。

[Swift5]AVAudioEngineを使ってリアルタイム処理を行う最小実装

環境

  • macOS: Catalina
  • Xcode 11.6
  • Swift 5.2

リアルタイムに音声に処理を行う

  • 入力をそのまま出力
  • 入力にエフェクトをかけて出力

ソースコード

class ViewController: UIViewController {

    var engine = AVAudioEngine()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupAudioSession()

        let input = engine.inputNode
        let output = engine.outputNode
        let format = engine.inputNode.inputFormat(forBus: 0)
        engine.connect(input, to: output, format: format)

        try! engine.start()

    }

func setupAudioSession() {
        do {
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(.playAndRecord, options: [.defaultToSpeaker, .allowBluetooth])
            try session.setActive(true)
        } catch {
            fatalError("Failed to configure and activate session.")
        }
    }

補足1 AVAudioEngine

AVAudioEngineは暗黙的にinputとoutputを持っているためノードをattachする必要性がない。
つなげる時はconnectを用いる。

補足2 AudioSession

func setupAudioSession() {
        do {
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(.playAndRecord, options: [.defaultToSpeaker, .allowBluetooth])
            try session.setActive(true)
        } catch {
            fatalError("Failed to configure and activate session.")
        }
    }

これはAVAudioSessionの設定で

ステレオオーディオを録音するには、recordまたはplayAndRecordカテゴリのいずれかを使用するアプリのオーディオセッションが必要。

また、optionでiPhoneのデフォルトのスピーカーを使用することとBluetoothイヤホンで再生と録音が可能に設定。

詳しくはappleサンプルを参考にしてください!

エフェクトをかけたい場合

エフェクトをかけたい時にはノードを追加してあげると簡単にエフェクトをかけることができる。
アプリ起動時はイヤホン付けてデバックしないととハウリングが起きるので注意!

    var engine = AVAudioEngine()
    var delay = AVAudioUnitDelay()
    var reverb = AVAudioUnitReverb()

override func viewDidLoad() {
        super.viewDidLoad()
        setupAudioSession()

        let input = engine.inputNode
        let output = engine.outputNode
        let format = engine.inputNode.inputFormat(forBus: 0)
        delay.delayTime = 2.0
        reverb.loadFactoryPreset(.largeHall)
        reverb.wetDryMix = 40

        engine.attach(delay)
        engine.attach(reverb)
        engine.connect(input, to: delay, format: format)
        engine.connect(delay, to: reverb, format: format)
        engine.connect(reverb, to: output, format: format)

        try! engine.start()

    }

補足3 エフェクトをつなげる時はattachしてから

inputNodeoutputNodeattachする必要性がなかったが、エフェクトNodeを使用する際にはattachしないといけない。


遅延を入れてリバーブをかけるとコンサートホールにいるみたいなエフェクトになった!Bluetoothでもわかるくらいリバーブがいい感じにかかってて感動!

エフェクトクラス一覧

他にもエフェクトがあるので紹介(今回のも含めて)

  • AVAudioUnitReverb リバーブ処理
  • AVAudioUnitTimeEffect 非リアルタイムエフェクト処理
  • AVAudioUnitTimePitch 高品質の再生速度と音程シフトを互いに独立して提供
  • AVAudioUnitVarispeed 再生速度の制御
  • AVAudioUnitDelay 遅延処理
  • AVAudioUnitEQ マルチバンドイコライザの実行 EQ→イコライザの略
  • AVAudioUnitDistortion 歪みエフェクト

まだ使っていないエフェクトを使って音遊びしていきたい。

まとめ

AVAudioEngineは暗黙的にinputとoutputを持っているためノードをつなげる必要性がない。

WWDC2019に新しくAVAudioEngineが新しくなったらしいので、

このセッションを見て復習する。

What's New in AVAudioEngine

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

[SwiftUI]TextField�のリターンイベントはonCommit

TextFieldを使う時にリターンが押された際の処理は必須だと思います。
SwiftUIのTextFieldでは、リターンのイベント処理をonCommitで定義できます。

TextField("ぷれいすほるだー", text: $name, 
    onCommit: {
        //任意の処理
    })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】Pythonista3でGIFアニメ。でハマったこと。

はじめに

GIFアニメを作るアプリはたくさんありますが、せっかくPythonista3があるので、多くの先輩方の記事を参考にしながら、自分でも作ってみようと思いました。

すぐにハマる

以前にWin10上のPythonでGIFアニメを作ったことがあったので、その記憶を頼りにPILで書いたのですが、1枚目の画像しか保存されませんでした。

testGIF.py
w,h = 100,100
images = []

for c in range(0,256,8):
  img = Image.new('RGB',(w,h),(c,c,c))
  images.append(img)

images += reversed(images)

SaveName = 'test.gif'
images[0].save(SaveName,
             save_all=True,
             append_images=images[1:],
             optimize=False,
             duration=20,
             loop=0)

いろいろ調べたのですが、結局win10上では動作確認ができたのでiOSのPILではうまくいかないという結論にしました。

その名も「images2gif」

調べてるうちに同様の質問があり、「images2gif」の存在を知りました。
僕のやりたかったことがそのまま名前になったようなモジュール名です。

それは公式のドキュメントにも紹介されていて初めからインストールされてるものでした。

「images2gif」の使い方

基本的には次の記述でいいみたいです。

writeGif( SaveName, ImageList, duration=0.1,repeat=True)

testGIF2.py
from PIL import Image
from images2gif import writeGif

w,h = 100,100
images = []

for c in range(0,256,8):
  img = Image.new('RGB',(w,h),(c,c,c))
  images.append(img)

images += reversed(images)

SaveName = 'test.gif'
writeGif( SaveName, images, duration=0.02,repeat=True)

test.gif

PythonのGIFアート

日本語ヘルプ

最後にビックリしたのは、探してたどり着いた日本語ヘルプの記事が、「以前、僕が自分で投稿したもの」で、いよいよヤバいと思いました。

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

initの初歩 ?【Swift】

 initとは??

  • インスタンスを生成するときに、自動で呼び出されるメソッド。
  • イニシャライザとも呼びます。
  • クラスのプロパティの初期値を設定するときに使用。

Swiftのメソッドは、3種類あります。
その一つが、「initメソッド」 Swiftのメソッドの種類

スクリーンショット 2020-08-12 10.33.58.png
キノコード / プログラミング学習動画のYouTuber 

class Car {
    var color = ""
    var wheel = 0

    init() {
        print("initは、インスタンスを生成するときに自動で呼び出される、特殊なメソッドです。")
    }

}

let car = Car() // インスタンス生成。
実行結果.
initは、インスタンスを生成するときに自動で呼び出される、特殊なメソッドです。
  • 英 initialize: 「初期化する」
  • 「初期化」=初めて変数にデータを入れること。

 インスタンスとは??

クラスは設計図。
クラスは、インスタンス化しないと使うことができません。
instance=「実態」

let car = Car() // クラス名()を、変数に代入。 「インスタンス化」

インスタンス化とは、

クラスという「設計図」から、インスタンスという実際に使える「モノ」を作ること。

 補足

クラス名()とすることでインスタンスが生成できます。

Car() // インスタンス生成。

クラス名().変数名とすることで、そのクラスの変数にアクセスできます。

print(Car().wheel) // インスタンス生成。
「クラス名().変数名」の実行結果.
initは、インスタンスを生成するときに自動で呼び出される、特殊なメソッドです。
0

しかし、インスタンスを「変数や定数に代入」してから
使用する方法が一般的です。


 init内で、プロパティの初期値を設定してみる。

class Car {
    var color : String // 型の指定。 
    var wheel : Int

    init() {
        color = "Red" // init内で、プロパティの初期値を設定。
        wheel = 4     // 同じく。
    }

}

let car = Car() // インスタンス生成。

print(car.color)
print(car.wheel)
実行結果.
Red
4
  • var color = ""でも良いですが、不要なので型指定だけ。
  • (varを削除すると「colorって何?」となるので、エラー。)
  • 型指定では、:を使います。
  • インスタンスは、(car.color)のように「インスタンス名.○○」で値を参照。

また、インスタンス生成時にinitが優先的に呼び出されるので、

var color = "Green"でも、var color = "Blue"でも、
実行結果には反映されない。

 なぜ、initを使うのか。?

理由は色々あるらしいです。

クラスの記述内容がより分かりやすくなり、
明確に初期化を行いソースコードの安全性を高める為。

プロパティが正しく初期化できない場合...

インスタンス生成時に「引数で、インスタンスプロパティの初期値を与える」
ということが出来ない。

メモリ安全でない。
(メモリ確保、初期化がされる前にインスタンスにアクセスしてしまう)

 initメソッドに、『引数』を指定してみる。

  • self.
  • = color, = wheel
  • (color: String, wheel: Int)

スクリーンショット 2020-08-12 13.42.32.png

 『self』

selfとは、「インスタンス自身」を指す言葉。

クラスのインスタンスメソッド内でのselfは、
自分自身(クラスのインスタンス)を示します。

自分のクラス内の何かにアクセスしたい時に使います。

 (color: String, wheel: Int)について。

 クラスの引数に、値だけ指定するとエラー?

// インスタンス生成のとき。

let car = Car("Red", 4) // <--- 引数に、値だけ指定するとエラー。
error.
Missing argument labels 'color:wheel:' in call

パラメーター(ラベル)、必須です。

パラメータとラベルは、同名でもOKだけど、違うモノらしい。
[Swift] 関数ラベルの使い方を学ぶ

 順番が違ってもエラー?

// インスタンス生成のとき。

let car = Car( wheel: 4, color: "Red")  // <--- 順番が違ってもエラー
error.
Argument 'color' must precede argument 'wheel'

おしまい。

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

initの初歩?【Swift】

 initとは??

  • インスタンスを生成するときに、自動で呼び出されるメソッド。
  • イニシャライザとも呼びます。
  • クラスのプロパティの初期値を設定するときに使用。

Swiftのメソッドは、3種類あります。
その一つが、「initメソッド」 Swiftのメソッドの種類

スクリーンショット 2020-08-12 10.33.58.png
キノコード / プログラミング学習動画のYouTuber 

class Car {
    var color = ""
    var wheel = 0

    init() {
        print("initは、インスタンスを生成するときに自動で呼び出される、特殊なメソッドです。")
    }

}

let car = Car() // インスタンス生成。
実行結果.
initは、インスタンスを生成するときに自動で呼び出される、特殊なメソッドです。
  • 英 initialize: 「初期化する」
  • 「初期化」=初めて変数にデータを入れること。

 インスタンスとは??

クラスは設計図。
クラスは、インスタンス化しないと使うことができません。
instance=「実態」

let car = Car() // クラスの呼び出しを、変数に代入。 「インスタンス化」

インスタンス化とは、

クラスという「設計図」から、インスタンスという実際に使える「モノ」を作ること。


 init内で、プロパティの初期値を設定してみる。

class Car {
    var color : String // 型の指定。 
    var wheel : Int

    init() {
        color = "Red" // init内で、プロパティの初期値を設定。
        wheel = 4     // 同じく。
    }

}

let car = Car() // インスタンス生成。

print(car.color)
print(car.wheel)
実行結果.
Red
4
  • var color = ""でも良いですが、不要なので型指定だけ。
  • (varを削除すると「colorって何?」となるので、エラー。)
  • 型指定では、:を使います。
  • インスタンスは、(car.color)のように「インスタンス名.○○」で値を参照。

また、インスタンス生成時にinitが優先的に呼び出されるので、

var color = "Green"でも、var color = "Blue"でも、
実行結果には反映されない。

 なぜ、initを使うのか。?

クラスの記述内容がより分かりやすくなり、
明確に初期化を行いソースコードの安全性を高める為。

 initメソッドに、『引数』を指定してみる。

  • self.
  • = color, = wheel
  • (color: String, wheel: Int)

スクリーンショット 2020-08-12 13.42.32.png

 self

selfとは、「インスタンス自身」を指す言葉。

クラスのインスタンスメソッド内でのselfは、
自分自身(クラスのインスタンス)を示します。

自分のクラス内の何かにアクセスしたい時に使います。

  • 「インスタンス変数」 それぞれのインスタンスに属する変数。
  • 「インスタンス メソッド」 基本的に、"メソッド"と言うと、これを指します。

Swiftのメソッドの種類

 (color: String, wheel: Int)について。

 クラスの引数に、値だけ指定するとエラー?

// インスタンス生成のとき。

let car = Car("Red", 4) // <--- 引数に、値だけ指定するとエラー。
error.
Missing argument labels 'color:wheel:' in call

パラメーター(ラベル)、必須です。

パラメータとラベルは、同名でもOKだけど、違うモノらしい。
[Swift] 関数ラベルの使い方を学ぶ

 順番が違ってもエラー?

// インスタンス生成のとき。

let car = Car( wheel: 4, color: "Red")  // <--- 順番が違ってもエラー
error.
Argument 'color' must precede argument 'wheel'

おしまい。

 おまけ

タイヤ8個の緑車を追加。

class Car {
    var color : String
    var wheel : Int

    init(color: String, wheel: Int) {
        self.color = color
        self.wheel = wheel
    }

}

let car = Car(color: "Red", wheel: 4) // インスタンス生成。
let secondCar = Car(color: "Green", wheel: 8) // インスタンス生成。

print(car.color, car.wheel) // 「,」で区切れます。
print(secondCar.color, secondCar.wheel)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

How to build MediaPipe hand tracking (iOS) as an Xcode project

Agenda

  1. Preparation (MediaPipe)
  2. Build from terminal
  3. Preparation (Tulsi)
  4. Build as an Xcode project

Demo

DEV

  • MacBook Pro: Catalina 10.15.4
    • Xcode: 11.6
  • iPhone SE (2nd generation): iOS 13.5.1

Preparation (MediaPipe)

1. Install Homebrew

  • Copy the command at https://brew.sh
  • Paste to your terminal and execute
  • The version of Homebrew I used this time was 2.4.7.
$ brew -v
# Homebrew 2.4.7

2. Install Command Line Tools

$ sudo xcodebuild -license

3. Check the Python version

Python is pre-installed on mac by default. Depending on the Python version, the build may not pass, so it is necessary to check the Python version. I confirmed that the build passed with Python 3.7.5, so I recommend building with this version of Python. At that time, there is a version manager called pyenv that can switch the Python version, so I will explain how to use it.

  • Clone the pyenv repository
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
  • Add to path .zsh_profile

zsh

$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zsh_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zsh_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.zsh_profile
  • Install Python 3.7.5
$ pyenv install 3.7.5
  • Rehash shim
$ pyenv rehash
  • Specifying the Python version

The global command sets the global Python version. This can be overridden with other commands, but is useful for ensuring you use a particular Python version by default. If you wanted to use 3.7.5 by default, then you could run this:

$ pyenv global 3.7.5

The local command is often used to set an application-specific Python version. You could use it to set the version to 3.7.5:

$ pyenv local 3.7.5

4. Install six library

Install six library for absorbing the difference between Python 2 and Python 3.

$ pip install –user future six

5. Clone the MediaPipe repository

$ git clone https://github.com/google/mediapipe.git

6. Install Bazel

  • Insatll Bazel
$ brew install bazel
  • The version of bazel I used this time was 3.3.0.
$ bazel --version
# bazel 3.3.0

7. Install OpenCV and FFmpeg

$ brew install opencv@3

8. Install numpy

$ pip install numpy

9. Check installation with Hello World

  • Execute Hello World desktop example
$ export GLOG_logtostderr=1
$ bazel run --define MEDIAPIPE_DISABLE_GPU=1 \
    mediapipe/examples/desktop/hello_world:hello_world

# After building bazel (it takes a few minutes), it is OK if "Hello World!" is displayed 10 times as shown below.
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!

Build from terminal

1. Prepare Provisioning Profile

In order to execute the iOS app on your device, a file for identifying the iOS device or app called the Provisioning Profile is required. Apple Developer Program subscribers can create and download from https://developer.apple.com/jp. Rename the downloaded file to provisioning_profile.mobileprovision and place it in mediapipe/mediapipe/.

2. Change Bundle Identifier

Next, fix BUILD at mediapipe/mediapipe/examples/ios/handtrackinggpu/. Change bundle_id to the same one which you set at Provisioning Profile.

mediapipe/examples/ios/handtrackinggpu/BUILD:36
bundle_id = BUNDLE_ID_PREFIX + ".HandTrackingGpu",
         ↓
bundle_id = "(Bundle Identifier)",

3. Build

Move mediapipe/ and execute the following command.

$ bazel build -c opt –config=ios_arm64 mediapipe/example/ios/handtrackinggpu:HandTrackingGpuApp

An IPA file is generated at the following directory.

bazel-bin/mediapipe/examples/ios/handtrackinggpu/

Preparation (Tulsi)

1. Clone the Tulsi repository

$ git clone https://github.com/bazelbuild/tulsi.git

2. Apply the patch

$ cd tulsi
$ git fetch origin pull/99/head:xcodefix
$ git checkout xcodefix

3. Execute the build script

sh build_and_run.sh

Occurred an error!!

ERROR: /Users/miwa/tulsi/BUILD:62:18: Linking of rule '//:tulsi.__internal__.apple_binary' failed (Exit 1) wrapped_clang failed: error executing command external/local_config_cc/wrapped_clang -Xlinker -objc_abi_version -Xlinker 2 -fobjc-link-runtime -ObjC -arch x86_64 -filelist ... (remaining 26 argument(s) skipped)

After checking this, I could execute the build script!

tulsi/WORKSPACE:6
tag = "0.17.2",
         ↓
tag = "0.18.0",

4. Open MediaPipe.tulsiproj and generate the Xcode project

  • Launch Tulsi.app and open Mediapipe.tulsiproj at mediapipe/mediapipe/.

Tulsi_Start.png

  • Push Generate button in Configs tab

Tulsi_Generate.png

Build as an Xcode project

Now connect your iPhone to your mac. Then open the generated Xcode project and start the build. After a while, the build will be completed and it will be installed on your iPhone.

Xcode_handtrackinggpu.png

Set to use the rear camera.

mediapipe/examples/ios/handtrackinggpu/ViewController.mm:107
_cameraSource.cameraPosition = AVCaptureDevicePositionFront;
         ↓
_cameraSource.cameraPosition = AVCaptureDevicePositionBack;

Displaying reversed image, so I changed the value of Mirrored.

mediapipe/examples/ios/handtrackinggpu/ViewController.mm:111
_cameraSource.videoMirrored = YES;
         ↓
_cameraSource.videoMirrored = NO;

Summary

I succeed in implementing high-precision real-time hand tracking on iOS.

In the future, I will develop learning iOS applications for the visually impaired with this technology.

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

[iOS] FirebaseDistributionへのテスター登録手順(developer証明書の場合)

iOSのbetaアプリ配布にFirebaseDistributionを利用している場合、且つ証明書がEnterprise版ではなくDeveloper版の場合、追加手順を共有するのが厄介だと思いましたので、ざっとまとめました。

追加したいメンバーなどにこのドキュメントを共有することで楽ができればとw

ちなみに、Firebase側管理画面の更新により手順が変わることがあると思います。
本ドキュメントは 2020/06月時点 のものですので、あしからず。
 

※下記ブログの転載です
https://rc-code.info/others/post-339/

管理側手順 (2020/06現在)

1.FirebaseDistribution のアプリ管理画面から招待リンクを取得します。
サンプル画像

2.テスターにテスター側手順の 22 まで進めていただきます。

3.管理者には下記のようなメールが届きますので、Appleの証明書にDevice登録を行います。
サンプル画像
DeveloperサイトでUDIDを登録してください
https://developer.apple.com/account/resources/devices/list
 

4.更新された証明書でアプリをビルドし、ipaを FirebaseDistribution にアップしてください。

テスター側手順 (2020/06現在)

1.招待リンクを頂いてください

2.テストしたい端末からリンクをタップしてください
下記画面が開きます
サンプル画像

3.Emailを入力して SignUp してください
サンプル画像

4.SignUpが完了したらメールが届くので開いてください
サンプル画像

5.メール下部の GetSetup ボタンを押してください
サンプル画像

6.下記左の画面が開くので、ボタンを押して右側の画面に進み、
チェックボックスを埋めた後、Accept Invitation をタップしてください。
サンプル画像サンプル画像

7.下記画面になりますので、再度チェックを埋め Start testing on this device をタップしてください。
サンプル画像

8.下記画面になりますので、Install をタップしてください。
サンプル画像

9.すると下記画面になりますので、Download profile ボタンをタップしてください
サンプル画像

10.下記のようなアラートが表示されますので、許可 ボタンを押してください。
(これはテストアプリを利用するために端末に悪意のないファイルを入れますよ、というアラートですので、ご安心ください?)
サンプル画像

11.ダウンロードが終わると下記アラートが出ますので、画面を閉じて 設定アプリ を開いてください。
サンプル画像

12.設定画面の 一般 をタップしてください
サンプル画像

13.続いて プロファイルとデバイス登録 をタップ
サンプル画像

14.Firebase App Distribution をタップ
サンプル画像

15.下記のような画面になるので、インストールをタップ
サンプル画像

16.パスコードを聞かれるので、端末を開く際のパスワードを入力してください。
サンプル画像

17.選択が出るのでインストールをタップ
サンプル画像

18.インストールが完了するので、設定アプリを閉じてください。
サンプル画像

19.App Distribution というアプリがインストールされているので、タップしてください。
サンプル画像

20.アプリを開くと再度 SignUp を求められるので、SignUpしてください。
サンプル画像

21.インストールできるテストアプリが表示されるので、タップしてみてください。
(サンプルは2つですが、おそらく1つ表示されていると思います)
サンプル画像

22.アプリを洗濯すると Waiting for developer と表示されていると思いますので、この状態で次のアプリ配信をお待ちください!
開発者があなたの端末をテスト端末として許可しますので次回配布のアプリからインストールが可能になります。
サンプル画像

23.開発者が端末の許可作業を終えたのち、再度アプリ配布が行われると、下記画面のように App Distribution のアプリでダウンロードが可能になります!
ダウンロードボタンをタップして、アプリのインストールを待ちましょう!
サンプル画像

24.以上でテストアプリの導入作業はおしまいです!
以後、テストアプリがアップデートされると同様に App Distribution のアプリからダウンロードできます。
ご協力ありがとうございます?‍♂️
お疲れ様でしたお疲れ様でした!

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

UIBarButtonItemのSystemItemで使えるUIImageを取得する

経緯

アプリからTwitterやメモアプリ等の外部アプリに画像など、情報を共有したい時のボタンにアクションボタンを使用したい場合がある
↓こういうやつ
IMG_7856.PNG
こういう場合に限らず、画像の横にある本の画像、またはゴミ箱画像とかも使いたい。
UIBarButtonItemだと使えるけど、それからUIImageを引き出す事は基本的にできない…
それに画像をネットから同じような画像を探すにしても、商用利用などを確認するのも面倒すぎる…?‍♂️

という事で、UIBarButtonItemのSystemItemからimageだけを使用する方法を共有します?‍♂️

実装

検証環境は下記の通りです
OS:10.15.6
Xcode:11.6
Swift:5.0

早速ですが、実装したコードの紹介をします

UIBarButtonItem.SystemItem+extention.swift
extension UIBarButtonItem.SystemItem {
    func image() -> UIImage? {
        let tempItem = UIBarButtonItem(barButtonSystemItem: self,
                                       target: nil,
                                       action: nil)

        let bar = UIToolbar()
        bar.setItems([tempItem],
                     animated: false)
        bar.snapshotView(afterScreenUpdates: true)

        // imageを取得する
        let itemView = tempItem.value(forKey: "view") as! UIView
        for view in itemView.subviews {
            if let button = view as? UIButton,
                let image = button.imageView?.image {
                return image.withRenderingMode(.alwaysTemplate)
            }
        }
        return nil
    }
}

使う時はこのように↓

UIBarButtonItem.SystemItem.action.image()

実行結果↓
スクリーンショット 2020-08-12 11.24.04.png
問題なく表示できました〜!☺️

候補: SF Symbol・systemNameを検討する

そもそも論ですが、iOS13からはSF SymbolUIImageViewでsystemNameが使用できるようになっています。
使用する方法は以下の通りです。

// UIKit
UIImageView(systemName: "xxx")

// SwiftUI
Image(systemname: "xxx")

この"xxx"に入る文字列は、下記URLよりDLできるSF Symbolアプリから確認できます。
https://developer.apple.com/design/resources/
FigamaやSketch等のデザインツールを利用すると細かくサイズ等を調整したりできるみたいなので
対応できる方はこっちの方がいいと思います。
※記事投稿時点ではBeta版です

最後に

Twitterのアカウントがありますのでフォローしてくれると嬉しいです!!
@swift_nita
なお今回のサンプルはGithubに上げていますのでご参考までに!
https://github.com/ni-ta/ButtonItemSystemItem

参考

Use UIBarButtonItem icon in UIButton
SF Symbolsの使い方とカスタマイズの仕方

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

StoryBoardでAn internal error occurred. Editing functionality may be limited.が発生した時の対処

事象

StoryBoardでAutoLayoutを作成中、
「An internal error occurred. Editing functionality may be limited.」という警告が発生し、青い線のみになった
そのままビルドするとレイアウトが崩れることも発生(必ずではない)

いろいろなサイトの情報から解消を試みるも尽く失敗。。。
ようやく解消できたので祝砲代わりの投稿

動作環境

Xcode 11.5
Xcode 11.3.1

やったこと

自分の環境では解消しなかったが、解消できることもあるらしい

プロジェクトのクリーン → 解消せず

  1. Product > [option] + Clean Build Folder
  2. Xcodeを再起動 → 解消せず

制約のエラーを解消 → 解消せず(Xcode 11.3.1で発生した時はこれで解消)

  1. Product > [option] + Clean Build Folder
  2. Xcodeを閉じる
  3. エラーが発生する直前のリビジョンに戻す
  4. Xcodeを起動 → 解消せず

DerivedDataを削除 → 解消せず

  1. Product > [option] + Clean Build Folder
  2. Xcodeを閉じる
  3. DerivedDataを削除 rm -rf ~/Library/Developer/Xcode/DerivedData/*
  4. Macの再起動
  5. Xcodeを起動 → 解消せず

Xcodeのキャッシュを削除 → 解消せず

  1. Product > [option] + Clean Build Folder
  2. Xcodeを閉じる
  3. DerivedDataを削除 rm -rf ~/Library/Developer/Xcode/DerivedData/*
  4. Xcodeのキャッシュを削除 rm -rf ~/Library/Caches/com.apple.dt.Xcode/
  5. Macの再起動
  6. Xcodeを起動 → 解消せず

Xcodeを再インストール → 解消せず

  1. Product > [option] + Clean Build Folder
  2. Xcodeを閉じる
  3. DerivedDataを削除 rm -rf ~/Library/Developer/Xcode/DerivedData/*
  4. Xcodeのキャッシュを削除 rm -rf ~/Library/Caches/com.apple.dt.Xcode/
  5. アプリケーションフォルダのXcodeを全てゴミ箱へ!
  6. Xcodeを完全アンインストール
  7. Xcodeを新しくインストール
  8. Xcodeを新規プロジェクトで起動 → 解消せず

これで解決!!

/private/tmp に権限を付与 → 解消した!

※Xcodeを再インストールの後で実施したが、おそらく再インストールは必要なかった
1. /private/tmpディレクトリが存在するか確認
ls -l /private/tmp
2. /private/tmpディレクトリが存在しなかった場合
$ sudo mkdir /private/tmp
3. 所有者を変更
sudo chown -R $(whoami) /private/tmp
4. Xcodeを起動 → 解消!

/private/tmp ディレクトリは存在したが、所有者を変更したら無事エラーが解消された
長い道のりだった。。。

参考

https://www.seishin.me/xcode-the-folder-disabled-plist-doesnt-exist/
https://qiita.com/UJIPOID/items/015805c89bddca540129
https://qiita.com/shtnkgm/items/c96a58579ec406194fa8
https://qiita.com/stoneBK7/items/146ee235a46abc9178da
https://qiita.com/y-aimi/items/209e7acce54ee1d38144
https://stackoverflow.com/questions/33456411/ios-project-showing-error-an-internal-error-occurred-editing-functionality-may

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

モバイルブラウザのキーボードの種類は制御できない

スマートフォンなどのモバイル端末のブラウザでは、input要素をフォーカスするとキーボードが出現する。
このキーボードは、「英語」「日本語 - かな」などの種類があり、日本語利用者なら切り替えながら使っているだろう。

で、input要素をフォーカスした時に出てくるこのキーボードの種類を制御したい、という要望はままあるだろう。
たとえば、英数字のみからなる何らかのシリアルコードのようなものの入力欄は、日本語ではなく英語キーボードを表示させたいだろう。
結論からいうと、キーボードの種類は制御できない。
もう少し正確に言うと、日本語ではなく英語キーボードを常に表示させるようにすることはできない。

type属性で制御できるのではないか

巷の技術系ブログなどではよく書かれている。

<!-- email入力用だから、英語キーボードであるべき? -->
<input type="email">

<!-- URL入力用だから、英語キーボードであるべき? -->
<input type="url">

たしかに、多くのモバイルブラウザは、type属性をemailurlに指定することによって、それに適したキーボードを表示する機能をサポートしてはいる。
iOS Safariの場合、type="email"のときは、スペーキーの隣に@.など、メールアドレスによく使う文字キーが配置されている。(前掲のスクリーンショット画像がまさにそれだ)

一見すると、求めていた挙動が実現されているように思うが、ちょっと待って欲しい。

日本語キーボードに切り替えしてみる

type="email"をフォーカスしてキーボードを表示したら、わざと日本語に切り替えた上でinput要素のフォーカスをはずしてみよう。そして、再び同要素をフォーカスしてキーボードを表示させてみる。
このとき日本語キーボードが表示されるだろう。これはあなたの求めている挙動だろうか。

次にページをリロードしてみて、再び同要素をフォーカスする。やはり日本語キーボードが出るだろう。

同じページ内に、別要素のtype="email"があるならば、そちらにフォーカスを当ててみよう。やはり日本語キーボードが出るはずだ。これはあなたの求めている挙動だろうか。

なんなら、他のページのtype="email"要素でキーボードを日本語にした後、当該サイトのtype="email"にフォーカスしてみよう。iOS Safariでは日本語キーボードが出るだろう。(Androidでは英語キーボードが出る)
何度も言うが、これはあなたが求めている挙動だろうか。1

あなたが求めているのは、当該要素をフォーカスしたときには常に英語キーボードが表示されることではないのか。2その要素に入力されるべきは英数字だけなのだから。

メールアドレスはマルチバイト文字が含まれ得る

なぜこんなことになるのか。
メールアドレス入力用のtype属性値ならば、英語キーボードだけ表示されればいいのではないのか。
現に、input[type="tel"]は数字キーボードのみ、input[type="password"]は英語キーボードのみであり、キーボードの種類を変更することはできない。
理由は、メールアドレスの入力には英語キーボードだけでは不十分だからだ。

RFCの定義によれば、メールアドレスに使われる文字には制限があり、マルチバイト文字は含まれないらしいのだが、Gmailではマルチバイト文字を含むメールアドレスへの受送信が可能だ。
本来の規格としてNGだったとしても、Gmailがサポートしている以上、マルチバイト文字を含むメールアドレスは存在する。存在する以上は、入力できなければならない。入力するためには、日本語キーボードに切り替え可能でなければならないのだ。
そして、ひとたびユーザがtype="email"要素で日本語キーボードに切り替えたならば、ブラウザは「ユーザーは日本語でemailを入力しようとしている」と解釈し、それ以後type="email"フォーカス時には日本語キーボードを表示するようになると考えられる。ユーザビリティとしては筋が通っている。

これは、input[type="url"]についても同様だ。
日本語ドメインは存在している。そうでなくても、ディレクトリ名やパラメータにマルチバイト文字が含まれることは普通にある。3
であるならば、日本語キーボードが表示されうる。

inputmodeという属性もあるが

inputmode="email" inputmode="url"とすることで、入力キーボードを指定できるという属性値だが、これとてもtype属性と同じ挙動だ。

入力値が英数字でなければならないtype属性は存在しない

ただし、パスワードtype="password"を除く。
type="password"は当然ながら伏字になるので、通常はパスワード以外には使えない。
というわけで、常に英語キーボードを表示させるようにする手段は、我々には与えられていない。

我々はどうすべきか

諦めよ。そして、ユーザを信頼せよ。
たとえ日本語キーボードが開いたとしても、英数字で書くべきことを理解すれば、ユーザーは自らの手で英語キーボードに切り替えて入力するであろう。

バリデーションと入力補助

通常は誤った値が入力された場合には、それと分かるエラー文言を表示するものだろう。ユーザに気づかせて軌道修正させればよい。
全角文字が入力された際には、JavaScriptで半角に自動変換する機能くらいは実装しておくと親切かもしれない。

なぜこの記事を書いたか

この入力欄に入力する値は英数字なので、英語キーボードが出るようにします。
その方がユーザに負担がかからないので。

あなたはこのように仕様を決めた手前、フォーカス時に日本語キーボードが出てしまう挙動を何とかしたいと思うかもしれない。
だが、立ち止まってよく考えてみて欲しい。これはそれほど問題なことなのか。
ユーザは自分でキーボードを切り替えることができ、適切に入力するのに何の支障もないではないか。

ユーザは自分がどうすべきか(英数字で入力)をきちんと理解できるようになっていれば、「英語キーボードを出す」などという些末な事象にとらわれる必要はどこにもない。

スマホで「英語キーボードを出す」ということについて、できる、できる、と書いてある記事ばかりなので、当然実装しなければならなくなってしまう。確かに概ね期待通りにできるのだが、重箱の角をつつくようなデバッグ4には耐えられず、不具合として報告されてしまう。報告された以上は、何とか対応しようとするものの、完璧に制御することはできないのだと理解した。
このことについて触れている記事がなく、とても困ったので書いた。5
実装を担当しているあなたが、クライアントやディレクターに、この記事を見せて、

ここに書いてある通り、表示するキーボードを完璧に制御することはできません。
type属性をemailにしておけば、初回フォーカス時には(100%ではないですが)概ね英語キーボードで開くようですので、このくらいの対応で手を打ちましょう。

と適当な妥結点を見出してくれることを期待する。


  1. type="url"でも同様。 

  2. もちろん、ユーザ手動で英語キーボードに戻しておけば、再度フォーカス時には英語キーボードが出る。 

  3. Wikipediaを見よ。 

  4. これがデバッガーの仕事なので当然である。 

  5. 問題の性質上当然なのだが、検索しても英語の記事は全く出てこない。 

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

【入門】iOS アプリ開発 #3【Sound を再生する】

Sound

今回はゲーム中のサウンドを流す処理を作成したい。SpriteKit は簡単なサウンド制御も提供している。「パックマン」程度であれば、この範囲で実現することができそうだ。

サウンドは効果音(Sound Effect) と BGM(BackGround Music) がある。この2種類を実現するクラスを作成する。

Sound Manager class の作成

効果音を再生する API は、

playSE(.EatDot)

とする。

BGMを再生する API は、

playBGM(.BgmPower)

とする。

このような API を持つ、次の CgSoundManager クラスを作成する。

CgSoundManager class

/// Sound management class plays sound with SpriteKit.
class CgSoundManager {

    // Kind of sound items to play back.
    enum EnKindOfSound: Int {
        case EatDot = 0
        case EatFruit
        // ・・・略・・・
        case Intermission
    }

    // List of sound files to load.
    private let table_urls: [[(resourceName: String, typeName: String, interval: Int)]] = [
        [ ("16_pacman_eatdot_256ms", "wav", 256) ],
        [ ("16_pacman_eatfruit_438ms", "wav", 438) ],
        // ・・・略・・・
        [ ("16_pacman_intermission_5200ms", "wav", 5200) ]
    ]
    private var view: SKScene?
    private var actions: [SKAction] = []
    private var table_playingTime: [Int] = []

    // Adjustment time for processing to play sound.
    private let triggerThresholdTime: Int = 16 //ms

    /// Create and initialize a sound manager object.
    /// - Parameters:
    ///   - view: SKScene object that organizes all of the active SpriteKit content.
    init(view: SKScene) {
        self.view = view
        table_playingTime = Array<Int>(repeating: 0, count: table_urls.count)

        for t in table_urls {
            appendSoundResource(resourceName: t[0].resourceName, typeName: t[0].typeName)
        }
        // ・・・略・・・
    }

    /// Append sound resources to SpriteKit.
    /// - Parameters:
    ///   - resourceName: File name for sound resource.
    ///   - typeName: Type name for sound resource.
    private func appendSoundResource(resourceName: String, typeName: String) {
        let fileName = resourceName+"."+typeName
        let sound: SKAction = SKAction.playSoundFileNamed(fileName, waitForCompletion: false)
        actions.append(sound)
    }

    /// Play back a specified sound.
    /// If the specified item is playing back, it will not be played back.
    /// - Parameter number: Kind of sound items to play back.
    func playSE(_ number: EnKindOfSound) {
        guard soundEnabled && number.rawValue < actions.count else { return }

        let _number = number.rawValue
        if table_playingTime[_number] <= triggerThresholdTime {
            let table = table_urls[_number]
            table_playingTime[_number] = table[0].interval
            view?.run(actions[_number])
        }
    }

enum で定義された値と、再生するサウンド・ファイルの table_urls テーブルを対応させておく。

クラス初期化時に、これらのファイルを SKActionのオブジェクトとして生成しておく。

またサウンド・ファイルの再生時間を値として管理しておく。これは同じ効果音が複数重ならないようにするためで、ある効果音が再生中に同じものを再生する場合は再生しないようにする。

また BGM を再生するときには、指定したサウンド・ファイルの再生が終了したことを知り、同じものを繰り返し再生するために使用する。自動で開始するためのトリガーは、updateメソッド内で行う。

サウンド・ファイル

今回のパックマンに使うサウンド・ファイルは 15個で、フォーマットは WAV形式、16bit、モノラル、サンプリング・レート 22050Hz で作成した。

8bit だと、少し音がこもる感じになるので、16bit にして処理の重さを気にしてサンプリング・レートを 22050Hzへ落とした。

再生でプチプチ・ノイズが出ないように、先頭は FadeIn、終端は FadeOut処理をしておく。

サウンド・ファイルの再生時間は編集ツールで確認しておく。

[Sound Files]
* 16_pacman_eatdot_256ms.wav 11,372bytes
* 16_pacman_eatfruit_438ms.wav 19,360bytes
* 16_pacman_eatghost_544ms.wav 24,040bytes
* 16_pacman_miss_1536ms.wav 67,788bytes
* 16_pacman_extrapac_1952ms.wav 86,166bytes
* 16_credit_224ms.wav 9,634bytes
* 16_BGM_normal_400ms.wav 17,852bytes
* 16_BGM_power_400ms.wav 17,750bytes
* 16_BGM_return_528ms.wav 23,366bytes
* 16_BGM_spurt1_352ms.wav 15,592bytes
* 16_BGM_spurt2_320ms.wav 14,212bytes
* 16_BGM_spurt3_592ms.wav 26,272bytes
* 16_BGM_spurt4_512ms.wav 23,018bytes
* 16_pacman_beginning_4224ms.wav 187,054bytes
* 16_pacman_intermission_5200ms.wav 229,388bytes

テスト・プログラム

GitHub に公開しているテスト・プログラムを実行すると、以下のような画面が表示され、BGMが5秒毎に切り替わる。スクリーンをタッチすると効果音が再生される。

Sound Manager クラスは、SoundManager.swift ファイルにコーディングしている。コメント入れて 200行未満となった。

class GameScene: SKScene {

    private var sound: CgSoundManager!

    override func didMove(to view: SKView) {

        // Create and reset sound object.
        sound = CgSoundManager(view: self)
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        sound.playSE(.EatDot)
    }

    private let bgm: [CgSoundManager.EnKindOfSound] = [.BgmNormal, .BgmSpurt1, .BgmSpurt2, .BgmPower, .BgmReturn]
    private var bgmIndex: Int = 0
    private var bgmTime: Int = 0

    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered

        // Play back BGM.
        if bgmTime == 0 {
            bgmTime = 16*60*5  // 5s
            sound.playBGM(bgm[bgmIndex])
            bgmIndex += 1
            if bgmIndex >= bgm.count {
                bgmIndex = 0
            }
        } else {
            bgmTime -= 16
        }

        // Update sound manager.
        sound.update(interval: 16 /* ms */)
    }
}

CgSoundManagerクラスは、SKViewオブジェクトをパラメータとして、オブジェクトを生成する。

SKScene からオーバーライドした touchesEndedイベントのメソッドで、SEを再生する。

また、同様に updateイベントのメソッドで、5秒毎にBGMを切り替えて再生する。

参考

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