あなたの文字制限をしているUITextFieldはクラッシュしているかもしれない

UITextFieldで入力した文字に対して何らかの制限を加えたい時はよくあると思います。文字数を制限したり、数字のみを入力させたい場合などです。

サンプルコードを用意しました。 https://github.com/akuraru/CrashTextField

数字のみを入力制限させたい場合の雑なサンプルは以下のようなものになると思います。このコードはクラッシュします。

extension ViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let b = string.isEmpty || string == String(Int(string) ?? 0)
        return b
    }
}

クラッシュの再現方法

  1. UITextFieldに制限される文字列をペーストする
  2. 端末をシェイクして取り消しアラートを表示する(シミュレータだと⌘+⌃+Z)
  3. 取り消す
  4. クラッシュ

対策1

入力を受け付けない場合、UndoManagaerをリセットします。これで、取り消しができなくなります。

extension ViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let b = string.isEmpty || string == String(Int(string) ?? 0)
        
        if !b {
            textField.undoManager?.removeAllActions()
        }
        
        return b
    }
}

対策2

シェイクで編集する機能を無効にします。この方法はアプリ全体で無効になってしまうのでできればやらないようにしたほうがいいでしょう。

application.applicationSupportsShakeToEdit = false

クラッシュの内容(おまけ)

アプリケーションのどこで起こったクラッシュなのか全くわからないクラッシュログが発生する

2018-05-21 22:20:05.268665+0900 CrashTextField[47789:859156] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSBigMutableString substringWithRange:]: Range {0, 38} out of bounds; string length 0'
*** First throw call stack:
(
    0   CoreFoundation                      0x00000001127fd1e6 __exceptionPreprocess + 294
    1   libobjc.A.dylib                     0x000000010ec76031 objc_exception_throw + 48
    2   CoreFoundation                      0x0000000112872975 +[NSException raise:format:] + 197
    3   Foundation                          0x000000010e663e72 -[NSString substringWithRange:] + 131
    4   UIKit                               0x0000000110360b1e -[NSTextStorage(UIKitUndoExtensions) _undoRedoAttributedSubstringFromRange:] + 136
    5   UIKit                               0x0000000110360f7d -[_UITextUndoOperationReplace undoRedo] + 319
    6   Foundation                          0x000000010e711695 -[_NSUndoStack popAndInvoke] + 280
    7   Foundation                          0x000000010e711424 -[NSUndoManager undoNestedGroup] + 433
    8   UIKit                               0x000000010f5225aa __58-[UIApplication _showEditAlertViewWithUndoManager:window:]_block_invoke.2495 + 31
    9   UIKit                               0x000000010f8db425 -[UIAlertController _invokeHandlersForAction:] + 105
    10  UIKit                               0x000000010f8dbe2a __103-[UIAlertController _dismissAnimated:triggeringAction:triggeredByPopoverDimmingView:dismissCompletion:]_block_invoke.461 + 16
    11  UIKit                               0x000000010f683d02 -[UIPresentationController transitionDidFinish:] + 1346
    12  UIKit                               0x000000010f687b72 __56-[UIPresentationController runTransitionForCurrentState]_block_invoke.436 + 183
    13  UIKit                               0x000000011026b274 -[_UIViewControllerTransitionContext completeTransition:] + 102
    14  UIKit                               0x000000010f5d410d -[UIViewAnimationBlockDelegate _didEndBlockAnimation:finished:context:] + 859
    15  UIKit                               0x000000010f5a6f09 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 343
    16  UIKit                               0x000000010f5a754c -[UIViewAnimationState animationDidStop:finished:] + 293
    17  UIKit                               0x000000010f5a7600 -[UIViewAnimationState animationDidStop:finished:] + 473
    18  QuartzCore                          0x00000001160117a9 _ZN2CA5Layer23run_animation_callbacksEPv + 323
    19  libdispatch.dylib                   0x000000011395d848 _dispatch_client_callout + 8
    20  libdispatch.dylib                   0x000000011396892b _dispatch_main_queue_callback_4CF + 628
    21  CoreFoundation                      0x00000001127bfc99 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
    22  CoreFoundation                      0x0000000112783ea6 __CFRunLoopRun + 2342
    23  CoreFoundation                      0x000000011278330b CFRunLoopRunSpecific + 635
    24  GraphicsServices                    0x0000000115189a73 GSEventRunModal + 62
    25  UIKit                               0x000000010f5120b7 UIApplicationMain + 159
    26  CrashTextField                      0x000000010e36db77 main + 55
    27  libdyld.dylib                       0x00000001139da955 start + 1
)

