2026/04/16

iOS 26 対応機種一覧【2026年最新】Apple Intelligence搭載版

2025年9月15日にAppleがリリースした iOS 26 は、大幅な機能追加とデザイン刷新が特徴です。

この記事では、iOS 26 に対応するiPhone機種の完全一覧新機能非対応機種、そして アップデート方法 をまとめます。


iOS 26 について

リリース日

  • 発表日:2025年6月9日(WWDC 2025)
  • リリース日:2025年9月15日
  • バージョン:iOS 26

主な特徴

iOS 26は、Apple Intelligenceの統合拡大、Liquid Glassデザイン言語の採用、多言語対応の強化が特徴です。


iOS 26 対応iPhoneモデル(完全一覧)

対応機種

iOS 26に対応するiPhoneは以下の通りです。Apple A13 Bionicチップ以降 を搭載した機種が対応しています。

最新シリーズ(iPhone 17系)
機種 リリース年 チップ
iPhone 17 2025年9月 A19
iPhone 17 Plus 2025年9月 A19
iPhone 17 Pro 2025年9月 A19 Pro
iPhone 17 Pro Max 2025年9月 A19 Pro
iPhone Air 2025年9月 A18
iPhone 16e 2025年9月 A17
iPhone 16シリーズ
機種 リリース年 チップ
iPhone 16 2024年9月 A18
iPhone 16 Plus 2024年10月 A18
iPhone 16 Pro 2024年9月 A18 Pro
iPhone 16 Pro Max 2024年9月 A18 Pro
iPhone 15シリーズ
機種 リリース年 チップ
iPhone 15 2023年9月 A17 Pro
iPhone 15 Plus 2023年10月 A17 Pro
iPhone 15 Pro 2023年9月 A17 Pro
iPhone 15 Pro Max 2023年9月 A17 Pro
iPhone 14シリーズ
機種 リリース年 チップ
iPhone 14 2022年9月 A15 Bionic
iPhone 14 Plus 2023年10月 A15 Bionic
iPhone 14 Pro 2022年9月 A16 Bionic
iPhone 14 Pro Max 2022年9月 A16 Bionic
iPhone 13シリーズ
機種 リリース年 チップ
iPhone 13 2021年9月 A15 Bionic
iPhone 13 mini 2021年11月 A15 Bionic
iPhone 13 Pro 2021年9月 A15 Bionic
iPhone 13 Pro Max 2021年9月 A15 Bionic
iPhone 12シリーズ
機種 リリース年 チップ
iPhone 12 2020年10月 A14 Bionic
iPhone 12 mini 2020年11月 A14 Bionic
iPhone 12 Pro 2020年10月 A14 Bionic
iPhone 12 Pro Max 2020年11月 A14 Bionic
iPhone 11シリーズ
機種 リリース年 チップ
iPhone 11 2019年9月 A13 Bionic
iPhone 11 Pro 2019年9月 A13 Bionic
iPhone 11 Pro Max 2019年9月 A13 Bionic
iPhone SE(第2世代以降)
機種 リリース年 チップ
iPhone SE(第2世代) 2020年4月 A13 Bionic
iPhone SE(第3世代) 2022年3月 A15 Bionic
iPhone SE(第4世代) 2025年3月 A18

iOS 26 非対応iPhoneモデル

iOS 26に対応しないiPhoneは以下の通りです。

アップデート対象外になった機種

機種 リリース年 最新対応OS
iPhone XS 2018年9月 iOS 25
iPhone XS Max 2018年9月 iOS 25
iPhone XR 2018年10月 iOS 25

理由:これら3機種は A12 Bionicチップ を搭載しており、iOS 26で要求される処理能力に対応できないためです。


iOS 26 新機能一覧

Apple Intelligence(アップルインテリジェンス)

Apple Intelligenceは、プライバシーを最優先 に設計されたAI機能です。iOS 26で大幅に拡張されました。

対応機種

  • iPhone 15 Pro以降
  • iPhone 16シリーズすべて
  • iPhone 17シリーズすべて
主なApple Intelligence機能
  1. Writing Tools(ライティングツール)
    • テキストの自動生成、要約、修正
    • 日本語対応
  2. Visual Intelligence(ビジュアルインテリジェンス)
    • 画面上のコンテンツを認識
    • ChatGPTと連携して質問回答
    • QRコード読み込み自動化
  3. Live Translation(ライブ翻訳)
    • Messages、FaceTime、Phoneで多言語翻訳
    • リアルタイム翻訳対応言語拡大
    • 日本語↔英語など18言語対応
  4. Smart Notification(スマート通知)
    • 優先度ベースで通知を並び替え
    • 重要な通知を見落とさない
  5. 記憶されたデバイス上の会話
    • Siriが会話履歴を保存
    • より自然な音声操作が可能

Liquid Glassデザイン

iOS 7以来の 大幅なUIデザイン刷新

特徴

  • 曲線的で流動的なデザイン
  • macOS、iPadOS、watchOS、tvOSと統一
  • すべてのAppleデバイス間での一貫性

通信機能の強化

Phone(電話)アプリ
  • スパムコール自動フィルタ強化
  • 不在着信に対する自動返信
  • 音声メッセージのテキスト自動変換
Messages(メッセージ)
  • RCS(リッチコミュニケーションサービス)フル対応
  • Android端末とのメッセージ互換性向上
  • グループチャットのリアクション機能拡大

Maps(地図)の更新

  • ハイライド機能(特定エリアのフォーカス表示)
  • 詳細な3D地形表示
  • リアルタイム渋滞情報の精度向上

CarPlay(カープレイ)の進化

  • ダッシュボード統合レベルの向上
  • 新しいウィジェットシステム
  • より多くのサードパーティアプリ対応

Apple Music(ミュージック)

  • Dolby Atmos対応曲の自動最適化
  • パーソナルプレイリスト自動生成
  • 歌詞の同期精度向上

Wallet(ウォレット)

  • デジタルID(運転免許証など)対応拡大
  • 支払い機能の拡張
  • 複数カード同時管理の最適化

iOS 26 へのアップデート方法

事前準備

アップデート前に以下を確認してください:

1. iPhoneのバッテリー容量
  • 推奨:80%以上
  • 最小:50%以上
  • 充電器接続推奨
2. ストレージ容量

iOS 26は約 5-7GB の空き容量が必要です。

3. WiFi接続
  • 高速Wi-Fi環境に接続
  • データ量制限なし(アップデートは 3-4GB)
4. バックアップ作成
1. iCloud → 設定 → [ユーザー名] → iCloud → iCloud バックアップ
2. または Finder/iTunes で PC にバックアップ

アップデート手順

方法 1: WiFi経由アップデート(推奨)
1. Wi-Fiに接続
2. 設定アプリを開く
3. 一般 → ソフトウェア・アップデート
4. iOS 26 が表示される
5. 「ダウンロードしてインストール」をタップ
6. パスコードを入力
7. 利用規約に同意
8. インストール開始(15-30 分程度)
方法 2: Finder/iTunes経由アップデート

Mac(Finder使用)

1. USB ケーブルで iPhone を Mac に接続
2. Finder を開く
3. サイドバーから iPhone を選択
4. 「アップデート」をクリック
5. 自動的にダウンロード・インストール

Windows(iTunes使用)

1. USB ケーブルで iPhone を PC に接続
2. iTunes を開く
3. デバイスアイコン選択
4. 「アップデート」をクリック
5. 自動的にダウンロード・インストール

