しめ鯖日記

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

AndroidのXMLの?attrについて調べる

Androidアプリ開発XMLを編集している時、@color/blackのような@を使う形式以外に?attrという?を使う形式があります。
今回はこの?について調べてみました。

?attrですが、こちらはThemeの属性になります。

<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?android:attr/statusBarColor"
    app:popupTheme="@style/Theme.MyApplication.PopupOverlay" />

属性はThemeで下のように定義します。
Theme毎に違う値をセットできるので、動的にThemeを変更する時などに使うと便利そうです。

<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="android:statusBarColor">#888</item>
    </style>
</resources

もしthemeの属性を増やしたい場合res/values/attrs.xmlで下のように定義します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyAttrs">
        <attr name="customColor" format="color" />
        <attr name="customValue" format="integer" />
    </declare-styleable>
</resources>

定義する事で下のように利用できるようになります。

<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    <item name="customColor">#fff</item>
</style>

ViewModelProviderでViewModelを呼び出す

ViewModelですがbyを使わずにViewModelProviderで呼び出せるようなので試してみました。

まずはViewModelを使うためのライブラリをインストールします。

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.5'

ViewModelは下のように定義します。

class MyViewModel: ViewModel() {
    var value = 0
}

Activity内でのViewModelの取得方法は下の通りです。
byを使って呼び出す事でActivityが同じものであればViewModelの同じインスタンスが返ってきます。

val viewModel: MyViewModel by viewModels()

ViewModelProviderを使う場合、下のように記述します。
条件によってclass名が変わる場合などはこちらの方が使いやすそうです。

val viewModel = ViewModelProvider(this)[MyViewModel::class.java]

SwiftのExpressibleByStringLiteralについて調べる

SwiftではExpressibleByStringLiteralに準拠させた構造体は文字列リテラルで初期化をする事が可能です。

struct MyString: ExpressibleByStringLiteral {
    let body: String
    
    init(stringLiteral value: String) {
        body = value
    }
}

初期化処理は下の通りです。

let myString: MyString = "Hello"
print(myString) // → MyString(body: "Hello")

()で数字などを埋め込むためにはExpressibleByStringInterpolationに準拠する必要があります。

struct MyString: ExpressibleByStringInterpolation {
    let body: String
    
    init(stringLiteral value: String) {
        body = value
    }
}

let myString: MyString = "Hello \(123)"

数字や文字列などを埋め込みですがStringInterpolationを拡張する事でラベル付きで行う事が可能です。

extension String.StringInterpolation {
    mutating func appendInterpolation(reversed value: String) {
        self.appendInterpolation(String(value.reversed()))
    }
}

let myString: MyString = "Hello \(reversed: "abc")"
print(myString) // → MyString(body: "Hello cba")

下のように文字列以外を埋め込む事も可能です。

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: Bool) {
        self.appendInterpolation(value ? "1" : "0")
    }
}

SwiftUIのTextに画像やTextを埋め込める機能もこの仕組みを利用しています。

struct ContentView: View {
    var body: some View {
        Text("Hello, world! \(Text("BOLD").bold()) \(Image(systemName: "globe"))")
    }
}

アプリを起動すると以下のようにTextや画像が埋め込まれた表示になります。

展開できている理由ですが、Textの引数がStringではなくLocalizedStringKeyというExpressibleByStringInterpolationに準拠した構造体になっているためです。
そのため下のように明示的にStringを渡すとTextや画像が期待通り展開されません。

Text("Hello, world! \(Text("BOLD").bold()) \(Image(systemName: "globe")) \(false)" as String)

ただLocalizedStringKeyはBoolなどの埋め込みには対応していない為、埋め込もうとすると下のようなエラーになります。

Text("Hello, world! \(Text("BOLD").bold()) \(Image(systemName: "globe")) \(false)")
// → Instance method 'appendInterpolation(_:formatter:)' requires that 'Bool' inherit from 'NSObject' というエラー

LocalizedStringKeyをBoolに対応させたい場合、下のようにLocalizedStringKey.StringInterpolationにappendInterpolationメソッドを追加します。

extension LocalizedStringKey.StringInterpolation {
    mutating func appendInterpolation(_ value: Bool) {
        self.appendInterpolation(value ? "1" : "0")
    }
}

起動すると下のような表示になります。

参考URL

[Swift] SwiftUIの不思議な機能を実現する変数展開"\()"について調べた - Qiita

Swift 5 で String Interpolation をカスタマイズする - Qiita

Android Studioでブレークポイントを使ってデバッグをする

Android Studioブレークポイントを使ってデバッグをしてみました。

ブレークポイントはコードの左側をクリックする事でセットする事ができます。

ブレークポイントを有効にするには、いつもの実行ボタンではなくデバッグボタン(虫マーク)でアプリを起動する必要があります。

ブレークポイントの場所を通ると処理が止まり下のようにデバッグ画面が表示されます。
ここではスタックトレースや変数一覧を見る事ができます。

各変数をしっかり見たい場合、デバッグ画面の「Evaluate Expression」を押します。

ボタンを押すと下のようなウィンドウが出ます。
ウィンドウ上部の入力欄に変数名を入れればその変数の情報が出てきます。

ブロックの中などで使うと下のようなエラーが出ることがあります。

その場合、その変数をローカル変数に入れておけば中身を見る事ができます。

binding.fab.setOnClickListener { view ->
    val t = this@MainActivity
    Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
        .setAction("Action", null).show()
}

ブレークポイントを使うと、その行に差し掛かった時に特定のログを出すという事も可能です。

ログの設定はRunのView Breakpointsから行います。

Breakpoints画面は下のとおりです。
左メニューでログを出したいブレークポイントを選んでLogの部分に表示したいメッセージを入れます。

