tomoyaonishiのブログ

iOSのことを中心に・・・その他もあるよ!

AppStoreでリリース中のアプリのソースコードを公開してみた。

僕が開発したiOSアプリ「百選めぐり」のソースコードを公開しました。

試験的にnoteで有料販売にしたいと思います。ソースコード売買のプラットフォームもあるのですが、利用側がわざわざアカウント作るモチベがいまいちイメージできなかったのでnoteにしてみました。

アプリはこちらです。

百選めぐり

百選めぐり

思いつきで一気に作ったので通信などロジック周りの作りは微妙かもしれません。

詳細はこちら。

note.mu

もし売れたらレポートしたいと思います。

HighSierraにアップデートするとパーミッションがおかしくなったのでHomebrewを再インストールする

High SierraにアップデートしたらMacパーミッションの周りが変わったみたいでhomebrewが使えなくなりました。

手動でパーミッション変えたり、解決策はあるようでしたが、めんどくさいのでHomebrew自体を再インストールすることにしました。

まずはアンインストール

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"

そのあとインストール

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

これで再インストール完了です。

sudoとかつけまくってよくわからなくなってる人とかもこれで。

たまにはお掃除しましょう!

UICollectionViewFlowLayoutAutomaticSizeを使用したCollectionViewの使い方

UICollectionViewFlowLayoutAutomaticSizeを使用したCollectionViewFlowLayoutでUICollectionViewを作ったことがなかったので勉強がてらまとめました。

だいたいありそうなUI構成は問題なくできそうです。UICollectionViewFlowLayoutAutomaticSizeを使いながらも手動でセルサイズを変える方法もみつけました。

contentView自体の大きさを決めてしまうとViewDebuggerでみたときにAmbigiousLayoutになっていたり、ハマりどころ多かったですが、UITableViewのやりかたと同じ要領でやればいいだけでした。。

layout.invalidateLayout()を、viewWillLayoutSubviewsで呼びたかったのですが、Cellの中のviewが更新されるタイミングなのかスクロール中にviewWillLayoutSubviewsが何度も呼ばれてしまって、無限ループになってしまう問題に直面しました。viewWillLayoutSubviewsでinvalidateしないほうがいいのか、Cellの構成が間違っているのか。。

これはこういうもんだった。willDisplayCellあたりでセルのレイアウトが走るからでした。

あと、よくない制約の貼り方でセルの大きさを決められないとき、以下のワーニングが出ます。ログの通り、セルが表示されなかったり、無限ループになったりして面倒でした。 1つずつセルをみていって、解消するしかないです。

CollectionViewMaster[39946:2565272] The behavior of the UICollectionViewFlowLayout is not defined because:
2018-06-24 14:11:24.247596+0900 CollectionViewMaster[39946:2565272] the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values.
2018-06-24 14:11:24.247730+0900 CollectionViewMaster[39946:2565272] Please check the values returned by the delegate.
2018-06-24 14:11:24.247976+0900 CollectionViewMaster[39946:2565272] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7fe0f0c17e50>, and it is attached to <UICollectionView: 0x7fe0f102ca00; frame = (0 0; 414 736); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x604000245700>; layer = <CALayer: 0x60400023dfe0>; contentOffset: {0, -64}; contentSize: {414, 8990}; adjustedContentInset: {64, 0, 0, 0}> collection view layout: <UICollectionViewFlowLayout: 0x7fe0f0c17e50>.
2018-06-24 14:11:24.248337+0900 CollectionViewMaster[39946:2565272] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.

ソースコードはこちら。

github.com

JavaScript, TypeScriptでシングルトン

iOSなどではよく(?最近はアンチパターンですかね)で使われるシングルトンをブラウザ環境上のJavaScriptでどう書くかわからなかったのでメモです。

コードはTypeScriptですが、こんな感じです。

定義側

interface StoreInterface {
    x: number;
    methodHoge: () => any;
}

class Store implements StoreInterface {
    x = 0;

    constructor() {
    }

    methodHoge() {
        this.x++;
    }
}

export default new Store();

利用側

import Store from "./Store";

render() {
      Store.methodHoge();

      // ...
}