参考

https://www.jianshu.com/p/b302a7c66741

XCUITestでアプリの初期状態を変更する

起動時にパラメータを渡すことができ、アプリケーション内でいつでもそれを確認することができます。 XCUITestでは外部からの操作がほとんどできません。この起動時のパラメータによって初期状態を変更することができそうです。

let app = XCUIApplication()
app.launchArguments.append("UITEST")
app.launch()
ProcessInfo.processInfo.arguments.contains("UITEST")

リモートワークは銭湯ですべき

前職での話です

リモートワーク制度で諸事情によって会社にこれないための制度です。 諸事情の解釈は通常家にいないといけない理由がある的なものかもしれませんが、会社の環境(主にネットワークあたりとか狭いとか)よくないということを理由により良い環境で働くためにリモートワークすることにしました。 より良い環境が必ずしも自宅とは限らない。 適当にググって良さそうだったのでなごみの湯にリモートワークしにいくことにしました。

https://www.google.co.jp/amp/s/multiness.net/tripandwork/nomad-super-sento/amp/

なごみの湯とは

https://www.nagomino-yu.com/ 都内の日帰り温泉のこと。 新宿から10分程度の荻窪にあり、駅からすぐ近くにあり割と簡単に行ける。

良い点

  • 会社のWifiより若干早い
  • 広い。すごく広い。誰も働いていないので3席分くらい占有できる。
  • 15分で温泉に浸かって休憩できる
    • タバコ休憩並なので、共用されていいはず
  • コンビニにおやつ買ってくる短くて健康的
  • 詰まったら温泉入りに行ける
  • サウナとかもある
  • リクライニングソファとかがある
  • 隣が秋吉

悪い点

  • 2000円くらいかかる
  • そんなにはお高くはない
  • 電源があるところの椅子は硬い
  • 一度入ったら出れない
  • 食べてから行くか、中で食べるか迷う
  • もちろんディスプレイとかはない
  • 個人的な理由
    • 会社より遠い
    • おそらく中央線ユーザ以外は大抵の人が会社より遠くなりがち

勤怠的な話

勤怠は現状のシステムとしては、出社して退社した後に再び出社して退社した場合はその間が休憩時間としてあるかわれます。よってTeamSpiritで論理出社と退社ボタンを使って休憩時間を記録することができます。食事などをしている時間をし始めるタイミングで退社を行い、終わったら出社することで正確な勤怠管理が可能です。

その他

  • 漫画がある
    • 読んでいないものがあれば漫画喫茶より安く済むかも
  • 縦に長い施設
  • 女子の方が休憩施設に近い
  • 女子風呂が2階で休憩施設が地下1階、男子は4階
  • 働いている人は2,3人見かけた

転職と原宿のおすすめの店10選

1月15日に退職して、16日にクレディセゾンに入社しました。給料と環境は向上した。エンジニア募集しているそうです。

ということでおすすめの原宿の10選です。

がらり

焼酎と味噌のお店。お昼は3種類の味噌汁が無限に飲める。何食べても旨い。そして常に混んでいる。

夜は味噌舐めながら、焼酎を煽る末期な飲み方がしていた。

https://www.google.co.jp/amp/s/s.tabelog.com/tokyo/A1309/A130901/13005444/top_amp/

タップルーム

クラフトビールのお店。昼はやっていない(はず)。常に10種類以上のクラフトビールがある。

今のところ飲み屋しかないが、1杯で潰れるので飲み屋は出てこない。

https://bairdbeer.com/ja/taprooms/

龍の子

麻婆豆腐のお店。会社からは竹下通りを通り抜けていくがそれだけの価値はある。麻婆豆腐以外もあるが、麻婆豆腐しか食べた事はない。

