しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

Docker+Railsでサーバー立ち上げメモ

Docker+Railsでサーバーを立ち上げた時のメモです。
元々Dockerを使わずに運営していたのですが、CentOS6がサポート切れになったことやbundle installでよく苦戦する事もあって環境を作り直す事にしました。
今回のOSはmacOSになります。

今回の構成ですがまずはCentOSのImageにRailsMySQLを乗せていく形で作っていきます。
DBやアプリケーションサーバーを分ける構成が推奨されているのですが、いきなり分割するのは難易度が高いので最初は1つのコンテナにすべて詰め込みます。

ローカル環境構築

まずはMacにDockerをインストールします。
インストールは下のサイトから行いました。

hub.docker.com

続けてDockerfileを作っていきます。
ここではrbenvを使ったRubyのインストールをしています。

下の方にあるENVはパスを通すためのコマンドです。
Dockerの実行中は.bash_profileを読んでくれないのでENVコマンドでパスを通しています。

FROM centos:8

# Ruby
RUN yum -y install git gcc make bzip2 openssl-devel readline-devel zlib-devel
RUN git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
RUN cd ~/.rbenv && src/configure && make -C src
RUN git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
RUN echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile && echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
ENV PATH /root/.rbenv/bin:/root/.rbenv/shims:$PATH
RUN rbenv install 2.5.1 && rbenv global 2.5.1

続けて下コマンドでImageを作成します。
tオプションはImageの名前やタグを指定するためのものです。

docker build -t my_image .

buildの後にdocker imagesコマンドを打つとImageが作られている事が分かります。

f:id:llcc:20210807172951p:plain

Imageを消したい場合はdocker rmi xxx(ImageのIDか名前)で削除する事ができます。
ImageIDは先頭の数文字だけでも問題ありません。
すべて一気に削除したい時は下コマンドが便利です。

docker images -aq | xargs docker rmi

次はImageからコンテナを作成して起動します。
起動は下のコマンドです。

docker run -itd --name my_container -p 3000:3000 --privileged my_image /sbin/init

http://localhost:3000でDockerの3000ポートに繋ぎたかったので下のように-pオプションでポートを指定しています。
他オプションですがdはバックグラウンド起動のため、iはホストの入力をコンテナに送るため、tはコンテナからの出力を受けるためのものです。
--privilegedはホスト側などへのアクセス権限を与えるオプションです。
マウント周りで権限関連のエラーが出たので追加したのですが、本番環境などでは避けた方が良いかもしれません。

コンテナへのアクセスする場合は下コマンドを使います。
loginオプションはbash_profileの読み込みの為に入れています、ただDockerfileのENVでRubyのパスを追加したので除いてもいいかもしれません。

docker exec -it my_container bash --login

試しにRubyバージョンを確認した所、無事に2.5.1が入っていました。

f:id:llcc:20210807173800p:plain

execを使うとコンテナ上でコマンドを実行します。
そのためbash --loginをlsに置き換えると下のようにファイル一覧を表示します。

f:id:llcc:20210809153556p:plain

立ち上げたコンテナですが停止したい時はdocker stop xxx(コンテナのIDか名前)、削除したい時はdocker rm xxx(コンテナのIDか名前)を使います。
起動中のコンテナを削除したい時は下のようにrmにfオプションを付けます。

docker rm -f xxx(コンテナのIDか名前)

次にMySQLをインストールします。
Dockerfileの下部に下の行を追加してImageを作り直して実行します。

# MySQL
RUN yum -y install mysql mysql-server

起動すると下のように実行できるようになっています。

f:id:llcc:20210807213453p:plain

最後にRailsを導入します。
Dockerfile最下部に下の処理を追加します。

# Rails
RUN yum -y install mysql-devel gcc-c++ nodejs crontabs wget fontconfig-devel
RUN wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && tar jxf phantomjs-2.1.1-linux-x86_64.tar.bz2 && cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/
RUN gem install bundler --version=1.16.3
RUN mkdir /my_app
WORKDIR /my_app
COPY . /my_app
RUN bundle install

