RxSwiftへ苦手意識がある方向けの RxSwift + MVVM でiOSサンプルコード書きました
はじめに
業務を一緒にやっている方に僕の書き方で苦手意識を払拭できたという嬉しいお言葉を頂いたので、今回サンプルコードを用意して本記事を書こうと思いました。
RxSwiftの最初の苦手意識が払拭でき、かなり助かりました!名前の付け方とかPublishSubjectの使い方含めてこれは復習すれば、様々な形に応用できるようになりそうです😊
— fumiyasac@iOSアプリ開発「UI 実装であると嬉しいレシピブック」著者 (@fumiyasac) April 29, 2019
※ 僕自身、今まで様々なプロジェクトに関わりながら、どれがいいかなーという感じで手探りで進めてきたので、もっとこうした方がいいよという意見がありましたら、コメントください!
サンプルアプリ
GitHub Api を使ってレポジトリを検索し、詳細をWebViewで表示するというシンプルなものです。
サンプルコード
解説
前置き知識
RxSwift を使うことで、イベントドリブンなコードを簡潔に書くことができます。 GoFのデザインパターンのObserverパターンを調べると理解を進めてくれるかもしれません。
主な使用ライブラリ
主な使用ライブラリは以下です。
RxSwiftCommunity/Action は見慣れない方も多いかもしれませんが、 RxSwift を使ってAPIコールするときに便利ライブラリです。 IN と OUT を整理して、 Loading や Error などの状態管理も可能です。
コードを追う
APIのベース部分は一般的な実装だと思うので割愛します。わからない場合は、聞いていただければお答えします。
今回は一覧画面についてしか書きません。
コードについての説明は、コード内にコメントを書いて処理の内容を説明していきます。
手抜きですみません 😅
一覧画面
この画面は検索結果をUITableViewに表示するだけですが、
UIパーツ
、データ
、アクション
、状態
を書き出してみます。
UIパーツ
- UITableView
- UITableViewCell
- UIActivityIndicatorView
データ (表示する)
アクション
- 初回データ取得
- スクロール時に追加データ取得
- セル選択
状態
UIパーツ
は、ViewControllerに配置し、
データ
、アクション
、状態
は ViewModelに配置します。
ViewModelでは、ViewController が扱いやすいようにViewModel内で加工して、適切な Observable に変換して公開します。
ソースコード解説
ViewModel
ここからは一旦、ViewModel 内での処理を見ていきます。
ViewModel では、 inputs
, outputs
というprotocol (インターフェイス) を切っています。
ViewModelに対するインプットとアウトプットが混ざってしまいバグになることがあるので、それを回避するためだったり、 可読性をあげるという目的もあります。
Inputs
は ViewControllerなどから ViewModel に入ってくるイベントなどを記述します。Outputs
は ViewModel から ViewController などに出て行くイベントを記述します。(ViewControllerなどから参照されるイベント)
protocol ListViewModelInputs { var fetchTrigger: PublishSubject<Void> { get } var reachedBottomTrigger: PublishSubject<Void> { get } } protocol ListViewModelOutputs { var gitHubRepositories: Observable<[GitHubRepository]> { get } var navigationBarTitle: Observable<String> { get } var isLoading: Observable<Bool> { get } var error: Observable<NSError> { get } } protocol ListViewModelType { var inputs: ListViewModelInputs { get } var outputs: ListViewModelOutputs { get } } final class ListViewModel: ListViewModelType, ListViewModelInputs, ListViewModelOutputs { var inputs: ListViewModelInputs { return self } var outputs: ListViewModelOutputs { return self } ...
一つ一つを見て行くと、 先ほど挙げた データ
、アクション
、状態
を記述していることに気づいて欲しいです。
// アクション // 初回データ取得 var fetchTrigger: PublishSubject<Void> { get } // スクロール時に追加データ取得 var reachedBottomTrigger: PublishSubject<Void> { get } // セル選択 は UI側で完結するので、記述なし
// データ (表示する) // GitHub 検索結果のレポジトリリスト (UITableView用) var gitHubRepositories: Observable<[GitHubRepository]> { get } // GitHub 検索文字列 (navigationBar title 用) var navigationBarTitle: Observable<String> { get } // 状態 // API Loading var isLoading: Observable<Bool> { get } // API エラー var error: Observable<NSError> { get }
イベント発火・通知するためのものには Subject
か AnyObserver
の Observer
を使用し、それを 購読してイベントを処理します。
Subject
クラス | 説明 |
---|---|
PublishSubject | 一切キャッシュしないSubject |
BehaviorSubject | 直近の値を1つだけキャッシュするSubjectで、初期値を与えることができる。 |
ReplaySubject | 指定した数または全てをキャッシュするSubjectで、初期値は与えられない。 |
状態、データには、基本的に Observable
を使用し、イベントや変更された値などを通知します。
View上では任意のObservableをUIコンポーネントやViewModelのpropertyに対してバインドするだけで、状態変更のロジックなどが実装されていないシンプルな実装を実現できます。
また命名については
イベント発火・通知するためのものには hogeHogeTrigger (...Trigger)、
状態、データは それ自体を表す名前をつけるようにしています。
この命名の仕方でも、RxSwift のコード理解を進めるようです。
実際に実装内容をコードベースで見てみます。
コード内にコメントしています。
final class ListViewModel: ListViewModelType, ListViewModelInputs, ListViewModelOutputs { var inputs: ListViewModelInputs { return self } var outputs: ListViewModelOutputs { return self } // MARK: - Inputs // 初回データ取得イベントトリガー let fetchTrigger = PublishSubject<Void>() // スクロール時に追加データ取得イベントトリガー let reachedBottomTrigger = PublishSubject<Void>() // API pagination 用 private let page = BehaviorRelay<Int>(value: 1) // MARK: - Outputs // GitHub 検索文字列 (navigationBar title 用) を通知する let navigationBarTitle: Observable<String> // GitHub 検索結果のレポジトリリスト (UITableView用) を通知する let gitHubRepositories: Observable<[GitHubRepository]> // Loading 状態を通知する let isLoading: Observable<Bool> // エラー 状態を通知する let error: Observable<NSError> // Action ライブラリの記述 (INとOUTを記述する) private let searchAction: Action<Int, [GitHubRepository]> private let disposeBag = DisposeBag() init(language: String) { // ナビゲーションバー用の文字列を生成し、Observableにして代入する self.navigationBarTitle = Observable.just("\(language) Repositories") self.searchAction = Action { page in // in に入ってきたときに以下が実行される return Session.shared.rx.response(GitHubApi.SearchRequest(language: language, page: page)) } // ViewModel内でGitHubレポジトリデータを保持する let response = BehaviorRelay<[GitHubRepository]>(value: []) // GitHubレポジトリデータを外部に公開するように Observable に変換して代入する self.gitHubRepositories = response.asObservable() // Actionライブラリが Loading 状態を持っているので、その状態を外部に公開するために公開用の変数に代入する self.isLoading = searchAction.executing.startWith(false) // Actionライブラリが エラー 状態を持っているので、その状態を外部に公開するために公開用の変数に代入する self.error = searchAction.errors.map { _ in NSError(domain: "Network Error", code: 0, userInfo: nil) } // Actionライブラリが API レスポンスを持っているので、それを購読する searchAction.elements .withLatestFrom(response) { ($0, $1) } // 前回のレスポンス情報と合わせたいので、ストリームに取り込む .map { $0.1 + $0.0 } // 実際にここで、前回のレスポンスと今回取得したレスポンスを合成する .bind(to: response) // response にバインドし、値の変更を通知する .disposed(by: disposeBag) // API レスポンスを購読し、次回のAPIリクエストするときのために、 page番号をインクリメントする searchAction.elements .withLatestFrom(page) .map { $0 + 1 } .bind(to: page) .disposed(by: disposeBag) // 初回データ取得イベントトリガーを購読し、 APIリクエストする fetchTrigger .withLatestFrom(page) .bind(to: searchAction.inputs) .disposed(by: disposeBag) // スクロール時に追加データ取得イベントトリガーを購読し、 APIリクエストする reachedBottomTrigger .withLatestFrom(isLoading) // API通信中はリクエストを送らないために、Loading フラグをストリームに取り込む .filter { !$0 } // 取り込んだ通信中フラグでフィルターをかける。フラグを判定し、trueの場合は次へ行き、 false の場合は イベント通知はここで終了する .withLatestFrom(page) .filter { $0 < 5 } // APIの使用上リクエスト制限があるので、 page番号でフィルターをかける .bind(to: searchAction.inputs) .disposed(by: disposeBag) } }
ViewController
今度は ViewController の方をみていきます。
ViewControllerは ユーザーアクションをViewModelに通知する、ViewModelの状態、データ、イベントを反映するだけにしたほうがいいです。
つまり、ViewModelと繋ぐだけの実装にしたほうがシンプルで、テストもしやすくなり、いいと思います。
あとは、 viewModel の型を ViewModel の inputs
, outputs
のみを公開している ListViewModelType にするのが肝ですかね。
final class ListViewController: UIViewController { // ViewController を 生成する時は ViewModel を引数に入れています。 ViewModelを外から差し込めるようにしておけば、Unit テストを実施することもできます。 static func make(with viewModel: ListViewModel) -> ListViewController { let view = ListViewController.instantiate() view.viewModel = viewModel return view } @IBOutlet private weak var tableView: UITableView! @IBOutlet private weak var indicatorView: UIActivityIndicatorView! // 必ずインターフェイスを公開するようにする private var viewModel: ListViewModelType! private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() // GitHub 検索文字列 (navigationBar title 用)をナビゲーションバータイトルにバインド viewModel.outputs.navigationBarTitle .observeOn(MainScheduler.instance) .bind(to: navigationItem.rx.title) .disposed(by: disposeBag) // GitHub 検索結果のレポジトリリスト (UITableView用) を UITableViewにバインド viewModel.outputs.gitHubRepositories .observeOn(MainScheduler.instance) .bind(to: tableView.rx.items) { tableView, row, githubRepository in // セル生成 let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "subtitle") cell.textLabel?.text = "\(githubRepository.fullName)" cell.detailTextLabel?.textColor = UIColor.lightGray cell.detailTextLabel?.text = "\(githubRepository.description)" return cell } .disposed(by: disposeBag) // セル選択時のイベントを購読 (選択時のROWに応じたGitHubRepositoryが通知される) tableView.rx.modelSelected(GitHubRepository.self) .observeOn(MainScheduler.instance) .subscribe(onNext: { [weak self] in // 詳細画面に遷移する let vc = DetailViewController.make(with: DetailViewModel(repository: $0)) self?.navigationController?.pushViewController(vc, animated: true) }) .disposed(by: disposeBag) // Loading 状態 を UIActivityIndicatorView にバインド viewModel.outputs.isLoading .observeOn(MainScheduler.instance) .bind(to: indicatorView.rx.isAnimating) .disposed(by: disposeBag) // Loading 状態 を 購読し、 TableView の contentInset を調整する viewModel.outputs.isLoading .observeOn(MainScheduler.instance) .subscribe(onNext: { [weak self] in self?.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: $0 ? 50 : 0, right: 0) }) .disposed(by: disposeBag) // エラー 状態 を 購読し、 エラーが通知された場合は アラートを表示する viewModel.outputs.error .observeOn(MainScheduler.instance) .subscribe(onNext: { [weak self] in let ac = UIAlertController(title: "Error \($0)", message: nil, preferredStyle: .alert) ac.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self?.present(ac, animated: true) }) .disposed(by: disposeBag) // スクロールして、下に到達した時に流れるイベントを購読して、ViewModelに通知する tableView.rx.reachedBottom.asObservable() .bind(to: viewModel.inputs.reachedBottomTrigger) .disposed(by: disposeBag) // 初回のデータ取得を、ViewModelに通知する viewModel.inputs.fetchTrigger.onNext(()) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tableView.indexPathsForSelectedRows?.forEach { [weak self] in self?.tableView.deselectRow(at: $0, animated: true) } } } extension ListViewController: StoryboardInstantiable {}
reachedBottomに関しては、Reactive Extension を書いてある
extension Reactive where Base: UIScrollView { var reachedBottom: ControlEvent<Void> { let observable = contentOffset .flatMap { [weak base] contentOffset -> Observable<Void> in guard let scrollView = base else { return Observable.empty() } let visibleHeight = scrollView.frame.height - scrollView.contentInset.top - scrollView.contentInset.bottom let y = contentOffset.y + scrollView.contentInset.top let threshold = max(0.0, scrollView.contentSize.height - visibleHeight) return y > threshold ? Observable.just(()) : Observable.empty() } return ControlEvent(events: observable) } }
任意の画面から一覧画面に遷移する
ViewController.swift
let vc = ListViewController.make(with: ListViewModel(language: "RxSwift")) navigationController?.pushViewController(vc, animated: true)
さいごに
僕自身、100%この書き方や、実装方法がいいのかわかりませんが、色々なプロジェクトに関わってきた結果、今の所、これが良さそうという感じです。。
もっとこうしたほうがいいよというコメントがある方はお待ちしています🙇♂️
サンプルコード
layer.borderWidth と layer.cornerRadius を同時に使用するとボーダーの一部に白いギザギザが出る
概要
layer.borderWidth と layer.cornerRadius を同時に使用すると、
ボーダーの一部に白いギザギザが出るCALayerのバグ? のようなものにハマりました。
UI系のライブラリのIssuesにも同じようなことを言っている方がいました。 https://github.com/IBAnimatable/IBAnimatable/issues/503
具体的には
例えば、 角丸 23pt, ボーダー 3ptのデザインを実装しようとした時にこうなります。
拡大するとこんな感じ。
layer.allowsEdgeAntialiasing というプロパティがありますが、trueにしても関係ありませんでした。
とても細かいですけど、背景が黒系だと白いギザギザが目立つんですよね。
解決方法
結構時間かけて、英語で調べたりしたんですけど、
CALayerのバグ?ということで具体的な解決方法が見つかりませんでしたが、
私は、Border用のView を用意することで解決しました。
新卒エンジニア、リクルートを退職した
はじめに
2017年4月に新卒でエンジニアとしてリクルートホールディングスに入社しまして、
2018年12月に退職します。2年弱在籍しました。
出向先
2017年度:リクルート住まいカンパニー
2018年度:リクルートテクノロジーズ
次は フリーランス
になります。
※ 前提として新卒入社して2年弱しか経っておらず社歴が浅い。また、会社から多くのチャンスをもらい、成長できて感謝しているというのをふまえて読んでほしい。
私について
私は大学の時に田舎から上京し、大学2年くらいからプログラミングをはじめました。
在学中はインターンや業務委託としてエンジニアをしていました。
バックエンド(2年) → フロントエンド(1,2年) → iOS (2年), Android (6,7ヶ月)
やっていたこと
新規事業開発
すでにエンジニアリング経験があったので、入社後はすぐに開発をしました。
入社後3ヶ月間は新規事業開発をしていましたが、もともと入社した理由が大規模サービス開発における、エンジニアリング組織と事業の進め方を経験したかったので、希望を出してSUUMO本体の開発チームに異動させてもらいました。
SUUMOアプリ
SUUMOアプリではiOS, Androidを担当しました。
アプリチームはエンジニアだけで 約30人います。こんなにエンジニアが多い組織はなかなかないので、貴重な経験ができました。
アプリチームでは 案件開発
、開発環境改善
、開発工程改善
を行いました。
印象に残っているタスクでいうと、開発環境改善系のタスクです。
私が参画した当時、SUUMO iOS のプロジェクトファイルは相当大きく、ビルド時間が20分以上と長かったので、それを解決するためにある程度の単位ごとに Embedded Framework を順次導入しました。
Framework化することで差分ビルドで済むようになり、長いビルド時間は解消されました。 さらにFramework単位でアプリを作り、対象画面の実装やデザインをすぐに確認できるようにして、開発時間の削減もできました。
どうだったか
入社した理由、やりたかったこと
私はずっとベンチャー企業でしか働いたことがなかったので、
大企業のエンジニアリング組織
や、100→ フェーズの事業開発
、開発手法
を見たかったです。
他にも優秀な同期や先輩、上司に会える確率が高いというのも理由としてありました。
100→ フェーズの開発現場 (SUUMO アプリの話)
大きく分けてネイティブアプリチームとAPIチームに分かれていますが、 アプリチームは社員エンジニアと常駐の業務委託エンジニアの方で構成されていて、内製比率は約2割です。チーム全体の平均年齢は30代中盤です。APIはこれまで社員エンジニアが関わっていなかったですが、ちょうどこの10月から見始めています。
上記のような構成になっており、業務委託の方が手を動かすので、社員エンジニアにはコーディング力に加えて、チームの推進、改善を求められます。
技術的なことでいうと、大昔からの大規模アプリで大改修が厳しい中、事業側との調整を行い、テスタブルな設計を導入したり、Unit Test を入れたり、できる限りレガシーを減らし、保守・開発コストを下げるようにしています。
ただ、技術力のばらつき、採用観点で、Rx などの慣れれば飛び道具になるようなモノや、テスタブルにするためのレイヤードアーキテクチャ、Androidではデファクトスタンダードになりつつある Kotlin の導入などには苦労しますが工夫をしながら進めています。
チームが大きくいろんな方がいるので講習のようなものを行い、チームのみんなが理解できている、納得できている状態にするのはまあまあ大変でコストもかかりますが、チームの成長のために協力しながら進めています。
開発手法
案件開発は、企画チームがあって、そこが考えた企画をエンジニアに流して、リリースまで行ってもらう。
リリース後に企画チームが分析し、また次の打ち手や企画を考えるという工程になっていました。
企画はGAやBigQueryを用いて定量で分析し、見つけた課題に施策を打つという王道パターンでした。
企画と開発はほぼ分業されています。
この手法でSUUMOアプリはグロースできていたので、こういう開発手法が合っているのだと思いますが、今後はお互いのスコープ拡大を目指していっています。
待遇・環境
給料は評価されればすぐに上がるし、一般的な水準より高めに設定されています。
仕事に関してもやりたいと手を上げれば誰でもやらせてもらえる、素晴らしい環境でした。
ただ、リクルートでよく言われる「お前はどうしたい?」という主体性がないと働き辛いと思います。
PCに関しても 「メモリ4GBでHDDのPCで...」のようなこと起こらず *1 、メモリ16GB、Core i5以上のMac or Windows お好みのPCが支給されます...
給料の具体的な数字は隠したいわけじゃないですが、あえて書くほどでもないので気になる方はDMなどで直接聞いてください。
よかったこと
入社前はエンジニアリングにしか興味がなかったですが、事業や社会をエンジニアリングでどうするかということへ視野を広げ、視座をあげることができました。また大企業での仕事の進め方、組織の中での進め方を間近で見て、実行できたこともよかったです。
エンジニア基本スタンス
課題設定力 「目的からプルする」 課題解決力 「手法論ではなく原理原則に立ち返る」 学びの質を上げる 「失敗から学ぶ」 学びまでのリードタイムを減らす 「ムダを減らす」
エンジニアは技術だけではダメで、案件を開発するだけでなく、事業にとっての意味や価値、本当の目的を考え行動する。という基本スタンスが設定されていました。当たり前だろと思う方もいるかと思いますが、入社したての私はそこまで意識できていなかったので、これを意識することで私は変われたと思います。
やめる理由
やめる理由の一番は、時間の自由度を高くしたいからです。
1日は通勤時間を合わせると、約10時間以上は会社のために使っています。
この時間を自分で好きに使えるとなったら、今自分に必要だと感じているハードスキル、ソフトスキルを習得できる仕事や環境を作れると思います。
ちなみに現在の私のフリーランス単価を考えると、金銭的自由度も今よりけっこう上がります。
でも、本当はフリーランスになって受託開発がしたいわけではなくて、事業やサービスをやりたいという理由もあります。
また純粋にモノを作るのが好きで、さらにそれを色んな人に使ってもらって少しでも誰かの役に立っていると気付かされた時、やっていてよかったと感じます。それがユーザーに近ければ近いほど、やりがいを感じます。これは承認欲求ではないと自分では思っていますが、実際には承認欲求なのかもしれません。笑
今でもいくつかアプリ*2を出していて、こういう感じで細々開発するのもいいなーと漠然と思っています。
個人で出来ない規模や内容、スピート感であれば、手段として法人にするのもアリだと思います。
ただ今でも、本当にやりたいことはナニかという問いの答えを出せていないので、まだ考えています。。
最後に
リクルートの現場、環境、感じたこと、今後について書きました。
最初は書くか迷ったんですが、こう言語化してみると、自分の中での振り返りや、今の気持ちの整理にもなるので書いてよかったです。
長文にも関わらず、最後まで読んでくださりありがとうございました。
先輩の退職エントリーも参考になります。*3
人気上昇中の私のアプリ
iOS12 AVAudioSessionPortImpl.mm:56:ValidateRequiredFields: Unknown selected data source for Port Speaker (type: Speaker) というログが出続ける
概要
iPhoneX, iOS12の実機でアプリのデバッグをしていたところ、
アプリ起動後30秒くらいの間、コンソールに以下のようなエラーログが出続けました。
ログが出続ける間、アプリも操作できず固まります。
AVAudioSessionPortImpl.mm:56:ValidateRequiredFields: Unknown selected data source for Port Speaker (type: Speaker)
forumにもスレッドが立っていました。
forums.developer.apple.com
結論
どうやらAdMobのバグのようです。
takattata.hateblo.jp
自分でもAdMobのバージョンを最新版にしたり、元に戻したりしましたが治りませんでした。
ちなみに試したバージョンは以下
7.31.0
, 7.34.0
, 7.35.1
※ 10/24時点での最新版は 7.35.1
です
ただ、僕の環境ではこの現象が起こるのはデバック環境のみでした。
7.34.0
の AdMobを利用してリリースしたアプリでテストしましたが、起動後に固まったり、カクカクすることはありませんでした。
外国人の友人から永住権の身元保証人を頼まれた話
めったに経験できそうにないことを経験したので記録として。笑
会社の同期で、友人でもある人から日本の永住権を取りたいから身元保証人になってほしいと頼まれました。
その友人は中国人で日本のこと好きで、日本にずっと住みたいという思いがあり、永住ビザを取りたいようです。そしてその永住ビザを取るためには、身元保証人という保証人が必要らしいです。
頼まれた時は、「保証人になってはダメだぞ」と小さい頃から親に言われていたので、保証人という言葉だけで少しイヤだなーって思っていました。
結論からいうと、永住権の保証人と借金をするときの連帯保証人とは違うということが理解できたので、身元保証人になることにしました。 他に頼める人がいるかもしれないけど断るのはかわいそうだし、彼を信じようという思いもありますね。
永住権の身元保証人と借金等の連帯保証人は全くの別物
ここを理解した方がいいかなと思います。 以下に身元保証人がどんなものなのかを記します。
今後、頼まれた方の参考になれば嬉しいです。
入国管理局のQ&A内容
Q7 提出書類に身元保証書がありますが,「身元保証人」とはどのようなものでしょうか。 また,身元保証した際の責任はどうなっているのでしょうか。
A 入管法における身元保証人とは,外国人が我が国において安定的に,かつ,継続的に所期の入国目的を達成できるように,必要に応じて当該外国人の経済的保証及び法令の遵守等の生活指導を行う旨を法務大臣に約束する人をいいます。 身元保証書の性格について,法務大臣に約束する保証事項について身元保証人に対する法的な強制力はなく,保証事項を履行しない場合でも当局からの約束の履行を指導するにとどまりますが,その場合,身元保証人として十分な責任が果たされないとして,それ以降の入国・在留申請において身元保証人としての適格性を欠くとされるなど社会的信用を失うことから,いわば道義的責任を課すものであるといえます。
入管法における身元保証人とは
入管法における身元保証人は、永住ビザ申請を行う外国人が日本で安定した暮らしが出来るように指導することを法務大臣に約束する人。
保証事項について身元保証人に対する法的な強制力はないです。
万が一、ビザ申請人が法律違反等を起こしても、身元保証人が罰則を受けたり、責任を追及されることはありません。
借金等の連帯保証人とは違います。
デメリットは、もし保証人が十分な責任を果たせなかったら次回以降身元保証人としての保証力が弱いと判断され、道義的責任を負います。
道義的責任とは
法的制裁を加えるほどのことではないが、社会的・倫理的非難に値し、ルール違反として世間的に非難される場合の責任です。また道徳や人として行うべき道理などから生じる、任務を行うべきであるということ、あるいは、任務を行わなかったことによる責めなどを意味する表現。
まとめ
万が一の場合は、法的に罰せられることはないが、
国からの社会的・倫理的な信用を失うことはあるということです。
その人がどういう人か、しっかり自分で見極めて、 自分で受ける受けないを決めましょう。
参考リンク:
Swift UIColor Extension でよくある 16進数カラーコードを UIColor に変換する時のビット演算について
前談
import UIKit extension UIColor { convenience init(red: Int, green: Int, blue: Int) { self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) } convenience init(rgb: Int) { self.init( red: (rgb >> 16) & 0xFF, green: (rgb >> 8) & 0xFF, blue: rgb & 0xFF ) } }
16進数カラーコードを UIColor
に変換する時の UIColor Extension
で上記のようなコードを見かけませんか?
UIColor Hex
とかで検索すると上記のような UIColor の Extensionが出てきます。
こんなの
今回は上記のコードで使用されているビット演算についてです。
普段あまりビット演算を使う場面がないので、
自分なりに何をしているか理解した範囲で話します。間違いがありましたらコメントで教えてください。
最低限、2進数、10進数、16進数の理解がないと難しいかもしれません。
例として16進数のカラーコード #3a294b
を入れて説明します。
ちなみに#3a294b
は 10進数 RGB
で表すと Red: 58, Green: 41, Blue: 75
(0 ~ 255) となります。
UIColor.init(rgb: 0x3a294b)
Swiftの 0x
はプリフィックスで、16進数の定数の前には 0x
を付けます。10進数の場合はプリフィックスは不要です。したがって Swiftで let a = 8
と書いた場合は10進数になります。
16進数 3a294b
を2進数に変換すると以下のようになります。
00111010 / 00101001 / 01001011
※わかりやすく 8bitごとに /
で区切って、桁数が揃うように先頭の00
を補っています。
他にも例えば 16進数 FFFFFF
を2進数に変換すると以下のようになります。
11111111 / 11111111 / 11111111
さっと2進数を確認したい場合は、以下のように 16進数から2進数に変換できます
String(0x3a294b, radix: 2) // "1110100010100101001011"
本題
こちらの方から1つずつ見て行きます。
convenience init(rgb: Int) { self.init( red: (rgb >> 16) & 0xFF, green: (rgb >> 8) & 0xFF, blue: rgb & 0xFF ) }
Red
red: (rgb >> 16) & 0xFF,
今回は rgb には 0x3a294b
が入ります。
0x3a294b >> 16
>>
は右ビットシフトを表すビットシフト演算子です。
したがって、式が表す意味は 16進数 3a294b
を 16bit
分を右シフトさせる。
00111010 / 00101001 / 01001011
16進数 3a294b
を 16bit
分を右シフトさせると
00000000 / 00000000 / 00111010
16進数 3a294b
の Redの部分 16進数 3a を取り出せました。
次に
0x3a & 0xFF
&
はビットANDを表すビットシフト演算子です。
したがって、式が表す意味は 16進数 0x3a
を 0xFF
とビットANDする。
これは 0x3a
と 0xFF
で ビットANDすることで最大数を16進数 FF
にするためです。
00111010
と
11111111
の ビットANDをとると
00111010
となり、これを10進数に変換すると58
になり, Red は 58
です。合っていますね。
Green
green: (rgb >> 8) & 0xFF,
同様に見て行くと
0x3a294b >> 8
式が表す意味は 16進数 3a294b
を 8bit
分を右シフトさせる。
00111010 / 00101001 / 01001011
16進数 3a294b
を 8bit
分を右シフトさせると
00000000 / 00111010 / 00101001
16進数 3a294b
の RedとGreenの部分 16進数 3a29 を取り出せました。
次に
0x3a29 & 0xFF
式が表す意味は 16進数 0x3a29
を 0xFF
とビットANDする。
00000000 / 00111010 / 00101001
と
00000000 / 00000000 / 11111111
の ビットANDをとると
00101001
となり、これを10進数に変換すると41
になり, Green は 41
です。合っていますね。
Blue
次に
blue: rgb & 0xFF
同様に見て行くと
0x3a294b & 0xFF
式が表す意味は 16進数 0x3a294b
を 0xFF
とビットANDする。
Blue 特に右シフトせずに、ビットANDを取っています。
Blue は 8bit 部分なので、ビットシフトする必要がありません。
00111010 / 00101001 / 01001011
と
00000000 / 00000000 / 11111111
の ビットANDをとると
00000000 / 00000000 / 01001011
つまり
01001011
となり、これを10進数に変換すると75
になり, Green は 75
です。合っていますね。
よって、今回の例だと
UIColor.init(red: 58, green: 41, blue: 75)
という形で
convenience init(red: Int, green: Int, blue: Int) { self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) }
こちらのメソッドを読んでいることになります。
また
convenience init(red: Int, green: Int, blue: Int) { self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) }
こちらの関数は、UIKit
で実装されている UIColor.init
に合うように
RGB
のそれぞれの値が 0 ~ 1
になるように 255
で 割っています。
以上です。
サンプルソースコードはこちらにあげています。
参考
2年以上更新をしなかったブログを再開しようと思った訳
ブログを再開します
最後に更新した記事から2年以上たっているわけですが、
ブログをまた始めようと奮起したので、こちらで再開します。
文章能力がないですが、お手柔らかに m _ _ m
※いくつか黒歴史になりそうだった記事は削除しましたw
再開する訳
いくつかの理由はありますが、私が今回再開する理由は以下の通り。
1. その当時の気持ちや考えていたこと、出来事を記録する
完全なメモ。あとで振り返れるように。あの時はこんなことがあったからこういうことを考えて行動したんだと。この出来事でこんな考えになったんだと。
2. アウトプットを増やす
社内のSlackで自分の意見をいうことがあるが、これに少し説明を付け加えるだけで世間へ向けたアウトプットできると思った。あわよくば、自分の考えや意見がどうなのか世間からのフィードバックが欲しい。もっというと、自分について知って欲しいし、世間で少しでも知ってくれている人を増やしたい。
こう考えるようになったのは、以下の出来事があったからです。
レシート換金系はほかにもあったのに、なぜこれだけバズったのか。。
このサービスがばずった理由をいろんな人と議論しました。
結論は、
- 1枚10円になりますというわかりやすさ (他はポイント付与)
- サービスのわかりやすさからくる拡散性
- 即金ニーズがあるというタイミング (Cashなど時流に乗ったタイミングだった)
- もともと注目されていた人でメディアが取り上げやすかった (&高校生)
もともと注目されていた人でメディアが取り上げやすかった (&高校生)
ここ!私も近いうちにサービスを出したいと考えているのですが、
最初から知名度がある人が出すサービスの方が普及しやすいです。
だって、芸能人が何かしたらすぐに話題になりますよね。そういうことです。
そういうヨコシマな思いもあり、再開することにしましたw
自己紹介
新卒でエンジニアとしてリクルートホールディングスに入りまして、リクルートテクノロジーズに出向しています。
業務では、SUUMO のiOS, Android の保守、運用をしています。
詳しくはこちら↓ をご覧ください。
www.wantedly.com