くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

Githubでいきなりプッシュできなくなったら

Githubでいきなりプッシュできなくなったのでメモに残します。

ここ最近発生している場合には同じ事象の可能性はあるので参考にしてみてください。

エラー内容

プッシュしようとしたら以下のエラーメッセージが表示されました。 @がたくさん表示されるとビビります!

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:xxxxx
Please contact your system administrator.
Add correct host key in /Users/xxx/.ssh/known_hosts to get rid of this message.
Offending RSA key in /Users/xxx/.ssh/known_hosts:1
Host key for github.com has changed and you have requested strict checking.
Host key verification failed.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

SSHでは初回接続時に接続先ホストの公開鍵を保存しておき、次回接続時にホスト鍵を比較して前回と同じホストに接続したかを確認するような仕組みになっています。 そのため、IPアドレスの振り直しやOS再インストールなどでホスト鍵が変わってしまった場合、エラーが発生してSSH接続が失敗します。

しかし、特にホスト鍵が変わるようなことをした覚えが全くなく、何か変えるようなことしたかなあと思っていたら、Githubにお知らせがありました。

github.blog

対応内容

エラーへの対処としては$HOME/.ssh/known_hostsの該当行を削除すれば同じエラーは出なくなります。 エディタで削除してもいいのですが、下記のようにssh-keygenコマンドの-Rオプションで消すこともできます。

$ ssh-keygen -R github.com
# Host github.com found: line 133 type RSA
/Users/xxxxx/.ssh/known_hosts updated.
Original contents retained as /Users/xxxxx/.ssh/known_hosts.old

困ったら、Githubのお知らせを見ると対処法が記載されているので確認するのが一番よさそうです。

DockerでRailsアプリケーションを動かす

今回はRails環境をDockerで構築する方法についてです。

Docker Compose V1が非推奨となり、Docker Compose V2が推奨となることも踏まえてV2で行おうと思います。

Docker Desktopはインストール済み、アプリケーションを格納するフォルダは作成済みの状態で進めます。

Dockerfileの作成

まずはDockerfileを作成します。
DockerfileはRailsの実行環境用のイメージを作成するためのファイルです。 次のコマンドでDockerfileを作成します。(今回はsamplev2というフォルダを作成したとします)

samplev2 % touch Dockerfile

そして、Dockerfileに以下の記述を追加します。 今回はRuby3.1.0を使用するため、冒頭にFROM ruby:3.1.0を記載します。

FROM ruby:3.1.0
RUN apt-get update -qq && apt-get install -y postgresql-client
WORKDIR /samplev2
COPY Gemfile/samplev2/Gemfile
COPY Gemfile.lock/samplev2/Gemfile.lock
RUN bundle install

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

少し記述について補足します。

RUN apt-get update -qq && apt-get install -y nodejs postgresql-clientRailsアプリケーションに必要なNode.jsとPostgreSQLのインストールを行うように指定します。(PostgreSQLは必須ではありません)

WORKDIR /samplev2は作業ディレクトリを指定します。ここに誤りがあるとうまくプロジェクトが作成されないので、注意が必要です。

COPY Gemfile/samplev2/GemfileはローカルのGemfileをsamplev2配下にコピーします。 COPY Gemfile.lock /samplev2/Gemfile.lockはローカルのGemfile.lockをsamplev2配下にコピーします。

RUN bundle install先程コピーしたGemfileに記載されているGemをbundle installするように指定します。

COPY entrypoint.sh /usr/bin/はローカルのentrypoint.shを/usr/bin配下にコピーします。

RUN chmod +x /usr/bin/entrypoint.shでコピーしたentrypoint.shに権限を付与します。

ENTRYPOINT ["entrypoint.sh"]

EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"]でポート番号を指定して、Railsを実行します。

exec形式([""]の形式)は指定したコマンドと引数がそのまま実行されるので、ENTRYPOINTやCMDはexec形式で記載しましょう。

Gemfileの作成

次にGemfileを作成します。
今回はRails7.0.4.2のアプリケーションを使用するため、作成したGemfileには次のように、gem 'rails', '7.0.4.2'を記載します。

source 'https://rubygems.org'
gem "rails", '7.0.4.2'

そして、Dockerfileをビルドするため、Gemfile.lockも作成します。 Gemfile.lockは空の状態で作成します。

entrypoint.shの作成

次にentrypoint.shを作成します。 既にserver.pidファイルが存在してしまうことで、サーバーを再起動できない問題を解消するために追加します。 (server.pidはサーバー起動時に生成されます。生成されている場合、サーバーは起動状態にあると認識されます。 サーバー終了後、server.pidが残存する恐れがあり、サーバーを再起動する場合に「サーバーは起動しています」というエラーが発生する場合があります。 よって、明示的に残存したserver.pidを削除する事で問題なくサーバーを起動する事ができるようになります)

samplev2 % touch entrypoint.shで空のファイルを作成して、次のように設定します。

#!/bin/bash
set -e

# すでにserver.pidがある場合には削除
rm -f /samplev2/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

exec "$@"でDockerfileのCMDで渡されたコマンド(→Railsのサーバー起動)を実行します。

compose.ymlの作成

次にアプリケーションを構成するサービス一覧を記載し、それらを一括で実行してくれるcompose.ymlを作成します。 V3からはcompose.ymlという名前が使えます。 以前使用していたdocker-compose.yml後方互換のためにしばらくサポートしているだけみたいなので、compose.ymlの方を使った方がいいと思います。

今回は、作成したcompose.ymlにdbとwebの2つを記載します。 dbはPostgreSQLを指定します。 samplev2 % touch compose.ymlで空のファイルを作成して、次のように設定します。

services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/samplev2
    ports:
      - "3000:3000"
    depends_on:
      - db

services:はservices配下にコンテナを定義します(今回はwebとdbのみ)。

image:は利用するイメージの指定します(今回はPostgreSQL)。

volumes:はデータを永続的に保存する場所を指定します(手元のデータをマウントしているなら、コンテナを立ち上げる度にデータが消えない)。 今回の場合は、手元でプロジェクトルートにファイルを作ると、コンテナ内の/sample2にも、ファイルが作られます。

environment:PostgreSQLに関する環境変数の設定(今回はパスワードを設定)

build: .はDockerfileがあるディレクトリを指定しています。

depends_on:はコンテナの実行順序の指定します(今回はDB起動からweb起動する設定にしています)

また、compose.yml内でversion: '3'のようにバージョンを記述する項目が非推奨になったので設定を入れていません。

Railsアプリケーションの作成

ここまでファイルを作成したら、Railsの新規アプリケーションを作成します。

samplev2 % docker compose run --no-deps web rails new . --force --database=postgresql

--forceを設定することで実行時にGemfileの上書きを行います。

--no-depsを設定することでリンクされたコンテナを起動させないようにします。

--skip-bundleを設定することでbundle installをスキップします(今回はgemを追加していないので)

ビルド

rails newで作成した新規アプリケーションのGemfileを更新するため、ビルドを実行します。

samplev2 % docker compose build

DBの作成

ビルドまで成功していれば、デフォルトのフォルダやファイルが作成されていると思います。 ここでDBを作成します。

まずはdatabase.ymlの編集を行います。 デフォルトではRailslocalhost上でDBが動作してしまうため、先ほど記述したDBコンテナを動作先に指定しましょう。 config/database.ymlのdefault部分を以下のように編集します。

// 編集前

default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

↓
↓
↓

// 編集後

default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: password
  pool: 5

編集後は以下のコマンドでDBを起動します。

samplev2 % docker compose run web rails db:create

成功すると次のようにDBが作られると思います。

Created database 'samplev2_development'
Created database 'samplev2_test'

アプリケーションの起動

ここまででDockerの設定を一通り終了しました。 最後にアプリケーションを立ち上げます。 Dockerを起動した状態で、次のコマンドを実行します。

samplev2 % docker compose up

http://localhost:3000/にアクセスしてみて次のような画面が表示されれば成功です。

参照

www.docker.com

docs.docker.com

qiita.com

matsuand.github.io

github.com

github.com

qiita.com

qiita.com

Google Mapを使ってみよう

今回はGoogle Mapをモバイル開発で使う流れをまとめようと思います。 AndroidiOS両方説明しようと思います。
AndroidiOSでできることにはほとんど差がないのでコードの書き方が少し違うだけだと思います。

iOSGoogle Mapの対応を行う

最初にマップを表示させるところから始めましょう!

事前準備

まずは、マップを使えるようにするために次の手順を行う必要があります。(順不同)

  1. APIキーを取得してプロジェクトに導入
  2. ライブラリを使えるようにする
  3. 位置情報の権限追加

まずはAPIキーを取得します。 どのアプリケーションからGoogle Mapを使うかを判定、リクエストを行うためにAPIキーを取得してプロジェクトで設定する必要があります。

APIキーを取得は次の公式ドキュメントを確認してください。 対象のアプリでGoogle Mapの使用するかでONを設定すると認証情報の欄でAPI Keyが確認できます。

developers.google.com

そして、AppDelegateで下記のようにAPIキーを設定します。

GMSServices.provideAPIKey("取得したAPIキーを設定")

次にライブラリを使えるようにpodファイルにGoogle Mapのライブラリを追加してpod installを行います。

