It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【Android】【メモ】ActivityやFragmentのライフサイクルをオブザーバーで受け取る

概要

androidx.lifecycle:2.2.0 からライフサイクルのイベントをオブザーバー経由で受け取れるようになったため実装のメモ

環境

  • targetSdkVersion: 29
  • kotlin: 1.3.72
  • androidx.appcompat:appcompat: 1.1.0

実装

LifecycleEventObserver

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        lifecycle.addObserver(LifecycleEventObserver { source, event ->
            when (event) {
                Lifecycle.Event.ON_CREATE -> {
                    Log.d("MainActivity", "on_create")
                }
                Lifecycle.Event.ON_START -> {
                    Log.d("MainActivity", "on_start")
                }
                Lifecycle.Event.ON_RESUME -> {
                    Log.d("MainActivity", "on_resume")
                }
                Lifecycle.Event.ON_PAUSE -> {
                    Log.d("MainActivity", "on_pause")
                }
                Lifecycle.Event.ON_STOP -> {
                    Log.d("MainActivity", "on_stop")
                }
                Lifecycle.Event.ON_DESTROY -> {
                    Log.d("MainActivity", "on_destroy")
                }
                else -> Log.d("MainActivity", "other")
            }
        })
    }
}

LifecycleOvserver を使った書き方

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        lifecycle.addObserver(object: LifecycleObserver {
            @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
            fun onCreate() {
                Log.d("MainActivity", "onCreate")
            }

            @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
            fun onResume() {
                Log.d("MainActivity", "onResume")
            }

            @OnLifecycleEvent(Lifecycle.Event.ON_START)
            fun onStart() {
                Log.d("MainActivity", "onStart")
            }

            @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
            fun onStop() {
                Log.d("MainActivity", "onStop")
            }

            @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
            fun onPause() {
                Log.d("MainActivity", "onPause")
            }

            @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
            fun onDestroy() {
                Log.d("MainActivity", "onDestroy")
            }

            @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
            fun onAny(source: LifecycleOwner, event: Lifecycle.Event) {
                Log.d("MainActivity", "onAny: $event")
            }
        })
    }
}

androidx.appcompat 自体にLifecycle自体はすでに組み込まれているようで、パッケージのインポートは特に不要なようです。

参考リンク

【Android】DataBinding+LiveData + ViewModelのHello World

概要

前回の記事では、LiveDataViewModelを使った基本的な数字カウントアップの実装を試しました。

inon29.hateblo.jp

今回は、前回のコードをベースにレイアウトにUIコンポーネントを直接紐付ける仕組みである DataBinding を追加してみます。

f:id:inon29:20200516191207p:plain

環境

  • compileSdkVersion: 29
  • Kotlin: v1.3.72
  • lifecycle-viewmodel-ktx: v2.2.0
  • lifecycle-livedata-ktx: v2.2.0

プロジェクトのセットアップ

まずは、DataBindingを使うためのプロジェクトのセットアップをします。
appディレクトリ配下のbuild.gradleに次の設定を追加します。

android {
    ・・・

    dataBinding {
        enabled true
    }
    
    ・・・
}

appディレクトリ配下の build.gradle にdataBindingについてのenable設定を記述します。

画面のレイアウト

前回の構成と同じ、数値を表示するTextViewとカウントアップするためのボタンを用意します。
(xmlについては、前回の記事を参照ください)

ViewModelとLiveDataの準備

ViewModelクラスについても前回の記事と変わりません。
(xmlについては、前回の記事を参照ください)

DataBindingのセットアップ①(レイアウトxml)

xmlで宣言しているレイアウトファイルにViewModelのデータをDataBindingを使って関連付けていきます。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        
        <variable
            name="viewModel"
            type="com.inon.apps.viewmodelsample.CountViewModel"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.text}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Count up"
            android:onClick="@{() -> viewModel.countUp()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

基本的な、画面構成は変わりませんが、ルートのタグが ConstraintLayout ではなく layout に変わっています。
このDataBindingを適用するためのxml構成については、Android Studioの機能で自動生成できます。
(alt + Enterで出てくるメニューから「Convert to data binding layout」を選択する)
詳しくは、Codelabsのサンプルがわかりやすいです。

<data>
    <variable
        name="viewModel"
        type="com.inon.apps.viewmodelsample.CountViewModel"/>
