WEB+DB Press総集編のファイル名をいい感じにする

WEB+DB PRESSが休刊してからしばらく経ち、総集編が販売開始されました。

gihyo.jp

今回はこのDVDに含まれるファイルの名前をPowerShellでいい感じに変更する話になります。

前提

DVDには、webdb_vol01.pdfのような形式でファイルが保存されています。
これをGihyo Digital Publishingで個別購入した場合のファイル名に近い形式である WEB+DB PRESS Vol.1.pdf に変更します。

スクリプト

0埋めせずにVol.1.pdfのような形式にしたい場合は以下を参考にしてください。

$files = Get-ChildItem -Path . -Filter "webdb_vol*.pdf"

foreach ($file in $files) {
    $volNumber = $file.Name -replace "webdb_vol(\d+)\.pdf", '$1'
    $newFileName = "WEB+DB PRESS Vol.$volNumber.pdf"
    Rename-Item -Path $file.FullName -NewName $newFileName
}

ソートを意識して0埋めするVol.001.pdfのような名前にする場合のスクリプトは以下です。

$files = Get-ChildItem -Path . -Filter "webdb_vol*.pdf"

foreach ($file in $files) {
    $volNumber = $file.Name -replace "webdb_vol(\d+)\.pdf", '$1'
    $newFileName = "WEB+DB PRESS Vol.{0}.pdf" -f [int]$volNumber
    Rename-Item -Path $file.FullName -NewName $newFileName
}

これでファイル名がいい感じになりました。

法人設立した話

先日法人を登記してきたので、その大まかな流れについてお話ししたいと思います。
法人設立を検討している方にとって、参考になる情報があれば幸いです。

TL;DR

  • K@zuki. は、会社員個人事業主(茨谷企画)代表社員 の掛け持ち状態になっている
  • 個人事業の方が圧倒的に楽なので、そのことを踏まえて法人設立を検討することが大切
  • 登記するエリアは、手続き先の煩雑さや法人税率、補助金の観点からよく検討することをオススメ
  • 英語名表記は正当性を出すために、最初から定款に含めておくことが良さそう
  • Money Forward クラウド会社設立は便利

はじめに

K@zuki. は、会社員個人事業主(茨谷企画)代表社員 の掛け持ち状態になっているので、会社員としては現役で仕事をしていまし、個人事業主としても働いています。
個人事業主の一部事業を法人化したというイメージをしてもらいつつ、会社員業もしていると思っていればとOKです。

法人設立の理由

私が法人設立を決めた主な理由は二つあります。

  • 法人向けでしか商取引できない商品があったこと
  • 思いついたアイデアを個人とは別人格で試してみたかったこと

特に後者のアイデアを試す人格として法人を設立すること自体には意義がありました。
個人事業主でも良かったのではないか?というのはありますが、人生は短いですし何事も経験しておきたいなという意味合いでもありました。

経緯

さて法人設立する経緯なんですが、実は約2年間悩んでいました。
しかし、人生は思っているよりも短いと感じ、30歳になったことをきっかけに、「とりあえずやってみる」という精神で決断しました。

設立の流れ

雇用先との調整を含めず、準備を開始してから税務署への設立届を提出し終えるまで約3週間ほどかかりました。
前提として事業化したい内容についてはある程度決めている前提で話を進める点について留意してください。

1. 雇用先との調整

役員報酬が発生する場合は、社会保険料源泉徴収で調整が必要なケースがあるため、事前に相談しておく必要があります。
また、会社の労務規定上、他社の役員になることを許していないケースもあるので早めに目を通しておく必要があります。

2. 法人形態を決める

法人の形態(株式会社・合同会社)を決める必要があります。
法人の形態は、費用や資金調達の有無などの観点から選ぶと良いかと思います。

今回は合同会社で設立することにしました。

3. 会社名を決める

会社名は、日本語名・英語名両方検討しておいた方が良さそうです。
特にアプリ開発をする場合や、海外との取引をする場合には D-U-N-S® Number が必要になるケースもあるので、それを踏まえて日本語名・英語ともにできる限り被らない名前が良さそうです。

www.tsr-net.co.jp

また、英語名を考える時はドメイン名が取得できるかどうかも検討すると良さそうです。

4. 登記するエリアでバーチャルオフィスの契約

