20190203のiOSに関する記事は10件です。

【iOS 12】フラットな絵文字 EmojiOne を iOS で使ってみる

iOS 標準の絵文字をこんなかんじのフラットな絵文字に置き換えてみます。

iOS 標準の絵文字 EmojiOne の絵文字
スクリーンショット 2019-02-03 18.35.38.png スクリーンショット 2019-02-03 18.35.05.png

この絵文字は、EmojiOne によって無料で提供されているものです。
iOS 12 以降では、EmojiOne をカンタンにアプリに組み込むことができるみたいです。

iOS 12 から OpenType-SVG をサポートしたらしい

iOS 12 から OpenType-SVG 形式のカラーフォントをサポートしたみたいです。
UIKit や TextKit に関する公式なアナウンスは見つけることが出来なかったのですが、What’s New in Safari には次の記載がありました。

  • OpenType SVG
    • Added support for defining letterforms in OpenType fonts using SVG.

WebKit は CoreText に依存しているらしいので、もしかしたら CoreText レベルでの変更によって、TextKit にも影響がでたのかもしれません。

ところで、OpenType-SVG とは、

OpenType-SVG は、OpenType フォントの字形のすべてまたはほんの一部が SVG(Scalable Vector Graphics)アートワークとして表されるフォント形式です。これにより、1 つの字形で複数のカラーおよびグラデーションを表示できます。こうした機能から、OpenType-SVG フォントを「カラーフォント」とも呼んでいます。

OpenType-SVG カラーフォント – Adobe

とのことで、1文字に複数のカラーを表示することできるフォントみたいです。

EmojiOne フォントをアプリに組み込む

アプリにカスタムフォントをバンドルすることで EmojiOne を利用することが出来ます。
まずは EmojiOne フォントをダウンロードします。

EmojiOne SVG-based Color Fonts で Open Type Font: emojione-svg.otf のリンクからダウンロードします。

スクリーンショット_2019-02-03_21_40_19.png

ダウンロードした emojione-svg.otf を Xcode プロジェクトにドラッグ&ドロップします。
次のようにチェックして、リソースに追加します。

AppDelegate_swift.png

これで、アプリ内で EmojiOne を利用できるようになります。

インターフェースビルダーから使用する

フォントを次のように指定します。

  • Font: Custom
  • Family: EmojiOne

スクリーンショット_2019-02-03_21_58_47.png

コードからは使用できない?

PostScript名 を指定して、

label.font = UIFont(name: "EmojiOneColor", size: 38)

あるいは、フォントファミリーを指定して

label.font = UIFont(name: "EmojiOne", size: 38)

これで動くはず。
と思ったのですが、なぜかシステムフォントにフォールバックしてしまいました。
なので、今回はコードからのフォントの設定は諦めました。

iOS 11 で実行すると?

OpenType-SVG はカラーのベクタデータだけでなく、モノクロのベクタデータも持っています。
iOS 11 の場合はモノクロのフォントにフォールバックするようです。

iOS 12 iOS 11
スクリーンショット 2019-02-03 18.35.05.png スクリーンショット 2019-02-03 18.33.50.png

EmojiOne の使用許諾契約について

クリエイティブ・コモンズ・ライセンスです。
リンクを貼ることで利用できます。

EmojiOne's graphics are free to use for any project, commercial or personal, under a free culture Creative Commons License (CC-BY 4.0). Proper attribution (link back) is required for the rights to use the emoji in commercial projects.

こちらの絵文字は EmojiOne によって無料で提供されているものを利用しました。

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

react-native+TypeScriptなプロジェクトでdotenvを読ませる

https://github.com/zetachang/react-native-dotenv

まずは追加。

yarn add react-native-dotenv

.babelrcに以下追加。

".babelrc"
...
  "presets": [..., "module:react-native-dotenv"],
...

次に、react-native-dotenvには型定義ファイルがないので自前でdeclareする必要がある。
./src/lib/vendor-typings.d.tsにモジュールを定義。

vendor-typings.d.ts
// declare module 'react-native-dotenv';

起動してみる。これで動くはず。

