文字っぽいの。

文字を書いています。写真も混ざります。

`WKUserContentController.add(_:name:)` はちゃんと解放しないとメモリリークする。

開発環境

背景

WKWebViewを使っていると、JavaScriptとSwiftでやり取りをしたいことがあります。そんな時に調べていると、こういうサンプルコードを良く見ます。

final class SomeViewController: UIViewController {
    @IBOutlet weak var webView: WKWebView! {
        didSet {
            webView.configuration.userContentController.add(self, name: "EventName")
        }
    }
}

extension SomeViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "EventName" {
            // JS側で `webkit.messageHandlers.EventName.postMessage(message);` を呼ぶとここが呼ばれる。
        }
    }
}

課題

上記コードの

webView.configuration.userContentController.add(self, name: "EventName")

では self を渡していますがこの self によって循環参照が発生して、メモリリークします。そのため、SomeViewControllerの画面を閉じたとしてもメモリが解放されず、SomeViewControllerを開けば開くほどメモリを食いつぶしていきます。

解決方法1

使い終わった際にはちゃんと解放してあげましょう。

webView.configuration.userContentController.removeScriptMessageHandler(forName: "EventName")

JavaScriptとSwiftの連携が不要になったタイミングや、viewDidDisappearなどで呼んでおくと良さそうです。

解決方法2

解決方法1では、大量にJSのEvent登録をしたり、removeScriptMessageHandler を呼ぶタイミングが難しいときに扱いが困難になります。そこで、渡した self がweakになるように間に1つclassをかませてあげます。

class WKScriptMessageHandlerWithWeakReference: NSObject, WKScriptMessageHandler {
    private weak var delegate: WKScriptMessageHandler?

    init(delegate: WKScriptMessageHandler) {
        self.delegate = delegate
        super.init()
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        delegate?.userContentController(userContentController, didReceive: message)
    }
}

使う時はこんな感じ

wkWebView.configuration.userContentController.add(WKScriptMessageHandlerWithWeakReference(self), name: "EventName")

これによってわざわざ removeScriptMessageHandler を呼ばなくても良くなります。

感想

昔のNotificationCenterやKVOを思い出しました。

参考文献

WKWebViewを利用した実装でメモリリークしやすいパターン2つ - Qiita

HHKB HYBRID Type-S 雪を "2枚" 買った。

キーボードを2枚使うとタイピング時の姿勢が良くなり、肩がこりにくくなる。つまり健康に良い。これは分割型のキーボードや自作キーボードが流行っていることからも分かる。

ならばHHKBを2枚使えば良いじゃないか。HHKBは最も使いやすいキーボードであり、最も打鍵感が良いキーボードである。

そんなわけで、以前まで有線式のHHKB Pro2を利用していた。

これでも十分な環境であったが、どうしても「有線である」というのが邪魔になってしまった。自分は仕事はMacを利用しているが、Windows機でFPSゲームをすることがある。FPSをするときにはHHKBの様な背の高いキーボードよりも、ロープロファイルなキーボードの方がプレイしやすい。そのため、ゲームをするたびにHHKBのケーブルを外してデスクをWindows仕様に変更し、ゲームを終えたらまた元の状態に戻している。これがなかなかに面倒で、特にケーブルの抜き差しが面倒なのである。

この課題を解決するためにHHKB HYBRID Type-S 雪を2台買った。

実際に配置してみた様子。パームレストFILCOなのは許して欲しい。

HYBRIDなら有線でもブルートゥースでも接続できるので前述した課題が解決できる。雪にしたのは単純にデザインがかわいかったから。印字が中央にあるのがとても良い。

HHKBを2枚使うメリット

分割式キーボードを買えばよいのでは?と思うかも知れないが、HHKB2枚使いには下記のようなメリットがある。

  • 入手が非常に容易
    • Amazonでも公式サイトでもいつでも購入できる
    • 自作系のキーボードとは異なり流通が安定しているのは壊れた時に安心(まぁ壊れないんだけど)
  • 壊れない
    • 以前まで利用していた有線式のHHKBの片方は大学院生の頃に購入したもので、まだ全然問題なく動く
    • ただしUSBの接続口だけは抜き差しで若干接続が悪くなってしまった
    • 自作キーボードのようにハンダ不良も発生しない
  • キー配列で困らない
    • 普通のキーボードが左右にあるだけなので、どんなホームポジションや運指でも安心
    • 分割式キーボードだと完璧なホームポジションと運指でないと、空中をタイピングしてしまう
  • 左右分割に飽きても安心
    • HHKBが2つ手に入るだけなので、もし飽きてしまっても売却したり、オフィス用と自宅用にすれば良い
    • 最初から分割式のものを購入してしまうと片方だけでは使えないので困ってしまう

HHKBを2枚使うデメリット

値段が高い!!!!!

が、この環境を手に入れてさえしまえば、働けなくなるまでこの環境で良くなる。今回購入したHHKB HYBRID Type-S 2枚は、自分が定年を迎えるまで使い続けることになるだろう。