pod 'GoogleMaps'

そして、位置情報の権限追加はinfo.plistに位置情報の権限を追加します。 細かい記載は今回割愛します。

マップを表示

事前準備を行ったら実際にマップを表示させます。

class MapViewController: UIViewController {
    private var locationManager: CLLocationManager?
    private var mapView: GMSMapView?

    deinit {
        if let manager = locationManager {
            manager.stopUpdatingLocation()
            locationManager = nil
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        setupLocationManager()
    }

    private func setupLocationManager() {
        let locationManager = CLLocationManager()
        locationManager.delegate = self
        // 位置情報の取得精度を指定
        locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters

        // 位置情報の権限確認
        let status = CLLocationManager.authorizationStatus()

        switch status {
        case .notDetermined:
            locationManager.requestWhenInUseAuthorization()
        case .restricted, .denied:
            setupGoogleMaps()
        case .authorizedAlways, .authorizedWhenInUse:
            locationManager.startUpdatingLocation()
        @unknown default:
            setupGoogleMaps()
        }

        self.locationManager = locationManager
    }
}

extension MapViewController {
    private func setupGoogleMaps() {
        if mapView != nil {
            return
        }

        var location: CLLocation!

        if let lastKnownLocation = locationManager?.location {
            location = lastKnownLocation
        } else {
            location = CLLocation(latitude: 0, longitude: 0)
        }

        // 現在地とマップがどれくらいズームするかを設定する
        let camera = GMSCameraPosition.camera(
            withLatitude: location.coordinate.latitude,
            longitude: location.coordinate.longitude,
            zoom: 12.5
        )

        // GMSMapViewを初期化する
        let mapView = GMSMapView.map(
            withFrame: CGRect(x: 0, y: 0, width: view.frame.width,height: view.frame.height),
            camera: camera
        )

        // 現在値を中心にするボタンを有効にする設定
        mapView.settings.myLocationButton = true

        // AutoLayoutで定義したUIViewにaddSubViewする
        mapBaseView.addSubview(mapView)
        mapBaseView.sendSubviewToBack(mapView)

        self.mapView = mapView
    }
}

// MARK: - CLLocationManagerDelegate

extension MapViewController: CLLocationManagerDelegate {
    // 権限が変更されたときの処理
    func locationManager(_ manager: CLLocationManager, 
                           didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .notDetermined:
            manager.requestWhenInUseAuthorization()
        case .restricted, .denied:
            setupGoogleMaps()
        case .authorizedAlways, .authorizedWhenInUse:
            manager.startUpdatingLocation()
        @unknown default:
            setupGoogleMaps()
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        manager.stopUpdatingLocation()
        setupGoogleMaps()
    }
}

上記は表示させるためのコードです。

setupLocationManager()で位置情報を許諾しているかの確認を行い、まだ1度も設定していない場合には確認のダイアログを表示させます。 許可している場合にはstartUpdatingLocation()で現在地を取得してマップに適用させます。
移動する場合にもCLLocationManagerDelegatefunc locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])で現在地を定期的に更新できます。

また、マップにはGMSMapViewクラスを使います。 GMSMapViewは、地図に関するすべてのメソッドの入り口で、表示するMapのクラスです。
setupGoogleMaps()でGoogleMapの表示設定を行いました。 大まかには初期表示の位置やどれくらいズームするかを設定したGMSMapViewを初期化して、AutoLayoutで定義したUIViewにaddSubViewすると表示できます。
それが上記のコードになります。

ピンを表示

次に特定の位置にピン(アイコン)を表示させる方法です。
ピンにはGMSMarkerクラスを使います。 先ほどの例のsetupGoogleMaps()の後にcreateMarker()メソッドを追加します。

class MapViewController: UIViewController {
    private var locationManager: CLLocationManager?
    private var mapView: GMSMapView?
}

extension MapViewController {
    private func setupGoogleMaps() {
        // 先ほどの例の部分は割愛

        createMarker()
    }

    private func createMarker() {
        let marker = GMSMarker()

        //ピンの色変更したい場合に設定
        marker.icon = GMSMarker.markerImage(with: .black) 
        marker.position = CLLocationCoordinate2D(latitude: 34.077875549971,
                                                 longitude: 134.56156512254)
        marker.title = "タイトル"
        //ピンの画像を変更したい場合に設定
        marker.icon = UIImage(named: "camera_1")!
        marker.zIndex = 2
        marker.map = mapView

        mapView.delegate = self
    }
}

extension MapViewController: GMSMapViewDelegate {
    // ピンをタップした時の処理
    func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
        return true
    }

    // マップをタップした時の処理
    func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) {
    }

    func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) {
    }
}

今回はposition(表示させる緯度・経度の情報)と titleを含むGMSMarkerオブジェクトを作成し、GMSMapViewに設定します。 titleはピンをタップすると情報ウィンドウに設定した文字列が表示されます。 iconはアイコンの情報を設定することができ、色や画像の変更を行えます。
GMSMapViewDelegateを設定しているのはアイコンやマップをタップしたときに処理を行う場合には必要です。
また、zIndexを指定していますが、これはピンが重なった時にzIndexの値が大きい方が前に表示されます。
意外と簡単だと思った方が多いと思いますが、ここまでで最低限のマップアプリは作れると思います。

AndroidGoogle Mapの対応を行う

AndroidiOSと同様に進めます。
iOSと大きな違いがあるわけではないのでそこまで難しいことはないと思います。

マップを表示

まずはiOSと同様にマップを表示させるところから始めましょう!

最初にAPI Keyを設定します。
AndroidManifest.xmlAPI Keyの設定を追加します。

<meta-data
        android:name="com.google.android.geo.API_KEY"
        android:value="取得したAPI Keyを設定" />

そして、ライブラリをbuild.gradleに追加します。

implementation 'com.google.gms:google-services:4.3.14'
implementation 'com.google.android.gms:play-services-location:21.0.1'
implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1'

Mapを表示させるにはGoogleMapクラスを使用します。 LocationServices.getFusedLocationProviderClientで位置情報を取得します。 Mapを表示させるための一例は次のコードです。

class MapFragment : Fragment(), OnMapReadyCallback {
    private var mapView: GoogleMap? = null
    private var fusedLocationClient: FusedLocationProviderClient? = null

    private var _binding: FragmentMapBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentMapBinding.inflate(inflater, container, false)

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        setupData()
    }

    override fun onResume() {
        super.onResume()
    }

    private fun setupData() {
        checkPermission()
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())

        val mapFragment =
            childFragmentManager.findFragmentById(R.id.mapView) as SupportMapFragment
        mapFragment.getMapAsync(this)     
    }

    override fun onMapReady(p0: GoogleMap) {
        this.mapView = p0
        fetchLocation()
    }

    private fun fetchLocation() {
        mapView?.uiSettings?.isMyLocationButtonEnabled = false
        if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.ACCESS_COARSE_LOCATION
            )
            != PackageManager.PERMISSION_GRANTED
        ) {
            return
        } else {
            mapView?.isMyLocationEnabled = true
            val task = fusedLocationClient?.lastLocation
            task?.addOnSuccessListener { location ->
                if (location != null) {
                    // Mapの中心の緯度・経度を設定して表示させる
                    mapView?.moveCamera(
                        CameraUpdateFactory.newLatLngZoom(
                            LatLng(location.latitude, location.longitude),
                            10.0f
                        )
                    )
                } else {
                    val request = LocationRequest.create()
                        .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
                        .setInterval(500)
                        .setFastestInterval(300)

                    fusedLocationClient?.requestLocationUpdates(
                        request,
                        object : LocationCallback() {
                            override fun onLocationResult(result: LocationResult) {
                                mapView?.moveCamera(
                                    CameraUpdateFactory.newLatLngZoom(
                                        LatLng(
                                            result.lastLocation!!.latitude,
                                            result.lastLocation!!.longitude
                                        ),
                                        12.5f
                                    )
                                )
                                fusedLocationClient?.removeLocationUpdates(this)
                            }
                        }, Looper.getMainLooper()
                    )
                }
            }
        }
    }       

今回はFragmentでGoogle Mapを表示させていますが、Activityで表示させる場合にも基本的には同じです。 FragmentManager.findFragmentById() を呼び出して、地図フラグメントに対するハンドルを取得します。
次に、getMapAsync()を使用して、GoogleMapインスタンス生成完了時に呼ばれる地図コールバックを登録します。 OnMapReadyCallbackインターフェースを実装し、GoogleMap オブジェクトが使用可能な場合に地図を設定するようonMapReady()をオーバーライドします。

ピンを表示

次に特定の位置にピン(アイコン)を表示させる方法です。
iOSではピンにはGMSMarkerクラスを使いましたが、AndroidではMarkerクラスを使用します。 Mapが表示した後に下記の処理を行います。(onMapReady()や初期設定後)

private fun createOption(latLng: LatLng): MarkerOptions {
    val options = MarkerOptions().position(latLng)
    // アイコンを指定
    options.icon(BitmapDescriptorFactory.fromResource(R.drawable.pin))

    options.zIndex(2.0F)

    // mapViewはGoogle Mapクラスのインスタンス
    mapView?.addMarker(options)
}

