20191201のiOSに関する記事は22件です。

Xcode11.0の画面収録につまずいた

はじめに

Xcode の シミュレータではターミナルで下記のコマンドを実行すると画面収録ができます。

xcrun simctl io booted recordVideo test.mov

しかし、Xcode11.0 で実行すると下記のようなエラーが表示されます:scream:

An error was encountered processing the command (domain=NSPOSIXErrorDomain, code=22):
Selected interface does not suport video recording.
Invalid argument

simctl コマンドについては下記の記事に詳しく書いてありました。

対応

Xcodeのパスを確認する

複数バージョンの Xcode をインストールしていたのでとりあえず指定のパスが正しいか確認します。

ターミナルで下記コマンドを実行します。

xcode-select -p

他の Xcode が指定されていた場合は下記コマンドでパスを修正します。

sudo xcode-select -s <Xcodeのパス>

もう一回実行!!

xcrun simctl io booted recordVideo test.mov

再び同じエラー:scream:

UUIDを指定してみる

画面収録のコマンドは下記のような構成になっています。

xcrun simctl io <対象のシミュレータのUUID> recordVideo <保存先パス> 

booted にしていたところを UUID を指定してみます。

ターミナルで下記コマンドを実行し、起動中のシミュレータの UUID を取得します。

xcrun simctl list | egrep '(Booted)'

結果は下記のようなもの

iPhone 11 Pro Max (AAAAAAAA-XXXX-XXXX-XXXX-XXXXXXXXXXXX) (Booted) 
Phone: iPhone 11 Pro Max (AAAAAAAA-XXXX-XXXX-XXXX-XXXXXXXXXXXX) (Booted)

UUID を指定して実行!!

xcrun simctl io AAAAAAAA-XXXX-XXXX-XXXX-XXXXXXXXXXXX recordVideo test.mov

はい、同じエラー:scream:

リリースノートを見てみる

Xcode のリリースノートを確認してみます。

発見!!!

Xcode11リリースノート

ここに

Video recording of the iOS 13, tvOS 13, and watchOS 6 simulator through xcrun simctl io recordVideo returns an error instead of recording video. (50625716)

とあります。

iOS13 では無理な模様!!iOS12 とかのシミュレータならいけるのかもしれません。(通信環境が劣悪なので他のシミュレータをダウンロードできず確認できませんでした...)

結論

Xcode11.0 では iOS13 のシミュレータで画面収録はできない模様。

Xcode11.2.1 ではできたのでバージョンを上げるか Quick TimePlayer を使いましょう!!

さいごに

リリースノートって大事ですね:laughing:

今まであんま見たことなかったんですがこれからはちょこちょこ確認するようにします...

とりあえず今回のように画面収録できなかった時は Quick TimePlayer と PicGIF Lite で GIF を作成してるのですが皆さん Qiita に GIF あげる時とかなに使ってるんですかね?

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

Android/iOSアプリのLightBlueでtoioコア キューブを動かしてみる

Android/iOSでBLEを直に使ってtoioコア キューブを動かしてみます

 toioコア キューブはBLE通信で制御できるとてもかわいい二輪ロボットです。
https://toio.github.io/toio-spec/
 本来、toioコア キューブはtoioコンソールとセットで動かすものですが、toioコア キューブ単体で、スマートフォンや、ノートPC、はたまたRaspberry PiのようなシングルボードコンピュータなどのBLE通信で動かすこともできます。
 この記事ではスマートフォンのBLE通信を使ってtoioコア キューブを動かしてみます。

スマートフォンでtoioコア キューブを動かしたい

 一番簡単な方法はweb bluetoothを使ってchromeブラウザからURLを開きtoio.jsを動かす方法です。
toio.jsをブラウザで動かしてみた
 動かすだけならこれでOKですが、もうちょっと生っぽくというかBLE通信のプロトコルを直に使って動かしてみましょう。

BLEの簡単な解説

  • BLE機器ははセントラル、ペリフェラルの2種類あります。
  • セントラルが、PCやスマートフォン、ペリフェラルが今回はtoioコア キューブにあたります。
  • セントラルはペリフェラルの出すアドバタイジングパケットをみて、どういう素性の機器かを判別し、ペリフェラルに接続します。
  • セントラルは接続したペリフェラルから、値を読み/書き/通知することのできるデータフィールドの情報を得ることができます。このデータフィールドのことをキャラクタリスティックといいます。
  • キャラクタリスティックは以下の3つの性質をもちます。
  • 読み(read)は値を読み込むことができるフィールドです。
    • 例えばバッテリー残量、ボタンを押したかどうかなどです。
  • 書き(write)は値を書き込むことでペリフェラルの動作に変化を与えます。
    • 例えばLEDの色の変更、モーターの回転速度の変更などです。
  • 通知(notify)は読み(read)に似ていますが、データフィールドの値に変化があったときにペリフェラルに値を通知します。
    • 例えばボタンを押したかどうか、モーションセンサによる衝突検知、読み取りセンサによるtoioコア キューブの置いてあるマットのIDやシールのIDの値が変化したときにどんどん通知されてきます。
  • toioコア キューブの場合、読み(read)できるキャラクタリスティックは通知(notify)にも対応しています。
    • モーター、設定のようにwrite、read、notifyの全部に対応しているキャラクタリスティックもあります。
  • BLEはほかにもいろいろありますが、とりあえずこれだけ知ってれば遊べます。
  • Android/iOSアプリのLightBlueを使うと簡単にキャラクタリスティックの読み(read)、書き(write)、通知(notify)を試すことができます。
  • Androidアプリ LightBlue
  • iOSアプリ LightBlue

準備するもの

  • スマートフォン(AndroidかiOSのもの)
  • LightBlueアプリをインストールする 前章のURLからそれぞれのOS用のアプリをインストールしてください。
  • toioコア キューブ 1個
  • 必要に応じて「トイオ・コレクション」のマットやステッカー

さっそくやってみよう

以下ではAndroidアプリのLightBlueの画面を使って説明します。iOSアプリ版もほぼ変わらない操作です。

LightBlueアプリを起動します。

  • 起動時にbluetoothへのアクセス許可、あるいは位置情報へのアクセス許可の確認画面が出る場合は許可します。(許可しないとBLE通信ができないのでtoioコア キューブと通信できません)

[Screenshot_20191201-224104.png]
[Screenshot_20191201-224113.png]

  • 以下の画面が出たら、「toio Core Cube」をタップして選びます。 [Screenshot_20191202-080759.png]
  • 「toio Core Cube」の下に出ている16進数はBLE機器ごとに割り当てられているアドレスです。このスクリーンショットでは下半分(3バイト分)を消していますが、実際には6バイト分あります。この6バイトで個体判別することができます。

toioコア キューブと繋がった状態

[Screenshot_20191202-080814.png]

  • toioコア キューブとつながるとこの画面になります。
  • この画面を下の方にスクロールして、以下の赤枠のところを表示します。 Screenshot_20191202-080907.png]
  • これがtoioコア キューブのキャラクタリスティックです。ここをタップするとキャラクタリスティックの選択画面が出ます。
    Screenshot_20191202-080927.png]

  • この中から読み(read)/書き(write)/通知を受ける(notify)したいキャラクタリスティックを選びます。

  • さて、ここでtoio コア キューブ 技術仕様のページの「通信概要」のところを一度みてみましょう。

バッテリー残量を読む

  • 前章のキャラクタリスティック選択画面から「Battery Information」をえらびます。

[Screenshot_20191202-080939.png]

  • バッテリーのキャラクタリスティックについての説明が出ます。赤枠で囲ったところにReadableとあり、チェックマークがありますので、「読み」ができます。その下にはWritableとありますが×になっているので「書き」はできないことを示しています。さらに下のSupports notifications/indicationsにはチェックマークがついているので「読み」に加えて「通知」もできることがわかります。
  • この画面を下のほうにスクロールしてREAD/INDICATED VALUEのところまできたら「READ AGAIN」ボタンをタップします。

[Screenshot_20191202-081011.png]

  • バッテリー残量の値が読み出されて表示されます。16進数で「64」なので100ですね。(「バッテリー」の仕様も確認してみてください)

[Screenshot_20191202-081024.png]

LED(ランプ)を灯もす

  • 今度は「書き」のほうをやってみます。戻るボタンで一旦、キャラクタリスティック選択画面まで戻って今度は「Light Control」を選びます。

[Screenshot_20191202-081050.png]

  • 今度はWritableのみ有効なので「書き」しかできません。
  • この画面を下の方にスクロールしてWRITTEN VALUEのところまできたら、toioコアキューブ仕様書の「ランプ」のところを参照し、「書き込み操作」の「例」のデータ「03100101FF0000」を書き込んでみます。
    • 「例」では0.16秒間LED(ランプ)を赤く点灯するデータが示されています。
  • 16進数で書き込んで「WRITE」ボタンをタップすると、toioコアキューブの底面のLED(ランプ)が0.16秒間赤く光ります。0.16秒は一瞬ですので気をつけてよく見ていてください。 [Screenshot_20191202-081155.png]

ボタンの状態を読む、変化した通知をうける

  • 続いて今度はボタンの情報を読んだり、通知を受け取れるようにしてみます。
  • 戻るボタンで一旦、キャラクタリスティック選択画面まで戻って今度は「Button Information」を選びます。

[Screenshot_20191202-081257.png]

  • この画面を下の方にスクロールして、[READ AGAIN]のボタンをタップすると、タップした瞬間のtoio コア キューブ底面のボタンの状態(押している、押していない)を読み取ることができます。

[Screenshot_20191202-081313.png

  • このスクリーンショットでの値は01 00、toio コア キューブのボタンは押されていません。
  • toioコアキューブのボタンを押した状態で、もう一度[READ AGAIN]のボタンをタップすると、今度は01 80です。値の意味はtoio コア キューブ技術仕様書の「ボタン」のところで調べてみましょう。

[Screenshot_20191202-081327.png]

  • [READ AGAIN]のボタンのとなりの[SUBSCRIBE]ボタンをタップすると、通知(notify)を受けるようになります。値が変化したときに通知されます。
  • この状態でtoio コア キューブのボタンを押したり離したりすると、状態が変化するタイミングで以下のように値が通知されてきます。

Screenshot_20191202-081414.png]

  • 通知をやめるときは[UNSUBSCRIBE]ボタンをタップします。

Screenshot_20191202-081355.png]

マットの上の座標などを通知させる

  • ここまでくればもうわかりますね。戻るボタンで一旦、キャラクタリスティック選択画面まで戻って今度は「ID Information」を選びます。
  • そして画面の下の方までスクロールしてから[SUBSCRIBE]ボタンをタップします。

Screenshot_20191202-081536.png]

  • toio コア キューブをマットに置いてみてください。また、位置を変えたり角度を変えたりしてみてください。ものすごい勢いで位置、角度データが通知されてくるのがわかります。
    • 通知されてきた値の意味はtoio コア キューブ技術仕様の「読み取りセンサー」で確認してみてください。

さいごに

 スマートフォン(Androido/iOS)のLightBlueアプリを使って、toio コア キューブと直接BLE通信して動かしてみました。あ、モーターは今回使ってないので「動いて」はいないですね。:sweat:
 まあ、toioコアキューブ技術仕様の「モーター」をみて、いろいろやってみてください。特に2.1.0になってからモーター制御コマンドでできることが増えていますのでなかなかやりがいがあると思います。
 ここまでわかってしまえばBLEでのtoio コア キューブのコントロールはそんなに難しくありません。Windows、Linux、MacOS、その他いろいろな環境で、お好みのプログラミング言語でtoio コア キューブを動かして楽しみましょう。

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

【iOS 13】アプリアイコンをダークモード対応させることは可能なのか検証してみる!!

はじめに

皆さんはダークモード対応されていますか?
だいぶ対応したアプリが増えてきたように感じます。
対応するのは大変(主に工数が)ですがやっぱり対応したら謎の満足感あります。

私は個人開発のアプリでは対応しましたが,
まだ業務で担当している案件では対応できていないです。

Xcode では,画像や色のアセットでライト/ダークモードで
それぞれ使う色,画像を設定できるのですごく楽ですよね。

アプリアイコンはダークモード対応できないのかな??
と思って調べてみました。

ちなみにアプリアイコンは Xcode にダークモードなどの設定はないですね。
01_img_AppIcon_no_settings.png

YUMEMI.swift #5 でトークした内容になります。
https://yumemi.connpass.com/event/153206/

