シンク掃除から学ぶ技術的負債の解消
この記事はコネヒトアドベントカレンダー 9日目の記事です。
今回お話するのは、一見ソフトウェア開発とは無関係なシンクの掃除の話です。
「なんで急にシンクの掃除?」と思われるかもしれませんが、12月ということで大掃除の季節、去年の大掃除でわたしはシンクの掃除を行ったのですが、シンクの掃除をしているうちにこれって結構ソフトウェア開発と通じるところがあるなと感じました。
シンク掃除は、シンクが何の汚れで汚れているのかを分析し、それに合った方法で汚れを落とすということをしないときれいに汚れを落とすことはできません。 この洗剤つかえば万事解決みたいなことはないです、そう銀の弾丸はないのです!
そして、掃除は毎日やるのは面倒だから年末とかにまとめてやればよいと思ってませんか?しかし放っておけばおくほど、落とすのに膨大な労力がかかり、時には大金を払って業者を頼む羽目になります。そうシンクの汚れはまさに技術的負債に似ていますね!
シンクの汚れの負債の解消がいかに大変かみていきましょう
シンクの掃除の仕方
シンクの掃除の仕方についてご存知の方はこの章は読み飛ばしてください。
※ コーティングしていないステンレスシンク前提の話になります。 ※ 調査はしているものの素人が書いてますので実施の際は自己責任でお願いします。
シンク掃除の理論編(汚れの分析)
シンク掃除の基本は、汚れの『性質』を見極め、それにあった対処をしていくことです。このプロセスもエンジニアリングな感じがしますね!
シンクでは様々なものを水と洗剤で洗うのでいろいろな汚れが付着します。 主な汚れとしては、水垢、石鹸カス、油よごれ、カビ、サビなどがあります。
これらを分類しますとこのようになります。
| 汚れの種類 | 性質 |
|---|---|
| 水垢、石鹸カス | アルカリ性 (酸性洗剤で中和) |
| 油よごれ | 酸性 (アルカリ性洗剤で中和) |
| カビ | その他 (塩素系で細胞破壊) |
| サビ | その他 (物理的研磨/溶解) |
キッチンで食器洗いでよく使われている洗剤は中性です。軽い汚れや付着したばかりの汚れ程度であれば、中性洗剤とそれに含まれる界面活性剤の力でアルカリ性だろうと酸性の汚れだろうと落とすことはできます。しかし、蓄積された頑固な汚れを落とすのは困難です。間違っても力をいれてこするといったことはしてはいけません。シンクに傷がついてしまいます。
頑固な汚れはどうするかというと、アルカリ性の汚れには酸性、酸性の汚れにはアルカリ性を使います。 これは、小学校の理科で習った中和反応を利用するアプローチです。汚れを構成するアルカリ性や酸性の成分を、反対の性質を持つ洗剤で中和し、水に溶けやすい物質に変えることで汚れを落としやすくするのです。
次にカビです。カビはカビの細胞を破壊するために塩素系カビ取り剤を使います。 塩素系のカビ取り剤を使うときの酸性とまぜると有毒ガスが発生するので注意が必要です。まぜるな危険です!(バグ修正とリファクタリングもそうですね)
ここまでの話でシンクの掃除になぜ銀の弾丸がないかわかりましたでしょうか? アルカリ性、酸性はまぜると中性になってしまい意味ないですし、 酸性と塩素系をまぜると有毒ガスが発生してしまうからですね。
最後にサビですが、サビ(酸化鉄)は性質が異なり、物理的に研磨剤で削り取るか、強酸性の洗剤で溶解させることになります。シンクを痛めるリスクが非常に高いので、そもそも錆びさせないように決してシンクには鉄製のものを放置しないようにしてサビないようにしましょう。
シンク掃除の実践編
シンクの汚れについての理解できたら、あとはそれにあった洗剤をつかって汚れを落とすだけです。
といっても簡単にはいきません。汚れが頑固であるほど、洗剤をかけて擦っただけではとれません。その場合は時間をかけて漬け置きしたり、強い洗剤を使うことになります。しかしそれはシンクを傷つけるリスクを伴います。
具体的な汚れ落とし方については汚れの状況によって異なるのでここでは触れませんが、汚れをためるとすごく面倒なことになるということをご理解いただけたら十分です。
すでにひどい汚れの場合は一回プロに頼んで綺麗にしてもらって、その後はご自身で継続的に掃除を続ける方法がおすすめではあります。
毎日の掃除は、中性洗剤でシンク全体を軽く洗って、最後にシンクに水を残さないように乾拭きすればよいです。
最後に
シンクの汚れは軽い汚れであれば簡単に落とせますが、汚れは貯めると本当に落とすのが大変です。毎日の簡単な掃除と乾拭きという名の小さなリファクタリングが最も効果的です!
ソフトウェア開発についても同様で、汚いけど急いでるからとりあえずこれでいっていつかきれいに描き直そうと未来に負債を持ち込むと、大抵いつまでたってもやらないとかいざやろうとするとものすごく大変みたいなことになりがちです。
シンクもソフトウェアも経年劣化やよりよい製品がでたら買い替えるのは仕方ないですが、メンテナンスの不備で寿命を短くしてしまうということはお財布てきにもなるべく避けたいものです。
今一度、ご自宅のシンクの状態を見て、そしてご自身の今日書いたソースコードに思いを馳せてみましょう。
シンクをきれいにメンテナンスし続ける精神はきっと良いコードへとつながると思います!
2024振り返り
2024年の振り返りです。
仕事
- 今年もプレイングマネージャーとして一年過ごしました。
- 昨年はプレイングマネージャーで10人弱は多すぎだったため、今年は5人以下にしプレイング要素が増えました。
- 結構React書いていたような気がします。