上記の処理を行うと指定した緯度・経度にマーカーが表示されます。 削除する場合にはremove()を使用します。

ダークモード対応

作成したコードをダークモードで実行してもライトモードのままだと思います。 ダークモード対応は追加で別で実装が必要となるので、最後に補足します。

iOS

次のメソッドをマップの初期化や画面復帰した際の処理に追加してください。

private func setMapStyle(_ mapView: GMSMapView?) {
    guard let mapView = mapView else {
        return
    }

    if UITraitCollection.current.userInterfaceStyle == .dark {
        // ダークモードのjsonを読み込ませる
        if let styleURL = Bundle.main.url(forResource: "map_dark", withExtension: "json") {
            do {
                mapView.mapStyle = try GMSMapStyle(contentsOfFileURL: styleURL)
            } catch {
                return
            }
        }
    } else {
        // ライトモードのjsonを読み込ませる
        if let styleURL = Bundle.main.url(forResource: "map_light", withExtension: "json") {
            do {
                mapView.mapStyle = try GMSMapStyle(contentsOfFileURL: styleURL)
            } catch {
                return
            }
      }
}

何をやっているかというと、GMSMapViewのmapStylejsonで定義した色などの情報を適用させています。 背景色やラベルの色、道路の色など設定できます。
下記に色を設定してjsonファイルを作成してくれるサイトがあるので、作成は楽だと思います。

https://mapstyle.withgoogle.com

Android

AndroidiOSと同様にjsonで定義した色などの情報を適用させます。 他にも方法はあるかもしれませんが、iOSと実装方法を共通化できる点でjsonを使うのがおすすめです。

private fun setMapStyle(mapView: GoogleMap?) {
        mapView?.let { view ->
            val nightModeFlags = requireContext().resources.configuration.uiMode and
                Configuration.UI_MODE_NIGHT_MASK

            if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) {
                view.setMapStyle(
                    MapStyleOptions.loadRawResourceStyle(
                        requireContext(), R.raw.map_dark
                    )
                )
            } else {
                view.setMapStyle(
                    MapStyleOptions.loadRawResourceStyle(
                        requireContext(), R.raw.map
                    )
                )
            }
        }
    }

最後に

両OSともにシンプルなコードでGoogle Mapを表示できたと思います。 もう少し突っ込んだことを実装したい場合には公式ドキュメントが細かく記載されているため確認してみるといいと思います。

参照

developers.google.com

qiita.com

developers.google.com

developers.google.com

akira-watson.com

qiita.com

qiita.com

KotlinのCoroutineについて

今回はCoroutineについてまとめようと思います

Coroutineは並行実行のデザインパターンです。 そして、特定のスレッドに束縛されない中断可能な計算インスタンスです。
非同期処理で用いられますが、Threadよりも軽量で、実行途中で処理を中断・再開することができます。
今まではRxJavaを使っていましたが、Googleが公式に提供しているのでCoroutineが非同期処理の王道になっています。

Coroutineを使うことによって、作成したクラスが残ってしまうことによるメモリリークを考慮する必要がありません。 ThreadやAsyncTaskを使用する場合、作成したクラスからライフサイクルが終了したActivityやViewModel内のメソッドを呼び出されメモリリークを引き起こすリスクがあります。

Coroutine Scope

CoroutineScopeは、Coroutineが実行される範囲です。
Coroutineは通常、CoroutineScopeで起動されます。 これにより、管理されずに失われるCoroutineがなくなり、リソースが無駄になりません。

CoroutineScopeはライフサイクルに関連付けられ、スコープ内のコルーチンの存続期間を設定します。
Scopeがキャンセルされると、そのJobはキャンセルされ、キャンセルが子Jobに伝播します。
Scope内の子ジョブが例外で失敗すると、他の子Jobがキャンセルされ、親Jobがキャンセルされて、例外は呼び出し元に再度スローされます。

CoroutineScopeは、launchまたはasyncを使用して作成したCoroutineをすべて追跡します(launchやasyncは後述します)。 Android では、独自のCoroutineScopeを提供しています。 たとえば、ViewModelには viewModelScope、Lifecycle(Activityなど)には lifecycleScopeがあります。 ただし、ディスパッチャとは異なり、CoroutineScopeでCoroutineは実行されません。

Coroutineは実行するためには次で説明するBuilderが必要です。

Builder

BuilderとはCoroutineScopeを起点にスレッドを起動して、引数で指定されたタスクブロックを実行します。 簡単にいうと、Builderの役割はコルーチンを開始することです。 Builderは3種類あるのでそれぞれみていきましょう!

launch

launch関数でCoroutineを開始でき、コードブロック内に並列に実行する処理を記述できます。 launchは新規コルーチンを開始し、呼び出し元に結果を返しません。

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        println("World!")
    }
    GlobalScope.launch {
        println("start")
    }
    println("end")
}

上記の処理だと"World!"と"start"は並列なのでどちらが先に出力されるかは明確には決まっておらず、順番が変わる可能性はあります。

また、上記の例の GlobalScopeは先ほど軽く触れたCoroutineScopeと呼ばれています。

※GlobalScopeは取扱注意のCoroutineScopeとして定義されています。 基本的には使わないという認識でいた方がいいと思われます。
GlobalScopeはどのJobにもバインドされません。 GlobalScopeはアプリケーションの寿命を通じて動作する最上位のコルーチンを起動するには使用され、途中でキャンセルされません。
GlobalScope で起動されたアクティブなCoroutineは、プロセスをaliveに維持するものではなく、 デーモンスレッドのように機能します。
GlobalScope は、使用すると誤ってメモリリークを発生させてしまう可能性があるため、取り扱いに注意な API です。

launch() 関数でコルーチンを起動すると、Jobインスタンスが返されます。 Jobは、コルーチンへの参照を保持するため、そのライフサイクルを管理できます。
Jobを使用すると、Jobのステータス(アクティブ、キャンセル、完了)を確認できます。
Coroutineと、そのCoroutineが起動したCoroutineがすべての処理を完了していれば、Jobは完了です。
なお、Coroutineは別の理由(キャンセルや例外による失敗など)で完了することもありますが、その時点でJobは完了したとみなされます。

また、メインの処理とは別スレッドになるので、launchしたスレッドが終わるまで待つということはありません。 スレッドの状態を確認するプロパティがあるので、うまく使って待ってあげる必要があります。

async

asyncはlaunchとの違い、戻り値の型に特に制限が無いため、任意の値を返せます。(Deferredを返却) また、launchの場合、処理が終わったかどうかをプロパティで判断していました。 asyncはDeferredオブジェクトを返します。
これは特定の型のインスタンスを後で返すオブジェクトです。
また、キャンセルも行えます。
Deferred に対して、 await を呼び出すことで最終的なデータを受け取ることができます。

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    val n = 2
    val add = async {
        delay(1000L)
        n + 1
    }
    val multi = async {
        delay(1000L)
        n * 3
    }

    val amount = add.await() + multi.await()
    println(amount) // (2+1と2 *3の合計の9が出力される)
}

runBlocking

runBlockingは自身を実行したスレッドをブロックするBuilderです。

fun main() {
    println("start")
    runBlocking {
        println("coroutine!")
    }
    println("end") // start、coroutine!、endの順で表示される
}

上記の例をみるとrunBlockingの処理が終わったら後続の処理が行われていることがわかると思います。

また、runBlockingは実行したCoroutineの戻り値を取得することもできます。 先程の例を次のように変更します。

fun main() {
    println("start")
    val text = runBlocking {
        "coroutine!"
    }
    println(text)
    println("end")
}

この場合も先程と同じく、"start", "coroutine!", "end" の順に出力されます。

runBlockingはCoroutineScopeの拡張関数ではないので、起点のCoroutineScopeを必要としません。 GlobalScopeをrunBlockingの定義内に持っているからです。 runBlockingはGlobalScopeを参照します。

最後にrunBlockingは自身を実行したスレッドをブロックするので、非同期という考えから外れてしまうので使うのをオススメしません。

suspend関数

次にsuspend関数についてです。 suspendは中断という意味があるので、suspend関数は中断可能な処理を表します。 中断のイメージは以下の画像です。

suspend関数は主に以下の特徴があります。

  • 暗黙に別のスレッドというのがあり、別のスレッドで行われる(コードには現れない)処理がある
  • 呼び出し自体はすぐ返ってくるが実際の処理はこの時点では行われていない
  • 実際の処理の終了は、呼び出した関数が終わったあとに、システムからコールバックで返ってくる
  • suspendというキーワードのついた関数のbody部にかかれている事は、全部この主スレッドで実行されます。 例えば、launchとかの中で呼び出せばUIスレッドであることを表す
  • suspend関数のbody部に書く関数は必ずsuspend関数になる
  • Coroutineを開始するには最低1つのsuspend関数がなければなりません

一般的な非同期処理では、呼び出した結果はすぐ戻ってきてしまいます。 呼び出した関数、例えばonCreateの実行はそのまま進んでしまい、サーバーに実際にリクエストがpostされるよりも前に最後まで実行を終えてreturnしてしまいます。 そして、別スレッドの処理が終わると、主スレッドであとからコールバックという関数が呼ばれます。