アップデート中の注意事項

  • ❌ iPhone を動かさない
  • ❌ USB接続を切らない(WiFi版は除く)
  • ✅ 電源は切らない(自動で再起動する)
  • ✅ 数十分かかる場合がある

アップデート後の確認

設定 → 一般 → ソフトウェア・アップデート
「お使いのソフトウェアは最新です」と表示されれば成功

iOS 26 アップデートのメリット

セキュリティ面

✅ 最新のセキュリティパッチ適用
✅ マルウェア対策強化
✅ プライバシー保護機能拡大

パフォーマンス面

✅ バッテリー消費最適化
✅ アプリ起動速度向上
✅ 全体的なシステム安定性向上

機能面

✅ Apple Intelligence の利用可能
✅ 新しいUIデザイン体験
✅ 最新の通信機能


iOS 26 アップデートの注意点

非対応機種の方へ

iPhone XS、XS Max、XRをお持ちの場合:

  • 最新OS:iOS 25(アップデート不可)
  • サポート期間:Apple が決定するまで続行予定
  • 新機能制限:一部 Apple Intelligence 機能は利用不可

ストレージ不足の場合

iOS 26 は約 5-7GB が必要です。不足の場合:

  1. 不要なアプリを削除
    設定 → 一般 → iPhone ストレージ
    → 不要なアプリを削除
  2. キャッシュをクリア
    設定 → Safari → 履歴とウェブサイトデータを削除
  3. クラウド写真利用
    設定 → [ユーザー名] → iCloud → 写真
    → iCloud フォトライブラリ有効化

よくある質問(FAQ)

Q1: iOS 26 は無料ですか?

A: はい。すべての対応iPhoneユーザーは 無料 でアップデート可能です。

Q2: アップデートに時間がかかります

A:

  • ダウンロード:20-30 分(Wi-Fi速度による)
  • インストール:15-30 分
  • 合計:30-60 分が目安

遅い場合は、より高速なWi-Fi環境で試してください。

Q3: アップデート後、古いバージョンに戻せますか?

A: 基本的に 戻せません。Apple は セキュリティ理由から、古いバージョンへのダウングレードを制限しています。

Q4: iOS 26 は不安定ですか?

A: iOS 26 は公式リリース版のため、一定以上の安定性が確保されています。ただし:

  • アップデート直後に不具合が出ることがある
  • バグは iOS 26.0.1 以降で修正される傾向
  • 重大な問題がある場合は iOS 25 への緊急ダウングレードツール提供

Q5: Apple Intelligence はすべてのiPhoneで使えますか?

A: いいえ。iPhone 15 Pro 以降 のみ対応です。

  • iPhone 15(無印):❌ 非対応
  • iPhone 15 Plus:❌ 非対応
  • iPhone 15 Pro:✅ 対応
  • iPhone 15 Pro Max:✅ 対応
  • iPhone 16 以上:✅ すべて対応

まとめ

項目 詳細
リリース日 2025年9月15日
対応iPhoneの下限 iPhone 11(A13 Bionic)
非対応になった機種 iPhone XS, XS Max, XR
主な新機能 Apple Intelligence, Liquid Glass, Live Translation
アップデート時間 30-60 分
容量目安 5-7GB 必要
費用 無料

iOS 26 への更新は、セキュリティとパフォーマンスの観点から 推奨されます。対応iPhoneをお持ちの方は、できるだけ早めのアップデートをお勧めします。


関連リンク


2026/04/15

SwiftUI で Image をカスタム形状にクリッピング - 5つの実装パターン

はじめに

SwiftUI で Image を表示する際、見た目を工夫したいことはよくある。丸いプロフィール画像、角丸カード、複雑なロゴ形状...こうした場合に活躍するのが clipShape modifier だ。

この記事では、clipShape を使った 5 つの実装パターン、mask との使い分け、iOS 18 での新機能、実装時の注意点まで、実践的に解説する。

clipShape とは何か

基本的な役割

clipShape は、View を指定した Shape の形状でクリッピングする modifier。Image だけでなく、あらゆる View に適用できる。iOS 13.0 以降で使用可能。

使い方はシンプル。View に .clipShape() を追加して、括弧の中に使いたい Shape を指定するだけ。

clipShape と mask:何が違うのか

同じクリッピングに見えるが、内部動作は異なる。clipShape は Shape のアウトラインを使ったシンプルなクリッピング。mask はピクセルレベルで自由に制御できる。

使い分けの指針は明確だ。形状がシンプルなら clipShape。複雑な形状やグラデーション、半透明を含むなら mask。

項目 clipShape mask
用途 Shape ベースのクリップ ピクセルレベル制御
パフォーマンス 高速(推奨) 遅い
扱える形状 Circle, Rectangle 等 任意(グラデーション等)
一般的な選択 9 割はこちら 特殊な要件時のみ

よく使う 5 つのパターン

1. 円形クリップ(プロフィール画像)

最も一般的。プロフィール画像、アバター、丸いボタン...こうした場面では Circle を使う。

import SwiftUI

struct ProfileView: View {
    var body: some View {
        Image("profileImage")
            .resizable()
            .scaledToFill()
            .frame(width: 120, height: 120)
            .clipShape(Circle())
            .overlay(
                Circle()
                    .stroke(Color.blue, lineWidth: 2)
            )
    }
}

ポイント:resizable() と scaledToFill() でアスペクト比を保ったまま枠を埋める。overlay で枠線を追加すると、さらに見栄えが良くなる。

2. 角丸四角形(カード、サムネイル)

RoundedRectangle でコーナーを丸くする。cornerRadius で丸さを調整。カード、サムネイル、バナーに使う。

struct CardView: View {
    var body: some View {
        Image("thumbnail")
            .resizable()
            .scaledToFill()
            .frame(height: 200)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .shadow(radius: 4)
    }
}

cornerRadius は 8-16 が標準的。大きすぎるとピルになってしまう。shadow() を組み合わせると、カードとして浮き上がって見える。

3. 楕円形(ロゴ、複雑な画像)

横長や縦長の楕円が必要な場合、Ellipse を使う。frame のアスペクト比に応じて形状が決まる。

struct LogoView: View {
    var body: some View {
        Image("logo")
            .resizable()
            .scaledToFill()
            .frame(width: 200, height: 100)
            .clipShape(Ellipse())
    }
}

4. カスタム形状(Path でオリジナル図形)

五角形、三角形、ハート...こうした複雑な形状は、Shape プロトコルに準拠したカスタムクラスで定義する。

struct StarClipView: View {
    var body: some View {
        Image("icon")
            .resizable()
            .scaledToFill()
            .frame(width: 150, height: 150)
            .clipShape(StarShape())
    }
}

// 五角形を定義
struct StarShape: Shape {
    func path(in rect: CGRect) -> Path {
        let center = CGPoint(x: rect.midX, y: rect.midY)
        let radius = min(rect.width, rect.height) / 2

        var path = Path()

        for i in 0..<5 {
            let angle = CGFloat(i) * 0.8 * .pi - .pi / 2
            let x = center.x + radius * cos(angle)
            let y = center.y + radius * sin(angle)

            if i == 0 {
                path.move(to: CGPoint(x: x, y: y))
            } else {
                path.addLine(to: CGPoint(x: x, y: y))
            }
        }

        path.closeSubpath()
        return path
    }
}

