文字っぽいの。

文字を書いています。写真も混ざります。

スネてる暇はないんだよ。

力を入れて書いたブログに反応がなかった時、これは賢いと思った設計変更が受け入れられなかった時、絶対にバズると思った企画が通らなかった時、これは通ると思ったプロポーザルが不採択になった時、便利なライブラリを作ったけどスターがつかない時、絶対ユーザーに喜ばれると思う機能開発が許可されない時。

無能感や気恥ずかしさから、ついつい僕たちは他人のせいにして「この素晴らしさが分からないなんて、なんて愚かな人たちだろう」とスネてしまう。

そんな暇はないんだ。本当に良いと信じるものならば、分かってもらう努力を続けないといけない。本当になにかを良くしたいなら、スネてる暇はないんだよ。

Swiftでファイル名からMIMEタイプを生成する。

やりたいこと

ファイル名からMIMEタイプを生成したい時がある。例えば、

  • "image.png" -> image/png
  • "movie.mov" -> video/quicktime

という感じ。

実装方法

Uniform Type Identifiers を利用する。そのため、iOS 14以降じゃないと使えない。

上述したPNG画像の場合はこんな感じ。

import UniformTypeIdentifiers

let url = URL(string: "file://path/to/image.png")!
let mimeType = UTType(filenameExtension: url.pathExtension)!.preferredMIMEType!

print(mimeType) // => "image/png"

動画も同じ感じで使える。

import UniformTypeIdentifiers

let url = URL(string: "file://path/to/movie.mov")!
let mimeType = UTType(filenameExtension: url.pathExtension)!.preferredMIMEType!

print(mimeType) // => "video/quicktime"

URLではなく、NSStringでも同様のことができる。Stringはそのままだと使えないので、一旦NSStringに変換することになる。

import UniformTypeIdentifiers

let nsString = NSString(string: "video.mov")
let mimeType = UTType(filenameExtension: nsString.pathExtension)!.preferredMIMEType!

print(mimeType) // => "video/quicktime"

備考

当然だが、これはファイル名(の拡張子)からMIMEタイプに変換している。実際にファイルの中身を検査している訳ではないため、拡張子がついていなかったり拡張子が信用できない場合には利用できない。

「僕の考えた最強のデスク環境」をアップデートした。

2年前にデスク環境をがっちり整えた。

fromatom.hatenablog.com

そこから時は経ち、書斎の棚を処分したり新しくWindowsPCを購入したりと環境も変わってきたので、アップデートをした。

この記事では、この状態になるまでに行った作業や、使った製品を紹介していく。

デスクのリメイク

前まで使っていたデスクはこんな感じの白いものだった。

この白い色にだんだん飽きてきたので、暗い茶色のデスクが欲しくなった。KANADEMONOだったり、PREDUCTSだったりと世の中にはおしゃれなデスクが様々あるけれどお値段が高すぎるのと「エンジニアなら既製品買わずにDIYできるやろ」とも思ったのでリメイクすることにした。

使った商品はこれ

item.rakuten.co.jp

いわゆるリメイクシートというやつで、この商品はシールになっているので接着剤が必要なくて便利。デスクの大きさにもよるけれど、自分のデスクサイズだと5,000円程度で購入できた。きれいに貼り付けるのは結構大変だけど、机を買い換えるより全然安いので頑張る。

できた。案外いい感じ。足が白いのだけ微妙だけど、流石に金属の塗装まで手を出すと大仕事になるのでやめておいた。

ちなみに、リメイクシートを買う時には、先にサンプルを購入するのを強くおすすめする。

item.rakuten.co.jp

ネットのサンプル画像では色味も表面のテカリ具合もテクスチャ感も全然分からないので、絶対にサンプルは買おう。

デスク天板裏

天板にリメイクシートを貼るために、デスクにくっついたすべてを取っ払ったので、天板裏のケーブル関係も更新することにした。前回の記事では、超強力両面テープでケーブルボックスを貼り付けていたけど、それは取り外した。

できたものはこんな感じ。

まず左奥にあるのは前回の記事で設置したMac mini用のホルダー。M1チップのMac miniになったので発熱もなくて天板裏でも安心して使えるようになった。今回は特に移動もさせず引き続き使っていくことにした。

次に右奥にあるのは、天板裏のケーブル収納では有名なサンワサプライのケーブルトレー。

