2026/04/21

フリーランスエンジニアの確定申告 freelance engineer tax return deductible expenses home office

フリーランスエンジニアの確定申告——家賃・光熱費・PC代金、実際どこまで経費にできるか

フリーランスになって最初に戸惑ったのが確定申告だ。会社員時代は年末調整で終わっていたのが、自分でやることになる。「経費で落とせる」という話は聞いていたけど、具体的に何をどう計上していいかがわからなくて、最初の年は国税庁のサイトを行ったり来たりしながら手探りで申告した。

今回は特に迷いやすい家賃・光熱費・PC代金の扱いを中心に、自分が実際にやっている経費計上の実例を書いていく。税理士ではないので最終判断は専門家に確認してほしいが、フリーランスエンジニアとしての実務経験をもとにした参考情報として読んでもらえればと思う。

前提:青色申告か白色申告か

経費の話の前に、申告方法の選択に触れておく。青色申告(65万円控除)を選んだ方が節税効果は圧倒的に大きい。開業届と青色申告承認申請書を税務署に提出すればいいだけなので、フリーランスを続けるなら迷わず青色申告にしておくことをすすめる。

青色申告の主なメリット:

  • 最大65万円の特別控除(e-Tax + 複式簿記の場合)
  • 赤字を3年間繰り越せる
  • 少額減価償却資産の特例(30万円未満の資産を購入年に一括で経費計上できる)

3つ目の少額減価償却資産の特例が、後のPC代金の話に直接効いてくる。

家賃——按分の計算方法と実際の割合

自宅で仕事している場合、家賃の事業用割合を「按分(あんぶん)」によって経費計上できる。全額をそのまま経費にはできないが、業務に使っている部分を合理的な基準で計算すれば問題ない。

面積比での按分(最もシンプル)

税務署に根拠として説明しやすいのは床面積の比率だ。

# 按分計算の例
自宅の床面積:60㎡
仕事部屋の面積:10㎡
按分割合:10 ÷ 60 ≒ 16.6%

月家賃:100,000円
経費計上額:100,000 × 16.6% = 16,600円/月
年間経費:16,600 × 12 = 199,200円

専用の仕事部屋がない場合、使用時間の割合で按分する方法もある(「1日8時間、週5日業務利用」など)。ただ面積比の方が根拠を説明しやすいので、書斎コーナーでも明確に区画できるなら面積での按分が楽だと思う。

按分割合が極端に高い(50%超)と税務調査の際に説明を求められやすいという話を聞いたことがある。自分は30%以内を目安にしている。

賃貸契約の名義と注意点

賃貸契約の名義が個人名でも業務用に使用していれば按分で計上できる。ただし賃貸契約によっては事業利用を禁止しているものもあるので、契約内容は確認しておく方がいい。

光熱費——電気代・ネット回線・スマートフォン

光熱費も家賃と同様に按分で経費計上できる。主に電気代・インターネット回線料金・スマートフォン代が対象になる。

電気代

電気代は家賃と同じ面積比で按分するのが一般的だ。按分割合を統一しておくと帳簿の整合性が取りやすい。

# 電気代の按分例
月の電気代:15,000円
按分割合:16.6%(家賃と同一)
経費計上額:15,000 × 16.6% ≒ 2,490円/月
年間経費:約29,880円

夏場にエアコンをよく使う月は電気代が上がるが、按分割合は年間で固定しておく方が管理が楽だ。

インターネット回線

自宅のインターネット回線は業務利用の割合が高いので、自分は70%で按分している。リモートワークが主体なら70〜80%でも合理的な説明がつくと思う。月額5,000円の光回線なら年間で42,000円(70%計上の場合)の経費になる。

スマートフォン

クライアントとの連絡・二要素認証・外出先でのリモートアクセスに使うので50〜60%で按分している。プライベートと業務で端末を分けていれば100%計上できるが、そこまでする人は少数派だと思う。

PC代金——購入価格によって処理が変わる

エンジニアにとって一番金額が大きい可能性があるのがPC購入費だ。金額によって3つの処理パターンに分かれる。

10万円未満:消耗品費として一括計上

10万円未満のPCは購入した年に全額を消耗品費として計上できる。エントリーモデルのノートPCや中古PC、モニター・キーボード・マウスなどの周辺機器の多くがこの範囲に入る。

10万円以上30万円未満:少額減価償却資産の特例(青色申告限定)