Shape プロトコルには path(in:) メソッドを実装する。CGPath を使ってフリーハンドで図形を描く。

5. アニメーション付きクリップ

状態に応じて形状を変える。@State で isExpanded を持って、tap で切り替える。形状を動的に変える場合は AnyShape でラップする。

struct ExpandableClipView: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            Image("icon")
                .resizable()
                .scaledToFill()
                .frame(
                    width: isExpanded ? 250 : 150,
                    height: isExpanded ? 250 : 150
                )
                .clipShape(
                    isExpanded ?
                    AnyShape(RoundedRectangle(cornerRadius: 32)) :
                    AnyShape(Circle())
                )
                .onTapGesture {
                    withAnimation(.spring()) {
                        isExpanded.toggle()
                    }
                }

            Text(isExpanded ? "Collapse" : "Expand")
                .font(.caption)
                .foregroundColor(.gray)
        }
        .padding()
    }
}

// 複数の Shape を一つの型で扱うためのラッパー
struct AnyShape: Shape {
    private let closure: (CGRect) -> Path

    init(_ shape: S) {
        self.closure = { shape.path(in: $0) }
    }

    func path(in rect: CGRect) -> Path {
        closure(rect)
    }
}

Tap でアニメーション付きで形状が変わる。withAnimation(.spring()) で自然な動きになる。

iOS 18 での新機能と対応

ResizableShape

iOS 18 では、一部の Shape がリサイズに対応した。従来は Path でカスタマイズが必要だった部分が、より簡潔に書ける。

ただし、iOS 17 以前との互換性を保つなら、条件分岐で対応版と非対応版を分ける必要がある。

互換性の保ち方