ネジ止め方式のものを購入した。そのまま木ねじを止めても良さそうだったけど、鬼目ナットを利用して固定している。結構太い穴をあけないといけないので大変だった。

次に右前にある白いやつはこれ

ティッシュボックスって、手が届くところに置きたいけど地味にでかいし、ダサいし、中身が切れると交換しないとだしで置き場所に困っていたけれど、このテーブル下ラックだとこんな感じで入れられて便利。

これは付属のネジでそのまま固定している。当然だけど、この机のように梁があるタイプだと、ちゃんとティッシュボックスが出し入れできる位置か確認してから取り付けないといけない。

ティッシュボックスがない場所には、引き出しがある。

サイズが丁度いいトレーが全く見つからなかったので、ダイソーのトレーにダイソーのフックを逆さまにつけて取っ手にしたハンドメイド。ダサいけど、机の下は見えないからOK。ちなみにそのままだと滑りが良すぎて使いにくいので、マグネットシートを貼って使っている。

あれこれ配線した後はこんな感じになる。床にケーブルが一切なくなって最高な状態。掃除機かけ放題。

ケーブルは速攻でケーブルトレー内に入るようにしている。ケーブルトレー内にはこの電源タップがドカンと入っていて、壁のコンセントから直接つながるようにしている。口数も多いしコンセント部分は180°回転するようになっているし、雷ガードもついているので大変便利。

ケーブルトレーの外に出ているケーブルはこれらの商品でまとめている。

新しいガジェットがやって来たり、配置を変えた時にはケーブルの配線を直す必要がでてくる。その時にとにかく楽に雑に整理がしやすいことをモットーにしている。ケーブル用のチューブでまとめると確かにかっこよくはなるけど、「巻き直すの面倒だからとりあえずこのままで」といってはみ出したケーブルが破滅を呼ぶのが分かりきっているので使わない。また、天板裏にこういったフックをつけたこともあるが……。

これはケーブルの総数が多いと全く役に立たなくなる。こういうフックは元々ケーブルが少なくてシンプルな構成の人だけが使えるものだと気づいて全部外した。クランプで全部まとめりゃええんや。

デスク上部

デスク上のここらへんに写っているものの紹介。

まずディスプレイだけど、下の大きいのはこれ

いまは11万円程度で売っているけど、一時期クーポンの設定を失敗したのか67,000円弱で買える時があって、その時に購入した。でっかいしキレイで便利。問題はUSB Type-Cでの接続はサポートしていないこと。そのため、MacとはDisplayPortで接続している。USBのハブ機能はついているので、こんな感じでディスプレイ裏にUSBハブをくっつけて使っている。

このUSBハブは

  • Webカメラ
  • LED照明
  • マウスレシーバー
  • USBマイク
  • iPad
  • キーボード

と色々がデバイスがつながるため、セルフパワー式のものを使っている。

次に上のディスプレイはこれ。

だいたいYouTubeが流れている。基本的にはメインディスプレイを横に2分割して使えば事足りるけど、たまに横幅を要求されることがあるので、そういう時にはメインとサブを使えて便利。

ディスプレイアームはメインはこれ

flexispot.jp

サブディスプレイとiPad

という長いポール1本に

を2つ通して使っている。アームだけで購入しないとポールが3本になるので注意。ちなみにiPadはこういうVESAに対応したホルダーがあるのでそれを使っている。

マイクとマイクスタンドはこれ。マイクスタンドの見た目が良いやつがなくて困っていたけど、コレが一番良かった。

スピーカーはもう売っていないけど、Bose Companion 3 Series II systemというやつ。手元にコントローラーが持ってこれるんだけど、コレが非常に便利。壊れないでほしい。

キーボードはHHKB Pro2を2台使っていて、マウスはロジクールのMX ANYWHERE 3。この組み合わせが一番使いやすいと思います。

キーボードの下に敷いているマットはGrovemadeというメーカーのもの。輸入になるので送料が高い。

grovemade.com

こんなマット要るんかいって思うけど、このマットごと奥に滑らせるとキーボードとリストレストとマウスを全部一気に机奥に移動させられるのが便利。机でなにか書き物をしたり、ご飯を食べたり、WindowsPC用のキーボードやマウスを出したりと、そういった場面で非常に役に立つ。