アウトプット
会社ブログ
プライベート
アプリのグロースのためにいくつか機能追加したのですがグロースにはあまり寄与せず。 AppleDevelopersProgramの更新を忘れて課金で障害を起こしてしまうなど大失敗をしてしまいました。
アプリのLPを作りました。Cloudflare Pagesを使ってます。 https://phrasie-app.com/
本
Swift 5.9からのデータ監視 Observationフレームワーク入門 https://amzn.asia/d/iJdCuzi
エレガントパズル https://amzn.asia/d/0YlOcf1
エンジニアリングが好きな私たちのための エンジニアリングマネジャー入門 https://amzn.asia/d/gBqvwwZ
スタッフエンジニアへの道 https://amzn.asia/d/eg0ZNmQ
プライベート
両親の介護問題がようやく一区切りつきました。 子育ては去年とあまり変わらずといった感じでした。
styled-componentsをゼロランタイムCSS in JSに置き換える検討
この記事はコネヒトアドベントカレンダー2024の20日目の記事です。
styled-componentsで書かれたプロジェクトをあまり移行コストをかけずにゼロランタイムCSS in JSに置き換えられないか、といくつかのゼロランタイムCSS in JSを軽く味見してみました。
なぜゼロランタイムCSS in JSにするのかということについてはいろいろなところで語られているかと思いますのでここでは省略させていだただきます。
選定条件
条件としては以下の2点で選定しました。
- styled-componentsの書き方に近い
- ゼロランタイムCSS in JSである
選定した結果以下の3つを味見しました。
- Linaria : Linaria – zero-runtime CSS in JS library
- panda : https://panda-css.com/
- Kuma UI : https://www.kuma-ui.com/
味見
環境
Remix, Vite, npm
styled-components
まずstyled-componentの場合の実装例です。
import { styled } from 'styled-components'; const SubTitle = styled.h2` font-weight: bold; color: ${(props) => props.color || 'black'}; `; export default function Content() { return ( <div> <h1>Hello, World!</h1> <SubTitle color="red">red</SubTitle> <SubTitle color="blue">blue</SubTitle> </div> ); }
サンプルコードはこちらのリポジトリのPullRequestにも載せてあります。 Pull requests · yanamura/zero-runtime-css-in-js-sample · GitHub
Linaria
setup
npm install @linaria/core @linaria/react @wyw-in-js/babel-preset npm i -D @wyw-in-js/vite
// vite.config.ts import { defineConfig } from 'vite'; import wyw from '@wyw-in-js/vite'; export default defineConfig(() => ({ // ... plugins: [wyw()], }));
実装
import { styled } from '@linaria/react'; const SubTitle = styled.h2` font-weight: bold; color: ${(props) => props.color || 'black'}; `; export default function Content() { return ( <div> <h1>Hello, World!</h1> <SubTitle color="red">red</SubTitle> <SubTitle color="blue">blue</SubTitle> </div> ); }
感想
styled-compontntsと全く同じコードで実装できたので、移植は楽そうかもしれません。
気になるところをあげるとすると、ドキュメントがGitHubのdocs以下しかなさそうなところと、babelに依存しているところくらいでしょうか。
panda
setup
npm install -D @pandacss/dev npx panda init --postcss
// package.json
{
"scripts": {
+ "prepare": "panda codegen",
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
}
}
// panda.config.ts import { defineConfig } from "@pandacss/dev"; export default defineConfig({ // Whether to use css reset preflight: true, // Where to look for your css declarations include: ["./src/**/*.{js,jsx,ts,tsx}", "./pages/**/*.{js,jsx,ts,tsx}"], // Files to exclude exclude: [], // Useful for theme customization theme: { extend: {}, }, syntax: 'template-literal', jsxFramework: 'react', // The output directory for your css system outdir: "styled-system", });
npx panda codegen --clean
ポイントとしてはstyled-components系のスタイル(JSX style)を使うには、panda.config.tsに jsxFramework: 'react' を追加して, npx panda codegen --clean する必要があるところです。
実装
import { styled } from '../styled-system/jsx'; const SubTitle = styled.h2` font-weight: bold; ` // color: ${(props) => props.color}; // 自前で定義したプロパティは使えない export default function Content() { return ( <div> <h1>Hello, World!</h1> <SubTitle>red</SubTitle> <SubTitle>blue</SubTitle> </div> ); }
感想
pandaは導入がややこしいです。ただ最初だけなのでそこまで気にする必要はないかもしれませんが。 prepareでpanda codegenが動いてstyled-systemフォルダ以下にファイルを生成しているのがちょっと気になりました。(ビルドしないとstyled-system以下を参照してもエディタ上ではエラーがでるのと、JSXを有効するときにこれを一回cleanしないといけなかったりと) 実装上は、styled-componentsのようにpropsを渡すことができないようなので、そういうコードを移植したい場合はちょっと困るかもしれません。
Kuma UI
setup
npm install @kuma-ui/core npm install -D @kuma-ui/vite
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import KumaUI from "@kuma-ui/vite";
export default defineConfig({
plugins: [
react(),
KumaUI({
// Enable WebAssembly support for Kuma UI. Default is false and will use Babel to transpile the code.
wasm: true // Optional
}),
],
});
実装
import { styled } from "@kuma-ui/core"; const SubTitle = styled.h2` font-weight: bold; `; // color: ${(props) => props.color}; // 自前で定義したプロパティは使えない export default function Content() { return ( <div> <h1>Hello, World!</h1> <SubTitle color="red">red</SubTitle>{/** colorはKumaUIのUtility Props(https://www.kuma-ui.com/docs/Components/UtilityProps#color) */} <SubTitle color="blue">blue</SubTitle> </div> ); }
感想
導入は一番楽でした! 実装上は、panda同様styled-componentsのようにpropsを渡すことができないようでした。 ただいくつかUtility Propsというのが用意されていてそれで代用できるケースもあるかもしれません。
総括
styled-componentの書き方を変えずにゼロランタイムCSS in JSにするには、Linariaがあまり既存のコードに影響が少なく持ってこれるという点ではよいかなという印象でした。
ただ、調べているうちに、Compiledではstyledの記法がdeprecatedになっていたりと、styled-componentsの書き方に縛られるのはあまりよくないのでは・・という感想を持ちました。 個人的にもstyled-componentsの書き方はパッと見で機能を持つコンポーネントなのかただ装飾だけのコンポーネントなのかわかりづらいのがあまり好きではないので、最初から採用するのであればstyledのスタイルよりはcssのスタイルのほうを選んだほうがよいかなと思っています。
static libraryをつかったマルチモジュール環境でCocoaPodsとSwiftPackageManagerを混在させる
このようにアプリの一部のモジュールをstatic libraryに切り出し、それぞれでCocoaPods経由でライブラリを使っているケースで、Swift Package Manager経由でライブラリを使いたいときの対処法です。