ここが青色申告の大きなメリットだ。租税特別措置法28条の2の規定により、青色申告者は30万円未満の資産を購入した年に全額を一括で経費計上できる。MacBook ProやThinkPad上位モデルなど、エンジニアが使うPCの多くがこの範囲に収まる。

# 少額減価償却資産の特例(青色申告)
対象:30万円未満の減価償却資産
処理:購入した年に全額を一括経費計上
上限:年間合計300万円まで

例:MacBook Pro 14インチ(税込220,000円)を業務用で購入
→ 購入年に220,000円を全額経費計上(工具器具備品 または 消耗品費)

ただし年間で合計300万円を超える場合は特例の対象外になる。通常のフリーランスエンジニアで300万円を超えることはまずないが一応確認しておく。なお本特例は時限措置で、現在は令和10年3月31日まで延長されている。適用前に最新の税制改正情報を確認することをすすめる。

30万円以上:減価償却

30万円以上の資産は通常の減価償却になる。パソコンの法定耐用年数は4年(サーバー用途は5年)なので、定額法なら購入金額を4年間に均等に分けて計上する。

# 減価償却の計算例(定額法)
購入金額:400,000円
法定耐用年数:4年
年間の償却費:400,000 ÷ 4 = 100,000円/年(4年間)

少額減価償却資産の特例が使えないので、30万円以上のPC購入は節税の観点では不利になる。機材選定の際に30万円を境界として意識しておくのも一つの考え方だ。

ソフトウェア・SaaSも忘れずに

見落としやすいのが月額・年額のサブスクリプション費用だ。以下のようなものは全て経費になる。

  • 開発ツール:JetBrains IDEライセンス、GitHub Team/Enterprise
  • クラウドサービス:AWS・GCP・Azureの利用費(業務用)
  • デザイン・ドキュメント:Figma、Notion、Confluence
  • AI ツール:Claude API、ChatGPT Plus、GitHub Copilot
  • 書籍・技術書:業務に関連する書籍(新聞図書費)
  • 勉強会・カンファレンス:参加費(研修費)、交通費

これらを月次で漏れなく計上していくと、年間でかなりの経費額になる。会計ソフトのクレジットカード連携を使うと計上漏れが減る。

まとめ

  • 家賃・光熱費は面積比で按分。電気代・回線費・スマートフォンも合理的な割合で計上できる
  • PC代金は30万円未満なら青色申告の少額減価償却資産特例で購入年に一括計上が可能
  • 30万円以上のPCは4年間の減価償却。節税効果の観点では30万円未満の選択が有利
  • SaaS・クラウドサービス・書籍など月次で発生する費用の計上漏れに注意
  • 青色申告(65万円控除)は開業届と承認申請書を出すだけで選択できる。フリーランスなら必須

確定申告で判断に迷う部分が出てきたら税理士への相談が確実だ。最近はクラウド会計ソフト(freee・マネーフォワードクラウド確定申告)と組み合わせて、年1回だけ税理士にチェックしてもらうパターンが費用対効果が高いと感じている。

業務委託契約書のチェック項目についてはこちらの記事、単価交渉の実際についてはこちらの記事もあわせてどうぞ。

業務委託契約書で必ずチェックすべき5項目——フリーランスエンジニアが見落としやすい条項を解説

業務委託契約書を「だいたいこんな感じでしょ」と流し読みして署名してしまった経験はないだろうか。自分も最初の頃はほとんど読まずにサインしていた。後から「そんな条件が入っていたのか」と気づいて損をしたことが何度かある。今回はフリーランスエンジニアが業務委託契約書を確認する際に、特に見落としやすい5項目を整理する。2020年の民法改正で変わった「契約不適合責任」についても触れる。

1. 知的財産権(IP)の帰属

一番重要と言っても過言じゃないのがIPの帰属条項だ。業務委託で作ったプログラム・コード・ドキュメントの著作権が、最終的に誰のものになるかを定める条項がここに入る。

契約書に何も書いていない場合、著作権法の原則では「制作した人(受託側)に著作権が帰属」する。しかし多くの業務委託契約では「制作物の著作権はクライアントに帰属する」または「制作物の著作権は制作完了・報酬支払い完了をもってクライアントに譲渡する」という条項が入っている。これ自体は一般的だ。

