しめ鯖日記

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

UIDatePickerのdatePickerModeをcountDownTimerにした時の挙動確認

今回はUIDatePickerのdatePickerModeをcountDownTimerに変えた時の挙動を見てみました。
まずはdatePickerModeに何もセットしない場合の挙動を見ます。

プロジェクトをViewControllerを作って下のように変更します。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let picker = UIDatePicker()
        picker.center = view.center
        view.addSubview(picker)
    }
}

下のようにデフォルトのDatePickerが表示されます。

f:id:llcc:20211219192539p:plain

変更時のイベントの受け取り処理は下のとおりです。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let picker = UIDatePicker()
        picker.center = view.center
        view.addSubview(picker)
        picker.addTarget(self, action: #selector(self.datePickerValueChanged(sender:)), for: .valueChanged)
    }

    @objc func datePickerValueChanged(sender: UIDatePicker) {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
        print(dateFormatter.string(from: sender.date))
    }
}

変更してから日付を変更するとコンソールに変更後の日付が出てきます。

f:id:llcc:20211219192712p:plain

今回はこのUIDatePickerのdatePickerModeをcountDownTimerにした時の動作を確認します。

picker.datePickerMode = .countDownTimer

変更すると下のような表示になります。

f:id:llcc:20211219193700p:plain

試しに数字を10時間6分に変えたら下のような表示になりました。
おそらくDatePickerのdateは当日の選択時間になるのかと思います。

f:id:llcc:20211219193746p:plain

次はPickerに現在の日時(19時40分)をセットしてみました。

picker.date = Date()

セットしたら下のように現在の時間と同じ時間になりました。

f:id:llcc:20211219194232p:plain

Storyboard+UIScrollView+AutoLayoutでhas ambiguous scrollable contentエラーが出ないようにする

Storyboard上でUIScrollView+AutoLayoutを使うと下のようなエラーが出る事があります。
今回はその対処法を調べました。
Xcodeのバージョンは13です。

f:id:llcc:20211212193115p:plain

参考にしたのは下URLです。

qiita.com

まずはプロジェクトを作ってMain.storyboardにUIScrollViewを配置します。
今回はUIScrollViewを縦にだけスクロールするようにしようと思います。

f:id:llcc:20211212193023p:plain

配置したらUIScrollViewに下のような制約を追加します。

f:id:llcc:20211212193424p:plain

制約を追加すると下のようにエラーになり線が赤くなります。

f:id:llcc:20211212193231p:plain

エラー対策の為、まずはUIViewをUIScrollViewの直下に配置します。

f:id:llcc:20211212193342p:plain

次に左側のメニューでViewとContent Layout Guideを選びます。

f:id:llcc:20211212222511p:plain

この状態で下のような制約を追加します。

f:id:llcc:20211212222609p:plain

続けてViewにHeightの制約を追加します。

f:id:llcc:20211212222653p:plain

これでyやheightに関するエラーは消えました。

f:id:llcc:20211212222723p:plain

次はxとwidthに関するエラーの対応をします。
今度はViewとFrame Layout Guideを選びます。

f:id:llcc:20211212222806p:plain

この状態でEqual Widthsの制約を追加します。

f:id:llcc:20211212222840p:plain

これでエラーが消えました。
実行するとスクロールのContentHeightが1000になっている事がわかります。

f:id:llcc:20211212222906p:plain

先程出てきたContent Layout GuideとFrame Layout Guideですが公式サイトに説明がありました。

https://developer.apple.com/documentation/uikit/uiscrollview/2865870-contentlayoutguide

https://developer.apple.com/documentation/uikit/uiscrollview/2865772-framelayoutguide

公式サイトの説明は下の通りです。
要するにContent Layout GuideはUIScrollViewのcontentSizeに関するガイドでFrame Layout GuideはUIScrollViewのframeに関するガイドです。
そのためFrame Layout GuideとViewにEqualWidthsを付ければcontentSize.widthがUIScrollViewのframe.widthと等しくなったのだと思われます。

■ Content Layout Guide
Use this layout guide when you want to create Auto Layout constraints related to the content area of a scroll view.
■ Frame Layout Guide
Use this layout guide when you want to create Auto Layout constraints that explicitly involve the frame rectangle of the scroll view itself, as opposed to its content rectangle.