アプリアイコンを変更する(iOS 10.3 ~)

アプリアイコンの変更や名前取得などは iOS 10.3 から使えます。

// hogeという名前のアプリアイコンを設定する
if #available(iOS 10.3, *) {
    UIApplication.shared.setAlternateIconName("hoge") { error in
        if let error = error {
            print(error.localizedDescription)
        }
    }
}

元のアイコンに戻すときは nil をセットすれば良い。

// 元のアイコンに戻す
if #available(iOS 10.3, *) {
    UIApplication.shared.setAlternateIconName(nil) { error in
        if let error = error {
            print(error.localizedDescription)
        }
    }
}

注意点として下記があります。

  • info.plist に利用するアイコン情報を記載する
  • ViewController に記載する
  • メインスレッドで実行する
  • 変更するアプリアイコンは xcassets を利用せず直接導入する

これらのコードとライト/ダークモードが切り替わった際に呼ばれる,
メソッドを override して,Appearance が切り替わった際に
それぞれのモードに対応したアプリアイコンを変更させようとしてみます。

実装

実装環境

  • Xcode 11.2.1
  • macOS Catalina 10.15.1
  • iOS 13 and later

サンプルコードは GitHub に用意しました。
必要に応じて参照ください。
https://github.com/MilanistaDev/AppIconCorrespondingToDarkMode
QRCode_APPICON_Darkmode.png

本実装

ライトモードで設定するアプリアイコン画像をセット

ライトモードで設定するアプリアイコンを xcassetsAppIcon で設定します。
いわゆるいつものアプリアイコンセット作業です。
02_img_AppIcon.png

ダークモードで設定するアプリアイコン画像を準備

ダークモード用アプリアイコンの画像を用意します。
@2x@3x でそれぞれ 120x120180x180 ピクセルです。
名前は AppIcon-Dark としました。
03_img_darkmode_icons.png

用意した画像をプロジェクトに追加

先にあった通り Assets.xcassets で AppIcon を追加しても
ダークモード用の設定ができません。
なので直接プロジェクトに追加します。
04_img_AddedDakModeAppIcon.png

info.plist を編集

追加した画像を利用するために info.plist を編集する必要があります。
下記コードを追加します。

<key>CFBundleIcons</key> 
<dict> 
    <key>CFBundlePrimaryIcon</key> 
    <dict> 
        <key>CFBundleIconFiles</key> 
        <array> 
            <string>AppIcon</string> 
        </array> 
        <key>UIPrerenderedIcon</key> 
        <false/> 
    </dict> 
    <key>CFBundleAlternateIcons</key> 
    <dict> 
        <key>AppIcon-Dark</key> 
        <dict> 
            <key>CFBundleIconFiles</key> 
            <array> 
                <string>AppIcon-Dark</string> 
            </array> 
            <key>UIPrerenderedIcon</key> 
            <false/> 
        </dict> 
    </dict> 
</dict>

見やすいプロパティリストで見ると下記のようになります。
Primary Icon の方がライトモードのアプリアイコン設定で,
黄色の四角の部分が追加したダークモード用アプリアイコン画像のファイル名です。
05_img_infoplist_pl.png

ライト/ダークモードの設定でアプリアイコンを切り替える

ViewControllerでアプリアイコンを変更するコードを書く必要があるので,
例えば ViewController.swift に下記のようなコードを書きます。
このコードで Appearance が変更された際に,
現在のモードを判別してアプリのアイコンを切り替える処理が実行されます。

ViewController.swift
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    if self.traitCollection.userInterfaceStyle == .dark {
        // ダークモード用のアプリを設定する
        UIApplication.shared.setAlternateIconName("AppIcon-Dark") { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    } else {
        // nilをセットしデフォルトのアプリアイコン画像に変更
        UIApplication.shared.setAlternateIconName(nil) { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    }
}

が,しかし・・・
コントロールセンターでライト/ダークモードを切り替えると
エラー出力部分を通過することがわかります。

エラー内容は下記です。

The operation was cancelled.

RPReplay_Final1574672398.gif

いろいろ調べて遅延実行するとうまくいくとのことで
下記コードで包んであげると・・・

DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
}

ブレイクポイント止めるとライト/ダークモード切り替え時に,
ちゃんとアプリアイコン変更ダイアログが表示されます。
しかし,ブレイクポイントを貼らなければ状況は変わりませんでした。

ライト->ダーク ダーク->ライト
RPReplay_Final1574673071.gif RPReplay_Final1574673262.gif

結果

ライトモードとダークモードの変更時にアプリアイコンを変更することはできなさそう。
この変更がユーザのアプリ内でのアクションではなく,
アプリ外の操作(iOSによるもの)のため,おそらくキャンセルされてしまうのかなと考えました。
(ユーザが起こしているアクションには違いないのですが?)

では,ユーザが起こす,アプリ内のトリガーで今のモードを判別して,
アプリアイコン画像をセットする処理を書くとちゃんと設定できるのかな?

ユーザが選択可能にする

というわけで,よくある仮の設定画面を用意し,
アプリアイコン画像を現在の Appearance に合わせる,的な
処理を書いてみます。

仮の設定画面はこんな感じで適当に準備しました。
最初のセクションの2番目のセルをタップして
アプリアイコン画像を変える処理を書いてみます。

ライトモード ダークモード
Settings_LIGHT.PNG Settings_Dark.PNG

UITableView の delegate メソッドのコード。

SettingsViewController.swift
extension SettingsViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.tableView.deselectRow(at: indexPath, animated: true)
        if indexPath.section == 0 && indexPath.row == 1 {
            // セルがタップされた際にアプリアイコン画像を変更
            self.matchAppIcon()
        }
    }
}

現在の Appearance の状態によってライトモード,ダークモード用の
アプリアイコン画像を設定するコードはこんな感じで書いてみました。

SettingsViewController.swift
/// Match the App Icon to the current Appearance
private func matchAppIcon() {
    if self.traitCollection.userInterfaceStyle == .dark {
        // ダークモード用のアプリを設定する
        UIApplication.shared.setAlternateIconName("AppIcon-Dark") { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    } else {
        // nilをセットしデフォルトのアプリアイコン画像に変更
        UIApplication.shared.setAlternateIconName(nil) { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    }
}

セルをタップしてみたところ,ダイアログが表示され,
アプリアイコン画像を変更することができました。

ライトモード ダークモード
Success_Light.PNG Success_Dark.PNG

実際の動きは下記のようになります。
ライトモード,ダークモードに切り替わった際にはアプリアイコンはそのまま,
セルのタップをした際に初めてダイアログが出てアプリアイコンが変更されるという感じです。

ライト=>ダーク ダーク=>ライト
RPReplay_Final1574677144.gif RPReplay_Final1574677169.gif

おわりに

コントロールセンターなどでライトモード,ダークモードを切り替える際に
それぞれのモードに対応したアプリアイコン画像を変更することはできないっぽい。

セルのタップやボタンタップなどユーザが起こすアクションをトリガーとすると,
変更した旨のダイアログが出て,各モードに対応したアプリアイコンに変更することは可能でした。

ライトモード,ダークモード切り替え時にアプリアイコン変更できるぜ,
もっとこうした方がクレバーですよー等ありましたらご教示いただければ幸いです。

ご覧いただきありがとうございました!

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

SwiftUIでViewを画像としてUIActivityを利用してSNSに共有する

はじめに?

SwiftUIで特定のViewをSNSにシェアするtipsを共有します?

使いたかったLPLinkMetadata

iOS13から使えるようになったLPLinkMetadataを利用します。
これを
IMG_0373.JPG
↓のようにすることができます。
IMG_0372.JPG

UIActivityViewControllerをRepresentaぶる

UIActivityViewControllerをSwiftUIで扱えるようにします。

struct ShareSheet: UIViewControllerRepresentable {
    let photo: UIImage

    func makeUIViewController(context: Context) -> UIActivityViewController {
        let text = "?"
        let itemSource = ShareActivityItemSource(shareText: text, shareImage: photo)

        let activityItems: [Any] = [photo, text, itemSource]

        let controller = UIActivityViewController(
            activityItems: activityItems,
            applicationActivities: nil)

        return controller
    }

    func updateUIViewController(_ vc: UIActivityViewController, context: Context) {
    }
}

iOS13からのLPLinkMetadataを使う

import LinkPresentation

class ShareActivityItemSource: NSObject, UIActivityItemSource {

    var shareText: String
    var shareImage: UIImage
    var linkMetaData = LPLinkMetadata()

    init(shareText: String, shareImage: UIImage) {
        self.shareText = shareText
        self.shareImage = shareImage
        linkMetaData.title = shareText
        super.init()
    }

    func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
        return UIImage(named: "AppIcon ") as Any
    }

    func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
        return nil
    }

    func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
        return linkMetaData
    }
}

ViewをUIViewに変換する

SwiftUI でViewをUIImageに変換する方法
前回書いた記事を参考にSwiftUIのViewをUIImageに変更する方法をご参照ください。

作成したShareSheetをモーダルで呼び出す

struct TestPage: View {
    @State private var rect: CGRect = .zero
    @State private var uiImage: UIImage? = nil
  // modalを表示するためのフラグを持つ
    @State private var showShareSheet = false

    var body: some View {
        VStack {
            HStack {
                Image(systemName: "sun.haze")
                    .font(.title)
                    .foregroundColor(.white)
                Text("Hello, World!")
                    .font(.title)
                    .foregroundColor(.white)
            }
            .padding()
            .background(Color.blue)
            .cornerRadius(8)
            .background(RectangleGetter(rect: $rect))
            .onAppear() {

            }

            Button(action: {
                self.uiImage = UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)
                self.showShareSheet.toggle()
            }) {
                Image(systemName: "square.and.arrow.up")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 50, height: 50)
                    .padding()
                    .background(Color.pink)
                    .foregroundColor(Color.white)
                    .mask(Circle())
            }.sheet(isPresented: self.$showShareSheet) {
              // 共有したいUIImageを渡す
                ShareSheet(photo: self.uiImage!)
            }.padding()
        }
    }
}

結果

IMG_0374.JPG

ちゃんとツイートできました!

全体のコード

https://gist.github.com/tsuzukihashi/d08fce005a8d892741f4cf965533bd56

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

SwiftUIでのMFMailComposeViewControllerの使用

前置き

やりたいこと

アプリ内からメールを送れるようにしたい。
sheetを用いてメーラーを表示し、ユーザーはそこで編集・送信・キャンセル等を行う。

環境

  • Xcode: 11.2.1
  • Swift: 5.1.2
  • 実機: 13.2.3

実装

下記のMailViewというViewを用意して、必要な箇所(今回の場合はContentView)でそれを呼び出すようにします。

MailView.swift
import SwiftUI
import MessageUI

struct MailView: UIViewControllerRepresentable {
    @Binding var isShowing: Bool

    func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> UIViewController {
        let controller = MFMailComposeViewController()
        controller.mailComposeDelegate = context.coordinator
        controller.setSubject("これが件名")
        controller.setToRecipients(["hogehoge@hogehoge.com"])
        controller.setMessageBody("これが本文", isHTML: false)
        return controller
    }

    func makeCoordinator() -> MailView.Coordinator {
        return Coordinator(parent: self)
    }

    class Coordinator: NSObject, MFMailComposeViewControllerDelegate, UINavigationControllerDelegate {
        let parent: MailView
        init(parent: MailView) {
            self.parent = parent
        }

        func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
            // 終了時の処理あれこれ

            self.parent.isShowing = false
        }
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<MailView>) {
    }
}

呼び出し側

ContentView.swift
import SwiftUI
import MessageUI

struct ContentView: View {
    @State private var isShowingMailView = false

    var body: some View {
        Button(action: {
            self.isShowingMailView.toggle()
        }) {
            Text("Open MailView")
        }
        .disabled(!MFMailComposeViewController.canSendMail())
        .sheet(isPresented: $isShowingMailView, content: {
            MailView(isShowing: self.$isShowingMailView)
        })
    }
}

動作gif

ボタンを押してMailViewを開き、その後はキャンセルボタンを押して下の画面に戻っています。
※ 差出人メールアドレスは一時的に変更した仮のものを使用しています。
mailViewTest.gif

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

#17 Xcodeでアプリの向きを縦に固定する1例

はじめに

個人のメモ程度の出来なのであまり参考にしないで下さい.

環境

