20190127のiOSに関する記事は6件です。

部分配列取り出し関数で時刻データを取り出してみた

はじめに

先日作った 部分配列の取り出し関数 のアルゴリズムを使って
時刻データを取り出す関数を作成した。

実装

ソースコード
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化する。
テンプレートの調査はもう少しあとで。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
    }

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】非同期処理で意外と見落とすバグをテストで自動チェックする

非同期処理を書く機会は多いとは思いますが、
意外な落とし穴に出会うことがあります。
(と、少なくとも私は思っています:sweat_smile:)

今回はそんなことが起きそうな事例と
テストを使って自動でチェックする方法について
検討してみたいと思います。

↓を参考にしています。
https://github.com/essentialdevelopercom/essential-feed-case-study/blob/master/EssentialFeed/EssentialFeedTests/Feed%20API/RemoteFeedLoaderTests.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はテストが失敗した箇所をわかりやすくするために追加しています。

これを追加することでテストが失敗し
メモリリークを確認することができるようになりました。

スクリーンショット 2019-01-27 11.05.05.png

メモリリークを解消

これは単純な話で[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
            }
            ...            
        }
    }

こうすることで処理が発生しなくなります。

まとめ

非同期処理はほぼ当たり前のように使用しており
気をつけなればいけない箇所は把握しているかもしれませんが
上記のような実は見落としているのかもしれないという可能性も捨て切れません。

そんな時に手動での確認となると
手間と時間がかかるのに加え
確認漏れが発生する可能性があるなど
意外と大掛かりな作業になってしまうかもしれません。

そこで
テストで自動確認できる仕組みを使って
そういった不安と負担を軽減できたら嬉しいですね:smiley:

何か間違いなどございましたらご指摘いただけますと幸いです:bow_tone1:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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;

@end
Defines.m
#import <Foundation/Foundation.h>
#import "Defines.h"

@implementation Defines

+ (NSString *) hogehogeUserDefaults { return kUSERDEFAULTS_HOGEHOGE; }

@end

そして呼び出し側からは、

Hogehoge.swift
UserDefaults.standard.set(password, forKey:Defines.hogehogeUserDefaults())

というところまで書いて、できたできたと、なる予定だったのですが、

Hogehoge.swift
UserDefaults.standard.set(password, forKey: kUSERDEFAULTS_HOGEHOGE)

でもOKでした。あれれ?ラッパーいらなかった?
っていうお話でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dart x Flutter : RenderObject を StatelessWidget 上に表示させてみる

StatelessWidget と StateWidget は、よく話題にあがりますが、
RenderObjectWidget は、話題上がらないでの、補完します。

英語圏でも情報がないので、そんなものとして、扱ってください

ezgif-3-99636bd7e2fd.gif

こんな感じで、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-27 9.30.08.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

指紋認証のアプリ組み込み時の懸念点

①内容

指紋認証をアプリに組み込む際に懸念していることをつらつらと書いてみました
(あくまで個人的な考えです)

②アプリへの実装方式

  1. Androidの場合
    Android Marshmallow (Android 6.0.0)以上の場合はFingerPrint、
    Android Pie (Android 9.0.0)以上の場合はBiometricPromptを使うかと思います
    上記以外は指紋認証センサー(Nexus Imprintセンサー)がないので諦める

  2. iOSの場合
    iPhone 5s以降でiOS 8.0 以上のiPhoneの場合はLocalAuthenticationを使ってTouch IDを呼び出す
    上記以外のバージョンはtouch IDが無いので諦める
    iPhone X・iPhone XS・iPhone XRの場合もtouch IDが無いので諦めるかFace IDで対応する

  3. ハイブリッドの場合
    Cordova(Monaca)で想定しています。
    cordova-plugin-keychain-touch-idをインストールして、使用する。
    なお、サードパーティ製APIなので、Proプランへの登録が必要
    (サードパーティ製なので、問題なく開発時に受け入れられるかという点も考慮する)
    この場合も当然ですが、端末に機能が無ければ無理なので、注意する

③設計時の考慮

社内で展開して同じ端末しか使わない、修理は正規の業者に依頼する場合には不要の観点です。
特定の端末は動作対象外としてアプリを開発・登録する、別方法の認証を用意するなどで回避する必要があると思います

  1. 端末のバージョンにより指紋認証ができない場合を考慮する
    ②に書いた通り、端末が対応していなければお話にならない

  2. 端末に不具合が生じて指紋認証ができない場合を考慮する
    例えば、非正規の店で画面修理をした場合です。
    iPhoneやiPadではiOSのアップデートやバックアップ復元時に他の部品が正規のものであるかチェックして、非正規以外の物があればブート時にエラー53と出て、Touch IDが利用できなくなります。
    (ちなみに私の実例です。。。)

④まとめ

アプリを開発する時には対象OS・バージョンなどを考慮する必要があるのは当然ですが、ユーザの不手際により一部機能が使用できなくなった時のことも考える必要があると思いました。

最近アプリ開発を始めてみましたが、バージョンの考慮が本当に大変ですね。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む