また、この環境を手に入れるとキーボードを衝動買いしなくて良くなる。どのようなキーボードを購入したとしても、この環境を上回ることは無いのだ(自分の中では)。

まとめ

HHKBを2枚使うのは良いぞ。

今回購入したのは、英字配列のこれ。

日本語配列はこちら。


HHKBのここが好き!

HHKBのここが好き!
by Happy Hacking Keyboard - 一度使ったら戻れない極上のキータッチ | PFU

SwiftUIでTabキー単体のキーショートカットを実装する方法。

SwiftUIでのキーボードショートカット

SwiftUIではButtonやToggleに対して .keyboardShortcut(_:modifiers:) でキーボードショートカットを実装することができる。

Toggle("Toggle", isOn: $flag)
    .keyboardShortcut("1", modifiers: [.control])

Button("Button") {
    print("Control+2")
}
.keyboardShortcut("2", modifiers: [.control])

タブ単体では動かない

エディタアプリを作っているとTabキーで \t を入力させずに、インデント処理を行わせたい場合がある。そのため、上述したノリで、

Button("Button") {
    print("Tab")
}
.keyboardShortcut(.tab, modifiers: [])

と実装してみるが動かない。ちなみに、modifiers に色々と入れられるが、試してみて動いたのは Option + Tab の組み合わせのみだった。

解決方法

色々試したが、UITextViewUIViewRepresentable で利用する方法になった。エディタアプリを作る時にはSwiftUIの TextEditor では機能不足で、 UITextView を利用することが多いのでちょうどよいといえばちょうどよい。

まず、 keyCommands を登録したCustom UITextViewを用意する。

protocol MyTextViewDelegate: AnyObject {
    func myTextViewDidInputTab()
}

final class MyTextView: UITextView {
    override var keyCommands: [UIKeyCommand]? {
        let command = UIKeyCommand(title: "Shortcut Title", action: #selector(keyCommandTab(_:)), input: "\t", modifierFlags: [])

        // iOS 15以降ではこれを設定しないとEditor側への入力が優先される
        // ref: https://developer.apple.com/documentation/uikit/uikeycommand/3780513-wantspriorityoversystembehavior
        command.wantsPriorityOverSystemBehavior = true

        return [
            command
        ]
    }

    weak var myDelegate: MyTextViewDelegate?

    @objc func keyCommandTab(_ sender: UIKeyCommand) {
        myDelegate?.myTextViewDidInputTab()
    }
}

次にこれをSwiftUIで利用できるようにする。

struct TextView: UIViewRepresentable {
    private let onInputCommandTab = PassthroughSubject<Void, Never>()

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    func makeUIView(context: Context) -> MyTextView {
        let view = MyTextView()
        view.myDelegate = context.coordinator

        return view
    }

    func updateUIView(_ uiView: MyTextView, context: Context) {
        context.coordinator.parent = self
    }

    func onEvent(_ onInputCommandTab: (() -> Void)? = nil) -> some View {
        return onReceive(self.onInputCommandTab) {
            onInputCommandTab?()
        }
    }
}

extension TextView {
    final class Coordinator: NSObject, MyTextViewDelegate {
        fileprivate var parent: TextView

        init(_ parent: TextView) {
            self.parent = parent
        }

        func myTextViewDidInputTab() {
            parent.onInputCommandTab.send()
        }
    }
}

これで使う準備は整った。実際に使う時にはこうする。

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack() {
            TextView()
                .onEvent {
                    print("Input Tab.")
                }
        }
        .padding()
    }
}

これでTabを入力した時にはUITextViewに \t は入力されずに、イベントが発火して "Input Tab." がログ表示される。また、iPadで表示できるショートカット一覧にも、しっかりと登録されている。

これで無事にTabキーをハンドリングできるようになった。めでたし。

2022年の買ってよかったもの。

良くて使い続けてるものだけ書いていく。

デスク周り

fromatom.hatenablog.com

デスク周りをどかっとリニューアルした。コード周りもいい感じにまとまったし、デスクの色もダークブラウンになって満足している。

エスプレッソメーカー

ついに買ってしまった。

これがかなり良い。エスプレッソというものがそもそも旨い。 すぐに飽きてしまうかと思ったけど、なんだかんだで定期的に飲んでいる。

朝起きた時や、食後や、仕事中に眠くなった時などに、エスプレッソをガッと入れてグッと飲むと最高。 のんびりしたいときは、レンチンしたミルクを入れればカフェラテも作れる。 フォームミルクは面倒なので作らなくなった。だってミルクぶちこめば味同じだし。

コーヒーの粉を用意するのも面倒かと思ったが、購入したエスプレッソメーカーはポッドに対応していた。

これなら電源を入れて、ポッドをセットして、抽出して、ポッドを捨てればいいだけなのですごい簡単。

デカフェのインスタントコーヒー

これが一番美味い。

深夜作業をしていると、どうにもコーヒーが飲みたくなる時がある。 が、流石に深夜にコーヒーを飲んでしまうと、寝れなくなって終わってしまう。 そんな時に、このデカフェのインスタントコーヒーが便利。