rails s の-bオプションは127.0.0.1で立ち上げるとうまく動かないためです。
こちらは下サイトを参考にしました。

docker上のアプリにlocalhostでアクセスしたらERR_EMPTY_RESPONSEが出る - Qiita

この状態でコンテナに入り下コマンドを実行するとサーバーを起動できます。

systemctl start mysqld
rake db:create && rake db:migrate
rake db:seed
bin/rails s -b 0.0.0.0

MySQLのデータの永続化

次はデータベースのデータの永続化をします。
今はコンテナを削除するとデータが消えてしまうので、volumeを作る事で永続化しようと思います。
最初はホスト側にデータを保存しようと思ったのですが、MySQLの起動がうまくいかなかったのでvolumeを作る事にしました。

まずは下コマンドでvolumeを作成します。

docker volume create my_volume

docker volume lsコマンドを打つと今作ったvolumeを確認する事ができます。
削除はdocker volume rmコマンドを使います。

f:id:llcc:20210808195004p:plain

volumeはdocker runの-vオプションで指定します。
今回はMySQLのデータ部分をvolumeに保存したかったので-v my_volume:/var/lib/mysqlというオプションを付けました。

docker run -itd --name my_container -p 3000:3000 -v my_volume:/var/lib/mysql --privileged my_image /sbin/init

これでコンテナを作り直してもデータを保持するようになりました。

Docker Composeでコンテナを管理

次はDocker Composeを使ってコンテナを管理します。
Docker Composeを複数のコンテナを管理できるツールです。

まずは下コマンドでDocker Composeが入っていることを確認します。
入ってない場合は各環境に応じた方法でインストールします。

docker-compose version

次はDocker Composeの設定ファイルであるdocker-compose.ymlを作成して下のように記述します。

version: '3'
services:
  app:
    container_name: my_container
    privileged: true
    volumes:
      - my_volume:/var/lib/mysql
    ports:
      - 3000:3000
    command: /sbin/init
    build: .

volumes:
  my_volume:

起動コマンドは下のとおりです。
先程使っていたdocker runに比べると非常にシンプルになりました。

docker-compose up -d

コンテナの停止と削除は下コマンドになります。

docker-compose down

Imageの削除も行いたい場合は下のオプションを追加します。

docker-compose down --rmi all

データベースを別コンテナ化

Docker Composeを入れたので、コンテナもアプリケーションとデータベースで分けたいと思います。
下のようにdocker-compose.ymlにデータベースコンテナを追加します。
ホスト側からDBにアクセスしない場合はportsの部分は削除して構いません。

environmentのMYSQL_USERとMYSQL_PASSWORDではユーザーを作成しています。
MYSQL_DATABASEでデータベースの作成とそのデータベースへの権限付与を行います。

appの方にはdepends_onを追加します。
これによってdbを起動してからappを起動できるようになります。

version: '3'
services:
  app:
    container_name: my_container_app
    privileged: true
    volumes:
      - my_volume:/var/lib/mysql
    ports:
      - 3000:3000
    command: /sbin/init
    build: .
    depends_on:
      - "db"
  db:
    container_name: my_container_db
    image: mysql:8.0
    volumes:
      - my_volume:/var/lib/mysql
    environment:
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: my_db
      MYSQL_ROOT_PASSWORD: password
    hostname: db_container
    ports:
      - 3306:3306

volumes:
  my_volume:

起動するとデータベースのコンテナも作られている事が分かります。

f:id:llcc:20210809220013p:plain

ホスト側からは下コマンドでアクセスできるようになります。

mysql -h localhost -u test --protocol=tcp -p

次はアプリケーション用コンテナから今作ったデータベースを見るようにします。
まずはDockerfileから下のようにMySQL関連の処理を削除します。

FROM centos:8

# Ruby
RUN yum -y install git gcc make bzip2 openssl-devel readline-devel zlib-devel
RUN git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
RUN cd ~/.rbenv && src/configure && make -C src
RUN git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
RUN echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile && echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
ENV PATH /root/.rbenv/bin:/root/.rbenv/shims:$PATH
RUN rbenv install 2.5.1 && rbenv global 2.5.1