export default のところで new をつけてインスタンス化しているところがポイントでしょうか。

UITableViewについて改めて考える

UITableViewについて改めて考えました。

iOSアプリとは切っても切れないUITableViewですが、リスト=UITableViewという考え方はどうかなとおもったのでメモがてら書きます。

結論から言うと再利用しないのであれば(もしくは再利用がごく少量)UIScrollViewのほうがいいじゃんということなのですが。

UITableViewはiPhoneOS2.0から利用可能です。当時はメモリが少なく目に見えない部分の無駄なViewを保持しておくということがあり得ない状況でした。そこでUITableViewのCellを再利用しメモリ使用量を減らすというアプローチが取られたわけです。

現在の主要なiPhoneは十分にメモリがあるので、そこまでセンシティブにならなくてもいいかなとおもっています。

リストだけれども、再利用しないViewで構成されている場合はScrollViewにaddしてそのViewの参照をプロパティにでも持っておくほうがスクロールのたびにViewを作り直すコスト(パフォーマンスとコード)がなくていいなと個人的に思います。

TwitterのようなツイートViewが大量にリスト形式で表示されるようなThe UITableViewだねってところはよいのですが、ユーザ情報入力画面のような各行(氏名、性別、プロフィール画像、コメントなど)が1度きりの登場でかつ10行程度のものであればUIScollViewにaddしてプロパティにViewの参照をもっておくほうがそれぞれのViewに入力された情報も簡単に取得できるので便利です。TableViewだとcellForRowで再描画するために、Viewの値をインデックス付きで保持するために、各Viewの値をdelegateなりで受け取って云々と面倒です。

めんどくさくなってきたのでソースコードへどうぞ。

github.com

ソースコードのVisualFormatの中身の順序を変えると表示順が変更されます。縦方向だけVisualFormat使うといいよ。

AutoLayoutでスワイプできるUITableViewCellのサンプルを更新しました。

セルをスワイプできるUITableViewのサンプルプロジェクトをずっと前に書いていましたがSwift1.2くらいだったので、Swift4に更新しました。

AutoLayoutを使っているのでConstraintを直接いじる形でもよかったのですが、一時的なViewの移動に最適なUIView.transformをいじる形にしてみました。

github.com

iPhoneX && UITabBarController && UIToolbar && SafeArea && Blur

iPhoneXでUITabBarControllerのタブをhiddenにしてもSafeAreaが伸びてくれずに、タブ上部に表示しているUIToolbarのblurがHomeIndicatorまで伸びてくれない場合に以下の対応で対症療法できることが判明しました。

UITabbarの上にUIToolbarの構成で、UIToolbarはSafeArea.bottomに対して貼り付けている状態です。

f:id:tomoyaonishi:20171020195548p:plain:w300

UITabbarをhiddenにするとHomeIndicatorまでBlurが伸びません。

f:id:tomoyaonishi:20171020195821p:plain:w300

UITabBarがない場合のSafeArea.bottomの位置までUIToolbarを下げてあげる(-49.0pt)となんとBlurがHomeIndicatorまで伸びてくれます。たぶんiOSのバグですが、その場しのぎの対応であればこれがいいかもしれません。

class FirstViewController: UIViewController {
    @IBOutlet weak var bottomMargin: NSLayoutConstraint!
    
    @IBAction func hideBar(_ sender: Any) {
        guard let tab = tabBarController?.tabBar else { return }
        if tab.isHidden {
            tabBarController?.tabBar.isHidden = false
            bottomMargin.constant = 0.0
        } else {
            tabBarController?.tabBar.isHidden = true
            bottomMargin.constant = -49.0
        }
    }

}

f:id:tomoyaonishi:20171020200716p:plain:w300

UIBarPositioningなど考えうるものすべていじりましたがこれでしか直せませんでした。

なんかバックグラウンドいって戻ってくるとUITabBarがhiddenでもSafeAreaの位置が正しくなってるような気がするんですが、よくわかりません。。

FoursquareのAPIについて

FoursquareAPIが無料枠アカウントでは、商用目的では使えないという旨の記載があった;;