Xcode:11.2.1
Swift:5.1.2
2019/11

part1

project名を選択し,GeneralDeployment InfoDevice OrientationPortraitのみを選択した状態にする.

スクリーンショット 2019-12-01 午後8.33.04.png

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

AutoLayoutをコードで指定する際の注意点(NSLayoutAnchor)[Swift]

Youtubeチャンネルの素材用にTinderの模擬アプリを作成する際、
シンプルだけど長い時間はまってしまったのでメモ。

状況

Viewの制約といっても色々あるが、今回ハマったのはViewの縁を設定する制約。
(図のようにViewControllerの縁に対して上下左右10pxずつのmarginを設定して配置したい、という感じ。)
girlsView-ex.jpeg
Storyboardでの配置だとcustomViewだろうが標準Viewだろうが縁の制約はAdd New Constraints
こんな感じで10をひたすら入れていけばできてしまうが
スクリーンショット 2019-12-01 19.48.17.png
NSLayoutAnchorを使ってコードで設定したとき同じ容量でこんな感じでやったら

girlsViewArray[$0].leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10.0).isActive = true
girlsViewArray[$0].trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 10.0).isActive = true
girlsViewArray[$0].topAnchor.constraint(equalTo: self.view.topAnchor, constant: 10.0).isActive = true
girlsViewArray[$0].bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 10.0).isActive = true

ViewViewControllerからはみ出してしまった

対策

NSLayoutAnchorconstantCGFloatの座標系(x: 右に行くほど増える, y: 下)かつequalToで設定した縁からの距離である
->Add New Constraintsはプラスの数値を入れれば選択したViewのsuperViewに対してスペースを作る(縮める)方向に勝手に制約を設定してくれる
以上を理解した上で修正した制約がこう(上下左右ViewControllerのViewに対して10pxずつマージンを持たせたい場合)
->trailingAnchor左に詰めたいのでconstantを-10.0にする必要がある
->bottomAnchor上に詰めたいのでconstantを-10.0にする必要がある

girlsViewArray[$0].leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10.0).isActive = true
girlsViewArray[$0].trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10.0).isActive = true
girlsViewArray[$0].topAnchor.constraint(equalTo: self.view.topAnchor, constant: 74.0).isActive = true
//84.0 = 44.0(navHeight) + 20.0(statusBarHeight) + 10(margin)
girlsViewArray[$0].bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -10.0).isActive = true
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】associatedtypeの使いどころ

使いどころがよくわかってなかったけど気になっていたSwiftのassociatedtype
個人開発アプリでいい感じに使えたのでここに書いておきます。

associatedtypeとは

  • protocolに定義する連想型です
  • protocolの準拠時に、具体的な型を指定します(または型推論で指定されます)
  • ジェネリクスにおけるT的なやつです

使いどころ

上記の通りなのですが、protocol定義時点では決められず、準拠側で指定したい型があるときが使いどころです。
具体的には、APIを叩いて、レスポンスに含まれるJSONから特定の型を作りたい!ってときに使えました。

具体例

以下、AlamofireとQiita APIをサンプルに使った例です。

やりたいこと

これを単純に書くとこんな感じになります。

protocol、associatedtypeを使わないパターン.swift
func hoge() {
    Alamofire
        .request("https://qiita.com/api/v2/users/akeome")
        .response(completionHandler: { response in
            guard
                let data = response.data,
                let user = try? JSONDecoder().decode(User.self, from: data) else {
                    return
            }

        print(user.id)  // akeome
        print(user.userDescription)  // ぁけぉめです。以下略
    })
}

なお、ここでのUser型はJSONを元に自動生成1したCodableなstructです。

もっとよくできないかしら

上記のような処理を繰り返し書くことがあれば、こんな不満と願望が湧いてきます

  • この長い処理を何度も書きたくない → リクエストメソッド自体は共通化したい
  • 処理を呼ぶたびにURLとレスポンスJSONの型をいちいち書きたくない → APIの呼び出し先を指定するだけでJSONをデコードする型も決まってほしい

こんな願いを叶えてくれる機能がSwiftにはちゃんと備わっているのです。
それがprotocol、そしてassociatedtypeです。

protocolにしてみよう

まずはAPIを呼び出せるprotocolを考えましょう。
リクエストメソッド自体は共通化しつつAPIの呼び出し先を変えるには、protocolが使えそうです。

APIConfigure.swift
protocol APIConfigure {
    // APIの呼び出し先
    static var path: String { get }
    // リクエストメソッド(※クロージャは後述)
    static func request()
}

呼び出し先は準拠する側で決めるとして、リクエスト処理は共通なのでprotoocol extensionで実装します。

APIConfigure.swift
extension APIConfigure {
    static func request() {
        Alamofire
            .request(Self.path)
            .response(completionHandler: { response in
                guard
                    let data = response.data,
                    //                                  ????
                    let xxx = try? JSONDecoder().decode(User.self, from: data) else {
                    return
                }
            })
    }
}

さてここで困ったことがあります。
JSONDecoder().decode()の第一引数に渡す型を指定しなければならないのです!

共通化したい!でも具体的な型はまだ決められん!準拠側で指定したいんや!
そんなときこそassociatedtypeの出番です。

そう、associatedtypeならね

associatedtypeの定義

protocol本体の実装に戻って、「今はまだ決められないけど準拠時に決めてね」という型を定義します。
今回の例の場合、準拠時にUser型に指定する型です。

APIConfigure.swift
protocol APIConfigure {
    associatedtype ResponseEntity  // ?追加

    // APIの呼び出し先
    static var path: String { get }
    // リクエストメソッド(※クロージャは後述)
    static func request()
}

protocol extension側を修正していきます。

APIConfigure.swift
extension APIConfigure {
    static func request() {
        Alamofire
            .request(Self.path)
            .response(completionHandler: { response in
                guard
                    let data = response.data,
                    //                                            ?associatedtype
                    let responseEntity = try? JSONDecoder().decode(ResponseEntity.self, from: data) else {
                    return
                }
            })
    }
}

型制約の追加

このままでは以下のエラーになります。