http://ryunoko.tokyo

Hutte

グリルバル。ステーキが食べれる店。ステーキを食べようと思ったらかなり長距離を移動しないとないので、会社から2番目に近い飲食店というのもあり得点が高い。

https://www.google.co.jp/amp/s/amp.retty.me/r/100001311117/

スパゴ

パスタ専門店。パスタの種類がかなりある。食べた限りでは全部美味しかった。たらこがおすすめ。

https://www.google.co.jp/amp/s/amp.retty.me/r/100000019992/

asatte

日替わり定食のみのお店。入るとご飯の量しか聞かれないので初見殺し感がある。毎日変わるので毎日行ける店。

https://www.instagram.com/asatte._.menu/?hl=ja

東京バーガー

東京感はない。なんなら常連はバーガーも食べない。ライスシリーズのタコライスロコモコを食べている。

https://g.co/kgs/FqQzPH

うさぎ

広島のお好み焼き屋。

https://g.co/kgs/XL2mqp

手打ち釜揚げうどん 禅

カレーうどんがおすすめ

https://g.co/kgs/FfvWEc

五○二

定食とうどんのお店

https://g.co/kgs/geR5f1

まとめ

原宿でも勧めれる10個もあったことに驚いている。一番のおすすめは同僚と食べにいく店です。

なぜSwiftでDIが流行っているのか

結論としてはテストを簡単にするためです。 DIでなくても簡単にする方法はあります。 この記事では、DIすることによってテストが簡単になる理由を説明します。

偽物によるテストの容易性の向上

 あるアプリを手動で正しく動いていることをチェックするとしよう。多くのアプリは外部の状態に依存しています。サーバーや時間、ユーザーの操作などです。アプリがちゃんと動いていることをチェックするために、それぞれがちゃんと動いていることを確かめたり、状態を確認しながらチェックをする必要があります。チェックを行うために、外側の環境の不安定で一向にアプリのチェックが進まない事は良くある話です。

 実際の開発を行うときは、手動でのチェックは極力避けていると思います。テストコードを書いてCIで自動テストを実行しています。1日のうちで何度も自動テストを行うのに外側の不安定な環境に依存していてはテストが失敗し、自動テストの効果が十分に発揮できません。

 本物の代わりに偽物を使って、テストを行えば安定したテストを行うことができます。例えば、必ず決まった値を返すサーバーのようなものや、同じ時間を返す時計、ユーザーのような操作をするロボットを使うことです。

テストダブルによるオブジェクトの置き換え

 話を小さくして、オブジェクト単位の話です。

 オブジェクトもアプリと同じように別のオブジェクトに依存しています。別のオブジェクトもさらに別のオブジェクトに依存しています。自動テストを行うときに、本物のオブジェクトを使ってテストをすると数十のオブジェクトを状態をセットアップしなければいけない時があります。テストのための準備するコードが実際にテストを行なっているコードより圧倒的に長くなるのは本末転倒です。テストしたい対象に焦点を当てたコードの方が良いテストです。

 偽物のオブジェクトを使うことによって、本物より早くセットアップできれば、テストしたい対象に焦点を当てたテストコードを書くことができます。この偽物のオブジェクトをテストダブルと言います。

Swiftによる型チェック

 偽物のオブジェクトに置き換えればテストが簡単になるという話をしてきましたが、Swiftでは少し面倒な問題があります。

 Swiftは強い型付き言語であり、最適化をしなければ実行時の型チェックも行われる言語です。変数の型が構造体の場合はその構造体のオブジェクトの必要になり、クラスに依存している場合はそのクラスかそのクラスを継承しているクラスのオブジェクトの必要がありです。無理矢理に型を誤魔化しても変数に代入しても実行時エラーになります。つまり、普通にコードを書くとテストダブルは使えず、常に本物のオブジェクトを使うしかありません。これは、テストを行うときに巨大なセットアップが必要なオブジェクトが存在し、テストを困難にさせます。