なんとかならないかと思い、直接Foursquareさんに問い合わせたところ、無料枠アカウントでも運用やソフトウェアの資金調達のための広告等はOKとの返事をもらいました。

これなら、もし運用費稼ぎたくなっても安心して使えますね!

引用

you can use the API's up to the current maximum rate limit for free and would not require a commercial license if you're just using advertising to fund your app/software.

Homebrewを再インストール

El CapitanにアップデートしたらMacパーミッションの周りが変わったみたいでhomebrewが使えなくなりました。

手動でパーミッション変えたり、解決策はあるようでしたが、めんどくさいのでHomebrew自体を再インストールすることにしました。

まずはアンインストール

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"

そのあとインストール

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

これで再インストール完了です。

CentOS7.1にてApacheとPythonでCGI

CentOS7.1にApacheを入れた状態でPythonCGIとして動かしたかったのでその時の手順をメモしておきます。 

IDCFクラウドの一番貧弱なサーバを借りてやっております。

Apacheファイアウォールなどの設定はできていて、index.htmlは見れる状態からスタートです。

apacheの設定ファイルを編集します。

$ sudo vim /etc/httpd/conf/httpd.conf

長々と設定が書かれていますが、以下の箇所を見つけます。

<Directory "/var/www/cgi-bin">
    AllowOverride None
    Options None
    Require all granted
</Directory>

次のように修正します。オプションにCGIで実行可能、実行するファイルの拡張子は.py .cgiとします。(.cgiは今回は使いません)

<Directory "/var/www/cgi-bin">
    AllowOverride None
    Options ExecCGI
    Require all granted
    AddHandler cgi-script .cgi .py
</Directory>

設定ファイルの編集はこれで終わりです。CGIスクリプトを作成します。

CGIスクリプトの置き場所に移動します。

$ cd /var/www/cgi-bin/

実行ファイルを作成します。

sudo vim test.py

---

#!/bin/python3.5

print("Content-Type: text/plain")
print("")
print("Hello World")

!bin/python3.5のところはpython3.5を入れているのでこうなっています。デフォルトのpython2.7でよければ#!bash/pythonでいけるとおもいます。 CGIスクリプトの出力方法については別途検索してください。ヘッダーとボディの間に1行改行のためのprintが入っています。

権限を付与します。

sudo chmod 755  test.py

ブラウザから

http://IPアドレス/cgi-bin/test.py

にアクセスすれば「Hello World」と出ます。

SwiftのAttributesをまとめた。

Swiftには定義や型に対して情報を補足するAttributesというものがあります。 Objective-CのNS_AVAILABLE_IOS(8_0)みたいなやつです。

Attributesのフォーマットは引数なしと引数ありの2通りです。

@attributename
@attributename(arguments)

@availability

利用できるプラットフォーム、OSなどを示します。

第1引数はプラットフォーム名で

  • iOS, iOSApplicationExtension, OSX, *

の4つのうちのどれかを指定します。*はワイルドカードで全てのプラットフォームを表します。

第2引数以降はいろいろな組み合わせがあります。

  • unavailable

利用不可能であることを示します。この属性がついたものを利用しようとするとコンパイラがエラーを吐きます。

@availability(iOS, unavailable)
func unavailableMethod() {
}

f:id:tomoyaonishi:20150320180035p:plain

  • introduced="version number"

指定されたバージョンから利用可能であることを示します。

@availability(iOS, introduced=6.0)
func shouldAutorotate() -> Bool
  • deprecated="version number"

指定されたバージョン以降で非推奨であることを示します。

@availability(iOS, introduced=3.0, deprecated=8.0)
var searchDisplayController: UISearchDisplayController? { get }
  • obsoleted ="version number"

指定されたバージョンから使われなくなり、そのプラットフォームから取り除かれていることを示します。対象のプラットフォーム、バージョンのときこの属性がついたものを利用しようとするとコンパイラがエラーを吐きます。

f:id:tomoyaonishi:20150320175728p:plain

  • message

deprecatd, obsoletedと一緒に用いて、コンパイラがワーニングやエラーを出すときにこのメッセージを表示します。

  • renamed

unavailableと一緒に用いて、新しい名称にリネームされたことを示します。