問題になりやすいのは以下のパターンだ:

  • 既存コードの扱い:受託前から自分が持っていたライブラリ・フレームワーク・汎用モジュールが「制作物に含む著作権を全部譲渡」に巻き込まれるケース
  • 著作者人格権の不行使:「著作者人格権を行使しない」という条項。これ自体は商習慣上よくあるが、内容を把握した上で同意すべき
  • ポートフォリオ利用の可否:制作物を自分のポートフォリオや実績として公開できるかどうか

確認ポイント:既存の自作コードを流用する場合は、その部分の権利関係を契約前に明示しておくこと。「既存の自己所有コードは本契約の譲渡対象外とする」という一文を追加してもらうのが安全だ。

2. 競業避止条項

競業避止条項は「契約期間中または終了後〇年間、同業他社または競合するサービスの開発に関与しない」という内容だ。フリーランスの場合、これが複数クライアントを並行して持つことや、独立後の事業展開に影響することがある。

特に注意が必要なのは範囲と期間だ。「同業他社」の定義が広すぎる場合、ほぼ全てのITプロジェクトが対象になってしまう。「競合するサービス」という表現も曖昧で、解釈次第で広くなる。

契約終了後の競業避止は、フリーランスエンジニアに対して過度に制限的な内容であれば無効になる場合もある(職業選択の自由)。ただし、無効を主張するには紛争になるリスクがある。引き受ける前に交渉して範囲を絞るか、期間を短縮してもらう方が現実的だ。

確認ポイント:競業避止の対象範囲・期間・地理的範囲を具体的に確認する。「エンジニアとして他のITプロジェクトに参加することは制限しない」という明示があるか確認するか、追記してもらう。

3. 契約不適合責任(旧:瑕疵担保責任)

2020年4月施行の改正民法で、従来の「瑕疵担保責任」は「契約不適合責任」に変わった。名称が変わっただけでなく、内容も変更されている点に注意が必要だ。

契約不適合責任とは、納品物が「契約内容に適合していない」場合の責任だ。バグがある・仕様を満たしていない・品質が不足しているといったケースで、クライアントから修補請求・代金減額請求・損害賠償請求・契約解除をされる可能性がある。

旧法との主な違いは「発見から1年」という期間起算点が「引渡し時から知っていた場合は除く」という形に変わったこと、および請求できる内容が広がったことだ。

フリーランスエンジニアとして特に確認すべきなのは責任期間だ。民法の原則は「不適合を知った時から1年以内に通知」だが、契約で短縮・免除することもできる。逆に「引渡しから〇年間」と長期間の責任を課す契約もある。

確認ポイント:

  • 契約不適合責任の期間(引渡し後何ヶ月か)
  • 責任の範囲(バグ修正のみか、損害賠償も含むか)
  • 賠償額の上限設定があるか(「報酬額を上限とする」等)

4. 秘密保持(NDA)

秘密保持条項は多くの契約に入っているが、内容をきちんと読んでいないケースが多い。特に確認すべきなのは「秘密情報の定義」と「有効期間」だ。

秘密情報の定義が「業務に関して知り得た一切の情報」となっている場合、かなり広い範囲をカバーしている。転職先や別クライアントへの参考情報として使うことが難しくなる場合がある。

有効期間も確認が必要だ。「契約終了後〇年間」という条項がある場合、終了後もしばらく情報を使えない。業種・技術分野によっては仕事に影響することがある。

確認ポイント:秘密情報の定義範囲、有効期間、一般に公開された情報の除外規定があるか。

5. 再委託・下請けの可否

「業務の一部を第三者に再委託することができる」かどうかを定める条項だ。フリーランスとして案件を受けて、一部を別のエンジニアに頼む場合に関係してくる。

再委託を禁止または事前承認制にしている契約は多い。これを知らずに再委託すると契約違反になる。逆に「再委託可」でも「再委託先にも同等の秘密保持義務を課す」という条件がついている場合が多いので、再委託先との契約もきちんと整える必要がある。

確認ポイント:再委託の可否、事前承認が必要かどうか、再委託先への義務の連帯責任があるか。

まとめ

  • IP帰属:既存コードが巻き込まれないか、ポートフォリオ利用の可否を確認する
  • 競業避止:範囲と期間が過度に広くないか確認し、必要なら交渉で絞る
  • 契約不適合責任:民法改正(2020年)で瑕疵担保から変更。責任期間と賠償上限を確認する
  • 秘密保持:秘密情報の定義範囲と有効期間を確認する
  • 再委託:可否と事前承認の要否を確認する