キーボードの手前にあるブーメランみたいなものはリストレスト

deltahub.io

これがなかなか便利で、リストレストに手をおいたまま手を滑らせて、キーボードとマウスを使い続けられる。よくあるデスク構成だとキーボード側にだけリストレストがあるため、マウスとキーボードを行き来する時に「よっこいしょ」と手を上げ下げする必要がある。このDeltaHubのリストレストなら、手を横に滑らせるだけで良いのでとても楽。ただ地味に高い。

デスク上にある小さい時計はこれ。可愛くて買ったけど、カチカチ音が結構うるさい。

七味は根元 八幡屋礒五郎。一番美味しい七味。

サブデスク(PCラック)

つぎにここらへんのもの。

サブデスク的に使っているけど、これはPCラックになっている。自分が買った商品はすでに売らなくなってしまったけど、こういったラックを購入した。

中にはWindowsPCとPS4と有線LANのハブが入れてある。

上においてあるのは、キングジムのペギー

今の所メガネスタンドとしての役割しかなくてかわいそう。

あとは、AnkerのQii充電スタンド

CO2モニターがおいてある。

このラックの奥側には、このケーブルホルダーが貼り付けてある。

基本的に充電ケーブルはここにすべてくっつけてあって、必要になったら引っ張り出して使う運用にしている。PCラックが充電用テーブル的に使われてる感じ。

壁掛け

ここらへんの壁にかかっている簡単な収納。

正面から見るとこんな感じになっている。

これは無印良品の壁につけられる家具棚を改造したもの。

www.muji.com

天面には、キーボードが前に落ちないようにホームセンターで買った黒い取っ手をネジ止めしている。それだけだと、キーボードがゴリッとなって傷ついてしまうので、ダイソータブレットスタンドを使っている。

realsound.jp

流石にでかい地震だとキーボードは落ちてくるだろうけど、まぁこれが落ちるレベルの地震なら命のほうが大事なので良いでしょう。

前面には、これまたホームセンターで買った黒いメタルフックと、Amazonで買ったヘッドホンフックをネジ止めしている。

ヘッドホンフック、1個単位で変える良い見た目のやつがないのでちょっと困った。6個もいらん。

ちなみに、どのパーツも付属するネジだと長くて貫通してしまうので、予めホームセンターで丁度よい長さのネジを購入しておいた。

音周り

音周りはこういう構成にしている。この構成だと常にスピーカーからは音を出しつつ、BTヘッドホンからも音が出せる。スピーカーのミュートが手元で簡単にできるからできる構成という感じがする。

トランスミッターはAnkerのものを使っていて、机の下のケーブルトレー内で常に充電状態にされている。

なんでこんな面倒なことをしているかというと、Bluetoothヘッドホンをつないだ時にヘッドホン側のマイクを使う設定に切り替えられてしまうのが不便すぎるから。マイクは常にYetiのものになってほしいので。副作用として便利だったのは、MTG中にヘッドホンの充電が死にかけたり、最初はスピーカーで聞いてたけどヘッドホンに切り替えるかって時に、音声を聞き漏らすことなく移行できる点。

まとめ

机の色を変えるには買い替えしかないと思っていたけど、リメイクシートでなかなかいい感じに安くできてよかった。デスク下の構成も一気に快適になったので、満足している。

#iOSDC Japan 2022で『 サポートiOSバージョンを定期的にあげる仕組みづくり』というLTをします。

今年もiOSDC Japanの季節がやってきましたね!ありがたいことにLTが採択されたので、登壇します!5年連続登壇することができるので、うれしみに舞い踊っております。

fortee.jp

プロポーザルは

サポートOSバージョン、どんどんあげていきたいですよね? しかし、サポートバージョンを減らすとユーザーも減るため、プロダクトオーナーに渋られることもよくあります。

加えて、開発の現場では「サポートOSバージョンあげたいけど、結構気合で解決できるし……」というエンジニアと、 「エンジニアから要望も来ないから、あげなくてもいいか」というプロダクトオーナーという、お見合い状態になることもあります。 実はサポートOSバージョンをあげてもよさそうなのに、キッカケがない……そんなことはありませんか?

このトークではこういった課題を解決するため、 ・アクティブユーザーのOSバージョン割合で判断するのは悪手!?本当に見るべき指標 ・定期的にサポートOSバージョンアップを検討できるキッカケの仕組み について話します。

