メモ帳アプリに Markdown プレビューを後付け実装中 — WKWebView の選択
| 開発記録 | メモ帳
タグ: #iOS #Swift #WKWebView #開発中
純正 Swift のメモアプリに Markdown プレビューを追加実装している進行中の話。ネイティブレンダラを書くか、WKWebView でやるか。最終的に WKWebView を選んだ理由をまとめます。次回アップデートでリリース予定です。
本記事は現在開発中の機能についての開発記録です。Markdown プレビューはまだリリースされておらず、次回のアプリアップデートで提供予定です。
なぜ Markdown プレビューなのか
「タブで切り替え可能なメモ帳」というシンプルなアプリですが、ユーザー層の中に開発者やライターが一定数いて、「Markdown で書きたい」「リスト記法を整形して表示したい」という要望が継続的にありました。
書く側は素の Markdown で十分書けます。問題は「表示」側で、# 見出し や - リスト をパースして整形する仕組みが必要でした。
選択肢: ネイティブレンダラ vs WKWebView
Swift で Markdown プレビューを実装する方法は大きく 2 つあります。
- ネイティブレンダラ(CoreText でひとつずつ装飾)
- WKWebView で HTML として表示
ネイティブの方が動作が軽く、UI 統合度も高い。けれど、Markdown のフル仕様(表、コードハイライト、リンク、画像、ネスト構造)を自前で書くのは膨大な工数です。
結論として、WKWebView + 軽量 JS パーサで実装することにしました。
WKWebView アプローチの設計
- メモ本文(Markdown)を JS パーサ(marked.js など)で HTML に変換
- CSS で iOS 風のフォント・色・余白に整える
- WKWebView に
loadHTMLStringで表示 - リンクタップは
decidePolicyFor navigationActionで intercept してネイティブブラウザに渡す
WKWebView への流し込みは loadHTMLString 一発ですが、相対パスのリソース解決のために baseURL を渡しておくと、後からローカル画像対応を入れる時に楽です。
func renderMarkdown(_ markdown: String, into webView: WKWebView) {
let escaped = markdown
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "`", with: "\\`")
let html = """
<!doctype html>
<html><head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="preview.css">
</head>
<body>
<div id="out"></div>
<script src="marked.min.js"></script>
<script>
document.getElementById('out').innerHTML = marked.parse(`\(escaped)`);
</script>
</body></html>
"""
let base = Bundle.main.url(forResource: "preview", withExtension: "css")?
.deletingLastPathComponent()
webView.loadHTMLString(html, baseURL: base)
}
JavaScript エスケープシーケンスエラー
ハマったポイントとして、メモ本文に JS の特殊文字(バックスラッシュ、シングルクォート、改行)が含まれていると、HTML 生成時にエスケープエラーになりました。
初期実装では \n の扱いを誤って、改行が全部消えてしまうバグも踏みました。本文のエスケープ処理を専用関数として切り出し、ユニットテストで主要パターンを潰しました。
スクロール位置の保持
編集モードからプレビューに戻ったときに、スクロール位置がリセットされる問題がありました。WKWebView のスクロール位置を手動で保存し、再表示時に復元する処理を追加しました。
let scrollPosition = webView.scrollView.contentOffset
// ...再ロード後
webView.scrollView.setContentOffset(scrollPosition, animated: false)
iOS 26 対応
後日 iOS 26 で WKWebView の挙動が一部変わり、レンダリング後のスクロールタイミングがずれる問題が出ました。viewDidLayoutSubviews で再調整するパッチを当てて対応しています。
まとめ
- Markdown プレビューは WKWebView + JS パーサが圧倒的に楽
- エスケープ処理は必ずテストを書く
- リンクは intercept してネイティブブラウザへ
- スクロール位置の保持は地味だが重要
「後付け機能」はアーキテクチャに無理を強いがちですが、WKWebView は「Markdown 程度の表示なら」十分な選択肢でした。