作成者別アーカイブ: tateo

AndroidでQRコードをリーダーを作る(超シンプル版)

GooglePlayサービス 7.8以降では「Mobile Vision API」ってのが追加されていて、これを使うとQRコードの認識が行えるようです。
ググってみると色々とサンプルはあるのですが、いかんせん複雑になっており「QRコードを認識する!」って箇所がわかり辛く思いました。
そこで超シンプルなサンプルを作成しましたよ。

簡単にQRコードが認識できると思いますよ。

まずは準備から。

build.gradleへ依存ライブラリを追記します。

dependencies {
  compile 'com.google.android.gms:play-services-vision:10.2.0'
}

次にManifestへ権限などを追記します。
下記の uses-feature、uses-permission、meta-dataを追記してください。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.eyewhale.sample">
  
  <!-- ↓ この2行を追記  -->
  <uses-feature android:name="android.hardware.camera" android:required="true"/>
  <uses-permission android:name="android.permission.CAMERA" />
  
  <application ・・ >
    
      <!-- ↓ このmeta-dataを追記  -->
      <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      <meta-data
          android:name="com.google.android.gms.vision.DEPENDENCIES"
          android:value="barcode" />

  </application>
</manifest>

これは説明がいるか不明ですが、、超シンプルなレイアウトファイル。
SurfaceViewにカメラから撮影された画像が表示されます。
(今回のサンプルではFragmentにしてみてます。よくあるサンプルがActivityだったので)

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/camera_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

ようやく実装です。
実際のコードは最後に記述しておきますが、
大きな流れはこんなかんじです。

1. SurfaceViewにコールバックを設定
2. BarcodeDetectorを作ってProcessorを設定する
3. CameraSourceを作成してBarcodeDetectorを設定
4. SurfaceViewの準備ができたらCameraSourceを開始
  →バーコード認識も開始される
5. バーコードが認識されたらProcessorのreceiveDetectionsが呼ばれるので適当に処理

あと、Pause/Resumeでカメラの停止/再開を行っています。

import com.google.android.gms.vision.CameraSource;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;

public class BarcodeSampleFragment extends Fragment {
  private final static String TAG = "BarcodeSampleFragment";
    
  private SurfaceView _surfaceView;
  private CameraSource _cameraSource;
  private boolean _surfaceCreated = false;
  
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_barcode_sample, container, false);

    // SurfaceViewにコールバックを設定
    _surfaceView = (SurfaceView)view.findViewById(R.id.camera_preview);
    _surfaceView.getHolder().addCallback(_surfaceCallback);
    return view;
  }

  @Override
  public void onResume() {
    super.onResume();
      
    // CameraSourceが未作成 or 破棄されていたら作成
    if (_cameraSource == null) {
        setupCameraSource();
    }
        
    // SurfaceViewが準備できていたらキャプチャを開始
    if (_surfaceCreated){
        startCameraSource(_surfaceView.getHolder());
    }
  }

  @Override
  public void onPause() {
    super.onPause();
    // キャプチャを停止
    if (_cameraSource != null){
      _cameraSource.stop();
    }
  }
    
  private void setupCameraSource(){
    // QRコードを認識させるためのBarcodeDetectorを作成
    BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(this.getContext())
        .setBarcodeFormats(Barcode.QR_CODE)
        .build();
    // DetectorにProcessorというコールバックを設定
    barcodeDetector.setProcessor(_detactorProcessor);

    // CameraSourceを作成
    _cameraSource = new CameraSource.Builder(this.getContext(), barcodeDetector)
        .setAutoFocusEnabled(true)
        .setFacing(CameraSource.CAMERA_FACING_BACK)
        .build();
  }

  private void startCameraSource(SurfaceHolder holder){
    try {
      _cameraSource.start(holder);
    } catch (IOException e){
        Log.e(TAG,"",e);
    }
  }
  
  private Detector.Processor<Barcode> _detactorProcessor = new Detector.Processor<Barcode>(){
    @Override
    public void release() {
    }
        
    @Override
    public void receiveDetections(Detector.Detections<Barcode> detections) {
      // 認識された結果がdetectionsに入ってる
      for(int i=0;i<detections.getDetectedItems().size();i++){
        int key = detections.getDetectedItems().keyAt(i);
        Barcode barcode = detections.getDetectedItems().get(key);
        String barcodeValue = barcode.rawValue;
        Rect rect = barcode.getBoundingBox();
        // 認識結果をログへ出力
        Log.d(TAG,"detect barcode:" + barcodeValue + " " + rect.toString());
      }
    }
  };

  private SurfaceHolder.Callback _surfaceCallback = new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
      try {
          // カメラのキャプチャを開始
          _cameraSource.start(holder);
      } catch (IOException e){
          Log.e(TAG,"",e);
      }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
      _cameraSource.stop();
      _cameraSource.release();
      _cameraSource = null;
    }
  }; 
}