@availability(*, unavailable, renamed="AfterClass")
class BeforeClass {

}

class AfterClass {

}

f:id:tomoyaonishi:20150322134652p:plain

@NSCopying

Objective-Cの@property(copy)と同じです。型はNSCopyingプロトコルに適応していなければいけません。

@objc

Objective-Cからも呼び出せるようにします。

@objc class CustomObject {
    
}

以下のように引数を指定すればObjective-Cからは()内の定義で呼び出せます。

@objc(ObjCObject)
class CustomObject {
    
    var editing: Bool {
        @objc(isEditing) get {
            return true
        }
        set {
        }
    }
    
}

@autoclosure

引数なしかつ評価値を返す式を自動でクロージャにラップします。簡単な遅延評価を行いたいときに便利です。XCTAssert系で使われていますね。


その他は割愛します。

@NSApplicationMain

@UIApplicationMain

@noreturn

@NSManaged

@IBAction

@IBOutlet

@IBDesignable

@IBInspectable


Objective-C版はこちら tomoyaonishi.hatenablog.jp


ドキュメント developer.apple.com

Swiftでxibで作成したカスタムビューのインスタンスを簡単に返す

Objective-Cではxibで作成したカスタムビューのインスタンスを以下のように生成して利用していました。

しかし、Swiftではinitのなかでselfに代入することはできないようです。(もし出来るならやり方教えてください。) なので以下のような書き方でxibからロードしたインスタンスを返すことはできないようです。

let view = CustomView()

f:id:tomoyaonishi:20150320161312p:plain

回避策としてジェネリクスを使ってどんなカスタムビューのインスタンスでも返すことができる関数を作ってみました。

func InstantiateCustomView<T: UIView>(classToCreate: AnyClass) -> T {
    let view = UINib(nibName: NSStringFromClass(classToCreate), bundle: nil).instantiateWithOwner(nil, options: nil)[0] as T
    
    return view
}

使い方は簡単です。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let view: CustomView = InstantiateCustomView(CustomView)
        self.view.addSubview(view)
    }

}

返り値の型Tが確定しないといけないので、 let view: CustomViewのように型を定義して受け取ります。(スマートじゃない。。)

CustomViewは以下のように定義しています。 

@objc(CustomView)
class CustomView: UIView {
    
}

@objcはCustomViewクラスのクラス名をObjC名前空間に"CustomView"という名前で書きだすという意味です。Swiftではクラス名は"モジュール名.クラス名"となってしまい、NSStringFromClassの返り値をそのままnibNameに渡せません。@objcとつけることでNSStringFromClassで取得できるクラス名がモジュール名なしの形になります。

ちなみにInstantiateCustomView関数は<T: UIView>としているのでTはUIViewのサブクラス以外受け付けません。

もうちょっとスマートにしたいです。。。

iOS8でコードでのAutoLayoutのやり方が地味に変わっていた件

あけましておめでとうございます。

仕事でAutoLayoutでUIを作っていてなんとなく-[UIView addConstraint], -[UIView addConstraints], -[UIView removeConstraint], -[UIView removeConstraints]の定義を見たところ、deprecatedになっていました。正式なdeprecatedではなく

// This method will be deprecated in a future release and should be avoided. 

とコメントに書いてありました。今まではNSLayoutConstraintで制約を作って対象のUIViewのaddConstraint, addConstraintsを呼んでいたわけですが、それが非推奨になっています。

-[UIView addConstraint]の代わりにNSLayoutConstraintのactiveプロパティを使えと書いてあります。-[UIView addConsraints]の代わりには+[NSLayoutConsraint activateConstraints]を使えと書いてあります。後者のクラスメソッドはおそらく引数に渡されたNSLayoutConsraintのactiveプロパティをすべてtrueにセットする便利メソッドだと思います。

NSLayoutConstraintのactiveプロパティをtrueにするとその制約が有効になるようです。今までは対象のviewに対してaddConstraint、removeConstraintで制約を追加、削除していました。activeプロパティは対象のviewに追加する必要がなく、有効にするか、しないかで制約を制御できるようになったようです。

具体的には以下のようにコードを変更します。

