読者です 読者をやめる 読者になる 読者になる

tomoyaonishiのブログ

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

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で試しました。

Xcode6でベクター画像を利用する。

Xcode6で密かにベクター画像が使えるようになっています。 これを使えば@1x, @2x, @3x画像を用意する煩わしさから解放されます! 使い方は非常に簡単です。

  • PDF画像を@1xのサイズで書きだす。
  • AssetCatalogをクリックする
  • New Image Setで画像セットを追加する
  • 画像のようにAttributesのTypesをVectorsに変更する

f:id:tomoyaonishi:20140921160517p:plain

  • 作成したPDF画像を置く。

こうすれば後は今までと同じように画像を扱えます。 注意しなければいけないことは完全なベクター画像の対応というわけではなく、ビルド時に@1x, @2xなどの各PNG画像が書き出されるようです。ソースコード上で拡大や縮小ができるわけではないようです。

デザイナーの負担もエンジニアの負担もすごく軽くなりますね。

WWDC 2014 Session Videos - Apple Developer

Swift: NSUserDefaultsに配列が保存できない

Swiftでプライベートで開発中のアプリでこんな出来事に遭遇しました。

        let array = ["1", "2", "3", "4"]
        NSUserDefaults.standardUserDefaults().setObject(array, forKey: "key")
        NSUserDefaults.standardUserDefaults().synchronize()
  
        let array = NSUserDefaults.standardUserDefaults().arrayForKey("key") as? [String]
        println(array) // nil

NSUserDefaultsに配列が保存できないという罠にハマりました。 原因究明まで結構時間がかかりましたが、結論としては保存する配列を as NSArrayとすることです。 コードでは以下のようになります。

        let array = ["1", "2", "3", "4"] as NSArray // NSArrayにキャストする!
        NSUserDefaults.standardUserDefaults().setObject(array, forKey: "key")
        NSUserDefaults.standardUserDefaults().synchronize()
  
        let array = NSUserDefaults.standardUserDefaults().arrayForKey("key") as? [String]
        println(array) // Optional ["1", "2", "3", "4"] 

なぜこうなるのか理解できていません。Xcode6 beta6です。Swiftの配列はNSCodingを実装していないから?ネットで検索するとSwiftの配列も普通に保存できると書いてある記事もある。 でも確実に自分の環境ではNSArrayにキャストしないと保存できない。これは確実です。 もし、NSCodingを実装していないからだというなら、SwiftからiOS開発する人にはちょっと厳しすぎる仕様ですね。むしろSwiftになったせいでObjective-Cの開発でも意識しないようなFoundationのもっと深い知識が各所で要求されるようになった気もしています。特にCoreFoundation系のAPIを使うときによく感じます。自分がまだ理解不足なだけだと思うのですが、CoreFoundation系を使っているとたまに変換したい型までもっていけないことがありますw(暗黙の型変換がないせいで)

この件に関してはバグかもしれないし、実際どうなのかはよくわかりません。

SLRequestを用いたTwitterのin_reply_toツイートがうまくいかない

Twitterのリプライでin_reply_toを実装していて少しハマりました。 in_reply_to_status_idをパラメータに入れているのに、会話形式で表示されないという問題に遭遇しました。

SLRequestに渡すパラメータのNSDictionaryの中にin_reply_to_status_idをキーにNSNumber型でリプライ先のツイートIDを指定していたのですが、これをリプライ先のツイートIDはid_strを取得するようにしてNSString型でセットしてみると会話形式で表示されるようになりました。数値か文字列かよくわかりません。

{
    "in_reply_to_status_id" = 505962876931043328; // NSString型にしておく
    status = "@Tomoya_Onishi \U306f\U306f\U306f\U306f";
}

SwiftからObjective-Cのenumを扱う時の注意事項2

注意事項というかObjective-CenumSwiftはどう解釈するかのメモ

ObjC側でのenum

typedef enum : NSUInteger {
    MyEnumValueA,
    MyEnumValueB,
    MyEnumValueC,
} MyEnumValue;

があったとする。この定数をSwift側からさわろうとするとほとんどの場合でエラーになります。

SwiftはNS_ENUMあるいはNS_OPTIONSで定義されたCスタイルのenumのみを自動でSwiftでのenumに変換します。

NS_ENUM, NS_OPTIONSでenumを定義するように変更する。