味もかなり良いし、デカフェだからという違和感もないのでとても助かっている。

ココア

一番うまいココア。

ココアってお湯で作るとなんだか物足りない感じになり、牛乳で作りたくなりがち。 しかし、このスイスミスのココアはお湯で作ってもすごい濃厚でバリバリに美味い。最高。

マシュマロ入りとかヘーゼルナッツ風味とかあるけど、普通のミルクココアが安定して美味しいのでおすすめ。

AirTag

鍵につけるように、AppleのAirTagを買った。

ケースはこっち。

今まで鍵にはMAMORIOをつけていたけれど、電池交換ができない買い替え方式で困っていた。 AirTagならボタン電池の入れ替えが可能なので、長く使い続けられそうなので移行した。

キーケース

キーケースを買い替えた。

家の鍵ぐらいしか無いのでこれで十分。かわいいしコンパクトだし、革の質感も良い。

ソープディスペンサー

手洗いをしまくるようになったので、洗面台用に購入。 勝手に泡が出るのはやっぱり便利。

綿入れ半纏

www.amazon.co.jp

あったかい。圧倒的あったかさ。 冬場はこれとこたつだけで十分。

HDMIキャプチャー

Splatoon3を録画したくて買った。

パススルーもできるしUSB-Cで繋げばそのまま使えるしで便利。 OBSでそのままYouTube配信をしておいて、あとから見返したりしている。

1000円台で買える安いHDMIキャプチャーも試してみたけど、音がうまくとれなかったのでダメだった。 こういうのは最初からそこそこ良いやつを買っておいたほうが後悔しなくて済む。

CIO NovaPort TRIO

小型の充電器。このサイズで65WでるのでM2 MacBook Airの充電には十分。 M2 MacBook Airはすごい充電がもつので、出先で充電したことは無いが、保険のために持ち歩ける小ささと軽さが魅力。

ロジクール MX ANYWHERE 3

もう何年もAnyWhereシリーズを使い続けているが、3が出ていたので買い足してみた。 今までのAnyWhereシリーズとは異なり、勢いをつけてホイールを回すと無抵抗モードになるという機能が追加されている。おもしろい。

温素

入浴剤。東京に住んでいると、強塩泉系の温泉が多く硫黄系やアルカリ系の温泉に入るには少し遠出をしないといけない。 この入浴剤はアルカリ系の入浴剤になっていて、入れるとちゃんとヌルヌルする。 お湯がヌルヌルするのではなく、肌がヌルヌルするので溶けてる〜感がある。

詰め合わせパックを試してみて、琥珀の湯が気に入ったのででかいサイズのを購入した。

時代は着る布団。

かなり前から着る毛布が流行っており、冬場の寒さ対策グッズとして人気です。しかし、日本には古来から着る布団があります。

そう、半纏(綿入れ半纏)ですね。前から欲しかったのですが、Amazonブラックフライデーで安くなっていたので購入しました。もう売り切れてしまっていますが、下記の商品です。

早速使っているんですが、かなり暖かくて最高です。寒いとエアコンをつけて作業をするのですが、暖房を強くしすぎると暖かくはあるんですが頭がポワポワしてきます。一方で弱くすると薄っすらと寒さが身体にしみてきて、なんとも難しい。そんな時にこの半纏が便利です。

布団よろしく綿が全体に詰め込まれているので、もこもこのふかふかでとっても暖かい。寝れる。また、着る毛布のように長くないので、ちょっと席を立って物を取ったりとか、そういう動きがとても楽にできます。

デメリットとしては、もこもこしているのでベイマックスみたいになります。でかい。まぁこれで外に出るわけじゃないので良いでしょう。

今年の冬は、この半纏でぬくぬく快適に過ごせそうです。

UITextViewで `""` が `“”` に、 `--` が`—` に勝手に変換される問題の対処法

課題

UITextViewでエディタを作っていると、

  • "" が勝手に “”に変換される
  • '' が勝手に ‘’ に変換される
  • -- が勝手に に変換される

という状況に出会って困ることがあります。

これはなに?

これはスマート引用符とスマートダッシュという機能です。Macでコードや技術的な文章を書いている人は、

設定画面のこのチェックを外しましょうという記事に出会います。このスマート引用符とスマートダッシュiOS/iPadOSのUITextViewにも導入されているため、iPhone/iPadでも同様の自動変換が発生してしまいます。

対応策

OSの設定を変える

設定アプリ > 一般 > キーボード > スマート句読点をOFF

これでOS全体でスマート系(自動変換系)の機能がOFFになります。

UITextViewの設定を変える

OS側の設定でも変更可能ですが、わざわざOS設定を変えなくても特定のUITextViewでこれらの自動変換機能を止めたい場合があると思います。

UITextViewには smartQuotesTypesmartDashesTypeというプロパティがあるので、これらに .no を指定してあげるだけで良いです。

コード例としては、

override func viewDidLoad() {
    super.viewDidLoad()

    textView.smartDashesType = .no
    textView.smartQuotesType = .no
}

こんな感じ。