@available(iOS 18, *)
struct iOS18View: View {
    var body: some View {
        Image("icon")
            .resizable()
            .scaledToFill()
            .frame(width: 150, height: 150)
            .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

// iOS 17 以前向け
@available(iOS, introduced: 13.0, deprecated: 18.0)
struct LegacyView: View {
    var body: some View {
        Image("icon")
            .resizable()
            .scaledToFill()
            .frame(width: 150, height: 150)
            .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

実装時の注意点

resizable() は必須

Image のデフォルトサイズは固定。clipShape を使う前に .resizable() を必ず呼ぶ。さもないと期待サイズに拡大されない。

frame は clipShape の前

順序が大事。frame を先に指定して、その後に clipShape を適用する。逆順だと期待通りにクリップされない。

// ✓ 正しい順序
Image("icon")
    .resizable()
    .frame(width: 150, height: 150)
    .clipShape(Circle())

// ✗ 間違い
Image("icon")
    .clipShape(Circle())
    .frame(width: 150, height: 150)
    .resizable()

高い解像度では shadow を避ける

clipShape + shadow は、複数の render pass が必要になり、パフォーマンスが低下する場合がある。本当に必要でない限り、shadow は避ける。

複雑な Path は計算量に注意

カスタム Path を定義する場合、複雑な計算は避ける。View の再描画のたびに path() が呼ばれるため、重い計算は実行時パフォーマンスに影響する。

よくある間違いと対処

「クリップが効かない」

原因は、大体 .resizable() が漏れているか、frame が指定されていない。Image のデフォルトサイズで試しているケースが多い。

「アニメーション中に形状がちらつく」

clipShape アニメーションの制限。見栄えが重要なら、mask で代替えすること。

「カスタム Shape がうまく動かない」

Shape の path() メソッドが rect に対応しきれていない。デバッグは、異なるサイズで試して、形状が正しく拡大縮小されるか確認。

関連記事

まとめ

clipShape で Image をカスタム形状にクリップするのは、SwiftUI では基本的なテクニック。使い分けは単純。

シンプルな形(丸、角丸)なら clipShape。複雑な形やグラデーションマスクなら mask。この基準で判断すれば、9 割のケースは解決する。

5 つのパターンを把握していれば、ほぼすべての画像クリッピング要件に対応できるはずだ。

Android PDF 作成と表示【完全ガイド】PdfDocument vs PdfRenderer【2026年版】

はじめに

Android アプリで PDF を作成・表示することは、レポート生成、請求書作成、ドキュメント共有など、実務的なアプリケーション開発で頻繁に必要になります。

この記事では、Android で PDF を扱う 2 つの主要な方法(PdfDocumentPdfRenderer)を完全解説します。2026 年時点での最新の API、Kotlin での実装、トラブルシューティングまでをカバーしています。

PdfDocument とは

概要

PdfDocument は、Android 5.0(API 21)で導入された PDF 生成用のクラスです。アプリケーション内でプログラマティックに PDF を作成・レンダリングできます。

  • 用途:PDF ファイルの作成(ジェネレーション)
  • 対応 API:API 19 以上
  • クラスandroid.graphics.pdf.PdfDocument

メリット・デメリット

項目 メリット デメリット
柔軟性 完全なカスタマイズが可能 実装が複雑
依存性 外部ライブラリ不要 Android Framework に依存
パフォーマンス 中程度(小~中規模 PDF) 大規模 PDF では遅延の可能性
用途 レポート、請求書、領収書作成 複雑なレイアウトは困難

PdfRenderer とは

概要

PdfRenderer は、Android 5.0(API 21)で導入された PDF 表示・レンダリング用のクラスです。既存の PDF ファイルを画像としてレンダリングして表示します。

  • 用途:PDF ファイルの表示・レンダリング
  • 対応 API:API 21 以上
  • クラスandroid.graphics.pdf.PdfRenderer

メリット・デメリット

項目 メリット デメリット
シンプル 実装が簡潔 カスタマイズが限定的
パフォーマンス 高速(ネイティブレンダリング) メモリ使用量が多い可能性
出力形式 高品質な画像として表示 PDF ファイル自体の編集は不可
用途 PDF ファイルの表示・閲覧 PDF 生成には不適

PdfDocument vs PdfRenderer 比較表

項目 PdfDocument PdfRenderer
目的 PDF 生成 PDF 表示
対応 API API 19+ API 21+
入力 アプリケーションコード PDF ファイル
出力 PDF ファイル ビットマップ画像
使用難度 中程度(複雑) 簡単
カスタマイズ性 高い 低い
パフォーマンス 中速 高速
主な用途 レポート、請求書、データエクスポート PDF ビューア、ドキュメント表示

実装方法

前提条件(環境セットアップ)

// build.gradle.kts (Module: app)
android {
    compileSdk = 34  // 2026年推奨:API 34-35

    defaultConfig {
        minSdk = 21    // PdfDocument/PdfRenderer対応の最小値
        targetSdk = 34
    }
}

dependencies {
    // Kotlin stdlib
    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.0")
}

PdfDocument での実装例

用途:レポート、請求書、ドキュメント作成

// Kotlin での実装例(2026年版)
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.pdf.PdfDocument
import android.os.Environment
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class PdfReportGenerator(private val context: Context) {

    suspend fun generatePdfReport(): File = withContext(Dispatchers.IO) {
        // PDF ドキュメント作成
        val pdfDocument = PdfDocument()
        val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create()
        val page = pdfDocument.startPage(pageInfo)
        val canvas = page.canvas

        // ページに描画
        val paint = Paint().apply {
            textSize = 16f
        }

        canvas.drawText("サンプル PDF レポート", 50f, 50f, paint)
        canvas.drawText("生成日時: 2026年4月15日", 50f, 100f, paint)
        canvas.drawText("内容: Android PDF 生成のデモンストレーション", 50f, 150f, paint)

        pdfDocument.finishPage(page)

        // ファイル保存
        val pdfFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "report.pdf")
        pdfDocument.writeTo(pdfFile.outputStream())
        pdfDocument.close()

        return@withContext pdfFile
    }
}

// 使用例
// val generator = PdfReportGenerator(context)
// val pdfFile = generator.generatePdfReport()

PdfRenderer での実装例

用途:PDF ファイルの表示・閲覧

// Kotlin での実装例(2026年版)
import android.content.Context
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import java.io.File

class PdfViewerUtil(private val context: Context) {

    fun renderPdfPage(pdfFile: File, pageNumber: Int): Bitmap? {
        return try {
            val fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
            val pdfRenderer = PdfRenderer(fileDescriptor)

            // ページ数確認
            if (pageNumber >= pdfRenderer.pageCount) {
                return null
            }

            // ページをビットマップとしてレンダリング
            val page = pdfRenderer.openPage(pageNumber)
            val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
            page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)

            page.close()
            pdfRenderer.close()

            bitmap
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }
}

// 使用例
// val viewer = PdfViewerUtil(context)
// val bitmap = viewer.renderPdfPage(pdfFile, 0)
// imageView.setImageBitmap(bitmap)

ライブラリを使った簡単実装

より高度な PDF 操作が必要な場合、以下のライブラリを検討してください。

iText(商用・オープンソース)

dependencies {
    // iText 5 (無料版)
    implementation("com.itextpdf:itextg:5.5.13.3")
}

PDFBox(Apache オープンソース)

dependencies {
    // Apache PDFBox
    implementation("org.apache.pdfbox:pdfbox-android:2.0.27.0")
}

権限設定(AndroidManifest.xml)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- ファイル読み書き権限(Android 13以前)-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <!-- Android 12+: Scoped Storage自動対応 -->

</manifest>

Kotlin での実装(2026年版 - モダン Android)

coroutine を使った非同期 PDF 生成

// Kotlin Coroutine での実装例
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class PdfGeneratorViewModel(private val generator: PdfReportGenerator) : ViewModel() {

    fun generatePdfAsync() {
        viewModelScope.launch {
            try {
                val pdfFile = generator.generatePdfReport()
                // UI 更新
                println("PDF 生成完了: ${pdfFile.absolutePath}")
            } catch (e: Exception) {
                // エラーハンドリング
                println("PDF 生成エラー: ${e.message}")
            }
        }
    }
}

// UI 層での使用例(Jetpack Compose)
@Composable
fun PdfGeneratorScreen(viewModel: PdfGeneratorViewModel) {
    Button(onClick = { viewModel.generatePdfAsync() }) {
        Text("PDF を生成")
    }
}

Android 12+ での変更点

Scoped Storage

Android 12 以降、外部ストレージへのアクセス方法が変わりました。

  • 変更点WRITE_EXTERNAL_STORAGE 権限が無視される
  • 推奨方法getExternalFilesDir() を使用(アプリ固有ディレクトリ)
  • 代替手段FileProvider または Intent.ACTION_CREATE_DOCUMENT
💡 推奨実装パターン:Android 12+ では getExternalFilesDir() を使用してアプリ固有ディレクトリにファイルを保存することが標準実装です。
// Android 12+ での推奨実装
val pdfDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
val pdfFile = File(pdfDir, "report_${System.currentTimeMillis()}.pdf")

// ファイルの保存・共有
val fileUri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", pdfFile)

// 共有インテント
val shareIntent = Intent(Intent.ACTION_SEND).apply {
    type = "application/pdf"
    putExtra(Intent.EXTRA_STREAM, fileUri)
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

よくあるエラーと対処法

FileNotFoundException

【エラーメッセージ】
java.io.FileNotFoundException: /storage/emulated/0/Documents/report.pdf (Permission denied)
【原因】
- ファイルの保存先に権限がない
- ディレクトリが存在しない
【対処法】
下記のように、Android 12+ 推奨の方法でファイルを保存してください。
// ❌ 間違い
val pdfFile = File("/sdcard/Documents/report.pdf")

// ✓ 正解(Android 12+ 推奨)
val pdfFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "report.pdf")
// または
val pdfFile = File(context.cacheDir, "report.pdf")

SecurityException(Android 12+)

【エラーメッセージ】
java.lang.SecurityException: Permission Denial: opening provider android.content.ContentProvider
【原因】
- 外部ストレージへのアクセス権限不足
- FileProvider の設定不備
【対処法】
1. FileProvider を res/xml/file_paths.xml で設定
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="documents" path="Documents/" />
</paths>

2. AndroidManifest.xml で宣言
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

OutOfMemoryError(大規模 PDF)

【エラーメッセージ】
java.lang.OutOfMemoryError: Failed to allocate [size] bytes
【原因】
- 複数ページの PDF を一度にメモリに読み込み
- 高解像度でレンダリング
【対処法】
// ページごとにレンダリング(メモリ効率化)
fun renderPdfPageSafely(pdfFile: File, pageNumber: Int): Bitmap? {
    return try {
        val fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
        val pdfRenderer = PdfRenderer(fileDescriptor)

        val page = pdfRenderer.openPage(pageNumber)

        // 低解像度でレンダリング(メモリ節約)
        val scale = 1.5f
        val bitmap = Bitmap.createBitmap(
            (page.width * scale).toInt(),
            (page.height * scale).toInt(),
            Bitmap.Config.RGB_565  // ARGB_8888 より軽い
        )
        page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)

        page.close()
        pdfRenderer.close()

        bitmap
    } catch (e: OutOfMemoryError) {
        e.printStackTrace()
        null
    }
}

実装例のリソース

関連記事

まとめ

Android で PDF を扱う場合、以下の判断基準で実装方法を選択してください。

  • PDF を生成したい場合PdfDocument を使用(または iText などのライブラリ)
  • PDF を表示・閲覧したい場合PdfRenderer を使用
  • 複雑な PDF 操作が必要な場合iTextPDFBox などのライブラリを導入

2026 年時点では、Kotlin + Coroutine での非同期処理が標準的な実装パターンです。また、Android 12 以降の Scoped Storage への対応は必須です。

この記事で紹介したコード例は、実際のプロジェクトに適応させて使用してください。不明な点や実装時のトラブルは、コメント欄でお気軽にお尋ねください。

2026/04/14

iOS UIViewController ライフサイクル完全ガイド【2026年UIKit + SwiftUI対応】

iOS UIViewControllerライフサイクル【2026年UIKit + SwiftUI 対応】

📌 関連記事: この記事は 【iOS】UIViewControllerのライフサイクル (Swift)(2019年)の2026年版アップデートです。UIKit に加えて SwiftUI 対応を追加しました。

2019年の記事では UIViewControlller のライフサイクルを紹介していた。2026年は状況が変わった。SwiftUI が標準になりつつあり、UIKit は「レガシー」扱いになりつつある。

ただ、既存プロジェクトや複雑な UI は UIKit が必須。両方理解すべき。


2019年 vs 2026年:フレームワークの立場

【2019年】

  • UIKit が主流
  • SwiftUI は登場したばかり(iOS 13)
  • ほぼ全員が UIKit を使っていた

【2026年】

  • SwiftUI が標準(iOS 16+対応アプリが主流)
  • UIKit は「既存プロジェクト対応」用途へ
  • 新規プロジェクト:SwiftUI 推奨
  • 既存プロジェクト:UIKit を理解する必要

UIKit:UIViewControllerのライフサイクル

全ライフサイクルメソッド(実行順序)

ビュー表示時

1. init(coder:) または init(nibName:bundle:)
   ↓
2. viewDidLoad()
   ↓
3. viewWillAppear(_:)
   ↓
4. viewWillLayoutSubviews()
   ↓
5. viewDidLayoutSubviews()
   ↓
6. viewDidAppear(_:)

ビュー非表示時

1. viewWillDisappear(_:)
   ↓
2. viewDidDisappear(_:)
   ↓
3. deinit (メモリから削除)

各メソッドの役割

class MyViewController: UIViewController {

    // ① 初期化時(1回のみ)
    override func viewDidLoad() {
        super.viewDidLoad()
        // UI の初期化、データの読み込み
        // 重い処理はここで実行 OK
    }

    // ② ビュー表示直前(毎回呼ばれる)
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // データの更新、画面リフレッシュ
        // 他の ViewController からの復帰時
    }

    // ③ レイアウト計算前
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        // Auto Layout の前処理
        // ビューのサイズが確定する前
    }