</data>

まずは、data タグを使って作成したViewModelクラスをxml上に宣言します。

<TextView
    ... 
    android:text="@{viewModel.text}"
    ... 

次に、TextViewに対して、ViewModelのプロパティで宣言しているLiveDataのtextを紐付けます。

<Button
    ... 
    android:onClick="@{() -> viewModel.countUp()}"
    ... 

最後に、カウントアップのアクションをButtonに紐付けます。

DataBindingのセットアップ②(Activity)

最後にActivityにDataBindingを使うための準備処理を実装します。

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.inon.apps.viewmodelsample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // NOTE: DataBindingUtilからBindingインスタンスを生成
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        // NOTE: ライフサイクルオーナにActivityを指定しないとデータの管理が始まらない
        binding.lifecycleOwner = this
        val viewModel = ViewModelProvider(this).get(CountViewModel::class.java)
        binding.viewModel = viewModel
    }
}

通常レイアウトを適用するには、 setContentView メソッドを使うかと思いますが、これを DataBindingUtil クラスが提供する setContentView に置き換えます。
このメソッドは戻り値としてDataBindingのインスタンスを返すのですが、ActivityMainBinding という今回作成していないクラスのインスタンスが返っています。
これは、xmlから自動生成されるDataBindingインスタンスです。(activity_main.xmlのためActivityMainBindingになっている)

DataBindingのインスタンスに対して、ライフサイクルのオーナーのインスタンス(Activityのインスタンス)を渡します。
そして、関連付けていたViewModelのインスタンスもDataBindingに紐付けます。

まとめ

DataBindingを使うと、LiveDataやそれに伴うイベントをxmlに宣言できるためActivityのコードは大分スッキリします。
基本的には、LiveData, ViewModel, DataBindingはセットで使うと良さそうです。

参考リンク

【Android】LiveData + ViewModelのHello World

概要

Android Jetpack に含まれる「LiveData」についての入門になります。
Jetpackが発表されて約2年ほど立ちますが、この間ほとんどAndroid開発は触っていなかったため、かなり置いていかれてしまいました。
最新のAndroid開発事情に追いつくためにもまずはLiveData,ViewModel周りから触っていきたいと思います。

HelloWoldとして、ボタンを押下したら画面上の数字カウンターをインクリメントしていくよくあるサンプルをViewModel + LiveDataを使って実装します。

f:id:inon29:20200516191207p:plain

環境

  • compileSdkVersion: 29
  • Kotlin: v1.3.72
  • lifecycle-viewmodel-ktx: v2.2.0
  • lifecycle-livedata-ktx: v2.2.0

プロジェクトのセットアップ

appディレクトリ配下のbuild.gradleに以下を追加する

dependencies {
・・・
    /* ViewModel */
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
    /* LiveData */
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
・・・
}

appディレクトリ配下の build.gradle にViewModelとLiveDataのモジュールインポートを宣言します。
(今までは、lifecycle-extensionsというオールインワンのモジュールがあったようですが、2.2.0でサポートが終わるようなので、今後は個別のモジュールでインポートするのが良さそうです。)

画面のレイアウト

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="count: 0"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Count up"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>

ここは特別な設定などはありません。
カウントを表示するための TextViewとカウントアップのための Button を用意しています。

ViewModelとLiveDataの準備

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class CountViewModel : ViewModel() {
    // NOTE: TextViewに表示するテキスト用のLiveData.
    // 変更可のためMutableLiveDataで作成する
    var text = MutableLiveData<String>()
    // NOTE: カウント管理用の変数
    private var count = 0

    // カウントアップボタンを押下されたときのイベントを受け取るためのメソッド
    fun countUp() {
        count += 1
        text.value = "count $count"
    }
}

ViewModelクラスを継承したカウントアップの値を保持する独自のViewModelを作成します。 TextViewに設定するためのtextを LiveData として用意します。
今回は、変更可能なデータとして扱いたいため MutableLiveData を使用しています。

ViewModelのセットアップ

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // NOTE: ViewModelの生成
        val viewModel = ViewModelProvider(this).get(CountViewModel::class.java)
        // NOTE: LiveDataの値の変更を監視。変更を受け取ったらTextViewに値をセット。
        viewModel.text.observe(this,
            Observer { resource -> findViewById<TextView>(R.id.textView).text = resource }
        )
        // NOTE: ボタンのクリックリスナーの設定
        findViewById<TextView>(R.id.button).setOnClickListener{ viewModel.countUp() }
    }
}

