- 投稿日:2019-01-27T23:11:39+09:00
[cocoa][swift]作譜用言語PL/0 構文に対する分析子の作成
有名な『Algorithms + Data Structures = Programs』の後半を独立して誕生した『COMPILERBAU:』を翻訳した『翻訳系構成法序論』を今の電子計算機環境で取り組んでみた。
使用するプログラミング言語Swiftを選択したのだが、コンパイラの実装には少々向いていない部分があるので、まずは、一文字読み込んで処理するサンプルを記述してみた。
import Foundation let parser = Parser()import Foundation class Parser { var ch: Character = "\0" var lineString: String = "" init() { readChar() S() } func readChar() { lineString = readLine()! ch = lineString[lineString.index(lineString.startIndex, offsetBy:0)] lineString = String(lineString.suffix(lineString.count - 1)) } /* 開式記号に対応する手続き。 */ func S() { } }それでは、教科書のサンプルコードを記述してみよう。
以下の約束事があるとする。
A="x"|"("B")". B=AC. C={"+"A}.これは以下のようになる。
x (x) (x+x) ((x)) ((x+(x+x)))
/* 分析子 */ class Parser { var ch: Character = "\0" var lineString: String = "" init() { print("\(#function)") readChar() A() } func A() { //print("\(#function)") if ch == "x" { readChar() } else if ch == "(" { readChar() A() while ch == "+" { readChar() A() } if ch == ")" { readChar() } else { error() } } else { error() } } func readChar() { if lineString.count <= 0 { lineString = readLine()! } ch = lineString[lineString.index(lineString.startIndex, offsetBy:0)] lineString = String(lineString.suffix(lineString.count - 1)) print(ch) } func error() { print("error: \(#function)") exit(-1) } }ここまでは、約束事を愚直にコードで処理しているという感じだ。
ソースコード
GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/mac/pl0 - GitHub【関連情報】
Cocoa.swift 2019-02
Cocoa.swift
Cocoa勉強会 関東
MOSA
Cocoa練習帳
Qiita
- 投稿日:2019-01-27T23:04:02+09:00
FlexLayoutを使用してタグ一覧画面を実装してみる
はじめに
タグリスト?どうやって作ろう?
イメージは以下スクショのようなものでどう実装しようか調べた際にFlexLayoutを見つけ実際に使ってみて簡単に実装できたので実装方法についてまとめてみました!
(最近自分の調理周りを改善に力を貸してくれたアプリのTag画面から引用させていただきましたmm)FlexLayoutの特徴
AutoLayout,UIStackViewと比べるとハイパフォーマンス
直感的に書ける
一度CSSのFlexboxを触れたことはあったのも恩恵としてあったと思いますが、
どう表現したいのか(横に追加する感じで並べたい?下に追加する感じで並べたい?)が直感的にできた。
またチェインで書けるのも直感的、気持ち良さの観点から良かったです!FlexboxとFlexLayoutでの対応表があるのも助かりました!
タグリストを実装していく
イメージ
タグリストの特徴
・横に並べていく
・画面に収まらない場合は折り返す
Tagを表示するUIViewを配置する
Tagを表示する範囲を指定するためのUIViewを用意します。(以後このUIViewをContainerと呼びます)
以下スクショでは範囲をわかりやすくするためにContainerの背景を黄緑色にしてます。
Storyboardで配置したTag表示用のContainerをViewControllerに紐づける
イメージ通りにTagを表示するロジック部分
タグリストの特徴
・横に並べていく
・Tag表示用のUIViewからはみ出る場合は折り返す
・横に並べていく
directionにrowを指定してあげると良さそう
・Tag表示用Containerからはみ出る場合は折り返す
wrapを指定してあげると良さそう
・横に並べていく -> direction(.row)
・Tag表示用のUIViewからはみ出る場合は折り返す -> wrap(.wrap)コードに起こしてみる
private func setupTags(_ tags: [String]) { self.tagContainerView.flex.direction(.row).padding(8).wrap(.wrap).define { flex in tags.forEach { let tagLabel = UILabel() tagLabel.text = "#\($0)" tagLabel.backgroundColor = UIColor.white tagLabel.layer.borderWidth = 1.0 tagLabel.layer.cornerRadius = 4 // タグ間隔のマージン指定 flex.addItem(tagLabel).margin(8) } } }Tag表示用のContainerの高さ設定
Containerの高さはTagの数とTagの文字列の長さに影響されるので高さを自動で決めるように設定する
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() self.updateFlexLayout() } private func updateFlexLayout() { self.tagContainerView.flex.layout(mode: .adjustHeight) }Tagを表示させてみる
override func viewDidLoad() { super.viewDidLoad() self.setupTags([ "噂の人気商品", "発売までもう少し", "インスタ栄え", "SSS", "地味に知られてない", "トレンド商品", "とりあえず最前線" ]) }こんな感じになりました
Containerの高さを自動で調整するよう設定しているので高さもほど良い!
わかりやすく背景を黄緑にしてみました!self.tagContainerView.flex.layout(mode: .adjustHeight)おまけ
良く使うアプリでこれもFlexLayoutでいけるんじゃないか?と思い簡易的に試してみた。
とりあえずdirectionをcolumnにしたらそれっぽくなるやろえい!
private func updateFlexLayout() { self.tagContainerView.flex.layout(mode: .adjustHeight) } private func setupTags(_ tags: [String]) { self.tagContainerView.flex.direction(.column).define { flex in tags.forEach { let tagLabel = UILabel() tagLabel.text = "\($0)" tagLabel.backgroundColor = UIColor.white tagLabel.layer.borderWidth = 1.0 tagLabel.layer.cornerRadius = 4 flex.addItem(tagLabel).margin(8) } } }さいごに
どうだったでしょうか?
今までなんとなく目にしてきたタグリストって意外と簡単にできそうだなって思いませんか?今回のはデザインをしっかり当てているわけではないので
let tagLabel = UILabel()な部分を独自のCustomViewTagLabel()とか定義してあげると見栄えも良くなるかと思います!FlexLayoutのREADMEが丁寧!ありがとうございます!!!
投稿して改めて気づいたQiitaさんもしっかりtag一覧ありましたね!mm
多分これはdirection(.row),wrap(.wrap)パターンだ!
↓
- 投稿日:2019-01-27T21:59:26+09:00
部分配列取り出し関数で時刻データを取り出してみた
はじめに
先日作った 部分配列の取り出し関数 のアルゴリズムを使って
時刻データを取り出す関数を作成した。実装
ソースコードfunc getElements(elements: Int, array: [Date]) -> [Date] { let cnt = round(target: elements, min: 0, max: array.count) return [Date](array[0..<cnt]) }テストコード(elements: -1, array: [makeTimeData(10,10), makeTimeData(11,11), makeTimeData(12,12), makeTimeData(13,13), makeTimeData(14,14)]), [], "取出個数 下限値未満") (elements: 0, array: [makeTimeData(10,10), makeTimeData(11,11), makeTimeData(12,12), makeTimeData(13,13), makeTimeData(14,14)]), [], "取出個数 下限値未満") (elements: 1, array: [makeTimeData(10,10), makeTimeData(11,11), makeTimeData(12,12), makeTimeData(13,13), makeTimeData(14,14)]), [makeTimeData(10,10)],"取出個数 下限値") (elements: 5, array: [makeTimeData(10,10), makeTimeData(11,11), makeTimeData(12,12), makeTimeData(13,13), makeTimeData(14,14)]), [makeTimeData(10,10), makeTimeData(11,11), makeTimeData(12,12), makeTimeData(13,13), makeTimeData(14,14)],"取出個数 上限値") (elements: 6, array: [makeTimeData(10,10), makeTimeData(11,11), makeTimeData(12,12), makeTimeData(13,13), makeTimeData(14,14)]), [makeTimeData(10,10), makeTimeData(11,11), makeTimeData(12,12), makeTimeData(13,13), makeTimeData(14,14)], "取出個数 上限値超過") (elements: -1, array: []), [], "取出個数 下限値未満") (elements: 0, array: []), [], "取出個数 下限値未満") (elements: 1, array: []), [], "取出個数 下限値") (elements: 5, array: []), [], "取出個数 上限値") (elements: 6, array: []), [], "取出個数 上限値超過")実行結果0 failures結果
予定通りの動作が完成した。
調べずに作ってしまったが、C++のテンプレート的なものがあったのかもと
今更ながらに思ってしまった。。。次回はfilter部分をDate化する。
テンプレートの調査はもう少しあとで。。。
- 投稿日:2019-01-27T21:03:52+09:00
@escapingを理解するための簡単なサンプルコード
クロージャがスコープから抜けても存在し続けるときに @escaping が必要になります。
具体的には以下のような場合です。
- クロージャがスコープ外で強参照されるとき
- クロージャを非同期的に実行するとき
よってクロージャをすぐに実行し、どこからも強参照されない場合は @escaping は必要ありません。
class ViewController: UIViewController { var myCallback: () -> () = {() in } override func viewDidLoad() { super.viewDidLoad() self.first(callback: { () -> Void in print("hello first callback") }) print("before myCallback") myCallback() } func first(callback: @escaping () -> ()) { print("start first function") self.second(call2: { () -> Void in print("hello second callback") callback() }) } func second(call2: @escaping () -> ()) { print("start second function") self.myCallback = call2 } }参考
- 投稿日:2019-01-27T20:59:43+09:00
UITabBarControllerをアイコン&文字列のToolbarとして使う方法
UITabBarControllerは本来であればTabを押すことで表示されているViewControllerを切り替えるために使用します。
ViewController内部でメニューボタンとして使用するのであれば、多くはUIToolbarで構築します。
しかし、UIToolbarではアイコンもしくは文字列のいずれかしか表示されません。
もしアイコンと文字列の両方を表示させたいのであれば、カスタムViewを作成することも考えられます。しかし今回はあえてUITabBarControllerを使用し、Tabを押すとメニューボタンとして機能するようにします。
タブをメニューボタンにする
タブを選択してからViewControllerを切り替えるか判断するためのデリゲートメソッドのtabBarController(_:shouldSelect:)
を使用します。メニューのためのダミーViewController(ここではDummyMenuViewController)タブが押下されたときは、
ViewControllerの切り替えを抑制して、メソッドを実行するようにします。func tabBarController(_ tabBarController: UITabBarController, func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { switch viewController { case is FirstViewController: // trueを返すため、表示される break case is DummyMenuViewController: let vc = viewController as! DummyMenuViewController switch vc.menuType { case .bookmark: showBookMarkMenu() case .contacts: showContactsMenu() } // ViewControllerの表示を抑制する return false default: break } // ViewControllerを表示する return true }以下全ソース
import UIKit class MainTabBarController: UITabBarController { var tabViewControllers: [UIViewController] = [] var mainViewController: UIViewController? required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.delegate = self caseSeparateViews() self.setViewControllers(tabViewControllers, animated: false) } func caseSeparateViews() { let storyboard = UIStoryboard(name: "Main", bundle: nil) let firstVC = storyboard.instantiateViewController(withIdentifier: "FirstViewController") firstVC.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarItem.SystemItem.mostRecent, tag: 1) firstVC.tabBarItem.badgeValue = "1" firstVC.tabBarItem.badgeColor = .green tabViewControllers.append(firstVC) // 表示はしないがタブを表示するためのダミーViewController let secondViewController = DummyMenuViewController() secondViewController.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarItem.SystemItem.bookmarks, tag: 2) secondViewController.menuType = .bookmark tabViewControllers.append(secondViewController) let thirdViewController = DummyMenuViewController() thirdViewController.menuType = .contacts thirdViewController.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarItem.SystemItem.contacts, tag: 3) tabViewControllers.append(thirdViewController) self.mainViewController = firstVC } func showBookMarkMenu() { let alert = UIAlertController(title: "Bookmark", message: "", preferredStyle: .actionSheet) let newAction = UIAlertAction(title: "新規", style: .default) { (action) in print("new") } let editAction = UIAlertAction(title: "編集", style: .default, handler: nil) let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil) alert.addAction(newAction) alert.addAction(editAction) alert.addAction(cancelAction) self.mainViewController?.present(alert, animated: true, completion: nil) } func showContactsMenu() { self.mainViewController?.view.backgroundColor = .yellow } } extension MainTabBarController : UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { switch viewController { case is FirstViewController: // trueを返すため、表示される break case is DummyMenuViewController: let vc = viewController as! DummyMenuViewController switch vc.menuType { case .bookmark: showBookMarkMenu() case .contacts: showContactsMenu() } // ViewControllerの表示を抑制する return false default: break } // ViewControllerを表示する return true } } class DummyMenuViewController: UIViewController { enum MenuType { case bookmark case contacts } var menuType: MenuType = .bookmark override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. self.view.backgroundColor = .blue } } class FirstViewController: UIViewController { @IBOutlet weak var label: UILabel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. self.view.backgroundColor = .red } }
- 投稿日:2019-01-27T14:34:27+09:00
リソースとシングルトン、あるいはマネージャー
対象読者
タイトルのキーワードに興味を持ってくれた方。
Swiftが途中で出てきますが、特にiOSに限った話ではありません。
シングルトンは危険なので使わない方が良いと考えている方(ひょっとしたら役に立つかも?しれません)注意
記事内で
システムという単語がよく出てきますが、組み込みシステムとかモバイルアプリケーションとかWebシステムなどを包括したものと捉えてください。シングルトン
シングルトンは分かりやすい考え方なので、ご存知の方はとても多いと思いますが、(最近特に近所で)よく「シングルトンは使うと危険だから封印」みたいな話を聞きます。1
しかしながらシングルトンはそれほど忌避すべきもののようには思えません。少しこのあたりから書き始めたいと思います。まず押さえておきたいのは、シングルトンとシングルトン・パターンは異なるものだということです。
シングルトンは、そのシステムで固有の責務を担ったクラスの唯一のインスタンスという意味です。(かなり個人的な定義です)
シングルトン・パターンは、上記のシングルトンを実装する場合のベストプラクティス、デザインパターンです。2
http://www.techscore.com/tech/DesignPattern/Singleton.html/シングルトンの危険性は、シングルトンとして使うことを想定した実装なのにそうだと知らずに別のインスタンスを作ってしまうケースがあります。しかしながら、それは言語や実装の問題であってシングルトンの問題ではありません。実はこのような問題を避けるパターンが、シングルトン・パターンです。
例えばSwiftではシングルトン・パターンは下記のようになります。
public final class Singleton { static public let shared = Singleton() private init(){} }イニシャライザーを呼び出せないことで、shared関数を使ってインスタンシエートするしかありません。
このため、Singletonクラスの別のインスタンスをインスタンシエートすることは不可能です。
ただしこのようなシングルトンは継承が使えないので、必ずfinalになります。気をつけるべきは、あるインスタンスがそのクラスの唯一のインスタンスであったとしても、他のクラスと責務が重複していればシステムに甚大な障害をもたらす可能性が高いことです。シングルトンがシングルトンたる所以は、
排他的な責務をただ一つのインスタンスが負うていることです。例えばメモリー管理の例で言えば、あるシングルトンのメモリー管理モジュールが、実メモリーを確保してアプリケーションに渡したとしましょう。しかし同時に全く別のクラス・インスタンスが同一アドレスを別の用途に使えば、システムは容易に破壊されます。
まず、適切な責務の分割を考えること(基本設計)がシングルトンを使う上でのポイントです。また、下記のような実装はアンチパターンです。
public final class Singleton { static public let shared = Singleton() private init(){} public var value: Int? // <-- ここ }例えばスレッドセーフにするためには、変数に対するCRUD3ではセマフォが必要なことがあります。Swiftの場合は、下記のように書くか、get/set関数で実装しましょう。
ちなみに下記のような変数の処理で事足りる責務で、自分はシングルトンは使わないと思うので、やるとすれば全て関数で実装すると思います。
public final class Singleton { static public let shared = Singleton() private let semaphore = DispatchSemaphore(value: 1) // セマフォ。この場合はバイナリーセマフォまたはMutexと呼ばれる。 private init(){} private var privateValue: Int = -1 public var value: Int { get { semaphore.wait() defer { semaphore.signal() } // 処理アレコレ return privateValue } set { semaphore.wait() defer { semaphore.signal() } // 処理アレコレ privateValue = newValue } } }リソース
シングルトンはひとまず置いておいて、次にリソースについて考えてみます。
経営的な視点では、「ヒト、カネ、モノ」がリソースです。いわゆる経営資源という意味のリソースです。
Web的な視点では、URL(またはURI)がリソースです。厳密に言えばURLが指し示している先の事物がリソースです。URLとは「Uniform Resource Locator」の略でURIは「Uniform Resource Identifier」の略です。
いずれにしても、
・有限の事物である
・識別が可能である
という条件には当てはまります。では、プログラミングの世界におけるリソースは何でしょうか?
ハードウェアリソース
iOSにようなモバイルアプリケーションを考える前に、分かりやすい例として「組み込み」の世界を考えてみます。
「組み込み」の世界ではハードウェアリソースは限定されているので、ほぼ全てのハードウェアがリソースとして認識されます。
- メモリー
- CPUの処理能力
- ディスプレイの数
- 位置、ジャイロ、加速度など各種のセンサー類
- グラフィックエンジン
- WiFi(デバイス)
などがそうです。
しかし、モバイルやPCのエンジニアは特殊な例を除いて
メモリーやCPUの処理能力を気にするエンジニアはいないかもしれません。それはCPUに備わった仕組みやOS、コンパイラによって、エンジニアがさほど意識しないでも容易に扱えるからです。このような場合はメモリー容量やCPUの処理能力はリソースでは無いと考えてよいと思います。本当は限りがあるけど、あたかも無限に存在するかのように見える上、ほぼ管理の必要が無いからです。4ハードウェアリソース管理のようにリソースが限定されており、排他的に実施すべき処理をシングルトンとして実装することはとても理にかなっていると考います。(これとは反対の意見もあるようです5)
ソフトウェアリソース
システムには、またソフトウェアリソースも存在します。
システム全体にまたがってサービスを提供するようなインスタンスは、排他的責務を持ち、かつ1つのインスタンスつまりシングルトンとしても良さそうです。例えば、オブザーバーパターンを用いてシステム全体にサービスを提供するNotificationCenterなどは好例です。
NotificationCenterは、それ自体が限定されたリソースではなく、NotificationというIdentifiedされた情報、つまりソフトウェア的リソースを管理するためにあります。NotificationCenterはNotificationを扱うというシステム全体のサービスであり、排他的責務を持つためにシングルトンとして存在することができます。iOSでシングルトンと思われるフレームワーク
Framework 説明 シングルトンの観点 UIApplication The centralized point of control and coordination for apps running in iOS. シングルトンなのは当然のような気がする NotificationCenter A notification dispatch mechanism that enables the broadcast of information to registered observers. メッセージのブロードキャストというシステム全体のサービスはシングルトンにマッチしている UserDefaults An interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app. UserDefaults.standardで呼び出すように正確にはシングルトンである必要はないが、システム全体のサービスであるため、事実上のシングルトン ABAddressBook The ABAddressBook class provides a programming interface to the Address Book アドレスデータベースにアクセスするためにシングルトンか?しかしデータベース自体が排他制御できる場合はシングルトンの意味はなさそう。実際新しいContactsフレームワークはシングルトンではない。 iOSでシングルトンと思われるフレームワークをピックアップしてみました。
他にもいくつかありますが、調べてみると意外にシングルトン実装は少ないです。
CLLocationManagerなどはシングルトンであっても違和感はないですが、シングルトンではありません。
これは、iOS開発で多用されるDelegateパターン、クロージャなどに理由がありそうです。下記の2つの記事は以前に書いたものですが、その理由はここからも推察できます。
・iOSにおけるSingletonとDelegate/Observer
・軽量なRxSwiftもどきを自作してみたCLLocationManagerのようにdelegateプロパティを実装する場合、Protocolで委譲されるインスタンスが1つに限られるためにこのパターンを利用しようとするとシングルトンにするのはちょっと躊躇われます。かといって、NotificationCenterでグローバルにNotificationメッセージを発行するのも嫌です。
RxSwift(あるいはモドキ)を使った実装のように、容易にオブザーバーパターンを用いることができれば、シングルトン化の問題は軽減されると思いますが、このような手段がAppleから標準で提供されない限りは、Appleのフレームワークでシングルトンが採用されることは少ないように思います。結論
- 責務が他のクラスと重複しないこと
- 責務の対象が、限定されたリソースであること
- 責務がシステム全体に渡るものであること
- シングルトン・パターンを適用すること
- スレッドセーフの必要性を検討すること
- オブザーバー・パターンによる通知機構が必要な場合がある
を考慮して設計・実装することでシングルトンを活用することができるのではないでしょうか。
マネージャー
最後にマネージャーについてひとこと。
xxxxManagerという命名ははシングルトンの実装と同じくらい忌避されているようですが、個人的には正しいシングルトンに対して命名するのはアリだと思っています。要は命名のルールをはっきりすることです。参考記事
これは参考までに→シングルトンパターンの誘惑に負けない ↩
singleton とは一枚札のことです。一枚札とはトランプの一組に唯一のカードです。Singleton パターンとは、このような唯一の存在を保証するためのパターンです。www.techscore.comより引用 ↩
CRUD(クラッド)とは、ほとんど全てのコンピュータソフトウェアが持つ永続性の4つの基本機能のイニシャルを並べた用語。 その4つとは、Create(生成)、Read(読み取り)、Update(更新)、Delete(削除)である。 ユーザインタフェースが備えるべき機能(情報の参照/検索/更新)を指す用語としても使われる。Wikipediaより引用 ↩
面白いことにユーザを何十万人、何百万人規模で扱うクラウドシステムでは、ハードウェアに対して処理規模のインパクトが大きすぎるために、メモリーやCPUの処理能力を無視できなくなります。AWSなどではコンピューティング・リソースという概念は重要なもので、負荷やレイテンシーに対する考え方も組み込みの世界に通じるところが多いと感じます。 ↩
- 投稿日:2019-01-27T12:21:09+09:00
【Swift】非同期処理で意外と見落とすバグをテストで自動チェックする
非同期処理を書く機会は多いとは思いますが、
意外な落とし穴に出会うことがあります。
(と、少なくとも私は思っています)
今回はそんなことが起きそうな事例と
テストを使って自動でチェックする方法について
検討してみたいと思います。実装の準備
よくありそうな非同期でデータを取得する処理を考えてみます。
// どこかからデータをloadして結果をコールバックで返却するprotocol protocol DataLoader { associatedtype T associatedtype E: Error func load(completion: @escaping (Result<T, E>) -> Void) } enum Result<T, Error> { case success(T) case failure(Error) }// 実際に通信をする機能を持つprotocol enum HTTPClientError: Error { case invalidResponse case error(Error) } typealias HTTPResponse = (Data, HTTPURLResponse) protocol HTTPClient { func get(from url: URL, completion: @escaping (Result<(HTTPResponse), HTTPClientError>) -> Void) }// 欲しいデータ(今回ほぼ出てきません) struct Item: Decodable { let id: String let name: String }// DataLoaderに適合したclass final class RemoteDataLoader: DataLoader { let client: HTTPClient let url: URL enum Error: Swift.Error { case invalidData case invalidStatus(Int) case unknown(Swift.Error) } init(client: HTTPClient, url: URL) { self.client = client self.url = url } func load(completion: @escaping (Result<Item, Error>) -> Void) { client.get(from: url) { result in switch result { case .success(let data, let response): guard response.statusCode == 200 else { completion(.failure(.invalidStatus(response.statusCode))) return } guard let item = ItemTranslator.map(data) else { completion(.failure(.invalidData)) return } completion(.success(item)) case .failure(let error): completion(.failure(.unknown(error))) } } } }// DataからItemに変換するstruct struct ItemTranslator { static func map(_ data: Data) -> Item? { // 呼ばれたかどうかを確認するためにコンソールに出力しています print("!!!!!!!!!!!!!!!!!!!map called!!!!!!!!!!!!!!!!!!!!!!!!!!!") return try? JSONDecoder().decode(Item.self, from: data) } }テストの準備
次に確認するためにテストを準備します。
class RemoteDataLoaderTests: XCTestCase { // 通信後に呼ばれるコールバックを記録しておいて // 任意のタイミングで呼び出せるようにするクラス private class HTTPClientSpy: HTTPClient { func get(from url: URL, completion: @escaping (Result<(HTTPResponse), HTTPClientError>) -> Void) { messages.append((url: url, completion: completion)) } var messages: [(url: URL, completion: (Result<(HTTPResponse), HTTPClientError>) -> Void)] = [] var urls: [URL] { return messages.map { $0.url } } // エラーの結果を返すためのメソッド func call(with error: HTTPClientError, at index: Int = 0) { messages[index].completion(.failure(error)) } // 正常な結果を返すためのメソッド func call(statusCode: Int = 200, data: Data, at index: Int = 0) { let response = HTTPURLResponse( url: urls[index], statusCode: statusCode, httpVersion: nil, headerFields: nil) messages[index].completion(.success((data, response!))) } } // セットアップ override func setUp() { super.setUp() let url = URL(string: "https://hogehoge.com")! client = HTTPClientSpy() sut = RemoteDataLoader(client: client, url: url) } // テスト用のインスタンスを用意するヘルパーメソッド private func prepareInstancesForTest( url: URL = URL(string: "https://hogehoge.com")! ) -> (HTTPClientSpy, RemoteDataLoader) { let client = HTTPClientSpy() let loader = RemoteDataLoader(client: client, url: url) return (client, loader) } }まずは通常の動作を確認してみます。
func test_通常処理() { let (client, loader) = prepareInstancesForTest() var results: [Result<Item, RemoteDataLoader.Error>] = [] // loadの中でHTTPClientのgetを呼んでいるので // loadのcompletionはSpyのmessagesに追加される loader.load { results.append($0) } // callを呼ぶとresultsに値は追加される client.call(data: Data(), at: 0) // loadのcompletionは呼ばれているはずなのでresultsのcountは1になる XCTAssertEqual(results.count, 1) }これで準備が整いました。
メモリリークを確認する
メモリリークは隠れたところに潜んでいることがあります。
Debug Memory Graphを使用すれば
最終的には見つけられる可能性は高いですが繰り返しチェックするのは
時間的にも精神的にも面倒に感じることもある一方でしばらくチェックをしないと色々な場所でメモリリークが発生して
原因が特定しづらくなってしまうということもあるのではないかと思います。そんな時にテストでチェックができる仕組みがあると
良いのではないかと感じています。まずメモリリークを発生させるために
RemoteDataLoaderのloadメソッドを下記のようにします。func load(completion: @escaping (Result<Item, Error>) -> Void) { client.get(from: url) { result in self.hoge() switch result { case .success(let data, let response): guard response.statusCode == 200 else { completion(.failure(.invalidStatus(response.statusCode))) return } guard let item = ItemTranslator.map(data) else { completion(.failure(.invalidData)) return } completion(.success(item)) case .failure(let error): completion(.failure(.unknown(error))) } } } private func hoge() { print("hoge") }こうすることで
クロージャの中でselfを強参照しているため
selfがdeinitされなくなります。ではテストを再度実行するとどうなるでしょうか?
成功します
処理的には間違っていることがないからです。
つまり
メモリリークが見逃されてしまう可能性がある
のです。そこで
メモリリークをテストを通して自動でチェックできるようにしてみます。RemoteDataLoaderTestsのprepareInstancesForTestを下記のようにします。
private func prepareInstancesForTest( url: URL = URL(string: "https://hogehoge.com")!, file: StaticString = #file, line: UInt = #line ) -> (HTTPClientSpy, RemoteDataLoader) { let client = HTTPClientSpy() let loader = RemoteDataLoader(client: client, url: url) checkMemoryLeaks(loader, file: file, line: line) checkMemoryLeaks(client, file: file, line: line) return (client, loader) } private func checkMemoryLeaks(_ instance: AnyObject, file: StaticString = #file, line: UInt = #line) { addTeardownBlock { [weak instance] in XCTAssertNil(instance, "メモリリーク!!!!", file: file, line: line) } }addTeardownBlockは
現在のテストの終了後のteardown処理をブロックで追加できるメソッドです。
https://developer.apple.com/documentation/xctest/xctestcase/2887226-addteardownblock※ fileとlineはテストが失敗した箇所をわかりやすくするために追加しています。
これを追加することでテストが失敗し
メモリリークを確認することができるようになりました。メモリリークを解消
これは単純な話で[weak self]をつければ解消します。
func load(completion: @escaping (Result<Item, Error>) -> Void) { client.get(from: url) { [weak self] result in self?.hoge() ... } }非同期処理時の思わぬ挙動を確認する
非同期処理を実装していると
思わぬときに
「あれ、なんでこのメソッド呼ばれているんだ?」
みたいな事象に遭遇することがあります。そんな事象を確認するために
下記のテストを追加します。func test_非同期の挙動チェック() { let url = URL(string: "https://hogehoge.com")! let client = HTTPClientSpy() // nilにしたいのでOptionalにする var loader: RemoteDataLoader? = RemoteDataLoader(client: client, url: url) var results: [Result<Item, RemoteDataLoader.Error>] = [] // loadの中でHTTPClientのgetを呼んでいるので // loadのcompletionはSpyのmessagesに追加される loader?.load { results.append($0) } // ここでテスト対象をnilにするので // callを呼んでもresultsに値は追加されないはず loader = nil client.call(data: Data(), at: 0) // loadのcompletionは呼ばれないはずなのでresultsは空のはず XCTAssertTrue(results.isEmpty) }テストを実行するとどうなるでしょうか?
失敗します
コンソールの出力を見てみるとtranslatorのmapメソッドが呼ばれています。
Test Case '-[FeedDataMemoryLeakDetectionTests.RemoteDataLoaderTests test_非同期の挙動チェック]' started. !!!!!!!!!!!!!!!!!!!map called!!!!!!!!!!!!!!!!!!!!!!!!!!!これはclientのHTTPClientSpyがcompletionを保持しているためです。
loaderのdeinitと同時にclientもdeinitするようにすれば処理は発生しませんが
clientがシングルトンであった場合などは困った状況になります。想定される場面としては
ある画面でデータをロード中に前の画面に戻ったときに
ViewControllerはdeinitされているのに
裏でcompletionの処理が動いてしまう。などが考えられます。
思わぬ挙動を解決する
これも非常にシンプルですが
RemoteDataLoaderのloadメソッドの中でインスタンスの存在チェックをします。func load(completion: @escaping (Result<Item, Error>) -> Void) { client.get(from: url) { [weak self] result in // selfがdeinitしていた場合は処理をしない guard self != nil else { return } ... } }こうすることで処理が発生しなくなります。
まとめ
非同期処理はほぼ当たり前のように使用しており
気をつけなればいけない箇所は把握しているかもしれませんが
上記のような実は見落としているのかもしれないという可能性も捨て切れません。そんな時に手動での確認となると
手間と時間がかかるのに加え
確認漏れが発生する可能性があるなど
意外と大掛かりな作業になってしまうかもしれません。そこで
テストで自動確認できる仕組みを使って
そういった不安と負担を軽減できたら嬉しいですね何か間違いなどございましたらご指摘いただけますと幸いです
- 投稿日:2019-01-27T11:31:11+09:00
SwiftとObjective-Cの定数を共有
Objective-Cで書かれたプロジェクトのswift化を進めています。進め方としては、既存のObjective-Cのコードはそのままに、新規に作る画面や機能をswiftで書くというやり方。
Objective-C側で書かれたdefineの定数。よくNSUserDefaultsなどのKeyとしてまとめてたりしますよね。
あれをSwiftからも使えるようにすべく、ラッパーをかいてみました。
まず、Objective-CのコードをSwiftで呼べるようBridging-Header.h に一行追加します。
Bridging-Header.h#import "Defines.h"そして、Objective-C用の定義ファイルであるDefines.hにクラスを追加します。今までは#defineだけの塊のファイルです。
Defines.h#define kUSERDEFAULTS_HOGEHOGE @"UserDefaults_HOGEHOGE" @interface Defines : NSObject + (NSString *)hogehogeUserDefaults; @endDefines.m#import <Foundation/Foundation.h> #import "Defines.h" @implementation Defines + (NSString *) hogehogeUserDefaults { return kUSERDEFAULTS_HOGEHOGE; } @endそして呼び出し側からは、
Hogehoge.swiftUserDefaults.standard.set(password, forKey:Defines.hogehogeUserDefaults())というところまで書いて、できたできたと、なる予定だったのですが、
Hogehoge.swiftUserDefaults.standard.set(password, forKey: kUSERDEFAULTS_HOGEHOGE)でもOKでした。あれれ?ラッパーいらなかった?
っていうお話でした。














