iPhoneアプリのサクサク感が出ない件について

先週は、製作中のアプリが画面遷移が遅かったり、操作がひっかかかる状態でしたが、今週3日ほどかけて幾つか修正をおこないました。先週の仮説から、画面遷移・サーバ通信・画面表示といった行程で分けて一つ一つ原因を見ていったのですが、ブラウザ上のWebサービスでは気にしていなかったような点で原因がありました。


描画はメインスレッドで行う

まずはこの参考サイトをdispatch_asyncでバックグラウンドタスクを読んでいただきたいのですが、GCDライブラリが紹介されています。GCDが直接関係するというわけではないのですが、この中に本件の解決策の手かかりがあります。キーワードはメインスレッドです。メインスレッドの存在を考慮していないことが、操作の引っ掛かりの原因の一つでした。メインスレッド以外のスレッドで描画に関する処理を行ってはいけません。このことを理解していなかったので、非同期でサーバ通信したり、DBからの大量データ取得を別スレッドに移しても、引っ掛かりが発生していました。

Swiftで書くとこんな感じになるかと思います

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)){
    // 裏で重い処理
    dispatch_async(dispatch_get_main_queue()) {
        // 描画はメインスレッド
    }
}

GCDについては、ここもわかりやすいです。
【iPhoneアプリ】これを使えるようにならないと「マルチスレッド」について 実装編

Webアプリでは

以前「FacebookのザッカーバーグCEO、「HTML5に賭けたのは失敗」 Androidアプリも間もなくネイティブに」という話題の時に、HTML5でもサクサク感が出せるという反論がありました。今見直すと「レンダリング処理のほうがずっと重要」とか「GPU関連がHTML5での核心」ということがすでに書いていあります。当時は、描画に関して考慮する事が複雑すぎて実業務では無理と考えていたのですが、HTMLでも考えないといけないことは同じだったようです。

iPhoneアプリとWebサイトの違い

稼働中のWebサービスiPhoneアプリ化していて、苦戦してることなど。話の前提としている移植元のWebサービスは、ストリーム放送とかフラッシュサイトではない普通のHTMLのECサイトです。

苦戦しているのはサーバアクセス中にアプリが固まったり、多少キャッシュをしてみても操作に引っ掛かりが残るということです。WEBサイトのレスポンスを送って帰って来るまでが1サイクルという考え方のままにアプリを作っており、HTMLにあたる部分をstoryboardとUIKIt、HTTPリクエストにあたる部分をNSURL系のクラスに単純に置き換えていたのが問題だったようです。

基本的にはデータはアプリ内にキャッシュしてサーバアクセスを抑える方向で考えているのですが、複雑なロジックはサーバ側で一元管理にしたり、マスタの変更がいつでもサーバサイドで行えたりといったことも実現したいと考えているので常にそれなりのやり取りを行っていても固まらない様にする必要があります。

いくつか目標にしているアプリがあるのですが、これらのアプリでは操作の引っ掛かりといったものがほとんどありません。このような操作性を実現するにはおそらくWebサービスの時のように一つの流れで考えているのではなくて、画面のレイアウト作成、サーバアクセス、モデルの表示といった単位で処理を分け、それぞれを非同期・遅延処理で行えば良いのではないかと考えています。

わかっている開発者には、フレームワークなり、実装のパターンなりがあったりする常識的な話なのかもしれませんが、自分には疎い分野で手探りな状態が続いています。

Sqliteで既に存在するかもしれないprimary keyを持つ複数valesを一括insert

久しぶりにSwift以外の話題を。Sqlite3で大量データを一括でinsert,updateしてみます。
以下全て、ターミナルでsqlite3 [tablename]コマンドでdbに入ってからの作業。


テーブル構成


create table tblA (id text,message text,flg boolean,primary key (id));

select * from tblA order by id;
(nothing)

初期データ追加


insert into tblA (id,message,flg) values ('100','a',0),('200','b',0);