NS_ENUM (NSUInteger, MyEnumValue) {
    MyEnumValueA,
    MyEnumValueB,
    MyEnumValueC,
};

そうするとSwift側で

enum MyEnumValue: Int {
    case A
    case B
    case C
}

という形で自動で読み込んでくれます。後はSwiftでのenumと同じ挙動になります。 ビット演算でのenumにはNS_OPTIONSを使ってください。

参考 Using Swift with Cocoa and Objective-C: Interacting with C APIs

SwiftからObjective-Cのenumを扱う時の注意事項

Swiftを使って開発していても、OSSなどはObjective-Cで書かれたものを使うことはよくあります。 SwiftからObjective-Cのクラスなどを使うにはヘッダーファイルを用意するだけですが、Objective-C側に書かれたenumの扱いには注意が必要です。

結論から言うと、Objective-C側のenumの定義には

NS_ENUM
NS_OPTIONS

のマクロを使って定義するようにしてください。

typedef enum 

での定義ではSwift側で扱えません。

ビット演算でのオプションの定義などはNS_OPTIONSマクロを使わないと、 swift側で | & などのビット演算子での処理ができません。(エラーになってしまいます。)

もしObjective-Cの定数をSwiftで使っていてエラーが出るときは、enumの定義の仕方を調べてみましょう!

マクロの使い方についてはこちらをどうぞ!

メソッド、クラス、変数、定数宣言時に使えそうなものまとめ - tomoyaonishiのブログ

Swift: AutoLayoutでUIVisualEffectviewをアニメーションさせてみた

iOS8からUIVisualEffectViewというものが追加されました。このビューは様々なエフェクトを自動で表示することができます。Appleの公式すりガラス処理を実現することができます。 今回は、Swiftを使って、このクラスの使い方とAutoLayoutによるアニメーションをまとめます。


スライドにもまとめています。

UIVisualEffectView

様々なエフェクトを自動で表示することができるビューです。Appleのすりガラス処理もしてくれます。具体的にどんなエフェクトにするかを決めるのはUIVisualEffectという別のクラスです。インスタンス生成時にこのクラスを渡します。

UIVisualEffect

エフェクトの種類を指定するクラスです。利用するのはそのサブクラスでUIBlurEffect, UIVibrancyEffectの2つが現在のところ用意されています。UIBlurEffectがすりガラス処理です。 UIBlurEffectにはスタイルが3つあります。非常に明るいブラー、明るいブラー、暗いブラーの3つです。

enum UIBlurEffectStyle : Int {
     case ExtraLight
     case Light
     case Dark
}

UIVisualEffectViewの生成方法

実際の生成コードは以下のようになります。

let effect = UIBlurEffect(style: UIBlurEffectStyle.Light)
let effectView = UIVisualEffectView(effect: effect)
// set frame
view.addSubview(effectView)

これだけのコードでApple公式のすりガラス処理が入るビューを利用することができます。UIImage+ImageEffectのように画像ではなく、動的に描画されるのでビューを動かしても問題ありません。また、動作も早いです。

AutoLayoutでアニメーション

今回はさらに、このビューをAutoLayoutを使ってアニメーションさせてみます。 frameでのアニメーションは一般的なので問題無いと思いますが、AutoLayoutでのアニメーションはどのようにやるのかわかりますか?

AutoLayoutではframe, boundsは基本的には触りません。むしろ勝手にいじるとAutoLayoutの整合性が保てなくなりクラッシュすることもあります。

では、どうするか。AutoLayoutの実態であるNSLayoutConstraintクラス(AutoLayoutではこれを制約という)を操作します。 NSLayoutConstraintクラスにconstantというプロパティがありますが、このプロパティはその名の通り、距離を表します。例えば、画面の左端から10ptあけてビューを表示するという制約(NSLayoutConstraint)があったとき、constantは10です。ここでconstantに100を入れると、画面左端から100ptの場所に移動します。frameは意識しなくてよいです。例えば、幅は100ptで固定するという制約があった場合、そのconstantに200を入れると幅が自動で200ptになります。ここでもframe, boundsを直接操作する必要はありません。 このようにAutoLayoutでは、制約のconstantを変更することでレイアウトを動的に変えることができます。もちろん、制約の再生成でもよいですが。

では、具体的にアニメーションのコードを見てみましょう。一部省略しています。 まずはUIVisualEffectViewを生成し、コード上でAutoLayoutを適用します。