※ 注意!
このサンプルはシンプルにするため色々と省いてます
– カメラへアクセスするための権限チェック
– GooglePlayServicesが利用可能であるかのチェック
– BarcodeDetectorが利用可能であるかのチェック
– その他もろもろ
実際に使う時は注意してくださいね!

DLNAで再生してたら途中で中断される問題!(DMCアプリ落ち)

ネットワークオーディオ機器を使ってみたくて「Marantz M-CR611」を購入したんですよ。こないだ。(→記事)
「Marantz Hifi Remote」からNASに保存されている上原ひろみの「SPARK」を再生して「ええ音や・・(涙」と聞いてたら数曲再生しただけで止まってしまう・・萎えるー

これが「DMCアプリ落ち問題!」
→ なんらかの原因(※)でDMCアプリが停止したら次の曲以降が再生されない。
※スリープ、バッテリー切れ、ゲームなどの高負荷アプリを動かす・・など

比較的新しいiPhoneであればバックグラウンドでそれなりの時間までは再生してくれるんですけど、なにぶんちょっと古めのAndroidとかだとすぐに再生が止まってしまいます。
最初はこれ、なんでか分かりませんでしたがDLNAの機能を考えるとまぁしょうがない。
DMRは再生するリスト(以下プレイリストと呼ぶ)を保持せず、DMCからの指示で動くのでDMCが動いてくれないとどうしようも無いのだ。
(とはいえ、「Marantz Hifi Remote」はMarantz純正アプリでもあるんだから機器に専用コマンドで送信してM-CR611をDMPとして動かしてくれよ!・・とは思います。)

補足1.
DLNAを構成する機能

DMS(デジタルメディアサーバー)
 - コンテンツを保存してそれらを配信する機能を有する機器。DLNA対応のNASとかがこれにあたる。
DMP(デジタルメディアプレイヤー)
 - DMSのコンテンツを自分で検索/再生する機器。M-CR611のようなネットワークオーディオ機器は大概これ。
DMC(デジタルメディアコントローラー)
 - DMSのコンテンツを選択してそれを再生する機器(DMR)に指示するリモコンみたいな機能。スマホアプリ(Kazooとか)とか。
DMR(デジタルメディアレンダラー)
 - DMCから指令を受ける再生機器 。DMPとの大きな違いはDMCからの指令で再生すれば良いのでDMSのコンテンツを検索する機能を持たなくても良い。

補足2.
M-CR611のようなネットワークオーディオプレイヤーは大抵DMPにもDMRにも対応しています。
DMPとして動かせば(例えば本体でサーバの曲を選んで再生するとか)DMCは関係無いので、この場合はアプリが落ちようと関係無く再生されます。

で、色々と調べているとこの問題を解決する「OpenHome」という規格があるらしい。
しかもどうやら 「BubbleUPnP Server」というものを使うとOpenHome非対応の機器もOpenHome対応機器として動かすことができるらしい。
やってみよう!

BubbleUPnP Server

「BubbleUPnP Server」はMacやWindowsはもちろん、LinuxやAndroid、さらにはSynologyやQNAPなどのNASでも動作する。

用途を考えると常時起動している機器にインストールするのがベスト。
RaspberryPiへインストールする事も考えたが、今回は自宅のNAS「Synology DS215j」にインストールすることとした。

※ Kazoo Server も気になりますが、まだsynology用のパッケージは提供されていないようです。
 
 

Synology DS215j(DSM 6.0)へのインストール手順

(以下は執筆時点の手順なので公式のインストール手順を事前に確認してください)
※自分は今のところ正常に動いていますけど、自己責任でお願いしますね。大事なデータが消えたりしても自己責任で!

1.Java8のインストール
DSMの パッケージセンター → デベロッパーツール から 「Java8」 を選んでインストール。

2.Java8をOracleJDKに更新
DSMのメインメニューから「Java8」の画面を開いて

20170113_001
→ [Javaをアップグレード]

20170113_002
→ 「Java SE Download」

20170113_003
→ 「Linux ARM 32 Hard Float ABI」をダウンロード
※執筆時には「jdk-8u111-linux-arm32-vfp-hflt.tar.gz」こんなファイルがダウンロードされました。

先ほどの画面でダウンロードしたファイルを参照して[OK]

※ダウンロードするファイルは機種によって違うらしいです。↓のサイトを参照してくれとのこと。
http://minimserver.com/install-synology.html

3.事前の設定

コントロールパネル > ユーザー > 詳細
20170113_004
[ユーザーホームサービスを有効にする]にチェックをいれて[適用]

証明書のチェックを無効に
パッケージセンター > 設定
20170113_005
トラストレベルを「全ての製造元」に変更して[OK]

4.「BubbleUPnP Server」のインストール

下記URLからSynology用のパッケージをダウンロード
https://www.bubblesoftapps.com/bubbleupnpserver
※[Download Synology package]ってところ
※自分が行った時は BubbleUPnPServer.spk というファイルでした

パッケージセンター を開き[手動インストール]からダウンロードしたファイルを選びインストールします。
スクリーンショット_2017-01-13_16_57_30
※ffmpegのインストールが推奨されているけど今回はインストールせず。
 
  

BubbleUPnP Serverの設定

インストールできたら次は設定
※インストール直後に設定画面が開かれますけど、まだ起動に時間がかかっていたりすると空白ページが開かれてしまいます。
その時は、ちょっと待ってからメインメニューから[BubbleUPnP Server]を選んで再度開きましょう。
20170113_006

1.ネットワーク系
インターネットからアクセスしたくはないので、[Network and Security]というタブを開いて[Allow to access the server from the Internet]のチェックを外す。

2.DMRをOpenHome機器として公開する設定
[Media Renderers]というタブを開く
20170113_007

出力対象とするDMR (この場合はM-CR611)を選んで、[Create an OpenHome renderer]にチェックを入れる

以上でサーバ側は完了。簡単。

後はHomeHomeに対応したアプリ(※)でレンダラーを指定すればOK。
DMRとしてのM-CR611とOpenHome化された同機器がどちらも表示されているのでOpenHomeの方を選択

同じレンダラーを参照したアプリでは同じプレイリストが表示されるし、アプリを落としてもそのまま再生されます。
※ AndroidだとBubbleUPnP、iPhoneだとKazooとか

NASのCPU負荷も特に上がっていませんね。
20170113_008
これなら大丈夫そう。

JSONパーサー Unbox をデータクラスを定義せずに使用する方法

Unboxは便利ですね〜
結構便利でswiftでjson扱う時はこればっかり使っています。
WEBで見てもユーザがどんどん増えている気がしますね。

さて、そんな便利なUnboxですが決まったデータクラスが存在しない時の扱いについてサンプルが少ないように感じました。
特にマップを要素に持つjson配列の扱いにちょっと迷いましたのでここにメモっておきます。

ポイントは performCustomUnboxing*** 系のメソッドを使う事と、 UnboxableDictionary を使う事です。



/*  
jsonStringには下記のjsonが入っているとして

{
   "storename":"金沢店",
   "stocks":[
      {"item":"白菜","stock":12},
      {"item":"甘エビ","stock":439}
   ]
}
*/

if let data = jsonString.dataUsingEncoding(NSUTF8StringEncoding) {           
  do {
                
    // performCustomUnboxing*** を使うのがポイント
                
    let result = try Unboxer.performCustomUnboxingWithData(data) { (unboxer) -> String in
                    
      // ルートの"storename"を取得
      let storename = unboxer.unbox("storename") ?? "unknown store"
                    
      // マップを含むjson配列はUnboxableDictionaryの配列として取得する
      let stockDictArray:[UnboxableDictionary] = unboxer.unbox("stocks")
                    
      // 取得したjson配列を performCustomUnboxingWithArray でパース
      let stocks = try Unboxer.performCustomUnboxingWithArray(stockDictArray) { (unboxer) -> (String,Int) in
                      
          let item = unboxer.unbox("item") ?? ""
          let stock = unboxer.unbox("stock") ?? 0
                        
          // 結果をtupleで返却
          return (item,stock)
      }
                    
      // パースした結果を文字列にして返却
      var caption = "storename:\(storename) \n"
      for stock in stocks {
        caption += " item:\(stock.0) stock:\(stock.1) \n"
      }
      return caption
    }
                
    print(result)
                
  } catch let error as NSError {
    print(error)
  }
}  

/*
[output]  
         
storename:金沢店  
item:白菜 stock:12  
item:甘エビ stock:439  
*/

「javaでファイルのリストを取得したら遅いよ」問題への対策

javaでファイルのリストを取得したら遅いよ。
ローカルでも遅いんだけど、ネットワーク越しだともうね。。

あまりに遅いので対策を考えてみました。

例えば指定されたディレクトリ以下のファイル名を全て取得するとこんなコードになったりしますよね。


private listFiles(File dir){
    File[] files = dir.listFiles();  // (a)
    for (File f : files) {
        if (f.isFile()){  // (c)
            String filename = f.getName();  // (b)
            System.out.println(filename);
        } else {
            listFile(f);
        }
    }
}

単純ですね。
ただ、これだと遅いんです。

Fileのインスタンスから取得している情報は下記の3つなのですが、どうやらそれぞれについて毎回システムコールを行っているようなのです。
それがネットワーク越しだと、ネットワークアクセスが発生します(たぶん)
・ディレクトリに含まれるディレクトリ/ファイルのFileインスタンス (a)
・ファイルの名前 (b)
・Fileインスタンスがディレクトリであるか (c)

で、代替手段として次のようにしました。
1. File.listFiles() の代わりにFile.list() を使う
listFilesはFileインスタンスを返しますが、listはファイル名を返します。
ファイル名からFileインスタンスを作成することで 上記の(a)と(b)が同時に取得できます。

2. isFile()の代わりに File.list() の戻り値がnullであるか判定する
対象のFileインスタンスがファイルの時、list()はnullを返すようです。
ですので、上記1でlist()を実行した際の戻り値を判定することで isFile を呼ぶ必要がなくなります。

3. (これは代替とはちょっと違いますが) Fileのメソッドの戻り値をキャッシュする
例えば File.getName() を複数回呼んでいる場合、変数に保存しておいてFileのメソッドは1回しか呼ばない

上記の処理を抜粋すると


// ファイル名のリストを取得する  
File f = new File(省略);  
String[] filenames = f.list();  
  
// ファイルであるか判定  
boolean isFile = (filenames == null);  
  
Fileのインスタンスを取得する  
File[] subfiles = Arrays.stream(filenames).map(name -> new File(f,name)).toArray(File[]::new);  

こんな感じです。

環境や処理内容にもよると思いますが、これらの対策で処理時間が1/3になってくれましたよ。

[Swift]UITableViewで無限スクロールしてみる

WEBAPIから取得したブログエントリーを無限スクロールで表示するビューアを作成する事になったので、
UITableViewを使って実装のサンプルを作ってみました。

実装の概要
・UITableViewのスクロールに応じて必要なデータを必要なタイミングでロード。
・上にスクロールした場合にはデータを先頭に、下にスクロールした場合にはデータを末尾に追加する
・データの変更時にはUITableViewの表示位置(contentOffset)を調整する
・セルの高さは動的に決定する(行毎の高さはキャッシュする)

表示用のデータはこんなものを想定します。

class MyData
{
    var date:NSDate;
    var text:String;
    
    init(date:NSDate,text:String){
        self.date = date;
        self.text = text;
    }
}

dateが並び順(タイトル)で、表示コンテンツとしてtextがあります。

で、この情報をUITableViewに表示するのですが、今回のサンプルはコンテンツの内容に応じてセルの高さを動的に決定しようと思います。
高さを求めるのはそれなりにコストが高いので、計算した高さを表示用データとペアにして格納するためのクラスを定義します。

class CellData<T> {
    // 表示データ
    var data:T
    // セルの高さ
    var height:CGFloat?
    
    init(data:T,height:CGFloat?){
        self.data = data
        self.height = height
    }
}

セルの高さを求める時には表示用のコードと殆ど同じになりますので、実際のセルに計算させると良いかと。
ですので、こんな感じのUITableViewCellを定義します。

class MyTableViewCell: UITableViewCell
{
    private var titleLbl:UILabel!
    private var textLbl:UILabel!
    
    // 計算済の高さが設定される
    var requiredHeight:CGFloat = 0
    
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String!)
    {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.setupSubviews()
    }
    
    required init?(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
        self.setupSubviews()
    }

    func setupSubviews()
    {
        self.titleLbl = UILabel()
        self.titleLbl.font = UIFont.boldSystemFontOfSize(15)
        self.addSubview(self.titleLbl)
        
        self.textLbl = UILabel()
        self.textLbl.font =  UIFont.systemFontOfSize(13)
        self.textLbl.numberOfLines = 0
        self.addSubview(self.textLbl)
    }
    
    // 表示用のデータを設定
    // 必要な高さも求めてrequiredHeightに保存
    func setData(data:MyData)
    {
        titleLbl.frame = CGRectMake(5, 5, self.frame.width - 5 * 2.0, 18)
        titleLbl.text = "\(data.date)"
        
        
        self.textLbl.frame = CGRectMake(5, titleLbl.frame.origin.y + titleLbl.frame.height + 5, self.frame.width - 5 * 2.0, 10)
	// 設定した文字列によってサイズを動的に変更
        self.textLbl.text = data.text
        self.textLbl.sizeToFit()
        
        // 必要な高さを保存しておく
        requiredHeight = textLbl.frame.origin.y + textLbl.frame.size.height + 5
    }

}