事務所がない場合の話になりますが、登記可能なバーチャルオフィスやレンタルオフィスを契約します。

この時、登記するエリアはしっかりと考えた方が良いです。 理由に関しては後からもでてきますが、補助金制度の充実度や、法人税率の違い、手続きする役所の多さに影響してきます。 自分が実際に事業をするエリアで検討することも重要ですが、今一度この辺に関してはチェックしておいた方が良さそうです。

5. Money Forward クラウド会社設立

ここからはMoney Forward クラウド会社設立を使って法人登記の準備を進めました。

biz.moneyforward.com

基本的にこれに従って印鑑の作成や定款の準備を進めて問題なさそうです。

6. 定款の作成

Money Forward クラウド会社設立のサービス内で、提携法人へ定款作成依頼をすることができます。
定款に関しては行政書士さんが詳しいかと思うので、どういった内容がいいのかは書きませんが、1点だけあると定款に含めると良さそうなものがあります。
それは「会社名の英語表記を定款に含める」です。
私は士業をやっているわけではないので正確なことは分かりませんが、公式文書に正式な英語名を記載しておく安心感は一定程度ありそうです。

7. 法人登記申請

クラウド会社設立で法務局へ提出するところまで進んだら、登記書類を印刷し押印します。
そして登記住所を管轄しているエリアの法務局に書類を提出しに行きます。
収入印紙を当日買うことになるので必要な金額を持っていけば、後は大体何とかなります。

登記には都道府県によって登記完了まで必要日数が変わるので、登記した際に登記完了予定日を教えてもらっておいてください。

8. 各種書類の準備

クラウド会社設立で進めると、年金事務所や税務署などへ提出する書類の準備が進められます。
登記申請が終わり次第、必要な書類を印刷して準備しておくことが望ましいです。
また各事務所は提出期限があるため、完了予定日次第では遅れることを確認しておいても良いかもしれません。

1点注意点なんですが、クラウド会社設立で表示される税務署などの提出先は正しい情報ではない場合があるので、
税務署や県税事務所・市町村役所のホームページで法人登記のやり方について確認してください。

9. 登記完了の確認

登記完了予定日に担当法務局へ電話するなどして登記が完了しているかを確認します。
不備がなければ終わっているはずですし、不備があれば事前に電話がかかってきます。

10. 印鑑カードの発行と各種証明書の発行

登記が完了していることを確認したら、法務局へ再度趣き、印鑑カードの発行と印鑑証明書・登記簿謄本の発行をします。
銀行口座の開設や、他の法人契約、税務署への法人設立届で何枚必要かを事前に把握しておきます。
こちらも当日収入印紙を購入するので必要な現金を持っていくようにします。

11. 法人設立届などの提出

8で準備した各種書類を年金事務所や、税務署、県税事務所、市町村役所など必要な役所に対して提出しに回ります。
郵送する方法もありますが、私は登記完了予定日が直前過ぎて休みをとって1日かけて巡りました。

非常に疲れました。
これで法人登記で必要な作業は大筋完了しました。

おわりに

以上が、私が経験した法人設立の大まかな流れを振り返りました。
法人設立には様々な手続きが必要ですが、適切な準備と心構えがあれば、スムーズに進められるはずです。
法人設立を検討している方の一助となれば幸いです。

別の記事で銀行口座開設や、D-U-N-S® Numberの取得について書きますね。

Docker Composeを使ってPalWorldのサーバを建てる

最近はPalWorldにどっぷりハマっています。
マルチプレイをする場合は、24時間起動し続けたいわけでなければ無理してサーバを建てるメリットはないんですが、 それでも建てたいかつ、比較的環境を汚したくない人向けの簡単な記事です。

ポート開放や正式な建て方に関しては公式や他のブログを参考にしてください。
あくまで弊宅ではKubernetesクラスタ上でPalWorldを動作させているため、 このやり方はデータのチェックや動作確認で利用していることが多いです。

TL;DR

  • PalWorldSettings.iniとGameUserSettings.iniを用意する
  • 記事のDockerfileとcompose.yamlを参考にして用意する

ディレクトリ構造

最終的なディレクトリ構造は以下のような形になります。

.
├── compose.yaml
├── Dockerfile
├── GameUserSettings.ini
├── PalWorldSettings.ini
└── SaveGames

【任意】SaveGamesを用意する

