リブートキャンプ by Swift 目次

 

 前回は力尽きてメンゴメンゴ。

 今回はマジにAuto Layoutだから。

 とか言いつつ、型変換の補足説明。

 いやホントちょっとだけっすから。たぶん。

 型変換には as? の他に as! ってのもあります。

 使い方は as? と同じで

 

 let text = script[0] as! String

 ↑何いってるか、わからない人は前回参照

 

てやるだけです。

 で、こっちを使うとどうなるかというと、受け取った定数・変数はオプショナル型ではなく、通常のString型となります。

 でも、それじゃnilが設定できないわけで、もし

 

 let text = script[1] as! String

 

なんてして、script[1]がString型に変換できない型だった場合はどうなるのかというと〜、アプリが死にます。

 デンジャラスなんで、ある程度覚悟して使いましょう。

 アンラップしないでいいのは便利なんだけどね。

 

サンプル:

http://tetera.jp/xcc/book-sample/unwrap.playground.zip

 

 で、ついでにいうと、型の方にも、nilが設定できるけど、アンラップしなくてもいい型があります。こいつはアンラップ済みのオプショナル型。

 

アンラップ済みのオプショナル型

 

 まず、復習を兼ねて変数宣言を整理すると、宣言時になんらかの値が必要なのがオプショナルじゃない型です。で、この場合nilは設定不可です。

 例えばInt型ならこんな感じ

 

 var x = 0   ← 値を設定してるので、型は書かなくても類推できる

 var y:Int = 1  ← 正式には、このように型も指定する

 x = nil    ← nilの設定は不可能

 

 これがオプショナルやアンラップ済みのオプショナル型なら、宣言時に値を書かなくてもよくなります。書かない場合はnilが入る。

 前回のオプショナル型なら

 

 var x:Int? = 0

 var y:Int?   ← 値を書かなくてもいい。その場合nilが設定される

 y = 1

 x = nil    ← nil設定も可能

 

 アンラップ済みのオプショナル型なら

 

 var x:Int! = 0

 var y:Int!

 y = 1

 x = nil    ← nil設定も可能

 

となる。

 こうしてみると、オプショナルとアンラップ済みのオプショナル型は、の違いだけじゃんてことになるんだけど、違うのは先に言ったように変数を使う場合。

 オプショナル型なら、使う前のアンラップ作業が必須で

 

 if y != nil {   ← 利用前にnilでないことを確認

  x = 2 * y! + 1 ← !をつけてアンラップして使う

 }

 

とかしないとダメなのが、アンラップ済みだと

 

 x = 2 * y + 1  ← Int型と同じ扱いができる

 

となります。とても楽。

 いちいちアンラップしなくてもいいのは楽なんだけど、もしyにnilが設定されている状態で同じことをやっちゃうとアプリが死にます。

 

 x = 2 * y + 1 ← yがnilの時に、これをやるとやばい

 

 なので、yがnilの可能性がある場合は、オプショナル型同様に

 

 if y != nil {   ← 利用前にnilでないことを確認

  x = 2 * y + 1 ← !は不要

 }

 

なんて書く必要もあるわけです、この点はオプショナル型と同じ。

 でも、書かなくてもコンパイラに怒られはしない。

 オプショナル型だと必須なnilでないかの確認作業を、アンラップ済みだと、プログラマ側で状況によってやるかやらないか選択できるんですな。

 そんな感じで、オプショナル型、アンラップ済みオプショナル型が使い分けられます。結構Appleのサンプルなんかで、出くわすと思うので覚えておきましょう。

 ちなみに正式には、オプショナル型、アンラップ済みオプショナル型は、それぞれ

 

 Optional<型> オプショナル型

 ImplicitlyUnwrappedOptional<型> アンラップ済みオプショナル型

 

と書きます。

 

 例)

 var x:Optional<Int>   ← var x:Int?の正式

 var y:ImplicitlyUnwrappedOptional<Int> ← var y:Int!の正式

 

ま、あんま使わないし見ないけど、変わりモンのソースコードを見た時に出てくるかも。

 

 それと、チェック付きアンラップ(オプショナルバインディングとか言うらしい)作業での

 

 var x:Int?

  ・・・

 if let x = x {

  ・・・ ← xを使った処理

 }

 

といった記述ですが、この場合、 { } (ブレースペア)の内側のxと外側のxは別物になります。

 

スコープ

 

 変数・定数のスコープ(範囲)と呼ばれるルールで、if letやfor、メソッド、関数の引数で宣言されたものは { } 内でしか認識されません。 { } 内で宣言されたものも同様。

 もし、その { } 外に同じ名前の変数・定数があったら、より近くで宣言されたものの方が優先となります。

 


 チェック付きアンラップ作業の場合、別の名前にするより、同じ名前にした方がプログラムが読みやすいんでよく使われるんですよ。やりたければ

 

 if let a = x, let b = y {

   ・・・ ← アンラップされたxとしてa、yとしてbを使う

 }

 

 てのもアリですが、いちいちaはxだったっけ、yだったっけと考えるより、同じ名前使った方がいいでしょ。

 

 このルールがあるおかげで、メソッドや関数を定義する時に、 引数や { } 内部で好き勝手な名前の変数・定数を定義できるわけです。

 

 

 

 ちなみに、クラスのプロパティとして定義されている名前と同名の変数・定数をメソッド内で使う場合は、優先されるのはメソッド内で宣言された変数・定数だけど、プロパティ側もselfをつけることでアクセスできたりもします。

 