setDataでMyDataを渡すと、requiredHeightにセルを表示するために必要な高さが設定されます。

これらのクラスを使ってUITableViewを使います。
コードはこんな感じ!(ズババン!)

class SampleViewController:UIViewController,UITableViewDelegate,UITableViewDataSource {

    // MARK: - Properties
    
    var tableView: UITableView = UITableView()
    
    
    // テーブルに表示するデータを保持する
    // ※SortedList(→コード)を使っていますが、なんでも大丈夫です
    var tableData:SortedList<CellData<MyData>> = SortedList<CellData<MyData>>(compFunc: { (left, right) -> CompareResult in
        switch (left.data.date.compare(right.data.date)){
        case .OrderedSame: return CompareResult.Equal
        case .OrderedAscending: return CompareResult.RightLarge
        case .OrderedDescending: return CompareResult.LeftLarge
        }
    })


    // セルの高さを算出するための作業セル
    var workingCell = MyTableViewCell()
    
    // データ処理の実行フラグ
    var isLoading = false


    // MARK: - Initialize
    
    init() {
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    


    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = UIColor.yellowColor()
        
        
        // テーブルの設定
        tableView.registerClass(MyTableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
        self.view.addSubview(tableView)
        
    
        // 初期データの読み込み  昨日から30日分のデータを取得して初期データとします
        let date = NSDate(timeInterval: 24 * 60 * 60 * -1, sinceDate: NSDate())
        self.loadDataFromDate(date, count: 30, toFuture: true, containTargetDate: true) {[weak self] (results) in
            
            self?.tableData.append(results)
            self?.tableView.reloadData()
            
            // 指定した日付が先頭に表示されるようにスクロール
            let indexPath = NSIndexPath(forRow: 1, inSection: 0)
            self?.tableView.selectRowAtIndexPath(indexPath, animated: false, scrollPosition: UITableViewScrollPosition.Top)
        }

    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()   
        tableView.frame = self.view.bounds;
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
    }    
    
    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
    }