既にあるセーブデータがあり再利用したい場合は、どうにかしてSaveGamesに以下のような構造でデータを持ってきます。

SaveGames
└── 0
    └── A091215A35CD4039A7860DEE387F2838
        ├── LevelMeta.sav
        ├── Level.sav
        └── Players
            ├── abcedf00000000000.sav
            └── fdecba00000000000.sav

PalWorldSettings.iniを用意する

PalWorldSettings.iniはジェネレーターなどが有志で公開されているので、それを参考に用意してください。

補足

ジェネレーターや他のブログを見ればわかりますが、現状反映されない項目が多くあります。
ですが、誰かがホストとしてゲームを実行している場合はより多くの設定が反映されるため、 現時点ではサーバを用意するメリットは基本的に薄いケースがあります。

GameUserSettings.iniを用意する

こちらは他のやり方だとあまり用意するケースはないかと思いますが、 このファイルには起動時に利用するセーブデータの指定を行うことができます。

コンテナの再作成や既にあるサーバのデータを再利用したいケースのために用意しています。

[/Script/Pal.PalGameLocalSettings]
AudioSettings=(Master=0.500000,BGM=1.000000,SE=1.000000,PalVoice=1.000000,HumanVoice=1.000000,Ambient=1.000000,UI=1.000000)
GraphicsLevel=None
DefaultGraphicsLevel=None
bRunedBenchMark=False
bHasAppliedUserSetting=False
DedicatedServerName=A091215A35CD4039A7860DEE387F2838
AntiAliasingType=AAM_TSR
DLSSMode=Performance
GraphicsCommonQuality=0

A091215A35CD4039A7860DEE387F2838 に関しては文字列に書き換えます。
デフォルトでは16進数で命名されていますが、正直なんでも良いです。
既にあるセーブデータを流用する場合は、SaveGames/0配下のディレクトリ名に合わせて指定してください。

SaveGames
└── 0
    └── B091215A35CD4039A7860DEE387F2121

この場合A091215A35CD4039A7860DEE387F2838B091215A35CD4039A7860DEE387F2121に書き換えます。

Dockerfileを用意する

以下を参考にDockerfileを作成します。

FROM ubuntu:22.04

