TECH PLAY

Swift

イベント

マガジン

技術ブログ

はじめに こんにちは、スタメンでプロダクトエンジニアをしている おしん ( @38Punkd ) です。 5月9日、ウインクあいちで開催された フロントエンドカンファレンス名古屋 2026 にLT枠で登壇させていただきました。 その発表内容をブログ形式でご紹介できればと思います。 WebViewの文字サイズ、固定されていませんか? この問題は、ネイティブとWebの境界に起因する「実装責務の曖昧さ」から生じがちです。 モバイルアプリ開発において、ユーザーのアクセシビリティへの配慮は不可欠です。特に、スマートフォンの設定で文字サイズを大きくしているユーザーにとって、アプリ内のテキストがその設定に追従するかどうかは、使いやすさに直結します。 私たちのアプリ TUNAG は、モバイルアプリではアプリネイティブとWebView(モバイルアプリでHTMLを表示できる機能)を併用しています。ネイティブUIはOSの文字サイズ設定に追従する一方で、アプリ内のWebViewだけが標準サイズのままでした。文字のリサイズが画面ごとに適用されたりされなかったりすると、ユーザーにとって読みにくいコンテンツになってしまうため、WebViewへの適用を本格的に開始しました。 本記事では、iOSとAndroidそれぞれのWebViewにおいて、OSの文字サイズ設定を適切に反映させるための具体的な実装方法と、その際の注意点について解説します。 文字サイズ設定は「特別対応」ではない 文字サイズ調整は、多くのユーザーが日常的に利用する標準機能です。ある調査によると、 モバイルユーザーの約33%が文字サイズ調整を有効化している というデータもあります *1 。iOSのDynamic Type *2 やAndroid 14以降の200%フォントスケーリング *3 など、OSもこの機能を重視しています。 結論としては、 ネイティブアプリ側のごくわずかな修正と、Web開発時のただ1点のポイントをおさえるだけ で、WebViewでもiOS, Android共に文字サイズ設定が反映されるようになります。 対応工数に対して UX改善のインパクトが大きい領域 であると言えそうです。 iOS WebView:JavaScript注入による動的なフォントサイズ反映 iOSのWebViewでDynamic Typeを反映させる最も柔軟で推奨される方法は、ネイティブ側でOSのフォントサイズを取得し、JavaScriptを介してWebViewに注入するアプローチです。 ① Swift:Dynamic Type 変更を検知してフォントサイズを JS で注入 NotificationCenter. default .addObserver( self , selector : #selector(dynamicTypeDidChange), name : UIContentSizeCategory.didChangeNotification , object : nil ) @objc private func dynamicTypeDidChange () { applyFontSize() } func webView (_ webView : WKWebView , didFinish navigation : WKNavigation! ) { applyFontSize() // ページロード完了時にも適用 } private func applyFontSize () { let size = UIFont.preferredFont(forTextStyle : .body).pointSize webView?.evaluateJavaScript( "document.documentElement.style.fontSize = ' \( size ) px'" , completionHandler : nil ) } UIContentSizeCategory.didChangeNotification を受信したら、Swift側でフォントサイズを取得し、 evaluateJavaScript でWebViewの :root 要素の font-size プロパティに直接書き込みます。この方式は リロードが不要で即時反映され、独自のスケールにも柔軟に対応できる 点が利点です。 ② CSS: font-size は rem 単位で指定 Swift側で :root の font-size を動的に書き換えるため、Webコンテンツ側では rem 単位でフォントサイズを指定することで、すべてのテキスト要素が連動してスケールするようになります。 : root { font-size : 17px ; /* フォールバック。Swift が evaluateJavaScript で上書きする */ } .title { font-size : 1.143rem ; } /* ✅ :root 基準でスケールする */ .description { font-size : 0.857rem ; } /* ✅ :root 基準でスケールする */ 参考:よりシンプルな代替案( -apple-system-body ) CSSに :root, body { font: -apple-system-body; } を指定し、Dynamic Typeの変更検知時に webView.reload() を呼ぶ方法もあります。この方法は手軽ですが、 WebViewのリロードが発生する 点と、 Apple提供のスケールに固定される 点がデメリットです。 Android WebView:「対応できている風」に注意 AndroidのWebViewでは、iOSとは異なる挙動と注意点があります。特に「文字サイズ設定に対応できているように見えるが、実は問題がある」ケースに注意が必要です。 見た目 実際に起きていること 文字が大きく見える Activity再生成によりWebViewが作り直される 全体が拡大される 文字だけでなく画像・余白もズームされる場合がある 設定変更後に戻る スクロール位置・入力中状態が失われる可能性がある android:configChanges に fontScale が含まれていない場合、OSのフォントスケール変更時にActivityが再生成され、WebView全体がズームされます。これは文字以外の要素も拡大し、レイアウト崩れや状態喪失の原因となります。 文字だけを適切に反映できているか を見極める必要があります。 AndroidはOS値をJSで橋渡しする Androidで文字サイズ設定をWebViewに適切に反映させるには、ネイティブ側でOSのフォントスケール値を取得し、JavaScriptを介してWebViewに伝えるアプローチが有効です。 ① AndroidManifest: fontScale を configChanges に追加 <activity android : configChanges = "fontScale|uiMode|density" ... /> AndroidManifest.xml の <activity> タグに android:configChanges="fontScale" を追加し、フォントスケール変更時のActivity再生成を防ぎます。 ② Kotlin:フォントスケール変化を検知して WebSettings.setTextZoom(int) で反映 // 起動時の初期化 applyFontScale(resources.configuration.fontScale) // フォントスケール変化を検知(iOS の didChangeNotification に相当) override fun onConfigurationChanged(newConfig: Configuration) { super .onConfigurationChanged(newConfig) applyFontScale(newConfig.fontScale) } private fun applyFontScale(fontScale: Float ) { // fontScale 1.0 → 100(標準), 2.0 → 200(200%) // px・em 問わず WebView 内のテキスト全体をスケールする webView.settings.textZoom = (fontScale * 100 ).roundToInt() } onConfigurationChanged で newConfig.fontScale を取得し、 WebSettings.setTextZoom(int) でWebViewのテキストズームレベルを設定します。 ③ CSS:テキストは単位を問わずスケールされる WebSettings.setTextZoom(int) はレンダリングエンジンレベルで適用されるため、HTML/JS の変更は不要です。 px 、 em 、 rem のいずれの単位で指定されたテキストもスケールされます。ただし、非テキスト要素はスケールされないため、Web側で柔軟な設計が必要です。 共通の考慮事項:拡大しても壊れないレイアウトにする OSの文字サイズ設定をWebViewに反映させるだけでなく、Webコンテンツ側で、文字が拡大されてもレイアウトが崩れないような柔軟な設計が求められます。 避けたい実装 推奨する実装 固定高さ 内容量に応じて伸びる高さ 1行前提 折り返し・複数行を許容 アイコンと文字の密結合 gap・flex-wrap・min-widthで逃がす 文字サイズ対応の本質は、値の反映だけでなく「拡大を許容するUI」を構築すること にあります。 まとめ 本記事では、iOSおよびAndroidのWebViewにおいて、OSの文字サイズ設定を適切に反映させるための具体的な実装方法と、その際の注意点について解説しました。 iOSでは、Swift側でDynamic Typeのフォントサイズを取得し、JavaScriptでWebViewの :root 要素の font-size を動的に書き換えるアプローチが最も柔軟です。これにより、リロードなしで即時反映が可能となります。Androidでは、 AndroidManifest で fontScale の変更を検知し、 WebSettings.setTextZoom(int) でWebView全体のテキストズームレベルを設定します。 そして、iOSとAndroidの両方でWebView内のテキストをOSの文字サイズ設定に追従させるための共通の鍵となるのが、 CSSでの rem 単位の活用 です。 デフォルトの文字サイズ 拡大した文字サイズ OSではJavaScriptによる :root 操作と連動させるために rem 指定が 必須 となります。一方で、Androidの setTextZoom は単位を問わず追従してくれます。つまり、Web側の実装をiOSに合わせて rem に統一しておけば、Android側でも一切の不都合なく自然に拡大縮小が行われ、両OSに矛盾なく対応できるのです。 スマートフォンのOS設定を尊重し、より多くのユーザーに読みやすいWebコンテンツを届けることは、ユーザーの満足度向上に繋がりやすく重要です。本記事が参考になりましたら幸いです。 サンプルリポジトリ 本記事で紹介した実装の詳細は、以下のサンプルリポジトリでご確認いただけます。 iOSアプリ : GitHub - iOS WebView Dynamic Type Sample Androidアプリ : GitHub - Android WebView Font Scale Sample herp.careers *1 : Ian Savchenko, “Designing for Accessibility: How Text Resizing Works in Different Web Browsers,” PayPal Technology Blog. https://medium.com/paypal-tech/designing-for-accessibility-how-text-resizing-works-in-different-web-browsers-bed9e424e071 *2 : Apple Developer Documentation, “Scaling fonts automatically.” https://developer.apple.com/documentation/uikit/scaling-fonts-automatically *3 : Android Developers, “Features and APIs Overview — Non-linear font scaling to 200%.” https://developer.android.com/about/versions/14/features
こんにちは、iOSエンジニアのyamakenです。2026年4月12日(日)から14日(火)の3日間にわたり開催された、try! Swift Tokyo 2026に、LINEヤフー株式会社はGOLDス...
はじめに こんにちは、ZOZOTOWN開発1部iOSブロックの @kitasuke です。 前回の記事「 ZOZOTOWN iOS のアーキテクチャとチームの進化 」では、MVCからMVVM、そしてMVVM + Repositoryへのアーキテクチャ進化を取り上げました。あわせて、レビュー文化をチームに根づかせてきた3年間も振り返っています。 ただ、アーキテクチャを文章で定義しても、書き手によって命名や責務分割はぶれが生じますし、AIに任せると過去の望ましくない実装パターンまで律儀に再現されます。 ドキュメントによる「努力目標」では、アーキテクチャは守りきれません。 そこで発想を逆にしました。アーキテクチャを「守るべきルール」ではなく、 構造化されたスキーマ として定義し、人間とAIの双方がそれに従うしかない形にします。Swiftの型システムがコンパイル時に不正を弾くのと同じ発想を、アーキテクチャのレイヤーにスキーマという形で持ち込みます。それが本記事で紹介する 「スキーマでアーキテクチャを縛る」アプローチ です。副産物として、設計からコードを自動生成するパイプラインも動いています。 目次 はじめに 目次 どんなスキーマを定義したのか architecture-guidelines.md — コンポーネントをスキーマで縛る architecture-templates.md — スキーマから Swift を導出するルール どうやって縛っているのか 画面ごとの設計を YAML で表現する /architectureと/codegen — 実際の運用 /architecture: 仕様書やデザインから YAML を起こす /codegen: YAMLからSwiftのコードを生成する 何が変わったのか AIの書くコードがレビューを通る水準になった レビューで「プロダクト品質」の話ができるようになった まとめ どんなスキーマを定義したのか 全体像はこうなっています。 仕様書 (Confluence) / デザイン (Figma) / 既存コード │ ▼ /architecture ┌─────────────────────┐ │ 設計 YAML │ ←── AI / Codegen 向け │ Human Doc (Markdown) │ ←── 人間向けレビュー資料 └─────────────────────┘ │ ▼(人間がレビュー・編集) │ ▼ /codegen Swift コード一式 ↑ 全工程でガイドラインとテンプレートが参照される 土台となっているのが、チームで整備した 2つのドキュメント です。 architecture-guidelines.md — 各コンポーネントのスキーマ(何が正しいか) architecture-templates.md — スキーマからSwiftを導出するテンプレート(どう書くか) architecture-guidelines.md — コンポーネントをスキーマで縛る 各コンポーネント(ViewModel、Repository、Translatorなど)を、型・依存・命名・必須ルール・禁止パターンなどのフィールドで厳密に定義しています。たとえばViewModelのスキーマは次のとおりです。 ### ViewModel - type : `@MainActor final class` - imports : [ Foundation, Combine ] - imports_forbidden : [ APIModule ] - depends_on : [ RepositoryProtocol, UIModelTranslator, DataModel, UIModel ] - nested_types : [ ViewState, Router ] - naming : { Feature } ViewModel - required : - ViewState enum で画面状態を管理(複数 Bool 禁止) - @Published private(set) で外部からの直接変更を防止 - 1 ユーザーアクション = 1 input メソッド(did{Verb}{Noun}) - forbidden : - キャッシュロジック(Repository の責務) - ログ送信の直接呼び出し(UseCase/別 Repository に分離) 自由に書ける余地を 意図的に潰している のがポイントです。ViewModelがAPIモジュールをimportした時点でアウトです。 @Published を private(set) にしなかった場合もアウトです。自己流のMVVM解釈を許さない設計になっています。 architecture-templates.md — スキーマから Swift を導出するルール スキーマだけではSwiftコードの具体的な書き方までは決まりません。命名規則、ファイルの生成順序、各レイヤーのSwiftコードテンプレートなどを、もう一段別のドキュメントで固めています。 ガイドラインがスキーマで、テンプレートが導出規則です。 この2つが揃うことで、アーキテクチャのスキーマから具体的なSwiftコードが一意で決まる状態になりました。 どうやって縛っているのか 人間・AI・ツールの全員が、同じスキーマで動くようになっています。順に見ていきます。 画面ごとの設計を YAML で表現する コンポーネントのスキーマが決まっても、画面ごとの実装は別物です。そこで、 画面ごとの設計を1枚のYAMLで記述 します。 feature : ProductList domain : Product api : - id : fetchProducts method : GET path : /products response : items : [ Product ] actions : - trigger : didAppear api : fetchProducts - trigger : didTapRetry api : fetchProducts condition : "state == .error" models : data : - name : Product fields : id : String name : String brandName : String price : Int imageURL : URL ui : - name : ProductListUIModel fields : nameText : String brandText : String priceText : String このYAMLは、ガイドラインが定めたスキーマの「値」にあたります。画面のAPI、アクション、データモデルが構造化されて並んでいるだけで、曖昧さの入り込む余地はありません。 /architecture と /codegen — 実際の運用 この縛りを日々の開発で実行しているのが2つのスラッシュコマンドです。 /architecture : 仕様書やデザインから YAML を起こす 重要なのは、このYAMLを人間がゼロから書いているわけではない という点です。Confluenceの仕様書やFigmaのデザインを入力にすると、 /architecture コマンドが設計YAMLと人間向けMarkdownの大部分を自動生成します。 人間の作業は「書く」ではなく「判断する」に寄っています。生成されたYAMLを読み、責務分割やエッジケースの扱いなど 設計判断が必要な箇所だけ に手を入れます。スキーマが縛ってくれているので、AIが起こしたYAMLも標準から外れた形にはなりません。 /codegen : YAMLからSwiftのコードを生成する レビューが終わったYAMLを /codegen に渡すと、Swiftコード一式が出力されます。具体的には、View / ViewModel / Repository / プロトコル / モック / ユニットテストの雛形 / 依存注入のコードです。 たとえば先ほどの ProductList.yaml のうち、以下の部分に注目します。 actions : - trigger : didAppear api : fetchProducts - trigger : didTapRetry api : fetchProducts condition : "state == .error" この部分を /codegen に流すと、ViewModelは次のように生成されます。 @MainActor final class ProductListViewModel : ObservableObject { enum ViewState { case loading case loaded(ProductListUIModel) case error(Error) } @Published private ( set ) var state : ViewState = .loading private let repository : ProductRepositoryProtocol func didAppear () async { await fetchProducts() } func didTapRetry () async { guard case .error = state else { return } await fetchProducts() } private func fetchProducts () async { state = .loading do { let products = try await repository.fetchProducts() state = .loaded(ProductListUIModelTranslator.translate(from : products )) } catch { state = .error(error) } } } ガイドラインで定義した制約が そのまま反映されている のが分かります。たとえば @MainActor final class 、 @Published private(set) 、ViewState enumでの状態管理、 did{Verb}{Noun} 命名規則などです。YAMLの actions はそのままViewModelのメソッドに、 condition はguard文に対応しています。コード生成は仕組みの主役ではなく、スキーマで縛った結果として得られる副産物です。 何が変わったのか AIの書くコードがレビューを通る水準になった スキーマで縛ったことで、実際にAIの出力が目に見えて安定しました。命名・配置・レイヤー構成がプロジェクト標準に揃い、ハルシネーションもほぼ消え、同じYAMLを何度通してもほぼ同じコードが出てきます。 AIの生成するコードは、そのままレビューを通る水準に達しました。 これが縛りの直接的な見返りです。 レビューで「プロダクト品質」の話ができるようになった コード品質(命名、配置、責務分割)はスキーマが自動的に揃えるので、レビューで議論する必要がなくなりました。その分、UXが成立しているか、エッジケースの仕様が妥当か、ビジネスゴールに沿っているか、といった プロダクトとしての品質 に時間を使えるようになっています。コードの良し悪しではなく、 プロダクトの良し悪し を議論できるようになったのは、狙い通りの大きな変化でした。 まとめ アーキテクチャは「努力目標」ではなく「スキーマ」で守ります。Swiftの型システムが不正を弾くのと同じ発想を、設計レイヤーにも持ち込みます。人間とAIを同じスキーマで動かすことで、チームのアーキテクチャを長く保てる状態を目指しています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com

動画

該当するコンテンツが見つかりませんでした

書籍