最後にActivityでViewModelを生成します。
ViewModelのインスタンスは、ViewModelPloviderを使って作成します。
ViewModelPloviderには、Activityを引数として渡す必要があります。

生成されたViewModelインスタンスのtext LiveDataに対して、observe メソッドを使うことで値の変更を監視することができます。
ここに渡した Observerクロージャに変更した値が送られるため、その値とTextViewを紐付けることでリアクティブなデータ更新ができるようになります。

まとめ

基本的な使い方としてはシンプルでコードもスッキリしそうです。
LiveData+ViewModelは、基本的にはDataBindingと組み合わせて使うことが多そうなので、そのへんも次回は基本から調べて行こうと思います。

【SwiftUI】フェードアニメーションで画面遷移を行う

概要

SwiftUIで画面遷移をする方法の1つとして「表示フラグを使用して遷移をコントロールする」というものがあります。

詳しくは、以前書いた記事を読んでいただければ幸いです。

inon29.hateblo.jp

この方法を使う場合は、画面遷移のアニメーションは自分で書く必要があります。
今回は画面遷移に使われるようなアニメーションの基礎を理解するためにフェードアニメーションによる画面遷移を実際に書いて試してみました。

環境

  • Swift: 5.2.2
  • Xcode: 11.4.1

遷移元の画面

f:id:inon29:20200516120243p:plain

struct FirstView: View {
    @State var isPresented = false
    var body: some View {
        ZStack {
            VStack {
                Button("Show FadeView") {
                    self.isPresented.toggle()
                }
            }
            // NOTE: Boolのフラグで表示非表示を分ける
            if isPresented {
                FadeView(isPresented: $isPresented)
            }
        }
    }
}

対象の画面を呼び出す元画面の実装です。
フェードアニメーションで表示する画面 FadeViewをボタン押下で表示します。
表示制御は、@StateのBoolの値(isPresented)をボタン押下時に変更することで行っており、isPresentedがtrueなら FadeView を描画するように制御しています。
isPresentedをFadeViewに渡しているのは、画面を閉じる制御をFadeView上で行うためです。
(詳細は後述するFadeViewの実装をみてください)

フェードアニメーションで表示する画面

f:id:inon29:20200516120314p:plain

struct FadeView: View {
    @State var opacity: Double = 0
    @Binding var isPresented: Bool
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            VStack {
                Text("SecondView")
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            Button(action: {
                withAnimation(.linear(duration: 0.3)) {
                    self.isPresented = false
                }
            }) {
                Text("閉じる")
                    .foregroundColor(Color.white)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.blue)
        .opacity(self.opacity)
        .onAppear {
            withAnimation(.linear(duration: 0.3)) {
                // NOTE: opacityを変更する画面再描画に対してアニメーションを行う
                self.opacity = 1.0
            }
        }
    }
}

フェードアニメーションを行う画面(FadeView)の実装です。

左上の閉じるボタンを押下することで画面を閉じることができます

画面表示のフェードインアニメーション

.onAppear {
  withAnimation(.linear(duration: 0.3)) {
    self.opacity = 1.0
  }
}

画面表示のフェードインアニメーションは、画面(ZStack)の onAppear に記述しています。
アニメーションには、SwiftUIでアニメーションを実装するときに便利な関数である withAnimation を使っています。

withAnimation は引数のクロージャ内で発生した画面描画に対してアニメーションを付けてくれる関数です。

上記処理では画面の透明度(opacity)を 0.0 => 1.0 に変更する処理をonAppearで行っており、この処理での再描画にアニメーションをつけることでフェードインアニメーションを適用しています。

withAnimation には、アニメーションの種類を引数として渡すことができます。
渡せるアニメーションの種類には、各種イージングなど様々なものが用意されていますが、今回は linear アニメーションを使用しています。
アニメーションによって実行時間も指定できます。
(今回は0.3秒でアニメーションしています。)

画面非表示のフェードアウトアニメーション

Button(action: {
    withAnimation(.linear(duration: 0.3)) {
        self.isPresented = false
    }
}) {
    Text("閉じる")
        .foregroundColor(Color.white)
}

