- 投稿日:2021-03-05T16:15:15+09:00
Swift 構造体よりクラスを採用したほうが良いケース
この記事について
この記事は、開発未経験の人間がインプットした内容が書かれています。
実際に開発経験を積まないと身につかない、かといって疎かにしたら後々痛い目にあう、といったジレンマを少しでも解消するために、「頭の中を言語化し、解釈違いを指摘してもらう」という手段を取ることとしました。
気になる点がありましたらツッコミをいただけると嬉しいです。記事の内容
Swiftで開発する際に、クラスを採用したほうが良いケースのまとめ。
インプット教材
書籍「Swift実践入門」第12章
開発環境
% swift -version Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28) % xcodebuild -version Xcode 12.4Swiftの開発は構造体(struct)が推奨されている
Swiftでの開発は、想定しない参照によるバグがないことや安全性の高さから構造体での開発を推奨されている。
とはいえ、依然としてクラスのほうが良いケースも存在するらしい。そのケースを理解していれば、それ以外は構造体で組めば良いことになるので、クラスの採用基準を注目し、記事にしてみた。クラスを採用する基準
1.変更した値などをインスタンスに保持したいとき
例) タイマーアプリを実装する。ターゲットを構造体とクラスとで挙動を比較する。
- 参照元のプロトコルでターゲット先の型名"identifier"とカウント数"count"を定義。
- 拡張機能を使ってカウントを進める関数を実装。
- プロトコルに準拠した2つのターゲットを用意し、プロパティに値を設定。
- タイマーを実行する"Timer"を定義。今回は5秒。
- それぞれをインスタンス化しスタートを実行。終了後の"count"を確認。
protocol Target { var identifier: String { get set } var count: Int { get set } mutating func action() } extension Target { mutating func action() { count += 1 print("id: \(identifier), count: \(count)") } } struct ValueTypeTarget : Target { var identifier = "Value Type" var count = 0 init() {} } class ReferenceTypeTarget : Target { var identifier = "Reference Type" var count = 0 init() {} } struct Timer { var target: Target mutating func start() { for _ in 0..<5 { target.action() } } } //構造体のターゲットを登録してタイマーを実行 let valueTypeTarget: Target = ValueTypeTarget() var timer1 = Timer(target: valueTypeTarget) timer1.start() valueTypeTarget.count //クラスのターゲットを登録してタイマーを実行 let referenceTypeTarget = ReferenceTypeTarget() var timer2 = Timer(target: referenceTypeTarget) timer2.start() referenceTypeTarget.count出力結果
id: Value Type, count: 1 id: Value Type, count: 2 id: Value Type, count: 3 id: Value Type, count: 4 id: Value Type, count: 5 0 // コピーを作っただけなので値は共有されていない id: Reference Type, count: 1 id: Reference Type, count: 2 id: Reference Type, count: 3 id: Reference Type, count: 4 id: Reference Type, count: 5 5 // 値を共有している構造体は値型であるため、ターゲットのコピーがインスタンスになる。つまり、ターゲットのプロパティの値が変更されても(countの値が5に更新されても)、インスタンスの値はコピーした時点での値のままである。
対して、クラスは参照型なのでインスタンスの値はターゲットと共有している。スタート実行後に値を確認すると反映されているのがわかる。
タイマーアプリのような、実行後の値をインスタンスにも共有したい場合などにはクラスが適切である。2.デイニシャライザを実装したいとき
クラスにしかない機能にデイニシャライザがある。デイニシャライザは型がnilになった時に実行される機能である。
例) 一時ファイルを作成し、削除する。
- ファイルの状態を表現する変数を定義
- インスタンス生成時にファイル作成、インスタンス削除時にファイル削除を定義したクラスを定義
- ファイルを生成、削除を実行し挙動の確認
var temporaryData: String? class SomeClass { init() { print("Create a temporary data") temporaryData = "a temporary data" } deinit { print("Clean up the temporary") temporaryData = nil } } //一時ファイル作成 var someClass: SomeClass? = SomeClass() temporaryData // "a temporary data" // 削除 someClass = nil temporaryData // nil出力結果
Create a temporary data Clean up the temporaryインスタンスの値がnilになるとデイニシャライザが実行されデータが削除される。
メモリに確保されたインスタンスやプロパティはARC(Automatic reference counting)によって自動的に破棄されるので通常はデイニシャライザを記述する必要はないが、ファイルの操作をする場合、自動でファイルを閉じたりしてくれないので、確実に閉じるためにデイニシャライザを利用するケースがある。
3.複数の型でプロパティの中身を共有できる
例) Animalクラスがあり、それを継承したクラスが複数ある。
Animalクラスにはownerプロパティがあり、プロパティオブザーバ(値がセットされた時に実行される機能)が定義されている。継承したクラスのインスタンスのプロパティに値をセットしたい。class Animal { var owner: String? { didSet { guard let owner = owner else { return } print("\(owner) さんが飼い主になりました。") } } } class Bear : Animal {} class Tiger : Animal {} class WildEagle : Animal {} // 値をセットするとプロパティオブザーバが実行 let bear = Bear() bear.owner = "吉田沙保里"出力結果
吉田沙保里 さんが飼い主になりました。クラスの参照機能を使えば親クラスのプロパティを子クラスにも引き継ぐことができる。
これを構造型で実装しようとすると、少々面倒な処理になる。// プロトコルではプロパティの宣言しかできない。 protocol Ownable { var owner : String { get set } } // 継承した各クラスにプロパティの中身を書かなければならない。 struct Dog : Ownable { var owner: String { didSet { print("\(owner)さんが飼い主になりました。") } } } struct Shark : Ownable { var owner: String { didSet { print("\(owner)さんが飼い主になりました。") } } } struct Buffalo : Ownable { var owner: String { didSet { print("\(owner)さんが飼い主になりました。") } } } // インスタンス化時に別の値を入れてから値を更新する var buffulo = Buffalo(owner: "") buffulo.owner = "吉田沙保里"出力結果
吉田沙保里さんが飼い主になりました。プロトコルでストアドプロパティやプロパティオブザーバは定義できない仕様になっているので、参照先の構造体で個別に定義してあげないといけない。また、プロパティオブザーバを実行させるためにインスタンス化時にいったん別の値を入れないといけない。
全然スマートじゃないので、参照元でストアドプロパティやプロパティオブザーバを定義したい場合はクラス型が良い。まとめ
クラスを採用したほうが良いと判断する基準は、
- 参照先で更新されたプロパティをインスタンスにも共有させたいか
- デイニシャライザを使いたいか
- 参照元でプロパティを定義したいか。それを複数の参照先で共有したいか
になるのかなぁ、と書籍を読んで思いました。
- 投稿日:2021-03-05T16:09:23+09:00
【Swift】Compositional Layoutsで実現する疎結合な実装
はじめに
Compositional Layouts
がWWDC2019で発表され、ここ数ヶ月でようやくiOS13以上をターゲットにしたプロジェクトが増えてきたのではないでしょうか?
SwiftUI
を取り入れている技術の記事も目立ってきましたが、iOS14にならないと不自由も多く、最初から機能が豊富なCompositional Layouts
を選択するのも1つの判断かと思います。本記事では実際にプロジェクトに導入してみたので、どのような構成で導入してみたのかをまとめています。Compositional Layouts の優位性
そもそも、
Compositional Layouts
で組むことは、何がメリットなのかというお話をざっくりしておきます。1. UICollectionViewDelegateFlowLayout のデメリット
iOS12 以下で
UICollectionView
を用いて複雑なレイアウトを組む場合、こちらを検討する人が多いでしょう。
UICollectionViewDelegateFlowLayout
をViewController
に継承して、画面の設定を直接書いていきます。Example
final class ExampleViewController: UIViewController, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { /* セルのサイズ */ } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { /* セルの間隔 */ } . . . }この実装にはいくつかの問題があります。その最も大きな問題は、実装する際に
ViewController
と密結合してしまうことです。どうして密結合になってしまうのかというと、それは継承の関連に問題があります。
UICollectionView
の実装ではUICollectionViewDelegate
の実装がほとんどの場合で必要になります。UICollectionViewDelegateFlowLayout
はそれを継承しているため、ViewController
から実装が剥がせないのです。※1これはViewControllerの肥大化にも繋がりよくありません。仮に
enum
やstruct
で設定を定数化して切り出したり、分岐処理を切り出すことはできても、呼び出し部分はどうしてもViewController
に残ってしまいます。※1:
UICollectionViewDelegate
(とさらに親のUIScrollViewDelegate
)のAPIを使用しないなら剥がすこともできますが、その場合はそもそも設計段階でUIScrollView
+UIStackView
を検討する方が適切な可能性があります。2. UICollectionViewLayout のデメリット
UICollectionViewLayout
を継承したカスタムクラスを作成する方法もあります。final class ExampleCollectionViewFlowLayout: UICollectionViewFlowLayout { override func prepare(){ super.prepare() // レイアウトなどの計算 } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { // IndexPathに応じたCellのAttributesを返す } . . . }1の時とは違い
ViewController
と独立して実装できるため疎結合な作りにすることができます。その反面として、APIのライフライクルがやや難しい部分もあり、実装が容易ではないという面をもちます。実際のところ、ほとんどのUIは1で事足りてしまうため、わざわざこちらで実装するのはオーバースペックなことが多く、疎結合にしたいがためにこちらで実装する、といったことを現場ではあまりしないのが実情でしょう。
3. Compositional Layoutsのメリット
ざっくり言えば、上であげた 1と2の良いとこどりできるよ! ってことになります。
- ①のように、ある程度決まった形で書ける
- ②のように、疎結合にできる
という点を兼ね備えています。
また、UIを組む上でも以前と比べてわかりやすくなったという点もあります。
具体的な説明に関しては、たくさん出回っているので説明はしません。
- 時代の変化に応じて進化するCollectionView ~Compositional LayoutsとDiffable Data Sources~今回は、疎結合な実装方法にフォーカスしていきます。
Compositional Layouts + MVP
今回は実装を考えたプロジェクトが
MVP
をベースとした設計のため、それを基本にコードの記載を行っていきますが、Clean Architecture
やVIPER
など、疎結合が実現可能なアーキテクチャーであれば、同じような形で実装を行うことができるでしょう。以下は、実際に運用しているアプリの構成を簡単にまとめたものです。
先に結論の概要から述べてしまうと、上の構成に
Compositional Layouts
を導入するとこのようになります。抽象的なプロトコルとして書き出すことで共通化し、
ViewController
に依存しないように切り出しています。実装
具体的なコードを見ていきます。
1. 通常の実装をする
想像しやすいように、通常の実装からどのように行うかを見ていきます。
以下は、Compositional Layouts
で複数のレイアウトを組む際の簡単な例です。final class ViewControlle: UIViewController { // MARK: Property private lazy var collectionView: UICollectionView = { let collection = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) /* ~ 略 ~ */ return collection }() private lazy var compositionalLayout: UICollectionViewCompositionalLayout = { let layout = UICollectionViewCompositionalLayout { [weak self] (section: Int, environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in // Section番号でレイアウトの分岐 switch section { case 0: return self?.createLayoutA() case 1: return self?.createLayoutB() default: fatalError() } } return layout }() // MARK: Method private func createLayoutA() -> UICollectionViewCompositionalLayout { /* ~ 略 ~ */ return UICollectionViewCompositionalLayout(section: section) } private func createLayoutB() -> UICollectionViewCompositionalLayout { /* ~ 略 ~ */ return UICollectionViewCompositionalLayout(section: section) } }複数のレイアウトを構成したい場合は、レイアウトの数に合わせて、その設定メソッドが増えていきます。
Compositional Layouts
でも、普通に実装した場合はViewControllerの肥大化を招きます。この肥大化を防ぐために、分離していきます。
2. レイアウトの抽象化をする
上記のコードからも分かる通り、
UICollectionViewCompositionalLayout
ではレイアウトをSection番号で分岐できるため、この部分を抽象化して取り出すことで、すっきりとした書き方にすることができます。具体的には、以下のレイアウトがあった場合、図右側のような抽象化を行います。
セクションに共通する処理を整理して抽象化していきます。
例として、抽象化するとこのようになります。protocol SectionProtocol { // セクションのアイテム数 var numberOfItems: Int { get } // レイアウトの生成 func layoutSection(_ view: UIView) -> NSCollectionLayoutSection // セルの生成 func configureCell(_ view: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell // セルタップ時 func selectItem(_ controller: ViewController, at indexPath: IndexPath) }各人の実装によって抽象化されるものは変わるかと思いますが、上記は大体共通して実装することになるでしょう。これはただ抽象化しただけではなく、画面を構成するモデルの役割もはたします。
このプロトコルを各セクションごとに継承し、セクションごとに設定を記載していきます。
struct SectionA: SectionProtocol { let numberOfItems = 1 func layoutSection(_ view: UIView) -> NSCollectionLayoutSection { /* 略 */ return UICollectionViewCompositionalLayout(section: section) } func configureCell(_ view: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { let cell = view.dequeueReusableCell(withReuseIdentifier: "your cell id", for: indexPath) as! SectionACell /* 略 */ return cell } func selectItem(_ controller: ViewController, at indexPath: IndexPath) { // do some action } }こうすることで、
ViewController
からレイアウト部分を、別クラスとして分離することができます。3. ViewControllerから分離する
別クラスとして分離したので、
ViewController
はこのようにすっきりとした形になります。final class ViewController: UIViewController { private var sections: [SectionProtocol] private lazy var collectionView: UICollectionView = { let collection = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) /* ~ 略 ~ */ return collection }() private lazy var compositionalLayout: UICollectionViewLayout = { return UICollectionViewCompositionalLayout { [weak self] section, _ in return self?.sections[section].layoutSection(self ?? .init()) // force cast でも問題ない } }() }また、抽象化した他のプロパティやメソッドは、以下のように呼び出すことができます。
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate { func numberOfSections(in collectionView: UICollectionView) -> Int { sections.count } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { sections[section].numberOfItems } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { sections[indexPath.section].configureCell(collectionView, at: indexPath) } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { sections[indexPath.section].selectItem(self, at: indexPath) } }抽象化されているため、分岐の処理がなくとても綺麗な実装になっています。
4. MVP構成にする
抽象化したセクションのモデル一覧である
private var sections: [SectionProtocol]を
ViewController
からPresenter
に移行するだけです。この部分はMVP構成にするだけなので、構成の仕方に関して記載は致しません。後述のリポジトリを見ていただけると幸いです。
その他
セクション側でアクションの処理を行いたい場合は、
ViewController
からPresenter
をフックしてあげることで、単一方向な処理を実現することができます。具体例として、セルをタップしたい際の挙動をあげておきます。
protocol Presentable: AnyObject { var sections: [SectionProtocol] { get } func selectItem(at indexPath: IndexPath) } final class Presenter: Presentable { private var sections: [SectionProtocol] func selectItem(at indexPath: IndexPath) { // do someting } }final class ViewController: UICollectionViewDelegate { private(set) var presenter: Presentable! /* 略 */ func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { presenter.sections[indexPath.section].selectItem(self, at: indexPath) } }struct SectionA: SectionProtocol { /* 略 */ func selectItem(_ controller: ViewController, at indexPath: IndexPath) { controller.presenter.selectItem(at: indexPath) } }先ほどの図に照らし合わせると、
このような関係になっています。
このように、抽象化して分離することで、
ViewController
の肥大化を防ぎつつ、疎結合な作りを実現することができるのです。終わりに
少しコードが多くなってしまい、わかりづらい部分もあるかもしれません。
動作するリポジトリを置いておくので、こちらからコードを読んでいただけると幸いです。
- CompositionalLayouts-MVPまた、この実装は
Compositional Layouts
をiOS12以下で使用するためのバックポートライブラリである
- kishikawakatsumi / IBPCollectionViewCompositionalLayoutをベースにしています。
そちらをみていただくと、より理解を深めることができます。
ご指摘などありましたらコメントいただけると幸いですmm
- 投稿日:2021-03-05T12:25:53+09:00
Charlesの便利な機能・使い方
Charles は HTTP(S) 通信の内容を閲覧したり、改変したりすることができるツールです。
Android や iOS のアプリ開発などで通信のデバッグをする際に非常に役に立つツールですので、便利な機能や使い方についてご紹介したいと思います。
注意点
この記事では Charles の導入・設定方法についての解説は省略させていただきます。
私が Mac ユーザーですので、記事内で紹介しているショートカットキーや画面キャプチャは Mac のものとなります。
また、Charles のバージョンは v4.6.1 時点のものとなります。
通信の閲覧
Structure と Sequence タブ
通信を閲覧する際に
Structure
とSequence
の2種類のタブがあります。
Structure
では Host ごとにグルーピングされて表示され、Sequence
では指定した順番に表示されます。Structure
Sequence
リクエスト、レスポンス情報の表示
画面上部にある
Contents
タブを選択するとリクエストヘッダ、レスポンスヘッダ、レスポンスボディを閲覧することができます。リクエストヘッダ、レスポンスヘッダ
以下のキャプチャの右側上部がリクエストヘッダの情報、右側下部がレスポンスヘッダの情報になります。
それぞれ
Headers
タブを指定することにより情報を見ることができます。レスポンスボディ
以下のキャプチャでは Qiita の API を使用しており、レスポンスボディには JSON 形式の文字列が返されます。 この場合
JSON Text
タブを選択することによりレスポンスボディの内容が確認できます。通信の改変
Throttling
通信速度の設定ができます。
通信が悪い状況を確認したい場合に使えます。
通信速度の設定
メニューの
Proxy
>Throttle Settings...
で設定画面が開きます。ショートカットメニューだと
shift + command + T
です。
Throttole preset
- 設定のパターンを選択することができます。いくつかデフォルトで設定のパターンが用意されています
Add Preset
- 好みの設定のパターンを追加することができますEnable Throttling
- チェックを入れると設定が有効になります
Only for selected hosts
- 指定したホストのみ設定を有効にすることもできます
通信速度設定の有効・無効切り替え
設定の有効・無効の切り替えは以下の方法でも可能です。
- ショートカットキー
command + T
- メニューの
Proxy > Start Throttling / Stop Throttling
- 亀のボタンによる切り替え(以下のキャプチャ参照)
Breakpoint
指定した通信のリクエスト / レスポンス時にブレークポイントを設定することができます。
ブレークポイントが設定された通信のリクエストが実行される前、もしくはレスポンスを受け取る前にブレークポイント画面が表示され、リクエストやレスポンスの内容を変更することが可能です。
私はよくレスポンスにブレークポイントを設定し、Web サーバーにリクエストは届くけどレスポンスは受け取れなかった状況を確認する際に使用したりしています。
ブレークポイントの設定
メニューの
Proxy
>Breakpoint Settings...
で設定画面が開きます。ショートカットメニューだと
shift + command + K
です。以下が設定画面になります。
Enable breakpoints
にチェックを入れるとブレークポイントの設定が有効になります。
Add
から Breakpoint に設定したい通信を設定します。ブレークポイントの設定は
Request
とResponse
の設定が可能です。(両方同時に設定も可能です)Charles で閲覧済みの通信をブレークポイントに設定するのであれば、その通信を選択して右クリックし
Breakpoints
を選択すると簡単です。ブレークポイントの有効・無効切り替え
ブレークポイントの有効・無効の切り替えは以下の方法でも行えます。
- ショートカットキー
command + K
- メニューの
Proxy
>Enable Breakpoints / Disable Breakpoints
- 六角形のボタンによる切り替え(以下のキャプチャ参照)
ブレークポイントの画面
ブレークポイントで指定された通信のリクエストやレスポンスが実行されるタイミングで、自動的にブレークポイントの画面が表示されます。
リクエスト時のブレークポイント
リクエスト時のブレークポイントの画面は以下のようになっています。
画面上部に
Edit Request
タブがあり、こちらに切り替えるとリクエストの編集が行えます。以下のキャプチャでは
URL
の編集が行えるようになっています。HTTP メソッドやパス、クエリパラメーターの変更や、クエリパラメーターの追加・削除などが行えます。
画面下部にあるタブを
Headers
に切り替えると、リクエストヘッダの編集も行えます。編集が完了したら画面下部にある
Execute
、Abort
、Cancel
のいずれかのボタンでその後の通信処理の動作を選択できます。
Execute
- 編集で変更した内容を適用した上で通信処理を継続する
Abort
- 通信を失敗させる
Cancel
- 編集で変更した内容を破棄して通信処理を継続する
レスポンス時のブレークポイント
レスポンス時のブレークポイントの画面は以下のようになっています。
画面上部に
Edit Response
タブがあり、こちらに切り替えるとレスポンスの編集が行えます。以下のキャプチャでは レスポンスヘッダの編集が行えるようになっています。
レスポンスボディが JSON 形式の文字列で返される場合、画面下部にあるタブを
JSON Text
に切り替えるとレスポンスボディの編集も行えます。編集完了後はリクエスト時と同様に
Execute
、Abort
、Cancel
のいずれかのボタンを押してその後の動作を選択します。ブレークポイントによる編集はちょっと変更したい時には便利ですが、変更内容が多かったり通信のたびに何度も変更する必要がある場合は不便です。
その場合は後述する
Rewirte
やMap Local
を使うことをオススメします。Map Remote
Map Remote ではリクエスト先を変更することができます。
例えばアプリ開発でクライアントも Web API も一人で開発していて、Web API の変更をクライアント側で試したい場合に接続先をローカルに立ち上げたサーバに変更することなどができます。
メニューの
Tools
>Map Remote...
で設定画面が開きます。ショートカットメニューだと
option + command + M
です。
Enable Map Remote
にチェックを入れると Map Remote の設定が有効になります。
Add
ボタンをクリックすると設定を追加することができます。
Map From
に既存の通信先を指定し、Map To
で変更したい通信先を指定します。この例だと
https://qiita.com
の通信をローカルに立ち上げたサーバhttp://localhost:3000
に接続先を変更しています。Map Local
Map Local は指定されたローカルファイルの中身を読み込んでレスポンスボディとして返す機能です。
レスポンスボディの内容を自由に設定することができるので、デバッグする際などにはとても重宝します。
メニューの
Tools
>Map Local...
で設定画面が開きます。ショートカットメニューだと
option + command + L
です。
Enable Map Local
にチェックを入れると Map Local の設定が有効になります。
Add
ボタンをクリックすると設定を追加することができます。
Map To
のLocal path
にレスポンスボディとして返したい内容を記述したファイルのパスを設定します。閲覧済みの通信を Map Local で設定するのであれば、通信を選択して右クリックして
Map Local
を選択すると簡単です。No Caching
リクエストヘッダ、レスポンスヘッダのキャッシュに関する設定を変更して、キャッシュを無効にしてくれます。
メニューの
Tools
>No Caching...
で設定画面が開きます。ショートカットメニューだと
option + command + N
です。
Enable No Caching
にチェックを入れることで、キャッシュが無効になるように設定されます。
Only for selected locations
にチェックを入れることで、Location
に設定された通信先のみが無効になるように設定することも可能です。Block List
Block List では指定された通信をブロック(失敗)させます。
メニューの
Tools
>Block List...
で設定画面が開きます。ショートカットメニューだと
option + command + B
です。
Enable Block List
にチェックを入れると、Location
に指定されている通信がブロックされます。
Blocking action
で失敗させる際の挙動を設定でき、以下の2つのパターンがあります。
Drop connection
Return 403 Response
Add
ボタンを押すと、Block List への追加設定が行えます。以下のキャプチャの下部に説明がありますが、空のフィールドは全ての値にマッチするようになっています。
また、
*
や?
によるワイルドカードの指定が可能です。Allow List
Allow List は Block List とは逆に指定された通信だけを許可し、それ以外の通信はブロック(失敗)させます。
メニューの
Tools
>Allow List...
で設定画面が開きます。ショートカットメニューだと
option + command + W
です。設定方法自体は Block List と同じなので、設定方法の説明は割愛いたします。
Rewrite
Rewrite は指定された通信のリクエスト、レスポンスの内容を変更するための機能で、以下のことが行えます。
- リクエストヘッダ、レスポンスヘッダへの値の追加、削除、変更
- ホスト、パスの変更
- クエリパラメータの追加、削除、変更
- レスポンスのステータスコードの変更
- ボディの変更
Rewrite はメニューの
Tools
>Rewirte
で設定画面が開きます。ショートカットメニューだと
option + command + R
です。画面左側には設定した内容の一覧が表示されます。
左側で設定した内容の一覧を選択すると、画面右上に
Location
の一覧(どの通信を Rewrite 対象にするか)、右下には Rewrite する際のルールが表示されます。
Location
の設定は Block List と同じなので割愛します。Rewrite のルールを設定する際の画面は以下のようになっています。
Type
で変更したい内容を選択でき、以下の項目が選択可能です。
Match
とReplace
の項目にそれぞれ値を設定することで、変更する内容を設定することができます。先ほどのキャプチャの例だと、
per_page
のクエリパラメータの値を 10 に設定するようにしています。正規表現を使用することもできるので、値の変更を行う際にはある程度柔軟に設定することも可能になります。
その他機能
Copy cURL Request
curl コマンドをクリップボードにコピーしてくれます。
例えば特定の Web API に何か問題が見つかってチームメンバーに共有する必要があった場合に便利で、 curl コマンドを共有することによって解決が容易になることが多いので、ぜひとも使ってみてください。
- 特定の通信を選択して右クリックする
Copy cURL Request
を選択する(キャプチャ参照)実行した結果、以下のようにクリップボードにコピーされます。
curl -H 'Host: qiita.com' -H 'user-agent: okhttp/4.9.1' -H 'if-none-match: W/"6687ae9470c7ca50e98cf521b5ec1291"' --compressed 'https://qiita.com/api/v2/items?page=1&per_page=20'Copy Response
レスポンスボディをクリップボードにコピーしてくれます。
前述の Map Local でローカルに配置するファイルを作成する際などに使えます。
- 特定の通信を選択して右クリックする
Copy Response
を選択する(キャプチャ参照)Repeat / Repeat Adcanced
Repeat
では指定した通信を再度リクエストします。
Repeat Advanced
ではリクエストをリピートする回数やその際の並列数、リクエスト間隔を指定することができます。
- 特定の通信を選択して右クリックする
Repeat
/Repeat Advanced...
を選択する(キャプチャ参照)まとめ
以下に今回紹介した機能をまとめてみました。
機能名 概要 Throttling 通信速度の設定 Breakpoint 指定した通信にブレークポイントを設定し、リクエスト・レスポンスの内容を変更して実行したり、通信自体を中断させることができる Map Remote 接続先の変更 Map Local 指定されたローカルファイルの内容を読み込んで、それをレスポンスボディとして返す No Caching キャッシュを無効にする Block List 指定した通信をブロックする Allow List 指定した通信のみを許可し、それ以外をブロックする Rewrite リクエスト・レスポンスの内容を変更する Copy cURL Request curl コマンドをクリップボードにコピーする Copy Response レスポンスボディの内容をクリップボードにコピーする Repeat / Repeat Advanced 通信の再リクエストを行う 最後に
以上、いくつか Charles の機能を紹介させていただきました。
Charles は今回紹介した機能以外にも便利な機能があるので、ドキュメントなどを参考にしてみてください。
Charles を使う際にこの記事が参考になっていただければ嬉しいです。
参考
- 投稿日:2021-03-05T08:26:20+09:00
デザイナさんと一緒に検討すること・検討しておいて欲しいこと
アプリ開発をしていて設計・実装者の立場から、デザイナ・UIUX担当との連携の初期段階で共有しておきたいことをリストアップしました。
要望をデザインに落とし込む手法とかとかは専門外なので、ここでは設計・実装者の立場から、デザイナ・UIUX担当との連携の中で漏らしてしまいそうな検討事項をまとめてます。
1. 一緒に検討したい仕様
まず、設計・実装上の理由から「こんな場面があるのでUIUX・デザインを考えて欲しい」という項目。
アクセス許可の取得箇所と許可してもらうための工夫
- 必要とするアクセス許可とその理由をデザイナと共有して、画面遷移のどこに挟むのがいいのか、なるべく許可してもらうための説明方法(ダイアログだけでいいのか、画面案内にしたがって許可を促すのか等)を検討。
情報がない場合のデザイン
- サーバからデータ取得中で情報がない場合の見せ方
初回起動で情報がない、キャッシュが古いのでキャッシュ表示はできない、という場合のデザイン- 表示件数が0件の場合の見せ方
- サーバエラーや通信エラーで表示するものがない場合の見せ方
サーバアクセス中や時間のかかる処理中のインジケーター
- 画面操作をブロックしてはダメな場所、良い場所(ブロックせざるを得ない場所)を振り分ける
- 操作ブロックしてはダメな場所でのインジケーターの見せ方(画面の一部をぐるぐる表示、画面の一部にプログレスバー配置等)
- 操作ブロックする場合、そのキャンセル方法の検討
- キャンセルボタンを配置
- ヘッダ等の戻るボタンタップでキャンセル
- 画面のどこかをタップするとキャンセルダイアログ表示、等
- 進捗付きのインジケーターにするか・否か
アプリ起動時に表示する画面
- アプリ強制終了時の画面を表示するのか(操作の継続性を優先)
- 一定時間が経っていたら最新情報が集まる画面等、なるべく見せたい画面を表示するのか
- 常に決まった画面を出すのか
表示する情報の更新方法
- 表示する情報の賞味期限により、更新の仕方が変わるので情報を分類。
- ログ情報・・・過去の出来事で変わらない情報。例)利用履歴、購入実績
- 即時情報・・・更新されたら即座に伝える必要がある情報。例)残額、住所等の個人情報
- 期限付き情報・・・一定頻度で更新が必要な情報。 例)セール情報、レコメンド情報
- 分類した情報のシチュエーション別の更新要否判断
- リロードボタン/プルリフレッシュ等、利用者操作による更新要否
- アプリ起動時の情報更新要否
- バックグラウンドでの情報更新要否
- アプリフォア時の情報更新要否
- 一定時間での自動更新要否
- 情報のキャッシュ要否
分類した情報別にキャッシュの有無、キャッシュ期間を決めるオフラインでできること、できないこと
- 画面毎にオフラインでの制約事項の明確化と、その際のUIUX・デザインを決める
検索機能の具体的な動作
- インクリメンタルサーチの必要性判断
アプリストアのレビュー訴求
- ★評価アラートを表示する条件とタイミングを決める
DynamicType対応要否
工数がかかるし、後で対応するのも大変なので関係者納得の理由で判断アイコンバッジに表示する件数の定義
Appスイッチャー/タスクスイッチャーの状態で表示する画像
- セキュリティ等の対策のため、OSのAppスイッチャーにスクリーンショットを表示したくない場合、何を表示しておくか
その他
- オープンソースライセンス表記どこにするか?
- VoiceOver/TalkBack対応要否
2. デザイナさん主体で決めてもらいたい仕様
ここはデザイナ・UIUX担当さんに忘れずに考えて欲しい項目。
- 条件を満たさない場合のボタンタップ可否ルール
- 非活性にして押させないのか、押させて動作しない理由の説明表示をするのか。
- 長いテキストの表示ルール
- 右端で折り返して全部表示するのか、文字を縮小してでも表示するのか。
- 行数の上限を設けるのか。
- 行数の上限を設ける場合、「…」はどこに入れるのか、いれないのか。
- 数値の場合、桁不足をどう見せるか。(NNN+とか)
- 日時のフォーマットとそれを使う場所のルール
- 202X年01月01日、202X年1月1日。202X/1/1
- 00時00分、00:00
- 12時間表記、24時間表記
- あとX日、あとX時間
- X日前、X時間前
- タップ以外での画面遷移
- スワイプで画面遷移できる画面とできない画面の明確化
- エッジスワイプで戻れる画面の明確化
- 表示要素のスケール
- 大きな画面では、大きく見せたいのか、たくさんの情報を見せたいのか。
大きく見せたい場合、各種アイコン・フォントのサイズは追従させるのか、させないのか。
追従させるなら、エンジニアと一緒に追従させる部分を決める(追従させる範囲はなるべく限定したい)- 小さい画面の場合にどう見せるのか。
構成要素を減らすのか、アイコン+フォントの組み合わせならアイコンだけにするとか。- 表示する画像のアスペクト比がバラバラな場合の表示
- 異なるアスペクト比の画像をどう敷き詰めるのか(グリッド状にするのか、隙間なく敷き詰めるのか)、Aspect Fit/Aspect Fillはどうするか。
- ダークモード/ダークテーマの対応要否
- 対応する場合、配色のルール
- 対応するデバイスの向きとデザイン
- スマホの縦横の専用デザインの有無
- タブレットの縦横の専用デザインの有無
- マルチウィンドウ対応要否(SlideOver、SplitView)
- アラート
- はい・いいえ、OK・キャンセル・閉じる or OK の文言の適用ルール
- はい・いいえ の配置ルール(破壊的/非破壊的の考慮要否)
- その他
- 既存アプリと同様のアクションをさせたいなら『XアプリのY部分と同じ』のようなデザイン指示をお願いしておく。
3. 進め方について合意しておきたいこと。
スムーズに作業を進めるためにデザイナさん(&プロジェクト全体)で合意しておく項目。
- デザイン環境
- デザインツール
開発するアプリの特徴にもよるが、なるべく意思疎通しやすいように動くもので確認したい。デザイナには高度な紙芝居ができるツール(AdobeXDとか)を使って欲しい(プロトタイプとして関係者に確認してもらいやすい)。- ツールの利用にライセンスが必要なら必要なラインセンス数の確認。
- デザインデータのバージョン管理方法
チケット・イシューと関係づけてどう管理するのか、変更箇所をどう記録するか、ツールが備えている機能と合わせて決める。- テキスト/メッセージの共有方法
ダイアログに表示するメッセージやテキストを一覧化し開発者とデザイナで共有する。トンマナ・文言見直しに共有が必要。多言語対応の場合は全てのテキストを共有。- デザインツールで使っている色空間(sRGB、AdobeRGB、Display P3)
実装時に色空間を合わせておかないと微妙にデザイン色と変わってしまうので注意。- 関係者との合意タイミング
- 表示要素・ワイヤーの決定タイミング。
これらが決まった後にサーバ側に用意してもらうAPI要件をまとめたりするため。- スマホ版/タブレット版のどちらを優先するか・同時にデザインするのかの順番、及び、先行する側を後続側で取り込むタイミング
- トンマナ・文言の見直しタイミング
- UIUX・デザイン承認者との合意タイミング、再レビューのタイミング
最後に
デザイナさんと絡んだ経験は多くないので他にも検討すべきことは多々あると思いますが、このリストが少しでも役立てば幸いです。あと、こんな検討も必要だよね、というものがあれば指摘もらえると助かります。