    // ④ レイアウト計算完了
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // サイズに依存した処理
        // フレーム値が確定した後
    }

    // ⑤ ビュー表示完了
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // アニメーション開始
        // ネットワーク通信開始
        // センサー (GPS など) の監視開始
    }

    // ⑥ ビュー非表示直前
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // タイマー停止
        // ネットワーク通信キャンセル
    }

    // ⑦ ビュー非表示完了
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // センサーの監視停止
        // リソース解放
    }

    // ⑧ メモリから削除
    deinit {
        // 最終クリーンアップ
        print("MyViewController が削除されました")
    }
}

SwiftUI:View のライフサイクル

SwiftUI では全く異なる

SwiftUI は 宣言型 UI で、UIViewControlller のような「ライフサイクルメソッド」は存在しない。

代わりに onAppear / onDisappear を使う。

struct ContentView: View {
    @State var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
        .onAppear {
            // ビュー表示時
            print("View appeared")
            // データ読み込み、API 呼び出しはここ
        }
        .onDisappear {
            // ビュー非表示時
            print("View disappeared")
            // リソース解放
        }
    }
}

SwiftUI のライフサイクル(概念的)

【初期化・表示】

  1. View 作成(body 計算)
  2. onAppear 実行
  3. ビュー表示

【状態変更】

  1. @State 更新
  2. body 再計算
  3. ビュー更新(自動)

【終了】

  1. onDisappear 実行
  2. View 削除

UIKit vs SwiftUI:ライフサイクル比較表

フェーズ UIKit SwiftUI
初期化 init(coder:) View 作成
初回ロード viewDidLoad() onAppear
表示前 viewWillAppear() なし(自動)
レイアウト viewWillLayoutSubviews() 自動(Combine)
表示完了 viewDidAppear() onAppear
非表示前 viewWillDisappear() なし(自動)
非表示完了 viewDidDisappear() onDisappear
終了 deinit View 削除

実装パターン:よくある用途別

UIKit版パターン 1:データの初期化

class UserProfileViewController: UIViewController {
    var user: User?

    override func viewDidLoad() {
        super.viewDidLoad()
        // ❌ ここで API 呼び出しは避ける
        // loadUser()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // ✅ ここで最新データを取得(毎回)
        loadUser()
    }

    func loadUser() {
        API.fetchUser { [weak self] user in
            self?.user = user
            self?.updateUI()
        }
    }
}

SwiftUI版パターン 1:データの初期化

struct UserProfileView: View {
    @State var user: User?
    @State var isLoading = false

    var body: some View {
        VStack {
            if let user = user {
                Text(user.name)
            }
        }
        .onAppear {
            loadUser()
        }
    }

    func loadUser() {
        isLoading = true
        API.fetchUser { user in
            self.user = user
            isLoading = false
        }
    }
}

UIKit版パターン 2:センサーの監視開始・停止

class MapViewController: UIViewController {
    var locationManager: CLLocationManager?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // ✅ ビュー表示時に監視開始
        locationManager = CLLocationManager()
        locationManager?.startUpdatingLocation()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // ✅ ビュー非表示時に監視停止
        locationManager?.stopUpdatingLocation()
    }
}

SwiftUI版パターン 2:センサーの監視開始・停止

struct MapView: View {
    @StateObject var locationManager = LocationManager()

    var body: some View {
        VStack {
            Text("Latitude: \(locationManager.latitude)")
        }
        .onAppear {
            locationManager.startUpdating()
        }
        .onDisappear {
            locationManager.stopUpdating()
        }
    }
}

class LocationManager: NSObject, ObservableObject {
    @Published var latitude = 0.0
    let manager = CLLocationManager()

    func startUpdating() {
        manager.startUpdatingLocation()
    }

    func stopUpdating() {
        manager.stopUpdatingLocation()
    }
}

UIKit版パターン 3:タイマー

class CountdownViewController: UIViewController {
    var timer: Timer?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.updateCountdown()
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        timer?.invalidate()  // ❌ これを忘れるとメモリリーク
        timer = nil
    }
}

SwiftUI版パターン 3:タイマー

struct CountdownView: View {
    @State var count = 10
    @State var timer: Timer?

    var body: some View {
        Text("\(count)")
            .onAppear {
                timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                    count -= 1
                }
            }
            .onDisappear {
                timer?.invalidate()  // 自動クリーンアップ
            }
    }
}

メモリリークを避けるためのベストプラクティス

UIKit

❌ メモリリーク:self をキャプチャしすぎ
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    API.fetchData { data in
        self.updateUI(data)  // self が保持され続ける
    }
}
✅ weak self で回避
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    API.fetchData { [weak self] data in
        self?.updateUI(data)  // self が解放される
    }
}

SwiftUI

// SwiftUI は自動的に管理してくれる場合が多い
struct MyView: View {
    @StateObject var viewModel = MyViewModel()

    var body: some View {
        Text(viewModel.data)
            .onAppear {
                viewModel.load()  // 自動的に適切にハンドル
            }
    }
}

2019年 vs 2026年:推奨される実装方針

状況 2019年の推奨 2026年の推奨
新規プロジェクト UIKit SwiftUI
既存 UIKit プロジェクト UIKit 継続 UIKit 継続(段階的に SwiftUI 導入)
複雑な UI UIKit UIKit(SwiftUI の限界回避)
学習用 UIKit SwiftUI

まとめ

【2019年】

UIViewController のライフサイクルを理解 = iOS 開発の基本

【2026年】

  • 新規プロジェクト:SwiftUI を使い、onAppear/onDisappear で対応
  • 既存プロジェクト:UIViewController ライフサイクルは依然重要
  • 両方の理解が必須(業界の過渡期)

実装判断:

  • iOS 16+ のみ対応 → SwiftUI
  • iOS 15 以下対応が必要 → UIKit
  • iOS 14 以下対応が必要 → 必ず UIKit

参考資料

2026/04/13

Ollama + Open WebUI:ローカルLLM構築【2026年完全ガイド】

ローカルで LLM を動かすなら Ollama + Open WebUI が標準。2025年の古い記事では断片的だったが、2026年版は実装から運用まで全て網羅。


なぜローカル LLM なのか