// viewDidLayoutSubviewsかviewDidAppear内で
// View
let effect = UIBlurEffect(style: UIBlurEffectStyle.Light)
let effectView: UIVisualEffectView = UIVisualEffectView(effect: effect) effectView.clipsToBounds = false
// falseにすると AutoResizingMaskをAutoLayoutの制約に自動変換しないようになる
effectView.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(effectView)

// AutoLayout
// NSDictionaryOfVariableBindings関数はどこにいったのかよくわからないので自分で辞書を生成
let views = ["effectView" : effectView]
let metrics = ["marginZero" : 0, "marginTop" : 100]

// 水平方向の制約を追加:superviewに対してぴったり張り付く
let horizontalConstraints: AnyObject[] =
NSLayoutConstraint.constraintsWithVisualFormat("|-marginZero-[effectView]-marginZero-|",
                                                options: NSLayoutFormatOptions(0),
                                                metrics: metrics,
                                                views: views)
view.addConstraints(horizontalConstraints)

// 垂直方向の制約を追加:superviewに対して上は100ptあける、下はぴったり張り付く
let verticalConstraints: AnyObject[] =
NSLayoutConstraint.constraintsWithVisualFormat("V:|-marginTop-[effectView]-marginZero-|",
                                                options: NSLayoutFormatOptions(0),
                                                metrics: metrics,
                                                views: views)
view.addConstraints(verticalConstraints) 

// アニメーションのために上からの制約を保持
let constraint : AnyObject = verticalConstraints[0]
marginTopConstraint = constraint as? NSLayoutConstraint

これでUIVisualEffectViewは画面に対して、上から100あけて、左、下、右は画面にピッタリくっつくように表示されます。AutoLayoutなので横向き、iPad、どんなサイズの画面でも同じように表示されます。frameもboundsもいじっていません。

次に、アニメーション部分です。Storyboard上では、ブラーの効果がわかりやすくするため画面いっぱい広がるように適当な画像を表示しておき、ボタンを1つおいてIBActionで接続しておきます。そのボタンを押すたびにUIVisualEffectViewがアニメーションします。

var flag : Bool = false
@IBAction func didTapButton(sender: UIButton) {
    if let constraint = marginTopConstraint {
        UIView.animateWithDuration(1.0,
            delay: 0.0,
            usingSpringWithDamping: 0.5,
            initialSpringVelocity: 0.1,
            options: UIViewAnimationOptions(0),
            animations: {
          // frameのアニメーションと同じ考えだとアニメーションできない 
          // constantを変更するだけでは足りない 
         constraint.constant = self.flag ? 150 : 500
          // 画面の再描画を呼び出す必要あり 
         self.view.layoutIfNeeded()
            },
         completion: nil)
         flag = !flag 
    }
}

これを実行すればびよんびよんとアニメーションするはずです。 基本的にはAutoLayoutはStoryboard上で設定するはずです。その場合は、IBOutletと同じようにNSLayoutConstraintを接続すれば変数が作れます。

個人的にはframeによるレイアウトはiOS7のころから終わっていると思っています。Xcode5にも16:9, 4:3などのレイアウト設定がありました。Androidはとっくの昔に絶対座標での指定は非推奨になっています。Xcode6のことはあまり詳しく書けませんが、さらに多種多様な画面サイズを試すことができるようになっているので基本的にはAutoLayoutでやりましょうというスタンスがよいと思っています。

メソッド、クラス、変数、定数宣言時に使えそうなものまとめ

iOS開発でメソッド、クラス、変数、定数の宣言に使えそうなものをまとめました。 Appleのヘッダーファイルを見ているとよく出てくる

NS_AVAILABLE_IOS

みたいなやつで個人的に使えそうだと思ったものをまとめます。


資料はこちら


NS_CLASS_DEPRECATED_IOS

指定したOSバージョンからそのクラスを非推奨にします。

NS_CLASS_DEPRECATED_IOS(7_0, 7_1)
@interface TestClass : NSObject

@end

iOS7.0まではOK、iOS7.1からは非推奨(Deprecated)という意味になります。 実際にXcodeでiOS7.1の環境でTestClassを使ってコンパイルしようとするとワーニングが出るようになります。

NS_CLASS_AVAILABLE_IOS

特定のOSバージョンからそのクラスを使えるようにします。