契約書は読むのが面倒だが、引き受けた後で条件に気づいても遅い。特にIP帰属と競業避止は後から変えにくい条項なので、署名前に必ず確認することをすすめる。

準委任契約の稼働時間精算についてはこちらの記事、単価交渉の実際についてはこちらの記事もあわせてどうぞ。

フリーランスエンジニアの単価交渉、実際どうやるのか18年目が解説する

「単価を上げたいけど、どう切り出せばいいかわからない」「交渉して断られたらどうしよう」——フリーランスエンジニアとして働いていると、必ずこの壁にぶつかる。自分も最初の数年は単価交渉がまったくできなかった。怖かったというのが正直なところで、その結果として割に合わない仕事を長く続けた時期がある。今回は18年やってきた経験から、単価交渉の実際を書いていく。

単価交渉は「要求」ではなく「すり合わせ」

まず考え方の話から。単価交渉を「給料アップの要求」のように捉えると、クライアントとの関係がギスギスしやすい。自分が今意識しているのは「お互いの条件をすり合わせる会話」として設定することだ。

フリーランスの単価は市場価格・スキルセット・稼働条件・クライアントの予算によって決まる。どれか一つで決まるわけじゃないので、「自分はこれだけの価値がある」という主張より「今の条件はこういう状況で、こう変えられないか」という提案の方が通りやすい。

これを意識し始めてから、交渉の成功率がかなり上がった。

タイミングが9割:いつ交渉するか

単価交渉で最も重要なのはタイミングだと思ってる。同じ提案でも、タイミングによって通るか通らないかが変わる。

交渉が通りやすいタイミング

一番通りやすいのは「成果が出た直後」だ。プロジェクトのリリース直後、大きな問題を解決した直後、クライアントから感謝の言葉をもらった直後——このタイミングで切り出すと、相手も「確かにこの人には価値があった」と感じている状態なので話が進みやすい。

次に通りやすいのは「契約更新のタイミング」だ。3ヶ月更新・6ヶ月更新の契約なら、更新前に「次回から単価を見直したい」と伝えるのが自然な流れになる。これが一番揉めにくい方法でもある。

避けるべきタイミング

逆に避けた方がいいのは「プロジェクトの佳境」だ。クライアントが追い詰められている時期に単価交渉を持ち出すと、「今それを言うの?」という印象を与えやすい。結果として通ったとしても、関係に微妙な空気が残る。

具体的な交渉の進め方

Step 1:市場単価を把握する

交渉前に自分のスキルセットの市場単価を調べる。エージェントサービス(レバテック、Midworks等)の公開単価情報や、同じスキルセットのフリーランス求人を見ると相場感がわかる。「市場価格がこれくらいで、今の単価はそれより低い」という根拠があると交渉しやすい。

Step 2:提案の組み立て方

交渉時の言い方として効いたのは、単価を上げてほしいと直接言うより「稼働条件の変更と合わせて単価を見直したい」と提案する形だ。例えば「週4日稼働を週5日に増やす代わりに月額を〇〇円に」「新しい技術スタックのキャッチアップにコストがかかるので単価に反映してほしい」のように、相手にとって何らかのメリットか合理的な理由がある形にする。

Step 3:断られた時の対応

断られるのは当然ある。「今期の予算上難しい」「まだ評価期間中」といった返答が来ることも多い。この時に「じゃあいつなら検討できますか」と次のタイミングを確認しておくのが大事だ。曖昧に流されると永久に話が進まなくなる。

あと、断られた後に関係が悪化するかどうかは、交渉の仕方次第だと経験上思ってる。「要求して断られた」という形より「提案して今回は合わなかった」という形なら、次につながることが多い。

単価交渉で実際に使った数字の話

具体的な話をすると、自分がここ数年で意識しているのは「年に1回は必ず単価の見直し会話をする」ことだ。物価上昇・市場単価の変化・自分のスキルアップ——どれかの理由で単価を上げる根拠は毎年出てくる。

上げ幅としては1回の交渉で5〜15%が通りやすいラインだと感じてる。それ以上一気に上げようとすると「それは難しい」となりやすい。小幅の交渉を継続する方が、長期的に単価が積み上がっていく。

