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

 

 前にUIViewが画面を管理するって言ったっけ?

 すまん、ありゃ嘘だ。

 まあ、嘘じゃないにしろ正確じゃない。

 正確には、画面表示に関してはUIViewオブジェクトが所有するCALayerオブジェクトが受け持ってます。

 それが、前回出てきたUIViewのlayerプロパティに設定されているオブジェクト。

 UIViewはいわば総合商社(ゼネコン)みたいなもんすね。

 発注を受けて下請けに丸投げしたりします。

 

 

 例えば、以前説明した、背景色の指定であるUIViewのbackgroundColorプロパティへのUIColor設定は、UIView内部で、このCALayerのbackgroundColorプロパティを設定してたりします。

 

 

 こんなまだるっこしい構成にしてるのは、一つは機能の分業化です。

 CALayerは画面表示に専念し、ユーザーからの画面タップには無関心。そっちはUIViewが担当することになってます。

 これが分業。

 もう一つはMac、iPhone開発時の共通機能の共有のため。

 画面表示機能はMacでもiPhoneでも考え方・扱い方は同じ(RGBで色を指定とか)なので、この機能を同じCALayerというクラスで提供してもらえると、アプリを作るプログラマは、1つのクラスを知ることでMacでもiPhoneでも画面表示プログラムを書けるようになる。

 覚えることが減って助かるんですよ。

 ただし、画面タップ、マウスクリックという機能はiPhone、Macで異なる部分なんで、そこは、それぞれのプラットフォーム(iPhone、Mac)別々のクラスを用意する。それがUIViewというわけです。ちなみにMacアプリで画面管理するオブジェクトはNSViewと言います。

 

 

 内部で複数のオブジェクトを構成して分業してるけど、外部とのやりとりは基本的に1つの元締めオブジェクト(UIView・NSView)が対応する。

 コンポーネントというプログラミング手法です。

 ま、そんなわけで実際の画面表示はCALayerが請け負ってるので、直接CALayerにナシをつけた方が細かな指示ができたりもするんですよ。

 UIView側に依頼窓口が用意されてないやつとかもある。

 それがlayerプロパティのconerRadiusやmasksToBoundsといったプロパティの設定。

    override func viewDidLoad() {
		・・・
        let back = UIVisualEffectView(frame:frame.insetBy(dx: 20, dy: 20))
        back.layer.cornerRadius = 16
        back.layer.masksToBounds = true
		・・・

 conerRadiusの方は矩形の四隅に丸みをつける指定です。角の丸みを半径で指定する。

 

 

 他に枠線の幅を指定(borderWidth)して枠線を表示したり、枠線の色(borderColor)を指定したりもできる。

 

サンプル:

http://tetera.jp/xcc/book-sample/layer.zip

 

        let sampleView = UIView(frame: CGRect(x: 100, ・・・
        self.view.addSubview(sampleView)
        sampleView.layer.borderWidth = 1
        sampleView.layer.cornerRadius = 8
        sampleView.layer.borderColor = UIColor(
            hue: 0.6, saturation: 0.2, brightness: 1, alpha: 1).cgColor
//        sampleView.layer.shadowOpacity = 0.5
//        sampleView.backgroundColor = .white

 

 

 borderColorプロパティに設定してるのが、UIColorじゃなくCGColorだってことにも注意が必要っす。ここら辺は以前教えたクイックヘルプで調べましょう。

 とにかくCALayerの色関係にはUIColorは指定できない。CGColorを直接作るのは手間なんで、一旦UIColorを作って、そのプロパティからCGColorを取り出して指定します。

 こんな感じで、変数や定数に入れなくても、オブジェクト作成のUIColor(hue:…)の後ろに .(ドット)とプロパティ名とすることで、作ったUIColorオブジェクトにアクセスできます。

 

 

 作ったUIColorオブジェクトは、変数や定数に入れてないんで、これ以後は使えなくなるだけど、CGColorを取り出したら用済みなんで問題ない。

 それと今回はRGBじゃなくHSB指定でUIColorを作ってます。hueが色相(0.0〜1.0 0.0が赤で0.33..で緑、0.66...で青と変わり1.0で赤に戻る)。

 

 saturationが彩度(0.0〜1.0 1.0で一番鮮やか), brightnessが明度(0.0〜1.0 1.0で一番明るい)。

 

 枠線の他に、shadowOpacityプロパティの値を、0以外にすると影が表示されたりもします。

 

 

 サンプルでコメントにしている

 

   // sampleView.layer.shadowOpacity = 0.5

 

の // 部分を消して有効にして試すといいでしょう。その後で、その次の行のコメント部

 

   // sampleView.backgroundColor = .white

 

を有効にして試すとこんな感じになります。違いがわかるかな?

 

 

 CALayerにどんな指定ができるか、詳しくはこの前教えたクイックヘルプを使って調べてみてください。

 

 それと、この角丸矩形にしてるUIVisualEffectViewは、画面にすりガラス効果を持たせるUIView派生オブジェクトです。

 effectプロパティにUIBlurEffectを指定することですりガラス効果を指定できる。

        let back = UIVisualEffectView(
                frame:frame.insetBy(dx: 20, dy: 20))
                ・・・
        back.effect = UIBlurEffect(style: UIBlurEffectStyle.light)

 自分の後ろにある画像を、すりガラス加工するので、後ろがただの白地だと効果はありません。

 

 

 で、このすりガラス処理した画像は矩形で表示されるので、このままだと角丸矩形にならないんですよ。そのために

        let back = UIVisualEffectView(
                frame:frame.insetBy(dx: 20, dy: 20))
        back.layer.cornerRadius = 16
        back.layer.masksToBounds = true
        back.effect = UIBlurEffect(style: UIBlurEffectStyle.light)

として、画像が角丸矩形をはみ出さないようにしてます。

UIView側で指定するなら

 

        back.clipToBounds = true

 

とします。どっちで指定してもいいです。

 その後のセリフ部分(文字)のUILabelに対する

        //  セリフ部分(文字)
        let message = UILabel(frame:frame.insetBy(dx: 30, dy: 30))
        message.font = UIFont.preferredFont(
                forTextStyle:UIFontTextStyle.title1)

はダイナミックタイプ用文字の指定。

 設定アプリの「画面表示と明るさ」項目にある「文字サイズを変更」で、文字の大きさを変更すると、それに合わせて文字の大きさが変わるようになります。

 

 

 ただし、ちゃんと対応してないので、起動した時だけです。

 なのでダイナミックタイプフル対応とは言えない。

 ここら辺の話は次回!

 

AD

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

 

 プロパティについては終わったので、viewDidLoadメソッドについて。

    override func viewDidLoad() {
        super.viewDidLoad()

        //  背景
        let backgroundImageView = UIImageView(frame:self.view.bounds)

 まずはbackgroundImageViewを作成する時に渡してるself.viewのboundsプロパティが何なのかから。self.viewが何かは「こんにちはデベロッパの世界」を参照。

 

 boundsプロパティはUIView自身の画面座標上での自分の矩形を表してます。

 frameプロパティは親側の画面座標上での位置なので、例えば矩形を、左上を親側の(x:100, y:50)の位置、縦横を200ポイントとするなら、frameプロパティのほうは(x:100, y:50, width:200, height:200)となりますが、boundsプロパティのほうは、左上を示す(x, y)座標が、親側の画面座標上での位置じゃなく、自分自身の座標上での位置なので(0, 0)となってることです。

 幅と高さは変わりません。

 

 

 

 

 今回の場合はself.viewのframe自体も、その親側の座標系で(0,0)を指定してるので、boundsとの違いはないんですが、上の図のように親側の左上からずれた位置に貼られている場合は、frameではなくboundsを使うのが正解ということになります。

 ま、こうすることでself.viewの矩形いっぱいの大きさで、backgroundImageViewを作ることができるわけですYO。

 

 続いて指定してるのが、親側UIViewの矩形が変更された時に、自分の矩形をどうするかの指定。

 それがautoresizingMaskプロパティの設定。

        ・・・
        let backgroundImageView = UIImageView(frame:self.view.bounds)
        backgroundImageView.autoresizingMask = [
                UIViewAutoresizing.flexibleWidth, 
                UIViewAutoresizing.flexibleHeight]

 これで、幅と高さについて親側の変形に合わせて調整するって指定になる。

 

 

 幅と高さ以外に上、下、左、右の4つをそれぞれ変形に合わせて調整する指定ができる。指定しなければ固定。

 初期状態は全てが固定になるので、その場合、親画面の左上からの位置、幅、高さは変わらない。

 

 

 今回のように幅と高さについて親側の変形に合わせて調整するって指定にしたのは、iPhoneの横置きに自動対応させるため。

 シミュレータだとHardware→Rotate LeftまたはRotate Rightメニューで画面が90度づつ回転します。

 

 

 画面を回転させた時に、self.viewの矩形自体はUIViewContollerが調整するんだけど、その子供UIViewに関しては各自で対応する必要があるんですな。

 autoresizingMaskプロパティはUIViewAutoresizingという構造体型で、事前に用意されたプロパティを指定する決まりになっとります。このプロパティは前回のクラスオブジェクトのプロパティと同じで

 

 型名.プロパティ名

 

で指定するようになってます。

 構造体の場合は、定義中でstaticってキーワードがついてる奴がこの手のプロパティです。

public struct UIViewAutoresizing : OptionSet {
	・・・
    public static var flexibleWidth: UIViewAutoresizing { get }

 まあ、もうUIViewAutoresizingは、なごり技なんですけどね…

 今はUIView画面位置の自動調整にAutoLayoutって別の機構を使います。実際、autoresizingMaskへの指定も、内部で、このAutoLayout機構に自動変換されてる。

 AutoLayoutは増え続けるiPhone/iPadの画面サイズに対応するために、iPhoneアプリのプログラマにとっては必須教養とも言える機構なんだけど、説明が長くなるので、ストーリーボード(こっちも、まだ話してない。ViewControllerオブジェクトが自動的に用意される仕組みなんですが、これも後で)の時に話します。

 

 ちなみに、このUIViewAutoresizing構造体の定義見て、あれ、OptionSetから派生してるんじゃん。構造体って派生できないんじゃなかったっけと思った人。

 鋭いです。

 

     ↓ どう言うことなのか、責任者はどこか?

    struct UIViewAutoresizing : OptionSet 

 

 実は、これは派生じゃなくてプロトコルの採用と言いまして、OptionSetは構造体でもなければクラスでもないんですな。

 OptionSetはプロトコルとして定義されてます。

 

プロトコル定義

 

 プロトコル定義は、「または私は如何にして心配するのを止めて…」で教えたXcodeのヘルプ機能を使って宣言を見てもらうとわかるけど、見た目は構造体やクラスの定義みたいに見えます。

 違いは、そこで宣言されたメソッドには、処理部分が用意されないってこと。処理部分はそのプロトコルを採用した側が用意する決まりです。

 今回ならUIViewAutoresizing側が用意する。

 オーバーライドしなければ派生元のメソッドが使えるというクラスの派生とは、この点が異なるわけです。

 このプロトコル定義ってのは結構重要で、いろいろな場面で出くわすことになるんですが、何でこんなものが必要なのかは、今度ViewControllerがプロトコルを採用する必要が出てきた時に話します。

 先送りって楽だ。

 興味が湧いた人は「iOS デリゲート プロトコル」あたりで検索してみましょう。

 

 UIViewAutoresizingに話を戻す。

 こいつはOptionSetプロトコルの採用によって、複数の値を一つにできるようになっていて、前回の配列の宣言時みたいに [ ] (ブラケットペア)で囲んで、設定したい値を , (カンマ)で区切って羅列することができます。

 

        backgroundImageView.autoresizingMask = [

                UIViewAutoresizing.flexibleWidth, 

                UIViewAutoresizing.flexibleHeight

        ]

 

 それと、autoresizingMaskに設定できるのはUIViewAutoresizing型って、わかってるので、UIViewAutoresizingを省略することも可能。ただしselfやsuperとは意味が違うので . (ドット)は省略できない。

 

        backgroundImageView.autoresizingMask = [

                .flexibleWidth, 

                .flexibleHeight

        ]

 

 でもって書き方も割と自由です。

 

        backgroundImageView.autoresizingMask = [

                .flexibleWidth,   .flexibleHeight]

 

 なんてしてもいい。自分(他人)が見やすいように字下げやスペースを調整しましょう。

 今更だけど、

    func tap() {
        if scriptIndex < script.count {
            message.text = script[scriptIndex]
            scriptIndex += 1
            if scriptIndex == script.count {
                UIView.animate(withDuration: 2, animations: { 
                    self.imageView.alpha = 1
                })
            }
        }
    }

なんて風に、メソッドの処理部 { } や if文の処理部 { } で字下げしてるのも、どっからどこまでが影響範囲か見やすくするためだけです。

 インデントを揃える、とかいいます。

 これは文法としてのルールじゃないで、望むなら

func tap() {if scriptIndex < script.count {
message.text = script[scriptIndex]
scriptIndex += 1
if scriptIndex == script.count {
UIView.animate(withDuration: 2, animations: { 
self.imageView.alpha = 1
})
}
}
}

なんて書いてもいいけど、友達なくします。自分を自分で嫌いになるかもよ。

 

 autoresizingMaskの後に指定してるcontentModeは、画像を矩形に合わせるかどうかの指定。

        ・・・
        backgroundImageView.autoresizingMask = [
                UIViewAutoresizing.flexibleWidth, 
                UIViewAutoresizing.flexibleHeight]
        backgroundImageView.contentMode = UIViewContentMode.scaleAspectFill

 元々の画像の縦横比を保ちつつ、矩形を埋めるのが.scaleAspectFill

 

 

.scaleAspectFill

 

 比率を無視して、矩形に合わせてしまう.scaleToFill

 

.scaleToFill

 

 比率を保ちつつ、矩形からはみ出さない最大の矩形で貼り付ける.scaleAspectFitとか色々あります。

 

.scaleAspectFit

 

 UIViewAutoresizingで説明したように、こいつもUIViewContentMode部分を省略可能です。

 

 続いて、セリフ付き前景画面では矩形を

 

   self.view.bounds.insetBy(dx: 10, dy: 10)

 

にしてます。insetByはCGRectのメソッドで、dx:、dy:がそれぞれ内側に何ポイント小さくするかの指定です。そしてその大きさの矩形が戻される。

 これでself.view.boundsの内側に10ポイントほど小さくなった矩形でbaseViewの矩形を設定してる。

 

 くだって、人物部分を作ってるところでやってるalphaプロパティの設定は、UIColorで説明したalphaと同じで不透明度の設定。

        ・・・
        imageView.image = UIImage(named: "girl")
        imageView.alpha = 0

 0にすることで、透明にしてます。そして出番が来たら不透明度を1にして表示してる。

 見える見えないはhidden(隠す:true、隠さない:false)ってプロパティもあるんだけど、じわっとフェードインさせたかったのでalphaプロパティを使ってます。

 そこらへんはアニメーション部分のところで改めて説明。

 

 セリフ部分(すりガラス)部分は、UIVisualEffectViewを使ってすりガラス風にしてる。でもってlayerプロパティで角丸矩形にしてるんですが、layerプロパティの型であるCALayerはかなり重要なクラスで画面描画根幹とも言えるクラス。

 その話は次回!

AD

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

 

 まずは前々回のサンプルの

 

   message.backgroundColor = UIColor.orange

 
から。
 messageのbackgroundColorプロパティには背景色を設定します。
 なので、そこに指定しているのは「色」ってことになるわけですが、プログラムでは、こいつを色情報を持つオブジェクトとして扱います。
 それがUIColor。
 

UIColor

 

 色情報とは何か?

 赤色なのか、青色なのか、暗めか、明るめか、鮮やかか、渋めか、表現は色々ですが、ハードウェアに依存した表現となるとRGB値ということになります。

 RGB値ってなんだというと

 

親切本より


 まず、色は目から受け取った光に対する、網膜を構成する3種類の細胞の生理学的反応です。そして、光はさまざまな波長の電磁波の集まりであり、3つの細胞はそれぞれに反応する電磁波の波長が異なります。 

 
 

 600nm前後の波長に反応する細胞は、脳に赤色を、550nm前後の波長は緑色、460nm前後は青色のイメージを作り出させます。 

 
 

 これら3つの波長の電磁波の比率が、人間の感じる色を決定するわけです。

したがって赤、緑、青、この3つの色を混ぜ合わせる事で、人間が感じる色はすべて(厳密にいえば、 レーザー光などで正確に表現できないものもある)再現できるということになります。

 液晶画面も虫眼鏡などで拡大すると、ひとつひとつの点がRGBの発光体で構成されている事が わかります。 

 


 

 強弱を0.0 〜 1.0の範囲で表すとして、赤 :Red、緑 :Green、青 :Blue、すべて 1.0 なら真っ白になり、 すべて 0.0 なら真っ黒になります。赤だけ 1.0 で他が 0.0 なら真っ赤です。 

 

 このようにして色を指定する方法が RGB 値による色の表現で RGB 表色系といいます。 Wiki:色空間より

 

 

 というように、液晶画面を虫眼鏡で見ると赤 :Red、緑 :Green、青 :Blueの3色のタイルのセットがずらずらと並んでるわけですよ。「スクールカーストとかヒエラルキとか」で話した液晶の画素ってのがこれです。

 

 ダイレクトに色を表現するなら、このRGB値ってことになって、UIColorオブジェクトを作るときは次のように、RGB値をそれぞれ0.0〜1.0の範囲で指定することになってます。

 

  UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.5)

 

 ちなみに最後のalphaはアルファ値といって、色の不透明度の表現となります。

 色付きのガラスの後ろの透け具合を想像してもらったらいいでしょう。0.0で無色透明で後ろが透け放題、1.0で完全に不透明になって後ろの色は透けなくなる。

 この指定だと半透明の赤色ってことになりますね。

 その他に、最初から用意されている馴染みの色もいくつかあって、red、blue、gray、whiteといった名前で、UIColorクラスオブジェクトのプロパティとして用意されていたりもします。

 

クラスオブジェクト

 

 UIColorクラスオブジェクトってのは、クラスごとに1つだけ、アプリケーション起動時に自動的に用意されるオブジェクトです。

 クラス定義では、プログラム中で個別に作成するオブジェクトと、このクラスオブジェクトの2種類を定義できるようになっていて、プロパティやメソッドの定義の前にclassキーワードをつけるとクラスオブジェクト側の定義ってことになります。

 

↓UIColorクラスの定義

class UIColor  … {

  ↓クラスオブジェクト側のプロパティ定義

    class var orange: …

 

  ↓個別に作るオブジェクト側のプロパティ定義

    var cgColor: …

}

 

 個別に作るオブジェクトごとじゃなく、クラスごとに用意したい特性は、こうやってクラスオブジェクト側に定義するのがSwift流です。というかObjective-C、というか、その源流であるSmalltalkからそうなんですが…

 ま、それは置いといて、クラス名を書くことでUIColorクラスオブジェクトを指定したことになるので、いつも通り、そこから . (ドット)で繋げればプロパティを指定できます。

 つまり

 

   UIColor.orange

 

てのは、UIColorクラスオブジェクトのorangeプロパティの指定であり、このプロパティには橙色の色情報を持つUIColorオブジェクトが設定されてます。

 これが

 

   message.backgroundColor = UIColor.orange

 

で、messageのbackgroundColorプロパティに設定されることになるんで、messageの背景は橙色になるんですな。

 

 

 

 

 で、いよいよ前回のサンプルの話。

 のっけにViewControllerに2つのプロパティ、imageViewとmessageを用意してるのは、viewDidLoadで作ったUIImageViewとUILabelオブジェクトを別のメソッドでも使うためです。

class ViewController: UIViewController {
    
    var imageView:UIImageView!      //  人物用
    var message:UILabel!            //  セリフ・説明用

 プロパティ名の後ろに : (コロン)とクラス名を書いているのは、このプロパティが、どの型用に使われるかを教えるため。

 これまでのように

 

   let imageView = UIImageView(frame:…

 

 とか

 

   let message = UILabel(frame:…

 

ていうように、宣言時に = で、記憶するべき内容を指定するのなら、わざわざ型を指定する必要はないんですが、今回の場合、記憶するべきオブジェクト(の識別子ね。参照と言います。何の話かわからん人は「こんにちはデベロッパの世界」を読みましょう)が設定されるのはViewControllerが作られた後のviewDidLoadメソッド呼び出し時なんで、それより前に用意されるプロパティには、型が決められないんですよ。

 なので、コロンを挟んでの型の明示が必要になります。でもって、この変数宣言で値を設定しないのはミスじゃなくて故意だよってのをXcodeに知らせるために、型名の後ろに!をつけるってのがルールです。

 

 

 で、その後に続く

    let script = [
        "雨の日はだるい…\n今日は学校休んじゃおっかな〜",
        "誰だ"
    ]

てのは文字列配列の用意。

 こいつは [ ] (ブラケットペア)で囲んだ中に、複数の文字列を , (カンマ)で区切って並べることで作ります。

 

 

 こうすると、それぞれの文字列は配列の要素として格納され、配列の0番目の要素、1番目の要素というように、0から始まる続き番号(インディックス)で指定できるようになるんですよ。

 プログラム中でのインディックスの指定方法は、配列名の後ろに [ ] で囲んだ数値という書き方で次のように書きます。

 

 

 この [ ] で囲んだ数値は添え字と呼ばれ、これには整数型の変数・定数や計算式の結果も指定できるんですよ。

 

  let str = ["a", "b", "c"]  

  ↑ こんな感じで行を変えずに文字列を並べてもいい

    1行に1文字列としたりするのは、読みやすさを考えてのこと

  var a = 0

  print(str[a])   ← 0番目の文字列 a が出力される

  a += 1

  print(str[a])   ← 1番目の文字列 b が出力される

  a += 1

  print(str[a])   ← 2番目の文字列 c が出力される

  print(str[a - 2])  ← 0番目の文字列 a が出力される

 

 これが、今回のサンプルの肝なわけで、場面の進行に合わせて表示する文字列を変えるのに使ってます。

 それが、新しく追加しているtapメソッドでやってること。

    func tap() {
        if scriptIndex < script.count {
            message.text = script[scriptIndex]
            scriptIndex += 1
            if scriptIndex == script.count {
                UIView.animate(withDuration: 2, animations: { 
                    self.imageView.alpha = 1
                })
            }
        }
    }

 このメソッドはviewDidLoadメソッドで、画面をタップされるたびに呼び出されるように設定してて、配列scriptの要素数よりscriptIndexの値が小さい時だけ処理を実行するようにしています。それが最初のif文。

 

        if scriptIndex < script.count { … }

 

 配列scriptの要素数はcountプロパティで調べることができるので、scriptIndexがこれより小さいかどうかで比較して真偽を決定してる。

 scriptIndexとscript.countの間の記号 < は数学の記号と同じ意味で、右側より小さいという意味になります。もしその通りなら真となってif文の { } 内の処理が実行されるわけですな。

 ちなみに左側が右側以下の ≦ なら <= と書きます。右側より大きいは > 、右側以上は >= です。

 

 実行されたらscriptIndexで指定される要素が新しい文字列としてmessage.textに設定され、scriptIndexの値は1つ増やされます。

 

            message.text = script[scriptIndex]

            scriptIndex += 1

 

 でもって、scriptIndexが要素数と一致したなら、scriptIndexが示すインディックスは、配列の最後の要素を超えているってことになり(インディックスは0から始まるので、配列の最後の要素のインディックスは要素数-1)最後の場面に到着したことがわかる。

 そこでimageViewをジワリと現れるアニメーション(フェードイン)で表示させるようにしてるのが、最後のif文

 

            if scriptIndex == script.count {

                UIView.animate(withDuration: 2, animations: { 

                    self.imageView.alpha = 1

                })

            }

 

てやつです。

 画面をタップされるとtapメソッドが呼ばれるようにするのはどうするか?imageViewをフェードインさせるアニメーションを実行させてるUIView.animate(withDuration:…てのは何なのか?

 

 待て次回!

 

AD