# Rails
RUN yum -y install mysql-devel gcc-c++ nodejs crontabs wget fontconfig-devel
RUN wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && tar jxf phantomjs-2.1.1-linux-x86_64.tar.bz2 && cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/
RUN gem install bundler --version=1.16.3
RUN mkdir /my_app
WORKDIR /my_app
COPY . /my_app
RUN bundle install

Railsのdatabase.ymlにDBコンテナのパスワードやホスト情報を追加します。

default: &default
  adapter: mysql2
  reconnect: false
  pool: 5
  username: user
  password: password
  host: db_container

development:
  <<: *default
  database: my_db

test:
  <<: *default
  database: my_db

production:
  <<: *default
  database: my_db

これでデータベースのコンテナを分割する事ができました。
あとは下コマンドでアプリサーバーを立ち上げれば完了です。

rake db:create && rake db:migrate
rake db:seed
bin/rails s -b 0.0.0.0

起動に関する処理の自動化

次は上に記載したrails sなども自動化していきます。
まずは自動化用のファイルdocker_app_command.shを作成して下のように記述します。

#!/bin/sh

rake db:create && rake db:migrate
rake db:seed
bin/rails s -b 0.0.0.0

ファイルには実行権限を与えておきます。

chmod 755 docker_app_command.sh

次はdocker-compose.ymlにcommandを追加して先程追加したファイルをセットします。
commandはコンテナ起動時に呼ばれる処理になります。

services:
  app:
    container_name: my_container_app
    ports:
      - 3000:3000
    build: .
    command:
      /sbin/init && ./docker_app_command.sh

ローカルでの更新を即時反映する

次はホスト側で編集したコードを即時反映されるようにします。
今は変更の度にImageの作り直しが必要なので時間がかかってしまいます。

コードの反映はdocker-syncというツールを使います。
ホスト側をマウントする形も考えたのですがパフォーマンスが良くないようなのでdocker-syncで進めていきたいと思います。

まずは下コマンドでdocker syncをインストールします。

gem install docker-sync

次にdocker-sync.ymlというファイルを作って下のように記述します。

version: "2"

syncs:
  my_sync_volume:
    src: "."
    sync_excludes:
      - "log"
      - "tmp"
      - ".git"

docker-compose.ymlのservices → app → volumesとvolumesにdocker-sync.ymlで定義したVolumeを追加します。

services:
  app:
    volumes:
      - "my_sync_volume:/my_app:nocopy"

volumes:
  my_volume:
  my_sync_volume:
    external: true

Docker Syncは下コマンドで動かします。

docker-sync start

docker-sync利用時ですが.docker-syncというフォルダが作られるので下のように.gitignoreに追加すると良いかと思います。

.docker-sync

同時にアプリケーションサーバーのDockerfileも修正します。
今までプロジェクト全体をCOPY . /my_appでコピーしていましたが下のように最小限にします。
そうする事で更新が最小限になってbundle install部分もレイヤーキャッシュを使ってくれるようになります。

COPY Gemfile /my_app/Gemfile
COPY Gemfile.lock /my_app/Gemfile.lock
RUN bundle install

コンテナ起動時にもbundle installをしてほしいのでdocker_app_command.shにbundle installを追加します。

#!/bin/sh

bundle install
rake db:create && rake db:migrate
rake db:seed
bin/rails s -b 0.0.0.0

これで無事にRailsを立ち上げられるようになりました。

その他利用したコマンド

キャッシュの全削除。

docker builder prune

キャッシュ利用状況の確認

docker system df

CentOS8へのインストール

Dockerをyumでインストールしたらうまくいかなかったので下コマンドでインストールと起動をしました。

dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf -y install --nobest docker-ce docker-ce-cli
systemctl enable docker
systemctl start docker

Docker Composeのインストールは下コマンドで実施しました。

wget https://github.com/docker/compose/releases/download/1.25.5/docker-compose-Linux-x86_64
mv docker-compose-Linux-x86_64 /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

参考URL

Docker入門(第一回)~Dockerとは何か、何が良いのか~ | さくらのナレッジ