モジュール分割したどちらか一方でのみSwift Package Manager経由でライブラリを追加する場合は普通にXcodeのPackage Dependenciesを使えばよいです。
上の図のライブラリBように複数のtargetから使われているライブラリをSwift Package Managerにしたい場合、XcodeのPackage Dependenciesで複数のtargetに追加すると duplicate symbols for architecture.. となってしまったりする場合があると思います。
対策
そういった場合はライブラリをラップするフレームワークをつくり、そのフレームワークを各モジュールから使うようにするとうまくいきます。

例えばTCAを全Targetから使いたい場合ですが、
まずラップするフレームワークとしてtargetにPackageContainerというフレームワークを追加し、そのフレームワークにSPMで追加したTCAをFramework&Libraryで追加します。

そして各targetのFramework&LibraryにPackageContainerを追加すれば完了です。


すべてSwift Package Managerにすることはできず、一部CocoaPodsにしないといけない場合などにどうぞ。
2023振り返り
2023年の振り返りです。
仕事
- 今年もプレイングマネージャーとして一年過ごしました
- EMとして10人弱見てる感じです

アウトプット
会社ブログ
プライベート
超がんばってiPhoneアプリをリリースしました。星5お願いします!
apps.apple.com本
Good Code, Bad Code ~持続可能な開発のためのソフトウェアエンジニア的思考 https://www.amazon.co.jp/gp/product/B0BSW72QKZ
単体テストの考え方/使い方 https://www.amazon.co.jp/gp/product/B0BLTG8Z9K
アジャイルリーダーシップ 変化に適応するアジャイルな組織をつくる https://www.amazon.co.jp/gp/product/B0BT747481
一冊でマスター!Swift Concurrency入門 https://www.amazon.co.jp/gp/product/B0B76V1HNC
ちょうぜつソフトウェア設計入門――PHPで理解するオブジェクト指向の活用 https://www.amazon.co.jp/gp/product/B0BNH1J2W2
スタッフエンジニア マネジメントを超えるリーダーシップ https://www.amazon.co.jp/gp/product/B0C231J7FC
ユーザーの問題解決とプロダクトの成功を導く エンジニアのためのドキュメントライティング https://www.amazon.co.jp/gp/product/B0BXSYF2N4
プライベート
子供が7歳、3歳になりました。 小学校は宿題やらなんやらたいへんですね・・
スクラムガイド輪読会で捕捉したりしてること
この記事はコネヒトアドベントカレンダー22日目の記事です。
スクラムガイド輪読会をこれまで社内で何度となくやってきて、スクラムガイドを読み進める上で各章のポイントや捕捉していることについてまとめてみました。
スクラムガイドを読んでもなんだかちょっとわかんないなというスクラム初心者のかたなどの助けになると幸いです。
わたしは認定スクラムマスターではありますが2日研修受けてテスト通ったくらいのあれなのと、 個人的な見解も含んでますので、 もしここ間違ってるよみたいなところがもしあれば教えていただけると幸いです。
また、ここに書いてあることだけ読んでもなんだこれって感じだと思いますので、スクラムガイド2020年版の該当章読んでそれからみていただけるとよいかなと思っております。
スクラムの定義のところ
スクラムはフレームワークかつ、意図的に不完全にしてあるのでこの通りにやればうまくいくわけではないです。 スクラムはまずは書いてある通りに試してみて、そこから自分たちにあった形に変えていくとよいです。
スクラムの理論のところ
スクラムの三本柱のところが一番重要なポイントで、ここだけは覚えておきましょう。 スクラムのイベントはスクラムの三本柱を実現するための手段なので、スクラムイベントをやることではなくこの3本柱を意識して、各イベントをすることが目的にならないように注意しましょう。
スクラムの価値基準のところ
スクラムを成功させるには各メンバーが5つの価値基準を実践できるかがポイントです。 例えば確約ができないと各スプリントでゴール達成できないですし、公開がされないと透明化されないので検査ができなくなってしまいます。 価値基準の実践に対して前向きになれない問題があればまずそれを対処していく必要があるかなと思います。
スクラムチームのところ
スクラムマスターが名前的にリーダーっぽいので勘違いする場合もありますが、スクラムマスターがやることを指示したり決めるのではなくスクラムチームが誰がなにをいつどのようにやるかを決定します。 ただ、全員スクラムはじめてで進め方全然わからんみたいなケースだとスクラムマスターの介入度は高くなるケースもあるかもですが、それはそれでOKでその状態をずっと続けず本来のあるべき姿に近づけていくと良いかなと思います。
スクラムイベント
スプリントのところ
スクラムの期間は一ヶ月以内と書いてありますが、最初のうちは短めにしておいたほうがよいと思います。最初のうちは失敗することも多いと思うので、スプリントが短いほうがより早く学びのサイクルをまわせます。はじめのうちは1週間とかでやってみるとよいのではと思っています。「決まった長さで」ってかいてあるから短いとダメなのではとつっこみがあるかもしれませんが、ずっと同じ期間でやらなければならないってわけではないと思っています。なぜ決まった長さとしているかは、スプリントの間隔がバラバラだと、スプリントの見積りで前回のベロシティが参考にならなかったり、毎スプリントで「今回のスプリントの長さどうする?」という余計な議論が必要になってくるなどがあるからかなと思うのです。なのでちゃんとした目的や必要性があればスプリントの期間をかえることは間違いではないと思います。いつもは2週間スプリントだけど、これからのプロダクトゴールは1週間でやったほうが短いスパンで改善サイクルを回してより不確実なことに対応できるみたいなケースもあるかもしれません。思考停止してルールに従うことは危険なので注意してください。
スプリントプランニングのところ
アジャイルだと計画たてないと思っている人もいますが、スプリント内の計画はちゃんと立てます。長期の計画は不確実性が高いので雑に見積りますが、スプリントは短いのである程度予測がたてられるはずです。 それを適当にやってしまうと妥当なスプリントゴールが立てられないので、スプリントゴールが目標として機能しなくなります。ゴールがゆるすぎるとそれにあわせてチームのパフォーマンスは落ちますし、ゴールがきつすぎると達成しないのが当たり前になる恐れがあります。 スプリントの計画をスプリントプランニングできちんとたてるには、プロダクトバックログを小さな作業アイテムに分割できるレベルに準備しておく必要があります。これはプロダクトバックログリファインメントを通してやっていきます。
デイリースクラムのところ
デイリースクラムは開発者のためのイベントです。プロダクトオーナーやスクラムマスターへのデイリー進捗共有会ではありません。 スプリントは短く予測性は高いとはいえ、うまく進まないことが発生するのが常です。それに対して日々チームでどう対処していくか検討する場です。 問題が発生した場合は次のデイリースクラムで話そうと問題解決を先送りしないでください。デイリースクラムが唯一のコミュニケーションの場ではないので、開発者は一日を通して頻繁に話し合ってください。
スプリントレビューのところ
ステークホルダーへの成果物のプレゼンや報告会ではありません。成果物を検査して、今後どうしていくかを決める場です。
スプリントレトロスペクティブのところ
スプリント内で問題や課題を見つけてもスプリントレトロスペクティブまで先送りしないでください。その場やデイリースクラムでチームに共有しましょう。 たくさんやりたい改善がでるかもしれませんが、最初のうちは一番重要なものに絞ってそれを確実にやりきりましょう。他の問題は放置するわけではなく、重要な問題であれば再度スプリントレトロスペクティブで問題として浮上してくるはずです。 スプリントレトロスペクティブで全然課題がでてこない場合は、透明性や検査ができていない可能性があるので、そのあたりに課題がないかチェックしてみるとよいです。
スクラムの成果物
プロダクトバックログのところ
スプリントプランニングのところでも説明しましたが、プロダクトバックログの優先順位が高いものはスプリントプランニングできる状態に準備しておく必要があります。
プロダクトゴールはプロダクトバックログ単体だとこれなんのためにやってるんだっけみたいなことになるのを防ぐためのものと思っておくとわかりやすいかもです。プロダクトバックログを作る際も、これやることでプロダクトゴール達成できるんだっけ?みたいな視点を持つことも重要です。
スプリントバックログのところ
スプリントバックログは一日以内で終わる単位くらいのサイズにしておく必要があります。小さいスプリントバックログであれば前日と比べてなにかしら動きが発生するはずなのでデイリースクラムでその動きをみることで検査ができますが、大きいスプロントバックログだと前日と動きがなかった場合それが普通なのか異常なのかの検査が簡単ではありません。
インクリメントのところ
インクリメントってあまりしっくりこないワードだと思いますが、ユーザーにとって価値のあるリリース可能な機能や機能改善といった感じです。 リリース可能とは完成の定義を満たしていることです。完成の定義が曖昧だと開発者はここまでやれば完成だと思っていたがプロダクトオーナーの認識は違っていたみたいなことが起こりリリースできる状態になかったみたいなことが起こる可能性があります。
最後に
スクラムガイドは何年かに一回内容がアップデートされているので、改定されたら目を通すことをおすすめします。
最後に一番気をつけてほしいことはスクラムガイドに書いてあることを守ることが第一ではないということです。スクラムガイドであってスクラムルールではありません。思考停止してガイドや本に書いてあることに従うのではなく、どういう目的でやっているかということを忘れないでほしいです。スクラムをすることが目的にならないように注意してください。
Phrasieを支える技術
個人開発で2023年5月末にiOSアプリをリリースしました!
apps.apple.com有料アプリなので課金はしなくてもよいですが(してもいいですよ!)、星5をなにとぞお願いします!!!
どんなアプリかというと、英語勉強を支援するアプリです。 機能としては、日本語で日記を書き、それを英語に自動で翻訳し、翻訳された英語の発音を聞いてシャドーイングの練習をするというアプリとなっています。
これまで個人開発では無料のアプリをつくってきましたが、今回は有料のAPIを用いているというのもあり個人開発では初の月額課金のアプリとしています。
使っている技術
- フルSwiftUI
- 個人開発ということでサポートOSを最新のみとしてSwiftUIをフル活用できるようにしました
- Firestore
- データはFirestoreで永続化しています。最初はお金がかからないのでCoreDataにしようかと思ったのですが、
APIがアレなので有料サービスということもあり機種変時のデータ移行とかもちゃんとできないといけなかったりするので、そのへんのテストはCoreDataだと確認が結構めんどくさいなということでFirestoreにしました。(リリースしたのがiOS17が発表される前だったのでもう少し遅ければSwiftDataを使ってみたさで意思決定は変わっていたかもしれません。。) - ちなみにRealmはアップデートが頻繁すぎて個人開発で最新追従し続けるのがたいへんなので選択肢からははずしています。
- データはFirestoreで永続化しています。最初はお金がかからないのでCoreDataにしようかと思ったのですが、
- FirebaseAuthentication
- データの引き継ぎのためにアカウントと紐付ける必要がありFireStoreと同様にFirebaseのFirebaseAuthenticationを利用しました。
- RevenueCat
- 課金周りは自分でサーバー側やるのはたいへんなのでRevenueCatを利用しました。
- 英語周りのところ
- こちらに関しては企業秘密ということで・・
ハマりどころ
AppStoreでのリジェクト
その1
"次のサブスクリプション期間の支払いが自動的に開始されることを明確にしていません" と怒られました。結構ちゃんと書いてたつもりでしたが完璧は難しいですね・・
We noticed that one or more of your auto-renewable subscriptions is marketed in the purchase flow in a manner that may mislead or confuse users about the subscription terms or pricing. Specifically: - Your app offers a free trial or introductory period but does not make it clear that a payment will be automatically initiated for the next subscription period. Next Steps To resolve this issue, please revise your auto-renewable subscription purchase flow to clearly indicate how long the free trial lasts and the amount that will be billed after the free trial is over.
その2
App Store Connectのプライバシーでユーザー追跡をしているにチェックを入れていたら、AppTrackingTransparencyのポップアップの表示が必須ということでした。
The app privacy information you provided in App Store Connect indicates you collect data in order to track the user, including Product Interaction, Other Usage Data, Customer Support, Search History, Browsing History, User ID, Purchase History, and Crash Data. However, you do not use App Tracking Transparency to request the user's permission before tracking their activity. Starting with iOS 14.5, apps on the App Store need to receive the user’s permission through the AppTrackingTransparency framework before collecting data used to track them. This requirement protects the privacy of App Store users.
その3
スクショにアプリの画面入れるのが面倒だったので、最初はなくていいかーと思ってたら、ダメ出しされました・・
We noticed that your screenshots do not sufficiently show your app in use. Specifically, your screenshots do not show the actual app in use in the majority of the screenshots. To help users understand your app’s functionality and value, your screenshots should highlight your app's core concept. For example, a gaming app should feature screenshots that capture actual gameplay within the app. Next Steps Please revise your screenshots to ensure that they accurately reflect the app in use on the supported devices. Keep in mind the following requirements: - Marketing or promotional materials that do not reflect the UI of the app are not appropriate for screenshots. - The majority of the screenshots should highlight your app's main features and functionality. - Confirm that your app looks and behaves identically in all languages and on all supported devices. - Make sure that the screenshots show your app in use on the correct device. For example, iPhone screenshots should be taken on iPhone, not on iPad.
その4
AppleIDログインを必須にしていたらリジェクトされました。 新規登録時には登録不要にしてあとからAppleID連携できるように変更しました。ただ、AppleID連携は機種変時のアカウント移行をトラブルなく行うために必要不可欠にしたかったのでこのリジェクトは結構痛かったです。
We noticed that your app requires users to register with personal information to purchase in-app purchase products that are not account based. Apps cannot require user registration prior to allowing access to app content and features that are not associated specifically to the user. User registration that requires the sharing of personal information must be optional or tied to account-specific functionality. Next Steps To resolve this issue, please revise your app to not require users to register before purchasing in-app purchase products that are not account based. You may explain to the user that registering will enable them to access the purchased content from any of their iOS devices and provide them a way to register at any time, if they wish to later extend access to additional devices. Please note that although App Store Review Guideline 3.1.2 requires an app to make subscription content available to all the iOS devices owned by a single user, it is not appropriate to force user registration to meet this requirement; such user registration must be optional.
ここについては初回起動時はFirebase Authenticationの匿名ログインを使ってユーザーを作成し、アカウント移行のために別途AppleIDログインを用意することで対応しました。
実装についてはこちらに記事に書きました。
技術的なところ
Firestore
ちょっとDB設計に戸惑いました。
今回の場合、各ユーザーが作成したユーザーのみが閲覧できる日記データを持つといった構造になります。
RDBの場合だと雑に考えるとdiaryテーブルとuserテーブルをつくってdiaryのカラムにuser_idもたせるみたいな感じになるかなと思います。
Firestoreの場合セキュリティルールで自分に関するデータしか見れないようにしたりしたいので、
- users
- userID
- diaries(サブコレクション)
このようなデータスキームにすることで、ユーザーIDに対しそのユーザーの日記のみを紐付け、必要な日記データのみを取ってこれるようにしました。Firestoreは読み取り、書き込み、削除したドキュメントの数で料金がかかってくるので、無駄な読み込みが減りコスト削減にもなっているかなと思います。
日記数も、countをつかうとデータを全部舐める必要が出てくると思うので日記数も保存するようにするなどといったこともやりました。
RevenueCat
RevenueCatはものの数十行で課金処理ができちゃうのでめちゃくちゃ便利でした。 実装自体はすごい簡単なのですが、どちらかというと設定のほうに苦労しました。 というのもRevenueCatはiOS/Androidといったプラットフォームによる課金の違いを別の概念で抽象化して表現しているので、その概念を理解するのにちょっと戸惑いました。
その他
アプリを公開するにあたって、プライバシーポリシーや利用規約などを用意しなければならず、これらのWebページをどうやって用意するかがちょっとこれまでは面倒でした。 今回Notionを使って試しにやってみたところ、普通に審査でも突っ込まれず、実装や運用コストもかからずめっちゃ楽で最高でした。
失敗したなというところ
Firestoreで検索ができない
like検索くらいはできるだろうと思っていたら、あとから検索機能をつけようとしたらないことが判明して結構困っております。。 ElasticやAlgoliaなどの3rd partyのサービスと連携すればできるみたいですが、ちょっと個人開発だと財政面できびしそうです
ローカライズ
海外展開すれば人口で単純計算するとダウンロード数めっちゃ増えるだろうと思って頑張って韓国語、中国語対応もいれたのですが、びっくりするほどダウンロードされていない、というか閲覧すらされていないので全然コスパよくなかったです。。 単純に翻訳しただけでは駄目という学び。
最後に
今回はじめて無料ではなく有料アプリをリリースしてみました。 幸い数名の方にご利用いただきとてもありがたいです。 今後も少しずつ改善していきたいなと思っています!
UIViewControllerがMainActorだから安心できるわけではない
前回の記事でUIViewControllerとMainActorのことについてかきました。UIViewControllerはMainActorなのでMainスレッドで実行されることは保証されていて安心!かと思いきやそうでもなかったのです。
以下のMainActorであるViewModelをMainActorではないクラスから呼び出そうとするとコンパイル時にエラーになります。MainActorを使うことでこのようにコンパイル時にチェックされて安全です。
class Good { let viewModel = ViewModel() // Error: Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context } @MainActor class ViewModel { func hello() { print("hello: \(Thread.current.description)") } }
UIViewControllerもMainActorなので上と同様なことをしてみます。 するとどうでしょうコンパイルに通ってしまいます。
class Bad { let vc = UIViewController() // エラーにならない }
このBadというクラスをMainスレッド以外で生成するとクラッシュしてしまいます。
どうしてこうなっているかというとSwift5ではSwiftConcurrencyのSendableのチェックがゆるくなっているためです。 これは、SwiftConcurrencyに対応していないモジュールとのやりとりを楽にするためにSendableを完全に強制していないからです。 Swift5, 6でのConcurrencyの違いはこちらの Concurrency in Swift 5 and 6 などを見てみると思想を知ることができます。
Swift5系のXcodeでは、BuildSettingの Strict Concurrency Checking はデフォルトでMinimalになっています。
これをCompleteに変えると先程のBadというクラスはコンパイルエラーになるようになります。
なので Strict Concurrency Checking はCompleteにしておいたほうがより安全です。
まだかなり先になると思いますがSwift6ではCompleteになるようですので対応できるなら先に対応しておいたほうが後々のSwift6への移行も楽になるかと思います。
ただ既存のコードだと利用しているライブラリも直さなければならなかったりとかなり大変だったりするので、既存のプロジェクトはTargetedからはじめ、新規プロジェクトでは最初からCompleteにしてやるなどするとよいかなと思います。
UIViewControllerからMainActorを呼ぶときにawaitしなくてよい理由
SwiftConcurrencyでは普通actorのメソッドを呼んだりするときはawaitしなければならないですが、UIViewControllerからMainActorのメソッドを呼ぶときはawaitせずに普通に呼べてしまいます。
これはなぜかというとUIViewControllerもMainActorだからです。
UIViewControllerの定義を見ると以下のようになっています。
@MainActor open class UIViewController : UIResponder, NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment {
MainActorとは
MainActorはGlobalActorの一種です。 GlobalActorは普通のActorだとそのActor内でしかisolatedな状態にできませんが、GlobalActorを使うことでその範囲を外部に広げることができるようになります。
このように別のactor間だとそれぞれでactor isolatedされてるのでお互いを呼ぶときはawaitしなければなりません。
actor A {
func execA() {
Task {
let b = B()
await b.execB()
}
}
}
actor B {
func execB() {
}
}
ここでGlobalActorをA,Bに適用するとこれらを同じActor isolatedな状態にできるため、awaitを使わずに呼び出すことが可能となります。
@MyGlobalActor struct A { func execA() { let b = B() b.execB() } } @MyGlobalActor struct B { func execB() { } } @globalActor final public actor MyActor : GlobalActor { public static let shared: MyActor }
さらにMainActorはメインスレッドのみで実行されるという特徴を持ちます。UI関連の処理を行う場合はMainActorを使うと便利です。
MainActorの実装について
ところで、MainActorはこのようにGlobalActorとして定義してやるだけでつくれるでしょうか?
@globalActor final public actor MyActor : GlobalActor { public static let shared: MyActor }
これだけではMainActorの重要な要素である メインスレッドで実行する が適用されません。
MainActorではメインスレッドでの実行保証をどうやって実現しているのでしょうか?
実行スレッドの固定は CustomExecutor によって実現されています。
GlobalActorはactor上でメソッドなどが実行されるときにどのように実行されるかをカスタムできるようになっています。
このようにMainActorではenqueue(job:)でジョブがキューイングされたら順番にメインスレッドで走るようなCustomExecutorを実現しているのでメインスレッドで動く様になっていると思われます。
@globalActor public actor MyMainActor: GlobalActor { public static var shared = MyMainActor() nonisolated let executor: SerialExecutor nonisolated public let unownedExecutor: UnownedSerialExecutor init() { let executor = MainExecutor() self.executor = executor unownedExecutor = executor.asUnownedSerialExecutor() } } final class MainExecutor: SerialExecutor { func asUnownedSerialExecutor() -> UnownedSerialExecutor { UnownedSerialExecutor(ordinary: self) } func enqueue(_ job: UnownedJob) { DispatchQueue.main.async { job._runSynchronously(on: self.asUnownedSerialExecutor()) } } }
MainActorの場合はこのようにメインスレッドで実行されますが、普通のGlobalActorはデフォルトのSerialExecutorによって順次実行されます。このSerialExecutorはSerial Dispatch Queueのときと同様で順次実行はされますが同一のスレッドで実行されるわけではないということは頭に入れておいたほうが良いかなと思います。(同一スレッドでの実行を前提としたコードを書かない) どうしてもMainActorみたいな同一スレッドを保証したGlobalActorを作りたい場合はNSThreadとかをつかって固定スレッドになるようにSerialExecutorを実装すればできると思いますが、固定スレッドをつかってしまうとSwiftConcurrencyのCPUに対して1スレッドが割り当ててスレッドの切り替えを少なくするという特性を阻害することになると思うのでなるべくやめといたほうが良いかと思います。
SwiftUIのモーダルの表示・非表示
※ iOS16でのやり方です
モーダルの非表示
説明の都合上、先に非表示(モーダルを閉じるとき)についてです。
モーダルを閉じる場合はこのようにEnvironmentのdismissを使って画面を閉じます。
// モーダルで表示する画面 struct ModalContents: View { @Environment(\.dismiss) private var dismiss var body: some View { Button("Done") { dismiss() } } }
このコードを最初見たときに違和感感じませんでしたか?わたしは感じました。
dismissってぱっと見valueっぽいですがdismiss()で呼んでいるのでclosureなのでしょうか。
dismissの正体
EnvironmentValuesにdismissは定義してあり、型は DismissAction でした
public var dismiss: DismissAction { get }
DismissAction はなにかというとstructです。
struct DismissAction
https://developer.apple.com/documentation/swiftui/dismissaction
structなのになぜ関数のように呼ぶことができるかというとcallAsFunction()が定義されているからです。 https://developer.apple.com/documentation/swiftui/dismissaction/callasfunction()
つまり、dismiss()とかくと暗黙的にdismissのcallAsFunction()を呼んでいることになるのです。
struct ModalContents: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
Button("Done") {
dismiss() // Implicitly calls dismiss.callAsFunction()
}
}
}
DismissActionのcallAsFunction()内で画面を閉じる処理が実行されていると考えられます。
モーダルの表示
モーダルを画面を開く場合は、 完全に全画面の場合fullScreenCover, 完全に全画面ではなく上にちょっと浮いた感じにする場合はsheetをを使います。
https://developer.apple.com/documentation/swiftui/view/fullscreencover(ispresented:ondismiss:content:) https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)
struct ContentView: View { @State private var isFullScreenCoverViewPresented = false var body: some View { NavigationStack { Group { Button { isFullScreenCoverViewPresented = true } label: { Text("+") } } .fullScreenCover(isPresented: $isFullScreenCoverViewPresented) { ModalContents() } } }
モーダルで開いた画面のナビゲーションバーに閉じるボタンをつける
モーダルで表示する画面側をNavigationStackでwrapしてToolbarItemを追加します。
struct ModalContents: View { @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { Group { } .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { dismiss() } label: { Label("Close", systemImage: "xmark") } } } } } }
同じような画面が何個もある場合はこういったContainerViewを用意しておくと ModalView { ModalContents() } とするだけで閉じるボタンが追加できるので便利かもしれません。
struct ModalView<Content: View>: View {
@Environment(\.dismiss) var dismiss
var content: () -> Content
var body: some View {
NavigationStack {
content()
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Label("Close", systemImage: "xmark")
}
}
}
}
}
}
2022振り返り
2022年の振り返りです。
年末に下の子が胃腸炎になりその後家族全滅する事態となり正月過ぎての振り返りとなってしまいました・・
仕事
- 今年もプレイングマネージャーとして一年過ごしました。