続けてContentHeightを動的に変える方法についても見ていきます。
まずはViewControllerにNSLayoutConstraintのIBOutletを作ります。

class ViewController: UIViewController {
    @IBOutlet var constraint: NSLayoutConstraint!
}

Storyboardから今作ったIBOutletとHeightに関するNSLayoutConstraintを紐付けます。

f:id:llcc:20211212223118p:plain

あとはNSLayoutConstraintの値を変えればContentHeightの値が変わります。

class ViewController: UIViewController {
    @IBOutlet var constraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        constraint.constant = 2000
    }
}

Android StudioのCPU Profilerで重い箇所を特定する

下URLを参考にCPU Profilerを試してみました。

developer.android.com

まずは新規プロジェクトを作成します。

f:id:llcc:20211205183039p:plain

Profiler画面は[View] > [Tool Windows] > [Profiler]と選択することで表示できます。

f:id:llcc:20211205184206p:plain

画面は最初は下のようになっています。

f:id:llcc:20211205184312p:plain

アプリを立ち上げると自動で測定開始してくれます。

f:id:llcc:20211205184349p:plain

測定しない場合、左上の+ボタンからセッションを選択します。

f:id:llcc:20211205184553p:plain

CPUを押すとCPU利用状況についてより詳しく見れるようになります。

f:id:llcc:20211205184701p:plain

より詳しく見たい場合、真ん中上部のRecordボタンで測定します。
測定停止したい所でStopボタンを押します。

f:id:llcc:20211205185556p:plain

Stopボタンを押すと下のように詳しく見れる画面に移動します。

f:id:llcc:20211205185933p:plain

テスト用に下のような重い処理を追加します。

view.findViewById<Button>(R.id.button_first).setOnClickListener {
    (0..100000).forEach { i ->
        (0..10000).forEach { j ->
            val r = i.toFloat() * j.toFloat()
        }
    }
    findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}

上を追加して測定した結果は下の通りです。

f:id:llcc:20211205190626p:plain

右側の画面でTop Downタブを開くと具体的にどの処理が重いかまでを追いかける事ができます。

f:id:llcc:20211205191104p:plain

StoreKit2のサンプルコードを動かしてみる

今回は下URLからダウンロードできるStoreKit2のサンプルコードを動かしてみました。

developer.apple.com

StoreKit2は旧来のStoreKitを改良したもので、シンプルなコードで購入処理が記述できるようになっています。
ただしiOS15限定以上なので利用する際はアプリの対象OSを15以上にするか旧StoreKitと併用する必要があります。

StoreKitの概要 - Apple Developer

StoreKit2の処理

商品情報の取得方法は下の通りです。
こちらはサンプルコードを少し分かりやすく改変したものになります。
StoreKit2ではawaitを使う事でブロックやDelegateを使わずに取得できるようになりました。

let products = try? await Product.products(for: ["com.example"])

購入処理下の通りです。
purchaseメソッドだけで購入できるので今までに比べて非常に楽になりそうです。

戻り値の型はVerificationResultなのでswitch文でTransactionを取得してfinishさせています。

func purchase(_ product: Product) async throws -> Transaction? {
    //Begin a purchase.
    let result = try await product.purchase()

    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)

        //Deliver content to the user.
        await updatePurchasedIdentifiers(transaction)

        //Always finish a transaction.
        await transaction.finish()

        return transaction
    case .userCancelled, .pending:
        return nil
    default:
        return nil
    }
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    //Check if the transaction passes StoreKit verification.
    switch result {
    case .unverified:
        //StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
        throw StoreError.failedVerification
    case .verified(let safe):
        //If the transaction is verified, unwrap and return it.
        return safe
    }
}

過去の購入情報はTransactionのlatestメソッドで取得する事ができます。
購入時同様、VerificationResult型が返ってくるのでswitch文で取得して検証しています。