このトークが皆さんのアプリのサポートOSバージョンアップにつながると嬉しいです。

という内容です。

大手の企業ではこういった仕組みがすでにできあがっていることが多く、あまり参考にはならないかもしれません。一方で、少数のチームで開発している場合には、明確な基準を決めるのが難しく、なかなかサポートOSバージョンをあげられないことがあります。

このLTではそういった人たちに向けて「こういう基準と仕組みを使うと便利でしたよ」という内容を話せたらと思います。

今回はオフラインでの開催も予定されており、自分も現地でLTをする予定なので、会場で会ったらぜひ声をかけてください!また、サポートOSバージョンをあげていく仕組みについて「うちではこうしてるよ」という話を聞けると嬉しいです。

ではみなさん、会場とインターネットでお会いしましょう!

ラーメン→スーパー銭湯→ラーメン

ゴールデンウィークのほとんどが飼い猫の看病と通院で消失してしまったので、ちょっとしたお出かけをした。「ラーメンと、温泉やな。」ということになり、弟と2人で電車で行ける範囲で出かけた。

味噌っ子 ふっく

朝の10時30分に荻窪駅に集合して、味噌っ子ふっくに向かった。

tabelog.com

開店は11時なので、もう並んでいそうだなぁと思っていたけど、予想以上に長い行列ができていた。開店時間から1時間半ほど並んで着席。

辛味噌らーめん(もやし大盛)

うまい。東京はうまい味噌ラーメンのお店が少ないので貴重。もやしが無料で大盛にできるのが嬉しい。汗をかく程度には辛いので、辛いのが苦手な人は普通のが良さそう。

なごみの湯

荻窪には駅前にでかいスーパー銭湯があるので、ここで夕飯のラーメンまで時間をつぶした。

www.nagomino-yu.com

館内着を着て、漫画スペースや仮眠室があって、時間を長くつぶせるタイプのスーパー銭湯。お風呂とサウナを楽しむ。外気浴コーナーの椅子はリクライニングタイプのチェアと、オットマン的に踏み台がおいてあって完璧感が高かった。

サウナ後はオロポ。

ポカリがでかい缶だったので、配分が難しかった。

このあとは夕飯のラーメンまで暇を潰すしかなく時間がありあまっているので、マッサージサービスを使ってみることにした。痛くて死ぬかと思った。

元祖スタミナ満点らーめん すず鬼

夕飯の時間になったので、荻窪駅から三鷹駅に電車で移動してすず鬼へ。

tabelog.com

外待ち12人でそこそこ並んでいたけど、回転が早くて1時間経たないくらいで入店。

スタ満ソバ(トッピング全部)+まさお+魚玉

スタ満ソバ(トッピング全部)、まさお、魚玉。すげぇうまい。スープを一口すすって半ライスを頼むかハチャメチャに迷ったけど、麺量280gあってそれだけで腹一杯になりそうなのでやめておいた。実際、麺に加えて上に乗っている豚肉がゴロゴロ大量に入っていて腹パンパンになったので良い判断だった。上の肉とスープだけでご飯3杯食べられる勢いだけど、お腹に入らないのが悲しい。近場にあったら毎週通ってしまうし、お腹ペコペコにしてライスを追加したい。

コンビニで黒烏龍茶を買って帰った。

Apexでソロダイヤを達成するために、買ってよかったゲーミングデバイス。

去年の2月にApexデビューをしてからコツコツ続けて、ようやく今年の3月にソロダイヤを達成しました。めでたい。

この記事では、ソロダイヤを達成するまでに買ってよかったと感じているデバイスを紹介します。

新しいPC

いやPCは持ってて当然でしょって感じですが、Apexをやり始めた頃は「なんでこの骨董品でApexが動くの?」というレベルのPCで遊んでいました。しかし、さすがに30fpsでれば御の字な環境だといろいろが厳しく、たまにクラッシュするようにもなってしまったので、TSUKUMOでBTO PCを買いました。

www.tsukumo.co.jp

詳しいスペックなどは下記の記事にまとめています。

fromatom.hatenablog.com