rbenvの使い方と仕組みについて - Qiita

docker images を全削除する - Qiita

【Docker】コンテナの停止と削除を同時に行う - (O+P)ut

Ruby on Railsプロジェクトの開発環境をDocker化する - Qiita

CentOS8 Docker/Docker Composeインストール - Qiita

PHPickerViewControllerで画像を取得する

PHPickerViewControllerというiOS14から登場したクラスで画像を取得してみました。
このクラスはUIImagePickerControllerと違って複数画像を選択することなども可能です。

まずは画面に画像選択用のボタンを設置します。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton()
        button.setTitle("画像選択", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(self, action:#selector(self.tapButton), for: .touchUpInside)
        button.sizeToFit()
        button.center = view.center
        view.addSubview(button)
    }
    
    @objc func tapButton() {
    }
}

画面の真ん中にボタンを設置できました。

f:id:llcc:20210514115016p:plain

次はボタンを押すとPHPickerViewControllerが立ち上がるようにします。
画像選択可能数などの設定はPHPickerConfigurationを使って行っています。

import PhotosUI

class ViewController: UIViewController {
    override func viewDidLoad() {
        // 省略
    }
    
    @objc func tapButton() {
        var config = PHPickerConfiguration()
        config.filter = .images
        config.selectionLimit = 0
        let pickerView = PHPickerViewController(configuration: config)
        present(pickerView, animated: true, completion: nil)
    }
}

無事にPickerを表示できました。

f:id:llcc:20210514115227p:plain

最後にUIImageの取得処理を実装します。
画像の取得はPHPickerViewControllerDelegateで行います。

class ViewController: UIViewController {
    @objc func tapButton() {
        let pickerView = PHPickerViewController(configuration: config)
        pickerView.delegate = self // 追加
        present(pickerView, animated: true, completion: nil)
    }
}

extension ViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true, completion: nil)
        results.forEach {
            $0.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
                print(image)
            }
        }
    }
}

ログを見ると正しく画像が選択できている事が分かります。

f:id:llcc:20210514115407p:plain

注意点ですがloadObjectは非同期の為、読み込み完了順に呼ばれます。
そのため画像選択の順番は保持されません。
もし順番を保持したい場合、保持するような仕組みを実装する必要があります。

AndroidのEditTextやTextViewのフォントを明朝体にする

android:fontFamilyをserifにするだけです。

<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_first_fragment"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:fontFamily="serif" />

f:id:llcc:20210502171809p:plain

日本語も対応しています。

f:id:llcc:20210502171836p:plain

android:typefaceをserifにしても同じ結果になります。
typefaceはboldやitalicを指定できるプロパティーです。

<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="こんにちは"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:typeface="serif" />

【Kotlin】Applicationを継承したクラスを試してみる

AndroidのApplicationクラスを使ってみました。
Applicationを継承したクラスは全Activityからアクセスできるクラスになります。

クラスは下のように定義します。

class MainApplication: Application() {
}

下のようにAndroidManifest.xmlのapplicationタグに追加することでActivityから呼び出せるようになります。

<application android:name=".MainApplication">
</application>

Activityからは下のように呼び出す事ができます。
もしAndroidManifest.xmlへの記載がない場合、applicationにはandroid.app.Applicationクラスのインスタンスが入ります。

val app = (application as MainApplication)

ライフサイクルメソッドとしては、インスタンス作成時に呼ばれるonCreateなどがあります。

class MainApplication: Application() {
    override fun onCreate() {
        super.onCreate()
    }
}

【Kotlin】NavControllerを試してみる

Empty Activityを選んでプロジェクト作成をします。

f:id:llcc:20210323205236p:plain

起動すると下のようにラベルだけの画面が表示されます。

f:id:llcc:20210323205601p:plain

次はナビゲーションを作成します。 まず新規作成でAndroid Resource Fileを選択します。

f:id:llcc:20210323205925p:plain

Resource TypeをNavigationにしてOKを押します。

f:id:llcc:20210323205857p:plain