NSLayoutConstraint.constraintsWithVisualFormatで複数の制約を作り、それらの制約を有効にする(activeプロパティをtrue)ためNSLayoutConstraint.activateConstraintsに渡します。(制約のactiveプロパティをtrueにすればいいのでKVCでtrueにしても多分同じように動くはずです。)

作った制約をsuperviewに追加するのか、自分自身に追加するのか気にしなくて良くなったので、多少便利になりましたね。

この変更はSizeClassのためでしょうか。Regular, Compact、縦、横画面に合わせてそれぞれ用意しておいた制約をactivateするだけでレイアウトが変えられるので便利な気がします。

正式なdeprecatedではないので、しばらくは大丈夫かと思いますが早めに乗り換えておくほうがよいかと思います。

AutoLayoutでスワイプできるUITableViewCellを実装する

AutoLayoutを使ってセル上を左にスワイプすると、セルの右側がオープンするUITableViewCellを実装してみます。セルの削除のときによく出てくるあれを自分で実装する感じです。 AutoLayoutやstoryboardにある程度知識がある方を前提としていますので、適宜不足している情報は補って実装してみてください><


準備編

  • まずは、通常通りにstoryboardでUITableViewを実装します。
  • 次に、スワイプした時にわかるようにセルの背景色を変えておきます。

f:id:tomoyaonishi:20141012212839p:plain

これで準備完了です。スワイプできるセルを実装していきます。

TableViewを貼り付けているViewControllerは以下のようになっています。普通です。

import UIKit

class ViewController: UIViewController, UITableViewDataSource {

    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        return 10
    }
    
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
        return tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
    }
    
    
}

実装編(storyboard)

  • セルの上にスワイプ用のビューを1枚重ねます。画像のようにセルのcontent view全面に張り付くようにAutoLayoutを設定します。

f:id:tomoyaonishi:20141013205953p:plain

このビューの名前をswipe viewとします。

f:id:tomoyaonishi:20141013210032p:plain

  • swipe viewに設定したAutoLayoutのうち、右側からのConstraintをカスタムセルクラスにIBOutletで接続します。

f:id:tomoyaonishi:20141013210304p:plain

ユーザがセルをスワイプしたとき、このConstraintの値を操作することでセルの右側を開いたり、閉じたりするというわけです。

  • swipe viewもIBOutletで接続しておきます。
@IBOutlet weak var swipeView: UIView!

実装編(コード)

  • ユーザのスワイプを認識するためにカスタムセルに対して、UIPanGestureRecognizerを追加します。
    var panGestureRecognizer: UIPanGestureRecognizer!
    
    
    override func awakeFromNib() {
        super.awakeFromNib()
        self.panGestureRecognizer = UIPanGestureRecognizer(target: self, action: "didPan:")
        self.panGestureRecognizer.delegate = self
        self.swipeView.addGestureRecognizer(self.panGestureRecognizer)
    }

これでユーザがセル上をスワイプした時に、didPan:メソッドが呼ばれるようになります。

  • didPanメソッド内では、ジェスチャーのstateに合わせてそれぞれ処理を行います。

実装は以下のようになるかと思います。

    func didPan(sender: UIPanGestureRecognizer) {
        switch sender.state {
        case .Began:
            let translation = CGPoint(x: -self.swipeViewMarginRight.constant, y: 0.0)
            self.panGestureRecognizer.setTranslation(translation, inView: self.swipeView)
        case .Changed:
            let translation = self.panGestureRecognizer.translationInView(self.swipeView)
            self.swipeViewMarginRight.constant = -translation.x
        case .Cancelled:
            fallthrough
        case .Ended:
            break
        default:
            break
        }
    }

UIPanGestureRecognizerは対象のビュー上をスワイプした量を保持しています。translationInViewメソッドで取得できます。このtranslationのxの値を見ることでユーザが横方向にどれだけスワイプしたのかが分かります。例えば、ユーザが左に100ptスワイプしたときには、translation.xは-100になります。この-100の値をswipe viewの右側からのConstraintにセットすることで、swipe viewはsuperviewの右端から100pt離れるようになります。このようにしてセルの開閉を実装していきます。