    // MARK: - 表示用データの取得
    
    // データの取得
    // DBやWebAPI使う事を前提に非同期処理としています
    func loadDataFromDate(date:NSDate,count:Int,toFuture:Bool
        ,containTargetDate:Bool,callback:(results:Array<CellData<MyData>>) -> ())
    {
        isLoading = true;
        // データ取得中はウエイトカーソル的なものを表示した方が良いかもね
 
        let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        let mainQueue = dispatch_get_main_queue()
        weak var weakself = self
        
        dispatch_async(queue,{
            autoreleasepool {
                // ダミーデータを作成する
                var loaded = Array<CellData<MyData>>()
                let dayInterval = Double(24 * 60 * 60 * (toFuture ? 1 : -1))
                
                var dt = containTargetDate ? date : date.dateByAddingTimeInterval(dayInterval)
                
                while loaded.count < count {
                    
                    let csdata = CellData(data: MyData(date: dt, text: weakself!.makeRandomString()), height: .None)
                    
                    loaded.append(csdata)
                    
                    dt =  dt.dateByAddingTimeInterval(dayInterval)
                }
                // 少しウエイトしてみる
                NSThread.sleepForTimeInterval(0.5)
             
                dispatch_sync(mainQueue, {
                    callback(results: loaded)
                    weakself!.isLoading = false;
                })
                
            }
        })
    }
    