画面を閉じる時のフェードアウトアニメーションです。
呼び出し画面から渡された isPresented フラグをfalseにすることで親画面が再描画され、FadeViewは非表示になります。
この際に画面表示と同様に withAnimation 関数を使用することでフェードアウトのアニメーションを適用することができます。
アニメーションの種類については、画面表示と同様のため説明は割愛します。

まとめ

SwiftUIでは、それほど複雑なアニメーションでなければシンプルにアニメーションを記述することができます。
フラグで画面遷移を実装することはNavigationLinkなどと比べると手間はかかりますが、アニメーションをカスタマイズできるという面では使い勝手は自由なので面倒くさがらず使っていこうと思います。

【Swift 5.x】クラス/構造体のプロパティ名を取得する

環境

  • Swift: 5.2.2
  • Xcode: 11.4.1

プロパティ名を取得する

for children in Mirror(reflecting: self).children {
    print(children)
}
  • Mirrorという構造体に参照したいオブジェクトや構造体を渡す
  • 各プロパティは、children から参照できる

サンプル

class MyClass {
    var hoge: Int = 0
    var fuga: String = ""
    
    func getProperties() {
        for children in Mirror(reflecting: self).children {
            print(children)
        }
    }
}

struct MyStruct {
    var hoge: Int = 0
    var fuga: String = ""
    
    func getProperties() {
        for children in Mirror(reflecting: self).children {
            print(children)
        }
    }
}

print("MyClass properties")
let c = MyClass()
c.getProperties()

print("MyStruct properties")
let s = MyStruct()
s.getProperties()

出力

MyClass properties
(label: Optional("hoge"), value: 0)
(label: Optional("fuga"), value: "")
MyStruct properties
(label: Optional("hoge"), value: 0)
(label: Optional("fuga"), value: "")
  • プロパティ名は、labelというプロパティで参照できる
  • 値は、valueというプロパティで参照できる

【SwiftUI】@BindingとBinding<型>について

概要

SwiftUIでは、変数を参照型として扱うためのBindingという型があります。
Bindingを扱うためには、@Binding という PropertyWrapperを使うことが多いと思いますが、Binding型 というデータ型と@Bindingの扱いでたまに混乱してしまうことがあります。

そのため、整理のための @Binding と Binding型について自分なりにまとめてみました。

自分が混乱しがちなことでもあり、誤った解釈があるかもしれませんので、お気づきの方がいればご指摘いただければ幸いです。

環境

  • Swift: 5.2.2
  • Xcode: 11.4.1

@Binding

@Bindingは、Binding型を扱うための PropertyWrapper です。
一般的な使い方としては、別のViewに対して参照型ではない値(Bool型など)を渡す場合に使われることが多いかと思います。

例えばモーダルのViewを表示するための .sheet というModifireでは、表示/非表示の制御のために Binding<Bool> の値を使っています。

■ モーダルを呼び出すときのサンプル

import SwiftUI

struct ContentView: View {
    @State var isPresented: Bool = false
    var body: some View {
        VStack {
            Button(action: {
                self.isPresented = true
            }) {
                Text("モーダルを表示")
            }
            .sheet(isPresented: $isPresented) {
                ModalView(isPresented: self.$isPresented)
            }
        }
    }
}

sheet Modifireに渡している isPresented という値が、Binding型の値です。
PropertyWrapperで$ をつけた値は、射影プロパティ と呼ばれますが、 @State射影プロパティ としてBinding型の値を返します。

この例では、ModalView にも $isPresented を渡しています。
このBinding型の値isPresentedを受け取るために ModalViewでは @Binding というPropertyWrapper を使います。

■ モーダルViewのサンプル

struct ModalView: View {
    @Binding var isPresented: Bool
    var body: some View {
        VStack {
            Button(action: {
                self.isPresented = false
            }) {
                Text("モーダルを閉じる")
            }
        }
    }
}

このように、@Binding をつけた変数を持つことでBinding型の値を受け取ることができます。
Binding型として、isPresented を受け取ることで ModalView 内部からも ContentViewが持っている変数を変更できるようになるというのが利点です。
この例では、ModalView内でisPresentedをfalseに変更して画面を閉じる処理を実装しています。