【クラウド API(ChatGPT など)】

  • 毎回データが外に出る(セキュリティ問題)
  • API 代がかさむ(月数千~数万円)
  • 速度が遅い場合がある(ネットワーク遅延)

【ローカル LLM(Ollama)】

  • データが自分のサーバーだけ(セキュアー)
  • 無料(初期構築後、ランニング費用ほぼゼロ)
  • 速度が速い(ネットワーク遅延ゼロ)

必要な環境

ハードウェア

【最小構成】

  • CPU:4コア以上(Ryzen 5 相当)
  • メモリ:8GB(推奨 16GB)
  • ストレージ:30GB(モデルサイズ次第)

【GPU あると大幅に高速化】

  • Nvidia GPU:RTX 3060 以上推奨
  • AMD GPU:RX 6700 XT 以上推奨
  • Mac M1/M2:統合 GPU で十分

【GPU なし場合】

  • CPU のみでも動作(ただし遅い)
  • 推論速度:1トークン/秒程度
  • GPU あり場合:10~20倍高速

ソフトウェア

【必須】

  • Docker
  • Docker Compose

【推奨(GPU使用時)】

  • Nvidia GPU ドライバー
  • CUDA Toolkit 12.0+

インストール手順

ステップ 1:Docker & Docker Compose インストール

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install docker.io docker-compose

# 権限設定(sudo なしで実行可能に)
sudo usermod -aG docker $USER
newgrp docker

ステップ 2:docker-compose.yml 作成

version: '3.8'

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
    gpus:
      - driver: nvidia
        all: true  # GPU 全て使用
    restart: always

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    ports:
      - "8080:8080"
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    depends_on:
      - ollama
    restart: always
    volumes:
      - webui_data:/app/backend/data

volumes:
  ollama_data:
  webui_data:

ステップ 3:起動

docker-compose up -d

# ログ確認
docker-compose logs -f

ステップ 4:モデルのダウンロード

# Ollama コンテナに入る
docker exec -it ollama bash

# モデルをダウンロード
ollama pull llama2:7b
# または
ollama pull mistral:7b
# または
ollama pull neural-chat:7b

ステップ 5:Open WebUI にアクセス

ブラウザで http://localhost:8080 を開く
→ ユーザー登録(初回のみ)
→ モデル選択して利用開始

GPU 設定(高速化のため)

Nvidia GPU の場合

# CUDA Toolkit インストール
sudo apt-get install cuda-toolkit-12-0

# docker-compose.yml で GPU を有効化(上記の例参照)
services:
  ollama:
    gpus:
      - driver: nvidia
        all: true

GPU が認識されているか確認

# コンテナ内で確認
docker exec -it ollama bash
ollama run llama2:7b

# プロンプトで GPU 使用状況を確認
# "GPU acceleration enabled" と表示されたら OK

推奨モデル一覧(2026年)

モデル サイズ 速度 品質 用途
Llama 2 7B 4GB 速い 標準 汎用・最初の1択
Mistral 7B 4GB 高速 高い 日本語・コード
Neural Chat 7B 4GB 速い 高い 会話型・日本語
Llama 2 13B 8GB 中程度 高い より精度重視
Code Llama 7B 4GB 速い コード特化 プログラミング
Mistral Large 34GB 遅い 非常に高い 重い処理用

初心者推奨:Mistral 7B(バランス型)


よくある落とし穴

1. GPU が認識されない

# 確認コマンド
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi

# 出力がなければ、nvidia-docker が必要
sudo apt-get install nvidia-docker2
sudo systemctl restart docker

2. メモリ不足エラー

"OOM Killed" または メモリ不足エラーが出た場合

対策 1:小さいモデルを使う

ollama pull llama2:7b  # 7B は 4GB
# ではなく
ollama pull phi:2.7b   # 2.7B は 2GB

対策 2:スワップメモリを増やす

sudo fallocate -l 16G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

3. Open WebUI が Ollama に接続できない

# docker-compose.yml で
environment:
  - OLLAMA_BASE_URL=http://ollama:11434

# これが正しくないと接続失敗
# ホスト名は「ollama」(service 名)
# ポートは 11434(Ollama のデフォルト)

4. 応答が遅い(GPU 未使用の場合)

# GPU が使われているか確認
docker exec -it ollama ollama ps

# "GPU acceleration disabled" が出たら GPU 未認識
→ 上記「GPU が認識されない」の対策を実施

実装時のベストプラクティス

1. モデルの永続化

volumes:
  ollama_data:/root/.ollama  # ← ここにモデルが保存される

# コンテナ削除後も モデルが残る
docker-compose down  # コンテナ削除
docker-compose up -d  # 再起動(モデルダウンロード不要)

2. Web UI のセキュリティ設定

open-webui:
  environment:
    - OLLAMA_BASE_URL=http://ollama:11434
    # ローカルホストのみアクセス許可
    - WEBUI_AUTH_REQUIRED=true  # ユーザー認証必須

3. 複数モデルの管理

# ダウンロード済みモデルを確認
docker exec -it ollama ollama list

# 不要なモデルを削除
docker exec -it ollama ollama rm llama2:7b

4. バックアップ

# モデルデータをバックアップ
docker cp ollama:/root/.ollama ./ollama_backup

# WebUI のデータ(会話履歴)をバックアップ
docker cp open-webui:/app/backend/data ./webui_backup

トラブルシューティング

ポート競合

"Address already in use" エラーが出た場合

docker ps  # 既存コンテナを確認
docker stop <コンテナ ID>
docker-compose up -d  # 再起動

メモリリーク

# コンテナのメモリ使用量を監視
docker stats

# メモリが増え続ける場合、モデルを再ロード
docker restart ollama

ネットワーク接続の問題

# コンテナ間の通信確認
docker exec -it open-webui ping ollama

# 失敗する場合、ネットワークドライバを確認
docker network ls
docker network inspect <network_name>

2025年版との変更点

項目 2025年 2026年
モデルサイズ Llama 2 のみ 複数選択肢
GPU 設定 簡潔 詳細なトラブルシューティング
セキュリティ 未記載 ユーザー認証設定
バックアップ 未記載 手順明記
トラブルシューティング なし 充実

次のステップ

【1日目】

  • Ollama + Open WebUI をセットアップ
  • モデル 1つをダウンロード・テスト

【1週間後】

  • GPU 最適化を試す
  • 複数モデルを試して相性を確認

【1ヶ月後】

  • 実運用に向けて、セキュリティ設定を強化
  • API 化(他アプリから Ollama を呼び出し)を検討

参考資料

Android Jetpack Compose LazyGrid で斜めスクロール実装【2026年版】

Android 斜めスクロールView実装 ~2026年Jetpack Compose版

📌 関連記事: この記事は 【android】ScrollViewで縦横斜めにスクロール(2015年)の2026年版アップデートです。当時のコンセプトが、今どう実装されているかをご紹介します。

かつて「Androidで斜めスクロールって実装できないのか?」という問い合わせがあった。2015年時点では標準APIになく、自作が必須だった。今は Jetpack Compose の登場で、状況が大きく変わった。

2026年なら、Compose + LazyGrid で 1行で解決できる。


昔の問題(2015年)vs 現在(2026年)

2015年:框組みで解決しようとしていた時代

ScrollView(縦)+ HorizontalScrollView(横)
  ↓
マージして「斜めスクロール」を実現
  ↓
カスタムViewGroupを自作

当時は効果測定も「コンセプト段階」で、実装されていなかった。