これでようやく快適な環境でApexができるようになったわけです。新しいPCにして画質や処理速度があがったのはもちろんですが、GeForce Experienceが使えるようになったのも良かったです。これにはインスタントリプレイの機能がついていて、保存ボタンを押したタイミングから過去最大20分の録画を保存してくれます。PS5やSwitchにも似た機能がありますね。これのお陰でゲームプレイの様子を録画して、あとから反省会をすることができるようになりました。

キーボード

LogicoolのG913を買った。色々と探してみたけども

  • USBワイヤレス(Bluetoothでない)
  • カニカル
  • テンキーレス

という条件で探すとほとんど候補がなく、自然とこれになった。特にBluetoothではないワイヤレスのものはほとんどない。

ゲーミングよろしく虹色に光る。最初はいらないと思っていたけど、ゲームごとの設定をする時に色も設定しておけば「ちゃんと切り替えできたかな?」というのが色の変化でわかって良い。Logicool製品用ソフトであるG HUBは結構挙動が怪しく、たまに設定が反映されなかったりするので、色でちゃんと効いているのが分かって便利。

自作パームレスト

G913にちょうどいいサイズのパームレストがなかったので自作した。詳しくは下記記事で紹介している。

fromatom.hatenablog.com

これはかなり満足度が高くて、今でも良い選択をしたと感じる。ただ、どうしても手汗や摩擦によって毛羽立って来てしまったので、防水ニスなどを塗布するか検討中。

マウス

世の中ではG PROが人気なようだけど、「サイドボタンが2つだけは不便だなぁ」と思ったので、もうちょっとボタンがあるやつを買った。結果として、サイドボタンの数もちょうど良かったし、使い心地も申し分ない。高いけど。

ちなみに、仕事では同じくLogicoolのAnywhere3を使っている。Apexを始めたての頃は、ゲームもAnywhere3でやっていたけれど根本的に手の使い方が違うので変えることにした。

Anywhere3は小型なのでつまむようにもって、指の動きだけでカーソルを動かしているけれど、FPSゲームにおいてこの感度が高い設定は相性が悪い。いわゆるハイセンシになるので、これで上手い人もいるけど平均的な値ではなくなる。そこで、ミドルセンシにすると、マウスをかなり動かすことになるので、指先でマウスを動かすのではなく、手でしっかりつかんで腕も使ってマウスを動かす。こういう動きのときにはAnywhere3の小ささが逆に使いにくくなり、G502の大きさがちょうどよくなる。

マウス用アンチスリップテープ

マウスに貼って滑り止めになるシール。最初は「いるのか?」と思っていたけど、かなり効果があった。マウスがマウスパッド端に来てしまったときに、素早く浮かせる動作をするとき、親指と小指で持ち上げるのだけど、その時の安定感が凄まじく上がる。前までは滑って落ちないように結構小指に力が入っていたけど、このシールのお陰でグリップが強いのでとても楽になった。

また、右クリック・左クリックにグリップがあるのもかなりいい。クリック時の指の動きは真下に行くのではなく、手前に指が曲がりながらクリックする動作をする。このため、指先が滑るとクリックが思ったタイミングとずれたり、指切りを失敗したりする。グリップテープがあると指先はちゃんと止まってくれているので、力のかけ具合が毎回一定になって良い。

レビューにも書いてあるけど、貼るのがちょっと難しいので頑張る必要がある。

マウスパッド

Logicoolのでっかいやつ。FPSを始める前は「こんなでかいマウスパッドとかいらんやろ」と思ってたけど必要だった。てか、このマウスパッドがおける程度に机にスペースがないとFPSをするのは大変になる。

上述したように、FPSと普段の仕事ではマウスの使い方が全く異なるので、マウスパッドもサイズが異なるやつを使うことになる。このマウスパッドはでかいので普段は巻いて片付けて、ゲームをするときだけ広げている。

イヤホン

もともと持っていたBOSEのインイヤーを使っている。

古いモデルなので今これを買う必要はないと思うけど、音はいいし、長時間つけていても耳が痛くならないのでとても便利。ヘッドホンだとどうしても耳が痛くなるし、夏場蒸れたりして大変だったけど、インイヤーなら余裕だった。

ただ有線なのが不便なので、AnkerのBluetoothレシーバーを利用してなんちゃって無線化をしている。

これが地味に便利で、ちょっと扇風機をつけたい時や、水をくみたい時にイヤホンを外したりつけたりしなくて良くなる。不便な点としては、充電が必要になること。