susupend関数でもほぼメカニズムは同じです。
suspend関数を呼んだ時にはすぐに返ってきて、あとから処理が終わったという通知が主スレッドに何かしらの形でやってきます。
ただ、普通のコールバックとは違ってsuspend関数の結果の呼び出しは直接はコードの上では見えないのが難しく感じる部分かもしれません。 主スレッドで呼び出されたら何か短い処理が走ってすぐ返ってきます。そして、別スレッドに通知がいって、コードには書いてない何かの処理が裏で行われる・ 別スレッドの処理が終わると、謎の通知メカニズムで主スレッドにあとから通知が来ます。
これがsuspend関数の実行されるスレッドを考える時の基本になります。

suspend関数のbody部に書く関数は必ずsuspend関数になるに関しては次のようなイメージです

suspend fun getContent(id: Int): String { /* ... */ }

suspend fun getPostContent(id: Int): String {
    val content = getContent(id)
    return content
}

「Coroutineを開始するには最低 1 つのsuspend関数がなければなりません」について、軽く補足です。

launchの定義は次のようになっています。

public fun launch(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

つまり、先程からlaunchなどでラムダを使用してCoroutineを作成していましたが、実はこれらは無名関数のsuspend関数だったのです。 このように明記されていなけどsuspend関数があるパターンもあります。

Channel

Channelについて説明していきます。

ChannelはメインからもCoroutineからでも使える入れ物のようなものです。
ただ、単なる入れ物ではなく、取り出す(受信する、コンシューマー)スレッドに待ってもらったり、Channelにデータを入れる(送信する、プロデューサー)スレッドにはデータが入っているので送信を待ってもらうなどの交通整理を行います。

そして、Channelは受け取ってもらえるまで送信側の処理が動き続けるので場合によってはメモリリークを引き起こす可能性があるため、注意が必要です。

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) channel.send(x * x)
    }

    // receive()は一つずつsendされた情報を受け取る
    // なお、ここで中断されるため、sendされない限り次の処理に進まない。非同期でないところに注意
    repeat(5) { println(channel.receive()) }
    println("Done!")
}

// 出力
// 1
// 4
// 9
// 16
// 25
// Done!

あまり使うイメージがわかない方もいると思いますが、次のようなメリットがあります

  • Channelを使用すると、コルーチンが待機することなく、キューに格納されたデータを受信することができます
  • Channelは、指定したサイズのバッファを持つことができます。これにより、キューに格納されたデータが多すぎても、コルーチンが待機することなく、プロデューサーが新しいデータを送信することができます
  • クローズすることができます。これにより、プロデューサーが新しいデータを送信できなくなったことを、コンシューマーに通知することができます
  • プロデューサーとコンシューマーの間で明確な制御ができます。プロデューサーが送信したデータをコンシューマーが受信するまで待機することができ、逆に、コンシューマーが受信するまで、プロデューサーが新しいデータを送信しないように制御することもできます
  • 複数のコルーチンで同じChannelにアクセスすることができます。これにより、プロデューサーとコンシューマーが別々のコルーチンで実行される場合でも、データの送受信が可能になります

Flow

Flowは、非同期ストリーム処理を扱うための機能で、Kotlinのコルーチンライブラリに含まれています。 suspend関数は非同期で一つの値を返しますが、Flowは非同期で複数の計算した値を返します。 APIからストリーミングされるリアルタイムデータやデータベースの変更通知など、非同期で取得される複数の値を処理する場合に使用することができます。

Flowは、以下のような特徴があります。

  • 再利用可能なコンポーネントとして設計されています。つまり、同じコードを複数の場所で使用することができます。Flowの定義を変更することで、複数の場所で異なる出力を生成することもできます。
  • コルーチンがキャンセルされたときに自動的にキャンセルされます。そのため、コードが複雑になることなく、非同期処理をキャンセルすることができます。
  • 多様なオペレーターを提供しています。これにより、簡単にフィルタリング、変換、結合、集約などの処理を行うことができます。
  • FlowはChannelと違い、受信する処理(collectメソッド)が実行されないと、送信処理(emitメソッド)を行いません。(ColdなObservable/ Stream)そのため、メモリーリークは発生しません。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    launch {
        for (k in 1..3) {
            delay(500)
        }
    }
    simpleFlow().collect { value -> print(value) }
}

fun simpleFlow(): Flow<Int> = flow {
    for (i in 1..3) {
        // Thread.sleep(500)
        delay(500)   
        emit(i)
    }
}

// 出力
// 1
// 2
// 3

emit関数を使用して値がemitされ、collect関数を使用して値を取得・集計します。

Coroutine使用例

APIをViewModelで呼び出す

個人的にもっとも使うパターンがViewModelでAPI実行などの非同期処理を行うパターンです。 ViewModelでAPIの処理を非同期で行い、結果をActivityやFragmentに伝えるという流れです。

そこで、APIの処理をcoroutineで行うパターンです。取得した結果はLiveDataを使って伝搬します。

class TestViewModel(val app: Application): AndroidViewModel(app) {
    private val _sample: MutableLiveData<CustomResponse> = MutableLiveData()
    val sample: MutableLiveData<CustomResponse> = _sample

    fun get() {
        viewModelScope.launch {
            try {
                val request = TestRequest()
                val response = request.get()
                _sample.postValue(response)
            } catch {
            }
        }
    }
}

class TestRequest: CustomResponse {
    // ここで本来はAPIを実行、今回はあくまでもサンプル
    suspend fun get() {
        return CustomResponse
    }
}

class TestActivity: AppCompatActivity() {
    override fun onCrerate(saveInstanceState: Bundle?) {
        viewModel = ViewModelProvider(this)[TestViewModel::class.java]

        viewModel.sample.observe(this) { sample ->
            // ViewModelからの結果を取得
        }
    }
}

CoroutineScopeをviewModelScopeで起動します。 APIの実行部分はsuspend関数にしないとエラーが発生します。

また、LiveDataはStateFlowに置き換えることも可能です。

class TestViewModel(val app: Application): AndroidViewModel(app) {
    private val _sample = MutableStateFlow(CustomResponse())
    val sample: StateFlow<CustomResponse> = _sample

    fun get() {
        viewModelScope.launch {
            try {
                val request = TestRequest()
                val response = request.get()
                _sample.value = response
            } catch {
            }
        }
    }
}

class TestRequest: CustomResponse {
    suspend fun get() {
        return CustomResponse
    }
}

class TestActivity: AppCompatActivity() {
    override fun onCrerate(saveInstanceState: Bundle?) {
        viewModel = ViewModelProvider(this)[TestViewModel::class.java]

        lifecycleScope.launch {
            viewModel.sample.collect {
            }
        }
    }
}

変更通知を受け取るには collect を使います。 collect は suspend 関数になってるので、 lifecycleScopeで起動します。

そして、MutableStateFlow の場合は必ずコンストラクタに初期値にパラメータが必要になります。 そのため collect したタイミングで初期値が必ず通知されることになります。 これはコンストラクタにnullを設定した場合でも通知されるのがLiveDataと異なるので意識しましょう!

MutableStateFlowはメインスレッド・バックグラウンドスレッド関係なくvalueを設定することで更新します。 StateFlowの場合はCoroutineContextによって値を受け取るときのスレッドを制御することができます。 lifecycleScope はデフォルトではメインスレッドで動くようになっています。 これは制御可能で、次のようにすることで collect の処理を IO Workerで動かすことが可能になります。そのため、UI変更の処理をしてるともちろんクラッシュします。

lifecycleScope.launch(Dispatchers.IO) {
    viewModel.stateFlow.collect {
        // メインスレッドで実行してないので、UIを変更しようとするとクラッシュする
        textView.text = "$it"
    }
}

EventBus

coroutinesを使ってEventBusのように使うことができます。

EventBusはEventが発火されたら、そのEventの発火を待ち受けていた別の箇所で処理をする仕組みです。 イベントを発行するオブジェクト(Publisher)と、そのイベントに対する処理を実行するオブジェクト(Subscriber)をつなぐ仕組みを提供します。 EventBusは、PublisherとSubscriberを疎結合にすることで、コードの柔軟性と保守性を向上させることができます。 つまり、PublisherとSubscriberは、EventBusを介して明示的に接続される必要はありません。 また、PublisherとSubscriberが同じスレッド上で実行されている必要もありません。

EventBusをRxJavaで作っていた方が今までは多いと思います。 Courtineで作成する場合には、Channelを使って可能ですが、演算のオペレータがなかったり、closeを忘れるとメモリリークを引き起こす可能性があります。 そこでFlowを使ってEvent Busを実現させます。

まずはイベントを管理するクラスを作成します

class EventFlow {
    private val flow = MutableSharedFlow<String>()

    suspend fun sendEvent(event: String) = flow.emit(event)

    fun subscribe(scope: CoroutineScope, onConsume: (String) -> Unit) {
        flow.onEach {
            onConsume(it)
        }.launchIn(scope)
    }
}

今回はホットストリームなFlowとして扱えるSharedFlowを使います。 これにより複数箇所から呼び出してもFlow側の処理数が増えることはありません。 そして、subscribe()の引数で指定したscopeの中で、Flowのイベントを待ち受けます。

class TestActivity : AppCompatActivity() {