app.tsx
import * as React from 'react'
import { Text } from 'react-native'
import {
     YOUR_ENV,
} from 'react-native-dotenv'

export default = (): JSX.Element => {
    return <Text>{YOUR_ENV}</Text>
}

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

iOSアプリとTwitter連携

はじめに

Social.frameworkまたはUIActivityViewControllerが使えなくなったしまったので、TwitterKit3.4.0で、ツィートするところまで作ってみました。

手順は

  • Twitter Developersでアプリ登録(本記事では省略)
  • TwitterKitのインストール
  • プロジェクトの設定
  • 認証部実装
  • ツィート実行部実装

で、実装しました。

TwitterKitのインストール

今回はCocoaPodsを使用しました。

Podfileは

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'twitterKitSmp' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for twitterKitSmp
  pod 'TwitterKit'  <-- 追加
end

のような感じです。

プロジェクトのディレクトリで

% pod init
<Podfileが作成されるので、pod 'TwitterKit'を追加>
% pod install

TwitterKitのインストールは以上です。

プロジェクトの設定

info.plistに下記の追加を行います。

<key>CFBundleURLTypes</key>
  <array>
    <dict>
      <!-- 追加ここから -->
      <key>CFBundleURLSchemes</key>
      <array>
        <string>twitterkit-{CONSUMERKEY}</string>
      </array>
      <!-- 追加ここまで -->
    </dict>
  </array>
  <!-- 追加ここから -->
  <key>LSApplicationQueriesSchemes</key>
  <array>
    <string>twitter</string>
    <string>twitterauth</string>
  </array>
  <!-- 追加ここまで -->

CFBundleURLTypes.CFBundleURLSchemesとLSApplicationQueriesSchemesを追加します。
CFBundleURLSchemesはTwitterAPI側のCallbackURLにも追記します。

認証部実装

まず、TwitterKitの初期設定等を行います。

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions
                 launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    // 1. TwitterAPIのAPI keyとAPI secret keyを設定
    TWTRTwitter.sharedInstance().start(withConsumerKey: "{CONSUMERKEY}",
                                       consumerSecret: "{CONSUMERSECRET}")
    return true
  }

  func application(_ app: UIApplication, open url: URL, 
                   options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    // 2. Twitter認証からのコールバック
    if TWTRTwitter.sharedInstance().application(app, open: url, options: options) {
      return true
    }

    return false
  }
  1. アプリ起動時にTwitterAPIのAPI keyとAPI secret keyを設定します。
  2. Twitter認証からのコールバックを受け取ります。

次に認証開始部分を実装します。  

ViewController.swift
func sndTweet() {
  // 1.ログインされているか?
  if TWTRTwitter.sharedInstance().sessionStore.hasLoggedInUsers() {
    // 2.ツィート開始
    sndTweetExec()
  } else {
    // 3.認証開始
    TWTRTwitter.sharedInstance().logIn(with: self, completion: { (session, error) in
      if let sess = session {
        print("Signed in as \(sess.userName)")
        // 4.ツィート開始
        self.sndTweetExec()
      } else {
        // 5.認証失敗
        print("login error: \(error?.localizedDescription)")
      }
    })
  }
}
  1. Twitterにログインされているか確認します。
  2. 既にログインされているため、ツィート開始へ進みます。
  3. Twitter認証を開始します。Twitterのログイン画面に遷移します。ログイン後、アプリの連携を許可するか聞かれるので許可すればアプリへ戻ります。
  4. 認証が成功したのでツィート開始へ進みます。
  5. 認証失敗したのでここで終わります。

ツィート実行部実装

ツィートはTwitterKitのTWTRComposerViewControllerを使用しました。

またツィート実行後にアラートを出すように実装します。

ViewController.swift
func sndTweetExec() {
  let str:String = "サンプルツィート"

  // 1.コントローラー初期化
  let comp = TWTRComposerViewController.init(initialText: str, image: nil, videoData: nil)

  // 2.デレゲート
  comp.delegate = self

  // 3.コントローラ表示
  present(comp, animated: true, completion: nil)
}
  1. TWTRComposerViewControllerを初期化します。今回はテキストのみ。
  2. デレゲートを設定します。デレゲートはキャンセルとツィート成功、失敗が定義されています。
  3. コントローラーを表示します。