    // ランダムな文字列を生成する
    func makeRandomString() -> String {
        let wordTable = ["りんご","梨","ひよこ","ごりら","犬","を食べる","を投げる","を拾う","を蹴る","自分","へ怒る","を割る","コップ","テレビ"];
        let wordCount = arc4random_uniform(50) + 1
        
        let  result = (0..<wordCount).reduce("") { (v, no) -> String in
            let idx = Int(arc4random_uniform(UInt32(wordTable.count)))
            return v + wordTable[idx]
        }
        
        return result
    }
    

    // MARK: - UITableViewDelegate,UITableViewDataSource 
    
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1;
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableData.count
    }
    
    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        
        let obj = tableData[indexPath.row]
        if let height = obj.height {
            return height
        } else {
            // セルの表示サイズを計算してキャッシュする
            workingCell.setData(obj.data)
            let height = workingCell.requiredHeight
            obj.height = height
            return height
        }
    }
    
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)        
        let obj = tableData[indexPath.row]
        
        if let csCell = cell as? MyTableViewCell {
            csCell.setData(obj.data)
        }
        
        return cell
    }

    
    func scrollViewDidScroll(scrollView: UIScrollView) {
        
        // データ取得中は処理しない
        if isLoading {
            return
        }
        
		// 事前読み込みを行う末端からの距離
		let margin:CGFloat = 100
        
        // 下方向への読み込み判定
        // ここの判定に一定のマージンを設ける事で事前読み込みが可能となります
        // このサンプルでは必要となったときにガバッと取得
        let bottomOffset = self.tableView.contentOffset.y + self.tableView.bounds.size.height
		let lengthFromBotton = self.tableView.contentSize.height - bottomOffset        
        if (lengthFromBotton >= margin){            
            let dt = tableData[tableData.count-1].data.date            
            self.loadDataFromDate(dt, count: 10, toFuture: true, containTargetDate: false) {[weak self] (results) in
                self?.tableData.append(results)
                self?.tableView.reloadData()
            }
        }
        

        // 上方向への読み込み判定
        if (self.tableView.contentOffset.y <= margin){            
            let dt = tableData[0].data.date            
            self.loadDataFromDate(dt, count: 5, toFuture: false, containTargetDate: false) {[weak self] (results) in
   		 // 上方向へのスクロールの場合にはスクロール位置の調整が必要
                var height:CGFloat = 0
                for v in results {
                    // セル毎の高さを求めるつつ、tableDataへ格納する
                    self!.workingCell.setData(v.data)
                    v.height = self!.workingCell.requiredHeight
                    height += v.height!                    
                    self?.tableData.append(v)
                }
                
                // 取得したセルの高さ(合計)だけスクロール位置を移動させる
                self?.tableView.contentOffset.y = self!.tableView.contentOffset.y + height
                
                self?.tableView.reloadData()
            }
            
        }
        
    }

}