    private val eventFlow = EventFlow()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //EventFlowをsubscribeしてイベントを待ち受ける
        eventFlow.subscribe(lifecycleScope) {
            when (it) {
                "BUTTON_PUSHED" -> {
                    // イベントを受けとったら処理を行う
                }
            }
        }

        findViewById<Button>(R.id.button).setOnClickListener {
            runBlocking {
                //Event発火
                eventFlow.sendEvent("BUTTON_PUSHED")
            }
        }
    }
}

上記のようにsendEventでイベントを発火したらsubscribeを定義している箇所で処理を行います。 今回はイベント発火とイベントを受け取るのを同一クラスで行いましたが、別々のクラスでも可能です。

最後に

Coroutineについて紹介しましたが、まだまだ理解が浅い箇所や知らない機能もあります。 しかし、ドキュメントが充実しているので1度確認するのをオススメします!

参照

developer.android.com

kotlinlang.org

kotlinlang.org

github.com

developer.android.com

developer.android.com

android.benigumo.com

iOSのカメラについて

今回はiOSのカメラ機能についてまとめようと思います

カメラ権限設定

最初にカメラと写真のライブラリーを使用するため、アクセスの許可をする必要があります。 プロジェクトのinfo.plistを開いて、アプリのアクセス許可を表示させるように設定を追加します

info.plistのInformationPropertyListの右側の+を押して Privacy - Camera Usage Description(カメラを使用することの許可を求める)を設定します。 右側のvalueに許可を求めるダイアログに表示する文言を設定します。 設定した文言が権限チェックのダイアログに表示されます。 権限を設定していない場合には下記のような権限確認のダイアログを表示させる処理を実装しましょう

AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in           
})

AVCaptureSessionの設定

次にAVCaptureSessionの設定を行います。 AVCaptureSessionはキャプチャ動作を管理し、入力デバイスからキャプチャ出力へのデータの流れを調整するオブジェクトです。 AVCaptureDeviceInputAVCaptureOutputの仲介役を行なっているのがAVCaptureSessionクラスになります。

AVCaptureDeviceInputは、AVFoundationフレームワークで使用されるクラスの1つで、 ビデオやオーディオなどのキャプチャデバイスからの入力をキャプチャセッションに提供するために使用されます

AVCaptureOutputも同様に、AVFoundationフレームワークのクラスの1つで、キャプチャーセッションからの出力データを処理するために使用されます。 キャプチャーセッションからのビデオフレームやオーディオサンプルなど、セッションからのデータを取得、加工、保存するために使用されます。

AVCaptureSessionの実装の流れは次の手順で行います

  1. AVCaptureDeviceインスタンスを生成
  2. AVCaptureDeviceインスタンスからAVCaptureDeviceInputを構築
  3. AVCaptureSessionにAVCaptureDeviceInputを登録
  4. AVCaptureSessionにAVCaptureOutputを登録
  5. AVCaptureVideoPreviewLayerで画面を構築

AVCaptureDeviceInputを構築

AVCaptureDeviceInputインスタンスを構築するためにはAVCaptureDeviceクラスを用いて、まず使用するデバイスを設定する必要があります

使用するAVCaptureDeviceインスタンスを生成するには以下の2つのどちらかを使用します

  • AVCaptureDevice.defaultメソッド
  • AVCaptureDevice.DiscoverySessionクラス

defaultメソッドは引数に指定されたタイプのデフォルトデバイスを返します。引数にはAVMediaTypeの任意の値を渡します

// .audioなど指定できる
guard let videoDevice = AVCaptureDevice.default(for: .video) else { return }

DiscoverySessionは特定の条件にマッチするAVCaptureDeviceを検索するクラスです。 引数にはデバイスタイプ(カメラの種類)とメディアタイプ(videoやaudioなど)、ポジション(カメラの位置)を渡します。 こちらの方がカスタマイズできます

let cameraDevice = AVCaptureDevice.default(
            AVCaptureDevice.DeviceType.builtInWideAngleCamera,
            for: AVMediaType.video,
            position: .back
        )

AVCaptureDeviceインスタンスからVCaptureDeviceInputを構築

設定したデバイスを元にAVCaptureDeviceInputインスタンスを生成します。 またAVCaptureSessionインスタンスもここで生成しておきます。そして、AVCaptureSessionインスタンスに対してInputとOutputを登録します

let cameraDevice = AVCaptureDevice.default(
            AVCaptureDevice.DeviceType.builtInWideAngleCamera,
            for: AVMediaType.video,
            position: .back
        )

let captureSession = AVCaptureSession()
        
let videoInput: AVCaptureDeviceInput
        
do {
    videoInput = try AVCaptureDeviceInput(device: cameraDevice)
} catch {
    captureSession.commitConfiguration()
    return
}

AVCaptureSessionにAVCaptureDeviceInputを登録

AVCaptureDeviceInputは、デバイスがカメラ、マイク、またはその他の種類であるかどうかに応じて、AVCaptureDeviceInputを使用してビデオデータまたはオーディオデータをキャプチャできます。 カメラの映像を処理するアプリケーションでは、AVCaptureDeviceInputを使用して、カメラからの映像を取得し、処理することができます

AVCaptureDeviceInputを登録する際はcanAddInputメソッドを使用して追加が可能かを識別し、問題なければ登録します

if captureSession.canAddInput(videoInput) {
    captureSession.addInput(videoInput)
} else {
    captureSession.commitConfiguration()
    return
}

AVCaptureSessionにAVCaptureOutputを登録

AVCaptureOutputは、AVCaptureSessionに接続され、キャプチャされたデータを受け取り、アプリケーションが処理できるフォーマットに変換することができます。 例えば、AVCaptureVideoDataOutputを使用して、キャプチャーセッションからのビデオデータをアプリケーションに提供することができます。 同様に、AVCaptureAudioDataOutputを使用して、キャプチャーセッションからのオーディオデータをアプリケーションに提供することができます

AVCaptureOutputもcanAddOutputメソッドを使用して追加が可能かを識別し、問題なければ登録します

let photoOutput = AVCapturePhotoOutput()
if captureSession.canAddOutput(photoOutput!) {
    captureSession.addOutput(photoOutput!)
}

AVCaptureVideoPreviewLayerで画面を構築

カメラデバイスからビューを表示するためのレイヤーを構築します。 この手順を行わないとカメラアプリを起動したときのように背景が画面が映らないので注意してください

let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
// photoViewはカメラを表示させるview(適時置き換えてください)
previewLayer.frame = photoView.bounds
// カメラから得られた映像の表示形式を設定
previewLayer.videoGravity = .resizeAspectFill
captureSession.sessionPreset = AVCaptureSession.Preset.photo
self.view.layer.addSublayer(previewLayer)

// 最後にstartRunningメソッドを実行して、セッションの入力から出力へのデータフローを開始します
captureSession.startRunning()

startRunning()を行うことでカメラアプリを起動したときのように端末を移動させると背景も移動するようになります。 逆にセッションを終了させる場合にはstopRunning()を行いましょう。 ここまでがカメラ撮影機能を実装する基本の流れです

撮影する

先ほどまでの状態実行するとでiPhone標準のカメラアプリを起動したときと同じになりはずです。 ここからはシャッターボタンを押したときの処理を追加する必要があります。 なお今回はシャッターボタンの作成は省きます。 シャッターボタンをタップして撮影するところからの実装です

AVCapturePhotoSettingsクラスで、撮影する際のフラッシュや手ぶれ補正などをおこなうかなどの設定をします。 そして、AVCapturePhotoOutputクラスのcapturePhotoメソッドで指定した設定で写真の撮影を開始します

let settings = AVCapturePhotoSettings()
// フラッシュの設定
settings.flashMode = .auto
// 撮影された画像をdelegateメソッドで処理
photoOutput?.capturePhoto(with: settings, delegate: self)

しかし、この処理だけだと不完全です。 撮影された写真データををdelegateメソッドで受け取る必要があります

撮影後の処理

写真を撮影した後、画像データを保存したりする場合にどうすればいいでしょうか?

AVCapturePhotoOutputクラスのcapturePhotoメソッドで撮影した画像データを受け取るために、AVCapturePhotoCaptureDelegatephotoOutputメソッドで撮影した画像データを取得します。 撮影された画像データは、パラメータのphotoにピクセルデータとメタデータなどの関連データと共に格納されています。 fileDataRepresentationメソッドでデータを生成し、UIImageオブジェクトに変換することが多い気がします

 extension ViewController: AVCapturePhotoCaptureDelegate {
    // 撮影した画像データが生成されたときに呼び出されるデリゲートメソッド
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            // Data型をUIImageオブジェクトに変換
            let uiImage = UIImage(data: imageData)
        }
    }
}

補足

ここからは今までの説明の補足をいくつか追加しようと思います

向きを変える

iPhoneだとあまりないと思いますが、iPadだと横向きに変更して撮影を行う場合があるかもしれません。 ここまでの実装だけだとカメラの向きが変わらないので、少し実装を追加する必要があります

// UIInterfaceOrientationをAVCaptureVideoOrientationにConvert(全方位カメラの向きを許容する)
func convertOrientation(orientation: () -> UIInterfaceOrientation) -> AVCaptureVideoOrientation? {
    let orientation = orientation()
    switch orientation {
    case UIInterfaceOrientation.unknown: return nil
    default:
        return ([
            .portrait: .portrait,
            .portraitUpsideDown: .portraitUpsideDown,
            .landscapeLeft: .landscapeLeft,
            .landscapeRight: .landscapeRight
        ])[orientation]
    }
}