NS_CLASS_AVAILABLE_IOS(7_1)
@interface TestClass : NSObject

@end

TestClassはiOS7.1から利用可能になります。

NS_AVAILABLE_IOS

指定したOSバージョンからクラス、メソッド、定数を使えるようにします。

@interface TestClass : NSObject

- (void)method NS_AVAILABLE_IOS(7_1);

@end

methodメソッドはiOS7.1以降の環境でないと呼べません。定数の後ろに宣言することで定数にも同じ制限がかかります。

NS_DEPRECATED_IOS

指定したOSバージョンからクラス、メソッド、定数を非推奨にします。

FOUNDATION_EXPORT 
CGFloat const kTestClassConstant NS_DEPRECATED_IOS(6_0, 7_0);

iOS7.0以上でこの定数を利用しようとするとコンパイラがワーニングを出してくれます。

NS_UNAVAILABLE

いかなる環境でもそのクラス、メソッド、定数を使えないようにします。

@interface TestClass : NSObject

- (void)unavailableMethod NS_UNAVAILABLE;

@end

unavailableMethodは絶対に呼ぶことはできません。利用しようとするとコンパイラはワーニングではなく、エラーを出します。

NS_REQUIRES_SUPER

サブクラスでオーバライドした際にスーパークラスメソッドを呼ぶようにします。 なにげにかなり便利です。

@interface TestClass : NSObject

- (void)overrideMethod NS_REQUIRES_SUPER;

@end

TestClassをサブクラス化し、overrideMethodをオーバーライドした時に、[super overrideMethod]の形で呼び出さないとコンパイラがワーニングを出します。

NS_REQUIRES_NIL_TERMINATION

可変長の引数の最後に必ずnilを指定するようにします。

@interface TestClass : NSObject

- (void)method:(NSString *)first, ... NS_REQUIRES_NIL_TERMINATION;

@end

methodの引数の最後にnilをしてしなければならないようになります。UIAlertViewのあれですね。

NS_ENUM

定数を列挙します。

typedef NS_ENUM(NSUInteger, TestClassOptions) {    
    TestClassTypeA,
    TestClassTypeB,
    TestClassTypeC,
    TestClassUnknown = NSUIntegerMax
};

NS_OPTIONS

ビット演算によるオプションを列挙します。

typedef NS_OPTIONS(NSUInteger, TestClassOptions) {
    TestClassOptionsNone    = 0,
    TestClassOptionsA          = 1 << 0,
    TestClassOptionsB          = 1 << 1,
    TestClassOptionsC          = 1 << 2,
    TestClassOptionsAll        = A | B | C
};

instancetype

返り値のインスタンスの型がそのクラスであることをチェックします。

@interface TestClass : NSObject

- (instancetype)init;

@end

initの返り値をTestClassとそのスーパークラス以外の型に入れようとするとコンパイラがワーニングを出します。

__unused

未使用であることを宣言します。

static CGFloat const __unused kConstant = 10.0;

unusedを付けないとコンパイラが未使用の定数とワーニングを出してきますが、unusedをつけることで黙らせることができます。統一性のために宣言だけしたい変数や将来使う定数、変数のために使うと便利かもしれません。

__strong

強参照でインスタンスが解放されないようにします。 変数宣言で使えます。

__weak

弱参照で参照先が解放されたとき自動でnilをセットします。 変数宣言で使えます。

__block

ブロックの中で書き換えできるようにします。 変数宣言で使えます。

__unsafe_unretained(非ARC)

参照先が解放されても自動でnilをセットせず、 retainもしないためクラッシュする可能性があることを示します。 変数宣言で使えます。

AVFoundationのキャプチャ機能について

AVFoundationによるキャプチャ機能をまとめたいと思います。


まず、普段あまり使うことがないAVFoundationの簡単な紹介です。このフレームワークAppleが用意する「音声・動画などの時間ベースのメディアの再生や作成、編集の細かい作業を行うための超強力な低レベルObjective-C API」です。

しかし、AVFoundationはおそらくAppleが用意するフレームワークの中でも1,2を争う巨大なフレームワークです。その機能を全て紹介することは難しいため、今回はその中でも音声・画像・動画のキャプチャ機能について実際のコードを絡めて紹介したいと思います。 AVFoundationを利用することでUIImagePickerControllerよりもさらにカスタマイズ可能なカメラやレコーダーを開発することができます。