.Changedの処理はユーザがビュー上をスワイプするごとに呼ばれます。よって、ここで移動量を取得し、それをConstraintにセットします。.Beganの処理はUIPanGestureRecognizerの保持しているtranslationの値の初期化です。文章で説明がしづらいので割愛しますが、この処理がある場合とない場合で実際に動かしてみるとわかると思います。

  • ひとまずスワイプできるセルの完成です。

実行してみると各セルが右、左にスワイプできることが確認できるかと思います。

f:id:tomoyaonishi:20141013214745p:plain

しかし!!ここで大問題が発生します。なんとスワイプはできてもTableViewのスクロールが出来ないのです。このコードが糞なわけではなく、GestureRecognizerの仕様がそうしています。実はUIGestureRecognizerはデフォルトでは2つ以上同時に認識してくれません。UITableViewにはスクロール認識用のUIPanGestureRecognizerが動いています。そしてセルには自分で追加したUIPanGestureRecognizerがあります。階層的に最初に動作するジェスチャーは自分で追加したジェスチャーですので、こちらの処理だけが動いて、TableViewのジェスチャーが動作しなくなっているというわけです。

  • 2つのジェスチャーが同時に動作するようにする やり方は実に簡単です。UIGestureRecognizerDelegateの
optional func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool

このデリゲートメソッドでreturn trueを返すだけです。最初の引数のジェスチャーは自分でセルに追加したジェスチャーです。2つ目の引数がTableViewのジェスチャーです。2つ目のジェスチャーはTableViewのジェスチャーとは限らないので、TableViewのジェスチャーかどうかをチェックして、そうであればtrueを返す処理にすべきです。

func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer == self.panGestureRecognizer && otherGestureRecognizer == TableViewのジェスチャー {
        
            return true
        }
        
        return false
    }
  • 実行します。

TableViewのスクロールもできて、セルのスワイプもできると思います。しかし、まだ動作が完璧ではありません。TableView上を少しでも横にスワイプするとセルがオープンしてしまい、使い勝手が悪いです。TableViewを斜めにスワイプするとセルがオープンしつつTableViewもスクロールしてしまうという微妙な実装になっています。できればTableViewのスクロールなのか、セルのスワイプなのかを判定してから、どちらかの動作しかしないようにしたいです。

  • TableVIewのスクロールなのかセルのスワイプなのかを判定する

ここは泥臭い処理になってしまうのですが、セルのジェスチャーの最初の数回のtranslationを監視して、スワイプの角度が一定以上であればTableViewのスクロールとみなしてセルのスワイプの処理をしない、逆にスワイプの角度が一定以下であればセルのスワイプとみなし、TableViewのスクロールをしないように切り替えることになります。ちょっとコードが複雑になっていますが、こんな感じになるでしょうか。(もっとスマートにできる方法知ってる方は教えて下さい!)

    var tableView: UITableView {
        
        return self.superview?.superview as UITableView
    }

    var trackingCount = 0
    var trackingTranslation = CGPointZero
    var allowsSwipe = false
    
    
    func didPan(sender: UIPanGestureRecognizer) {
        switch sender.state {
        case .Began:
            let translation = CGPoint(x: -self.swipeViewMarginRight.constant, y: 0.0)
            self.panGestureRecognizer.setTranslation(translation, inView: self.swipeView)
        case .Changed:
            let translation = self.panGestureRecognizer.translationInView(self.swipeView)
            
            // 最初の数回のtranslationを別途保持してスワイプの角度を計算し、
            // TableViewのスクロールなのかセルのスワイプなのかを判断する
            self.trackingCount++
            self.trackingTranslation.x += translation.x
            self.trackingTranslation.y += translation.y
            if trackingCount == 3 {
                self.allowsSwipe = self.allowsSwipeWithTranslation(self.trackingTranslation)
            }
            
            if self.allowsSwipe {
                self.tableView.scrollEnabled = false
                let translation = self.panGestureRecognizer.translationInView(self.swipeView)
                self.swipeViewMarginRight.constant = -translation.x
            }
        case .Cancelled:
            fallthrough
        case .Ended:
            self.trackingCount = 0
            self.allowsSwipe = false
            self.trackingTranslation = CGPointZero
            self.tableView.scrollEnabled = true
        default:
            break
        }
    }
    
    
    func allowsSwipeWithTranslation(translation: CGPoint) -> Bool {
        // スワイプの角度を計算して判断する
        let slope: CGFloat = abs(translation.y) / abs(translation.x)
        let radian: CGFloat = atan(slope)
        let angle = radian * 180 / CGFloat(M_PI)
        
        if angle >= 0 && angle <= 30 {
            
            return true
        }
        
        return false
    }

