記録。

めも。

kubernetesの個人的メモ(Discovery&LBリソース)

エントリーのリソースの違いがあやふやになってしまったので、整理した。

Discovery&LBリソース

デプロイされているPod(コンテナ)に外部からアクセスできるようにするリソース。

エンドポイントの提供などをしれくれる。

大きく、SerivceとIngress に分かれる。

両者の違いは、SerivceリソースはL4のロードバランシングを提供するのに対して、 IngressリソースはL7のロードバランシングまで提供してくれる。

つまり、Ingressはパスルーティングを実現できたりする。

Serviceリソース

細かくは7つに分かれる。

  • ClusterIP
  • ExternalIP
  • NodePort
  • LoadBalancer
  • Headless(None)
  • ExternalName
  • None-Selector

下から3つについては、今回は省略する。

Cluster IP

クラスタ内部ネットワークのみで疎通できる仮装IPが割り当てられる。 指定した Podに対してロードバランシングを行う。

例えば、クラスタで動くPodAから 3台動いているPodBにアクセスしたいとき、PodAからはCluster IPで割り当てられたIPにアクセスすると、 いい感じにロードバランシングしてくれて、PodBにアクセスすることができる。

構成ファイル例

kind: Service
apiVersion: v1
metadata:
  namespace: wakashiyo-development
  name: wakashiyo-dev-clusterip
spec:
  selector:
    app: nginx
  type: ClusterIP
  ports:
  - name: "http-port"
    port:  8080
    targetPort:  80

selectorにはdeployment等でデプロイしているPodのラベルを設定している。

portでCluster IPが受け付けるポート番号を指定し、

targetPort で転送先のコンテナのポート番号を指定する。

External IP

前述のCluster IPは内部疎通だけだったが、これはクラスタ外からも疎通できる。

指定したクラスタIPアドレスを指定することで、そのNodeへのアクセスを指定したコンテナにロードバランシングする。

ロードバランシングされるのは、指定したIPのNodeにあるコンテナだけでなく、デプロイされているコンテナ全てにロードバランシングされる。

kind: Service
apiVersion: v1
metadata:
  namespace: wakashiyo-development
  name: wakashiyo-dev-clusterip
spec:
  selector:
    app: nginx
  type: ClusterIP
  # kubectl get nodes -o custom-columns="NAME:{metadata.name},IP:{status.addresses[].address}"
  externalIPs:
    - 10.146.0.16
  ports:
  - name: "http-port"
    port:  8080
    targetPort:  80
# It did not work

先ほどのCluster IPと異なるのは、externalIPs という項目が増えている。 これは、GKEであれば、GCEインスタンスの外部IPではなく、OSなどから確認できる内部のIPを使用することができるらしい。

NodePort

こちらは、external IPが外部疎通性のあるノードを指定したのに対して、 こちらは、全てのNodeの外部疎通性を提供する。

(厳密には、0.0.0.0:portnumberでListenし、Node全てのIPにBindする形でロードバランシングされる)

なので、3台のノードが存在した場合、いずれかのIPアドレスにアクセスすると、NodePortからロードバランシングされ、 コンテナにアクセスができる。

構成ファイル例

kind: Service
apiVersion: v1
metadata:
  namespace: wakashiyo-development
  name: wakashiyo-dev-nodeport
spec:
  selector:
    app: nginx
  type: NodePort
  ports:
  - name: http-port
    port: 8080
    targetPort: 80
    nodePort: 30080
# It did not work when use not ingress

ports.nodePort という項目が増えている。

これは、NodePort -> ClusterIP -> Pod(container)

のようにクラスタ内部ではClustert IP のようにトラフィックが流れる。

port はCluster IPで受け付けるポート番号に対して、nodePortはクラスタのnodeで受け付けるポート番号になる。

LoadBalancer

今までは外部疎通性を提供するのに、node自身のIPを外部疎通できるように設定していたのに対して、

これは完全にクラスタ外に作成されるイメージになる。

その名の通り、クラスタ外のロードバランサが指定したコンテナに対してロードバランシングしてくれる。(L4レベルで)

とあるNodeが障害の場合、そのNodeへのトラフィックは流れないようにしてくれる。

GKEだと内部的にはロードバランサのサービスが使用されているらしい。

構成ファイル例