次にデレゲートの処理を実装します。

ViewController.swift
extension ViewController: TWTRComposerViewControllerDelegate {
  // キャンセル時
  func composerDidCancel(_ controller: TWTRComposerViewController) {
    print("Cancel")
  }

  // ツィート失敗時
  func composerDidFail(_ controller: TWTRComposerViewController, withError error: Error) {
    print("Error")
    let store = TWTRTwitter.sharedInstance().sessionStore
    if let userID = store.session()?.userID {
      store.logOutUserID(userID)
    }
    dismiss(animated: false, completion: nil)
    DispatchQueue.main.async {
      self.tweetAlert(memo:"Twitterに投稿に失敗しました")
    }
  }

  // ツィート成功時
  func composerDidSucceed(_ controller: TWTRComposerViewController, with tweet: TWTRTweet) {
    print("Ok")
    dismiss(animated: false, completion: nil)
    DispatchQueue.main.async {
      self.tweetAlert(memo: "Twitterに投稿しました。\nご協力ありがとうございます。")
    }
  }

}

今回はツィート実行後にアラートを表示したいため、デレゲートメソッド内でdismiss()をコールして、Viewを閉じています。

DispatchQueue.main.asyncは必要なかったかも知れませんが、念のため。。

サンプルアプリ

こんな感じのアプリにしてみました。

ソースコード
にソース一式を置きました。

まとめ

以上、ざっとまとめましたが、木になる点として

  • CallbackURLが指定できなかった。
    指定方法がありそうな気がしましたが、見つけられませんでした。
  • ツィート画面のカスタマイズしたい。
    できそうな感じがしたのですが、今回はできませんでした。
    次回、機会があれば再トライしてみます。

なお、下記の記事を参考にさせていただきました。ありがとうございました。

https://qiita.com/naoto0n2/items/8d80dde2584d970317e7

iOS11のTwitter投稿対応(Social.framework → TwitterKit)

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

ようやくGUI! SwiftでUIKitのLabel部品でHello World

はじめに

ずっとPlaygroundでコンソール(?)プログラミングをしてきた。
Swiftのコード文法てきな部分はわかってきたので
統合開発環境としてのXCodeの使い方を含めて少しずつ挑戦の幅を広げる。

手順概略

GUIアプリになると、途端に取り扱うファイルが増えるので
コードを書くだけでは解決しなくなる。
ここではざっくり手順を載せる。

  1. [Main.storyboard]を開く
  2. ラベルを配置する
  3. [アシスタントエディタ]で配置したラベルをソースコードへ取り込む
ラベルの定義
@IBOutlet weak var label: UILabel!
  1. ソースコード上でラベルにテキストを設定する
ラベルにテキストを設定
label.text = "Hello World"

ソースコード全文

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        label.text = "Hello World"//<-手打ちで追加
    }

    @IBOutlet weak var label: UILabel!//<-アシスタントエディタで追加
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mac で開発中のサイト(localhost)を iPhone で表示させる

http://localhost:3000
というような今開発しているローカルサイトを手元にある iPhone ですぐに見られるようにします。
基本方針は Bluetooth を使ったインターネット共有で、ハード・ソフト面で他に何も準備しないバージョン。
https の対応はまた今度。

環境

  • MBP
    • Wifi でインターネット接続
  • iPhone
    • 特別な設定なし
  • 有線 LAN なし
  • ケーブル類出したくない

方法

1: システム設定 -> 共有 を開きます

Screen Shot 2019-02-03 at 21.28.56.png

2: インターネット共有を設定します

Screen Shot 2019-02-03 at 21.34.13.png

右側の Bluetooth を選んで左側のインターネット共有をオン。
上の下線を引いてある (pcの名前).local をメモ。

3: iPhone から Bluetooth で Mac に接続

4: iPhone のブラウザを開いてアドレス欄に http://(pcの名前).local を入力してアクセス