func isPurchased(_ productIdentifier: String) async throws -> Bool {
    //Get the most recent transaction receipt for this `productIdentifier`.
    guard let result = await Transaction.latest(for: productIdentifier) else {
        //If there is no latest transaction, the product has not been purchased.
        return false
    }

    let transaction = try checkVerified(result)

    //Ignore revoked transactions, they're no longer purchased.

    //For subscriptions, a user can upgrade in the middle of their subscription period. The lower service
    //tier will then have the `isUpgraded` flag set and there will be a new transaction for the higher service
    //tier. Ignore the lower service tier transactions which have been upgraded.
    return transaction.revocationDate == nil && !transaction.isUpgraded
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    //Check if the transaction passes StoreKit verification.
    switch result {
    case .unverified:
        //StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
        throw StoreError.failedVerification
    case .verified(let safe):
        //If the transaction is verified, unwrap and return it.
        return safe
    }
}

まとめ

簡単に動かしてみましたが従来に比べて処理がシンプルになりそうで良かったです。
シンプルんになっただけでなく返金対応が増えていたり継続課金の検証が簡単だったりするようなので実際に使う時は更に詳しく調べていきたいと思いました。

ShazamKitで音声認識を試してみる

下の記事を参考にShazamKitを動かしてみました。

ichi.pro

ShazamKitは音楽認識ライブラリで、マイク経由で曲を渡すと曲名を出してくれたりします。
ShazamKitのiOS15から登場したライブラリで、SDKAndroidでも使えるようです。

developer.apple.com

初期設定

まずは使いたいアプリのBundleIDのShazamKitを有効化します。
AppleのMemberCenterにアクセスしてShazamKitを使いたいBundleIDを登録してから「App Services」にあるShazamKitにチェックを入れて保存します。

f:id:llcc:20210927135717p:plain

実装

新規にプロジェクトを作成します。
BundleIDは先程登録したIDを使います。

f:id:llcc:20210927140444p:plain

音声認識の際にマイクを使うので、Info.plistにNSMicrophoneUsageDescriptionを追加してマイクを使う理由を記述します。

f:id:llcc:20210927140043p:plain

まずはマイクの音を取り込む実装をします。
ViewControllerを下のように修正します。

class ViewController: UIViewController {
    let engine = AVAudioEngine()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let audioSession = AVAudioSession.sharedInstance()
        try? audioSession.setCategory(.record)
        try? audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        
        let inputNode = engine.inputNode
        try? engine.start()
    }
}

起動するとマイクの音声を読み取る事ができるようになります。

f:id:llcc:20210927141056p:plain

次は読み取った音声を元に曲名を調べる実装をします。
ViewControllerを下のように変更します。

import UIKit
import ShazamKit

class ViewController: UIViewController {
    let session = SHSession() // 追加
    let engine = AVAudioEngine()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let audioSession = AVAudioSession.sharedInstance()
        try? audioSession.setCategory(.record)
        try? audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        
        let inputNode = engine.inputNode
        try? engine.start()
        
        // ここから追加
        session.delegate = self
        let format = inputNode.outputFormat(forBus: .zero)
        inputNode.installTap(onBus: .zero, bufferSize: 1024, format: format) { buffer, _ in
            self.session.matchStreamingBuffer(buffer, at: nil)
        }
    }
}

extension ViewController: SHSessionDelegate {
    func session(_ session: SHSession, didFind match: SHMatch) {
        print(#function)
        print(match.mediaItems)
    }
    
    func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature, error: Error?) {
        print(#function)
        print(error)
    }
}

音声をShazamKitにわたす処理は下の箇所で行っています。

inputNode.installTap(onBus: .zero, bufferSize: 1024, format: format) { buffer, _ in
    self.session.matchStreamingBuffer(buffer, at: nil)
}

読み取り成功時や失敗時はSHSessionDelegateのメソッドが呼ばれます。

extension ViewController: SHSessionDelegate {
    func session(_ session: SHSession, didFind match: SHMatch) {
        print(#function)
        print(match.mediaItems)
    }
    
    func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature, error: Error?) {
        print(#function)
        print(error)
    }
}

曲を流すと下のように正しく曲が認識されました。

f:id:llcc:20210927141534p:plain

日本の曲も試した所、正しく認識する事ができました。

f:id:llcc:20210927141813p:plain

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