select * from tblA order by id;
100|a|0
200|b|0


キーが重複するinsert文をそのまま実行すると失敗する


insert into tblA (id,message,flg) values ('100','A',1),('300','c',1);
Error: UNIQUE constraint failed: tblA.id

select * from tblA order by id;
100|a|0
200|b|0


insert or replaceだと既存キーの情報は変更される。


insert or replace into tblA (id,message,flg) values ('100','A',1),('300','c',1);

select * from tblA order by id;
100|A|1
200|b|0
300|c|1


既存データはuodateではなくて、delete insert。


sqlite> insert or replace into tblA (id,flg) values ('100',1),('300',1);

sqlite> select * from tblA order by id;
100||1
200|b|0
300||1


データが多すぎると[Error: too many terms in compound SELECT]エラーとなる。
手元の環境では上限はちょうど500件でした。

Auto LayouでUIScrollViewにいれたUILabelを自動改行させる

Auto Layoutを使用して、Windowいっぱいに広げたUIScrollViewに自動改行するUILabelをいれてもうまく改行されませんでした。解決方法は簡単で、Labelのレイアウト指定の仕方をScrollViewのTop,Left,Right,Bottomから、Top,Left,Width,Bottomに変えればいいだけでした。


import UIKit

class ViewController: UIViewController {
    

    override func viewDidLoad() {
        super.viewDidLoad()

        //スクロールViewを画面いっぱいに表示
        let scrollView = UIScrollView()
        scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
        self.view.addSubview(scrollView)
 
        self.view.addConstraints([
            NSLayoutConstraint(item: scrollView, attribute: .Top, relatedBy: .Equal, toItem: self.topLayoutGuide, attribute: .Top, multiplier: 1.0, constant: 0),
            NSLayoutConstraint(item: scrollView, attribute: .Left, relatedBy: .Equal, toItem: self.view, attribute: .Left, multiplier: 1.0, constant: 0),
            NSLayoutConstraint(item: scrollView, attribute: .Right, relatedBy: .Equal, toItem: self.view, attribute: .Right, multiplier: 1.0, constant: 0),
            NSLayoutConstraint(item: scrollView, attribute: .Bottom, relatedBy: .Equal, toItem: self.view, attribute: .Bottom, multiplier: 1.0, constant: 0),
        ])
  
        
        //ラベルをスクロールViewに追加
        var label = UILabel()
        label.text = "秋の田の かりほの庵の 苫をあらみ 我が衣手は 露にぬれつつ"
        label.font = UIFont.systemFontOfSize(78)
        label.numberOfLines  = 0; //自動改行用設定
        label.lineBreakMode = NSLineBreakMode.ByWordWrapping //自動改行用設定
 
        label.setTranslatesAutoresizingMaskIntoConstraints(false)
        scrollView.addSubview(label)

        scrollView.addConstraints([
            NSLayoutConstraint(item: label, attribute: .Top, relatedBy: .Equal, toItem: scrollView, attribute: .Top, multiplier: 1.0, constant: 0),
            NSLayoutConstraint(item: label, attribute: .Left, relatedBy: .Equal, toItem: scrollView, attribute: .Left, multiplier: 1.0, constant: 0),
            //NSLayoutConstraint(item: label, attribute: .Right, relatedBy: .Equal, toItem: scrollView, attribute: .Right, multiplier: 1.0, constant: 0),
            NSLayoutConstraint(item: label, attribute: .Width, relatedBy: .Equal, toItem: scrollView, attribute: .Width, multiplier: 1.0, constant: 0),
            NSLayoutConstraint(item: label, attribute: .Bottom, relatedBy: .Equal, toItem: scrollView, attribute: .Bottom, multiplier: 1.0, constant: 0),
        ])
    
     }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

同じことをswift-layoutで書いた場合