http://localhost:3000 で開発しているなら http://(pcの名前).local:3000 を入力。

5: 見れます

備考

  • Mac の Wifi が余ってるなら Bluetooth じゃなくてそっちで繋げられます
  • というか同じ Wifi 繋げるなら IP 直接打てばいいみたいなところもあります
    • 同じネットワークに繋げない特殊環境で役立ててください
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスタブルコードの研究

はじめに

おみくじクラスを作ってみた でおみくじを引く関数を作った。
動作は問題ないが、よりテスタブルなコードへ書き換える。

検討

現在のソースコードは以下の通り。

元コード
func get() -> String {
    let result = base.randomElement()!      //ランダムに配列要素を取り出す
    let dateStr = toDateStr(date: Date())   //現在時刻を文字列へ変換
    history.append(dateStr + "  " + result) //履歴配列へ[結果+日時]を追加
    return result                           //おみくじ結果を返す
}

テストがやりにくい点は以下の点である。

  1. resultは値がランダム
  2. dateStrは値が実行時に変化する
  3. historyはメンバ変数

resultのランダムは必須機能なので仕方ない。
それ以外はget関数から追い出す。

get関数から履歴記録部分を別関数へ分離
func get() -> String {
    let result = base.randomElement()! //ランダムに配列要素を取り出す
    setHistory(result: result)         //履歴を記録
    return result                      //おみくじ結果を返す
}

さらに履歴文言の生成は別関数へ追い出し、必要な情報を渡すようにする。

履歴文言の生成を追い出す
func setHistory(result: String) {
    //履歴記録用文字列の生成
    let dateStr = toHistoryString(result: result, date: Date())

    //履歴配列へ結果を追加
    history.append(dateStr + "  " + result)                   
}

履歴文言の生成は、外部の情報を元に組み立てる関数に閉じる

履歴文言生成関数
func toHistoryString(result: String, date: Date) -> String {
    let dateStr = toDateStr(date: date) //時刻を文字列へ変換
    return (dateStr + "  " + result)    //履歴配列へ[結果+日時]を返却
}

結果

今回はテストコードまでは書かなかったが
履歴文言生成関数は、入力が決まれば出力が一意に決まる「テスタブル」なコードができた。

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

AR顔認識アプリを開発して原宿で街頭インタビューしてYouTubeにアップした話

2019年1月からエンジニア系YouTubeを始めました。今YouTubeにあるエンジニア系の動画は「エンジニアは稼げる」とか「フリーランスエンジニア自由最高」みたいな動画が多く、それはそれいいんですが、技術で攻めて駆逐せねばならぬという僕の謎の正義感が働いているため頑張っています。

基本的にはARやエンジニアの生活について発信するチャンネルですが、今回はいかにもYouTuberっぽい動画且つエンジニアらしくアプリも作ったのでその話を紹介していきたいと思います。

企画

先輩YouTuberでxRエンジニア界隈の友人である @nkjzm と一緒に企画を考えました。

要件は2つ。

  • K-BOYがARエンジニアなのでARに沿った内容にしたい
  • せっかくコラボするのでインパクトがある内容にしたい

この要件のなかで昨日の13:30-14:00くらいでディスカッションし、14:00-15:00でアプリ開発と撮影準備をし、原宿に飛び出したわけであります。

インパクトを残すという意味で少し下ネタを絡めるというテーマでディスカッションしていました。目的は「IT界隈じゃない人も興味を持ってくれる動画にしよう!」だったので、ギリギリ大丈夫そうな下ネタの経験人数というテーマにしました。

そしてARを絡めるために最終的にたどり着いたのが「ARKitの顔認識を使って顔を読み取って経験人数を予測するアプリ

この企画プロセスはハッカソンっぽいなあと思います。

アプリ仕様

GIFイメージ-818991CA90C7-1.gif

ARKitのFaceDetectionを使って顔をスキャン感を出す

詳しくはコードをご覧ください。

iPhoneX等で使えるフロントカメラの顔認識を使用しています。