kind: Service
apiVersion: v1
metadata:
  namespace: wakashiyo-development
  name: wakashiyo-dev-lb
spec:
  selector:
    app: nginx
  type: LoadBalancer
  ports:
    - name: "http-port"
      port: 8080
      targetPort: 80
      nodePort: 30082
  # firewall rule (default: 0.0.0.0/0)
  loadBalancerSourceRanges:
    - 10.0.0.0/8

こちらのnodePortはロードバランサで受け付けるポート番号のことを指す。

NodePortと同様にクラスタ内部ではCluster IPが存在して、Cluster IP経由でコンテナまで疎通することができる。

loadBalancerSourceRangesを使用することで、指定したIPレンジからの送信しか受け付けないようにすることができる。

冷やっとしたのは、GKEの場合、Load Balancerを作成した時点で、GCPのロードバランサが作成され、静的IPが払い出される。

これは、クラスタを削除したりしても消えないので注意した方がいい。

Ingress

LoadBalancerと違って、L7のロードバランシングまで提供してくれる。

こちらも内部的には、GCPのロードバランサが使用されている。

構成ファイル例

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  namespace: wakashiyo-development
  name: w-d-ig
spec:
  rules:
    - http:
        paths:
          - path: /path1/*
            backend:
                serviceName: w-d-np1
                servicePort: 8080
          - path: /path2/*
            backend:
                serviceName: w-d-np2
                servicePort: 8080
          - path: /path3/*
            backend:
                serviceName: w-d-np3
                servicePort: 8080

backendには転送したいserviceを指定する。

ここでは、3つのNodePortをあらかじめ用意してからingressをデプロイしている。

というのも、Ingressはserviceをバックエンドとして転送する仕組みになっているため、

Serivceをbackendとして指定している。

ingressを通して公開されるコンテナはロードバランサからのヘルスチェックに応答できるようにしなければならない。

ヘルスチェックで何が行われるかというと、ルートパスへのGETリクエストが来るので、そのリクエストに対して200を返すようにしなければならない。

このことを最初知らずに、いつまで経ってもデプロイが失敗して1時間ほど溶かしてしまった。

Ingress を使用した HTTP(S) 負荷分散  |  Kubernetes Engine のドキュメント  |  Google Cloud

「入門 監視」を読んだ

こちら。

www.oreilly.co.jp

監視に興味を持ち始めていましたので、読みました。

読んでみて

監視の基本の原理原則的なものを幅広く抑えられていました。

私には、監視の「か」の字をわかっているかと言われたら危うい感じだったので、初歩から標準のことを知ることができたと思っています。

なので、ある特定の部分の監視のことで。。。みたいな具体的な悩みを持って、それを解決するためにこの本を読もうとしていると少し検討違いになってしまうと思いました。

また、ある程度仕事の中で監視をやってきて経験を積まれている方からしても、そこまで奥深いことが書いているわけではないので、読んだら「なんだ当たり前なことばかりじゃねーか」みたいになってしまうのかなと思いました。(基本がとても整理されている本だと思うので、復習みたいにはなるのかもしれない)

どんな構成だったのか

目次はいろんなwebページにも書いてありますが、

  • 監視の原則
  • 監視の戦略

と大きく二つに分かれていて

監視の原則では

など、について書かれていました。

こちらは、「今から目の前のシステムの監視の設定、実装をしていくぞ!」というわけではなく、

「監視とはなんなのか?」

「今までの監視の悪いところをあげ、こうした方がいい的な」

など、監視の実装を始める前の基本的な考え方をアンチパターンデザインパターンから説明されていました。

監視の戦略では、

  • ビジネスKPI
  • フロンエンド
  • アプリケーション
  • サーバー
  • ネットワーク
  • セキュリティ

など、各領域ごとに監視のパターンが書かれていました。

色々なwebサービスがある中で、フロントエンド監視も見逃してはいけないものと取り上げられていました。

印象に残っていること(というよりも覚えていること)

読んだのにもう忘れている。。。

結構当たり前なことですが、印象に残っていたり、まだ忘れていないことを羅列していきます。

whatから始めない

アンチパターンで述べられていました。

まず、ツール依存が結構多いらしいということ。

「このツールを使えば、OKでしょ。使おう」的なwhatが最初から来てしまい、結局導入した監視によって何も効率的な恩恵を受けられないというもの。

ツールを検証目的で色々試すのはOKだと思いますが、あの会社もあの会社もいいと言っていたから導入するのではなく、目的やワークフローに合っているかの見極めをちゃんとした上で導入なんだと教えられました。

自動化しましょう

このへんのアンチパターンは前職時代と結構紐付きながら読んでいました。。。

まさに「このコマンドを実行して、次にこのコマンドを。。。」みたいな手順書が溢れていましたので、

そういうのは自動化してプロダクトに集中しましょう。

導入は時間をかけて(かけすぎはよくない)、導入後は自動化してコストかけない

こんな感じにできると良さそうですね。

過去の値も必要

KPIに関することをやっていたので、少し引っかかりました。

今はある時点でのデータしか取って来ていないので、過去の値と時系列に見れるようにしたら、周期性が出て分析できたり、、、

この本でそこまで強く言ってないですが、時系列データ、過去のデータを蓄積してみないと原因もわからないと言ってました。

作るのではなく買う

これは企業の規模やフェーズに依存する話でした。

  • スタートアップみたいに規模の小さいところだったら、監視サービスを簡単に導入してプロダクトに集中すべき
  • 大企業になったら、独自の課題が出てしまい、監視サービスでは解決できない。そうしたら、自作する

企業が大きくなるにつれて監視への課題は変わってくるので、そうなったら、自作した方がいい(するしかない)ですが、そうでもなければ、わざわざコストを払って自作する必要ないので、その辺の塩梅はちゃんと判断しましょうということでした。

ついでに、監視の仕組みは1度作って終わりではなく、絶えず変化していくので、その都度、会話・判断・実装が必要です。

監視は単一の問題ではない

非常に大きな問題の塊であるため、1つのツールで解決できるものではないと述べられていました。

書籍では、それぞれ専門的なツールが増えるのは構わない的な形で書かれていました。(使用するものが多くなって複雑になってしまうことに対して恐れることが多いようです) 増えることがいいことではなく、1つの統合ツールで解決できない問題を解決するのであれば、上記のようにツールが増えても構わないということだそうです。

それぞれのツールが疎結合になっていれば、あるツールだけワークフローに合わなくなったりしたら、それだけ削除して新しいものに置き換えればいいと、組み合わせ可能な監視プラットフォームの構築を推奨していました

組み合わせ可能な監視を構成する要素

  • データ収集
  • データストレージ
  • 可視化
  • 分析とレポート
  • アラート

優先すべきは外側から

どこから監視をしていくのかという問いです。

OSのメトリクスなどももちろん有用ですが、まずはユーザーに近いところから着手すべきだと(ちゃんと動いているのかなど)

本書ではOSのメトリクスがちょいちょいと批判されていました。

それが不必要なものではということを言っているわけではなく、 例えば、CPU使用率が急上昇したとしても、ユーザーが使用するのに影響を与えているわけではないのであれば、アラートを上げて動かなくてもいいということでした。

この本の場合、どこを重視するかという点においてユーザーに関わる部分、つまりアプリケーションが動いているかという部分が重要になっているため、OSのメトリクスは上記の重要な部分とは関連性が薄いということで優先度が低くなるというものでした。

なので、この辺は企業や状況によって変わるような気がします。

ちなみにOSのメトリクスに関する話しは付録Cで @songmuさんのお酒に例えた話しがわかりやすく、「直接の原因であり、コントロール可能な数値を監視すべき」という言葉でとても腑に落ちました。

外側からという考えに従うと、個人的にはユーザーに関わる部分、ビジネスに関わる部分に対しては優先的に動けるようにしたいと思いました。

フロントエンドも重要

昔からの慣習なのか、この辺の監視の意識が薄いようです。

ただ、ページが表示されるまでの時間がかかってしまうとユーザーが離れてしまうという実績は様々な企業からたくさん出ているので、この領域の監視は見逃せないものだと思い知らされました。

個人的にはフロントエンドの開発をしたりはしているのですが、この辺に重きを置いて開発したりしたことはなかったので、今度休日に少し遊んでみようと思いました。

さいごに

個人的に、ネットワーク監視は、ほぼ経験したことがなかったので、少し頭に入りづらいことが多かったです。

今度こちらも休日に遊んでみようと思いました。

realmの様々な初期化を知らなかった。。。

realmはちょこちょこと使ったことはあるんですが、
あれ?テストとかどうすんだ??となったので、ドキュメントを読み直したりしました。
realmがどういう仕組みなのか知っていなかったというかちゃんと読むべきところを読んでいなかったと気づいたので反省です。。。

realmはどこに作られるのか?

これです。.realmファイルが作成されてそこにデータの読み書きをしにいくということまでは知っていました。
しかし、

  • どのディレクトリに作成されるのか?
  • default.realmしか作成できないんじゃないか?

ということを思っていました。

ドキュメントを読み直したら書いてありました。
公式ドキュメントのTesting and Debuggingにしっかり書いてありました。(個人的に知りたいことが全て書いてあったので嬉しい)

どのディレクトリに作成されるのか?

let realm = try! Realm()

上記のようなよく見る形で初期化すると、
Documentsディレクトリ以下にdefault.realmというファイルが作成されます。

default.realmしか作成できないんじゃないか?

違いました。パスを指定することで、default.realmとは別の.realmファイルを作成することができます。

let documentDirPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let path = documentDirPath[0] + "/test.realm"
let url = URL(fileURLWithPath: path)
let realm = try! Realm(fileURL: url)

上記の場合は、Documentsディレクトリ以下にtest.realmファイルを作成しています。
これで、default.realmとは別のファイルを作成することができ、データベースを使い分けることができます。(テストできる!)

その他(インメモリDBとしての使用)

公式ドキュメントのTesting and Debuggingには、
もう一つ、テストをする方法としてインメモリDBとして使用する方法が書いてあります。

var config = Realm.Configuration.init()
config.inMemoryIdentifier = "inMemory"
let realm = try! Realm(configuration: config)

上記のように初期化することで使用できます。これでもテストできます!
※当然ですがインメモリのため、データの永続化はされません。

初期化する方法がわかったので、うまく使い分けて行きたいと思います。

参考

realm.io

qiita.com

qiita.com

www.nowsprinting.com

標準的なUIを作る 〜Card〜

サブタイトル付きなので、シリーズ的にやっていきたいと思っています。

CardのUIは以前作りたいなと思ったのですが、調べてもなかなか見つからなく、調べることに時間を割いてしまった思い出があるので、ブログに書くことにしました。

作るもの

f:id:jksdaba:20190323165924g:plain
cardui

最近のフロントエンドのライブラリなんかには、標準のコンポーネントとして提供されています。

material-ui.com

vuetifyjs.com

ということで、こんな感じのものをswiftで実装していきたいと思います。

環境

Xcode 10.1
Swift 4.2

主な構成

  • UIViewController
  • UICollectionView
  • UICollectionViewCell

後述しますが、UIViewControllerは今回はxibで作りました。 理由としては、個人的な感想ベースですが、

  • 個人的に使い回ししやすい
  • インスタンス生成が楽
  • カスタムのイニシャライザを定義することができる

ということ感じで、storyboardより使いやすいと感じているため、最近よくxibでViewControllerを作っています。

Cardを作成する上で今回は、UICollectionView, UICollectionViewCellを使いました。 試していないですが、同じ機構であるUITableView, UITableViewCellでも作れると思います。

つくっていく

xibでViewControllerを作る

f:id:jksdaba:20190323171437p:plain

UICollectionViewを貼り付けるだけです。

レイアウトは、
top, leading, trailing, bottom 全てsafeareaに合わせています。

cellの作成

今回は、画像を表示させるだけのcardをつくっています

f:id:jksdaba:20190323171741p:plain

UIImageViewを貼り付けるだけです。
レイアウトは、
top, leading, trailingを大元のviewに合わせて、アスペクト比を4:3にしています。

cellの設定をする

cardっぽい立体的な見た目にしていきます。

masktoboundsとshdowOffset, shadowRadius, shadowOpacityを使うことで立体的で浮いているような見た目になります。

class CollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var imageView: UIImageView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
    
    func setUp(str: String) {
        imageView.image = UIImage(named: str)
        
        // UICollectionViewのcontentViewプロパティには罫線と角丸に関する設定を行う
        self.contentView.layer.masksToBounds = true
        self.contentView.layer.cornerRadius = 10.0
        
        // UICollectionViewのおおもとの部分にはドロップシャドウに関する設定を行う
        self.layer.masksToBounds = false
        self.layer.shadowOffset = CGSize(width: 2, height: 4)
        self.layer.shadowRadius = 10.0
        self.layer.shadowOpacity = 0.5
    }

}

以上。

ソースはgithubにあげてあります。

StandardUI/CardUI at master · wakashiyo/StandardUI · GitHub

docker-composeでnginx+go+mysqlの一式を作る

最近は、サーバーサイド、インフラよりなお仕事が多め? そんな日々を過ごしています。

今、個人的に開発しているアプリのインフラとかで「やってきていないことにチャレンジしたいなー」と思い、
Docker、kubernetesを使ってみてます。

今日は、サクッと開発環境的な感じで、タイトルの通りの構成を作ってみました。

構成

リバースプロキシ:nginx
apiサーバー:go
DB:mysql

使っているもの

  • nginx
  • mysql
  • gin(webフレームワーク)
  • xorm(ORM)
  • go-sql-driver/mysql

https://github.com/gin-gonic/gin

https://github.com/go-xorm/xorm

GitHub - go-sql-driver/mysql: Go MySQL Driver is a MySQL driver for Go's (golang) database/sql package

作っていく

apiサーバー

とりあえず、mysqlにつなげれられればいいや程度で作りました。
簡単に、userの情報をDBに突っ込むだけの処理を書いてます。

main.go

// ルーティング
func main() {
  router := gin.Default()

  v1 := router.Group("api/v1")
  {
    v1.POST("/user", users.Register)
  }

  router.Run(":9000")
}

users.go

//ユーザー情報の構造体
type Users struct {
    ID            int         `xorm:"id"`
    Name     string    `xorm:"name"`
    Email      string    `xorm:"email"`
}

//DBに情報を登録
func SignIn(c *gin.Context) {
 
        //dcoker-composeの名前解決の仕組みでmysqlにつなぐことができます。
    engine, _ := xorm.NewEngine("mysql", "test:test@tcp(db:3306)/test")

        name := c.Query("name")
        mail := c.Query("mail")

    user := Users{Name: name, Email: mail}

    _, err := engine.Insert(&user)

    if err == nil {
        c.JSON(http.StatusOK, gin.H{
            "status":  "success",
            "message": "success register user",
        })
                 return 
    } 
}

リバースプロキシ(nginx)

最終的にDockerfileにまとめています。
必要な情報だけdefualt.confにまとめています。

default.conf

server {
    listen 80;
    server_name localhost;

    location / {
        ## docker-compose.ymlのserviceでapiサーバーをwebとしている
        ## 9000のポートを開いている
        proxy_pass http://web:9000;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

Dockerfile

FROM nginx:latest

COPY ./default.conf /etc/nginx/conf.d/default.conf

DB(mysql

docker-compose.yml内で、imageも環境変数もしているため、特に何もしていません。

compose作成

こんなdocker-compose.ymlになりました

version: "3"

services:
  db:
    image: mysql:5.7.22
    restart: always
    ports:
      - 3306:3306
    volumes:
      - ./mysql:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: test
      MYSQL_PASSWORD: test
      MYSQL_DATABASE: test

  web:
    build: .
    ports:
      - 9000:9000
    depends_on:
      - db

  proxy:
    build: nginx/.
    ports:
      - 8000:80
    depends_on:
      - web

少しハマったところとしては、depends_onを最初書いておらず、 作成順序がおかしくなり、DBに接続できないことがありました。

docker-compose depends_onとlinksの違い - Qiita

Control startup and shutdown order in Compose | Docker Documentation

上記を参考にしました。 linksとdepends_onの違いが腹落ちしました。

depends_on => コンテナの立ち上がり順を制御(ただし、本当にそのサービスが準備完了ってところまでは、待ってくれない)
links => depends_on + 別のコンテナにエイリアス名でアクセスできるようにする(使用推奨されていない)

※linksは今後廃止されるそうなので、使用することが推奨されていないっぽいです。

docker-composeのversion2以降(たしか)は、networksを書かなくてもデフォルトでネットワークをはってくれるので、
特に何もせずに名前解決で繋げることができます。
それもあって以前まで、使用していたであろうlinksを使用しなくてもよくなったということですね。

といういことで以上です。

あっ、サンプルコードはここにあげてます。

GitHub - wakashiyo/compose-sample: docker compose sample

Alertを自作してみる

はじめに

普段は業務系のアプリの開発に携わっているので、 リッチなUIやUIのカスタマイズという部分は、求められることや実際の経験というのは少ないのかなと思っていたりします。

そんなことを思い始めたので、よく見るものから「どうなってるんだろ?」と調べて実装してみたので、 それを書いていきたいと思います。

UIAlertControllerを自作する

今日はこの話で書きます。

デフォルトのUIAlertControllerで実際は楽々済むのですが、 意外とちょっとした場面で見たり、今後自身が必要だと思うことが当然のようにあるのではないかと思って作ってみました。

完成はこんな感じになります。

f:id:jksdaba:20181216144508g:plain
customAlert

今日の内容はアニメーションで表示させるところまでで、実際のAlertの中のボタンやタイトル、メッセージの実装は省略します。

自作するにあたって

これは調べて見たら、そこまで複雑な実装はないことがわかりました。 以下の2つのプロトコル(1つはデリゲート)を実装することで、表示の遷移のアニメーションを表現することができました。

  • UIViewControllerAnimatedTransitioning
  • UIViewControllerTransitioningDelegate

この2つは画面遷移に関するアニメーションを実現するAPIで、これを使用することで 2つのクラスを作成して、自作のAlertを実現します。

  • AlertAnimation (アラートの表示時のアニメーションを実装する) UIViewControllerAnimatedTransitioningに準拠
  • AlertController (アラートのViewを管理する) UIViewControllerを継承、UIViewControllerTransitioningDelegateに準拠

AlertControllerを作る

画面の構成はこんな感じです。

f:id:jksdaba:20181216161216p:plain

右2つのViewから成り立っています。

  1. 右から2番目の薄いグレーのView (UIView)

これは標準のUIAlertControllerでも見るのもので、一時的にアラート以外の入力はできないようにしたり、アラートだということを示すための土台となるViewです。

  1. 一番右のView (UIView)

これが実際のアラートの画面です。今回は省略していますが、ここにボタンやタイトルなどを配置していきます。

実装コードはこのようになります。

import UIKit

class AlertController: UIViewController, UIViewControllerTransitioningDelegate {
    
    //1. の土台のView
    lazy var baseView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.gray
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    //2. のアラートのView
    lazy var AlertView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.white
        view.layer.cornerRadius = 10
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        //init時に宣言しないと、前の画面を透過しない
        self.providesPresentationContextTransitionStyle = true
        self.definesPresentationContext = true
        self.modalPresentationStyle = UIModalPresentationStyle.custom
        self.transitioningDelegate = self
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        layoutView()
    }
    
    func layoutView() {
        view.addSubview(baseView)
        view.addSubview(AlertView)
        
        baseView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        baseView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        baseView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        baseView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        
        AlertView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        AlertView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
        AlertView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
        AlertView.heightAnchor.constraint(greaterThanOrEqualTo: self.view.heightAnchor, multiplier: 0.2).isActive = true
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AlertAnimation(true)
    }
    
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AlertAnimation(false)
    }

}

また、実際にアラートっぽく表示させるためにデリゲートに準拠します。

class AlertController: UIViewController, UIViewControllerTransitioningDelegate {
    
    //...中略
    
    //表示時のアニメーション
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AlertAnimation(true)
    }
    
 //閉じる時のアニメーション
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AlertAnimation(false)
    }

}

returnしているAlertAnimationはアニメーションが実装されているクラスで、この後説明します。 このDelegateによってアニメーション(画面遷移)をAlertAnimationに委譲します。

Animationを実装する

画面遷移のアニメーションを実装するには、UIViewControllerAnimatedTransitioningに準拠し、 以下2つのメソッドを実装する必要があります。

    
 //アニメーションの時間
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
    
 //実際のアニメーション処理
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) 
    

実際のコードは以下のようになりました。

class AlertAnimation: NSObject, UIViewControllerAnimatedTransitioning {
    
    //true: dismiss
    //false: present
    let isPresent: Bool
    
    init(_ isPresent: Bool) {
        self.isPresent = isPresent
    }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.25
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        if isPresent {
            dismissAnimation(transitionContext)
        } else {
            presentAnimation(transitionContext)
        }
    }
    
    //表示時のアニメーション
    func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
        let alert = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! AlertController
        
        let container = transitionContext.containerView
        
        alert.baseView.alpha = 0
        alert.AlertView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
        
        //すでにfromのviewControllerはaddSubviewされているので、addSubviewやinsertSubviewの必要はない
        container.addSubview(alert.view)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       animations: {
                        alert.baseView.alpha = 0.7
                        alert.AlertView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) },
                       completion: { bool in
                        UIView.animate(withDuration: 0.1, animations: {
                            alert.AlertView.transform = CGAffineTransform.identity
                        })
                        transitionContext.completeTransition(true) })
        
    }
    
 //閉じる時のアニメーション
    func dismissAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
        
        let alert = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! AlertController
        
        UIView.animate(withDuration: 0.3, animations: {
            alert.baseView.alpha = 0
            alert.AlertView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
        }, completion: { finished in
            transitionContext.completeTransition(true)
        })
    }
    
}

表示の時も、閉じる時も同じanimateTransition(using transitionContext: UIViewControllerContextTransitioning)が呼び出されるため、 Bool値で判定させて、処理を分岐しています。

transitionContextのviewConroller(forkey: UITransitionContextViewControllerKey)で、遷移元と遷移先のViewControllerを取得できるので、 そのViewをCGAffainTransformやUIView.animateを使用してアニメーションを実装すれば、アニメーション処理は終わりです。

画面を透過させるために

ただし、これだけだとUIAlertControllerのように遷移元の画面が透けて見えるようになりません。 それを実現するためには先ほど作成したAlertControllerのinit時に以下のプロパティを設定する必要があります。

class AlertController: UIViewController, UIViewControllerTransitioningDelegate {
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        //init時に宣言しないと、前の画面を透過しない
        self.providesPresentationContextTransitionStyle = true
        self.definesPresentationContext = true
        self.modalPresentationStyle = UIModalPresentationStyle.custom
        self.transitioningDelegate = self
    }
}

プレゼンテーションのスタイルを設定するプロパティで、これを設定することで遷移元のviewControllerの画面が透過して見えます。

textField.rx.textではbindできなかった

最近は、以前挫折したRxSwiftをもう一度勉強中な感じです。

RxSwiftやMVVM周りでわかったことは今後別の記事にあげていこうと思っています。

今日は小ネタな感じで、少しつまづいたことを書いていこうと思います。

textField.rx.text.bindはエラーと言われてしまう

UITextFieldのObservableをViewModel側のObserverにbindしようとした時、 なぜかできませんでした。

f:id:jksdaba:20181209103823p:plain

※bindしようとしている対象はObserverです。(ViewModel側で以下のように宣言して公開しています)

// viewModel

import RxSwift
import RxCocoa
import Foundation

class viewModel {

  private let titleEditStream = PublishSubject<String>()
  
  var titleEdit: AnyObserver<String> {
      return titleEditStream.asObserver
  }

  //以下省略

}

というように、textFieldに入力されたら(イベントが流れ始めたら)、viewmodel側にイベントストリームを流したいと考えていたのですが、 上記の画像のようにエラーになってしまいました。

理由

どうしてエラーが発生するのかというのは、単純でした。 流れてくるイベント(この場合だと入力された文字列)はStringではなく、String?だったからです。

これはRxを使用していなくても標準のもので、UITextField.textの型はString?です。

宣言しているObserverの方の型(ジェネリクスの部分)はStringのため、String?→Stringに変える必要があります。

どうするか

RxにはorEmptyというpropertyがあります。 それを使用することで、String?→Stringに変換してくれます。

orEmptyがどのように実装されているのかはこんな感じでした。

f:id:jksdaba:20181209105349p:plain

※RxCocoa/ControlProperty.swift

コードの中のコメントにも書いているように、String? → Stringに変換してくれています。 実際は、mapで変換していましたね。

let values: Observable<String> = original._values.map{ $0 ?? "" }

画像のソースコードについて
※valuesはObservableです。(returnでObservableに変換されています)
※valuesSinkはObserverです。(AnyObserver, returnでAnyObserverに変換されています)

f:id:jksdaba:20181209110656p:plain

ということで、orEmptyを使用することでbind(to: ~)を使用してもエラーが表示されなくなりました。

f:id:jksdaba:20181209110512p:plain