.Changedで最初の3回までself.allowsSwipeがfalseなのでセルのスワイプ動作は起こらず、3回目のif文の中で3回分の移動量でスワイプの角度を計算します。xの移動量とyの移動量が取れるので簡単にわかります。そしてその角度が一定以下であればself.allowsSwipeにtrueを入れるようにしてセルのスワイプを動作させます。そのときTableViewはスクロールしないようにします。 逆に3回目のif文でTableViewのスクロールだと判断した場合には、self.allowsSwipeにはfalseが入り、セルのスワイプ処理は動作しません。

.Endedでは、一連の処理で使用した変数の初期化を行います。


まとめ

今回はスワイプできるセルをAutoLayoutで実装しました。骨組み的なところしか載せていないので、細かい処理は記述していません。セルを開くときにアニメーションさせたり、右側だけでなく左側も開けるようにしたり、開くことができる幅の上限を処理したりと完璧なスワイプを実装するにはまだまだ多く実装することがあります。

最後に気づいた方もいるかもしれませんが、今回の実装だとAutoLayout的にswipe viewのwidthが小さくあります。swipe viewが圧縮されるとでもいいますか。左側のConstraintはそのままですから、全体が横にスライドするわけじゃありません。なのでsiwpe viewにUILabelなどを載せているとスワイプする度にUILabelの中身が幅に合わせてリサイズされて再描画されます。そのままスライドしているように見せるには、右側のConstraintの値にマイナスを掛けた値を左側のConstraintにセットします。そうすれば幅は固定のままswipe view全体が左に移動しているように見えます。

以上です。

ソースコード

TomoyaOnishi/SwipableCell · GitHub

UITableViewCellの高さを自動で計算する: UITableViewAutomaticDimension

UITableViewCellのUITableViewAutomaticDimensionを使えば、セルのそれぞれの高さを自動で計算させることができます。

    optional func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat

こいつとはおさらばです。

以下実装方法です。


まず通常のUITableViewの実装の方法でStoryboardを作っていきます。 カスタムセルに対してUILabelを1つ置きます。ここで以下の画像のようにセル全体にLabelが広がるようにAutoLayoutを設定します。なお、カスタムセルではなくBasicStyleのセルであればここでは何もしなくてもよいです。

f:id:tomoyaonishi:20140927160105p:plain

次にカスタムセル、BasicStyleのセルであったとしても以下のようにLabelのlineプロパティを0にして複数行入るように変更します。

f:id:tomoyaonishi:20140927160113p:plain

後はViewController側で以下のようなソースコードを書きます。

class ViewController: UIViewController, UITableViewDataSource {

    @IBOutlet weak var tableView: UITableView!
    
    
    let phrases: [String] =
    [
        "やっほー",
        "すげええええええええええええええええええええええええええええええええええええええええええええええええええええええええええ",
        "TableViewCellが自動でセルの高さを計算しています!!!!!!!!!!!!!",
        "\n\n\n\n\nホントです。",
    ]

    
    // MARK: Life cycle
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.estimatedRowHeight = 100.0
        self.tableView.rowHeight = UITableViewAutomaticDimension
    }
    
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        self.tableView.reloadData()
    }

    
    // MARK: UITableViewDataSource
    
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        return self.phrases.count
    }
    
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as MyTableViewCell
        cell.label.text = self.phrases[indexPath.row]
        
        return cell
    }
    
}

これで実行するとセルの高さが自動で計算されてTableViewが表示されます。

f:id:tomoyaonishi:20140927161107p:plain

サブビューが2つ以上ある場合でもAutoLayoutをうまく貼れば自動で計算されます。 iOS8, Xcode6で試しました。