顔のgeomertyをupdateし続けて、あみあみのmaterialを張っているだけで、サンプルコードレベルの実装です。

ゲージをアニメーションさせてスキャン感をだす

UIProgressViewをいじったカスタムクラスを以前作ったことがあったので、同じように再現実装しました。

起動から5秒後に、10秒かけてアニメーションさせてます。

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    self.titleLabel.isHidden = false

    UIView.animate(withDuration: 5, delay: 0, options: [], animations: {
        self.hpView.hpBar.setProgress(1, animated: true)
    }, completion: nil)

    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
        self.titleLabel.text = "経験人数は..."
        self.countLabel.isHidden = false
    }
}

経験人数は適当に配列で用意

もちろん、ネタ企画なので経験人数は配列に入れときます。最速で実装し、原宿に飛び出して使ってもらうのが目的なため、複雑なアルゴリズムを作っている暇はありません。リアルっぽさを出すために少ない人数を多めにしときました。

let array: [String] = [
    "0人",
    "1人",
    "2人",
    "3人",
    "4人",
    "5人",
    "6人",
    "7人",
    "8人",
    "11人",
    "12人",
    "14人",
    "18人",
    "19人",
    "22人",
    "28人",
    "35人",
    "100人",
    "160人",
]

このarrayの中からrandomで一個選びます

countLabel.text = self.array.randomElement()!

ユーザーの反応

スクリーンショット 2019-02-03 16.17.27.png

詳しくはYouTubeを見てもらいたいと思いますが、ウケてました。本当にアルゴリズムがあるかどうかは重要ではないということがわかりました。

「違うよ!」「え!当たってる!」など反応を楽しむことが目的。

その目的は達成できたのではないかなと思います。嬉しいです。

動画制作

  • 撮影はiPhone XS Maxで1時間半
  • 動画編集はFinal Cut Proで4時間くらい
  • サムネはAdobe Illustlatorで30分くらい

まとめ

アプリを作ってインタビュー、それを動画にするというエンジニアらしいYouTube制作ができたのではないかと思います。

エンジニアといえばブログのイメージが強いですが、より多くの層にリーチするためにYouTubeを撮ってみるのはいかがでしょうか?想像以上にクリエイティブな作業で、編集の大変さがわかると思います。

では!

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

[iOS] アプリの画面の方向を固定させる方法メモ

環境

  • macOS Mojave 10.14.3
  • Xcode 10.1

アプリの画面の向きを決める方法

Xcodeのアプリの設定でできます。チェックボックスのチェックを入れるだけ。

  1. 左サイドバーのプロジェクト名を選択
  2. 「TARGETS」にありプロジェクト名を選択
  3. 「General」タブを選択
  4. 「Deployment Info」にある「Device Orientation」にあるチェックボックスのチェックをつけたり外したりする。

スクリーンショット 2019-02-03 16.17.52.png

 参考

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

[iOS] [Swift] Sequence Protocolのサンプルコード(社内勉強会資料)

はじめに

弊社内の勉強会で、Sequence Protocolの中でも頻繁に使うモノをサンプルコード付きで紹介しました。
ごくシンプルなコードですが、せっかく書いたものを社内だけにとどめることは勿体無いので公開いたします。

参加者に「お題」だけを配って(答えを削って)、ハンズオンなどをやってみると楽しいと思います!:grinning:

環境

・Xcode 10.1
・Swift 4.2

サンプルコード

Sequence.playground
import UIKit

struct Student {
    var code: String
    var firstName: String
    var lastName: String
    var score: Int
    var fullName: String {
        return firstName + " " + lastName
    }
}

var students = [Student]()
students.append(Student(code: "AB100000", firstName: "Taro", lastName: "Yamada", score: 62))
students.append(Student(code: "AB100001", firstName: "Ichiro", lastName: "Suzuki", score: 81))
students.append(Student(code: "AB100002", firstName: "Hanako", lastName: "Sato", score: 96))
students.append(Student(code: "AB100003", firstName: "Jiro", lastName: "Takahashi", score: 58))
students.append(Student(code: "AB100004", firstName: "Ichiro", lastName: "Tanaka", score: 75))
students.append(Student(code: "AB100005", firstName: "Hanako", lastName: "Yamada", score: 96))