        //スクロールViewを画面いっぱいに表示
        let scrollView = UIScrollView()
        Layout.regist(scrollView, superview:self.view)
            .top(0).fromBottom(self.topLayoutGuide)
            .leftIsSameSuperview()
            .widthIsSameSuperview()
            .bottomIsSameSuperview()

        //ラベルをスクロールViewに追加
        var label = Layout.createWordWrappingLabel("秋の田の かりほの庵の 苫をあらみ 我が衣手は 露にぬれつつ")
        Layout.regist(label, superview:scrollView)
            .topIsSameSuperview()
            .leftIsSameSuperview()
            .widthIsSameSuperview()
            .bottomIsSameSuperview()
            .font(UIFont.systemFontOfSize(78))

AutoLayoutでUITableViewCellに異なる高さを設定する

先週の勉強会#swiftwozに来てくださった方ありがとうございました。自分のパートは準備不足ですみませんでした。何人かの方にGithubswift-layoutを使っていただき、重ね重ねありがとうございます。


今日はUITableViewのcellでAuto Layoutを使ってみました。
このようなケースを想定しています。

  • UITableViewに各行で異なる高さのUITableViewCellを表示
  • CellのレイアウトはAuto Layoutで設定
  • CellはdequeueReusableCellWithIdentifierでキャッシュされたものを使う


UITableViewCellのcontentViewにAuto Layoutで書いたsubviewを入れても、なぜかcontentViewの高さが変更されないところではまりました。setNeedsLayou(),layoutIfNeede(),layoutSubviews()あたりを使っても高さが変わらなかったので、subviewのboundsから高さを直接計算してheightForRowAtIndexPathの戻り値にしているところがポイントです。

また、heightForRowAtIndexPathメソッド内で高さを計算する時に、tableView.dequeueReusableCellWithIdentifier("MyTableViewCell", forIndexPath: indexPath)を使用すると実行時に落ちるので、高さ計算用のCellインスタンス(tmpCalculatHeightCell)を一つ用意しています。


サンプルコードは、Auto Layoutをラップしたswift-layoutを使用していますが、直接NSLayoutConstraintを使用しても動くはずです。



コード

https://github.com/grachro/DEMO-Swift-AutoLayout-ResizableTableViewCell
全文

import UIKit

class ViewController: UIViewController {
    
    let tableView = UITableView()
    var tmpCalculatHeightCell:MyTableViewCell?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //Auto Layout
        Layout.regist(tableView,superview: self.view)
            .coverSuperView()
        
        self.tableView.registerClass(MyTableViewCell.self, forCellReuseIdentifier: "MyTableViewCell")
        self.tableView.dataSource = self
        self.tableView.delegate = self
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    
}

extension ViewController:UITableViewDataSource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 50
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = self.tableView.dequeueReusableCellWithIdentifier("MyTableViewCell", forIndexPath: indexPath) as MyTableViewCell
        
        let containtHeight = CGFloat(indexPath.row + 1) * 5
        cell.setHeight(containtHeight)
        
        return cell
    }
    
}

extension ViewController:UITableViewDelegate {
    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        
        let containtHeight = CGFloat(indexPath.row + 1) * 5
        
        if self.tmpCalculatHeightCell == nil {
            self.tmpCalculatHeightCell = self.tableView.dequeueReusableCellWithIdentifier("MyTableViewCell") as? MyTableViewCell
        }
        
        if let cell = self.tmpCalculatHeightCell as MyTableViewCell! {
            cell.setHeight(containtHeight)
            
            cell.setNeedsLayout()
            cell.layoutIfNeeded()
            
            return cell.totalCellHeight
        }
        
        return 0
    }
    
}

class MyTableViewCell : UITableViewCell {
    
    var widthConstraint: NSLayoutConstraint?
    var heightConstraint: NSLayoutConstraint?
    
    var baseViewLayout:Layout?
    var resizableViewLayout:Layout?
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        self.contentView.backgroundColor = UIColor.redColor()
        
        //Auto Layout
        self.baseViewLayout = Layout.registUIView(superview: self.contentView)
            .widthIsSame(self.contentView)
        
