- 投稿日: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-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-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でした。あれれ?ラッパーいらなかった?
っていうお話でした。
- 投稿日:2019-01-27T09:31:53+09:00
Dart x Flutter : RenderObject を StatelessWidget 上に表示させてみる
StatelessWidget と StateWidget は、よく話題にあがりますが、
RenderObjectWidget は、話題上がらないでの、補完します。英語圏でも情報がないので、そんなものとして、扱ってください
こんな感じで、3Dっぽい表現を、Flutterアプリに簡単に追加できます。
コード
https://github.com/kyorohiro/memo_flutter_2019四角形を表示するRenderObject を StatelessWidget に 貼り付ける
import 'package:flutter/material.dart' as ma; import 'package:flutter/material.dart' as sky; import 'package:flutter/widgets.dart' as sky; import 'package:flutter/rendering.dart' as sky; import 'package:vector_math/vector_math_64.dart' as vec; import 'dart:async'; main() async{ await new Future.delayed(Duration(seconds: 2)); ma.runApp(MyApp()); } class MyApp extends ma.StatelessWidget { // This widget is the root of your application. @override ma.Widget build(ma.BuildContext context) { return ma.MaterialApp( title: 'Flutter Demo', theme: ma.ThemeData( primarySwatch: ma.Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends ma.StatelessWidget { @override ma.Widget build(ma.BuildContext context) { return new ma.Scaffold( appBar: ma.AppBar(title: ma.Text("Hello")), body: createBody(context), ); } } ma.Widget createBody(ma.BuildContext context) { // return DrawRectWidget(); return ma.Row(children: <ma.Widget>[ ma.Text("Hello"), DrawRectWidget(), ma.Text("Render"), ],); } // // Rect // class DrawRectWidget extends sky.SingleChildRenderObjectWidget { sky.RenderObject createRenderObject(sky.BuildContext context){ return new DrawRectObject(); } } class DrawRectObject extends sky.RenderBox { @override bool hitTestSelf(sky.Offset position) => true; @override void performLayout() { this.size = sky.Size(50,50); } @override void handleEvent(sky.PointerEvent event, sky.BoxHitTestEntry entry) {} void paint(sky.PaintingContext context, sky.Offset offset) { print("${offset} ${this.size}"); sky.Paint p = new sky.Paint(); context.canvas.transform(vec.Matrix4.translation(vec.Vector3(offset.dx,offset.dy, 1.0)).storage); p.color = new sky.Color.fromARGB(0xff, 0x55, 0x55, 0x55); sky.Rect r = new sky.Rect.fromLTWH(0.0, 0.0, 50.0, 50.0); context.canvas.drawRect(r, p); context.canvas.transform(vec.Matrix4.translation(vec.Vector3(-offset.dx,-offset.dy, 1.0)).storage); } }
- 投稿日:2019-01-27T06:31:45+09:00
指紋認証のアプリ組み込み時の懸念点
①内容
指紋認証をアプリに組み込む際に懸念していることをつらつらと書いてみました
(あくまで個人的な考えです)②アプリへの実装方式
Androidの場合
Android Marshmallow (Android 6.0.0)以上の場合はFingerPrint、
Android Pie (Android 9.0.0)以上の場合はBiometricPromptを使うかと思います
上記以外は指紋認証センサー(Nexus Imprintセンサー)がないので諦めるiOSの場合
iPhone 5s以降でiOS 8.0 以上のiPhoneの場合はLocalAuthenticationを使ってTouch IDを呼び出す
上記以外のバージョンはtouch IDが無いので諦める
iPhone X・iPhone XS・iPhone XRの場合もtouch IDが無いので諦めるかFace IDで対応するハイブリッドの場合
Cordova(Monaca)で想定しています。
cordova-plugin-keychain-touch-idをインストールして、使用する。
なお、サードパーティ製APIなので、Proプランへの登録が必要
(サードパーティ製なので、問題なく開発時に受け入れられるかという点も考慮する)
この場合も当然ですが、端末に機能が無ければ無理なので、注意する③設計時の考慮
社内で展開して同じ端末しか使わない、修理は正規の業者に依頼する場合には不要の観点です。
特定の端末は動作対象外としてアプリを開発・登録する、別方法の認証を用意するなどで回避する必要があると思います
端末のバージョンにより指紋認証ができない場合を考慮する
②に書いた通り、端末が対応していなければお話にならない端末に不具合が生じて指紋認証ができない場合を考慮する
例えば、非正規の店で画面修理をした場合です。
iPhoneやiPadではiOSのアップデートやバックアップ復元時に他の部品が正規のものであるかチェックして、非正規以外の物があればブート時にエラー53と出て、Touch IDが利用できなくなります。
(ちなみに私の実例です。。。)④まとめ
アプリを開発する時には対象OS・バージョンなどを考慮する必要があるのは当然ですが、ユーザの不手際により一部機能が使用できなくなった時のことも考える必要があると思いました。
最近アプリ開発を始めてみましたが、バージョンの考慮が本当に大変ですね。。。