my_navigation.xmlが作られたので、次はここに登録するFragmentを2つ作ります。
今回はFirstFragmentとSecondFragmentという2つのフラグメントを作成しました。

f:id:llcc:20210323210344p:plain

Fragmentができたのでmy_navigation.xmlに登録します。
my_navigation.xmlを開いて画面上部の+ボタンからフラグメントを2つ追加します。

f:id:llcc:20210323210437p:plain

下のようにフラグメントが2つ登録されました。

f:id:llcc:20210323210614p:plain

次はナビゲーションを表示します。
activity_main.xmlを下のように変更します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/my_navigation"
        app:defaultNavHost="true" />

</androidx.constraintlayout.widget.ConstraintLayout>

アプリを起動するとFirstFragmentの内容が表示されました。

f:id:llcc:20210324111557p:plain

次はSecondFragmentへの遷移を実装します。
my_navigation.xmlの右側のActionsの+からActionを作成します。

f:id:llcc:20210324111815p:plain

次はFirstFragmentに画面遷移用のボタンを作成します。
fragment_first.xmlを下のように変更します。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FirstFragment">

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fragment_first_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />
</FrameLayout>

FirstFragment.ktのonViewCreatedにボタン押下時の画面遷移処理を追加します。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    view.findViewById<FloatingActionButton>(R.id.fragment_first_button)?.setOnClickListener {
        findNavController().navigate(R.id.action_firstFragment_to_secondFragment)
    }
}

起動するとボタンだけの画面が表示されます。
このボタンをタップすれば次の画面に移動します。

f:id:llcc:20210324112600p:plain

先程作ったActionを選択してAnimationsの所を編集すれば遷移時のアニメーションを指定できます。

f:id:llcc:20210324122213p:plain

ActionBarの戻るボタンですが、ActivityのsetupActionBarWithNavControllerを呼び出す事で表示できます。
押されたときの動作はonSupportNavigateUpで実装します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setupActionBarWithNavController(findNavController(R.id.nav_host_fragment))
    }

    override fun onSupportNavigateUp(): Boolean {
        return findNavController(R.id.nav_host_fragment).navigateUp()
    }
}

f:id:llcc:20210324123037p:plain

次は遷移のときに変数を渡してみます。
まずはそれぞれのbuild.gradleで下のようにプラグインを設定します。

classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4"
apply plugin: "androidx.navigation.safeargs.kotlin"

プラグインを入れたらmy_navigation.xmlを開いて遷移先を選択、画面右側Argumentsの+から変数を追加します。

f:id:llcc:20210324123624p:plain

この状態でビルドするとFirstFragmentDirectionsというクラスが生成されます。
画面遷移時のnavigateメソッドの引数にこのクラスを渡すようにします。

findNavController().navigate(FirstFragmentDirections.actionFirstFragmentToSecondFragment("testValue"))

遷移先では自動生成されたSecondFragmentArgsというクラスを使って変数を取り出します。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    arguments?.let {
        val args = SecondFragmentArgs.fromBundle(it)
        Log.d("test", "${args.testValue}")
    }
}

ログを見ると無事に渡せていることが分かります。

f:id:llcc:20210324125220p:plain

もし上記クラスが自動生成されない場合、Android studioの「Sync Project with Gradle Files」を選択すると良いかもしれません。

f:id:llcc:20210324124848p:plain

SF Symbols + UIImageでアイコンを表示する

SF SymbolsというAppleの提供しているアイコンを使える機能を試してみました。

developer.apple.com

SF Symbols + UIImageでアイコンを表示する

使い方は簡単で、下のようにUIImageの初期化でアイコン名を渡すだけです。
引数のラベルがUIImage(name: "")ではなくUIImage(systemName: "")なのでそこだけ注意して下さい。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imageView = UIImageView(image: UIImage(systemName: "doc.badge.gearshape.fill"))
        imageView.frame.size = CGSize(width: 100, height: 100)
        imageView.center = view.center
        view.addSubview(imageView)
    }
}

実行すると下のようにアイコンを表示できます。

f:id:llcc:20210107145814p:plain

アイコン名を確認したい時は下URLからアプリをダウンロードして下さい。