受託の場合は「要件が増えたら追加見積もり」「次の案件は単価を見直す」という形で、案件単位で調整する。一度決めた単価を案件途中で変えるのは難しいので、次の案件に繋げる段階で交渉するのが現実的だ。

単価を上げにくい状況から抜け出す方法

「このクライアントは予算がなくて上げられない」という状況が続く場合、正直に言うと単価交渉だけでは限界がある。その場合は別の案件を並行して探す、または新規クライアント獲得で単価の基準を上げる方が現実的だ。

一つのクライアントに依存していると交渉のカードが少なくなる。「断られたら困る」という状況では強く出られない。複数のクライアントを持つことが、長期的な単価維持・向上の一番の方法だと思ってる。

まとめ

  • 単価交渉は「要求」ではなく「すり合わせ」として設定すると通りやすい
  • タイミングは成果直後・契約更新前が最適。プロジェクト佳境は避ける
  • 市場単価を根拠にした提案と、相手にもメリットがある形にするのがポイント
  • 断られても「次はいつ検討できるか」を確認して、話を継続させる
  • 年1回の見直し・5〜15%の上げ幅を積み重ねる方が長期的に効く
  • 一クライアント依存を脱することが交渉力の根本的な強化につながる

準委任契約の稼働時間精算(中央割・上下割)についてはこちらの記事もあわせてどうぞ。

準委任契約を結ぶ前に確認すべき稼働時間の精算方式——中央割と上下割の違いと落とし穴

準委任契約を引き受ける時、金額ばかりに目がいって稼働時間の精算方式を読み飛ばしていないだろうか。これ、自分が若い頃にやらかしたパターンで、後から「あれ、思ったより入らなかった」と気づく原因になりやすい。今回は準委任契約の稼働時間精算について、実務でよく見る2つの方式と、引き受け前に必ず確認すべき項目を整理する。

準委任契約は「時間の提供」が対価の基本

まず前提として、準委任契約は成果物ではなく「業務への従事」が対価の対象になる契約だ。受託開発(請負)のように「完成物を納品する」義務はなく、クライアントの指示のもとで一定時間働くことへの報酬が発生する。

だから報酬の計算ベースは「時間」になる。月額固定の場合でも、その固定額の根拠には「月あたり何時間稼働する」という想定が必ず存在する。この部分の条件確認を怠ると、実際の稼働と報酬がずれてくる。

準委任の稼働時間精算方式は契約によって異なり、主に「中央割」と「上下割」の2種類がある。どちらかによって、月の稼働が想定より増えた時・減った時の収入への影響がまったく違う。

中央割とは何か

中央割は「一定の稼働時間の範囲内であれば月額固定」という精算方式だ。

例えば「月額50万円、精算幅140〜180時間」という契約の場合、140時間以上180時間以下で働けば月額50万円がそのまま支払われる。140時間未満になっても180時間を超えても、月額は変わらない。

この方式の特徴を整理すると:

  • 精算幅の中に収まる月は報酬が安定する
  • 180時間を超えて働いても追加報酬が発生しない(超過分が実質タダになる)
  • 繁忙で稼働が減っても一定水準以上なら減額されない

シンプルで予測しやすい反面、忙しい月に超過分が一切カウントされない点は見落としやすい。「先月はかなり頑張ったのに報酬が変わらなかった」という不満が出やすいのもこの方式だ。

上下割とは何か

上下割は「標準時間との差分を時間単価で精算する」方式だ。稼働が多ければ加算、少なければ減算される。

例えば「月額50万円、標準160時間、単価3000円/時」という契約で170時間働いた場合、超過10時間分の3万円が加算されて53万円になる。逆に150時間しか稼働できなかった場合、不足10時間分の3万円が減額されて47万円になる。

この方式の特徴:

  • 多く働いた分は正直に報酬に反映される
  • 稼働が足りなかった月は減額が発生する
  • 月ごとの収入が稼働時間によって変動する

受託開発と並行している場合、この「稼働が足りなかった月の減額」が特に問題になりやすい。受託の納期が重なった月は準委任の稼働が削られ、そのまま減額になる。受託で頑張った月なのに総収入が下がった、というケースが起きる。

月ごとの稼働時間が読めない問題

準委任契約を引き受ける時に見落としがちなのが、「月ごとに稼働できる時間は変わる」という現実だ。

