記録。

めも。

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

最近

反省

最終更新日が7/16だったので、だいぶ更新していない状態が続いてしまった。。。 7月後半から秋にかけてPJが炎上したりとだいぶ忙しかったこともあるが、約5ヶ月も空いてしまった。。。

近況報告

春から夏

どんな技術に関わってきたかという観点で、今年度の春から振り返ってみる。 春から夏終わりまではモバイルアプリ開発のPJに入っていた。

cordovaを使用したクロスプラットフォームでの開発だったが、 iOSのネイティブで開発する必要がある機能もたくさんあったため(主にカメラ)ネイティブ部分を担当して開発していた。

夏にかけてJS側(共通部分)で多くのバグが発生してしまったため、その火消しのために集中していた。 クロスプラットフォームのJS側はVue.jsで開発されていため、Vue.jsで開発(というよりはデバッグして修正、テストだったか、)

個人的にはwebフロントの開発はどのフレームワークでも結局CSSが苦手だということに行き着いてしまう。 (今後、頑張りましょう。そして早く慣れましょう)

あとは、jpgのメタデータを操作する機会があったため、その辺の仕組みなどを今度記事に書く。

夏から今

前期終わりのタイミングで、モバイルアプリ開発は無事終了し、 そのあとは、webフロントエンドの開発(Vue.js)とアプリケーションの基盤となるインフラの構築(AWS, Linux)をやっていた。

で、今に至る。

今とこれから

まずは、ブログの更新頻度をあげよう。

あとはGithubに草をもっと生やそう。

興味を持ったことへはなるべく実践しながら使えるようにしよう。

運用・保守に興味を持ったので、個人ではその辺にフォーカスして開発したりインプットしてみようと思う。

興味を持った理由や背景は今度書く。

運用・保守といっても、泥臭くサーバー内で作業をすることがしたいわけではない。 運用・保守のコストを抑えられるような技術、今までやってきたアプリ開発などの品質などを高めるためのアプローチ(設計やテストなど)に 興味・関心というよりは必要性をより感じるようになった。

ということで、最近は、Swift書きつつ、DockerやKubenetesのことについてもインプットをしている。

書くネタはたくさん溜まったと思うので、更新頻度を上げていく。

virtual boxに作成したcentOSサーバーにssh接続しようとしたら、「WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!」

centOSのサーバーを作成してssh接続しようとしたら見たことないエラーが出てきて 接続できなかったので、その時のことを対処法を含めてメモ。

どんなエラーが出たか?

以下の画像を参照

f:id:jksdaba:20180716192205p:plain

それで調査したら、、、 サーバー上のRSA鍵情報とクライアントのRSA鍵情報が異なっていたためこのようなエラーが発生してしまった。

なんでこのようなエラーが起こってしまったのか?

サーバーは作成したばかりだったので、なんで鍵情報が異なると言われたのかがよくわからなかった。

しかし、この後の対処方法でも使用するUsers/.../.ssh/known_hostsを見たらわかった。

今回作成したサーバーのIPと以前に作成したサーバー(すでに削除してしまった)のIPが同じだった。 しかし、known_hostsには同じIPでも以前作成したサーバーに接続した時の鍵情報を持っていたため 異なると言われ、エラーが出てしまった。

対処法は?

Users/.../.ssh/known_hostsに記載されている 不必要な鍵情報を消去(IPアドレスハッシュ値が記載されていると思うので、それを削除した)

参考

www.server-memo.net