developer.apple.com

アプリを起動すると下のようにアイコン一覧とアイコン名が表示されます。

f:id:llcc:20210107150047p:plain

画像の色を変えたい場合、下のようにtintColorを変更します。

imageView.tintColor = .red

画像が赤になりました。

f:id:llcc:20210107150609p:plain

画像はベクターデータの為、大きくしても劣化しません。
下はサイズを500x500に変更したものです。

f:id:llcc:20210107150540p:plain

SF Symbols + UILabelで文字の中にアイコンを入れる

SF Symbolsは画像でしか取れないので、文字の中に入れたい場合は下のようにNSTextAttachmentを使います。

let label = UILabel()
let text = NSMutableAttributedString(string: "アイコン前")
text.append(NSAttributedString(attachment: NSTextAttachment(image: UIImage(systemName: "doc.badge.gearshape.fill")!)))
text.append(NSAttributedString(string: " 後"))
label.attributedText = text
label.sizeToFit()
label.center = view.center
view.addSubview(label)

実行すると文字の間にアイコンが入った事が分かります。

f:id:llcc:20210107151055p:plain

参考URL

Apple Developer Documentation

iOS13のカスタムフォントを試してみる

iOS13から使えるようになっていたカスタムフォントのAPIを試してみました。
この機能を使うと端末へのフォントインストールやフォントの確認ができます。

まずはフォント一覧の取得をしてみようと思います。 最初にテスト用のプロジェクトを作ります。

f:id:llcc:20201031141030p:plain

プロジェクトを作ったらプロジェクト設定の「Signing & Capabilities」でFontsを有効化します。

f:id:llcc:20201031141015p:plain

Storyboardで下のようにラベルとボタンだけ配置します。

f:id:llcc:20201031142117p:plain

最後にボタンを押したらUIFontPickerViewController(フォント一覧画面)を表示するような実装をします。

class ViewController: UIViewController {
    @IBAction func tapBtn() {
        let v = UIFontPickerViewController()
        present(v, animated: true, completion: nil)
    }
}

アプリを起動してボタンを押すとフォント一覧を見る事ができました。

f:id:llcc:20201031143020p:plain

次に一覧からフォントを選んで使います。
コードは下のとおりです。
ViewControllerをUIFontPickerViewControllerDelegateに準拠させました。

class ViewController: UIViewController {
    @IBOutlet weak var testLabel: UILabel!
    
    @IBAction func tapBtn() {
        let v = UIFontPickerViewController()
        v.delegate = self
        present(v, animated: true, completion: nil)
    }
}

extension ViewController: UIFontPickerViewControllerDelegate {
    func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
        guard let fontDescriptor = viewController.selectedFontDescriptor else {
            return
        }
        testLabel.font = UIFont(descriptor: fontDescriptor, size: 17)
    }
}

これでフォント選択したらラベルのフォントが変わるようになりました。

f:id:llcc:20201031143619p:plain

フォント一覧ですが、filteredLanguagesPredicateを使うと日本語対応フォントのみといった絞り込みができます。

let v = UIFontPickerViewController()
v.configuration.filteredLanguagesPredicate = UIFontPickerViewController.Configuration.filterPredicate(forFilteredLanguages: ["js"])

次はフォントのインストールをしてみます。
フォントはNikkyou Sansというフォントを使わせて頂きました。

Nikkyou Sans Font | daredemotypo | FontSpace

ダウンロードしたフォントをxcassetsに追加します。

f:id:llcc:20201031151439p:plain

フォントのインストール処理は下のとおりです。
CTFontManagerRegisterFontsWithAssetNamesメソッドでxcassetsのフォントを登録できます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        CTFontManagerRegisterFontsWithAssetNames(["NikkyouSans-B6aV"] as CFArray, nil, .user, true, { arr, result in
            print(arr)
            print(result)
            return true
        })
    }
}

アプリを起動すると下のようにインストール確認画面が表示されます。

f:id:llcc:20201031151602p:plain

インストールしたフォントは下のようにフォント一覧画面で選べるようになります。

f:id:llcc:20201031151713p:plain