アウトプット
会社ブログ
ちょっと油断していたところだいぶ世間から遅れてきてそうだったので、がんばって負債の返済やモダン化を進めました。
本
マネジメントに関する良い本もどんどん出版されていてとても勉強になるのでありがたいです。
新一分間マネージャー https://www.amazon.co.jp/gp/product/B0113I0NNQ
Clean Craftmanship https://www.amazon.co.jp/gp/product/B0B9LPZ4R7
ソフトウェアアーキテクチャの基礎 https://www.oreilly.co.jp/books/9784873119823/
並行プログラミング入門 https://www.oreilly.co.jp/books/9784873119595/
リーダーの作法 https://www.oreilly.co.jp/books/9784873119892/
エンジニアリングマネージャーのしごと https://www.oreilly.co.jp/books/9784873119946/
プライベート
子供が6歳、2歳になりました。来年から小学生はやいですね。 全然子育て楽になる気配がありません。
GitHub Actionsを自作している人はwarningに注意
GitHub Actionsは実行に成功していても、密かにwarningが出ていたりするので、GitHub Actionsを自作している人は気をつけましょう、将来的に急に動かなくなります。
わたしの自作のGitHub Actionsでは以下のようなwarningが出ていました。
The `set-output` command is deprecated and will be disabled soon. Please upgrade to using Environment Files. For more information see: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
Node.js 12 actions are deprecated. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/. Please update the following actions to use Node.js 16
set-output command is deprecated
私の場合は@actions/core のsetOutputを使っていたためこのwarningが発生していました。
対応としては @actions/core をv1.10.0以上にすればOKです。
Node.js 12 actions are deprecated
こちらについてはactions.ymlで using: "node12" となっているところをnode16に変えるだけでOKでしたが、Nodeのバージョンに依存するようなコードがある場合は修正が必要になります。
まとめ
割と簡単に対応できるのでOSSで見つけた場合はさらっと対応するとコントリビュートチャンスかなと思います
Xcode Cloud所感
Xcode CloudがXcode 13.4.1から使えるようになったので試してみました。
Xcodeの左のナビゲーションの一番右の Report navigator もcloudタブからワークフローを作ることができるようになっています。