カレンダー上の営業日数は月によって違う。祝日が多い月は稼働日が減る。年末年始・GW・お盆の時期は顕著だ。さらに自分の体調や、別の仕事の繁忙によっても稼働時間は変動する。

特に確認が必要なのは以下の点だ:

  • 祝日が多い月(例:5月や8月)の稼働時間はどう扱われるか
  • 有給休暇は稼働時間にカウントされるか否か
  • 標準稼働時間の設定が「月平均」なのか「毎月固定」なのか

祝日を稼働時間にカウントしない契約の場合、5月や9月は標準に届かないまま上下割の減算が発生するケースがある。「なんでこの月だけ減ってるんだろう」と気づいた時には既に終わった月の話になっている。

引き受け前に確認すべき項目チェックリスト

準委任契約を検討する際に、金額の他に必ず確認しておくべき項目を整理する。

  • 精算方式:中央割か上下割か、またはそれ以外か
  • 標準稼働時間:月何時間が基準になっているか
  • 精算幅・許容範囲:中央割の場合、上下の幅は何時間か
  • 超過・不足の時間単価:上下割の場合、1時間あたりいくらで計算されるか
  • 祝日の扱い:祝日は稼働時間にカウントされるか否か
  • 有給・休暇の扱い:取得した場合の稼働時間カウントはどうなるか
  • 精算サイクル:月次か、それ以外か

これを口頭で「だいたい月160時間で」と確認しただけで進めると、契約書に書いてある条件が想定と違っていたということが起きる。特にフリーランスとして複数案件を掛け持ちする場合は、稼働時間の変動リスクが大きいため、契約書の精算条件は細かく読むべきだ。

18年目が今でもやること

今でも新しい準委任契約を検討する時は、金額の次に精算方式と稼働時間の条件を確認する。これをやらずに引き受けて後悔した経験が自分にもある。

契約書を読むのは面倒くさい。でも引き受けた後で「こんな条件だったとは」となる方が、はるかに面倒くさい。準委任を検討している人には、金額と精算条件をセットで確認することを強くすすめる。

準委任契約と受託開発の同時並行は地獄だった。経験者が本気でやめておけという理由

受託開発と準委任契約を同時に抱えたことがある。今思い返すと、あの時期は本当にしんどかった。「どちらも回せる」と判断した自分の見通しが甘すぎた。今回はその経験を正直に書く。これから同じことを考えている人には、少しでも参考になれば。

準委任契約と受託開発、何が違うのか

まず整理しておくと、準委任契約と受託開発(請負契約)は契約形態が根本的に違う。

受託開発は「成果物を納品する」契約だ。期日までに動くものを完成させる責任がある。仕様が変わればスコープ管理が必要で、納品まで責任が続く。

準委任契約は「業務に従事する時間を提供する」契約だ。成果物への責任ではなく、一定時間クライアントの指示のもとで働くことへの対価が発生する。いわゆる時間売りで、SES(システムエンジニアリングサービス)がこれに当たることが多い。

この2つを同時に抱えると何が起きるか。端的に言うと「マインドセットの切り替えコストが想像以上に高い」という問題が発生する。

なぜ同時並行が地獄になるのか

受託開発と準委任を同時に回していた時期の自分が直面した問題を、そのまま書く。

問題1:スケジュール管理の主体が混在する

受託開発は、自分でスケジュールを組んで納期に向けて進める。裁量がある分、自己管理が全てだ。一方の準委任は、クライアントのスケジュールや指示に従って動く。この2つが同時に動いていると、「今日は受託の進捗を詰めるべきか、準委任先の要件ミーティングを優先すべきか」という判断が毎日発生する。

準委任先の割り込み対応が入るたびに、受託のコンテキストが飛ぶ。コードの続きを書こうとすると「あ、さっきの話はどこまで進んだっけ」から始まる。これが積み重なると、受託の進捗が目に見えて遅れる。

問題2:コミットメントの重さが非対称

準委任契約は基本的に時間のコミットメントだ。週何時間、月何時間という枠で動く。でも受託開発は時間じゃなくて成果物へのコミットメントで、「完成するまで終わらない」という性質がある。

準委任先から「来週この機能のレビューに参加してほしい」と言われると断りにくい。しかし受託の納期が迫っていれば、そちらを優先したい。このせめぎ合いが毎週起きる。どちらも大事なクライアントだから余計に消耗する。

問題3:頭の切り替えコストが見積もれない