// UIInterfaceOrientationをAVCaptureVideoOrientationにConvert
func appOrientation() -> UIInterfaceOrientation {
    let scenes = UIApplication.shared.connectedScenes
    let windowScenes = scenes.first as? UIWindowScene
    guard let orientation = windowScenes?.interfaceOrientation else {
        retrun .landscape
    }

    return orientation
}

プレビューに関しては AVCaptureVideoPreviewLayer を利用しており、AVCaptureVideoPreviewLayer.connection.videoOrientation、 撮影後の写真に関しては AVCaptureConnection.videoOrientationをそれぞれ画面の回転に合わせて更新する必要があります

画面の向きについてはUIApplication.shared.connectedScenesから取得しています

上記の処理をプレビューのフレームを設定するときや写真を撮る前、画面が回転したときに行うと正しい向きで処理が行われます。 次のように使います

// 画面を回転したとき、プレビューの向き設定
override func viewWillTransition(
        to size: CGSize,
        with coordinator: UIViewControllerTransitionCoordinator
    ) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate(
            alongsideTransition: nil,
            completion: {(UIViewControllerTransitionCoordinatorContext) in
                if let orientation = self.convertOrientation(orientation: {return self.appOrientation()}) {
                    previewLayer.connection?.videoOrientation = orientation
                }
        })
}


// 撮影後の写真の向き設定
if let connection = photOutPut?.connection(with: .video), 
    if let orientation = self.convertOrientation(orientation: {return self.appOrientation()}) {
    connection.videoOrientation = orientation
}

lockForConfiguration()について

focusModeexposureModeなどデバイスのプロパティを設定・更新を行う場合には更新前にlockForConfiguration()で端末をロックして設定を更新します。 更新が終わったらunlockForConfiguration()をロックを解除します

イメージとしてはDBを更新する際に他からの更新がないようにロックするようなイメージです

下記はライトを点灯させる処理です。 ちなみにライトが搭載されていない端末で下記の処理を行おうとするとクラッシュするので、try~catchで囲みましょう

let avCaptureDevice = AVCaptureDevice.default(for: AVMediaType.video)

if avCaptureDevice!.hasTorch, avCaptureDevice!.isTorchAvailable {
    do {
        try avCaptureDevice!.lockForConfiguration()
        try avCaptureDevice!.setTorchModeOn(level: 1.0)
    } catch let error {
        print(error)
    }
    avCaptureDevice!.unlockForConfiguration()
}

参照

developer.apple.com

superhahnah.com

qiita.com

note.com

qiita.com

shiba1014.medium.com

developer.apple.com

developer.apple.com

RxSwiftのSubject・Relayについて

今回はRxSwift(RxCocoa)のSubject・Relayについてまとめようと思います

Rxについては以前記事にしたので読んでみてください!

kumaskun.hatenablog.com

Subject

まずはSubjectについてです

Observableはイベントを検知するためのクラスですが、Subjectと後述するRelayはイベントの検知も、イベントの発生もできるクラスです

SubjectはonNextonErroronCompleteの3種類のイベントを流すことができます。 なので、通信処理やDB処理などでメッセージを出したいなどのエラーによって処理を分けたいときに使用します

Subjectには次の3種類があります

  • PublishSubject: subscribeしてから発生したeventを受け取れる
  • ReplaySubject: subscribe以前に発生したeventも受け取れる。バッファサイズを指定する
  • BehaviorSubject: 一つ過去のeventを受け取れる。購読した際に必ず一度イベントが発生する。初期値が必要

PublishSubjectはイベントが発生したタイミングで、直前の値をストリームに流します。よって、次のようにイベントごとにsubscribe()します。 また、PublishSubjectは初期値を設定する必要がありません

let sub = PublishSubject<String>()
        
sub.subscribe(onNext: { str in
    print(str)
}).disposed(by: disposeBag)
        
sub.onNext("1")
sub.onNext("2")
sub.onNext("3")
sub.onCompleted()

// printされるのは"1"、"2"、"3"

ReplaySubjectは購読直後に引数で指定したbufferSizeの分だけ前の値をストリームに流します。 前の値がない場合はPublishSubjectと同じように振る舞います

let sub = ReplaySubject<String>.create(bufferSize: 2)
        
sub.onNext("1")
sub.onNext("2")
sub.onNext("3")
sub.onNext("4")
sub.onNext("5")
        
sub.subscribe(onNext: { str in
    print(str)
}).disposed(by: disposeBag)

sub.onCompleted()

// printされるのは"4"、"5"

今回はbufferSizeに2を指定することによって、購読直後に2つ前の値をストリームに流す設定になっています。 また、subscribeをonNextよりも前で定義すると「前の値がない場合」に当てはまるので、イベントごとにsubscribe()を行います

BehaviorSubjectはストリームを購読する際に、直前の値を一度流してからsubscribe()します。 つまり、購読した際に必ず一度イベントが発生します

let sub = BehaviorSubject<String>(value: "1")

sub.subscribe(onNext: { str in
    print(str)
}).dispose()

sub.onNext("2")
sub.onNext("3")
sub.onCompleted()

// printされるのは"3"のみ

Relay

次はRelayについてです

RelayもSubjectと同様にイベントの検知も、イベントの発生もできるクラスです。 ただ、Subjectとの違いはonNextのみイベントとして流すことができる点です(Relayはイベントを流す際にはonNextではなくacceptを使用します)。

Relayには、BehaviorRelayPublishRelayがあります。 PublishRelayはPublishSubjectのwrapperで、BehaviorRelayはBehaviorSubjectのwrapperになります。 BehaviorRelayは初期値があり、PublishRelayには初期値がありません。 そして、subscribeしたときにBehaviorRelayは現在値を流し、PublishRelayは現在値を流さないです。

let sub = PublishRelay<String>()
sub.accept("1")
sub.accept("2")
                
sub.subscribe(onNext: { str in
    print(str)
}).disposed(by: disposeBag)
                
sub.accept("3")

// printされるのは"3"のみ(subscribeする前の"1"、"2"は出力されない)

BehaviorRelayは現在の値を取得する際にvalueを使います

PublishRelayの際にはsubscribeで値が取得できていましたが、BehaviorRelayのsubscribeではイベントが流れてきます

let behaviorRelay = BehaviorRelay<String>(value: "1")

behaviorRelay.subscribe{ _ in
    print(behaviorRelay.value)
}.disposed(by: disposeBag)

behaviorRelay.accept("2")
behaviorRelay.accept("3")
behaviorRelay.accept("4")

// printされるのは"1"、"2"、"3"、"4"が出力される

Hot/Coldについての補足

ObservableはHotとColdの2種類の性質があります

HotなObservableとはsubscribeしているObserverがいなくても、値が流れ続けるObservableのことで、 ColdなObservableとはsubscribeしているObserverが発生するまで、値が流れないObservableのことです

これをSubjectやRelayを例にして補足します

let testRelay = PublishRelay<String>()

let cold = testRelay.map { str in
    print("\(str) ここを通る")
}

testRelay.subscribe(onNext: { str in
    print(str)
})

testRelay.accept("Test")

上記の例のPublishRelayはHotなObservableです。 そして、HotなObservableをmapなどのoperatorを使うことでColdなObservableに変換しています。 coldはColdなObservableなのでsubscribeしないとflatMapやmapの中身が評価されることはありません。 なので、"Test"は出力されますが、print("\(str) ここを通る")は出力されません

その上で次のパターンを見てみましょう

let testRelay = PublishRelay<String>()

let cold = testRelay.map { str in
  print(" \(str) ここを通る")
}

testRelay.subscribe(onNext: { str in
  print(str)
})

testRelay.accept("hoge")

cold.subscribe(onNext: { _ in
  print("ここを通る!!")
})

testRelay.accept("fuga")

実行してみるとprintしたものすべてが表示されます

実行結果から、fugaがtestRelayに流れてきたときにcoldにも値が流れていることが確認できます。 hogeでは発火していないのは、値が流れた時点ではObserverが存在しなかったためです。(cold.subscribeが実行されていないため) このようにHotやCold意識しないとsubscribeするタイミングによって挙動が変わり、バグに繋がったケースがあり得るので注意が必要です

参照

reactivex.io

github.com

zenn.dev

Webサイトが表示されるまで

今回はWebサイトが表示されるまでに行われることやネットワークの基礎中の基礎をまとめたいと思います

URLについて

皆さんは、URLを入力してWebサイトに接続すると思います

URLにはアクセスしたいファイルやドメインの情報が含まれていてブラウザはURLを解析して目的のファイルを取得します

例えば、このブログのURLであるhttps://kumaskun.hatenablog.com/entry/2023/01/08/123900を例にみていきましょう!

プロトコル・スキーム

まずはhttps://の部分です。

httpsの部分はプロトコルと呼ばれています。 URLは通信をするときに使うので、「通信プロトコル」とも呼びます