class C {

    var str = "property"

    func x() {

        let str = "method"

        print(str)    ← methodと出力される

        print(self.str)  ← propertyと出力される

    }

}

 

サンプル:

http://tetera.jp/xcc/book-sample/scope.playground.zip

 

 ここら辺の仕組みを詳しく知りたい人は「変数 スコープ swift」あたりで検索かけて見ましょう。

 ということで寄り道終わり!

 

 こっからがAuto Layoutの話ね。

 まずは前々回のサンプルにAuto Layout機能を組み込んだサンプルをRunだ!

 

サンプル:

http://tetera.jp/xcc/book-sample/dynamictype+AL.zip

 

 設定側の変更で、ちゃんとUILabelの矩形が伸び縮みするざましょう。

 

 

 これがdynamic type対応アプリじゃい!

 

 じゃまず、今回のサンプルでUILabelの作成時に、全てのUILabelの矩形の左上位置を(0,0)、幅と高さを(200,60)に固定してる点から説明します。

 Auto Layoutを使うなら、ここでの矩形指定はあんまり意味を持ちません。

 なので、元のままでもいいんですが、Auto Layoutがきっちり効いてることを示すために全UILabelの矩形を同じにしました。

 なので前のサンプルで使った変数yは使いません。

    override func viewDidLoad() {
                ・・・
        var y = 50  ←いらない
        for textStyle in textStyles {
            let label = UILabel(frame:
                CGRect(x: 0, y: 0, width: 200, height: 60))
                ・・・
            label.numberOfLines = 0
                ・・・
        }

 ぶっちゃけ

 

 let label = UILabel()

 

でも動いたりします。サンプルでframe:指定しといてなんだけど、そっちの方が素直かもしれん。

 そのあとのnumberOfLinesプロパティは、前にも言ったけど表示のための行数を最大何行にするかなんですが、0を指定することで、UILabelの矩形幅で折り返した際に、全文字列を表示できるだけの行数って意味になります。

 もっとも、この指定はAuto Layoutを導入して、UILabelの矩形高さを調整するようにしないとあまり意味はない。高さが固定だと、結局はみ出した部分は表示されないからね。

 

 というわけで、下に追加してるのがAuto Layoutの導入処理。

 

Auto Layout

 

  Auto Layoutは、2つの画面矩形が、配置時に互いにどのように影響しあうかを指定しておく事で、実際の画面配置をiPhone側に任せるという仕組みっす。

 で、この2つの画面間の配置取り決めの事を制約と呼んで、NSLayoutConstraintクラスで表現するんですな。 

 

NSLayoutConstraint(制約)

 

 Auto Layoutの導入ってのは、このNSLayoutConstraintの作成に他なりません。実際、サンプルでもNSLayoutConstraintを複数作ってconstraintsという配列にまとめてます。

    override func viewDidLoad() {
                ・・・
        NotificationCenter.default.addObserver(self・・・

        //  Auto Layout機能追加
        var constraints = [NSLayoutConstraint]()
        var upperObject:Any = self.topLayoutGuide
        for label in self.labels {
       ・・・
            //  横の制約を作成しconstraintsに追加する
            let h_constraint = NSLayoutConstraint(
                item:label, attribute:.leading,
                relatedBy:.equal,
                toItem:self.view, attribute:.leading,
                multiplier:1, constant:50)
            constraints.append(h_constraint)

            //  縦の制約を作成しconstraintsに追加する
            let w_constraint = NSLayoutConstraint(
                item:label, attribute:.top,
       ・・・
            upperObject = label
        }
        ・・・

 じゃあ、この制約ってのは、具体的には何なのか?

 

   Y = aX + b

 

 この式が制約です。

 

親切本545ページ〜からを、今のSwift 3に対応させつつ抜き出し(親切本は秀和システムのサポートページからダウンロードするサンプルプロジェクト群の方だけSwift 3に対応させてる)


 まず、制約とは何かという事ですが、これは横なら横、縦なら縦の位置に関する2つの画面位置の関係を次の数式で表したものです。 

 

 

 先のソースコードでは、forループの中でおこなっているNSLayoutConstraintインスタンスの作成が、1つの制約の作成となっています。

 ・・・

 

NSLayoutConstraint

制約を表現する

 

 

 引数のitem:とattribute:が数式の左辺である位置Yを定義し、toItem:とattribute: が位置X、そしてmultiplier:とconstant:が、それぞれ数式のaとbを表し右辺となります。 relatedBy:には、この両辺を結ぶ「=、≦、≧」といった等記号や不等記号をNSLayoutRelation型 の値である、.equalや.lessThanOrEqualで指定します。 

 

NSLayoutRelation 

 等記号や不等記号を定義する。 

 

 item:やtoItem:には、UIViewといった位置情報を持つオブジェクトを指定します。そのオブジェ クトのどの部分の位置を利用するかを指定するのがattribute:です。 

 次のような値が、NSLayoutAttribute型として定義されています。

NSLayoutAttribute

 オブジェクトの部位を定義する。 

 

 

 このうちMarginと付くものは iOS 8から利用できる値で

 ・・・ 省略 ・・・

 

 .baselineや、.firstBaseline、.leading、.trailingといった値は、文字に対する位置調整で意味を持ちます。 

 

 

 左から右に文字を書く英語や日本語で.leadingと.left、.trailingと.rightに違いはありませんが、アラビア語など右から左に文字を書く言語では位置が逆転します。 .leadingMarginや.trailingMarginもそれぞれ.leftMargin、.rightMarginに対応します。 

 

 

 サンプルで作成している横位置用の制約は、左辺側のlebelの.leadingを、右辺の式(1 * self. viewの.leading + 50)をみたすように調整する制約となります。 

 

 

 縦位置の制約では、左辺側のlabelの.topを、右辺の式(1 * upperObjectの.bottom + 8)をみたすように調整する制約となります。 

 

 


 

 ここで出てきたupperObjectというのは、ループで設定してるUILabelの一つ上のUILabelを意味してます。

    override func viewDidLoad() {
       ・・・
        var upperObject:Any = self.topLayoutGuide
        for label in self.labels {
       ・・・

            //  縦の制約を作成しconstraintsに追加する
            let v_constraint = NSLayoutConstraint(
                item:label, attribute:.top,
                relatedBy:.equal,
                toItem:upperObject, attribute:.bottom,
                multiplier:1, constant:8)
            constraints.append(v_constraint)
       ・・・
            upperObject = label
        }
        ・・・

 

 

 ただし、最上位のUILabelの、その上にUILabelは存在しないので、最上位だけはupperObjectにUILabelではなく、UIViewControllerのtopLayoutGuideプロパティに設定されているオブジェクトを指定するようにしてる。

 このオブジェクトは、ステータスバーといった、自分のviewに覆いかぶさっている領域をはぶいた、最上位位置を示すようになってるオブジェクトっす。

 

 

 topLayoutGuideプロパティに設定されているのは、UILayoutSupportというプロトコルを採用したオブジェクトっぽいけど詳細は不明。いずれにせよupperObjectには、UILabelかこのオブジェクトを設定するのでupperObjectの型はAnyにしてます。

 

 var upperObject:Any = self.topLayoutGuide

 

 NSLayoutConstraint作成時のitem:やtoItem:引数の型はAnyなので、これで問題ない。

 幅の制約にはtoItem:にnilを指定したりもしてます。自分自身の幅を固定の幅にするだけなので相手がいないんですな。attribute:も.notAnAttributeとして、ただconstant:に渡した200を設定するようにrelatedBy:に.equalを指定してる。

    
            //  幅の制約を作成しconstraintsに追加する
            let w_constraint = NSLayoutConstraint(
                item:label, attribute:.width,
                relatedBy:.equal,
                toItem:nil, attribute:.notAnAttribute,
                multiplier:0, constant:200)
            constraints.append(w_constraint)

 こういった制約を配列に集め、NSLayoutConstraintクラスオブジェクトのactivateメソッドで有効にすることで、AutoLayoutが有効となります。

    override func viewDidLoad() {
                ・・・
        var constraints = [NSLayoutConstraint]()
                ・・・
        for label in self.labels {
                ・・・
            constraints.append(h_constraint)
                ・・・
            constraints.append(v_constraint)
                ・・・
            constraints.append(w_constraint)
                ・・・
        }
        //  制約群を有効にする
        NSLayoutConstraint.activate(constraints)
    }

 

 なんですが…

 重要な点として、UIViewの初期設定はautoresizingMaskプロパティの値をNSLayoutConstraintに自動翻訳するって設定になってるんですよ。

 覚えてるかなautoresizingMaskプロパティ

 Auto layoutが導入されるのは、iPhoneのシステムのバージョンが、6になってからなんだけど、それまでは画面配置の自動化はautoresizingMaskでやりくりしてたわけで、そういったautoresizingMask前提のアプリの画面配置が破綻しない措置なんだけど、そのせいでNSLayoutConstraintが無視されちゃうんですな。

 なので、この設定を解除する必要があります。

 そのためにやってるのがtranslatesAutoresizingMaskIntoConstraintsプロパティにfalseを設定する作業。

    override func viewDidLoad() {
                ・・・
        for label in self.labels {
            //  labelに対しAuto Layoutを有効にする。
            label.translatesAutoresizingMaskIntoConstraints = false
                ・・・
        }
                ・・・
    }

 これで、NSLayoutConstraint側が優先されることになる。

 ふう。

 

 めっちゃめちゃめんどくさい。

 

 NSLayoutConstraint使うのやめっか?

 と思う人が多いと思うけど、本来、NSLayoutConstraintをこうやってプログラミングで設定することは少ないです。

 普通は「重箱の隅をつつくようにネチネチと進めてみる」で言ったようにストーリーボードを使う。

 そこらへんも含め次回に続く。