ポイントは上方向にスクロール(先頭方向)した場合のスクロール位置の調整ですかね。
下方向にスクロールした場合にはデータを末尾に追加するのでスクロール位置は変更しなくて良いのですが、上方向の場合には今表示している位置より前にデータを追加することになります。
ですので、今の表示位置を読み込んだセルの高さ分だけ下に調整してあげる必要があります。

このサンプルではMyDate.dateで勝手にソートされるようにSortedListを使っていますが、普通のArrayでも大丈夫です。
上にスクロールするときは先頭にデータを追加、下にスクロールした時は末尾に追加すれば良いのです。

ScalaでSeq(List)をソート

要素をそのまま比較する単純なソート (sorted)

val s1 = Seq(6,1,3,3)
val r1 = s1.sorted

r1: Seq[Int] = List(1, 3, 3, 6)

 

要素に演算とかを施して比較する場合 (sortBy)

// tupleの最初の要素を大文字にした結果でソート
val s2 = Seq(("C",3),("a",1),("B",2))
val r2 = s2.sortBy(_._1.toUpperCase) 


 r2: Seq[(String, Int)] = List((a,1), (B,2), (C,3))

インスタンスの特定要素で単純にソートするならこれが便利

 

関数を渡して比較するソート (sortWith)

val s3 = Seq("a","Z","C","e")
val r3 = s3.sortWith((a,b) => a.compareToIgnoreCase(b) < 0)