なお、この記事ではかなり噛み砕いて説明しているため初心者の方でも問題無いですが、AVFoundationの機能を使って開発を行うためには、Objective-Cの基礎文法、ブロック、Foundation, CoreFoundation, retainCount、サブスレッドやキューの作成などの知識があるほうがよいです。


こちらの資料にも簡単にまとめてあるので、こちらを見てもどうぞ。


AVFoundationにおけるキャプチャの全体像

全体像としては以下のようになっています。

f:id:tomoyaonishi:20140629011434j:plain

真ん中の赤で示しているAVCaptureSessionというものがキャプチャに関連する入出力を管理するクラスです。 次に緑で示しているAVCaptureDeviceというものが前面カメラや背面カメラ、マイクといったデバイスそのものを表現するクラスです。 AVCaptureDeviceをAVCaptureSessionにそのまま接続することはできません。AVCaptureSessionに接続するためのクラスが紫のAVCaptureDeviceInputというクラスです。その名の通り入力を表現します。 青で示しているAVCaptureOutputというものが出力方法を表現するクラスです。画像、動画、フレームデータ、音声データ、メターデータなど様々な出力クラスが用意されています。 AVCaptureOutputとAVCaptureSessionの間の黒い矢印はAVCaptureConnectionというクラスです。入力データの向きを変更したりできます。

AVCaptureSession

このクラスはキャプチャに関する入力と出力の管理を行います。

self.session = [[AVCaptureSession alloc] init];

で生成することができます。

セッションに対してキャプチャクオリティの設定ができます。

@property(nonatomic, copy) NSString *sessionPreset;

AVCaptureSessionPresetHigh, AVCaptureSessionPresetMedium, AVCaptureSessionPresetLow, AVCaptureSessionPresetPhotoなどを設定することができます。 AVCaptureSessionPresetPhotoは静止画のキャプチャに対してのみ有効でそのデバイスの最大解像度でキャプチャできます。フレームデータや動画のキャプチャに対しては有効になりません。

セッションの設定を変更する際は

- (void)beginConfiguration;

を呼び、変更し終わった後に、

- (void)commitConfiguration;

を呼ぶようにしてください。

後述するInputやOutputを追加し準備ができた段階で

- (void)startRunning

を呼ぶとセッションが動き出し実際にデータが取得できるようになります。忘れないようにしてください。 またこのメソッド非常に遅いため非同期でも問題ないなら非同期で呼ぶことをおすすめします。

AVCaptureDevice

このクラスはカメラやマイクといったデバイスそのものを表現します。

背面カメラを取得するのであれば

self.camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

となります。

マイクを取得するのであれば

self.microphone = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];

となります。

背面カメラか前面カメラの判定は以下のプロパティを使用します。

@property(nonatomic, readonly) AVCaptureDevicePosition position;

任意のポジションのカメラを取得したい場合は

for (AVCaptureDevice *camera in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
        if (camera.position == AVCaptureDevicePositionFront) {
    // 前面カメラ
       }
       else if (camera.position == AVCaptureDevicePositionBack) {
    // 背面カメラ
       }
}

といったコードになります。

iPhoneのカメラには様々な設定をすることができます。 露出モード

@property(nonatomic) AVCaptureExposureMode exposureMode;

フォーカスモード

@property(nonatomic) AVCaptureFocusMode focusMode;

ホワイトバランスモード

@property(nonatomic) AVCaptureWhiteBalanceMode whiteBalanceMode;

フォーカス位置

@property(nonatomic) CGPoint focusPointOfInterest;

などなど他にもフラッシュやトーチなど多くの設定ができます。 また、各プロパティに値をセットすることができるか確認するメソッドがあるので、必ず確認してから値をセットするようにしてください。 例えばiPadでフラッシュをONにしようとするとクラッシュします。

AVCaptureDeviceに対して何かしらの設定を行う場合はデバイスをロックしなければなりません。

- (void)lockForConfiguration:(NSError **)error

でロックし、設定が終わり次第

- (void)unlockForConfiguration

でロックを解除します。

AVCaptureDeviceInput

このクラスは指定したデバイスをセッションに入力するときに使います。

self.cameraInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.camera error:NULL];

で生成することができます。

[self.session addInput:self.cameraInput]

とすることでセッションにカメラが入力機器として接続された状態になります。