2026年:Jetpack Compose なら標準

LazyVerticalGrid / LazyHorizontalGrid
  ↓
フリングスクロール、慣性スクロール完備
  ↓
nested scroll も自動対応

標準APIs の充実で、カスタム実装が不要になった。


2026年の実装:Jetpack Compose

ステップ 1:依存関係の追加

dependencies {
    implementation "androidx.compose.foundation:foundation:1.7.0"
    implementation "androidx.compose.material3:material3:1.3.0"
    implementation "androidx.activity:activity-compose:1.9.0"
}

ステップ 2:LazyGrid で斜めスクロール実現

@Composable
fun DiagonalScrollView() {
    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        modifier = Modifier
            .fillMaxSize()
            .padding(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(100) { index ->
            GridItem(index)
        }
    }
}

@Composable
fun GridItem(index: Int) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(120.dp),
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFF6200EE)
        )
    ) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = "Item $index",
                color = Color.White,
                fontSize = 16.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}

ステップ 3:複数方向スクロール(横 + 縦)

単純な縦スクロール + 横スクロール両対応が必要なら:

@Composable
fun BiDirectionalScroll() {
    LazyVerticalGrid(
        columns = GridCells.Fixed(4),
        modifier = Modifier
            .fillMaxSize()
            .horizontalScroll(rememberScrollState())
    ) {
        items(200) { index ->
            GridItem(index)
        }
    }
}
注意:
  • horizontalScroll()LazyVerticalGrid でも使用可能
  • フリングスクロール、慣性スクロールは自動対応
  • パフォーマンスは Compose の仮想化により最適化済み

実装時の落とし穴

1. NestedScrollConnection の設定を忘れずに

親スクロールと子スクロールが両方ある場合:

val nestedScrollDispatcher = remember { NestedScrollDispatcher() }

LazyVerticalGrid(
    columns = GridCells.Fixed(3),
    modifier = Modifier
        .nestedScroll(nestedScrollDispatcher.asNestedScrollConnection())
) {
    // ... items
}

2. パフォーマンス:アイテム数が大きい場合

❌ 悪い例:すべてのアイテムを描画
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
    repeat(10000) { index ->
        GridItem(index)  // 全項目メモリに残る
    }
}
✅ 良い例:仮想化による遅延描画
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
    items(10000) { index ->
        GridItem(index)  // 見える範囲だけ描画
    }
}

3. スクロール位置の保存

ユーザーが別の画面から戻ってきた時、スクロール位置を復元:

val listState = rememberLazyGridState()

LazyVerticalGrid(
    columns = GridCells.Fixed(3),
    state = listState
) {
    items(100) { index ->
        GridItem(index)
    }
}

// スクロール位置を保存
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .collect { index ->
            // ローカルDBに保存
            saveScrollPosition(index)
        }
}

2015年実装 vs 2026年実装:比較

項目 2015年 2026年
フレームワーク View(XML + Java) Jetpack Compose(Kotlin)
コード行数 100~200行 30~50行
パフォーマンス メモリ負荷大(全アイテム保持) 軽量(仮想化)
実装難度 中~高(カスタムViewGroup必須) 低(標準APIで解決)
フリングスクロール 手実装 自動対応
nested scroll対応 複雑 シンプル
テスト容易性 高(Composable テスト用ツール充実)

代替案:特殊な要件がある場合

要件 1:斜め45度のカスタムスクロール

@Composable
fun DiagonalCustomScroll(
    angle: Float = 45f  // 45度斜めスクロール
) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        modifier = Modifier
            .fillMaxSize()
            .rotate(angle)  // 斜めに回転
    ) {
        items(100) { index ->
            GridItem(index)
        }
    }
}

要件 2:スクロール速度をカスタマイズ

@Composable
fun CustomSpeedScroll() {
    val scrollState = rememberScrollState()

    Column(
        modifier = Modifier
            .verticalScroll(
                scrollState,
                flingBehavior = ScrollableDefaults.flingBehavior()
            )
    ) {
        repeat(100) { index ->
            Text("Item $index", modifier = Modifier.padding(16.dp))
        }
    }
}

まとめ

2015年:「カスタムViewGroup自作が必須」

2026年:「Compose の LazyGrid で 30行で完成」

Android 開発は急速に進化している。かつての「困難な実装」が「標準機能」に変わるのは珍しくない。

古い記事の実装方法に固執するのではなく、2026年のプラットフォーム能力を活用する方が圧倒的に効率的。


参考資料

最近、Excel で選択したセルの内容を Ollama で要約・添削できる Add-in を作ってみた

最近、Excel で選択したセルの内容を Ollama で要約・添削できる Add-in を作ってみた。

正直なところ、想定外の落とし穴がいっぱいあった。特に「ローカルネットワーク別 PC の Ollama に、Excel Add-in から安全にアクセスする」という部分が、予想以上に複雑だった。その試行錯誤プロセスを記録しておく。


背景:なぜこんなもの作ったのか

仕事で長めの文章を扱うことが多い。メール、レポート、提案書とか。

最初は ChatGPT API で要約・添削をやってみたけど、3 つの理由でやめた:

  1. データを外に出したくない - 機密文書を API に送るのは避けたい
  2. API コストが地味に積もる - 毎日使うと月額が結構かかる
  3. レスポンス待ちが遅い - 社内 LAN 上なら圧倒的に速い
そこで目を付けたのが Ollama。ローカルネットワーク別 PC で Ollama を建てて、そこに直接アクセスできれば、データも保護できるし、レスポンスも速い。

「Excel から直接使えたら最高だな」と思いついて、Add-in を作り始めたのが始まり。


技術構成

実装してみた構成は以下:

【Excel Add-in】
  ↓ HTTPS でロード
https://localhost:8888(React UI の配信)
  ↓ HTTP リクエスト
【Go バックエンド】(localhost:8888)
  ↓ ローカルネットワーク経由
【Ollama API】(http://172.19.10.10:11434)

バックエンド: Go(標準 net/http パッケージ)

  • シンプルに HTTP サーバーを立てるだけなので、フレームワークは不要だと判断
  • 自己署名証明書での HTTPS 対応

フロント: React

  • Office JavaScript API で Excel セルの読み書き
  • Go API への HTTP 通信

Ollama

  • ローカルネットワーク別 PC 上で実行
  • セルの内容を送信 → 要約・添削結果を取得

予想外の落とし穴:Excel Add-in の HTTPS 必須ポリシー

実装し始めてすぐにぶつかったのが、Excel Add-in は HTTP ではなく HTTPS が必須という制約。

「localhost で動かすんだから HTTP でいいか」と思ってた。甘かった。

さらに複雑だったのが、Ollama が ローカルネットワーク別 PC にあること。

構図:
  PC A(Excel を使ってる)
    ↓
  自分の PC(Go + React のサーバー)
    ↓ ローカルネットワーク
  PC B(172.19.10.10 に Ollama が動いてる)

やりたいこと:

  • Excel Add-in の UI(HTML/React)を自分の PC から HTTPS で配信
  • Add-in から Ollama(別 PC)に HTTP でアクセス

問題:

  • Excel Add-in は HTTPS 配信が必須だけど、自己署名証明書では「信頼されていない」と判定される
  • 別 PC の Ollama にはネットワーク経由でアクセスする必要がある

解決策 1:オレオレ証明書を localhost で動かす

まず自己署名証明書を生成した。

openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes \
  -subj "/CN=localhost"

この証明書と秘密鍵で、Go の net/http サーバーを立ち上げる。

package main

import (
    "net/http"
)

func main() {
    // ハンドラーを登録
    http.HandleFunc("/api/summarize", handleSummarize)
    http.HandleFunc("/api/proofread", handleProofread)

    // HTTPS で起動(自己署名証明書使用)
    http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil)
}

