【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
概要
前回の記事では、LiveData
とViewModel
を使った基本的な数字カウントアップの実装を試しました。
今回は、前回のコードをベースにレイアウトにUIコンポーネントを直接紐付ける仕組みである DataBinding
を追加してみます。
環境
- 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を使って実装します。
環境
- 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つとして「表示フラグを使用して遷移をコントロールする」というものがあります。
詳しくは、以前書いた記事を読んでいただければ幸いです。
この方法を使う場合は、画面遷移のアニメーションは自分で書く必要があります。
今回は画面遷移に使われるようなアニメーションの基礎を理解するためにフェードアニメーションによる画面遷移を実際に書いて試してみました。
環境
- Swift: 5.2.2
- Xcode: 11.4.1
遷移元の画面
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の実装をみてください)
フェードアニメーションで表示する画面
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の普及も進んでくるような気がします。