ちなみに、FPS界隈では同じくBOSEのノイキャン付きインイヤーが人気らしい。

充電ケーブル

これだけ周辺機器を揃えていくと、充電も大変になってくる。毎回USBの上下を確認して、充電ケーブルを差すのもアホらしくなってきたのでマグネット式のものを買った。

これがかなり効果的で、今まで自分は結構気合を入れて充電作業をしていたんだと気づくことができた。これを買ったら「とりあえずくっつけとこ」で充電が始まるので、非常に簡単かつノーストレス。

問題点としては、このマグネット式の充電ケーブルは軒並み品質が怪しく、どこのを買っても外れそうだし壊れそうという点。上で紹介しているケーブルも燃えたというレビューもある。かなり便利だけど、このメーカーのが良いよと勧めることができなくて不便。

ちなみに、G502は充電口が奥まっていて使えないので、仕方なく直接差して充電している。

KovaaK's

store.steampowered.com

これはデバイスじゃないけど、エイム練習ソフト。ゲームをするのに練習?とおもうかも知れないけど、FPSはeスポーツの1ジャンルにもなるジャンルなのでスポーツ同様にある程度練習が必要。普通にApexをしているだけでもある程度うまくなるけど、どうしても頭打ちするのでこのソフトで練習した。

バスケのドリブルやラケット競技でボールを思った方向に打つのが最初は難しいように、FPSでもエイムを練習しないと思ったように弾を当てられない。

自分はこの動画を見て、同じ練習メニューでコツコツ練習を続けた。

www.youtube.com

買わなかったもの

Apexコイン

スキンとかと交換できる課金アイテム。Apexは基本無料で遊べて、このコインで課金アイテムを買うタイプのやつ。

スキンが変わろうと、エイムが上手くなったり立ち回りが良くなるわけじゃないので要らない。銃のスキンの中には若干見やすさが上がるやつもあるけど、それが影響を及ぼすレベルのエイム力になるにはマスター帯を目指してる頃なので今はいらないかな。ガチャで当たったら嬉しいなというレベル。

あと、愛用レジェンドのヒューズくんのスキンはなんか微妙なのばっかりなので、そのために課金する気も起きない感じ。ヒューズのスパレジェが出たらその時に課金する予定。いつになるやら。

ミックスアンプ

正直不要だと思っている。ゲーミング界隈のYouTubeとか見ていると、

とか

が人気で良く紹介されている。これらを使うと、音の定位が良くなり足音が分かりやすくなるらしい。ただ個人的にはWindowsに標準搭載されているWindows sonic for headphones機能で十分だと思う。これをONにするだけで、かなり足音の方向はわかりやすくなるので、使ったことがない人は使ってみてほしい。最初はちょっと違和感があると思うけど、すぐ慣れると思う。ただ、普通にYouTubeとか見るときには邪魔なので普段は切っておくと良い。

UISegmentedControlを角丸にする

できたもの

f:id:FromAtom:20220404230101g:plain

コード

import UIKit

final class RoundedSegmentedControl: UISegmentedControl {
    private let segmentInset: CGFloat = 5

    override func layoutSubviews() {
        super.layoutSubviews()

        layer.cornerRadius = bounds.height / 2.0

        let foregroundIndex = numberOfSegments
        if subviews.indices.contains(foregroundIndex), let foregroundImageView = subviews[foregroundIndex] as? UIImageView {
            foregroundImageView.bounds = foregroundImageView.bounds.insetBy(dx: segmentInset, dy: segmentInset)
            foregroundImageView.image = generateImage(color: selectedSegmentTintColor ?? .white)
            foregroundImageView.layer.removeAnimation(forKey: "SelectionBounds")
            foregroundImageView.layer.masksToBounds = true
            foregroundImageView.layer.cornerRadius = foregroundImageView.bounds.height / 2.0
        }
    }

    private func generateImage(color: UIColor) -> UIImage? {
        let size = CGSize(width: 1, height: 1)
        let rect = CGRect(origin: .zero, size: size)
        let renderer = UIGraphicsImageRenderer(bounds: rect)
        return renderer.image { context in
            context.cgContext.setFillColor(color.cgColor)
            context.fill(rect)
        }
    }
}

参考にしたサイト

https://stackoverflow.com/a/60661794/17132714