プロトコルによるテストの容易性の向上と実体の喪失

 変数の型をプロトコルにすることで、プロトコルを実装しているオブジェクトなら何でも代入することができます。テストダブルが使えるため、意図した動作をテストが用意になります。その代わりに、変数の依存をプロトコルにすることによって、本物の型がわからなくなってしまいます。テストしやすくするためには仕方ないことだったとは言え、このままではアプリを為すことができません。

オブジェクトの注入(DI)

 アプリとしての形を為すためにプロトコルでの依存している本物のオブジェクトを解決する必要があります。本物のオブジェクトを解決するためのパターンがDIなのです。DIによって、アプリとしての形を保ちつつ、テストしやすいコードを書くことができるのです。

 DIの実装方法はいくつかあり、DICやCakeパターンなどがあります。実装方法については自分で調べてください。

更新できなければ淘汰されるiOSアプリ

2020年12月7日に更新

最近の事情

UIWebViewの完全廃止

 UIWebViewはiOS8以降に非推奨になっていたが新規アプリの申請を終了する予定でした。2020年末以降に延長した。今のところ未定となった。 WKWebViewに移行しなければいけない。依存しているライブラリに組み込まれている可能性があるので、今一度確認した方がいいだろう。iOS15では完全に使えずに実行時にクラッシュになるかもしれない。 developer.apple.com

Apple Push Notification Serviceのアップデート

 古いプッシュ通知の方法が使えなくなる。2021年3月31日に延期になりました。新しいプッシュ通知の方法を使うように切り替える必要がある。 developer.apple.com

Xcode12でのビルドが必須

 2021年4月末からXcode12でのビルドする必要がある。必然的に下記のOSで変更になったUIの挙動に対応した状態にしておかなければ、意図しない挙動になっている可能性がある。 * iOS 14 * watchOS 7 * tvOS 14

https://developer.apple.com/ios/submit/ iOS14対応をまとめてみた - Qiita

UIAlertView

 UIAlertViewはiOS8以降に非推奨となっている。申請時に注意されたという話も聞くこともありUIWebViewと同様に禁止になる日もそう遠くない。

Swiftが基本

 Swiftが2014年に発表され5年経過した。新規に開発アプリや追加機能はSwift以外にありえない状況になった。Objective-Cは淘汰され、既存のアプリはSwiftに移行しなければ採用はでき辛くなって行っている。Objetive-CからSwiftに移行するにあたってもそれができるエンジニアがいなければ移行できないし、予算と時間が必要になる。長い目で見れば今すぐにSwiftに移行した方が良い。そうでなければ、アプリのアップデートを諦めて低予算運用に切り替えた方が良い。

ダークモード対応

 iOS13以降ではダークモードが使えるようになりました。今のところ、ダークモード非対応の不利益はおりません。おそらく、2,3年のうちにアップデート時に必須になる可能性がある。ダークモードだけでなく3つ目4つ目のカラーモードを設定できる想定で対応を行なった方が良いだろう。 developer.apple.com

SwiftUI

 今のところ移行するノウハウすらない状態。  いつか移行するために情報収集や移行するためのコードのリファクタリングを行なっておうた方がいいかもしれない。

過去にあった淘汰の歴史

Sign in with Apple

 2020年4月末からサードパーティログインのみで認証が完結するアプリには Sing in with Apple が必須になります。GoogleFacebookTwitterのアカウントでログインできるのであればAppleのアカウントでもログインできるようにする必要があります。

独自のサインインシステムや特定のシステムに直接サインインするクライアントアプリなどは、Sign in with Appleを実装する必要はありません。

https://developer.apple.com/news/?id=03042020d

マルチタスク対応

 没入型の体験を提供するアプリでない限り、iPadではマルチタスクに対応していない場合は2020年4月末からアプリの申請が通らなくなる。

 「没入型の体験を提供する」が何を持って、提供していると言えるかは動画内で語られてはいないが、主にゲームを主にターゲットにしていると思われる。 iPad専用アプリの場合は程度大きい画面サイズを想定していることがあるが、今後はそうも行かなくなる。 あらゆるサイズを想定したレイアウトを組む必要がある。

 同時にiPhoneサイズにも対応するのが良いだろう。

https://developer.apple.com/ios/submit/