この状態でセッションのstartRunningメソッドを呼ぶことでカメラからのデータがセッションに入ります。

AVCaptureOutput

このクラスには様々なサブクラスが用意されています。

  • AVCaptureStillImageOutput(静止画)
  • AVCaptureMovieFileOutput(動画ファイル)
  • AVCaptureAudioFileOutput (音声ファイル)
  • AVCaptureVideoDataOutput(動画フレームデータ)
  • AVCaptureAudioDataOutput(音声データ)

任意の出力方法を選びセッションに追加することで画像や動画、データがキャプチャできます。

AVCaptureStillImageOutput

カメラからの入力から静止画をキャプチャします。

self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init];

で生成できます。 iOS7以降は手ぶれ補正をオンにすることができます。

@property(nonatomic)  BOOL automaticallyEnablesStillImageStabilizationWhenAvailable

あとはセッションに追加するだけです。

[self.session addOutput:self.stillImageOutput];

画像をキャプチャする

- (void)captureStillImageAsynchronouslyFromConnection:(AVCaptureConnection *)connection completionHandler:(void (^)(CMSampleBufferRef imageDataSampleBuffer, NSError *error))handler;

を呼ぶと非同期で静止画がキャプチャされ、結果がimageDataSampleBufferという形で取得できます。 このままではUIKitで使いづらいため、NSDataに変換するメソッドが用意されています。

(NSData *)jpegStillImageNSDataRepresentation:(CMSampleBufferRef)jpegSampleBuffer

です。 NSDataにはExifなどのメタデータも含まれています。

AVCaptureMovieFileOutput

このクラスは簡単に動画をキャプチャすることができます。

self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];

で生成できます。 撮影時間は以下のプロパティで取得できます。

@property(nonatomic, readonly) CMTime recordedDuration;

その他様々なプロパティが用意されています。

あとはセッションに追加するだけです。

[self.session addOutput:self.movieFileOutput];

動画のキャプチャを開始を開始するには以下のメソッドを呼びます。

[self.movieFileOutput startRecordingToOutputFileURL:self.movieURL            recordingDelegate:self];

停止するには以下のメソッドを呼びます

[self.movieFileOutput stopRecording];

開始停止のメソッドを呼ぶと対応したAVCaptureFileOutputRecordingDelegateメソッドが呼ばれます。

キャプチャを停止した場合、以下のデリゲートメソッドが呼ばれます。

(void)captureOutput:(AVCaptureFileOutput *)captureOutput                 didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error

outputFileURLに動画ファイルが保存されています。

AVCaptureAudioFileOutput

AVCaptureMovieFileOutputと同じようなものなので省きます。

AVCaptureVideoDataOutput

このクラスは入力機器から入ってくる映像の各フレームデータをそのまま取得できます。

self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];

で生成します。

また、映像の設定として以下の辞書をセットします。

self.videoDataOutput.videoSettings = @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA) };

実際にフレームを取得し始めるには

self.videoDataQueue = dispatch_queue_create("jp.co.xxx.videoDataQueue", DISPATCH_QUEUE_SERIAL);
[self.videoDataOutput setSampleBufferDelegate:self queue:self.videoDataQueue];

と、サブスレッド用のシリアルキューを用意しそこで処理をさせます。フレームデータは1秒に何度も取得するためメインスレッドを止めないような設計になっています。

あとはセッションに追加するだけです。

[self.session addOutput:self.videoDataOutput];

各フレームデータはAVCaptureVideoDataOutputSampleBufferDelegateの

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection

でsampleBufferという形で取得できます。sampleBufferからUIImageに変換することもできます。 ただし、このメソッド内で重い処理をしないようにしましょう。1秒間に30回前後呼ばれます。

AVCaptureAudioDataOutput

AVCaptureVideoDataOutputと同じようなものなので省きます。

すべてのoutputについて生成後、セッションに追加するのを忘れないようにしてください。また、セッションを走らせなければ実際にデータは取得できません。そこも忘れないようにしてください。

AVCaptureVideoPreviewLayer

このクラスはカメラからの映像をユーザに見せるプレビュー画面を簡単に作成できます。

AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
previewLayer.frame = self.view.bounds;

で生成でき、あとは任意のビューに追加するだけです。

以上の知識のみで自由度の高いカメラアプリを開発することができます。細かなTipsはたくさんありますが、今回はここで終わりにしたいと思います。