        //Auto Layout
        self.resizableViewLayout = Layout.registUIView(superview: self.baseViewLayout!.view)
            .top(10).fromSuperviewTop()
            .bottom(10).fromSuperviewBottom()
            .width(0).lastConstraint(&widthConstraint)
            .height(0).lastConstraint(&heightConstraint)
            .horizontalCenterInSuperview()
            .backgroundColor(UIColor.yellowColor())
        
    }
    
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setHeight(height:CGFloat) {
        self.widthConstraint?.constant = height
        self.heightConstraint?.constant = height
    }
    
    var totalCellHeight:CGFloat {
        return baseViewLayout!.view.bounds.height
    }
}

SwiftでAuto Layout設定した直後に一部を角丸表示

Auto Layout後に一部を角丸表示

UIViewで一部を角丸にするにはUIBezierPathとCAShapeLayerでmaskを使用するとできますが、Auto Layout設定後にこの方をそのまま使うと角丸表示されません。例によって、view.layoutIfNeeded()を読んでやる必要があるようです。


一部を角丸にする方法の参考サイト:
cornerRadiusを片側だけ効かせる(UIViewの一部だけ角丸にする)
Rounded UIView using CALayers - only some corners - How?


swift-layout対応

この機能を使って、swift-layoutで角丸ができるようにしました。

//UIView生成
let view = UIView(frame: CGRect(x: 10, y: 50, width: 100, height: 100))
view.backgroundColor = UIColor.redColor()
self.view.addSubview(view)
        
//右上と左上を角丸に
Layout.roundRect(view, byRoundingCorners: (UIRectCorner.TopLeft | UIRectCorner.TopRight),cornerRadii:20)

とあるUIViewかそのサブクラスを角丸にしたい場合

let layout = Layout.registUIView(superview: self.view)
    .top(30).fromSuperviewTop()
    .left(50).fromSuperviewLeft()
    .width(100)
    .height(100)
    .backgroundColor(UIColor.redColor())
    .roundRect(byRoundingCorners: (UIRectCorner.TopLeft | UIRectCorner.TopRight), cornerRadii: 20)

全部Layoutクラスで書く場合


roundRectTop(20)


(UIRectCorner.TopLeft | UIRectCorner.TopRight)の代わりに、.roundRectTop(20)でも可能

let layout1 = Layout.registUILabel(superview: self.view)
    .top(30).fromSuperviewTop()
    .left(50).fromSuperviewLeft()
    .width(100)
    .height(100)
    .backgroundColor(UIColor.redColor())
    .text("Top")
    .roundRectTop(20)

let layout2 = Layout.registUILabel(superview: self.view)
    .top(10).fromBottom(layout1.view)
    .left(50).fromSuperviewLeft()
    .width(100)
    .height(100)
    .backgroundColor(UIColor.cyanColor())
    .text("Right")
    .roundRectRight(20)

let layout3 = Layout.registUILabel(superview: self.view)
    .top(30).fromSuperviewTop()
    .left(10).fromRight(layout1.view)
    .width(100)
    .height(100)
    .backgroundColor(UIColor.yellowColor())
    .text("Left")
    .roundRectLeft(20)

let layout4 = Layout.registUILabel(superview: self.view)
    .top(10).fromBottom(layout3.view)
    .left(10).fromRight(layout2.view)
    .width(100)
    .height(100)
    .backgroundColor(UIColor.greenColor())
    .text("Bottom")
    .roundRectBottom(20)

let layout5 = Layout.regist(Layout.createCharWrappingLabel("TopLeft\nBottomRight"), superview: self.view)
    .top(10).fromBottom(layout2.view)
    .left(50).fromSuperviewLeft()
    .width(100)
    .height(100)
    .backgroundColor(UIColor.grayColor())
    .roundRect(byRoundingCorners: (UIRectCorner.TopLeft | UIRectCorner.BottomRight), cornerRadii: 20)