要するに @Binding は、指定のプロパティの型を Binding<型> として受け取るため使われる PropertyWrapperです。

@BindingのプロパティにBinding型の変数を格納する

@Binding は、Binding型を扱うためのPropertyWrapperですが、このプロパティに Binding型の変数を格納するにはどうすればいいのでしょうか?

上記の例では、struct が自動生成するイニシャライザに代入処理を任せているため、意識していないですが、自分でイニシャライザを作る場合は次のように記述します。

init(isPresented: Binding<Bool>) {
    self._isPresented = isPresented
}

受け取る引数は、Bindig型の変数で宣言して、プロパティに代入するときは、 _isPresented というプロパティ名に アンダースコア(_)をつけたものに代入しています。

init(isPresented: Binding<Bool>) {
    self.isPresented = isPresented
}

このように書いてしまうと Cannot assign value of type 'Binding<Bool>' to type 'Bool' というコンパイルエラーになってしまいます。
isPrerentedのプロパティは、Boo型なのでBinding<Bool> とは型が異なるためです。

このように PropertyWrapperを定義したプロパティに(_)をつけると PropertyWrapperの内部変数にアクセスできます

つまり @Binding var isPresented: Bool の場合は、@Bindigが管理してくれている Binding<Bool> にアクセスするために(_)をつける必要があります。

余談ですが、$ をつけると射影プロパティにアクセスできますが、これは内部変数(_をつけた変数)から projectedValue というプロパティへアクセスするのと等価になります。

以下の構文は等価です。

_isPresented.projectedValue
$isPresented

@Bindingを使わない場合の例

先程のModalViewのサンプルコードを、@Bindingを使わずにBinding型の変数を使って実装することも可能です。

struct ModalView: View {
    var isPresented: Binding<Bool>
    init(isPresented: Binding<Bool>) {
        self.isPresented = isPresented
    }
    var body: some View {
        VStack {
            Button(action: {
                self.isPresented.wrappedValue = false
            }) {
                Text("モーダルを閉じる")
            }
        }
    }
}

この場合のイニシャライザは、引数とプロパティの方はともにBinding<Bool>のため直接代入できます。
異なる点は、「値の更新方法」でBinding型から内包している値へアクセスするために wrappedValue というプロパティを使います。

@Bindingを使って値を更新する場合

@Binding var isPresented: Bool
...
self.isPresented = false

Binding<変数>を使って値を更新する場合

var isPresented: Binding<Bool>
...
self.isPresented.wrappedValue = false

このように、Binding型を直接プロパティとして持つことは可能です。
しかし、実践上は @Bindingを使ことでBindingされていないデータ型と同じ記述で参照/更新を行えるため、特別な理由がない限りは@Bindingで受け取るのが良いかと思います。

Binding型のデータの値変更の注意点

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper @dynamicMemberLookup public struct Binding<Value> {
  ...
    /// The value referenced by the binding. Assignments to the value
    /// will be immediately visible on reading (assuming the binding
    /// represents a mutable location), but the view changes they cause
    /// may be processed asynchronously to the assignment.
    public var wrappedValue: Value { get nonmutating set }
  ...
}

Binding型で内部の値を変更するには、wrappedValueを使いますが、wrappedValueの定義には上記のような記載があります。
このコメントによると、wrappedValueへの代入による変更は 非同期で行われる場合がある とのことです。 nomutating がセッターについていることからも直接代入により値を変更するわけではないようです。

簡単なサンプルで確認してみます。

struct ContentView: View {
    @State var num: Int = 1
    var body: some View {
        VStack {
            SecondView(num1: $num)
        }
    }
}

struct SecondView: View {
    @Binding var num1: Int

    var body: some View {
        return VStack {
            Button(action: {
                self.num1 += 1
                print(self.$num1.projectedValue)
                print(self.num1)
            }) {
                Text("count up")
            }
        }
    }
}

このサンプルでは、Binding<Int> 型の値をSecondViewで受け取り、ボタン操作によって値を増加させています。

print(self.num1) ではゲッターにより値を直接参照していて、print(self.$num1.projectedValue)Binding<Int> オブジェクトの中身を表示しています。

このボタンを押下するとコンソールは以下のように出力されます。

■ print(self.num1)

2

■ print(self.$num1.projectedValue)