Google Analyticsの統合

FabricはFirebaseに統合されることに伴って、 Google Analyticsは廃止される。早くFirebaseまたは代わりのサービスに移行する必要がある。

12ヶ月の猶予ということから2020年10月ごろには完全にデータが削除されると予想される。

https://support.google.com/analytics/answer/9167112?hl=ja

Fabricの統合

FabricはFirebaseに統合されることに伴って、 Fabricは2020年3月31日に廃止される。Firebaseにアプリを移行する必要がある。

Firebase

iPhone5対応等のディスプレイサイズの増加

 iPhone4S までは3.5インチとiPad 9.7インチのディスプレイサイズしか存在していなかった。「Androidはあらゆるディスプレイサイズがあって大変だなぁ」なんて呑気に話していた物だった。iPhone5が発売されて、ほどなくして4インチの画面サイズのサポートが必須になった。縦480ポイントであることを前提にしたレイアウトの組み方をしていたアプリは改修を余儀なくされた。当時はオートレイアウトもなくViewの位置を左上からの位置を計算を行って配置を行っていたため、それなりに大変だった。

 iPhone6や6+が登場してiPhone系の端末を4つのサイズをサポートすることになった。この頃にはオートレイアウトができており、オートレイアウトへの移行が必要に迫られることになになった。その後iPadでもiPad Proの登場やマルチタスク対応があった。今後は画面サイズが動的に変更され、あらゆるサイズが存在しうることを前提として開発を行っていく必要がある。

arm64対応必須

 2015年2月1日、arm64に対応していないアプリはストアから削除されることになった。対応としてはただ追加するば終わりという物だったため更新のあるアプリはほとんど何もすることもなくアップデートしれば終わりではあった。ほとんど更新されていなかったアプリがごっそりと消えた。amr64e対応必須はありえる。

デザインの変更

 iOS7にスキューモーフィズムからフラットデザイン変更があった。iOSでは時々多いくデザインが変更されていくため、それに合わせて修正を行っていく必要がある。

まとめ

 iOSアプリは新しいアプリを作ったら1年以内に儲かる目処を立てて更新を行っていく必要がある。

参考

https://developer.apple.com/app-store/review/guidelines/

個人目標を給料査定に使ってはいけないっぽい

OKRの本とかre:Work とかを読んでいて、個人目標を給料査定に使ってはいけないと言うことに気がついた。

会社の目標は挑戦的なものが好ましい。

無謀ではないが実力の上限ギリギリなものが目標として好ましい。やる気を出して新しいことに挑戦することよって、革新的な成果を期待している。 OKRでの目標設定では60〜70%程度の達成率で成功とするので、立てるべき目標はかなり高めだと言える。

個人目標も挑戦的である必要がある

個人目標は会社やチームの目標と自分のキャリアプランなどとすり合わせつつ立てる必要がある。この目標も会社の目標の一部を担ってもらっているだけに挑戦的なものを要求せざるおえない。もちろん、個人の能力や考えのあるので本人が納得の行く目標を立てて、モチベーションを上げてもらう必要がある。

目標の度合いと給料査定を連動させる

 目標を達成したら給料が上がり、しなかったら給料が下がるような会社だったとしよう。大抵の人は明らかに達成可能が目標を立てようとします(毎日適時に出社するとか)。全くもってアホらしい目標にしかなりません。メンバーもモチベーションは上がらないし、挑戦的とは程遠いものになってしまいます。  難易度が高ければ高いほど給料の上がり幅が高いような会社だったとしよう。それでも大抵の人はまぁまぁな目標を立て用途します(今回達成した9割くらいの目標とか)。そこそこの目標になりそうです。今まで通りにやっていれば達成できるようなものになりそうです。  達成したかに関わらず挑戦した内容による会社だったとしましょう。目標と関係ありません。給料査定と連動しているしないと明言してあげた方がいいでしょう。

Googleではどうしているのか

re:Work 公正な給与制度を設計し運用する を読む限りでは、職業に応じて市場価値によって決めるとあります。転職しても同じくらいの給料で働いていると思える会社であれば、転職のリスクは下がりそうです。