実行すると下のようにメッセージが表示されます。

KotlinのListenerの省略形について調べる

KotlinではOnClickListenerなどをListenerを省略(SAM変換)して書く事ができます。
今回は省略できる条件について調べてみました。

SAM変換可能なListenerは下のようにメソッドが1つである必要があります。
それとinterfaceの先頭にfunを付ける必要もあります。

fun interface MyListener {
    fun test()
}

上記のようなInterfaceの場合、下のようにsetListenerの引数を簡素化できます。

fun setListener(listener: MyListener) {}

setListener {
}

省略しない場合は下のように書く必要があります。

setListener(object : MyListener {
    override fun test() {
    }
})

下のようにメソッドが2つある場合は省略する事ができません。

fun interface MyListener {
    fun test()
    fun test2()
}

Listenerのメソッドの引数が1つだけの場合、下のように引数を省略する事も可能です。
引数が2つ以上の場合は省略する事ができません。

setListener {
    Log.i("test", "$it")
}

fun interface MyListener {
    fun test(value: Int)
}

構成プロファイル(拡張子: mobileconfig)について調べてみる

格安SIMの初期設定時などによく使う構成プロファイル(拡張子: mobileconfig)について調べてみました。

構成プロファイルはMacApple Configuratorというアプリで作成します。

Apple Configuratorをダウンロードして開くと下のような表示になっています。

プロファイルはメニューの「ファイル」の「新規プロファイル」から作成します。

新規プロファイルを選ぶと下の画面に移動します。
この画面で色々な設定を行っていきます。

格安SIMで使われるモバイル通信以外にも、ドメインの監視・カレンダーの追加など様々な事を行う事が可能です。
一部設定は監視対象端末にしか適用できないので注意が必要です。

今回はWEBクリップを使ってホーム画面にアイコンを追加します。
設定は下の通りです。

アイコンは下URLのものを利用しました。

動物 | FLAT ICON DESIGN -フラットアイコンデザイン-

あとはメニューバーの保存を押せばmobileconfigファイルを保存できます。

mobileconfigですが実態は下のようなXMLファイルです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>FullScreen</key>
            <false/>
            <key>Icon</key>
            <data>
            iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAAAXNS
            省略…
            gOYGkdeNgH5AN0fo+SdwALUIFMO12gNvAAAAAElFTkSuQmCC
            </data>
            <key>IgnoreManifestScope</key>
            <false/>
            <key>IsRemovable</key>
            <true/>
            <key>Label</key>
            <string>WEBさいと</string>
            <key>PayloadDescription</key>
            <string>Webクリップの設定を構成します</string>
            <key>PayloadDisplayName</key>
            <string>Webクリップ</string>
            <key>PayloadIdentifier</key>
            <string>com.apple.webClip.managed.XXXXXXXX</string>
            <key>PayloadType</key>
            <string>com.apple.webClip.managed</string>
            <key>PayloadUUID</key>
            <string>XXXXXXXX</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
            <key>Precomposed</key>
            <false/>
            <key>URL</key>
            <string>https://google.co.jp</string>
        </dict>
    </array>
    <key>PayloadDisplayName</key>
    <string>名称未設定</string>
    <key>PayloadIdentifier</key>
    <string>xxxxx</string>
    <key>PayloadRemovalDisallowed</key>
    <false/>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadUUID</key>
    <string>XXXXXXX</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
</dict>
</plist>

mobileconfigファイルを作ったらどこかにアップロードしてiPhoneにダウンロードします。
ダウンロード後は設定アプリからプロファイルのインストールを行います。

インストールするとホーム画面に先程作ったアイコンが表示されます。
タップすると先程設定したURLに遷移します。

プロファイルを削除すれば先程作ったアイコンも消えます。

ActionProviderでサブメニューを表示する

ActionProviderというクラスを使ってAndroidアプリのメニューにサブメニューを入れてみました。

Activityでメニューを表示して動かするためには下のようなメソッドを実装します。

class MainActivity : AppCompatActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
    }
}

onCreateOptionsMenuで呼び出しているmenu_main.xmlは下のようになっています。

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.myapplication.MainActivity">
    <item
        android:id="@+id/action_settings"
        android:orderInCategory="100"
        android:title="@string/action_settings"
        app:showAsAction="never" />
</menu>

アプリを起動すると右上にメニュー(…表示)が出ます。
ここをタップするとmenu_main.xmlの内容が表示されます。

今回はActionProviderを使ってメニューのサブメニュー部分を作ってみました。
まずはサブメニュー用にmenu_test.xmlを作成します。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:title="Menu1" />
    <item android:title="Menu2" />
    <item android:title="Menu3" />
</menu>

次にActionProviderを継承したクラスを作ります。
hasSubMenuとonPrepareSubMenuでサブメニューの設定をしています。
onPerformDefaultActionはonOptionsItemSelectedが実装されていない時に呼ばれるメソッドです。

class MyActionProvider(private val context: Context): ActionProvider(context) {
    override fun onCreateActionView(): View {
        return View(context)
    }

    override fun onPerformDefaultAction(): Boolean {
        Log.e("Test", "test")
        return super.onPerformDefaultAction()
    }

    override fun hasSubMenu(): Boolean {
        return true
    }

    override fun onPrepareSubMenu(subMenu: SubMenu) {
        subMenu.clear()
        MenuInflater(context).inflate(R.menu.menu_test, subMenu)
    }
}

最後にmenu_main.xmlでactionProviderClassを使ってActionProviderを設定します。

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.myapplication.MainActivity">
    <item
        android:id="@+id/action_settings"
        android:title="@string/action_settings"
        app:actionProviderClass="com.example.myapplication.MyActionProvider" />
</menu>

これでメニューを選んだ時、サブメニューが出るようになりました。