これで https://localhost:8443 で HTTPS サーバーが起動。

ただし、ここまでではまだ不十分。Excel Add-in のコンテキストでは、この自己署名証明書が「信頼されていない」と判定される。


解決策:自己署名証明書 + ネットワークルーティング

ここが工夫の見せどころ。

2 つのポイントで解決した:

  1. localhost で自己署名証明書を使った HTTPS サーバー起動 - Excel Add-in の HTTPS 必須要件を満たす
  2. Go バックエンド - HTML/React を配信しつつ、Ollama への API リクエストを中継
【構図】
Excel Add-in
  ↓ HTTPS でロード
https://localhost:8888
  ↓ HTML/React の配信 + API エンドポイント
【Go サーバー】
  ├─ /(HTML/React UI を配信)
  └─ /api/summarize, /api/proofread(Ollama に転送)
    ↓ HTTP でローカルネットワーク経由
http://172.19.10.10:11434
  ↓
【Ollama API】

実装

まず自己署名証明書を生成。

openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes \
  -subj "/CN=localhost"

Go のサーバーコード:

package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
)

func main() {
    // Ollama API へのリバースプロキシを設定
    ollamaURL, _ := url.Parse("http://172.19.10.10:11434")
    reverseProxy := httputil.NewSingleHostReverseProxy(ollamaURL)

    // HTML/React UI を配信
    fs := http.FileServer(http.Dir("./public"))
    http.Handle("/", fs)

    // /api/generate は Ollama に中継
    http.HandleFunc("/api/generate", func(w http.ResponseWriter, r *http.Request) {
        reverseProxy.ServeHTTP(w, r)
    })

    // HTTPS で起動(自己署名証明書)
    http.ListenAndServeTLS(":8888", "server.crt", "server.key", nil)
}

React 側から Ollama API を呼び出すときは、相対パス /api/generate を使う。

// React コンポーネント
const handleSummarize = async (cellContent) => {
    const response = await fetch('/api/generate', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            model: 'llama2:7b',
            prompt: `要約してください:\n\n${cellContent}`,
            stream: false
        })
    });

    const result = await response.json();
    return result.response;
};

重要なポイント:

  • localhost:8888 で自己署名証明書を使った HTTPS サーバーを起動
  • Excel Add-in のセキュリティ要件(HTTPS 配信)を満たす
  • React は相対パス /api/generate で Ollama に通信(実際には Go バックエンドが仲介)
  • Go バックエンド内で、localhost から 172.19.10.10 へのネットワークルーティングを実現
  • シンプル:プロキシツール不要で、Go だけで実装完結

セルを選択→要約・添削:実装の流れ

実装が決まったら、次は実用機能。

Excel で文章が入ってるセルを選択して、「要約」「添削」ボタンを押すと、Ollama で処理してセルに結果を返す。

流れ:

  1. セル選択時のイベント取得
await Excel.run(async (context) => {
    const cell = context.application.getSelectedData();
    // セルの内容を取得
});
  1. Go API に送信
const summarized = await fetch('https://localhost:8888/api/summarize', {
    method: 'POST',
    body: JSON.stringify({ text: cellContent })
});
  1. Ollama から要約結果を取得
  • モデルはデフォルトで llama2:7b を使用
  • プロンプトエンジニアリングで「要約」「添削」を区別
  1. 結果をセルに書き込み
await Excel.run(async (context) => {
    context.worksheets.getActiveWorksheet()
        .getRange("A1")
        .values = [[result]];
});

実装時の工夫:

  • ローディング表示 - Ollama はレスポンスに数秒かかるので、「処理中...」を表示
  • エラーハンドリング - ネットワーク接続や Ollama が落ちてる場合の対応
  • タイムアウト設定 - 長すぎる文章は処理に時間がかかるので、上限を設ける
  • プロンプト最適化 - 同じモデルでも「要約」「添削」でプロンプトを変える
// 要約用プロンプト
func getSummarizePrompt(text string) string {
    return fmt.Sprintf(
        "以下のテキストを簡潔に要約してください。箇条書きで3点まで。\n\n%s",
        text,
    )
}

// 添削用プロンプト
func getProofreadPrompt(text string) string {
    return fmt.Sprintf(
        "以下のテキストの文法、表現、敬語を確認して、改善案を提示してください。\n\n%s",
        text,
    )
}

実際に使ってみて

完成して 1 ヶ月ほど使ってみた感想。

便利だったこと:

  1. データが外に出ない - 機密文書を扱うときは本当に安心。API に送ってる時間を気にしなくていい
  2. レスポンスが速い - ローカルネットワーク経由なので、ChatGPT API より圧倒的に高速。待ち時間がストレスにならない
  3. コストがゼロ - Ollama はローカル実行なので追加コストなし。PC のリソース(GPU)を使ってる分だけ

予想外だったこと:

  • 要約の質が思ったより良い - llama2:7b で十分実用的。わざわざ大型モデルに乗り換える必要がない
  • 添削より要約の方が使用頻度が高い - 最初は「添削機能メインで作ろう」と思ってたけど、実用では「要約」がメイン用途に
  • 複数人での利用が想定より難しい - 自分の PC でプロキシを立ててるので、他の PC からはアクセスできない。チーム展開するなら、プロキシを別サーバーに移す必要があった

今後の改善案:

  • Ollama の複数モデル対応 - llama2 以外の軽量モデル(mistral など)も選べるようにしたい
  • Web UI での設定画面 - 今は Go コード内でハードコードしてる設定を、UI で変更できるように
  • ローカルプロキシをサーバー化 - チーム利用を想定して、プロキシを別サーバーに移す

技術的な学び

このプロジェクトで学んだことが 3 つ。

1. Excel Add-in のセキュリティ要件は思ったより厳しい

Excel が HTTPS・自己署名証明書・CORS まで厳密にチェックする。SPA(Single Page Application)の世界と違って、企業向けアプリケーションの厳格さを感じた。

2. ローカル接続は「仲介役」として超有効

別ネットワークにある Ollama にアクセスする時、直接アクセスではなくローカルサーバー経由にすることで、セキュリティと利便性のバランスが取れた。

3. ローカル LLM の強さ

API の待ち時間がゼロに等しく、コストもかからない。確度は ChatGPT より落ちるかもしれないが、実務用途(要約・添削)には十分。「LLM の大型化よりローカル化の時代が来たのかも」と感じた。


まとめ

「Go + React + Ollama」という組み合わせで Excel Add-in を作った。

最大の工夫は「自己署名証明書 + ネットワークルーティング」という組み合わせで、ローカルネットワーク別 PC の Ollama に安全にアクセスする仕組み。

完成してみると、想定外の利便性があった。特に「データが絶対外に出ない」という安心感と「レスポンスの速さ」は、クラウド API では代替できない価値。

チーム展開や本番運用となると、プロキシのサーバー化やアーキテクチャの見直しが必要だけど、個人ツールレベルなら「自己署名証明書 + ネットワークルーティング」で十分実用的。

ローカル LLM の活用方法は、これからも増えていくんだろうなと思った。