しめ鯖日記

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

GitHub Copilotを使って自動コード補完

GitHub CopilotというAIが自動でコード補完してくれるサービスを試してみました。

github.com

Xcodeは公式未対応なので、下のプラグインを利用しました。
サードパーティーアプリなので入れる際は会社のセキュリティーポリシー上問題ないかご確認下さい。

github.com

まずはGitHub Copilotを有効にします。
有料ですが無料トライアル期間もあります。

次にCopilot for Xcodeをインストールします。
インストールはBrewを使いました。

brew install --cask copilot-for-xcode

インストールしたらCopilot for Xcodeアプリを立ち上げます。
Node.jsを使うのでインストールしていない場合はインストールします。
インストールをしたらPath to NodeにNode.jsのパスを入れます。

次に設定アプリの「プライバシーとセキュリティー」の「拡張機能」の「Xcode Source Editor」でCopilotを有効にします。

再度Copilot for Xcodeを開き直すと下のような表示になるのでSign inからGitHubにログインします。
もしうまく行かない場合、Xcodeを閉じる・Copilot for Xcodeを開き直すなどの対応をします。

下のように許可を求める表示が出るので許可を押します。

Copilot for XcodeでRefreshを押すと下のようにログイン済み表示になります。

これで設定は完了です。
次はXcodeで実際に使ってみます。

新規プロジェクトを作成して下のようなコードを書きます。

class ViewController: UIViewController {
    func readFile() {
        
    }
}

readFileメソッドにフォーカスをあてた状態でEditorのCopilotのGet Suggestionsを押します。

数秒待つとXcode右下に下のようなコード候補がでます。

許可を押すと下のようにコードが展開されます。
今回はDocumentフォルダのtest.txtファイルの文字を読み込むコードがサジェストされたようです。

class ViewController: UIViewController {
    func readFile() {
        let fileManager = FileManager.default
        let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let fileURL = documentDirectory.appendingPathComponent("test.txt")
        do {
            let text = try String(contentsOf: fileURL, encoding: .utf8)
            print(text)
        } catch {
            print(error)
        }
    }
}

ViewControllerのreadFileの1行上にフォーカスをあてたら下のようにviewDidLoadメソッドを提案されました。

signInWithTwitterという利用頻度が低そうなメソッドの中で使ったら下のようにコメントだけサジェストされました。
もしかするとSwiftではsignInWithTwitterというメソッドがGitHub上になかったからかもしれません。

class ViewController: UIViewController {
    func signInWithTwitter() {
        // ...
    }
}

使ってみた感想ですがよく使われる機能のサンプルコードを探したりするのには便利そうでした。
プラグインがまだうまく動かない事もあったりするので、公式で対応されたらまた試してみようと思います。
それとコメントを入れる事でサジェストが改善されるようなのでこれも今後試していきたいと思います。

参考URL

【Xcode】XcodeでGitHub Copilotを使ってみた - Qiita

GitHub Copilot にいいコードを書いてもらう方法 - GMOインターネットグループ グループ研究開発本部(次世代システム研究室)

【Kotlin】Androidでファイルアプリへの保存・取得を試す

Androidでファイルへの保存・取得を試してみました。

ファイルへの保存するために、まずはregisterForActivityResultでActivityResultLauncherインスタンスを作ります。

val  writeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    if (result.resultCode != Activity.RESULT_OK)
        return@registerForActivityResult

    result.data?.data?.let { uri ->
        // 保存処理
    }
}

次に下のようにIntentを作ってwriteResultLauncherのlaunchメソッドに渡します。

val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
    addCategory(Intent.CATEGORY_OPENABLE)
    type = "text/plain"
    putExtra(Intent.EXTRA_TITLE, "ファイル名")
}
writeResultLauncher.launch(intent)

そうするとregisterForActivityResultのコールバックが呼ばれます。
その時、保存先URLが渡されるのでそこにデータを保存します。

val writeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    if (result.resultCode != Activity.RESULT_OK)
        return@registerForActivityResult

    result.data?.data?.let { uri ->
        contentResolver.openOutputStream(uri)?.use { outputStream ->
            outputStream.write("テスト".toByteArray())
        }
    }
}

ファイルの読み込みもregisterForActivityResultを使います。
今度は読み込んだファイルのURLが渡されるので下のように読み込みます。

val readResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    if (result.resultCode != Activity.RESULT_OK)
        return@registerForActivityResult

    result.data?.data?.let { uri ->
        BufferedReader(InputStreamReader(contentResolver.openInputStream(uri)))?.use { reader ->
            val stringBuilder = StringBuilder()
            var line = reader.readLine()
            while (line != null) {
                stringBuilder.append(line)
                line = reader.readLine()
            }
            Log.i("Test", stringBuilder.toString())
        }
    }
}

Intentを渡す方法は下の通りです。
今回は読み込みなのでIntentの初期化の引数をACTION_OPEN_DOCUMENTにしています。

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
    addCategory(Intent.CATEGORY_OPENABLE)
    type = "text/plain"
}
readResultLauncher.launch(intent)

Intentを渡すと下のようにファイルを選ぶ画面に遷移できます。

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)
}