Instance method 'decode(_:from:)' requires that 'Self.ResponseEntity' conform to 'Decodable'`

JSONDecoder().decode()の第一引数に渡す型はDecodableに準拠している必要があるのです。
このままでは、ResponseEntityがJSONから変換可能な型なのかどうかがわからないのです。

そこで型制約の追加です。

APIConfigure.swift
protocol APIConfigure {
    //                            ?型制約の追加
    associatedtype ResponseEntity: Codable

    // APIの呼び出し先
    static var path: String { get }
    // リクエストメソッド(※クロージャは後述)
    static func request()
}

これでResponseEntityはJSONから変換可能な型(=Codable)であると制約をかけられました。

そしてクロージャへ

リクエスト処理にクロージャを追加して、呼び出し元でJSONをデコードした型として扱えるようにします。

APIConfigure.swift
protocol APIConfigure {
    associatedtype ResponseEntity: Codable

    // APIの呼び出し先
    static var path: String { get }
    // リクエストメソッド               ?クロージャを追加
    static func request(completion: ((ResponseEntity) -> ())?)
}
APIConfigure.swift
extension APIConfigure {
    static func request(completion: ((ResponseEntity) -> ())?) {
        Alamofire
            .request(Self.path)
            .response(completionHandler: { response in
                guard
                    let data = response.data,
                    let entity = try? JSONDecoder().decode(ResponseEntity.self, from: data) else {
                    return
                }

                completion?(entity)
            })
    }
}

ここまでで、悲願の「リクエストメソッドの共通化」「APIの呼び出し先を指定するだけでJSONをデコードする型の確定」を満たせそうなprotocolができあがりました。

準拠側

Qiita APIのユーザー情報取得を扱うstructを作り、protocolに準拠させます。

UserGet.swift
struct UserGet: APIConfigure {
    typealias ResponseEntity = User  // ?ここで具体的な型を指定
    static let path = "https://qiita.com/api/v2/users/akeome"
}

protocolで定義したassociatedtypeを準拠側で明示的に指定するにはtypealiasを使います。
これでUserGetを使ったリクエスト処理のクロージャで受け取る型はUser型に指定できました。

APIを呼び出す共通的なstructを作ってまとめていくことが考えられます。

APIClient.swift
struct APIClient {
    // ユーザー情報取得
    struct UserGet: APIConfigure {
        typealias ResponseEntity = User
        static let path = "https://qiita.com/api/v2/users/akeome"
    }

    // 記事一覧取得
    struct ItemsGet: APIConfigure {
        typealias ResponseEntity = [Item]
        static let path = "https://qiita.com/api/v2/items"
    }

    // その他いろいろ
}

associatedtype使ってみた結果

protocol、associatedtypeを使ったパターン.swift
func hoge() {
    APIClient.UserGet.request(completion: { user in
        print(user.id)  // akeome
        print(user.userDescription)  // ぁけぉめです。以下略
    })
}

こんな感じで、リクエスト処理のクロージャで受け取る型を自動的にUserにできました。
とてもすっきりしたのではないでしょうか。

今後ユーザー情報取得だけでなく記事一覧も取得したくなっても
APIClient.GetItems.request(〜と書くだけです。

比較のため、冒頭に記載したassociatedtypeを使わないパターンも再掲しておきます。

protocol、associatedtypeを使わないパターン.swift
func hoge() {
    Alamofire
        .request("https://qiita.com/api/v2/users/akeome")
        .response(completionHandler: { response in
            guard
                let data = response.data,
                let user = try? JSONDecoder().decode(User.self, from: data) else {
                    return
            }

        print(user.id)  // akeome
        print(user.userDescription)  // ぁけぉめです。以下略
    })
}

まとめ

今回は割愛しましたが、

  • protocolにHTTPMethodを持たせてGETやPOSTに対応する
  • protocolにパラメーターを持たせる
  • protocol extensionでベースURLを定義する
  • リクエスト処理のエラーに対応する

などすれば、より実用的なコードになるかと思います。

associatedtypeについての記事がなかなか見つからなかったので参考になる方がいらっしゃれば幸いです。

おまけ

associatedtypeを使ってこんなアプリを作ってます。
CardPort - App Store

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

【Xcode】細分化する iOS Architecture に向き合う上で気をつけなければらないただひとつのエラーについて

はじめに

こんにちは。Reactive な世界に生命の息吹を感じるたかねです。

今年もやってきました,iOS Advent Calendar 2019 1日目です!
みなさまよろしくお願いいたします!

本記事は unable to spawn process (Argument list too long) という Xcode からのすてきな (!) メッセージについてです。
普段の開発では見慣れないエラーかと存じます。しかし,本問題を知り,本問題を見据えて開発することは,きっと数年後の iOS 開発者であるあなたの役に立つと思います。少し長いですが,ぜひご覧いただけましたら幸いです。

目次

最近のソフトウェアアーキテクチャに関する流行と振り返り

本題に入る前に少しおさらい、もとい振り返りをさせてください。2018年は iOSアプリ設計パターン入門 を始め,Clean Architecture: A Craftsman's Guide to Software Structure and Design の訳書である Clean Architecture 達人に学ぶソフトウェアの構造と設計 が出版され,日本の iOS 開発者が様々なソフトウェアアーキテクチャに対し知見を得る1年となりました。実際に MVVM を始め、多くの企業は細分化されたアーキテクチャを採用している様子が確認できるようになってきています。我々 iOS エンジニアは設計というスキルを得ることができ,次なる問題に取りかかることができるようになりました。

プロジェクトが適切に肥大化し続ける中待ち受ける,たったひとつのエラー

閑話休題。ここでの「プロジェクト」とは Xcode の Project と同義であり,ソースコードとその周辺環境を指します。
過去に FATViewController などと揶揄されたこともあった UIViewController へのドメインロジック混入による肥大化は,View と Model を分離 (プレゼンテーションとドメインの分離; Presentation Domain Separation) することを目的とした各種設計により解決されました。

素晴しく綺麗な環境で開発される iOS プロジェクトは,適切に分割されたレイヤーによりテストがし易く堅牢で,かつ明快である。その環境下では無限にスケールしてもその効用を維持し続けることができる,その最たる例が Clean Architecture である。

そう,私もそう信じてやみませんでした。件のエラーが発生するまでは。

以下をご覧ください。これは,ある条件下において Xcode でビルドした際に発生するエラーです。

unable to spawn process (Argument list too long) エラーです。

これが Xcode である証拠に,画面全体のスクリーンショット も添付しておきます。

また,xcodebuild コマンドによる CLI 経由でも発生します。

これは一体何でしょうか? この記事をご覧になっている皆様にも発生する問題なのでしょうか?

unable to spawn process (Argument list too long) とは? その発生条件

unable to spawn process (Argument list too long) エラーが発生する原因はただひとつ。エラー文の通り「引数リストが多過ぎる」というエラーです。

Compile Swift source files ステップにて, コンパイラに渡されるソースファイルの Full path とオプションの文字数の合計がおよそ 260,000 文字程度※1 を越えるとコンパイルが必ず失敗する というものです。

............遭遇しなければ気がつかないエラーです?????

いくつかの対処方法と,Xcode 11 における解決方法

ファイル数が多い,絶対パスが長くなるような深いディレクトリに存在する,極端に長いファイル名を使用しているなどの原因により本問題は発生します。

その解決方法は swiftc (Swift コンパイラ) に対し渡すコマンドの総量をどうにかして減らすことのみでした。今秋リリースされた Xcode 11 ではようやく解決可能なフラグが追加されました。
いくつかの対処方法と,Xcode 11 における解決方法を順に解説いたします。

1. ライブラリをビルド済みバイナリ形式でビルドする

こちらは CocoaPods を利用している場合に限ります。Carthage はビルド済みバイナリを利用しますが,CocoaPods の場合ビルドが含まれるため,その分パスが長くなります。どうしても CocoaPods を利用したい方は,cocoapods-binary というプラグインを導入することで,インストール時にバイナリとして事前にコンパイルさせておくことが可能です。

余談ですが,cocoapods-binary はオプションによりソースコードを残しつつバイナリを生成することもできます。本オプションにより,Carthage の弱点であった「開発時に Xcode 内からライブラリ側のソースコードを辿れない」という問題に対応しつつ,ビルド時にはコンパイル済みのバイナリを利用してコンパイルを早くするといったことが可能となります。便利です。

2. スクリプトによってコンパイル対象のソースファイル名を変更する

愚直な解決方法として挙げられるのはこちらです。「ファイル名の長さが原因」なので,実際にコンパイラに渡すまでに短いファイル名に変更してからコンパイルするようにすれば良い,ということになります。
具体的な方法はあまりにも泥臭いため省略します。

3. プロジェクトをルートディレクトリに移動させる

コンパイラに渡されるソースファイルのパスは Full Path であるという仕様を利用する方法です。リポジトリを Clone するディレクトリをルート直下に配置するだけです。全てのソースファイルに影響があるためファイル数が多いプロジェクトほど効果があります。ちなみに Apple の Developer forum にも同様の提案がされています。
一方で,これは一時的解決に過ぎず,この状態で開発を続けてもいずれエラーとなることは目に見えています。
(Bitrise がルートディレクトリに Clone する仕様で助かりましたね!)

4. Embeded Framework を利用してコンパイルする

Embeded Framework を用い,分割コンパイルすることで一度にコンパイルするソースファイルの数を減らす方法です。これが一番まともであり,根本解決とは異なりますが正攻法と言えるでしょう。

5. USE_SWIFT_RESPONSE_FILEYES と指定する (Xcode 11 以降)

Xcode 11 以上の環境で build settings USE_SWIFT_RESPONSE_FILEYES を代入する ことで解決できるとの情報が更新されていました。Xcode 10 まででは根本解決に至らなかったこのエラーは,Xcode 11 によってようやく解決されたようです。

まとめ

  • Xcode には コンパイラへ渡すソースファイルの内容によって unable to spawn process (Argument list too long) が発生する致命的な問題を潜在的に抱えていた
  • Xcode 11 未満は Embeded Framework による Target 分割を始めとした回避方法が存在する
  • Xcode 11 以降であれば USE_SWIFT_RESPONSE_FILEYES を代入することで解決できる

秋に発表された Xcode 11 は図らずとも我々に良い結果をもたらしました。
一方で堅牢な設計を維持するためには Embeded Framework を利用することがコンパイル時間の短縮のためにも良いかと思います。投稿が少し遅くなりましたが,ここらで筆を置かせていただきます。

(おまけ) なぜ日本語記事が存在しなかったのか

本章は考察です。Xcode には以前よりこのような問題を抱えていました。少なくとも 2017 年には
私が iOS 開発を始めたのも 2017 年ですので、それ以前の流れは分かりません。設計に関する関心は恐らくこれと同じ時期、もしくはそれ以後に盛況を呈したものと思われます。

2017 年以後,設計に関心を持つ開発者が本問題に遭遇することはなかったのか?

この疑問については,周囲から話を聞く限りでは「細分化される Embeded Framework を利用しており,発生する前に自然と回避していた」と考えられます。

Swift における名前空間について

他言語ではディレクトリ名が名前空間として予約され,異なるディレクトリ間でも問題なく扱える機能がありますよね。
Swift における名前空間は暗黙的であり,利用するためには Framework による Target の分離が必要です。Framework によって分割するということはコンパイルソースを分割することにもなるため,名前空間による解決手法は結果的に上で記載した手法に含まれます。
(この仕様自体も,ソースファイルの命名が長くなる要因だったり......?)

エラー原因の背景1 – XcodeGen の導入とリファクタリングの順序 –

今回エラーとなった原因の背景には「MVC から Clean Architecture へのリファクタリング → XcodeGen によるコンフリクトの解消 → Embeded Framework による分割」 のステップを検討しており,同時に Framework 化を進められなかった事情がありました (こんなエラーが発生することを予期できていれば最初から Target を分けて作成していましたよ!)。その結果,1 target に全ての層が入りこみ,MVC から分割されたファイルは必然的に増える結果を生みました。

エラー原因の背景2 – Presentation 層への Atomic design を用いた介入 –

さらに,今日までのソフトウェアアーキテクチャにおける欠点のひとつに「View (Presentation) の実装については関心を持たない」という点も加筆しておかねばなりません。昨年 Sketchと1対1を目指すAtomic designなStoryboardの作り方 - Qiita という記事を書きました。Presentation 層に対し無関心であった部分について,Atomic design を利用して再利用可能なレベルに分割する手法を導入しました。こちらも,再利用を意識し過剰な .xib ファイルとその対となる .swift が生成され,ファイル数の増加に繋がりました。

Presentation 層におけるかすかな希望

Atomic design 自体が悪かった訳ではありません。実際に再利用によってボタンやフォームを始めとする UI の作成速度は向上し,意図せぬ再実装がなくなりました。また,SwiftUI の登場は Storyboard および Xib ファイルからの開放,コード生成と GUI による生成の共存という結果を生み出しました。これだけでも旧実装よりファイル数,コード数ともに削減させる一手と成り得る希望を生みました (完全に Replace できるかについて,ここでは言及を避けるものとします)。

注釈

※1) 上限の境界については未検証のため正確ではありません。263,627 文字で失敗を確認しています。

文献

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

XcodeGenを使うとチーム開発が楽になる話

この記事は豊田高専コンピュータ部アドベントカレンダー1日目の記事です

XcodeGenとは

iOSアプリ開発で複数人での開発をすると色々とつらいことがあるのでそのつらいことをなるべく無くそうという感じのツールです。具体的にはプロジェクトへのファイルの追加など行うとプロジェクトファイルが書き換わるのですが、そのプロジェクトファイルは人間におおよそ理解のできない形式で書かれており、チーム開発をしているとそれが恐ろしいコンフリクトを生み、その解消がとてもつらいという問題があります。そのプロジェクトファイルをコード(yaml)から生成することでチーム開発をやりやすくしようという感じのツールになります。
https://github.com/yonaskolb/XcodeGen

触ってみる

なにはともあれ百聞は一見にしかずなので触ってみましょう。まずはXcodeGenをインストールします

$ brew install xcodegen

XcodeGenを入れられたら次はXcodeのプロジェクトを作成します。プロジェクト名は今回は「SampleGen」とします。プロジェクトの作成手順は今回は割愛します。

プロジェクトを作成できたらプロジェクトファイルの内容をproject.ymlに書き起こしていきます。

project.yml
# プロジェクト構成・設定
name: SampleGen
configs:
  Debug: debug
  Release: release
options:
  groupSortPosition: top
attributes:
  ORGANIZATIONNAME: ymgn
fileGroups:
 - SampleGen
 - SampleGenTests

# ターゲット
targets:
  SampleGen:
    type: application
    platform: iOS
    sources:
      - path: SampleGen
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: ymgn.SampleGen
        ASSETCATALOG_COMPIER_APPICON_NAME: AppIcon
        INFOPLIST_FILE: SampleGen/Info.plist
      configs:
        Debug:
          GCC_OPTIMIZATION_LEVEL: O
        Release:
          GCC_OPTIMIZATION_LEVEL: s
  SampleGenTests:
    type: bundle.unit-test
    platform: iOS
    dependencies:
      - target: SampleGen
    settings:
      TEST_HOSTS: $(BUILT_PRODUCTS_DIR)/SampleGen.app/SampleGen
      INFORPLSIT_FILE: SampleGenTests/Info.plist
    sources:
      - SampleGenTests

# スキーム
schemes:
  SampleGen:
    build:
      targets:
        SampleGen: all
    run:
      config: Debug
    test:
      config: Debug
      gatherCoverageData: true
      targets:
        - SampleGenTests
    profile:
      config: Release
    analyze:
      config: Debug
    archive:
      config: Release

要素一つ一つを説明するのはつらいのでXcodeGenのリポジトリにあるドキュメントを参照してもらうとして、プロジェクトファイルの項目に対応した要素が存在します。プロジェクトファイルからyamlへの書き起こしは手で書きましたが、自動で書き起こしてくれる方法などあればぜひコメントで教えてください。

project.ymlはプロジェクトファイルと同じ階層に配置し、プロジェクトに追加する必要はありません。
project.ymlが出来上がったらプロジェクトファイルは削除しても構いませんが、写しミスがあった場合悲しいので残しておくとよいでしょう。
project.ymlがあるディレクトリで

$ xcodegen generate

を実行するとプロジェクトファイルを生成します。既存のプロジェクトファイルがある場合は書き換えられます。

最低限の実装になっているのでそのままビルドするとおそらくSigningでエラーが出ると思いますので、Teamは手動で追加してもらうとエラーは消えるはずです。

これで一通りの導入は終了です。

まとめ

上記では手順だけ示したのであまり恩恵がわからないかもしれませんが、targetの追加や、ライブラリ、Embeded Frameworkの追加などもproject.ymlで記述できるのでコマンド一発でどの環境でもコンフリクトのない状態のプロジェクトファイルが生成されます。実際に使用する際はプロジェクトファイルは.gitignoreなどで管理には含めないようにすることに注意してください。
もっと詳しく知りたい場合はXcodeGenのリポジトリや他記事を参照してもらえると多くの設定が利用できたり、設定ファイルを分割できたりすることがわかると思います。iOSアプリのチーム開発で困っている人はぜひ試してみてください。

おまけ

XcodeGenを使ったプロジェクトをBitriseなどのCI/CDツールに載せるとプロジェクトファイルが存在しないため、validationが通らなくて焦るかもしれません。しかし、最初のvalidationが通らなくてもPROJECT_PATHには単に生成される.xcodeproj(もしくは.xcworkspace)のファイル名を入力して、workfrowにXcodeGenをビルド前に実行するようにすればビルドできるようになります。

スクリーンショット 2019-12-01 16.23.10.png

Bitriseであればこのようにxcodegenのコマンドを使用するためのユーティリティが存在します。

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

【Swift】Delegateとは(別オブジェクトへの処理の委譲)

 私がiOSアプリを開発する中で、デリゲートパターンについてなーなーなまま進めて痛い目に遭ってきたので今回ここにまとめたいと思います。おかしいと思うところがあったらどしどしコメントください。

デリゲートパターン

 デリゲートパターンは、あるオブジェクトの処理を別のオブジェクトに代替させるパターンです。デリゲートもとのオブジェクトは、適切なタイミングでデリゲート先のオブジェクトに通知を送り、その通知を受けたデリゲート先のオブジェクトは、自身や別のオブジェクトの状態を変更したり、結果をデリゲート元のオブジェクトに返したりすることができます。
 デリゲートとして他のオブジェクトに渡す処理は、事前にプロトコルとして宣言しておく必要があります。

実装例

 今回の例では、デリゲート元であるGreetingクラスの処理(greeting()メソッド内のstrやdidMorning、didAfternoon)を、Greetクラスに委譲します。

 まず、protocolにデリゲート元に渡したい処理を記述します。デリゲート先のクラスはこのprotocolに準拠すべき(GreetクラスはGreetDelegateに準拠したクラスclass Greet: GreetDelegate)で、渡す処理を全て実装しておかなければなりません。そのため、protocol内に記述した処理がない場合はコンパイルエラーになります(処理が実装されてませんってエラー出してくれるからわかりやすい)。

delegateSapmple.prayground
import UIKit
//デリゲート先のクラスにて、
//デリゲート元に渡したい処理をprotocolとして宣言する
protocol GreetDelegate: class {
    var str: String { get }
    func didMorning(_ greet: Greeting)
    func didAfternoon(_ greet: Greeting)
}

//デリゲート先のクラス
class Greet: GreetDelegate {
    var str: String { return "Hey!" }

    func didMorning(_ greet: Greeting) {
        print("Good morning!")
    }

    func didAfternoon(_ greet: Greeting) {
        print("See you!")
    }
}

//デリゲート元のクラス
class Greeting {
    weak var delegate: GreetDelegate?

    func greeting() {
        print("\(delegate?.str ?? "Default")")
        print("In the morning...")
        delegate?.didMorning(self)
        print("In the afternoon...")
        delegate?.didAfternoon(self)
    }
}

//結果を確認しよう
let delegate = Greet()
let greeting = Greeting()

//Greetingクラスのインスタンスgreetingのデリゲート先として、
//Greetクラスを指定する
greeting.delegate = delegate
//greeting()を実行!
greeting.greeting()

結果↓

Hey!
In the morning...
Good morning!
In the afternoon...
See you!

 Greetingクラスは、greeting()メソッドの中で、delegateプロパティを通じてデリゲート先のクラスにstrを問い合わせています。また、朝の挨拶(didMorning)と昼の挨拶(didAfternoon)のタイミングをデリゲート元に伝えています。

関数の命名規則や例

 デリゲートパターンでは、デリゲート先(Greetクラス)にデリゲート元(Greetingクラス)から呼び出されるメソッド群を実装する必要があります。実装する必要のあるメソッドは、プロトコルとして宣言していました。
 メソッドやプロトコルの命名には規則が存在するようです。例えば、みなさんがよく使う(と思う)UITableViewクラスのデリゲートメソッドのなかに、UITableViewCellがタップされた時に実行されるメソッドがあります。それは以下のように宣言されています。

public protocol UITableViewDelegate: NSObjectProtocol, UIScrollViewDelegate {
    (...)
    optional public func tableView(_ tableView: UITableView,
                                   didSelectRowAt indexPath: IndexPath)

 デリゲートパターンではどのタイミングで呼ばれるかということを明示するため、didやwillをメソッドの先頭につけます。そして、デリゲート先が必要とする情報を引数として渡します。この例では、どのインデックスのセルがタップされたかという情報が必要なので、IndexPath型のindexPathが引数になっています。
 よく見ると第一引数にデリゲート元のオブジェクトを渡していますね。なぜかというと、複数のプロトコルに準拠する際、デリゲートメソッド同士の名前が衝突してしまうことがあるかもしれないからです。tableView(_:didSelectRowAt)メソッドの例では、セルが選ばれた(selectRow)後に(did)UITableViewクラスが呼ぶメソッドであるということが、メソッド名を見るだけでわかるようになっています。
 以上のことを踏まえ、Appleの提供するCocoa,Cocoa Touchフレームワーク内で利用されているデリゲートパターンでは次の命名規則にしたがっているとまとめることができます。

・didやwillを用いてメソッドの呼ばれるタイミングを示す
・メソッド名はデリゲート元のオブジェクト名から始め、続いて処理の内容を説明する
・第一引数にはデリゲート元のオブジェクトを渡す

 慣れてくるとこれらの規則を満たさなくても記述できると思います。しかし、名前の衝突を回避できたり、初めてコードを読む人にも理解させやすいといった利点があると思うので、命名規則は守る方がいいかなと思いました。

まとめ

 今回はSwiftのデリゲートについて細かく紹介しました。デリゲート元とデリゲート先が逆で捉えていたり、具体的な使い方がわからない方(私もまだまだわかっていません…)もいると思います。そう言った方達が少しでも理解してくれたら幸いです。(実践的なコードも更新していけたらいいなと思っています。よろしくお願いします。)

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

SwiftUI でViewをUIImageに変換する方法

もちべ?

特定のSwiftUIのViewをShareSheetを使ってシェアするために、特定のViewをUIImageに変換する必要がありました。
最初はUIHostingControllerなどを使ってVIewからUIImageを取得しようとしましたが、上手くいかず...

SwiftUIの問題は、大抵GeometryReaderを使えば解決できるという巷の噂に身を任せたら解決できました☕️
とにかくコード見せてって人はこちらから
https://gist.github.com/tsuzukihashi/41fbb0c594e26e317cfcec878e9948f4

よくわかる解説

まず解説のために適当なViewを用意しました。このViewをUIImageに変換していきます。

スクリーンショット 2019-12-01 15.06.46.png

struct TestPage: View {
    var body: some View {
        HStack {
            Image(systemName: "sun.haze")
                .font(.title)
                .foregroundColor(.white)
            Text("Hello, World!")
                .font(.title)
                .foregroundColor(.white)
        }
        .padding()
        .background(Color.blue)
        .cornerRadius(8)
    }
}

次にUIImageに変換したいViewのサイズと位置を取得したいです。
今回で言うとHStackのCGRectがわかれば良いでしょう。

ここで一つ目のポイントなのですが、ViewのCGRectを取得するためにbackgroundメソッドを利用します。
backgroundの引数はViewプロトコルに準拠していれば良いので、Viewを返しつつサイズを取得するようなRectangleGetterを作成します。

以下がbackgroundメソッドの定義です。Viewを返せば良いらしい?

func background<Background>(_ background: Background, alignment: Alignment = .center) -> some View where Background : View

https://developer.apple.com/documentation/swiftui/view/3278516-background

@Bindingで親ViewのCGRectを指定します。
ここでGeometryReaderの出番です!
GeometryReaderで親Viewのgeometryを取得し、createViewメソッドでViewのサイズと位置情報をBindingしたrectに与えます。
Viewを返さなくてはいけないので透明なRectangleViewを返します。

struct RectangleGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { geometry in
            self.createView(proxy: geometry)
        }
    }

    func createView(proxy: GeometryProxy) -> some View {
        DispatchQueue.main.async {
            self.rect = proxy.frame(in: .global)
        }
        return Rectangle().fill(Color.clear)
    }
}

先ほど作成した、TestPageに@Stateのrectを用意します。
HStackのbackgroundにRectangleGetterをかませます。

struct TestPage: View {
  //追加 >>>
    @State private var rect: CGRect = .zero
  // <<<
    var body: some View {
        HStack {
            Image(systemName: "sun.haze")
                .font(.title)
                .foregroundColor(.white)
            Text("Hello, World!")
                .font(.title)
                .foregroundColor(.white)
        }
        .padding()
        .background(Color.blue)
        .cornerRadius(8)
      // 追加 >>>
        .background(RectangleGetter(rect: $rect))
      // <<<
    }
}

取得したいViewの位置とサイズが分かったので、最前面の画面が取得できるUIApplication.shared.windows[0].rootViewController からUIViewを取得します。

UIViewからUIImageに変換するのは従来のやり方でUIGraphicsImageRendererを利用します。
UIViewをextensionしてgetImageメソッドを追加します。

extension UIView {
    func getImage(rect: CGRect) -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: rect)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}

そして、TestPageの必要なタイミングでUIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)を呼ぶことでUIImageを取得することができます。

UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)
struct TestPage: View {
    @State private var rect: CGRect = .zero
    //追加 >>>
    @State var uiImage: UIImage? = nil
    // <<<
    var body: some View {
     //追加 >>>
        VStack {
    // <<<
            HStack {
                Image(systemName: "sun.haze")
                    .font(.title)
                    .foregroundColor(.white)
                Text("Hello, World!")
                    .font(.title)
                    .foregroundColor(.white)
            }
            .padding()
            .background(Color.blue)
            .cornerRadius(8)
            .background(RectangleGetter(rect: $rect))
     //追加 >>>
            Button(action: {
                self.uiImage = UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)
            }) {
                Text("Button")
                    .padding()
                    .foregroundColor(Color.white)
                    .background(Color.red)
                    .cornerRadius(8)

            }

            if uiImage != nil {
                VStack {
                    Image(uiImage: self.uiImage!)
                }.background(Color.green)
                    .frame(width: 500, height: 500)
            }
        }
    // <<<
    }
}

スクリーンショット 2019-12-01 15.01.00.png

以上のようになり。ボタンをタップすると

Imageが表示されます。ありがとうございました?‍♂️
スクリーンショット 2019-12-01 14.55.32.png

まとめ

SwiftUIのViewからUIImageを取得するためには
1. View全体を得るためにbackgroundを使う
2. GeometryReaderを使ってCGRectを取得する
3. そのCGRectを使ってUIViewからUIImageを取得する

完成したコード
https://gist.github.com/tsuzukihashi/41fbb0c594e26e317cfcec878e9948f4

問題点

この方法は完全にViewだけをUIImageにしているのではなく、Viewの範囲をとってUIImageに変換しています。
したがって、このViewの上に別のViewがかぶっていた場合でも、かぶった範囲が一緒にUIImageになってしまいます。

参考にした記事URL

iOS:最前面に画面を出す/最前面の画面を知る
SwiftUIの肝となるGeometryReaderについて理解を深め
SwiftUIでChildViewのFrameを取得する

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

cygwinでSwiftを使う方法

はじめに

Windows 7 以降の 64BIT 版 Windows には、64BIT 版の cygwin の中に Apple iOS の標準言語であるところの Swift をインストールして使うことが出来ます。

ただし、予め以下のもののインストールが必要です。

  • cygwin 64BIT バージョン。
  • cygwin 版 clang ver 8 パッケージ。
  • cygwin 版 libiconv パッケージ。x86_64-w64-mingw32 というもの。
  • cygwin 版 libicu60 (6.0.2-1) パッケージ。

インストールすると、少なくとも以下のコマンドが使えるようになります:

  • swiftc : *.swift から cygwin で実行できる native binary を生成するコンパイラ。
    print("Hello World") をコンパイルして実行できることを確認済みです。
    また、LLVM の可読的中間言語(IR) の *.ll を出力できることも確認済みです。
    swiftc は、バックエンドに clang と ld を使っています。

  • swift : 対話式インタプリタ(REPL)。
    1 + 2[ret] と入れると 3 と出ます。

インストール方法

インストール方法を手順を追って説明します。

Win7, 64BIT に 64BIT 版 cygwin をインストール

cygwin の GUI インストーラーを使って、cygwin 公式サイトからインストールします。
以下で cygwin のインストールフォルダは、c:\cygwin64 とします。

その際、インストーラーで以下のパッケージをチェック状態にしておきます。

  1. clang の ver 8 のパッケージ。

  2. libiconv 系パッケージ。x86_64-w64-mingw32 的な名前のもの。これがインストールされると、
    /usr/x86_64-w64-mingw32/sys-root/mingw/lib
    に libiconv.a と libiconv.dll.a がインストールされる。これは、swiftc コンパイラが使う。

  3. Category: libs の Package: libicu60 (6.0.2-1) パッケージ。
    ICU とは、International Components for Unicode のこと。
    これをインストールすると、少なくとも C:\cygwin64\bin に cygicui18n60.dll, cygicuuc60.dll
    がインストールされます。これは、swiftc でコンパイルして生成された実行ファイルが起動されるときに動的ライブラリとして読み込まれます。

Swift のダウンロードと展開

https://swiftforwindows.github.io/news/2018/02/12/Swift-for-Windows-Cygwin-20180212/
https://github.com/tinysun212/swift-windows/releases/tag/swift-4.0.3+cygwin.20180212

にて、cygwin 用に非公式に公開されている swift-4.0.3.cygwin.20180212-bin.tar.gz をダウンロードします。*.7Z 形式の方がサイズがだいぶ小さいのでそちらの方が良いかも知れません。以下では、*.tar.gz の方で説明しておきます。

$ tar -xvzf swift-4.0.3.cygwin.20180212-bin.tar.gz

のようにして展開すると、xxx/usr/bin にコマンド類が、xxx/usr/lib にライブラリが配置されます。
ただし、xxx は、./swift-4.0.3.cygwin.20180212-bin となるはずです。

  • xxx/usr/bin には swift.exe と swiftc.exe が出来ています。
  • swiftc.exe がコンパイラで、swift.exe が対話式インタプリタです。
  • swiftc.exe は、*.ll のような LLVM コードを出力することも出来ます。

パスの設定

上記の xxx/usr/bin のフォルダのフルパスを、PATH 環境変数に追加しておきます。

swift-4.0.3.cygwin.20180212-bin.tar.gz を展開した場所が c:\xxx だった場合、以下の様になります。

$ export PATH=/cygdrive/c/xxx/usr/bin:$PATH

ライブラリパスの設定

clang のリンカ ld がライブラリの *.a, *.o をリンクする際に検索対象にする のパスを指定するため、cygwin の端末の中から、

$ export LIBRARY_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw/lib

とします。

テスト

以下の様なテストプログラムを作っておきます:

HelloWorld.swift
print("Hello World!")

コンパイル

$ swiftc -v HelloWorld.swift

とすると、

Swift version 4.0.3 (swift-4.0.3+cygwin.20180212)
Target: x86_64-unknown-windows-cygnus
/cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift -frontend -c -primary-file elloWorld.swift -target x86_64-unknown-windows-cygnus -disable-objc-interop -module-name HelloWorld -o /tmp/HelloWorld-1bba48.o
/cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift-autolink-extract tmp/HelloWorld-1bba48.o -o /tmp/HelloWorld-40659b.autolink
/usr/bin/clang++ -Xlinker -rpath -Xlinker /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin /tmp/HelloWorld-1bba48.o -L /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin -lswiftCore --target=x86_64-unknown-windows-cygnus @/tmp/HelloWorld-40659b.autolink -o HelloWorld

と出力されます。

実行テスト

$ ./HelloWorld
Hello World!

となります。

対話式インタプリタ

$ swift

とすると、対話式インタプリタが起動します。

備忘録

もし、shared library が見つからないと言うエラーが出たような場合、

cygcheck ./HelloWorld

とすると、どんな名前の shared library が見つからないかが、stderr に出力されます。ですので、

$ cygcheck ./HelloWorld 1>a 2>b

として、b のファイルをテキストエディタで見ればよいです。それは例えば、

cygcheck: track_down: could not find cygicui18n60.dll

cygcheck: track_down: could not find cygicuuc60.dll

のように出力されます。

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

Windows 上の cygwinでSwift コンパイラ/インタプリタを使う方法

はじめに

Windows 7 以降の 64BIT 版 Windows には、64BIT 版の cygwin の中に Apple iOS の標準言語であるところの Swift をインストールして使うことが出来ます。

ただし、予め以下のもののインストールが必要です。

  • cygwin 64BIT バージョン。

  • cygwin 版 clang ver 8 パッケージ。
    cygwin用のパッケージとしては、ver6, ver8 は有りますが、ver7 は有りません。cygwinパッケージとしてではなく、Windows 用の clang ver7をLLVM公式サイトからインストールしてそこにパスを通している場合は、今回のバージョンの Swift とは恐らくコマンドラインの書き方の一部に互換性が無いようです。よって、必ず ver 8 の clang を使ってください。
    以下は、インストールに作業には関係ない話ですので読み飛ばしてもらってかまいません。
    Swiftにおいて、clang または、clang を実装するためのコアライブラリが、llvm の backend として使われているようです。ただし、ここでいうコアライブラリとは、clang をインストールしていなければ使えないようなものではなく Swift 自身が予め静的リンクしている可能性があるようなものです。Swift は、論理的には *.swift を、Swift Intermediate Language (SIL) というSwift 専用の中間言語ファイル *.sil にコンパイルした後、 可読的 LLVM 言語 *.ll に直し、LLVM バイナリ *.bc に直し、native のオブジェクトコード *.o に直し、最後にリンカの ld で native の実行ファイルの *.exe などにリンクする、ということをしています。ただし、ディスク上のファイルを出力せずに一気に *.o に変換することも出来ます。なお、一旦テンポラリディスクファイルを作ってから消しているとは限らず、中間形式はオンメモリで処理されている可能性も有ります。なお *.ll は、オンメモリにすら作られずに、一気に *.bc に直されている可能性があります。

  • cygwin 版 libiconv パッケージ。x86_64-w64-mingw32 というもの。

  • cygwin 版 libicu60 (6.0.2-1) パッケージ。

インストールすると、少なくとも以下のコマンドが使えるようになります:

  • swiftc : *.swift から cygwin で実行できる native binary を生成するコンパイラ。
    print("Hello World") をコンパイルして実行できることを確認済みです。
    また、LLVM の可読的中間言語(IR) の *.ll を出力できることも確認済みです。
    swiftc は、バックエンドに clang と ld を使っています。

  • swift : 対話式インタプリタ(REPL)。
    1 + 2[ret] と入れると 3 と出ます。

インストール方法

インストール方法を手順を追って説明します。

Win7, 64BIT に 64BIT 版 cygwin をインストール

cygwin の GUI インストーラーを使って、cygwin 公式サイトからインストールします。
以下で cygwin のインストールフォルダは、c:\cygwin64 とします。

その際、インストーラーで以下のパッケージをチェック状態にしておきます。

  1. clang の ver 8 のパッケージ。

  2. libiconv 系パッケージ。x86_64-w64-mingw32 的な名前のもの。これがインストールされると、
    /usr/x86_64-w64-mingw32/sys-root/mingw/lib
    に libiconv.a と libiconv.dll.a がインストールされる。これは、swiftc コンパイラが使う。

  3. Category: libs の Package: libicu60 (6.0.2-1) パッケージ。
    ICU とは、International Components for Unicode のこと。
    これをインストールすると、少なくとも C:\cygwin64\bin に cygicui18n60.dll, cygicuuc60.dll
    がインストールされます。これは、swiftc でコンパイルして生成された実行ファイルが起動されるときに動的ライブラリとして読み込まれます。

Swift のダウンロードと展開

https://swiftforwindows.github.io/news/2018/02/12/Swift-for-Windows-Cygwin-20180212/
https://github.com/tinysun212/swift-windows/releases/tag/swift-4.0.3+cygwin.20180212

にて、cygwin 用に非公式に公開されている swift-4.0.3.cygwin.20180212-bin.tar.gz をダウンロードします。*.7Z 形式の方がサイズがだいぶ小さいのでそちらの方が良いかも知れません。以下では、*.tar.gz の方で説明しておきます。

$ tar -xvzf swift-4.0.3.cygwin.20180212-bin.tar.gz

のようにして展開すると、xxx/usr/bin にコマンド類が、xxx/usr/lib にライブラリが配置されます。
ただし、xxx は、./swift-4.0.3.cygwin.20180212-bin となるはずです。

  • xxx/usr/bin には swift.exe と swiftc.exe が出来ています。
  • swiftc.exe がコンパイラで、swift.exe が対話式インタプリタです。
  • swiftc.exe は、*.ll のような LLVM コードを出力することも出来ます。

パスの設定

上記の xxx/usr/bin のフォルダのフルパスを、PATH 環境変数に追加しておきます。

swift-4.0.3.cygwin.20180212-bin.tar.gz を展開した場所が c:\xxx だった場合、以下の様になります。

$ export PATH=/cygdrive/c/xxx/usr/bin:$PATH

ライブラリパスの設定

上記でインストールした iconv のライブラリ libiconv.a と libiconv.dll.a に「ライブラリ検索パス」を通す必要があります。
これは、clang のリンカであるところの ld がライブラリの *.a やオブジェクトファイル *.o を探すパスです。このパスを指定するため、cygwin の端末の中から、

$ export LIBRARY_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw/lib

とします。

テスト

以下の様なテストプログラムを作っておきます:

HelloWorld.swift
print("Hello World!")

コンパイル

$ swiftc -v HelloWorld.swift

とすると、

Swift version 4.0.3 (swift-4.0.3+cygwin.20180212)
Target: x86_64-unknown-windows-cygnus
/cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift -frontend -c -primary-file elloWorld.swift -target x86_64-unknown-windows-cygnus -disable-objc-interop -module-name HelloWorld -o /tmp/HelloWorld-1bba48.o
/cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift-autolink-extract tmp/HelloWorld-1bba48.o -o /tmp/HelloWorld-40659b.autolink
/usr/bin/clang++ -Xlinker -rpath -Xlinker /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin /tmp/HelloWorld-1bba48.o -L /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin -lswiftCore --target=x86_64-unknown-windows-cygnus @/tmp/HelloWorld-40659b.autolink -o HelloWorld

と出力されます。

実行テスト

$ ./HelloWorld
Hello World!

となります。

対話式インタプリタ

$ swift

とすると、対話式インタプリタが起動します。

備忘録

もし、shared library が見つからないと言うエラーが出たような場合、

cygcheck ./HelloWorld

とすると、どんな名前の shared library が見つからないかが、stderr に出力されます。ですので、

$ cygcheck ./HelloWorld 1>a 2>b

として、b のファイルをテキストエディタで見ればよいです。それは例えば、

cygcheck: track_down: could not find cygicui18n60.dll

cygcheck: track_down: could not find cygicuuc60.dll

のように出力されます。

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

cygwinでSwiftを使う方法。

はじめに

Windows 7 以降の 64BIT 版 Windows には、64BIT 版の cygwin の中に Swift をインストールして使うことが出来ます。

ただし、予め以下のもののインストールが必要です。

  • cygwin 64BIT バージョン。
  • cygwin 版 clang ver 8 パッケージ。
  • cygwin 版 libiconv パッケージ。x86_64-w64-mingw32 というもの。
  • cygwin 版 libicu60 (6.0.2-1) パッケージ。

インストールすると、少なくとも以下のコマンドが使えるようになります:

  • swiftc : *.swift から cygwin で実行できる native binary を生成するコンパイラ。
    print("Hello World") をコンパイルして実行できることを確認済みです。
    また、LLVM の可読的中間言語(IR) の *.ll を出力できることも確認済みです。
    swiftc は、バックエンドに clang と ld を使っています。

  • swift : 対話式インタプリタ(REPL)。
    1 + 2[ret] と入れると 3 と出ます。

インストール方法

インストール方法を手順を追って説明します。

Win7, 64BIT に 64BIT 版 cygwin を、GUI インストーラーを使ってインストールする。

以下で cygwin のインストールフォルダは、c:\cygwin64 とする。

cygwin に、以下のパッケージをインストールしておく:

  • clang の ver 8 のパッケージ。

  • libiconv 系パッケージ。x86_64-w64-mingw32 的な名前のもの。これがインストールされると、
    /usr/x86_64-w64-mingw32/sys-root/mingw/lib
    に libiconv.a と libiconv.dll.a がインストールされる。これは、swiftc コンパイラが使う。

  • Category: libs の Package: libicu60 (6.0.2-1) パッケージ。
    ICU とは、International Components for Unicode のこと。
    これをインストールすると、少なくとも C:\cygwin64\bin に cygicui18n60.dll, cygicuuc60.dll
    がインストールされる。これは、swiftc でコンパイルして生成された実行ファイルが起動されるときに動的ライブラリとして読み込まれる。

Swift の DL とインストール

https://swiftforwindows.github.io/news/2018/02/12/Swift-for-Windows-Cygwin-20180212/
https://github.com/tinysun212/swift-windows/releases/tag/swift-4.0.3+cygwin.20180212

にて、cygwin 用に非公式に公開されている swift-4.0.3.cygwin.20180212-bin.tar.gz を DL する。
例: K:\Commu\FromOther\cygwin\Swift\swift-4.0.3

tar -xvzf swift-4.0.3.cygwin.20180212-bin.tar.gz

のように展開すると、xxx/usr/bin にコマンド類が、xxx/usr/lib にライブラリができます。
ただし、xxx は、./swift-4.0.3.cygwin.20180212-bin となるはずです。

xxx/usr/bin には swift.exe と swiftc.exe が出来ている。

swiftc.exe がコンパイラで、swift.exe が対話式インタプリタである。

swiftc.exe は、*.ll に LLVM コードを出力することも出来る。

パスの設定

上記の xxx/usr/bin のフォルダのフルパスを、PATH 環境変数に追加しておきます。

swift-4.0.3.cygwin.20180212-bin.tar.gz を展開した場所が c:\xxx だった場合、以下の様になります。

$ export LIBRARY_PATH=/cygdrive/c/xxx/usr/bin:$PATH

環境変数の設定

clang のリンカ ld がライブラリの *.a, *.o をリンクする際に検索対象にする のパスを指定するため、cygwin の端末の中から、

$ export LIBRARY_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw/lib

とします。

テスト

以下の様なテストプログラムを作っておく:

HelloWorld.swift
print("Hello World!")

コンパイル

$ ./swiftc -v HelloWorld.swift

とすると、

Swift version 4.0.3 (swift-4.0.3+cygwin.20180212)
Target: x86_64-unknown-windows-cygnus
/cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift -frontend -c -primary-file elloWorld.swift -target x86_64-unknown-windows-cygnus -disable-objc-interop -module-name HelloWorld -o /tmp/HelloWorld-1bba48.o
/cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift-autolink-extract tmp/HelloWorld-1bba48.o -o /tmp/HelloWorld-40659b.autolink
/usr/bin/clang++ -Xlinker -rpath -Xlinker /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin /tmp/HelloWorld-1bba48.o -L /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin -lswiftCore --target=x86_64-unknown-windows-cygnus @/tmp/HelloWorld-40659b.autolink -o HelloWorld
となる。

対話式インタプリタ

$ ./swift

とすると、対話式インタプリタが起動する。

備忘録

もし、shared library が見つからないと言うエラーが出たような場合、

cygcheck ./HelloWorld

とすると、どんな名前の shared library が見つからないかが、stderr に出力される。なので、

$ cygcheck ./HelloWorld 1>a 2>b

として、b のファイルをテキストエディタで見ればよい。それは例えば、

cygcheck: track_down: could not find cygicui18n60.dll

cygcheck: track_down: could not find cygicuuc60.dll

のように出力される。

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

修羅の道 to iOSの電波強度取得

概要

2019年も残りあとわずか!
あと少しで、令和初の年越しを体験できそうですね。
そんな2019/12/1の、ユアマイスターアドベントカレンダー1日目です。
https://qiita.com/advent-calendar/2019/yourmystar
この記事では、iOSの電波強度を取得するために色々と調べたことをまとめます。
この記事の最新のiOSバージョンとは、iOS 13.2.3のことを指します。

道のり

以前は、iOSの電波強度を取得するために statusBar から情報を取得することができていました。
しかし、最新のiOSバージョンではは statusBar に signalStrength or RSSI の値は保有していないことがわかりました。。。

statusBarとは?

statusBar とはiPhoneの上部にある電波マークや電池のマークがある部分のことを言います。
今現在もアプリからstatusBarの色を変更させたり、wifiに接続されているかということは確認ができます。
最新のiOSバージョンでも、アプリからではなく端末からだとRSSI値が確認できます。その方法は下記の参考記事にあります。

現在、アプリから電波強度を取得するには

色々と探した結果、statusBarから情報を取得する以外に、NEHotspotHelperを用いる方法があるようです。 NEHotspotHelperはXcodeが用意してくれているAPIでSSIDの情報などを得ることができます。

注意点

NEHotspotHelperでSSIDの情報から電波強度を取得できると言いましたが注意点があります。

其の1 ユーザの許可が必要

SSIDの情報から位置情報などがわかるためユーザに許可を得る必要があります。こちらは、info.plistに以下を追加すればOKです。

<key>NSExtension</key>
<dict>
    <key>NSExtensionPointIdentifier</key>
    <string>com.apple.networkextension.app-proxy</string>
    <key>NSExtensionPrincipalClass</key>
    <string>MyCustomAppProxyProvider</string>
</dict>

其の2 無料アカウントではダメ

Xcodeの無料アカウントでは使えないAPIのようです。Apple Developer Programに登録して年間1万円ちょい支払う必要があります。

其の3 有料ユーザでも使えるかわからない?

スクリーンショット 2019-12-01 11.53.19.png
自分用のアプリとして作っているのでお金払って使えなかったら嫌だなと思い、Xcodeのサポートに問い合わせてみました。すると、AppleDeveloperProgramに登録したユーザでも使える訳では無いとのこと。使う旨をXcodeに伝え審査を通ると使えるようになるようです。審査の内容については聞くことはできませんでした。意外と1番の落とし穴かもしれません。

参考にした記事

SwiftでWiFi電波強度を取得する
著 p9KYcJ5V さん
https://qiita.com/p9KYcJ5V/items/718ffa057302beff757d

iPhone 電波マークを数字表示にして電波強度を分かり易くしよう♪
https://www.kototoka.com/entry/2014/07/31/apple-iphone-lte-3g-antennamark-suuji

[iOS 10] 接続中のWi-FiのSSIDを取得する
https://dev.classmethod.jp/smartphone/ios-10-cncopysupportedinterfaces/

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

iOSのSpotlight検索に自分のアプリ内コンテンツを検索対象にする方法

Spotlight検索とは・・・?

ホーム画面で下にスワイプしたりすると出るあれです。

スクリーンショット 2019-11-30 14.14.00.png

入力欄にキーワードを入力すると、各アプリのコンテンツなどから、一致するものを探してもらえるので、横断的にアプリのコンテンツを検索できるので便利です。

今回は、このSpotlight検索に、自分のアプリのコンテンツを検索結果に表示させる方法についての記事です。

TodayExtensionやリッチプッシュなどのように、追加で証明書などもいらず、簡単に実装できてアプリへの導線が増やせるので、やってみてはいかがでしょうか?

実装方法

1.準備

プロジェクトにMobileCoreServiceとCoreSpotlightのFrameworkを入れます。
スクリーンショット 2019-11-30 13.11.59.png

2.保存処理を実装する

今回は、例として映画の情報である以下のMovie構造体をSpotlight検索に保存してみましょう。

Movie.swift
struct Movie {

    /// 映画固有の識別番号
    let id: Int = 0

    /// 名前
    let title: String = "スパイダーマン"

    /// あらすじ
    let summary: String = "平凡な少年、ピーター・パーカーは放射能汚染された蜘蛛に噛まれたことで、超人的な能力を得てしまう・・・"

    /// 画像
    let thumbnail: UIImage? = UIImage(named: "spider")

    /// 役者名の配列
    let actorNames: [String] = ["トビー・マグワイア", "キルスティン・ダンスト", "ジェームズ・フランコ"]
}
SpotlightManager.swift
import Foundation
import MobileCoreService
import CoreSpotlight

final class SpotlightManager {

    func save(_ movie: Movie) {
        let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String)

        // ①タイトル
        attributeSet.title = movie.title

        // ②説明文
        attributeSet.contentDescription = movie.summary

        // ③画像
        attributeSet.thumbnailData = movie.thumbnail.pngData()

        // キーワード(表示されないが、タイトルや説明文に入ってない文言をここに入れておけば、検索した時に引っかかるようになる)
        attributeSet.keywords = movie.actorNames

        /* 
         uniqueIdentifierはAppDelegateで取り出すことができるので、
         Spotlight検索経由でアプリを開いた時のためのURLスキームを入れておく
        */
        let item = CSSearchableItem(
            uniqueIdentifier: "my-app://open/movie?id=\(movie.id)",          
            domainIdentifier: "my-app",
            attributeSet: attributeSet
        )
        CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil)
    }
}

実際の表示された際は以下の様になります。
スクリーンショット_2019-11-30_14_51_52.png

3.起動時の処理を実装する

AppDelegate.swift
import CoreSpotlight

extension AppDelegate {

    func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> 
Bool {
        // Spotlightで開かれたかどうかをチェックする
        switch userActivity.activityType {
        case CSSearchableItemActionType:
            return self.openApplicationFromSpotlight(userActivity)
        default:
            return false
        }
    }

    private func openApplicationFromSpotlight(_ userActivity: NSUserActivity) -> Bool {
        // userActivityからURLスキームを取得する
        guard let urlScheme = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else {
            return false
        }

        // URLスキームを開いた時の処理を実装する
        return true
    }
}

4.保存処理を作る

ViewController.swift
final class ViewController: UIViewController {

    let spotlightManager = SpotlightManager()

    private func show(_ movie: Movie) {
        self.spotlightManager.save(movie)
    }
}

5.確認する

titledescriptionkeywordに設定した文字列を検索欄に入力すると表示されます。
スクリーンショット 2019-11-30 14.19.11.png

部分検索でもヒットします。
スクリーンショット 2019-11-30 14.20.05.png

以上で、ざっくりとした実装は以上です。

おまけ

今回紹介した実装では、Spotlightに保存する画像をローカルのものを使用しましたが、URLで画像を取得して表示することも可能です。
例として画像キャッシュライブラリのNukeを使用した方法を記載しておきます。

SpotlightManager.swift
import Foundation
import MobileCoreService
import CoreSpotlight
import Nuke

func save(_ movie: Movie) {
    ImagePipeline.shared.loadImage(
        with: movie.thumbnailUrl,
        progress: nil) { response, _ in
            guard let thumbnail = response?.image else {
                return
            }

            let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String)

            attributeSet.title = movie.title

            attributeSet.contentDescription = movie.summary

            attributeSet.thumbnailData = thumbnail.pngData()

            attributeSet.keywords = movie.actorNames

            let item = CSSearchableItem(
                uniqueIdentifier: "my-app://open/movie?id=\(movie.id)",          
                domainIdentifier: "my-app",
                attributeSet: attributeSet
            )
            CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil)    
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSのSpotlight検索で自分のアプリ内コンテンツを検索対象にする方法

Spotlight検索とは・・・?

ホーム画面で下にスワイプしたりすると出るあれです。

スクリーンショット 2019-11-30 14.14.00.png

入力欄にキーワードを入力すると、各アプリのコンテンツなどから、一致するものを探してもらえるので、横断的にアプリのコンテンツを検索できるので便利です。

今回は、このSpotlight検索に、自分のアプリのコンテンツを表示させる方法についての記事です。

TodayExtensionやリッチプッシュなどのように、追加で証明書などもいらず、簡単に実装できてアプリへの導線が増やせるので、やってみてはいかがでしょうか?

実装方法

1.準備

プロジェクトにMobileCoreServiceとCoreSpotlightのFrameworkを入れます。
スクリーンショット 2019-11-30 13.11.59.png

2.保存処理を実装する

今回は、例として映画の情報である以下のMovie構造体をSpotlight検索に保存してみましょう。

Movie.swift
struct Movie {

    /// 映画固有の識別番号
    let id: Int = 0

    /// 名前
    let title: String = "スパイダーマン"

    /// あらすじ
    let summary: String = "平凡な少年、ピーター・パーカーは放射能汚染された蜘蛛に噛まれたことで、超人的な能力を得てしまう・・・"

    /// 画像
    let thumbnail: UIImage? = UIImage(named: "spider")

    /// 役者名の配列
    let actorNames: [String] = ["トビー・マグワイア", "キルスティン・ダンスト", "ジェームズ・フランコ"]
}
SpotlightManager.swift
import Foundation
import MobileCoreService
import CoreSpotlight

final class SpotlightManager {

    func save(_ movie: Movie) {
        let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String)

        // ①タイトル
        attributeSet.title = movie.title

        // ②説明文
        attributeSet.contentDescription = movie.summary

        // ③画像
        attributeSet.thumbnailData = movie.thumbnail.pngData()

        // キーワード(表示されないが、タイトルや説明文に入ってない文言をここに入れておけば、検索した時に引っかかるようになる)
        attributeSet.keywords = movie.actorNames

        /* 
         uniqueIdentifierはAppDelegateで取り出すことができるので、
         Spotlight検索経由でアプリを開いた時のためのURLスキームを入れておく
        */
        let item = CSSearchableItem(
            uniqueIdentifier: "my-app://open/movie?id=\(movie.id)",          
            domainIdentifier: "my-app",
            attributeSet: attributeSet
        )
        CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil)
    }
}

実際の表示された際は以下の様になります。
スクリーンショット_2019-11-30_14_51_52.png

3.起動時の処理を実装する

AppDelegate.swift
import CoreSpotlight

extension AppDelegate {

    func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> 
Bool {
        // Spotlightで開かれたかどうかをチェックする
        switch userActivity.activityType {
        case CSSearchableItemActionType:
            return self.openApplicationFromSpotlight(userActivity)
        default:
            return false
        }
    }

    private func openApplicationFromSpotlight(_ userActivity: NSUserActivity) -> Bool {
        // userActivityからURLスキームを取得する
        guard let urlScheme = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else {
            return false
        }

        // URLスキームを開いた時の処理を実装する
        return true
    }
}

4.保存処理を作る

ViewController.swift
final class ViewController: UIViewController {

    let spotlightManager = SpotlightManager()

    private func show(_ movie: Movie) {
        self.spotlightManager.save(movie)
    }
}

5.確認する

titledescriptionkeywordに設定した文字列を検索欄に入力すると表示されます。
スクリーンショット 2019-11-30 14.19.11.png

部分検索でもヒットします。
スクリーンショット 2019-11-30 14.20.05.png

以上で、ざっくりとした実装は以上です。

おまけ

今回紹介した実装では、Spotlightに保存する画像をローカルのものを使用しましたが、URLで画像を取得して表示することも可能です。
例として画像キャッシュライブラリのNukeを使用した方法を記載しておきます。

SpotlightManager.swift
import Foundation
import MobileCoreService
import CoreSpotlight
import Nuke

func save(_ movie: Movie) {
    ImagePipeline.shared.loadImage(
        with: movie.thumbnailUrl,
        progress: nil) { response, _ in
            guard let thumbnail = response?.image else {
                return
            }

            let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String)

            attributeSet.title = movie.title

            attributeSet.contentDescription = movie.summary

            attributeSet.thumbnailData = thumbnail.pngData()

            attributeSet.keywords = movie.actorNames

            let item = CSSearchableItem(
                uniqueIdentifier: "my-app://open/movie?id=\(movie.id)",          
                domainIdentifier: "my-app",
                attributeSet: attributeSet
            )
            CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil)    
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】nilってなんだろう?

nilってなんだろう?

 nilとは、値が存在しないことを示すものです。他の言語ではnillやnoneと言った名前の場合もありますよね。
 多くの言語で、変数や定数の初期化がされていない状態や参照先が存在しない状態を示す値として活用されています。一方で、初期化されていない値や参照先が存在しない値へのアクセスによる実行時エラーを招いてしまうという問題もありました。

Swiftでのnilの扱い

 Swiftでは、nilを許容する特別な型へしかnilを代入できない仕組みになっています。こうすることで、予期せぬところでnilが使用されず、上で述べたような実行時エラーをなくすということを実現しています。
 Swiftでnilを扱える代表的な型は、Optional型です。これは、WrappedのところにIntやStringなどの具体的な型を指定して使用します。Intがnilを扱えないのに対し、Optional型はIntとnilの両方を扱えます。下に例を示します。

optional.swift
let number: Int //Int型なのでnilは入れられない
let optionalNumber: Optional<Int> //nilを入れられる

number = nil //コンパイルエラー
optionalNumber = nil //OK

まとめ

 Swiftでは、基本的にはnilを扱えない仕様となっています。この仕様により、コンパイル時に値の有無を判断しています。これにより、意図していないところでnilが発生し、プログラムやアプリがクラッシュすることを未然に防いでいると言うことができるのではないでしょうか?

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

たった2日でマスターできるiOSアプリ開発 集中講座(xcode 9 swift4)

じゃんけんアプリの作成

準備

  1. xcodeをApple Storeからインストールする(最新はXcode11)
    https://apps.apple.com/jp/app/xcode/id497799835?mt=12

  2. Gitlabにアカウントを作成する
    https://gitlab.com/

  3. プロジェクトの作成
    XcodeでCreate new project → Single View Appを選択する
    ProductNameを適当につける

  4. GitLabと連携
    Projectを作成するとGitのレポジトリーが自動で生成されている
    リモートレポジトリを追加する

git remote add origin https://gitlab.com/****/****
git add .
git commit -m "Initial commit"
git push -u origin master

実装

ストーリーボードに部品を追加する

    @IBOutlet weak var answerImageView: UIImageView! 
    @IBOutlet weak var answerLabel: UILabel!
    // じゃんけん数字
    var answerNumber = 0

ボタンが押された時にじゃんけん画像を切り替える
(じゃんけんの画像は本にあるリンクからダウンロードができる)

    @IBAction func shuffleAcction(_ sender: Any) {

        // 新じゃんけんの結果を一時的に格納する変数
        var newAnswerNumber = 0

        // ランダムに結果を出すが、前回と異なる場合のみ採用
        repeat {
            // 0,1,2の数値をランダムに算出(乱数)
            // arc4ramdom_uniform()の戻り値はUInt32だがswiftの標準的な整数型にキャスト(変換する)
            newAnswerNumber = Int(arc4random_uniform(3))

        } while answerNumber == newAnswerNumber

        answerNumber = newAnswerNumber

        if answerNumber == 0 {
            // グー
            answerLabel.text = "グー"
            answerImageView.image = UIImage(named: "gu")
        } else if answerNumber == 1{
            // チョキ
            answerLabel.text = "チョキ"
            answerImageView.image = UIImage(named: "choki")
        } else if answerNumber == 2 {
            // パー
            answerLabel.text = "パー"
            answerImageView.image = UIImage(named: "pa")
        }

        // 次のコードの削除
        answerNumber = answerNumber + 1
    }

注意

Xcodeを画面分割(Assistant Editor)モードを使うときは、メニューバーのEditor→Assistantを選択する
Xcode11とXcode9では少し画面分割の呼び出し方が違う

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

AVAudioSessionの深い話

この歳になると、アドベントカレンダーを毎日書くやる気はないので毎週1つ記事あげます。
今回はiOSでおなじみのAVAudioSessionの話です。
AVAudioSessionでは端末のオーディオの振る舞い方を制御することが出来ますが、あまり知られていない機能がいくつかあるのでご紹介します。

Mode

https://developer.apple.com/documentation/avfoundation/avaudiosession/mode
通常はdefaultを選択すると思いますが、このモードは振る舞い方が一部変わります。
例えばvoiceChat/videoChatを選ぶと、Voice Processing I/Oを使うようになるので、ノイズキャンセリングが有効になるため、VoIPで使うケースでは大変重宝します。ちなみにVPIOを有効にすると、iPad Proの場合はマイクに近いほうのスピーカーから音が出なくなりますが仕様みたいです。
measurementは名前の通り測定するときに使います。DSPなどは無効化され、信号処理をしやすくするためにするみたいです。
moviePlaybackは動画再生のときに使います。これを使うと、端末のスピーカーでステレオ再生が強化されます。例えばiPhone Xを横にすると左右のスピーカーから再生されますが、そういったサラウンド感を出すときに使うみたいです。
spokenAudioは途中で音声の割り込みがあるときに使います。一番わかりやすいのはカーナビゲーションの音声案内のときに再生中のソースの音量を一時的に下げて、音声案内を聞き取りやすくするとかでしょうか。
videoRecordingは動画収録をするときに使います。これは背面マイクを優先的に使うようになります。

Category

https://developer.apple.com/documentation/avfoundation/avaudiosession/category
ambientは、他のアプリが音楽再生中であっても、その音楽は停止しません。スクリーンロックやサイレントにした場合にはこのアプリが再生するサウンドは聞こえなくなります。
playbackは、主に再生で使います。アプリを起動していると他のアプリの音楽は停止されます。サイレントスイッチオンまたはスクリーンロックでも再生は継続されます。
recordは、主に録音で使います。サウンドの再生は出来ません。ユーザーが録音する場合は権限を付与する必要があります。
playAndRecordは録音と再生の両方を行います。主にVoIPなどで使われます。他のアプリで再生しながら録音するには、AVAudioSessionCategoryOptionMixWithOthersを使う必要があります。
multiRouteは複数の出力デバイスにルーティングするときに使います。主にUSB Audioと本体のスピーカーで同時に流したりする場合などに使われます。

setPreferredSampleRate

https://developer.apple.com/documentation/avfoundation/avaudiosession/1616523-setpreferredsamplerate
サンプリングレートを指定できます。接続しているデバイスによって、サンプリングレートは変わってくるので、固定してあげたほうが良いでしょう。
ただ、あくまでも優先するだけであって、必ずしもサンプリングレートを固定できるわけではないようです。

setPreferredDataSource

https://developer.apple.com/documentation/avfoundation/avaudiosessionportdescription/1616554-setpreferreddatasource
使うマイクを指定出来ます。先ほどAVAudioSession.Modeの話をしましたが、どうしてもこのマイクを使いたいというときには明示的に指定することができます。
AVAudioSessionDataSourceDescriptionと組み合わせて使います。

setPreferredInput

https://developer.apple.com/documentation/avfoundation/avaudiosession/1616491-setpreferredinput
上と似ていますが、こちらは使うポートを指定できます。どういうことかというと、例えばUSB Audioを優先的に使いたい場合はこちらで指定することができます。setPreferredDataSourceがマイクの向きに大して、setPreferredInputはデバイスを選択するというイメージですね。
こちらはAVAudioSessionPortDescriptionと組み合わせて使います。

overrideOutputAudioPort

https://developer.apple.com/documentation/avfoundation/avaudiosession/1616443-overrideoutputaudioport
出力ルートを強制的に上書きすることができます。例えば強制的に内臓スピーカーで音を鳴らしたい場合に使うことができます。


今回紹介したもの以外にもAVAudioSessionには色々な機能があります。
みなさんもAVAudioSessionをマスターして、良いクリスマスをお過ごしください :)

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

Delphi で Pull To Refresh

PullToRefresh

Delphi で簡単に PullToRefresh しようよ~(ジョイマン風に)

PullToRefresh とは

上から下に引っ張ると表示内容が更新される機能です。

ListView PullToRefresh

TListView には PullToReflesh が機能として提供されています。

PullToRefresh の所にも書いてありますが、使い方は

  1. PullToRefresh プロパティを True にする
  2. OnPullRefresh イベントハンドラに更新する内容を記載する
  3. 更新が終わったら StopPullReflesh を呼ぶ

という感じです。

Object.png

OnPullRefresh
procedure TfrmMain.ListView1PullRefresh(Sender: TObject);
begin
  CreateRandomValue; // ListView にランダムな値を設定するメソッド
  ListView1.StopPullRefresh; // iOS の場合に待機アニメーションが停止される
end;

また PullRefreshWait というプロパティがあります。
このプロパティは iOS でしか効果がありません。
このプロパティを True にすると待機アニメーションが自動的に止まるようになります。
このプロパティを False にした場合は、コードで示したように StopPullRefresh を手動で呼び出します。
本来、データの更新というのは時間が掛かる物なので、PullRefreshWait を True にする機会は少ないかも知れません。

実際に動かすとこんな感じ

iOS
P2RSample.gif

Android
Xperia.gif

ListView 以外ではどうする?

ここからが本題!
ListView 以外の時にこの動作をさせたくても、そんなコントロールは無いのでした。

ということで Top に TLayout を置いて OnMouseDown, OnMouseMove, OnMouseUp で引っ張り出せるようにしてみました。
image.png

利点

この方法の利点は全部の OS で使えるということです。
先に上げた TListView の方法だとモバイルだけ。しかも Android の表示はそもそも Pull 感が無くて超解りづらい…

欠点

引っ張り出せるようにするために上部に少しコントロールを出しておかないといけない、というところです。
なので、画面上部にユーザーが操作する UI を置いておくことができません。

実装

これを実装した PK.GUI.PullToRefreshLayout を作成しました。
TPullToRefreshLayout を使うコードは↓こんな感じ

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  FP2RLayout := TPullToRefreshLayout.Create(Self); // FP2RLayout は TPullToRefreshLayout として定義
  FP2RLayout.MaxPullingLen := 80; // どこまで引っ張り出せるか

  FP2RLayout.BoundsRect := Layout1.BoundsRect; // "PullToRefresh Sample" とあるエリアと同じ位置にする
  FP2RLayout.Parent := Self;
  FP2RLayout.BringToFront; // 最前面に持ってきて触れるようにする

  Path1.Parent := FP2RLayout; // 矢印を表示している Path を載せる

  FP2RLayout.OnRefresh := P2RLayoutRefresh; // イベントハンドラ

  CreateRandomValue;
end;

procedure TfrmMain.P2RLayoutRefresh(Sender: TObject);
begin
  CreateRandomValue; // 更新
end;

コレを使うと↓のような動作になります!

Windows
P2RSample.gif

macOS
P2RSample.gif

iOS
P2RSample.gif

Android
P2RSample.gif

まとめ

元から用意されている場合はそれを使って、用意されていない場合は工夫することが大事ですね!

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