RUN apt update -qq \
    && apt install -y vim ca-certificates curl lib32gcc-s1 xdg-user-dirs \
    && apt clean \
    && rm -rf /var/lib/apt/lists/*

RUN useradd -m steam
USER steam

# FYI: https://developer.valvesoftware.com/wiki/SteamCMD#Linux
ENV STEAM_ROOT_PATH /home/steam/Steam
ENV STEAMCMD /home/steam/Steam/steamcmd.sh
RUN mkdir -p $STEAM_ROOT_PATH
WORKDIR $STEAM_ROOT_PATH
RUN curl -sqL "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" | tar zxvf - \
    && $STEAMCMD +quit
RUN mkdir -p $HOME/.steam/sdk64/ \
    && $STEAMCMD +login anonymous +app_update 1007 +quit \
    && cp $STEAM_ROOT_PATH/steamapps/common/Steamworks\ SDK\ Redist/linux64/steamclient.so $HOME/.steam/sdk64/

# # FYI: https://tech.palworldgame.com/dedicated-server-guide#linux
RUN $STEAMCMD +login anonymous +app_update 2394010 validate +quit
WORKDIR $STEAM_ROOT_PATH/steamapps/common/PalServer
RUN mkdir -p Pal/Saved/SaveGames
CMD ["./PalServer.sh"]

copmose.yamlを用意する

以下を参考にcompose.yamlを作成します。

services:
  palworld:
    build: .
    image: palworld
    restart: always
    ports:
      - 32111:8211/udp
    volumes:
      - ./PalWorldSettings.ini:/home/steam/Steam/steamapps/common/PalServer/Pal/Saved/Config/LinuxServer/PalWorldSettings.ini
      - ./GameUserSettings.ini:/home/steam/Steam/steamapps/common/PalServer/Pal/Saved/Config/LinuxServer/GameUserSettings.ini
      - ./SaveGames:/home/steam/Steam/steamapps/common/PalServer/Pal/Saved/SaveGames

ちょっと長いですが、PalWorldSettings.ini、GameUserSettings.ini、SaveGamesをコンテナにマウントしています。

コンテナを起動する

ここまでくれば後は起動は簡単です。

docker compose up -d

と実行し、しばらく待てば接続できるようになります。
停止させたい場合は、

docker compose stop

と入力するだけです。

おまけ

WSL 2で動かしてるけど接続できない

Docker Desktop for Windowsではなく、WSL 2に直接インストールしたDocker Engineの場合、 WSL 2がUDPのポートフォワードに対応していないためアクセスすることができません。

大人しくDocker Desktop for Windowsをインストールしておきましょう。

サーバのアップデートが必要

コンテナを停止した後にコンテナイメージを再作成すればOKです。

# 停止
docker compose stop

# イメージの再作成
docker compose build --no-cache

# 起動
docker compose up -d

Helm ChartでデプロイしているWordPressにads.txtを配置する

元々広告を設置しているWordPressのサイトがあったんですが、本格的にk8sクラスタへ移行するにあたってads.txtを配置する必要がでてきました。
今回はその備忘録になります。

TL;DR

  • BitnamiのWordPressのChartを使っている場合の話に限る
  • このイメージは /opt/bitnami/wordpressWordPressディレクト
  • ここに配置するためにextraVolumesextraVolumeMountsをvaluesとして定義すればOK

ads.txtを配置する

1. ads.txtを準備する

Google Adsenseを使っているので、Google Adsenseからads.txtの中身の情報を取得しておきます。

2. ConfigMap or Secretでads.txtを用意する

以下を参考にしながら、ads.txtの中身を用意します。
正直公開情報になるのでConfigMapで十分だとは思います。

apiVersion: v1
kind: ConfigMap
metadata:
  name: ads-txt-file
data:
  ads.txt: |
    # ここにads.txtの内容を記述します
    # 例:
    google.com, pub-0000000000000000, DIRECT, 123456789

後はこれがデプロイされるようにkustomization.yamlなどを変更しておきます。

3. values.yamlにextraVolumesとextraVolumeMountsを定義する

BitnamiのChartでは、extraVolumesextraVolumeMountsというキーが定義されており、これを定義することで先ほど書いたConfigMapをマウントしてくれるようになります。

extraVolumes:
  - name: ads-txt
    configMap:
      name: ads-txt-file

extraVolumeMounts:
  - name: ads-txt
    mountPath: /opt/bitnami/wordpress/ads.txt
    subPath: ads.txt

これで大まかな作業としては完了で、こちらもデプロイすれば反映自体は完了で、実際にアクセスしたWordPressのサイトでads.txtが確認できるかと思います。

個人開発したアプリをストア申請するまでの手順とハマりどころ

最近個人用にアプリを作っていたのですが、ついでにアプリストアに申請した時のハマりどころがあったのでメモとして残しておきます。

TL;DR

  • 基本的に https://zenn.dev/moutend/articles/feebf0120dce6e6426fa に従って進めるでOK
  • 5.5 inchのシミュレーターがApple Silicionだと動かせない
    • 別のシミュレーターで撮ったスクショをサイズ変形して申請して回避
  • 個人名でアプリが公開される

大まかな手順

色んな方が書いてくれていますが、以下の記事が比較的網羅的かつハマりどころは少なかった印象があります。

zenn.dev

ハマったところ

5.5 inchのスクショをどう作るか

アプリのスクショはシミュレーターを使って撮るのが一般的かと思いますが、 iPhone向けのスクショは、6.5 inchと5.5 inchが必須になっています。

ですが、Apple SiliconなMacでは5.5 inchのシミュレーターを動かすことができませんでした。
なので今回は、6.5 inchのスクショをサイズ変形して登録しました。

個人アカウントで開発者登録するとアカウントに登録している名前が公表される

今回は諦めましたが、個人アカウントで開発者登録するとアプリで表示される開発チーム名?が個人名になります。
気になる人は屋号付きで開業届を出して、DUNSナンバーの申請をして法人登録した方が良さそうです。

既に準備を始めているので、もしアプリが公開できたらすぐに移管していそうです。

サポートページ・プライバシーポリシーのページ作成が面倒

これは完全に俺個人の感想ですが、この手のページのコンテンツをそれなりに考えるのが面倒くさいです。
個人情報の収集や課金コンテンツなどはないので、法的にそこまでやらなくても問題ないかもしれませんが、なんとなくプレッシャーとして感じます。

LLMを使ってテンプレートを作ってもらいましたが、他の方々がテンプレートや事例を公開してくれているので、 それを参考にすると良いかもしれません。

GitHub PagesとGoogle Formでアプリ申請に必要なページを最小限の労力で作成する

最近個人用にアプリを作っていたのですが、ついでにアプリストアリリースする気持ちになったので、そこで必要になるページをGitHub Pages+Google Formで作成した話です。

執筆時点ではまだ申請中なので、申請が却下されたり許可が通ったらしたら追記します。 申請通ったので参考にしても問題はないはず。

TL; DR

  • アプリ申請では、サポートページとプライバシーポリシーについてのページが必要となる
  • サポートページとプライバシーポリシーはdocsディレクトリにマークダウンファイルとして保存する
  • そこからの問い合わせ先としてGoogle Formを用意する

bookil.zuki.dev

ストア申請に必要なページ

アプリ申請に必要なページは

  • サポートのページ
  • プライバシーポリシーのページ

の2つが必要になります。

以下の記事を参考にしながら進めさせてもらい、実際に必要なことも確認しました。

zenn.dev

今回は最小限の労力でいきたかったので、GitHub Pagesですぐに使えるJekyllと、問い合わせフォーム用にGoogle Formを用意して進めることにしました。

GitHub PagesとJekyll

GitHub PagesはGitHubで利用できる無料のWebページのホスティングサービスです。
このGitHub Pagesでは、GitHub Actionsなどを自分で書かなくてもJekyllを使ってサイトをすぐに公開することができるため、他の静的サイトジェネレータより手軽に始めることができます。

とりあえずはデザインにこだわる必要も現時点ではないため、雑に作るために採用しています。

docs.github.com

Markdownファイルを用意する

Jekyllでページを生成してもらうために、アプリを管理しているプライベートリポジトリにdocsというディレクトリを作成し、そこに以下のような構成でファイルを作成しました。

docs
├── _config.yml # Jekyllの設定ファイル 
├── how_to_use.md # 使い方ページ
├── index.md # ルートページ
├── privacy_policy.md # プライバシーポリシーページ
└── support.md # サポートページ

各ファイルの内容に関しては、実際のサイトを見てください。

bookil.zuki.dev

_config.ymlはJekyll用の設定ファイルになり、今回は以下のような最小限の内容に留めています。

title: Bookil
theme: minima

header_pages:
- how_to_use.md
- support.md
- privacy_policy.md

これをコミット&プッシュしておいて、ファイルの準備は完了です。

ちなみにファイルのポリシーに関しては、LLMを利用してテンプレートを生成してもらって、書き換えたりセクションを追加しています。

GitHub Pagesの設定をする

GitHub Pagesの設定を行うんですが、かなり楽でBuild and deploymentBranchの設定を行うだけになります。

  • Branch ... main
  • Direcotry ... /docs

これ以外の設定は不要で、すぐにビルドが実行されてGitHub Pagesが公開されます。
カスタムドメインを設定する場合は、公式の案内に従って設定してください。

これでほぼ完了ではあるんですが、問い合わせ先がないとサポートページとプライバシーポリシーのページは完成とはいえないのでフォームを作ります。

Google Formで問い合わせフォームを作成する

実際のフォームは以下のようになりますが、これをGoogle Formでサクッと作成します。

forms.gle

特筆する点としては、

  • メールアドレスを取得しない(Googleアカウント無しでも利用可)
  • Spreadsheetsとの連携を有効にして見返したり、集計しやすくする

という点ぐらいで、それ以外は何か細かい要件はないかと思います。

これで問い合わせフォームの作成が終わったら、サポートページやプライバシポリシーのページにリンクを貼り、コミット&プッシュをするとGitHub Pagesにすぐに反映して確認できるかと思います。

これで申請に必要なページを最小限の労力で作れたかと思います。

iOSアプリでファイル取得処理にRepositoryパターンを活用してPreviewする

年末にやっていた個人アプリ開発でアプリでのTipsの1つです。
他に良いやり方もあるかもしれませんが、Swift触り始めて4日目ぐらいの感想になります。

TL;DR

  • Previewでドキュメントディレクトリにアクセスするとエラーが出る、もしくはクラッシュして2度と起動しなくなる
  • ファイルの取得処理はRepositoryに任せることで差し替えしやすくする
  • Previewではテスト用に実装されたRepositoryに差し替えることでエラーも出ずに確認できる

なぜ取得処理をRepositoryパターンとして実装しようとしたのか?

Repositoryパターンでも問題はないんですが、取得処理をViewから剥がすことが重要でした。
SwiftUIではとても便利なPreviewという機能があるんですが、アプリのドキュメントディレクトリにあるファイルを取得して表示するようなViewなどの場合、 このアクセスがきっかけでエラーで表示できないことや、Previewがクラッシュして同じ端末では2度と起動できないという現象に遭遇しました。

これらを防ぐために取得処理自体を別の層に移し、Previewでは実際にファイルアクセスをしないようなダミーの実装クラスを用意したかったのが経緯になります。

ファイルの取得処理をRepositoryパターンで実装する

今回はサンプルとしてドキュメントディレクトリに保存されているPDFファイルを一覧で表示するViewを題材にします。

表示後のイメージは以下のような形です。

ファイルの取得処理を実装する

はじめから実装の話になりますが、まずはファイルの取得処理のためのProtocolを定義します。

protocol FileRepository {
    func fetchDocuments() -> [PDFDocument]
}

URLではなくPDFDocumentを返しているのは、その方がView側での使い勝手が良いためと、
ファイルアクセスが入るとPreviewでエラーもしくはクラッシュする可能性が高まるためです。

そして、次にアプリとして動作させるための本実装をします。
特筆することはありませんが、PDF拡張子であるファイルのURLを元にPDFDocumentを生成しています。

class FileRepositoryImpl: FileRepository {
    private let fileManager = FileManager.default

    func fetchDocuments() -> [PDFDocument] {
        guard let path = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return [] }
        
        do {
            let urls = try fileManager.contentsOfDirectory(at: path, includingPropertiesForKeys: nil)
            return urls
                .filter { $0.pathExtension == "pdf" }
                .map { PDFDocument(url: $0)! }
        } catch {
            print("Error while enumerating files \(path.path): \(error.localizedDescription)")
            return []
        }
    }
}

Viewを実装する

テスト用の実装の前に先にViewを作ります。

struct ContentView: View {
    var fileRepository: FileRepository = FileRepositoryImpl()
    private var documents: [PDFDocument] {
        fileRepository.fetchDocuments()
    }
    private var columns: [GridItem] {
        Array(repeating: .init(.flexible()), count: 2)
    }

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 20) {
                ForEach(documents, id: \.documentURL?.lastPathComponent) { document in
                    VStack {
                        Image(uiImage: generateThumbnail(document: document)!)
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .cornerRadius(10)
                        
                        Text(document.documentURL!.lastPathComponent)
                            .font(.headline)
                            .frame(minHeight: 2 * UIFont.systemFontSize * 1.5, alignment: .topLeading)
                            .lineLimit(2)
                    }
                    .shadow(radius: 5)
                    .padding()
                }
            }
        }
    }
    
    private func generateThumbnail(document: PDFDocument) -> UIImage? {
        let scale = 1.0
        guard let page = document.page(at: 0) else { return nil }
        let pageSize = page.bounds(for: .mediaBox)
        let thumbnailSize = CGSize(width: pageSize.width * scale, height: pageSize.height * scale)
        return page.thumbnail(of: thumbnailSize, for: .mediaBox)
    }
}

サンプルとしては雑な実装となっていますが、以下のようにfileRepositoryをプロパティとして定義し、 ContentViewの初期化時に上書きできるような定義をしておきます。

    // デフォルトではFileRepositoryImpleを使うが
    // ContentView(fileRepository: Dummy())のような宣言ができるようにする
    var fileRepository: FileRepository = FileRepositoryImpl()

PreviewのためのRepositoryを実装する

さて下準備はできたのでPreviewのための実装をします。
前提条件としては以下をおさらいしておきます。

  • Previewでドキュメントディレクトリにアクセスするとエラーもしくはクラッシュするのでアクセスさせない
  • FileRepositoryではPDFDocumentの配列として返す
  • documentURLのlastPathComponentにアクセスしている
  • サムネイルを生成して表示しているので何かが表示されているデータになっているとPreviewで確認しやすい

これができるように実装を進めていきます。

class PreviewFileRepositoryImpl: FileRepository {
    class DummyPDFDocument: PDFDocument {
        private let url: URL
        init(url: URL, data: Data) {
            self.url = url
            super.init(data: data)!
        }
        
        override var documentURL: URL? { return url }
    }
    
    private let words: [String] = [
        "Spring", "Summer", "Autumn", "Winter",
        "January", "February", "March", "April", "May", "June",
        "July", "August", "September", "October", "November", "December",
        "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
        "Day", "Night", "Morning", "Evening", "Noon", "Midnight",
        "Sun", "Moon", "Star", "Sky", "Cloud"
    ]
    
    func fetchDocuments() -> [PDFDocument] {
        return words.map { makePDF(name: $0)! }
    }
    
    func makePDF(name: String) -> DummyPDFDocument? {
        let data = NSMutableData()
        let pdfRect = CGRect(x: 0, y: 0, width: 612, height: 792)
        UIGraphicsBeginPDFContextToData(data, pdfRect, nil)

        UIGraphicsBeginPDFPage() // 描画開始
        if UIGraphicsGetCurrentContext() != nil {
            let style = NSMutableParagraphStyle()
            style.alignment = .center
            let attributes: [NSAttributedString.Key: Any] = [
                .font: UIFont.systemFont(ofSize: 64),
                .paragraphStyle: style
            ]
            
            let size = name.size(withAttributes: attributes)
            let rect = CGRect(
                x: (pdfRect.width - size.width) / 2,
                y: (pdfRect.height - size.height) / 2,
                width: size.width,
                height: size.height
            )

            name.draw(
                in: rect,
                withAttributes: attributes
            )
        }
        UIGraphicsEndPDFContext() // 描画終了
        
        let url = URL(fileURLWithPath: "/path/to/\(name).pdf")
        return DummyPDFDocument(url: url, data: data as Data)
    }
}

さてまずはDummyPDFDocumentについてです。
PDFDocument(data: data) だけでもPDFデータは生成できるのですが、この定義の場合documentURLnilになってしまいます。
ここに依存している処理がViewでは存在しているため、DummyPDFDocumentクラスを用意して、初期化時に渡されたURLを返すようにoverrideしておきます。
こうすることで、URLやパスの取得処理が走る場合には対応できるケースが多いです。

    class DummyPDFDocument: PDFDocument {
        private let url: URL
        init(url: URL, data: Data) {
            self.url = url
            super.init(data: data)!
        }
        
        override var documentURL: URL? { return url }
    }

次にファイルの取得処理についてです。
ここでは、事前に定義されているwordsごとにDummyPDFDocumentを生成して返すような実装になっています。

    func fetchDocuments() -> [PDFDocument] {
        return words.map { makePDF(name: $0)! }
    }
    
    func makePDF(name: String) -> DummyPDFDocument? {
        let data = NSMutableData()
        let pdfRect = CGRect(x: 0, y: 0, width: 612, height: 792)
        UIGraphicsBeginPDFContextToData(data, pdfRect, nil)

        UIGraphicsBeginPDFPage() // 描画開始
        if UIGraphicsGetCurrentContext() != nil {
            let style = NSMutableParagraphStyle()
            style.alignment = .center
            let attributes: [NSAttributedString.Key: Any] = [
                .font: UIFont.systemFont(ofSize: 64),
                .paragraphStyle: style
            ]
            
            let size = name.size(withAttributes: attributes)
            let rect = CGRect(
                x: (pdfRect.width - size.width) / 2,
                y: (pdfRect.height - size.height) / 2,
                width: size.width,
                height: size.height
            )

            name.draw(
                in: rect,
                withAttributes: attributes
            )
        }
        UIGraphicsEndPDFContext() // 描画終了
        
        let url = URL(fileURLWithPath: "/path/to/\(name).pdf")
        return DummyPDFDocument(url: url, data: data as Data)
    }

makePDFは、DummyPDFDocumentを作成しますが、サムネイルとして表示するためのページを作成したいので、UIGraphicsを使って気合いで描画しています。

Previewする

最後にPreviewですが、先程実装したPreview用のFileRepositoryを初期化時に渡してあげることでうまく動作することが確認できるかと思います。

#Preview {
    ContentView(fileRepository: PreviewFileRepositoryImpl())
}

こうすることで最初に提示したイメージ通りにPreviewが表示されることが確認できました。

まだ触って5日目程度で書いた記事なので、別の良い実装があれば気軽にコメントいただければと。