ここからXcode上でポチポチしていくだけで、ワークフローの設定だけでなく、リポジトリとの認証、AppStoreConnectでアプリの追加までも行うことができます。
ただAppStoreConnectと密結合されているのでアプリではなくSwiftのライブラリの開発でXcode Cloudを利用することはできません。
Xcodeでできるワークフローの設定
Environment
Environmentではビルド環境の設定ができます。変更できる設定は以下です。
- Xcodeバージョン
- 利用するXcodeのバージョンが指定できます
- beta版などもすぐに使えるようになっているようです
- macOS Version
- 利用するmacOSのバージョンが指定できます
- Clean
- cleanにチェックをいれるとderived dataなどのキャッシュが使われません
- archiveする際にはこの設定は必須になります
- Environment Variable
- 環境変数が設定できます
Start Conditions
Start Conditionsではワークフローを発火するトリガーを設定できます。 Start Conditionsは以下の4種類があります
- Branch Changes
- ブランチに変更があった場合に発火
- 特定のブランチを指定することもできます
- 特定の文字から始まるブランチも指定できます(branches beginning with "release-" など)
- 発火する条件にフォルダやファイルを指定することができます
- Pull Request Changes
- Pull Requestが作成された場合に発火
- 指定できる条件はBranch Changesと同じもの、プラス targetブランチが指定できます
- Tag Changes
- タグに変更があった場合に発火
- 特定のタグを指定することもできます
- 特定の文字から始まるタグも指定できます(tags beginning with "v" など)
- 発火する条件にフォルダやファイルを指定することができます
- On a Schedule for a Branch
- 設定したスケジュールで特定のブランチで発火
- cloneみたいな感じで設定できます。
Actions
Actionsはワークフローで実行されるものです。Actionsには以下の4種類があります。
- Build
- Test
- テストを実行します。
- ビルドと大体同じなのですが、テストを実行するデバイスを選べます。複数選ぶと並行に複数環境でテストすることが可能です。
- Analyze
- メモリリークなどの問題を発見することができます。やっていることはxcodebuild analyzeと同じです。
- Archive
Post-Actions
Post-ActionsではActions実行後に行うActionを設定できます。Post-Actionsには以下の3種類があります
- TestFlight Internal Testing
- Internal TesterにTestFlightで配布
- TestFlight External Testing
- External TesterにTestFlightで配布
- Notify
- ワークフローが成功や失敗した場合にslackやemailで通知することができます
このようにワークフローのトリガー、Action、Post-Actionをポチポチするだけで簡単に設定することが可能となっています。 ただしこれらの設定は設定ファイルに残るわけではなくXcode Cloud側で保持されています(コードで管理できない)
ポチポチするだけではできないこと
ビルドの事前準備
2022/9時点ではXcode Cloud上で提供されているツールは、macOSやXcodeに含まれているものとHomebrewだけになります。
よって、例えば外部ライブラリを利用するのにCocoaPodsやCarthageが必要であったり、XcodeGenをつかってプロジェクトを生成したりする場合にはこれらをインストールする必要があります。
CocoaPodsやCathageなどのツールのインストール方法
- Xcode Projectやworkspaceと同じ階層に
ci_scriptsという名前のディレクトリを作成 - ci_scriptsの中に
ci_post_clone.shファイルを作成し、このファイルにCocoaPodsやCathageのインストールスクリプトを記載します
## ci_post_clone.sh #!/bin/sh brew install cocoapods pod install
ci_post_clone.sh以外にも、ci_pre_xcodebuild.sh, ci_post_xcodebuild.shも配置可能です。それぞれxcodebuildの実行前後にやりたいことがあればここにスクリプトを記載すれば実現可能です。
まとめ
これまではiOSアプリのCI/CD導入には証明書の取り扱い、xcodebuildやAppStoreConnect API, fastlaneなどといった知識が必要で導入にハードルがありましたが、Xcode CloudによってCI/CDの導入ハードルは非常に下がり誰でも導入しやすくなったことが大きいと思います。
逆に既にCI/CD導入済みの場合は現時点では不満に思えるところは少なからずあるように思います。個人的には、設定をコードで管理できない点とderived data以外のキャッシュがきかない(CocoaPodsなどのtool系のビルドが毎回必要)という点のデメリットが大きいなと思っています。
個人的には
- 現時点でCI/CD環境が整っている場合
- 現時点でCI/CD環境がなにもできていない場合
のような考え方が良いのではと思いました。
Xcode Cloudはまだできたばかりなのでこれからに期待ですね
leetcodeのeasyだけどeasyじゃなかった問題1
2分木の直径(左右ノードが最長となるときの長さ)を求める問題です。
この問題はノードの高さを求めるアルゴリズムを知ってる前提だと確かに簡単なのですが、知らないとそこから考えないといけないのでちょっと大変です。
前提としてのノードの高さの求め方
左右のノードの高いほうに1を足すとノードの高さになるというのを再帰的に行うとノードの高さが求められます。
int tree_height(Node* root) {
if (root == NULL)
return 0;
else {
int left_height = tree_height(root->left);
int right_height = tree_height(root->right);
return max(left_height, right_height) + 1;
}
2分木の直径
簡単な方法
全てのノードで左右の高さを計算し最大のものを求める方法です。
ただこれだと無駄に計算量がかかってしまいます。O(N2)
class Solution {
public:
int diameter = 0;
int diameterOfBinaryTree(TreeNode* root) {
traverse(root);
return diameter;
}
// 全てのノードを探索
void traverse(TreeNode* root) {
if (root == NULL) return;
int left_height = tree_height(root->left);
int right_height = tree_height(root->right);
diameter = max(diameter, left_height + right_height);
traverse(root->left);
traverse(root->right);
}
// ノードの高さを取得
int tree_height(TreeNode* root) {
if (root == NULL)
return 0;
else {
int left_height = tree_height(root->left);
int right_height = tree_height(root->right);
return max(left_height, right_height) + 1;
}
}
};
最適な方法
ノードの高さを求めるアルゴリズムの途中で最大値を求めてしまえばO(N)で求めることができます。
class Solution {
public:
int diameter = 0;
int diameterOfBinaryTree(TreeNode* root) {
tree_height(root);
return diameter;
}
int tree_height(TreeNode* root) {
if (root == NULL)
return 0;
else {
int left_height = tree_height(root->left);
int right_height = tree_height(root->right);
// 左右が最大になるものを記録
diameter = max(diameter, left_height + right_height);
return max(left_height, right_height) + 1;
}
}
};
Swift Concurrencyのパフォーマンス
Swift Concurrencyは並列プログラミングの書きやすさや便利さがよくなっただけでなくパフォーマンスも改善されています。
これまでの問題
これまでのGCDを使った並列処理では、CPU Coreに対して一つスレッドを割り当てそれをスレッドプールにプールして再利用するのでnon blockingな処理を並列処理する分にはスレッドの切り替えが発生しないためパフォーマンスには問題ありません。
しかし、スレッドをブロックするような処理する場合はスレッドが止まってしまい、止まっている間CPUが無駄にならないように別の処理を実行するためには、新たにスレッドを生成してそのスレッドに切り替えが発生します。スレッドを切り替えるには、今あるスレッドの状態(レジスタとスタック)を退避し、次に実行するスレッドの状態を復元する必要があります。これらスレッドの生成や状態の退避と復元がコンテキストスイッチで、これを行うにはもちろんコストがかかるのでパフォーマンスに影響します。
また、スレッドをブロックする処理が大量に発生し、スレッドが枯渇(スレッド生成数の限界を超える)する可能性もあります。これをThread Explosionと呼びます。 Thread Explosionが発生するとそれ以上スレッドが生成できなくなるだけでなく、dead lockを起こす可能性があります。(ちなみにlimitは64らしい)
Thread Explosionによるdead lock
WWDC2015のビデオの例です。

①main threadからmain threadに対してdispatch_syncするタスクを大量にdispatch_asyncしてスレッドを枯渇させます。
②つぎに、dispatch_syncで呼ばれた処理内でserial queueにdispatch_asyncするとそのserial queueはスレッド不足のためスレッドの空き待ちになります。そのあと同じserial queueにdispatch_syncするとスレッドのスレッドの空き待ちが解消するまでdispatch_syncの処理は実行されないです。このdispatch_syncの処理が終わらないとmain threadはブロックされ、更にmain threadがブロックされると①のdispatch_syncも終わらないのでスレッドが開放されずdead lockとなります。
これまでの問題のまとめ
- スレッドをブロックするような処理があるとコンテキストスイッチが発生してパフォーマンスが落ちる
- スレッドが枯渇するとdead lockを起こすおそれがある
Swift Concurrencyの場合
Swift Concurrencyでは、CPUに対して1スレッドが割り当てられ、スレッドの切り替えが起こらないようになっています。
どうやっているかというとGCDのようにスレッドを内包して使いやすくしているのではなく、continuationという軽量スレッド的な仕組みを使っているようです。 (詳細はこちらのSwift concurrency:Behind the scenes)
asyncが呼ばれてsuspendされた際に元の状態に戻すための情報(stack上に持っているlocal変数など)をheapに保存し、resumeするときにheapからstackに戻すことで再開されているようです。

スレッドとの主な違いは、
- スレッドは生成時にスタックサイズ分のメモリの確保が必要だが、continuationの場合は必要な分だけ確保?
- スレッドの場合は切替時にレジスタの退避復帰が必要だが、continuationの場合はheapからstackに戻すだけ
ということなのかなと思われます。(違ってたらご指摘いただけると幸いです)
結局のところはスレッドであろうとcontinuationであろうと大量生成するとメモリは食うし切り替えコストは発生します。ですので、いくらSwift Concurrencyで改善されたからといって大量に並列処理を行っても速くなるとは限りません。
このようにSwift Concurrencyではこれまでよりパフォーマンスは改善されていますが、依然として並列処理にはメモリや切り替えコストは発生するのでその点は意識して使っていく必要はありますね