午前中は受託のコードを書いて、午後は準委任先のオンラインミーティングに出て、夕方また受託に戻る。文字にすると普通っぽく見えるが、実際は全然普通じゃない。

受託の仕事はその案件の技術スタック・仕様・コンテキストに深く入り込んで初めて効率が出る。準委任先もそれぞれの技術環境・チーム文化・プロジェクトの背景がある。複数の「別世界」を一日の中で行き来するのは、頭の中のメモリ切り替えコストが想像以上にかかる。

これを実感したのは、あるバグを受託案件で調査していた夕方に準委任先のスラックが大量に流れてきた時だった。対応しながら、元のバグ調査に戻った時には完全にコンテキストが飛んでいた。30分かけて元の状態に戻るまで、実質何も進んでいない。

やめておくべき理由をもう一つ:品質が両方落ちる

同時並行の最大のリスクは「どちらも中途半端になる」ことだ。

受託は成果物の品質が直接クライアントの信頼に影響する。納品物に品質問題があれば、それは自分の責任として残る。準委任は時間売りとはいえ、出したアウトプットの質はそのまま評価に繋がる。

どちらも「とりあえずこれで」という水準になってくる。自分が許容できるラインより下の品質で出してしまったことが何度かあって、その時の後悔はかなり引きずった。

同時並行が許容されるケースと、そうでないケース

ただ、全ての同時並行が絶対ダメかというと、そうとも言い切れない。現実には複数の仕事を掛け持ちしている人は多い。

許容されやすいケース:

  • 受託案件が保守フェーズ(突発対応が少なく、週数時間で回る)
  • 準委任先の稼働が週1〜2日程度で、スケジュールの自由度が高い
  • どちらの案件も同じ技術スタックで、コンテキスト切り替えコストが低い

危険なケース:

  • 受託が開発フェーズ中で納期まで余裕がない
  • 準委任先がフルタイムに近い稼働を求めている
  • どちらのクライアントも「優先してほしい」と思っている

自分がはまったのは危険なケースの3つ全部に当てはまっていた。それでも「どうにかなる」と思って引き受けたのが失敗だった。

18年目が出した結論

準委任と受託の同時並行は、余裕があるように見える時期でも相当慎重に判断すべきだと思ってる。どちらかが開発の佳境に差し掛かっている時は、もう一方を断るか調整交渉するのが正直なところ。

「せっかく来た仕事を断るのはもったいない」という感覚はわかる。自分もそれで何度も判断を誤った。でも無理して両方引き受けた結果として品質と信頼を失う方が、長期的にはるかに損だ。

断ること、調整すること、スコープを絞ること。これも受託開発のスキルのうちだと、18年かけて腹に落ちた。

受託開発18年目の本音:見積もり・要件定義・お金の失敗談

受託開発を始めて18年が経った。気づいたら人生の半分近くをこの仕事に費やしてた計算になる。長いようで、振り返るとあっという間だったなとも思う。ただ、18年という年数は伊達じゃなくて、若い頃には絶対わからなかったことがたくさん見えてきた。今回はそのあたりを正直に書いてみようかなと思う。

受託開発の現実を若い自分に教えたかった話

20代の頃の自分は「良いコードを書けば評価される」と本気で信じていた。技術力があれば仕事は上手くいく、みたいな。でも実際はそんなに単純じゃなかった。

受託開発で一番大事なのは、コードよりもコミュニケーションだっていうことを、自分は5年くらいかけてようやく理解した。遅い。恥ずかしいくらい遅いんだけど、それが現実だった。

クライアントが本当に求めているものと、要件として書かれているものが一致しないことが多い。要件に書いてあることを完璧に実装しても、「なんか違う」と言われる経験を何度したか。最初のうちは腹も立ったし、なんで仕様通りに作ったのにって思ってた。でも今は違う。「なんか違う」が出る時は、要件定義の段階で何かがすれ違っていたんだと捉えるようになった。

要件の裏側にある「本当の課題」を掘り起こす

18年やってきて、一番スキルが上がったと感じているのは「ヒアリング」だ。コーディングじゃなくて、ヒアリング。なんかプログラマーっぽくないけど、それが正直なところ。

クライアントが「在庫管理システムを作ってほしい」と言ってきたとする。でもよく聞くと、本当の問題は在庫のカウントじゃなくて、発注タイミングの判断が属人化してることだったりする。その場合、在庫管理の画面を作るより、発注アラートの仕組みを整える方が価値が高い。