プロトコルはインターネット上で通信をするとき、このルールに従いましょうという取り決めのことを指します。 中でも、通信プロトコルには、次のように目的に応じて様々な種類があります

  • http→ WebサーバーとWebブラウザ間でデータを送受信する
  • https→ httpにデータ暗号化機能を付与したもの
  • mailto→ メール送信先を指定する
  • ftp→ ファイルをダウンロードする

よく使うのはhttphttpsだと思います

httpとは、Hyper Text Transfer Protocolの略で、WebサーバーとWebブラウザがデータを送受信するときに使う通信上のルールのことです。 例えば、Google ChromeWebブラウザからhttpという通信プロトコルを使ってWebサーバーに要求を出すことで、そのページを表示させて利用できるようになります。

httpsHyper Text Transfer Protocol Secureの略で、httpにデータを暗号化する機能がついたものです。 Secure Socket Layer、transport layer securityはそれぞれSSLTLSと省略し、どちらもデータを暗号化して送受信する技術のことを指します。 SSLTLSの技術でデータを暗号化することにより、個人情報などの漏洩を防ぎます。 現在のURLの形式はhttpsがほとんど占めると思います

https://はまとめてスキームと呼ばれています。 前述したプロトコルというルールを規定するものだと理解しましょう。 上図の場合では、このURLはhttpsのルールを使って通信することを表しています

ドメイン

次はドメインについてです。ドメインkumaskun.hatenablog.comの部分です

ドメインは、ホスト名と同じく、IPアドレスを分かりやすくするために設定する文字列のことを指します。 インターネット上の住所となるので、同じドメインは1つしか存在しません。Webサイトを作成する際にはまず、ドメインの取得から行うことが多いと思います

ドメインには、更に細かくトップレベルドメインセカンドレベルドメインサードレベルドメインの3つに分けられます。

トップレベルドメインは、ドメインの一番最後にあるjpcomnetなどです。 特定の領域や分野ごとに割り当てられた分野別トップレベルドメイン(gTLD)と、国別のコードに割り当てられたトップレベルドメインccTLD)に大別されます。 例えば、日本のccTLDは上図の「jp」です。

セカンドレベルドメインは、ドメインの中で2番目に属しているドメインです。(今回はhatenablogの部分) この階層はあくまで順番を表しているため、トップレベルドメインの種類によって、どのようなドメインがセカンドレベルドメインになるかは変わります。

トップレベルドメインがjpの場合は、co(組織)or(非営利法人)など、組織の種類を表すドメインが入ります。

一方、トップレベルドメインが下図のようにcomやnetなどの場合は、サードレベルドメインがセカンドレベルドメインの位置に入るため、サードレベルドメインがないURLもあります

サードレベルドメインは、セカンドレベルドメイン以下に属しているドメインです。 上図の場合、このサードレベルドメインのkumaskunは、重複がない限り登録者が自由に設定できます

IPアドレスとは?

補足情報としてIPアドレスについて軽く触れておこうと思います

IPアドレスは、スマホやPCなど、ネットワーク上の機器に割り当てられるインターネット上の住所のようなものです。 インターネットでページを閲覧したり、メールの送受信を行うには、データの送信元や送信先を識別する必要があります。 そこで、この識別に使うための番号がIPアドレスです。 ネットワーク上でデータを送受信する際、通信相手を指定するために使われています

IPアドレスネットワーク番号ホスト番号から構成されています。 どのサブネット(ハブにコンピューターを接続したもの)に属しているかを示す番号をネットワーク番号、 サブネット内のどのコンピューターからを表す番号をホスト番号と呼びます

IPアドレス32bitのデータで8bitずつドットで区切り10進数で表記します。 例えば、100.12.11.100のようなイメージです

しかし、100.12.11.100だけだとどこまでがネットワーク番号でどこまでがホスト番号がわかりません。 そこで、100.12.11.100/16と書くことで最初の16bitがネットワーク番号になり、残りの16bitがホスト番号とわかるようになります

ホスト名

今回の例のURLにはないのですが、URLにはホスト名というものが存在します

例えば、wwwなどの文字を1度は見たことがあるのではないでしょうか?

ホスト名は、ネットワークに接続されたホスト(機器やサーバー)の名前を指します。 通信する際に、特定のホストを識別するために使います

ホスト名は、後述するドメインとほとんど同じ意味だと捉えて構いません。 人間が管理しやすいように、前述したIPアドレスを文字に置き換えているの文字列が「ホスト名」と「ドメイン」です。

ホスト名もドメインも、どちらも登録者の任意で自由に決めることができます。 ホスト名は、代表的な「www」にするか、自由に設定するか、必要がなければ省略することもできます

ディレクト

ディレクトリは、サーバー内のフォルダ名と位置を表しています。 ホームページの場合、複数のコンテンツを持っていることがほとんどです。 そのコンテンツを分類し、このURLがホームページ内のどの階層にあるページなのかを示しています。

ディレクトリが何層もある場合は、URLの中では一番左の、一番大きなディレクトリより後のものをサブディレクトリといい、「/」で区切ります。 今回の例の場合には2023ディレクトリにあたり、/01/08/はサブディレクトリに相当します

ファイル名

ファイル名は、URLの末尾に配置される一番小さなファイルを表します。 そのURLの中では一番最後の部分です。今回は123900の部分です。 今回の例では、ディレクトリの中の123900に分類されたファイルを開くようなURL構成になっています

リクエストメッセージ

ここまでで、HTTPプロトコルHTTPSプロトコルを使ってどのドメインやどのファイルにリクエストするかがURLを見ればわかるようになったと思います

次にブラウザはサーバーに対してデータをリクエストする旨を伝えるリクエストメッセージを作成します。 リクエストメッセージはHTTPプロトコルに準拠している必要があります

ここからはリクエストメッセージの構成に関して説明します。まずは下記にリクエストとレスポンスのイメージ画像を載せます

MDN Web Docsから抜粋

メソッド

メソッドはWebサーバーにどのような処理をして欲しいのかを記述します。今回の例ではPOSTが該当します。 他にはGETPUTなどがあります。 例えばGETはリソースを取り込むこと、POSTはデータをサーバーへ送信すること (リソースを作成または変更する、あるいは返送する一時的なドキュメントを生成する) ことを示します

HTTPバージョン

どのバージョンのHTTPプロトコルに準拠しているかを定義しています。今回の場合にはHTTP/1.1でバージョン指定を行っています

メッセージヘッダー

メッセージヘッダーはリクエストに対して付加情報を記述したい場合には追加します。上記の図ではHTTP Headersと命名されている箇所です

使用できるリクエストヘッダーは多数あり、いくつかのグループに分類されます

  • 一般ヘッダーは: Via など、メッセージ全体に適用される
  • リクエストヘッダー:User-Agent, Accept-Typeなど指定するとリクエストを変更するもの (Accept-Languageなど)、状況を示すもの (Refererなど)
  • エンティティヘッダー:Content-Lengthなど、リクエストの本文に適用されます。リクエスト内に本文がない場合はこれらのヘッダーが送信されません

メッセージボディー

リクエストの最後の部分が本文です。 本文が存在しないリクエストもあります。 リソースを取り込むリクエストであるGET, HEAD, DELETE, OPTIONSは通常、本文は不要です。 サーバー内のデータを更新するためにデータを送信するリクエストもあり、 POST リクエストでよくあります (HTML フォームのデータを持つ)

本文は、大きく 2 種類に分類されます。

  • 単一リソースの本文。1 個のファイルで構成され、Content-TypeとContent-Lengthの 2 つのヘッダーで定義される
  • 複数リソースの本文。マルチパートの本文で構成され、それぞれが異なる情報を持ちます。これは主に、 HTML フォームと関連付けられる

レスポンスメッセージ

レスポンスメッセージはHTTPリクエストに対するレスポンスです。 少し細かくいうと、ホームページを見るときに使うソフト(Webブラウザ)からホームページのファイルが置いてあるコンピュータ(Webサーバ)に対して出されるリクエストに対するWebサーバさんからの返事です

レスポンスメッセージはリクエストメッセージで表示させているイメージをベースに説明します

ステータスコード

ステータスコードHTTPリクエストが正常に処理されたかを示す数字です

大まかに5種類に分けられます

レスポンスメッセージ

レスポンスの最後の部分が本文です。 本文を持たないレスポンスもあります。

本文は、大きく 3 種類に分類されます。

  • 大きさが判明している 1 個のファイルで構成される、単一リソースの本文。 Content-Type と Content-Length の 2 つのヘッダーで定義される
  • 大きさが不明な 1 個のファイルで構成される、単一リソースの本文。Transfer-Encodingをchunkedに設定して、 chunked 形式でエンコードされる
  • 複数リソースの本文。マルチパートの本文で構成され、それぞれが異なる情報のセクションを持ちます。これは比較的まれ

DNS

ここまででサーバーにリクエストを行い、レスポンスが返却されるという流れは把握したと思います

リクエスト時にデータを送信するためには宛先のIPアドレスを知る必要があります

URLから得られる情報はドメイン名(IPアドレスを人間が理解しやすくしたもの)なので、IPアドレスに変換する必要があります。そこでDNSという仕組みがあります

DNSIPアドレスドメイン名を対応づけします。 例えば、https://kumaskun.hatenablog.com/IPアドレスを問い合わせるとDNSサーバーが100.100.100.100のようにIPアドレスを教えてくれます。 DNSサーバーを使用してIPアドレスを取得するのを名前解決と呼びます