Binding<Int>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.LocationBox<SwiftUI.Binding<Swift.Int>.(unknown context at $7fff2c9b3008).ScopedLocation>, _value: 1)

getter自体は即時2を返していることがわかります。
しかし、Bindingオブジェクトの_valueは1のままのようです。
これを見ると、値セット時に直接値を変更しているわけではないということが確認できます。

【SwiftUI】SwiftUIでQRコードを表示する

概要

SwiftUIでQRコードを表示する必要があり、調べた内容になります。
UIKitと全く同じだと若干ハマりどころもあるため、備忘録として残しておきます。

環境

  • Swift: 5.2.2
  • Xcode: 11.4.1

QRCodeの画像を作成

import CoreImage
import UIKit

class QRCode {
    static func makeQRImage(_ input: String) -> UIImage {
        let inputData = input.data(using: .utf8)!
        // NOTE:誤り訂正レベルはとりあえず「Q」を指定
        let qrFilter = CIFilter(name: "CIQRCodeGenerator",
                          parameters: ["inputMessage": inputData,
                                       "inputCorrectionLevel": "Q"])
        let ciImage = qrFilter!.outputImage!
        // NOTE: 元のCIImageは小さいので任意のサイズに拡大
        let sizeTransform = CGAffineTransform(scaleX: 10, y: 10)
        let scaledCiImage = ciImage.transformed(by: sizeTransform)
        // NOTE: CIImageをそのまま変換するとImageで表示されないため一度CGImageに変換してからUIImageに変換する
        let context = CIContext()
        let qrCgImage = context.createCGImage(scaledCiImage, from: scaledCiImage.extent)!
        return UIImage(cgImage: qrCgImage)
    }
}

まず、QRコードを作成します。(簡単のため、エラー処理などは除外しています)

SwiftでQRコードを作成するには、CoreImage のCIFilerを使います。

let qr = CIFilter(name: "CIQRCodeGenerator",
                  parameters: ["inputMessage": inputData,
                               "inputCorrectionLevel": "Q"])

CiFilterに CIQRCodeGenerator という「name」を渡すことでQRコードのCIImageを作成することができます。
「parameters」には、inputMessage にQRCodeに変換したい文字列を Data 型のオブジェクトとしてわたします。
inputCorrectionLevelは、「誤り訂正レベル」というQRコードの仕様で「L」、「M」、「Q」、「H」のいずれかを指定します。

let ciImage = qrFilter!.outputImage!
// NOTE: 元のCIImageは小さいので任意のサイズに拡大
let sizeTransform = CGAffineTransform(scaleX: 10, y: 10)
let scaledCiImage = ciImage.transformed(by: sizeTransform)

作成したCIFilterからCIImageを取り出し、拡大処理をしています。
(そのままのCIImageは、かなりサイズが小さいので10倍位で300pt前後になると思います)

let context = CIContext()
let qrCgImage = context.createCGImage(scaledCiImage, from: scaledCiImage.extent)!
return UIImage(cgImage: qrCgImage)

最後に作成したCIImage を一旦 CGImage に変換して UIImageに変換します。

ここが、UIKitの場合と異なる箇所です。
UIKitを使う場合、画像の表示には UIImageView を使うのが一般的ですが、UIImageViewはCIImageから変換したUIImageを渡すだけで描画することができます。

しかし、SwiftUIの Image コンポーネントは、CIImageから変換したUIImageだと正しく描画することができません。(エラーにならないが描画されない)

(詳細な理由は理解できていないのですが、 SwiftUIのImageのイニシャライザに、UIImageとCIImageはあるがCGImageがないので描画方式に違いがあるのかもしれません)

そのため、一旦CGImageに変換したものを再度UIImageに変換します。

(前述の通り、Image コンポーネントは、直接 CGImage を受け取るイニシャライザをもっているため、UIImageに変換せず、CGImageを返すだけでも問題ありません。)

View側の処理

struct ContentView: View {
    var body: some View {
        VStack {
            Image(uiImage: QRCode.makeQRImage("test"))
        }
    }
}

View側は Image コンポーネントに作成した UIImageを渡すことでQRコードを表示できます。

所感

微妙なハマりどころですが、UIKitからコード移植する場合などは注意が必要かもしれません。
この辺のノウハウがもう少し世の中に公開されてくるとSwifUIの普及も進んでくるような気がします。