でもこれ、最初から言ってくれるクライアントはほぼいない。引き出さないといけない。そのためのヒアリング力は、正直ある程度の経験年数がないと身につかないと思ってる。

18年で変わった技術環境と、変わらなかったもの

18年前と今では、使っている技術がまるで変わった。当時はPHPとMySQLがメインで、フレームワークも今ほど洗練されていなかった。jQueryが出てきた時は「これは革命だ」って思ったし、クラウドが当たり前になった時も衝撃を受けた。最近はAIがコードを書く時代になって、また世界が変わりつつある。

変化のスピードに正直ついていくのが辛い時期もあった。40代に差し掛かってから特に。若い頃は新しい技術を覚えることが楽しかったのに、ある時期から「また覚え直しか」っていう感覚になった。これは受託開発あるあるなのかもしれない。

変わらないのは「問題解決の本質」

ただ、18年やってきて変わらないものもある。問題を正確に定義して、シンプルに解決する。これは技術が何になっても変わらない。AIがコードを書こうが、どのフレームワークが流行ろうが、「何を解決したいのか」を見極める力は人間がやらないといけない部分だとずっと思ってる。

少なくとも今のところは、そこが受託開発の仕事として残っている領域だと感じている。

お金の話を避けてきた結果どうなったか

これは正直に書く。受託開発をやっていく上で、一番失敗を重ねたのが価格設定と契約の部分だ。

若い頃は、値段の話をするのがなんとなく嫌だった。「仕事をください」という立場だったし、クライアントから「高い」と言われるのが怖かった。結果として、明らかに割に合わない案件を受け続けた時期がある。

工数を読み違えて赤字になったことも一度や二度じゃない。仕様変更が10回以上発生した案件で、追加費用を請求できずにボランティア状態になったこともある。それでも「次につながるかも」と言い聞かせてた。次につながった案件より、つながらなかった案件の方が多かったけど。

要件定義がない案件は見積もり2倍が正解

今は変わった。最初の見積もりで正直な金額を出せるようになったし、仕様変更が発生したら都度確認を取るフローを徹底している。これを始めてから、収益は安定した。

特に重要だと思っているのが、要件定義の有無で見積もりを変えるという考え方だ。要件定義書がない状態で「とりあえず作って」という案件は、最低でも通常見積もりの2倍を出すようにしている。経験上、要件定義ナシの案件は必ずといっていいほど途中で仕様が膨らむ。「そこまで想定してなかった」「やっぱりこの機能も欲しい」が連発する。2倍でもトントンになることがある。

見積もりスキルは受託開発のコアスキルだと今は思ってる。コーディングと同じくらい、もしかしたらそれ以上に。これが身につかないと、どれだけ技術力があっても受託では消耗する。

価格の話を避ける人は多い。特にエンジニア上がりの人は。でも受託で長く続けていくなら、ここは避けて通れない。

長続きする案件とそうでない案件の違い

18年で何百社かとやり取りしてきた。中には10年以上続いている付き合いのクライアントもいるし、1回限りで終わったところもある。長続きしている案件を振り返ると、共通点がある。

それは「定期的に話す機会がある」こと。納品したら終わり、じゃなくて、その後もちょいちょいコミュニケーションが続く関係のところほど長続きする。別に毎月何かを作り続けている必要はなくて、「最近どうですか」みたいな会話があるだけで全然違う。

逆に、最初の案件で「完璧に作って納品」することだけを目指すと、なかなか関係が続かない。これも若い頃には気づかなかったことで、納品クオリティを高めることに全力を注いでいた時期があった。もちろん品質は大事なんだけど、それだけじゃないんだよね。

18年やって思うこと

結局、受託開発って、クライアントの愚痴を整理して技術に翻訳する仕事なんだと思う。それを18年かけて、痛い目に遭いながら腹に落としてきた。

ヒアリング、見積もり、契約、関係の維持。これ全部、コーディングより先に身につけるべきだったかなと今は思ってる。でも若い頃の自分は絶対信じなかっただろうから、まあ仕方ない。

最後に一個だけ言っとく。要件定義がない案件の見積もりは、倍で出してみて。怒られることも断られることもあるけど、それより受けた後の消耗の方がしんどいから。

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年のプラットフォーム能力を活用する方が圧倒的に効率的。


参考資料