// 70点以上の生徒を抽出する
let filtered = students.filter { (student) -> Bool in
    return student.score >= 70
}
for (i, student) in filtered.enumerated() {
    print("70点以上の生徒\(i+1)人目は\(student.fullName)です。")
}
/*
70点以上の生徒1人目はIchiro Suzukiです。
70点以上の生徒2人目はHanako Satoです。
70点以上の生徒3人目はIchiro Tanakaです。
70点以上の生徒4人目はHanako Yamadaです。
*/

// "コード, フルネーム"という形式の文字列の配列に変換する
let stringArray = students.map { (student) -> String in
    return "\(student.code), \(student.fullName)"
}
stringArray.forEach {
    print($0)
}
/*
AB100000, Taro Yamada
AB100001, Ichiro Suzuki
AB100002, Hanako Sato
AB100003, Jiro Takahashi
AB100004, Ichiro Tanaka
AB100005, Hanako Yamada
*/

// 全生徒の点数を合計する
let total = students.reduce(0) { (total, student) -> Int in
    return total + student.score
}
print("全生徒のScoreの合計は\(total)点です")
/*
全生徒のScoreの合計は468点です
*/

// key=コード、value=フルネームというDictionaryに変換する
let dic = students.reduce(into: [String: String]()) { (dic, student) in
    return dic[student.code] = student.fullName
}
dic.forEach {
    print($0)
}
/*
(key: "AB100000", value: "Taro Yamada")
(key: "AB100001", value: "Ichiro Suzuki")
(key: "AB100003", value: "Jiro Takahashi")
(key: "AB100004", value: "Ichiro Tanaka")
(key: "AB100005", value: "Hanako Yamada")
(key: "AB100002", value: "Hanako Sato")
*/

// Satoが含まれているか?
let existsSato = students.contains { (student) -> Bool in
    return student.lastName == "Sato"
}
print("Satoさんが含まれているか = \(existsSato)")
/*
Satoさんが含まれているか = true
*/

// Yamadaさんの1件目を取得する
let first = students.first { (student) -> Bool in
    return student.lastName == "Yamada"
}
if let first = first {
    print(first.fullName)
} else {
    print("見つかりませんでした")
}
/*
Taro Yamada
*/

// FirstName>LastName順にソートする
let sorted = students.sorted { (student1, student2) -> Bool in
    if student1.firstName == student2.firstName {
        return student1.lastName < student2.lastName
    } else {
        return student1.firstName < student2.firstName
    }
}
for (i, student) in sorted.enumerated() {
    print("FirstName>LastName順の生徒\(i+1)人目は\(student.fullName)です。")
}
/*
FirstName>LastName順の生徒1人目はHanako Satoです。
FirstName>LastName順の生徒2人目はHanako Yamadaです。
FirstName>LastName順の生徒3人目はIchiro Suzukiです。
FirstName>LastName順の生徒4人目はIchiro Tanakaです。
FirstName>LastName順の生徒5人目はJiro Takahashiです。
FirstName>LastName順の生徒6人目はTaro Yamadaです。
*/

勉強会で使えるリファレンス

サンプルコードで紹介した以外にも、便利なメソッドはたくさんあるかと思います。

SwiftDoc.org は、
・サンプルコードが載っている
・一覧性が優れていて探しやすい
ことから、勉強会の教材として使いやすいと思います。

Sequence Protocolの項はこちらになります。
https://swiftdoc.org/v4.2/protocol/sequence/

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

RxCocoa の UITextField.rx.text を購読するとリターンキーでキーボードが閉じるようになる

はじめに

RxCocoa で UITextField の入力値をイベントストリームとして受け取る場合、UITextField.rx.text を使います。

import UIKit
import RxSwift
import RxCocoa

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    private let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        textField.rx.text.subscribe().disposed(by: bag)
    }
}

このように UITextField.rx.textsubscribe() すると、入力値をイベントストリームとして受け取るだけでなく、なぜか、キーボードのリターンキーのタップでキーボードが閉じるようになりました。