r3: Seq[String] = List(a, C, e, Z)

これは色んなケースに対応できて一番使う気がします。つかこれを覚えておけば大体いける。

 

Javaで言うComparator的な比較用インスタンスを渡してソート (sorted + Ordering )

val s4 = Seq("a","Z","C","e")
val r4 = s4.sorted(new Ordering[String]{
  def compare(a:String,b:String):Int = {
    a.compareToIgnoreCase(b)
  }
})

r4: Seq[String] = List(a, C, e, Z)

うん、面倒。
だけどOrderingクラスを作っておけば使いまわせるのがイイですね。
複雑な要件でのソートであればこれか。

Slick3.0 トランザクション内で、クエリした結果を使って更新処理を行う

クエリした結果(リスト)から別の更新処理を行う方法です。

トランザクション内で処理するために、DBActionにまとめないとダメなのですが、
このへん、ちょっとややこしいですよね。

以下はTableAから得た結果(リスト)からTableBの削除を行う例です

val action =
(for {
   // TableAをクエリ
   list <- TableA.filter(name === "tateo").result
   // その結果を使ってTableBを削除
   _ <- DBIO.seq( list.map(r => TableB.filter(id === r.id).delete  ) : _*  )
} yield () ).transactionally

db.run(action)

SlickでANDとORを混在させたクエリを発行する

例えばこんなSQLを発行したかったりします。

select * from cities
where (population > 100000) and (class = '都' or class = '府')

Slickではこんな感じで書けます

Cities.filter(row => row.population > 100000 && ( class === '都' || class === '府' ))


“AND” は “&&”、”OR” は “||” で記述し、必要な箇所でカッコを記述すれば目的のクエリとなってくれました。

これが動的になった場合にはどうするか?
例えば 下記のSQLで都道府県の抽出が可変の場合(inで書けってのは無しで・・)

select * from cities
where (population > 100000)
  and (class = '都' or class = '府' or class = '道' or class ='県')

試行錯誤した末にfunctionを作って対応しました。

def classConditions(row:Cities,t:Boolean,d:Boolean,f:Boolean,k:Boolean):Rep[Boolean] = {
  var condition:Rep[Boolean] = true
  if (t) condition = condition || (row.class === "都")
  if (d) condition = condition || (row.class === "道")
  if (f) condition = condition || (row.class === "府")
  if (k) condition = condition || (row.class === "県")
  condition
}

Cities.filter(row => row.population > 100000 && ( classConditions(row,true,true,false,false) ))

このへんの情報がなかなか見つからずに苦労します。