DNSサーバーとresolver

ドメイン名の情報を管理し、外部からの問い合わせに応答するコンピュータやソフトウェアのことをDNS server、サーバへの問い合わせを行いDNS情報を参照・利用する側のコンピュータやソフトウェアをDNS clientあるいはDNS resolverという。 DNS resolverはIPアドレスを問い合わせる側のことです

resolverには以下3つの情報が含まれています。これらの情報をリソースレコードとも呼びます

  1. 名前:サーバーやメールの@以降の情報
  2. クラス:DNSがどのような目的で使用されているかがわかる
  3. タイプ:AタイプとMXタイプがある。Aタイプは名前とIPアドレスが対応づけされている場合でMXタイプは名前とメールサーバーが対応づけされている場合

DNSサーバーは複数存在しています。インターネット上には数枚台というDNSサーバーがありそれぞれ自分が担当するドメインを持っています

実際に問い合わせを行う場合には、ツリー構造でつながれているネットワーク上に配置されたほかのDNSサーバーに対して、問い合わせを受けたDNSサーバーが問い合わせをします。 この問い合わせをするプログラムのことをフルサービスリゾルバといいます。

フルサービスリゾルバは該当のデータが見つかるまでほかの権威DNSサーバーへ問い合わせを繰り返し、引き当たったIPアドレスをスタブリゾルバに返します。 それをスタブリゾルバが端末に返すことで、リクエストを送信した端末がアクセス先のIPアドレスが何であるかを把握し、Webページにアクセスできるという仕組みです。 イメージは下記です

フルサービスリゾルバのイメージ

サーバーからデータを受け取る

ここまでリクエストを送信することに特化して解説していました。 ここではリクエストを受け取った後のことを説明しようと思います

Webサイトが表示するためにはレスポンスを返却する必要があります

ブラウザにはメッセージを送信する機能がないため、OSに送信を依頼します。 OSがメッセージを送信するための通信プロトコル郡をプロトコルスタックと呼びます。 データの送信をしたいときは上のレイヤから下のレイヤへ送信を依頼することで、データ送信をしています

プロトコルスタックの一例(参照のリンク先のもの表示)

HTTPリクエストを送信したい場合は、Socketライブラリを使用してデータを送信します。 データ通信時には受信側/送信側の両方でソケットというデータの出入り口を作成して、それらを接続することで通信を行います。 クライアントがデータを受信するまでの流れは次の4ステップで行われます

  1. アプリケーションは直接ソケットを触ることができないため、Socketライブラリを通してソケットの作成を行います。 ソケットはSocket関数にサーバのIPアドレスを渡すことで作成でき、ソケットの作成が完了するとソケットの識別子であるディスクリプタが返ってきます。 1つのソケットにつき1つのサーバとしか通信が行えないです

  2. サーバのソケットに接続する。 クライアントはソケットを作成した後、サーバ側のソケットへ接続するようプロトコルスタックに依頼します。 サーバに接続する際はSocketライブラリのconnect関数にディスクリプタIPアドレス、ポート番号の3つを渡すことで接続を確立してくれます

  3. データの送受信 接続確立後はソケットにデータを入れるだけで、相手にデータを届けることができます。 データの送受信を行うためにはSocketライブラリのread/write関数を使用します。 ブラウザとOSがデータのやりとりをするために、受信バッファ/送信バッファというものを使用します。 ブラウザは送信したいデータを送信バッファに書き込むことで、OSにデータを渡すことができます。 同様に、OSは受信したデータを受信バッファに書き込むことで、ブラウザにデータを渡すことができます

  4. 接続を切断する データの送受信が終了した場合にはSocketライブラリのclose関数を使うことで、接続を切り、ソケットを抹消することができます

データを受信したら、受け取ったレスポンスメッセージをもとに、受信した内容をブラウザに表示します。 これによりホームページを表示することができます

参照

ferret-plus.com

developer.mozilla.org

www.rworks.jp

sites.google.com

Closuresのドキュメントを見る前に

Swiftのクロージャーについてですが、クロージャを細かく説明するわけではありません。 Swiftのクロージャーの公式ドキュメントを見ると単純に和訳するだけでは理解するのが難しい用語があります(個人的にかもしれませんが・・・)

docs.swift.org

今回はドキュメントを読む前に必要な前提知識を解説しようと思います

Capture

まずは、キャプチャーについてです

キャプチャとはスコープ外の変数などをクロージャ内から操作、参照できる仕様のことです

通常、関数内で定義された変数はその関数外からは参照することができません。 これはその変数のスコープが関数内に制限されており、関数の中で使用済みで不要になった変数は自動で破棄されるからです

func sample() -> Int{
  var localCount = 0
  return localCount
}

//  「Cannot find 'localCount' in scope」というコンパイルエラー
print(localCount) 

しかしクロージャでは関数内に定義した変数を外部から参照・操作できます。 例えば次のような例です

func sample() -> () -> Int {
    var localCount = 0
    let closure = { () -> Int in
        localCount = localCount + 1
        return localCount
    }
  return closure
}

var sample = sample()

// 実行のたびにlocalCountが+1されていく
sample()
sample() 
sample() 

sample関数の返り値はクロージャにしてあります。 これにより中に定義したクロージャをそのまま外部へ持ち出すことができます。 つまり変数sampleに格納されているのは{ () -> Int in localCount = localCount + 1 return localCount }ということになります

クロージャの中ではlocalCountは未定義のはずですが、問題なく実行することができ、さらにクロージャの実行のたびにインクリメントされていきます。 つまりlocalCountは自動で破棄されることなく、メモリに残ったままです

Capture List

キャプチャと関連してキャプチャリストも和訳だけだと理解できないかもしれません

クロージャでキャプチャした変数や定義はメモリと強い参照(強参照)で紐付けられます。 そのため循環参照を引き起こし、メモリが解放されなくなる恐れがあります

循環参照を防ぐために、キャプチャリストを使用して明示的に変数の参照を制御することができます

記述方法は[変数名]形式で引数の前に記述します。 クロージャの省略記法を使用している場合でもキャプチャリストを記述する場合はinが必要になります

参照の強さはweakunownedキーワードを使用して指定します

// 暗黙的な強参照
{ () -> Void in print("TEST") }   

// 暗黙的な強参照
{ print("TEST") } 

// 明示的な強参照
{ [self] in print("TEST") }

// 明示的な弱参照
{ [weak self] in print("TEST") } 

// 明示的な非所有参照
{ [unowned self] in print("TEST") } 

Tuple

次にタプルについてです

タプルとは順序付けされた値の集合体のことで、異なる型の値をひとつの変数、または定数で扱えます

配列や辞書はデータ型に非常に厳格で基本的に同じデータ型しか入らない一方、タプルを使用すればそのままのデータ型で保存できる、つまり各値を使用するときとかにいちいち型変換をしなくてもそのままの形で演算できます

ただし、値の追加、削除、また繰り返し処理などはできません

基本的には次のように定義して、値を取得できます

// タプルの宣言
let tuple = (id: 123, name: "ABC", isFlag: true)

// 値の取得(ラベル指定)
print(tuple.id) // 結果: 123
print(tuple.name) // 結果: ABC
print(tuple.isFlag) // 結果: true

// 値をインデックス番号から取得
print(tuple.0) // 結果: 123
print(tuple.1) // 結果: ABC 
print(tuple.2) // 結果: true

また、下記のように定義することもできます

// タプルの宣言
let (id, name, isFlag) = (123, "ABC", true)

// 値の取得
print(id) // 結果: 123
print(name) // 結果: ABC
print(isFlag) // 結果: true

値の更新は次のように行います。 また、タプルは参照渡しではなく、値渡しです

// タプルの宣言
var tuple = (id: 123, name: "ABC", isFlag: true)

// 値の更新
tuple.id = 456

// 値の取得
print(tuple.id) // 結果: 456

in-out parameters

in-out parametersについてです。 in-outパラメータとは参照渡しを実装できるパラメータです。 つまり、関数の中でその引数に変更を加えた場合、呼び出し側の元の変数にも変更が適用されます

次のようにinoutというキーワードを設定することで参照渡しを実装できるパラメータとなります

func main(type: inout Int) -> Int {
    return result
}

注意点としては、参照を渡す場合は、&を付与します。 そして、引数に直接データを渡すと、その引数は imutableになるのでコンパイルエラーになります

func main(type: inout Int) -> Int {
    return result
}

var type = 1
let result = main(type: &type)

let result = main(type: 1) // NG

inoutパラメータを使うメリットとしては、参照を使用することでメモリコピーによるオーバーヘッドを避けることができます

しかし、inoutパラメータを作るのは、基本的には避けた方がよいでしょう。 意図しないところで値の書き換えが起こるというのが、バグの元になります。 関数から新しいデータを返してもらったほうが、プログラムのロジックを追うのが簡単になるので、良い気がします

最後に

個人的にここで挙げた内容を把握した上で英語ドキュメントを読むと理解しやすく、また理解が深まると思います!!

参照

タプル(tuple)について | Swift入門編 - ウェブプログラミングポータル

www.geeksforgeeks.org