output.gif

RxCocoa だけでなく、ReactiveCocoa でも似たような挙動になります。

ReactiveCocoa では、次のようにして UITextField の入力値をイベントストリームとして受け取る事ができ、やはり、キーボードのリターンキーのタップでキーボードが閉じるようになります。

import UIKit
import ReactiveSwift
import ReactiveCocoa

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        textField.reactive.continuousTextValues.observe { _ in }
    }
}

なぜキーボードが閉じるのか?気になったので、調べてみました。

UITextField.rx.text の実装の概要

RxCocoa の rx.text の実装 を追っていくと、次のような実装であることが分かりました。

  • UIControl.Events の .allEditingEvents.valueChanged をターゲットアクションに追加する
  • アクションが送信されると、UITextField の text プロパティを RxSwift のイベントストリームとして発行する

また、ReactiveCocoa の reactive.continuousTextValues の実装 では .allEditingEvents をターゲットアクションに追加するようでした。

リターンキーの入力でキーボードが閉じるようになる理由

RxCocoa に限らず、.allEditingEvents をターゲットアクションに追加すると、リターンキーの入力によってキーボードが閉じるようになります。

次のようなコードで確認することが出来ます。

import UIKit

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // リターンキーの入力でキーボードが閉じるようになる
        textField.addTarget(self, action: #selector(self.hoo(sender:)), for: .allEditingEvents)
    }

    @objc func hoo(sender: Any) {
    }
}

.allEditingEvents は UITextField の全ての編集イベントを含んでいるので、.editingDidEndOnExit も含んでいます。

.editingDidEndOnExit をターゲットアクションに追加すると、リターンキーの入力でファーストレスポンダをやめるようになります。これが本質的な理由です。

次のようなコードに置き換えると .editingDidEndOnExit イベントが送信されないので、リターンキーをタップしてもキーボードは閉じません。

import UIKit

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // リターンキーをタップしてもキーボードは閉じない
        var events = UIControl.Event.allEditingEvents
        events.subtract(.editingDidEndOnExit)
        textField.addTarget(self, action: #selector(self.hoo(sender:)), for: events)
    }

    @objc func hoo(sender: Any) {
    }
}

UITextField.rx.text でキーボードが閉じる処理を抑止するには

キーボードが閉じるのは便利ですし、通常はこのままでも問題ないと思いますが、次のように UITextFieldDelegate に適合することで、この挙動を抑止することも出来ます。

  • textFieldShouldReturn(_:)false を返す
  • textFieldShouldEndEditing(_:)false を返す

textFieldShouldReturn(_:)false を返した場合は、.editingDidEndOnExit が送信されなくなるので、リターンキーをタップしても UITextField.rx.text の next イベントが発行されず、キーボードを閉じることを抑止することができます。

final class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet private weak var textField: UITextField!

    private let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        textField.delegate = self

        textField.rx.text
            .bind(onNext: { print($0.debugDescription) })
            .disposed(by: bag)
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // onNext は呼ばれない、キーボードも閉じない
        return false
    }
}

textFieldShouldEndEditing(_:).editingDidEndOnExit の送信後に呼ばれるので、リターンキーのタップで UITextField.rx.text の next イベントが発行され、かつキーボードが閉じるを抑止することができます。

final class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet private weak var textField: UITextField!

    private let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        textField.delegate = self

        textField.rx.text
            .bind(onNext: { print($0.debugDescription) })
            .disposed(by: bag)
    }

    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        // onNext は呼ばれるが、キーボードは閉じない
        return false
    }
}

ということで、意図に合わせて UITextFieldDelegate を実装すれば良さそうです。
RxCocoa ではなく、UIKit の仕様に依存した内容なので、ReactiveCocoa も同様の制御が可能です。

まとめ

  • rx.text を購読するとリターンキーのタップでキーボードが閉じるようになる
  • この挙動は .editingDidEndOnExit をターゲットアクションに追加した際の UIKit の標準的な挙動である
  • この挙動を抑止したい場合は UITextFieldDelegate を実装する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む