Toyship.org 2023-06-14T15:28:38+09:00 toyship Hatena::Blog hatenablog://blog/6435988827675974011 iOS17で自分の声を作ってみました hatenablog://entry/820878482941287863 2023-06-14T15:28:38+09:00 2024-03-19T06:49:34+09:00 この秋にリリースされる予定の iOS17では、Personal Voiceという新機能が搭載されています。 自分の声をiPhone に学習させ、どんな文章でも自分の声で読んでもらうことができる機能です。 Personal Voiceの作り方 まず、iOS17のiPhoneで、言語設定を英語にしてください。 設定の「Accessibility」のメニューの「Personal Voice」という項目から「Create a Personal Voice」を選び、あとは画面の指示に従いましょう。 15分ほどずっと英文を読み続けなくてはいけないので、ちょっとつらいです。 周囲に騒音がない場所でやってくだ… <p>この秋にリリースされる予定の iOS17では、Personal Voiceという新機能が搭載されています。 自分の声をiPhone に学習させ、どんな文章でも自分の声で読んでもらうことができる機能です。</p> <h2 id="Personal-Voiceの作り方">Personal Voiceの作り方</h2> <p>まず、iOS17のiPhoneで、言語設定を英語にしてください。 設定の「Accessibility」のメニューの「Personal Voice」という項目から「Create a Personal Voice」を選び、あとは画面の指示に従いましょう。</p> <p>15分ほどずっと英文を読み続けなくてはいけないので、ちょっとつらいです。 周囲に騒音がない場所でやってくださいね。</p> <p>あとは端末を電源につないで、一晩待ちましょう。 (音声はiPhone内部で生成されますが、だいぶ時間がかかります。気長に待ちましょう。)</p> <h2 id="Personal-Voiceの使い方">Personal Voiceの使い方</h2> <p>Personal Voiceは、iOS17の新機能Live Speechなどで使えます。</p> <p>また、開発者はspeech APIを使うことによって、好きな文章を読ませることもできます。</p> <p>AVSpeechSynthesizer.requestPersonalVoiceAuthorizationで、Personal Voiceを使うためのAuthをとり、AVSpeechUtteranceを作ってそのvoiceに取得したPersonal Voiceを付加するだけなので、簡単です。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> AVFoundation <span class="synPreProc">func</span> <span class="synIdentifier">speechMyVoice</span>(){ <span class="synPreProc">let</span> <span class="synIdentifier">synthesizer</span> <span class="synIdentifier">=</span> AVSpeechSynthesizer() AVSpeechSynthesizer.requestPersonalVoiceAuthorization(completionHandler<span class="synSpecial">:</span> { status <span class="synStatement">in</span> <span class="synStatement">if</span> status <span class="synIdentifier">==</span> .authorized { <span class="synPreProc">let</span> <span class="synIdentifier">personalVoices</span> <span class="synIdentifier">=</span> AVSpeechSynthesisVoice.speechVoices().filter{<span class="synIdentifier">$0</span>.voiceTraits.contains(.isPersonalVoice)} <span class="synPreProc">let</span> <span class="synIdentifier">myUtterance</span> <span class="synIdentifier">=</span> AVSpeechUtterance(string<span class="synSpecial">:</span> <span class="synConstant">&quot;He was an old man ...&quot;</span>) myUtterance.voice <span class="synIdentifier">=</span> personalVoices.first synthesizer.speak(myUtterance) } </pre> <h2 id="作ってみたPersonal-Voice">作ってみたPersonal Voice</h2> <p>さっそく、自分でもPersonal Voiceを作って、ヘミングウェイを読ませてみました。 自分の英語の声を聞く機会がないんですが、かなり似ているような気がします。 (英語の発音が悪いのはご容赦ください。)</p> <p>iPhone が作ってくれた私の音声 <audio src="https://drive.google.com/uc?id=1fKTgj0K9AnLe9d-jpW_36aLISXBGkBhU" controls></audio></p> <p>自分で直接読んでみた音声(時々文を間違えて読んでたりします……。) <audio src="https://drive.google.com/uc?id=1RBgxgRJlCoq6vTP2HoVJd4oqXRLx9AVn" controls></audio></p> <p>iOS17のパブリックベータはそろそろ配布されるはずですので、一般の方も試せるようになると思います。</p> <p>「発話できない、または発話能力を失いつつある人々のために開発された機能」なのですが、いろいろと応用がききそうな気もします。 今は英語だけですが、日本語にも対応してほしいですよね。</p> toyship ハイラルの座標系(ネタバレなし) hatenablog://entry/4207575160648773070 2023-05-14T15:10:21+09:00 2023-05-14T15:14:42+09:00 ゼルダの伝説、ティアーズオブキングダム、一昨日発売されてさっそくはまっています。今作から導入された座標系について、ネタバレなしです。 座標の意味 マップの画面の右下に3個の数字がありますね。 1個目がx位置、2個目がy位置、3個目がz位置を表しています。 座標 座標の単位はm。原点はマップの中央です。 たとえば、この画像の位置「0999 0231 0048」の場合、原点から999m東(マップ上では右)、231m北(マップ上では上)、標高が48mということになります。 右下に座標が表示されます 座標から、ハイラルは原点から東西南北に向かって3500m程度広がっているのがわかります。 マップの広さ… <p>ゼルダの伝説、ティアーズオブキングダム、一昨日発売されてさっそくはまっています。今作から導入された座標系について、ネタバレなしです。</p> <h2 id="座標の意味">座標の意味</h2> <p>マップの画面の右下に3個の数字がありますね。 1個目がx位置、2個目がy位置、3個目がz位置を表しています。</p> <p><figure class="figure-image figure-image-fotolife" title="座標"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20230514/20230514142607.jpg" width="800" height="450" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>座標</figcaption></figure> 座標の単位はm。原点はマップの中央です。</p> <p>たとえば、この画像の位置「0999 0231 0048」の場合、原点から999m東(マップ上では右)、231m北(マップ上では上)、標高が48mということになります。</p> <p><figure class="figure-image figure-image-fotolife" title="右下に座標が表示されます"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20230514/20230514142602.jpg" width="800" height="450" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>右下に座標が表示されます</figcaption></figure></p> <p>座標から、ハイラルは原点から東西南北に向かって3500m程度広がっているのがわかります。 マップの広さはだいたい7km四方くらいですね。</p> <h2 id="原点はどこ">原点はどこ?</h2> <p>座標の原点はマップの中央です。 メーベの町跡の少し北のあたりですね。</p> <p><figure class="figure-image figure-image-fotolife" title="ハイラルの原点"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20230514/20230514012547.jpg" width="800" height="450" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ハイラルの原点</figcaption></figure></p> <h2 id="山の標高">山の標高</h2> <p>せっかくなので、ハイリアの山々に登って標高を調べてみました。</p> <table> <thead> <tr> <th> 山 </th> <th> 標高 </th> </tr> </thead> <tbody> <tr> <td> デスマウンテン </td> <td> 896m </td> </tr> <tr> <td> へブラ山 </td> <td> 738m </td> </tr> <tr> <td> ゲルド山頂 </td> <td> 708m </td> </tr> <tr> <td> ラネール山 </td> <td> 539m </td> </tr> <tr> <td> 双子山(北) </td> <td> 435m </td> </tr> <tr> <td> フロリア山 </td> <td> 387m </td> </tr> <tr> <td> 双子山(南) </td> <td> 385m </td> </tr> </tbody> </table> <p>ハイリアで一番高い山はデスマウンテンでした。(下の画像はブレスオブザワイルドの山頂ですが、ティアーズオブキングダムでもちゃんと山頂の標識はありました。)</p> <p><figure class="figure-image figure-image-fotolife" title="デスマウンテン山頂"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20230514/20230514132727.jpg" width="800" height="450" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デスマウンテン山頂</figcaption></figure></p> <p>ヒマラヤのような厳しい冬山のへブラ山ですが、実際の高さは738mでした。 高尾山(599m)より少し高いくらいですね。 狭いマップに高山を置いたので実際の雪山と同じ高さにするのは少し難しかったんですね。</p> <h2 id="情報共有によさそう">情報共有によさそう</h2> <p>今までは、「ミスミ橋の少し南」などと地名をもとに位置決定をしていましたが、座標を使えば位置が正確に説明できます。 情報共有がしやすくなってますますゲームにはまりそうです……。</p> toyship Matplotのグラフにフォントを指定する hatenablog://entry/4207112889985948325 2023-04-30T22:25:38+09:00 2023-04-30T22:34:12+09:00 Matplotを使ってグラフを描いた場合、環境によって文字が表示されないことがあります。 そんな時には、好きなフォントを設定してみましょう。 フォントを指定しない場合 まずは普通にsinグラフを描いてみましょう。 import numpy as np import matplotlib.pyplot as plt # x軸の範囲を設定 x = np.linspace(-np.pi, np.pi, 300) # sinのグラフを描画 plt.plot(x, np.sin(x)) # グラフのタイトルの設定 plt.title('Sin(x) のグラフ') # x軸、y軸のラベルを設定 plt.x… <p>Matplotを使ってグラフを描いた場合、環境によって文字が表示されないことがあります。 そんな時には、好きなフォントを設定してみましょう。</p> <h2 id="フォントを指定しない場合">フォントを指定しない場合</h2> <p>まずは普通にsinグラフを描いてみましょう。</p> <pre class="code" data-lang="" data-unlink>import numpy as np import matplotlib.pyplot as plt # x軸の範囲を設定 x = np.linspace(-np.pi, np.pi, 300) # sinのグラフを描画 plt.plot(x, np.sin(x)) # グラフのタイトルの設定 plt.title(&#39;Sin(x) のグラフ&#39;) # x軸、y軸のラベルを設定 plt.xlabel(&#39;x&#39;) plt.ylabel(&#39;y&#39;) # グラフを表示 plt.show()</pre> <p>これで描画すると、こう表示されます。「Sin(x) のグラフ」のなかの「のグラフ」の文字が豆腐になってしまっていますね……。 <figure class="figure-image figure-image-fotolife" title="フォント指定なしのグラフ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20230430/20230430221644.png" width="587" height="455" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <h2 id="フォントを指定した場合">フォントを指定した場合</h2> <p>そこで、ヒラギノフォントを指定してみましょう。 (ついでに、retina解像度で表示するための設定も追加。)</p> <pre class="code" data-lang="" data-unlink>import numpy as np import matplotlib.pyplot as plt import matplotlib.font_manager as fm # retina解像度で表示するための設定 %config InlineBackend.figure_format = &#39;retina&#39; # ヒラギノを指定する hiragino_font = fm.FontProperties(fname=&#39;/System/Library/Fonts/ヒラギノ角ゴシック W0.ttc&#39;) plt.rcParams[&#39;font.family&#39;] = hiragino_font.get_name() # x軸の範囲を設定 x = np.linspace(-np.pi, np.pi, 300) # sinのグラフを描画 plt.plot(x, np.sin(x)) # グラフのタイトルの設定 plt.title(&#39;Sin(x) のグラフ&#39;) # x軸、y軸のラベルを設定 plt.xlabel(&#39;x&#39;) plt.ylabel(&#39;y&#39;) # グラフを表示 plt.show()</pre> <p>日本語の文字がちゃんと表示されて、Retina指定でグラフもくっきり表示されていますね。</p> <p><figure class="figure-image figure-image-fotolife" title="ヒラギノを指定したグラフ"> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20230430/20230430221900.png" width="1172" height="910" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <h2 id="システムのフォントの取得">システムのフォントの取得</h2> <p>システムで利用可能なフォントの取得はこれで大丈夫。</p> <pre class="code" data-lang="" data-unlink>import matplotlib.font_manager as fm # フォントのリストを取得 font_list = fm.findSystemFonts() # フォントを表示 for font in font_list: print(font)</pre> toyship Custom UIContentConfiguration で楽にCell 管理 hatenablog://entry/13574176438064001053 2022-02-17T09:58:34+09:00 2022-02-17T09:59:55+09:00 iOS14から使えるようになった、 UIContentConfiguration 、便利ですよね。 CollectionViewCellを作らずにCollectionViewを使えます。 UICollectionViewListCell のdefaultContentConfiguration まずは、 UICollectionViewListCell の、 defaultContentConfiguration を使ってみましょう。 cellからdefaultContentConfigurationでConfigurationを取得し、それにStringやImageを渡すだけで要素が表示され… <p>iOS14から使えるようになった、 <code>UIContentConfiguration</code> 、便利ですよね。 CollectionViewCellを作らずにCollectionViewを使えます。</p> <h2>UICollectionViewListCell のdefaultContentConfiguration</h2> <p>まずは、 UICollectionViewListCell の、 defaultContentConfiguration を使ってみましょう。</p> <p>cellからdefaultContentConfigurationでConfigurationを取得し、それにStringやImageを渡すだけで要素が表示されます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">func</span> <span class="synIdentifier">collectionView</span>(_ collectionView<span class="synSpecial">:</span> <span class="synType">UICollectionView</span>, cellForItemAt indexPath<span class="synSpecial">:</span> <span class="synType">IndexPath</span>) <span class="synSpecial">-&gt;</span> <span class="synType">UICollectionViewCell</span> { <span class="synPreProc">let</span> <span class="synIdentifier">cell</span> <span class="synIdentifier">=</span> collectionView.dequeueReusableCell(withReuseIdentifier<span class="synSpecial">:</span> <span class="synConstant">&quot;cell&quot;</span>, <span class="synStatement">for</span><span class="synSpecial">:</span> <span class="synType">indexPath</span>) <span class="synStatement">as!</span> <span class="synType">UICollectionViewListCell</span> <span class="synPreProc">let</span> <span class="synIdentifier">info</span> <span class="synIdentifier">=</span> sampleData[indexPath.row] <span class="synPreProc">var</span> <span class="synIdentifier">cellConfiguration</span> <span class="synIdentifier">=</span> cell.defaultContentConfiguration() cellConfiguration.text <span class="synIdentifier">=</span> info.title cellConfiguration.secondaryText <span class="synIdentifier">=</span> info.subTitle cellConfiguration.image <span class="synIdentifier">=</span> UIImage(systemName<span class="synSpecial">:</span> <span class="synType">info.iconName</span>) cell.contentConfiguration <span class="synIdentifier">=</span> cellConfiguration <span class="synStatement">return</span> cell } </pre> <p><figure class="figure-image figure-image-fotolife" title="defaultContentConfiguration"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20220216/20220216132616.jpg" alt="f:id:toyship:20220216132616j:plain:w200" width="400" height="805" loading="lazy" title="" class="hatena-fotolife" style="width:200px" itemprop="image"></span><figcaption>defaultContentConfiguration</figcaption></figure></p> <p>コードの全体はこちら。 <a href="https://gist.github.com/TachibanaKaoru/a6dea8f1a221ba9d68e3098b0df2b10e">UICollectionViewListCell defaultContentConfiguration &middot; GitHub</a></p> <h2>UICellAccessory</h2> <p>UICollectionViewListCellには、アクセサリーをつけることも簡単です。</p> <p>システムのアクセサリーはcheckmarkやdeleteなどが用意されています。 一度に表示できるシステムアクセサリーは一つです。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> cell.accessories <span class="synIdentifier">=</span> [.checkmark()] cell.accessories <span class="synIdentifier">=</span> [.disclosureIndicator(displayed<span class="synSpecial">:</span> .always, options<span class="synSpecial">:</span> .<span class="synIdentifier">init</span>(isHidden<span class="synSpecial">:</span> <span class="synType">false</span>, reservedLayoutWidth<span class="synSpecial">:</span> <span class="synType">nil</span>, tintColor<span class="synSpecial">:</span> <span class="synType">UIColor.red</span>))] cell.accessories <span class="synIdentifier">=</span> [.delete()] </pre> <p>カスタムアクセサリーも表示することができます。 カスタムアクセサリーは一度に複数表示することができます。 表示位置は、leading/trailing、 編集中だけ表示、常時表示の切り替えもできます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">customAccessory1</span> <span class="synIdentifier">=</span> UICellAccessory.CustomViewConfiguration( customView<span class="synSpecial">:</span> <span class="synType">UIImageView</span>(image<span class="synSpecial">:</span> <span class="synType">UIImage</span>(systemName<span class="synSpecial">:</span> <span class="synConstant">&quot;bandage&quot;</span>)), placement<span class="synSpecial">:</span> .leading(displayed<span class="synSpecial">:</span> .always)) </pre> <p>こちらは、3つのカスタムアクセサリーと一つのシステムアクセサリーを表示した例。 バンドエイドマークとトレイマークがleading側につけたアクセサリー、自転車マークがtrailing側につけたアクセサリーです。それにくわえてcheckmarkのシステムアクセサリーをつけています。</p> <p><figure class="figure-image figure-image-fotolife" title="UICellAccessory"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20220216/20220216133558.jpg" alt="f:id:toyship:20220216133558j:plain:w200" width="400" height="817" loading="lazy" title="" class="hatena-fotolife" style="width:200px" itemprop="image"></span><figcaption>UICellAccessory</figcaption></figure></p> <p>全体のコードはこちらです。</p> <p><a href="https://gist.github.com/TachibanaKaoru/68f770ee382b2f595f586280169bb904">UICellAccessory &middot; GitHub</a></p> <h2>updateHandler</h2> <p>iOS15からは、updateHandlerも用意されていて、より柔軟にConfigurationを設定することができます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> cell.configurationUpdateHandler <span class="synIdentifier">=</span> { cell, state <span class="synStatement">in</span> <span class="synPreProc">var</span> <span class="synIdentifier">content</span> <span class="synIdentifier">=</span> UIListContentConfiguration.cell().updated(<span class="synStatement">for</span><span class="synSpecial">:</span> <span class="synType">state</span>) content.text <span class="synIdentifier">=</span> info.title <span class="synIdentifier">+</span> <span class="synConstant">&quot;!&quot;</span> content.secondaryText <span class="synIdentifier">=</span> info.subTitle content.image <span class="synIdentifier">=</span> UIImage(systemName<span class="synSpecial">:</span> <span class="synType">info.iconName</span>) <span class="synStatement">if</span> state.isDisabled { content.textProperties.color <span class="synIdentifier">=</span> .systemGray } cell.contentConfiguration <span class="synIdentifier">=</span> content } </pre> <h2>Custom UIContentConfiguration</h2> <p>手軽に使える defaultContentConfiguration ですが、そもそもUICollectionViewListCellでしか使えないんですよね。UICollectionViewCellで同じようなことをしたいな……と思ったら、Custom UIContentConfigurationを作ってみましょう。</p> <p>ここでは、画像を大きめに表示する LargeImageConfig という UIContentConfiguration と、それが描画するView としてLargeImageContentViewというViewを作っています。</p> <p>LargeImageConfigには、このConfigurationで保持したいデータを定義します。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">struct</span> <span class="synIdentifier">LargeImageConfig</span><span class="synSpecial">:</span> <span class="synType">UIContentConfiguration</span> { <span class="synPreProc">var</span> <span class="synIdentifier">mainImage</span><span class="synSpecial">:</span> <span class="synType">UIImage?</span> <span class="synIdentifier">=</span> <span class="synConstant">nil</span> <span class="synPreProc">var</span> <span class="synIdentifier">mainTitle</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synPreProc">var</span> <span class="synIdentifier">subTitle</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synPreProc">var</span> <span class="synIdentifier">detail</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synPreProc">func</span> <span class="synIdentifier">makeContentView</span>() <span class="synSpecial">-&gt;</span> <span class="synType">UIView</span> <span class="synIdentifier">&amp;</span> UIContentView { <span class="synStatement">return</span> LargeImageContentView(configuration<span class="synSpecial">:</span><span class="synType">self</span>) } <span class="synPreProc">func</span> <span class="synIdentifier">updated</span>(<span class="synStatement">for</span> state<span class="synSpecial">:</span> <span class="synType">UIConfigurationState</span>) <span class="synSpecial">-&gt;</span> <span class="synType">LargeImageConfig</span> { <span class="synStatement">return</span> <span class="synIdentifier">self</span> } } </pre> <p>LargeImageContentViewは、表示するView部分。 UILabelやUIImageViewなどを置いてみます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synIdentifier">LargeImageContentView</span> <span class="synSpecial">:</span> <span class="synType">UIView</span>, UIContentView { <span class="synStatement">private</span> <span class="synPreProc">var</span> <span class="synIdentifier">mainImageView</span><span class="synSpecial">:</span> <span class="synType">UIImageView</span> <span class="synIdentifier">=</span> UIImageView(frame<span class="synSpecial">:</span> <span class="synType">CGRect.zero</span>) <span class="synStatement">private</span> <span class="synPreProc">var</span> <span class="synIdentifier">mainTitle</span><span class="synSpecial">:</span> <span class="synType">UILabel</span> <span class="synIdentifier">=</span> UILabel() <span class="synStatement">private</span> <span class="synPreProc">var</span> <span class="synIdentifier">subTitle</span><span class="synSpecial">:</span> <span class="synType">UILabel</span> <span class="synIdentifier">=</span> UILabel() <span class="synStatement">private</span> <span class="synPreProc">var</span> <span class="synIdentifier">detail</span><span class="synSpecial">:</span> <span class="synType">UILabel</span> <span class="synIdentifier">=</span> UILabel() <span class="synPreProc">var</span> <span class="synIdentifier">configuration</span><span class="synSpecial">:</span> <span class="synType">UIContentConfiguration</span>{ <span class="synStatement">didSet</span>{ updateConfig() } } <span class="synIdentifier">init</span>(configuration<span class="synSpecial">:</span> <span class="synType">UIContentConfiguration</span>) { <span class="synIdentifier">self</span>.configuration <span class="synIdentifier">=</span> configuration <span class="synIdentifier">super</span>.<span class="synIdentifier">init</span>(frame<span class="synSpecial">:</span>.zero) addSubview(mainImageView) mainImageView.frame <span class="synIdentifier">=</span> CGRect(x<span class="synSpecial">:</span> <span class="synConstant">220</span>, y<span class="synSpecial">:</span> <span class="synConstant">0</span>, width<span class="synSpecial">:</span> <span class="synConstant">80</span>, height<span class="synSpecial">:</span> <span class="synConstant">80</span>) mainImageView.layer.borderColor <span class="synIdentifier">=</span> UIColor.lightGray.cgColor mainImageView.layer.borderWidth <span class="synIdentifier">=</span> <span class="synConstant">1.0</span> mainImageView.layer.cornerRadius <span class="synIdentifier">=</span> <span class="synConstant">5.0</span> mainImageView.contentMode <span class="synIdentifier">=</span> .scaleAspectFit mainTitle.frame <span class="synIdentifier">=</span> CGRect(x<span class="synSpecial">:</span> <span class="synConstant">0</span>, y<span class="synSpecial">:</span> <span class="synConstant">0</span>, width<span class="synSpecial">:</span> <span class="synConstant">300</span>, height<span class="synSpecial">:</span> <span class="synConstant">40</span>) mainTitle.font <span class="synIdentifier">=</span> UIFont.boldSystemFont(ofSize<span class="synSpecial">:</span> <span class="synConstant">30</span>) addSubview(mainTitle) subTitle.frame <span class="synIdentifier">=</span> CGRect(x<span class="synSpecial">:</span> <span class="synConstant">0</span>, y<span class="synSpecial">:</span> <span class="synConstant">40</span>, width<span class="synSpecial">:</span> <span class="synConstant">300</span>, height<span class="synSpecial">:</span> <span class="synConstant">20</span>) subTitle.font <span class="synIdentifier">=</span> UIFont.systemFont(ofSize<span class="synSpecial">:</span> <span class="synConstant">15</span>) addSubview(subTitle) detail.frame <span class="synIdentifier">=</span> CGRect(x<span class="synSpecial">:</span> <span class="synConstant">50</span>, y<span class="synSpecial">:</span> <span class="synConstant">60</span>, width<span class="synSpecial">:</span> <span class="synConstant">250</span>, height<span class="synSpecial">:</span> <span class="synConstant">80</span>) detail.font <span class="synIdentifier">=</span> UIFont.systemFont(ofSize<span class="synSpecial">:</span> <span class="synConstant">10</span>) detail.numberOfLines <span class="synIdentifier">=</span> <span class="synConstant">0</span> addSubview(detail) updateConfig() } <span class="synStatement">required</span> <span class="synIdentifier">init</span>?(coder<span class="synSpecial">:</span> <span class="synType">NSCoder</span>) { fatalError(<span class="synConstant">&quot;init(coder:) has not been implemented&quot;</span>) } <span class="synPreProc">func</span> <span class="synIdentifier">updateConfig</span>(){ <span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">conf</span> <span class="synIdentifier">=</span> configuration <span class="synStatement">as?</span> <span class="synType">LargeImageConfig</span> { mainImageView.image <span class="synIdentifier">=</span> conf.mainImage mainTitle.text <span class="synIdentifier">=</span> conf.mainTitle subTitle.text <span class="synIdentifier">=</span> conf.subTitle detail.text <span class="synIdentifier">=</span> conf.detail } } } </pre> <p>表示するとこうなります。 <figure class="figure-image figure-image-fotolife" title="Custom UIContentConfiguration"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20220216/20220216165450.jpg" alt="f:id:toyship:20220216165450j:plain:w200" width="400" height="827" loading="lazy" title="" class="hatena-fotolife" style="width:200px" itemprop="image"></span><figcaption>Custom UIContentConfiguration</figcaption></figure></p> <p>全体のコードはこちらです。 <a href="https://gist.github.com/TachibanaKaoru/205aaa11817e566ba39104cd78f952fd">Custom UIContentConfiguration &middot; GitHub</a></p> <h2>TVMediaItemContentConfiguration</h2> <p>iOSのUICollectionViewCellには画像付きのconfigurationには画像付きのものはありませんが、tvos の UICollectionViewCellには、TVMediaItemContentConfiguration.wideCell などの、画像つきのセルを表示できるconfigurationがデフォルトで用意されています。</p> <p>tvosでは、画面に表示する要素が限定的なため、このようなConfigurationが用意されていますが、iOSでは画面の表示要素がアプリによって大きく異なるため、システムに画像つきConfigurationが用意されることはなさそうです。</p> <h2>まとめ</h2> <p>UIContentConfigurationを使うとUICollectionViewCellを独自に用意しなくてもUICollectionViewが使えます。 ContentViewやConfigurationを作る必要があるので、手間としてはそれほど変わらないと思う方もいるかもしれませんが、ContentViewやConfigurationは、Modelから独立したレイアウト要素として分離して定義できるので、全体の設計がかなりすっきりしてくると思います。</p> <p>なお、CollectionViewには、ここであげたUIContentConfiguration以外にも便利な機能が増えています。まだキャッチアップできていない感じなら、こちらのビデオをみるとよいでしょう。 CollectionViewの今までの歴史から始めて、ざっと新しい機能について説明してくれます。 詳しいことはこっちのビデオをみるといいよ!というリンクで他の動画に誘導してくれるので、概要を理解することができます。 <a href="https://developer.apple.com/videos/play/wwdc2020/10097/">Advances in UICollectionView - WWDC20 - Videos - Apple Developer</a></p> <p>UIContentConfigurationが含まれた詳細なサンプルコードはこちら。 Implementing Modern Collection Views <a href="https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views">https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views</a></p> toyship UIView で Property Wrapper を導入する hatenablog://entry/13574176438044376540 2021-12-20T09:15:38+09:00 2021-12-20T09:15:38+09:00 UIViewでもこっそり Property Wrapperが使えるようになっていました。 (なお、iOS15からです。) UIViewの表示の更新 今まで、UIViewの見た目を変更するには、View自体を作り直すか、Viewの更新処理を手動でよんだり(setNeedsDisplay)していました。 iOS15から導入された UIViewInvalidating を使えば、値を更新するだけで表示も更新されるようになります。 指定したい変数に下記のように@Invalidatingをつけ、必要なInvalidationTypeを設定するだけなので簡単ですね。 @Invalidating(wrapp… <p>UIViewでもこっそり Property Wrapperが使えるようになっていました。 (なお、iOS15からです。)</p> <h3>UIViewの表示の更新</h3> <p>今まで、UIViewの見た目を変更するには、View自体を作り直すか、Viewの更新処理を手動でよんだり(<code>setNeedsDisplay</code>)していました。</p> <p>iOS15から導入された <code>UIViewInvalidating</code> を使えば、値を更新するだけで表示も更新されるようになります。</p> <p>指定したい変数に下記のように<code>@Invalidating</code>をつけ、必要な<code>InvalidationType</code>を設定するだけなので簡単ですね。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synType">@Invalidating</span><span class="synSpecial">(</span><span class="synType">wrappedValue: &quot;Hello&quot;</span>,<span class="synType"> .display</span><span class="synSpecial">)</span> <span class="synPreProc">var</span> <span class="synIdentifier">title</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synType">@Invalidating</span><span class="synSpecial">(</span><span class="synType">wrappedValue: CGPoint.zero</span>,<span class="synType"> .display</span>,<span class="synType"> .layout</span><span class="synSpecial">)</span> <span class="synPreProc">var</span> <span class="synIdentifier">myPosition</span><span class="synSpecial">:</span> <span class="synType">CGPoint</span> </pre> <p>上記の場合、<code>title</code>を更新すると、<code>setNeedsDisplay</code>、<code>myPosition</code>を更新すると<code>setNeedsDisplay</code>、<code>setNeedsLayout</code>がよばれます。</p> <h3>実例</h3> <p>簡単なUIViewで見てみましょう。</p> <p>まずは、今までの方法を使っているUIView。<code>MyOldView</code>というクラス名にしますね。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synIdentifier">MyOldView</span><span class="synSpecial">:</span> <span class="synType">UIView</span>{ <span class="synPreProc">var</span> <span class="synIdentifier">lineColor</span><span class="synSpecial">:</span> <span class="synType">UIColor</span> <span class="synIdentifier">=</span> UIColor.red <span class="synPreProc">var</span> <span class="synIdentifier">titlePosition</span><span class="synSpecial">:</span> <span class="synType">CGRect</span> <span class="synIdentifier">=</span> CGRect(x<span class="synSpecial">:</span> <span class="synConstant">0</span>, y<span class="synSpecial">:</span> <span class="synConstant">0</span>, width<span class="synSpecial">:</span> <span class="synConstant">50</span>, height<span class="synSpecial">:</span> <span class="synConstant">30</span>) <span class="synPreProc">var</span> <span class="synIdentifier">title</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;Hello&quot;</span> <span class="synPreProc">var</span> <span class="synIdentifier">labelTitle</span><span class="synSpecial">:</span> <span class="synType">UILabel?</span> <span class="synIdentifier">=</span> <span class="synConstant">nil</span> <span class="synStatement">override</span> <span class="synIdentifier">init</span>(frame<span class="synSpecial">:</span> <span class="synType">CGRect</span>) { <span class="synIdentifier">super</span>.<span class="synIdentifier">init</span>(frame<span class="synSpecial">:</span> <span class="synType">frame</span>) <span class="synPreProc">let</span> <span class="synIdentifier">aLabelTitle</span> <span class="synIdentifier">=</span> UILabel(frame<span class="synSpecial">:</span> <span class="synType">titlePosition</span>) aLabelTitle.text <span class="synIdentifier">=</span> title addSubview(aLabelTitle) labelTitle <span class="synIdentifier">=</span> aLabelTitle } <span class="synStatement">required</span> <span class="synIdentifier">init</span>?(coder<span class="synSpecial">:</span> <span class="synType">NSCoder</span>) { fatalError(<span class="synConstant">&quot;init(coder:) has not been implemented&quot;</span>) } <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">draw</span>(_ rect<span class="synSpecial">:</span> <span class="synType">CGRect</span>) { print(<span class="synConstant">&quot;called from setNeedsDisplay&quot;</span>) <span class="synIdentifier">super</span>.draw(rect) <span class="synIdentifier">self</span>.layer.borderColor <span class="synIdentifier">=</span> lineColor.cgColor <span class="synIdentifier">self</span>.layer.borderWidth <span class="synIdentifier">=</span> <span class="synConstant">2.0</span> } <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">layoutSubviews</span>() { print(<span class="synConstant">&quot;called from setNeedsLayout&quot;</span>) <span class="synIdentifier">super</span>.layoutSubviews() labelTitle?.frame <span class="synIdentifier">=</span> titlePosition } } </pre> <p>そして、@Invalidatingを導入した UIView。<code>MyNewView</code>としましょう。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synIdentifier">MyNewView</span><span class="synSpecial">:</span> <span class="synType">UIView</span> { <span class="synType">@Invalidating</span><span class="synSpecial">(</span><span class="synType">wrappedValue: UIColor.red</span>,<span class="synType"> .display</span><span class="synSpecial">)</span> <span class="synPreProc">var</span> <span class="synIdentifier">lineColor</span><span class="synSpecial">:</span> <span class="synType">UIColor</span> <span class="synType">@Invalidating</span><span class="synSpecial">(</span><span class="synType">wrappedValue: CGRect</span><span class="synSpecial">(</span><span class="synType">x: 0</span>,<span class="synType"> y: 0</span>,<span class="synType"> width: 50</span>,<span class="synType"> height: 30</span><span class="synSpecial">)</span>, <span class="synType"> .display</span>,<span class="synType"> .layout</span><span class="synSpecial">)</span> <span class="synPreProc">var</span> <span class="synIdentifier">titlePosition</span><span class="synSpecial">:</span> <span class="synType">CGRect</span> <span class="synPreProc">var</span> <span class="synIdentifier">title</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;Hello&quot;</span> <span class="synPreProc">var</span> <span class="synIdentifier">labelTitle</span><span class="synSpecial">:</span> <span class="synType">UILabel?</span> <span class="synIdentifier">=</span> <span class="synConstant">nil</span> <span class="synStatement">override</span> <span class="synIdentifier">init</span>(frame<span class="synSpecial">:</span> <span class="synType">CGRect</span>) { <span class="synIdentifier">super</span>.<span class="synIdentifier">init</span>(frame<span class="synSpecial">:</span> <span class="synType">frame</span>) <span class="synPreProc">let</span> <span class="synIdentifier">aLabelTitle</span> <span class="synIdentifier">=</span> UILabel(frame<span class="synSpecial">:</span> <span class="synType">titlePosition</span>) aLabelTitle.text <span class="synIdentifier">=</span> title addSubview(aLabelTitle) labelTitle <span class="synIdentifier">=</span> aLabelTitle } <span class="synStatement">required</span> <span class="synIdentifier">init</span>?(coder<span class="synSpecial">:</span> <span class="synType">NSCoder</span>) { fatalError(<span class="synConstant">&quot;init(coder:) has not been implemented&quot;</span>) } <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">draw</span>(_ rect<span class="synSpecial">:</span> <span class="synType">CGRect</span>) { print(<span class="synConstant">&quot;called from setNeedsDisplay&quot;</span>) <span class="synIdentifier">super</span>.draw(rect) <span class="synIdentifier">self</span>.layer.borderColor <span class="synIdentifier">=</span> lineColor.cgColor <span class="synIdentifier">self</span>.layer.borderWidth <span class="synIdentifier">=</span> <span class="synConstant">2.0</span> } <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">layoutSubviews</span>() { print(<span class="synConstant">&quot;called from setNeedsLayout&quot;</span>) <span class="synIdentifier">super</span>.layoutSubviews() labelTitle?.frame <span class="synIdentifier">=</span> titlePosition } } </pre> <p>この二つのViewをViewController上に生成してみます。<code>MyNewView</code>を左に、<code>MyOldView</code>を右におきます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synIdentifier">ViewController</span><span class="synSpecial">:</span> <span class="synType">UIViewController</span> { <span class="synPreProc">var</span> <span class="synIdentifier">oldView</span><span class="synSpecial">:</span> <span class="synType">MyOldView?</span> <span class="synIdentifier">=</span> <span class="synConstant">nil</span> <span class="synPreProc">var</span> <span class="synIdentifier">newView</span><span class="synSpecial">:</span> <span class="synType">MyNewView?</span> <span class="synIdentifier">=</span> <span class="synConstant">nil</span> <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">viewDidLoad</span>() { <span class="synIdentifier">super</span>.viewDidLoad() <span class="synPreProc">let</span> <span class="synIdentifier">aNewView</span> <span class="synIdentifier">=</span> MyNewView(frame<span class="synSpecial">:</span> <span class="synType">CGRect</span>(x<span class="synSpecial">:</span> <span class="synConstant">50</span>, y<span class="synSpecial">:</span> <span class="synConstant">50</span>, width<span class="synSpecial">:</span> <span class="synConstant">100</span>, height<span class="synSpecial">:</span> <span class="synConstant">100</span>)) view.addSubview(aNewView) newView <span class="synIdentifier">=</span> aNewView <span class="synPreProc">let</span> <span class="synIdentifier">aOldView</span> <span class="synIdentifier">=</span> MyOldView(frame<span class="synSpecial">:</span> <span class="synType">CGRect</span>(x<span class="synSpecial">:</span> <span class="synConstant">300</span>, y<span class="synSpecial">:</span> <span class="synConstant">50</span>, width<span class="synSpecial">:</span> <span class="synConstant">100</span>, height<span class="synSpecial">:</span> <span class="synConstant">100</span>)) view.addSubview(aOldView) oldView <span class="synIdentifier">=</span> aOldView <span class="synComment">// あとボタンなども作っておきます。</span> } } </pre> <table> <thead> <tr> </tr> </thead> <tbody> <tr> <td><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20211220/20211220011546.png" alt="f:id:toyship:20211220011546p:plain:w300" width="842" height="641" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></span></td> </tr> </tbody> </table> <p>そして、二つのViewのプロパティを変えてみましょう。左の<code>MyNewView</code>だけ、表示が更新されました。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synType">@IBAction</span> <span class="synType">func</span> changeValue(_ sender<span class="synSpecial">:</span> <span class="synType">Any</span>) { oldView?.lineColor <span class="synIdentifier">=</span> UIColor.blue newView?.lineColor <span class="synIdentifier">=</span> UIColor.blue oldView?.titlePosition <span class="synIdentifier">=</span> CGRect(x<span class="synSpecial">:</span> <span class="synConstant">50</span>, y<span class="synSpecial">:</span> <span class="synConstant">50</span>, width<span class="synSpecial">:</span> <span class="synConstant">50</span>, height<span class="synSpecial">:</span> <span class="synConstant">50</span>) newView?.titlePosition <span class="synIdentifier">=</span> CGRect(x<span class="synSpecial">:</span> <span class="synConstant">50</span>, y<span class="synSpecial">:</span> <span class="synConstant">50</span>, width<span class="synSpecial">:</span> <span class="synConstant">50</span>, height<span class="synSpecial">:</span> <span class="synConstant">50</span>) } </pre> <table> <thead> <tr> </tr> </thead> <tbody> <tr> <td><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20211220/20211220012228.png" alt="f:id:toyship:20211220012228p:plain:w300" width="842" height="627" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></span></td> </tr> </tbody> </table> <p>両方のViewに対して<code>setNeedsDisplay</code>をよんでみましょう。<code>MyOldView</code>の色も変わりました。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synType">@IBAction</span> <span class="synType">func</span> buttonSetNeedsDisplay(_ sender<span class="synSpecial">:</span> <span class="synType">Any</span>) { oldView?.setNeedsDisplay() newView?.setNeedsDisplay() } </pre> <table> <thead> <tr> </tr> </thead> <tbody> <tr> <td><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20211220/20211220012240.png" alt="f:id:toyship:20211220012240p:plain:w300" width="808" height="611" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></span></td> </tr> </tbody> </table> <p>最後に、両方のViewに対して<code>setNeedsLayout</code>をよんでみましょう。<code>MyOldView</code>のテキストの位置も変わりました。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synType">@IBAction</span> <span class="synType">func</span> buttonSetNeedsLayout(_ sender<span class="synSpecial">:</span> <span class="synType">Any</span>) { oldView?.setNeedsLayout() newView?.setNeedsLayout() } </pre> <table> <thead> <tr> </tr> </thead> <tbody> <tr> <td><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20211220/20211220012251.png" alt="f:id:toyship:20211220012251p:plain:w300" width="767" height="622" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></span></td> </tr> </tbody> </table> <p><code>MyNewView</code>は、<code>setNeedsDisplay</code>も<code>setNeedsLayout</code>も呼ぶ必要がないことがわかりましたね。</p> <h3>InvalidationType</h3> <p>@Invalidatingで指定している<code>.display</code>や<code>.layout</code>はInvalidationTypeです。 <code>.display</code>を指定すると、<code>setNeedsDisplay</code>、<code>.layout</code>を指定すると、<code>setNeedsLayout</code>がよばれます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synType">@Invalidating</span><span class="synSpecial">(</span><span class="synType">wrappedValue: &quot;Hello&quot;</span>,<span class="synType"> .display</span><span class="synSpecial">)</span> <span class="synPreProc">var</span> <span class="synIdentifier">title</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synType">@Invalidating</span><span class="synSpecial">(</span><span class="synType">wrappedValue: CGPoint.zero</span>,<span class="synType"> .display</span>,<span class="synType"> .layout</span><span class="synSpecial">)</span> <span class="synPreProc">var</span> <span class="synIdentifier">myPosition</span><span class="synSpecial">:</span> <span class="synType">CGPoint</span> </pre> <p>UIViewで利用できるInvalidationTypeは、<code>.configuration</code>、<code>.constraints</code>、<code>.display</code>、<code>. intrinsicContentSize</code>、<code>.layout</code>の5個です。</p> <h3>まとめ</h3> <p>ここ2、3年、SwiftUIを主流にしたい意図からだろうと思いますが、UIKitのアップデートはあまり大々的にアナウンスされていませんが、よく見るとこっそり変化していたりします。</p> <p>この機能も、Combineなどと組み合わせたらかなり便利につかえるのではないでしょうか。</p> <p>でも、iOS15からしか使えないんだったら、UIKitではなくSwiftUIを使うよなぁ……という気もしますね。</p> toyship 夜中にjsonのデコードで泣かないために hatenablog://entry/13574176438040087441 2021-12-06T14:43:07+09:00 2021-12-06T14:43:07+09:00 夜中にコーディングしていて、サーバーAPIから取得したjsonデコードに失敗したんだけど、もう疲れていて詳しく調べるのがめんどくさいことってありませんか? そんな時にはDecodingErrorをみてみましょう。 jsonデコードのエラー iOSの標準のjson decoderはJSONDecoderですが、このクラスは、デコードに失敗したときに、詳細な情報を返してくれます。 探しているキー情報がないときにはDecodingError.keyNotFound、データ自体が壊れている場合はDecodingError.dataCorrupted、値の型が違う場合にはDecodingError.ty… <p>夜中にコーディングしていて、サーバーAPIから取得したjsonデコードに失敗したんだけど、もう疲れていて詳しく調べるのがめんどくさいことってありませんか?</p> <p>そんな時にはDecodingErrorをみてみましょう。</p> <h3>jsonデコードのエラー</h3> <p>iOSの標準のjson decoderは<code>JSONDecoder</code>ですが、このクラスは、デコードに失敗したときに、詳細な情報を返してくれます。</p> <p>探しているキー情報がないときには<code>DecodingError.keyNotFound</code>、データ自体が壊れている場合は<code>DecodingError.dataCorrupted</code>、値の型が違う場合には<code>DecodingError.typeMismatch</code>、値がない場合には<code>DecodingError.valueNotFound</code>、などがよくあるエラーですね。</p> <p>エラー情報には、エラーが発生したキーの情報なども含まれているので、一括catchをするのではなく、デバッグログなどに詳細情報を出したりしておくとデバッグしやすいですね。</p> <p>下記のコードは、myURLから取得したデータをMySuperDataにデコードしたときのエラー処理です。</p> <p>Errorから取得したデバッグ情報をStringとし、myErrorPublisherというPublisherで他のクラスに通知しています。 (Publisherの使い方については、<a href="https://www.toyship.org/2020/07/24/195001">Combine &#x6700;&#x521D;&#x306E;&#x4E00;&#x6B69; - Toyship.org</a>をみてください。)</p> <p>エラー情報にurlも含んでおくとデバッグの時に便利です。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synStatement">do</span>{ <span class="synPreProc">let</span> <span class="synIdentifier">myURL</span> <span class="synIdentifier">=</span> URL(string<span class="synSpecial">:</span> <span class="synConstant">&quot;http://www.test.com&quot;</span>)<span class="synIdentifier">!</span> <span class="synPreProc">let</span> (data, _) <span class="synIdentifier">=</span> <span class="synStatement">try</span> await URLSession.shared.data(from<span class="synSpecial">:</span> <span class="synType">myURL</span>) <span class="synPreProc">let</span> <span class="synIdentifier">decoder</span> <span class="synIdentifier">=</span> JSONDecoder() <span class="synPreProc">let</span> <span class="synIdentifier">mySuperData</span> <span class="synIdentifier">=</span> <span class="synStatement">try</span> decoder.decode(MySuperData.<span class="synIdentifier">self</span>, from<span class="synSpecial">:</span> <span class="synType">data</span>) } <span class="synStatement">catch</span> DecodingError.keyNotFound(<span class="synPreProc">let</span> <span class="synIdentifier">key</span>, _) { <span class="synPreProc">let</span> <span class="synIdentifier">errorString</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;url[</span><span class="synSpecial">\(</span>myURL<span class="synSpecial">)</span><span class="synConstant">]に</span><span class="synSpecial">\(</span>key.stringValue<span class="synSpecial">)</span><span class="synConstant"> という名前の要素が見つかりません。&quot;</span> <span class="synPreProc">let</span> <span class="synIdentifier">errorToSend</span> <span class="synIdentifier">=</span> MySetupError.decodeError(errorString) myErrorPublisher.send(completion<span class="synSpecial">:</span> .failure(errorToSend)) } <span class="synStatement">catch</span> DecodingError.dataCorrupted(_) { <span class="synPreProc">let</span> <span class="synIdentifier">errorString</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;url[</span><span class="synSpecial">\(</span>myURL<span class="synSpecial">)</span><span class="synConstant">]から取得したデータがこわれています。&quot;</span> <span class="synPreProc">let</span> <span class="synIdentifier">errorToSend</span> <span class="synIdentifier">=</span> MySetupError.decodeError(errorString) myErrorPublisher.send(completion<span class="synSpecial">:</span> .failure(errorToSend)) } <span class="synStatement">catch</span> DecodingError.typeMismatch(<span class="synPreProc">let</span> <span class="synIdentifier">type</span>, <span class="synPreProc">let</span> <span class="synIdentifier">context</span>) { <span class="synPreProc">var</span> <span class="synIdentifier">pathString</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">for</span> pathInfo <span class="synStatement">in</span> context.codingPath{ <span class="synPreProc">let</span> <span class="synIdentifier">p</span> <span class="synIdentifier">=</span> pathInfo.stringValue pathString <span class="synIdentifier">+=</span> <span class="synConstant">&quot;\\&quot;</span> <span class="synIdentifier">+</span> p } <span class="synPreProc">let</span> <span class="synIdentifier">errorString</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;url[</span><span class="synSpecial">\(</span>myURL<span class="synSpecial">)</span><span class="synConstant">]から取得したデータの [</span><span class="synSpecial">\(</span>pathString<span class="synSpecial">)</span><span class="synConstant">]の要素のタイプが[</span><span class="synSpecial">\(</span>type<span class="synSpecial">)</span><span class="synConstant">]であると予想していたんですが、違いました。&quot;</span> <span class="synPreProc">let</span> <span class="synIdentifier">errorToSend</span> <span class="synIdentifier">=</span> MySetupError.decodeError(errorString) myErrorPublisher.send(completion<span class="synSpecial">:</span> .failure(errorToSend)) } <span class="synStatement">catch</span> DecodingError.valueNotFound(<span class="synPreProc">let</span> <span class="synIdentifier">type</span>, _){ <span class="synPreProc">let</span> <span class="synIdentifier">errorString</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;url[</span><span class="synSpecial">\(</span>myURL<span class="synSpecial">)</span><span class="synConstant">]から取得したデータ[</span><span class="synSpecial">\(</span>type<span class="synSpecial">)</span><span class="synConstant">]の値がNULLでした。&quot;</span> <span class="synPreProc">let</span> <span class="synIdentifier">errorToSend</span> <span class="synIdentifier">=</span> MySetupError.decodeError(errorString) myErrorPublisher.send(completion<span class="synSpecial">:</span> .failure(errorToSend)) } <span class="synStatement">catch</span> <span class="synPreProc">let</span> <span class="synIdentifier">error</span> <span class="synStatement">as</span> <span class="synType">NSError</span>{ <span class="synStatement">if</span> error.domain <span class="synIdentifier">==</span> NSURLErrorDomain{ <span class="synPreProc">let</span> <span class="synIdentifier">errorToSend</span> <span class="synIdentifier">=</span> MySetupError.networkError(error.localizedDescription) myErrorPublisher.send(completion<span class="synSpecial">:</span> .failure(errorToSend)) } <span class="synStatement">else</span>{ <span class="synPreProc">let</span> <span class="synIdentifier">errorToSend</span> <span class="synIdentifier">=</span> MySetupError.anotherError(error.localizedDescription) myErrorPublisher.send(completion<span class="synSpecial">:</span> .failure(errorToSend)) } } <span class="synStatement">catch</span>{ <span class="synPreProc">let</span> <span class="synIdentifier">errorToSend</span> <span class="synIdentifier">=</span> MySetupError.anotherError(error.localizedDescription) myErrorPublisher.send(completion<span class="synSpecial">:</span> .failure(errorToSend)) } </pre> <p>エラーはこんな感じです。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">enum</span> <span class="synIdentifier">MySetupError</span><span class="synSpecial">:</span> <span class="synType">Swift.Error</span> { <span class="synStatement">case</span> decodeError(String) <span class="synStatement">case</span> networkError(String) <span class="synStatement">case</span> anotherError(String) } </pre> toyship Async 、 Task 、そして MainActor hatenablog://entry/13574176438039004171 2021-12-03T10:55:51+09:00 2021-12-05T21:09:51+09:00 async/await使ってみましたか? コードがすっきり書けるようになって、とても便利ですよね。 必要な情報だけチェックしてすぐ動かして確認したい方向けに、ざっくりした情報を書いてみました。 すぐ使えるSwift Concurrency!なので、とりあえず書いてみてください。 ざっくりasync async/awaitは、Swiftに導入された新しい非同期処理です。 多くのAPIがasync/awaitに対応していますが、動作確認で一番手軽なのはURLSessionですね。 今までのコードだとこんな感じですが…… let url = URL(string: "https://www.appl… <p>async/await使ってみましたか? コードがすっきり書けるようになって、とても便利ですよね。</p> <p>必要な情報だけチェックしてすぐ動かして確認したい方向けに、ざっくりした情報を書いてみました。</p> <p>すぐ使えるSwift Concurrency!なので、とりあえず書いてみてください。</p> <h3>ざっくりasync</h3> <p>async/awaitは、Swiftに導入された新しい非同期処理です。</p> <p>多くのAPIがasync/awaitに対応していますが、動作確認で一番手軽なのはURLSessionですね。</p> <p>今までのコードだとこんな感じですが……</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">url</span> <span class="synIdentifier">=</span> URL(string<span class="synSpecial">:</span> <span class="synConstant">&quot;https://www.apple.com/&quot;</span>)<span class="synIdentifier">!</span> URLSession.shared.dataTask(with<span class="synSpecial">:</span> <span class="synType">url</span>, completionHandler<span class="synSpecial">:</span> {data, respose, error <span class="synStatement">in</span> <span class="synComment">// エラー処理</span> <span class="synPreProc">let</span> <span class="synIdentifier">decoder</span> <span class="synIdentifier">=</span> JSONDecoder() <span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">data</span> <span class="synIdentifier">=</span> data{ <span class="synPreProc">let</span> <span class="synIdentifier">receivedData</span> <span class="synIdentifier">=</span> <span class="synStatement">try</span><span class="synIdentifier">!</span> decoder.decode(MyClass.<span class="synIdentifier">self</span>, from<span class="synSpecial">:</span> <span class="synType">data</span>) } <span class="synComment">// 画面処理など</span> }).resume() </pre> <p>async/awaitでかくとこうなります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">url</span> <span class="synIdentifier">=</span> URL(string<span class="synSpecial">:</span> <span class="synConstant">&quot;https://www.apple.com/&quot;</span>)<span class="synIdentifier">!</span> <span class="synPreProc">let</span> (data, response) <span class="synIdentifier">=</span> <span class="synStatement">try</span><span class="synIdentifier">!</span> await URLSession.shared.data(from<span class="synSpecial">:</span> <span class="synType">url</span>) <span class="synPreProc">let</span> <span class="synIdentifier">receivedData</span> <span class="synIdentifier">=</span> <span class="synStatement">try</span><span class="synIdentifier">!</span> decoder.decode(MyClass.<span class="synIdentifier">self</span>, from<span class="synSpecial">:</span> <span class="synType">data</span>) <span class="synComment">// エラー処理</span> <span class="synComment">// 画面処理など</span> </pre> <p>ここでは、URLSessionのdata(from url: URL)がasyncに対応したAPIになっています。</p> <p>asyncなメソッドをよぶときには、awaitを記述します。</p> <p>非常にすっきりと、直感的な記述ができますね。</p> <h3>asyncをよべる場所は限られる</h3> <p>実は、asyncなコードをどこからでもよべるわけではありません。</p> <p>例えば、UIViewControllerのviewDidLoadなどでasyncなメソッドをよぼうとすると、「'async' call in a function that does not support concurrency」とwarningが表示されてコンパイルに失敗します。</p> <p>asyncなコードをよべるのは下記の3箇所だけです。</p> <ul> <li>非同期な関数やメソッドの中</li> <li>mainメソッドや@mainがつけられた場所</li> <li>detachedTaskやunstructured Taskの中</li> </ul> <p>この unstructured TaskとdetachedTaskがどんなものなのかみてみましょう。</p> <h3>unstructured Taskと detached Task</h3> <p>どちらのTaskも、優先度をつけて生成することもできますし、デフォルトの優先度で生成することができます。</p> <ul> <li>unstructured Task</li> </ul> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synComment">// デフォルトの優先度の場合</span> Task{ <span class="synComment">// ここでasyncなメソッドを呼ぶ</span> } <span class="synComment">// 優先度backgroundの場合</span> Task(priority<span class="synSpecial">:</span> .background){ <span class="synComment">// ここでasyncなメソッドを呼ぶ</span> } </pre> <ul> <li>detached Task</li> </ul> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synComment">// デフォルトの優先度の場合</span> Task.detached(){ <span class="synComment">// ここでasyncなメソッドを呼ぶ</span> } <span class="synComment">// 優先度userInitiatedの場合</span> Task.detached(priority<span class="synSpecial">:</span> .userInitiated) { <span class="synComment">// ここでasyncなメソッドを呼ぶ</span> } </pre> <p>Taskの優先度は .high、.middle、.backgroundなどありますが、処理の優先度に応じて選びましょう。</p> <h3>unstructured Taskと detached Taskの違い。</h3> <p>さて、この二つの種類のTaskは何が違うのでしょうか。</p> <p>unstructured Taskは実行コンテキストを引き継ぎますが、detached Taskは実行コンテキストを引き継ぎません。</p> <p>わかりやすくいうと、この二つのTaskは開始時の実行スレッドが違います。</p> <p>unstructured Taskは、そのTaskがよばれた場所の実行スレッドをそのまま引き継ぎ、detached Taskは、引き継ぎません。</p> <p>unstructured Taskは、メインスレッドからよばれればメインスレッドで実行され、バックグラウンドスレッドでよばれればバックグラウンドスレッドで実行されます。 それに対して、detached Taskはどのスレッドからよばれても、<s>バックグラウンドスレッド</s>よばれたスレッド以外のスレッドで実行されます。</p> <p>例えばViewControllerのviewDidLoadでTaskをよんだとき、どんなスレッドでよばれるかをみてみましょう。</p> <p>(Thread.isMainThreadを使うと、その行がメインスレッドで実行されているかどうかがわかります。)</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">class</span> <span class="synIdentifier">ViewController</span><span class="synSpecial">:</span> <span class="synType">UIViewController</span> { <span class="synIdentifier">...</span> <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">viewDidLoad</span>() { <span class="synIdentifier">super</span>.viewDidLoad() print(<span class="synConstant">&quot;isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) <span class="synComment">// これはtrueになります。</span> Task{ print(<span class="synConstant">&quot;isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) <span class="synComment">// これはtrueになります。</span> } Task(priority<span class="synSpecial">:</span> .background){ print(<span class="synConstant">&quot;isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) <span class="synComment">// これはtrueになります。</span> } Task.detached(){ print(<span class="synConstant">&quot;isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) <span class="synComment">// これはfalseになります。</span> } Task.detached(priority<span class="synSpecial">:</span> .userInitiated) { print(<span class="synConstant">&quot;isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) <span class="synComment">// これはfalseになります。</span> } } } </pre> <p>UIViewControllerのviewDidLoadはメインスレッドでよばれるので、そこで作られたunstructured Taskはメインスレッドでよばれ、detached Taskはメインスレッドではよばれないということです。</p> <p>こんな感じでunstructured Taskとdetached Taskを組み合わせるとどうなるでしょうか。(自分で確認してみましょう。)</p> <pre class="code lang-swift" data-lang="swift" data-unlink> Task(priority<span class="synSpecial">:</span> .background){ Task.detached(priority<span class="synSpecial">:</span> .userInitiated){ print(<span class="synConstant">&quot;isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) } } Task.detached(priority<span class="synSpecial">:</span> .userInitiated){ Task(priority<span class="synSpecial">:</span> .background){ print(<span class="synConstant">&quot;isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) } } </pre> <p>(2021/12/5 追記)なお、Task{}のなかがずっと同じスレッドで実行されるわけではなく、awaitをはさんだタイミングで実行スレッドがかわる場合もあります。</p> <h3>メインスレッドですべき処理、するべきではない処理。</h3> <p>さて、ここで初心者の方むけに、メインスレッドですべき処理、するべきではない処理を復習しましょう。</p> <p>基本的に、画面描画に関わる処理はメインスレッドで行わなければいけません。画面に表示する文字列を変更したり、新しい画面を開いたり、画面に表示された画像を変更したり……あらゆる処理はメインスレッドで行いましょう。</p> <p>また、時間のかかる処理をメインスレッドで行ってしまうと、画面の動きがもたついたり、ユーザーに対するレスポンスが遅くなってしまうので、バックグラウンドのスレッドで行う必要があります。</p> <p>たとえば、URLSessionで取得したjsonデータをdecodeし、なんらかの画面表示を行う場合には、</p> <ul> <li>Networkよびだし -> バックグラウンドのスレッドで行う</li> <li>エラー処理(画面表示には関係ない部分)-> バックグラウンドのスレッドで行う</li> <li>エラー処理(エラー画面表示など、画面表示をする部分)-> メインスレッドで行う</li> <li>json decode-> バックグラウンドのスレッドで行う</li> <li>json decode の結果を画面に表示する-> メインスレッドで行う</li> <li>json decode の結果から、画面表示とは関係のない処理を行う-> バックグラウンドのスレッドで行う</li> </ul> <p>が望ましいです。</p> <p>ただ、あまり細かく実行スレッドを指定すると、スレッド切り替えの際にバグが発生してしまうので注意が必要です。</p> <h3>MainActor</h3> <p>そこで登場するのがMainActorです。</p> <p>iOS13から登場したActorを使うと、どこからよばれてもメインスレッドで実行するように指定することができます。</p> <p>指定方法はいろいろありますが、ここでは簡単にクラス単位の指定とメソッド単位の指定をみてみましょう。</p> <p>クラス単位で@MainActorをつけておくと、そのクラスのすべてのメソッドがメインスレッドでよばれるようになります。 (asyncなメソッドは別です)</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synType">@MainActor</span> <span class="synPreProc">class</span> <span class="synIdentifier">IsMainActor</span>{ <span class="synPreProc">func</span> <span class="synIdentifier">hello</span>(){ print(<span class="synConstant">&quot;Thread.isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) <span class="synComment">// どこからよばれてもtrue</span> } } </pre> <p>また、クラスの一部の処理だけメインスレッドで実行したい場合には、メソッドに@MainActorをつけると対応できます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synIdentifier">IsNotMainActor</span>{ <span class="synPreProc">func</span> <span class="synIdentifier">hello</span>(){ print(<span class="synConstant">&quot;Thread.isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) <span class="synComment">// こちらはよばれた場所によります。</span> } <span class="synType">@MainActor</span> <span class="synType">func</span> helloMain(){ print(<span class="synConstant">&quot;Thread.isMainThread:</span><span class="synSpecial">\(</span>Thread.isMainThread<span class="synSpecial">)</span><span class="synConstant">&quot;</span>) <span class="synComment">// どこからよばれてもtrue</span> } } </pre> <p>ちなみに、UIViewControllerは@MainActorがついているので、普通のメソッド(async以外)は常にメインスレッドでよばれます。 UIViewControllerのメソッドには@MainActorをつけなくても、常にメインスレッドでよばれます。</p> <p>ですので、UI更新系の処理には必ず@MainActorをつけておくことで、安全なコードを書けますね。</p> <h3>まとめ</h3> <p>こんな感じで書くと、ネットワーク処理やjson decodeはバックグラウンドスレッドでよばれ、UIを含む処理はメインスレッドでよばれるようになります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">class</span> <span class="synIdentifier">ViewController</span><span class="synSpecial">:</span> <span class="synType">UIViewController</span> { <span class="synIdentifier">...</span> <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">viewDidLoad</span>() { <span class="synIdentifier">super</span>.viewDidLoad() Task.detached(){ <span class="synPreProc">let</span> <span class="synIdentifier">url</span> <span class="synIdentifier">=</span> URL(string<span class="synSpecial">:</span> <span class="synConstant">&quot;http://www.mysite.com/&quot;</span>)<span class="synIdentifier">!</span> <span class="synStatement">do</span>{ <span class="synPreProc">let</span> (data, response) <span class="synIdentifier">=</span> <span class="synStatement">try</span> await URLSession.shared.data(from<span class="synSpecial">:</span> <span class="synType">url</span>) <span class="synPreProc">let</span> <span class="synIdentifier">decoder</span> <span class="synIdentifier">=</span> JSONDecoder() <span class="synPreProc">let</span> <span class="synIdentifier">decodedData</span> <span class="synIdentifier">=</span> <span class="synStatement">try</span> decoder.decode(MySuperClass.<span class="synIdentifier">self</span>, from<span class="synSpecial">:</span> <span class="synType">data</span>) <span class="synComment">// UI変更を含む処理(メインスレッドで実行される)</span> await IsMainActor().hello() await IsNotMainActor().helloMain() <span class="synComment">// UI変更を含まない処理(メインスレッド以外で実行される)</span> await IsNotMainActor().hello() } <span class="synStatement">catch</span>{ await <span class="synIdentifier">self</span>.showError() } } } } </pre> <p>ただ、Appleのサンプルなどをみていても、上記のようなコードでTask.detached(){}ではなくてTask{}を使っていたりもするので、それほど使い分けに注意しなくてもいいかもしれないですね。</p> <p>(長くなるので端折りますが、サスペンションポイントがあるのでDispatchQueue.mainより安全ですし……。)</p> <p>(2021/12/5 追記) また、この記事をかいたあとに、<a href="https://twitter.com/tokorom">tokorom</a>さんと話して、AppleのConcurrency関連の資料を見ていると、Task.datached{} より Task{} のほうが推奨されているような印象があるよね……という結論になったので、上ではこう書きましたが、個人的にはTask{}を使おうかなと思います。</p> toyship URLSession で http header 情報を確認する hatenablog://entry/13574176438035669036 2021-11-23T12:00:52+09:00 2021-11-23T12:00:52+09:00 たまに、APIの結果を処理するときに、MIME Typeを見てから確認したいってことはありませんか? 基本的にはjsonが返るAPIだけど、エラー時にはhtmlになるのでjsonかhtmlか判別してから処理をしたいとか。 そんなときにはURLResponseを見てみましょう。 API Headerをみる まずは、GithubのAPIでheaderを見てみましょう。 $ curl -i https://api.github.com/users/TachibanaKaoru header情報はこんな感じです。 HTTP/2 200 server: GitHub.com date: Tue, 23 … <p>たまに、APIの結果を処理するときに、MIME Typeを見てから確認したいってことはありませんか? 基本的にはjsonが返るAPIだけど、エラー時にはhtmlになるのでjsonかhtmlか判別してから処理をしたいとか。</p> <p>そんなときにはURLResponseを見てみましょう。</p> <h2>API Headerをみる</h2> <p>まずは、GithubのAPIでheaderを見てみましょう。</p> <pre class="code" data-lang="" data-unlink>$ curl -i https://api.github.com/users/TachibanaKaoru</pre> <p>header情報はこんな感じです。</p> <pre class="code" data-lang="" data-unlink>HTTP/2 200 server: GitHub.com date: Tue, 23 Nov 2021 01:14:34 GMT content-type: application/json; charset=utf-8 cache-control: public, max-age=60, s-maxage=60 x-frame-options: deny x-content-type-options: nosniff x-ratelimit-limit: 60 x-ratelimit-remaining: 57 x-ratelimit-reset: 1637633003 x-ratelimit-resource: core x-ratelimit-used: 3 accept-ranges: bytes content-length: 1420 x-github-request-id: F181:4C52:2DB5F1D:2FC7BBC:619C4079 { &#34;login&#34;: &#34;TachibanaKaoru&#34;, &#34;id&#34;: 225811, &#34;node_id&#34;: &#34;MwQ6VsdlcjIy3regxMQ==&#34;, &#34;avatar_url&#34;: &#34;https://avatars.githubusercontent.com/u/225811?v=4&#34;, &#34;gravatar_id&#34;: &#34;&#34;, &#34;url&#34;: &#34;https://api.github.com/users/TachibanaKaoru&#34;, ...</pre> <p>これをURLSessionでよんで、header情報を見てみましょう。</p> <p>取得できるresponseからMIME Typeが取得できます。</p> <p><code>HTTPURLResponse</code>にキャストして、<code>response.allHeaderFields</code> でheader情報を取得できます。</p> <p>当然 http status codeなどもとれるので、サーバーの状態に応じて細かい処理を行うこともできますね。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synStatement">guard</span> <span class="synPreProc">let</span> <span class="synIdentifier">url</span> <span class="synIdentifier">=</span> URL(string<span class="synSpecial">:</span> <span class="synConstant">&quot;https://api.github.com/users/TachibanaKaoru&quot;</span>) <span class="synStatement">else</span> { <span class="synStatement">return</span> } URLSession.shared.dataTask(with<span class="synSpecial">:</span> <span class="synType">url</span>, completionHandler<span class="synSpecial">:</span> { data, response, error <span class="synStatement">in</span> <span class="synPreProc">let</span> <span class="synIdentifier">mimeType</span> <span class="synIdentifier">=</span> response?.mimeType <span class="synComment">//&quot;application/json&quot; が返ります</span> <span class="synPreProc">let</span> <span class="synIdentifier">textEncodingName</span> <span class="synIdentifier">=</span> response?.textEncodingName <span class="synComment">//&quot;utf-8&quot; が返ります</span> <span class="synComment">//その他いろいろ。</span> <span class="synPreProc">let</span> <span class="synIdentifier">suggestedFilename</span> <span class="synIdentifier">=</span> response?.suggestedFilename <span class="synPreProc">let</span> <span class="synIdentifier">returnedURL</span> <span class="synIdentifier">=</span> response?.url <span class="synPreProc">let</span> <span class="synIdentifier">expectedContentLength</span> <span class="synIdentifier">=</span> response?.expectedContentLength <span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">response</span> <span class="synIdentifier">=</span> response <span class="synStatement">as?</span> <span class="synType">HTTPURLResponse</span> { <span class="synComment">// statusCodeをみてみる。</span> <span class="synPreProc">let</span> <span class="synIdentifier">statusCode</span> <span class="synIdentifier">=</span> response.statusCode <span class="synComment">// statusCodeの説明文を取得する。</span> <span class="synComment">// (localizedStringですが、表示されるのは英語のみ)</span> <span class="synPreProc">let</span> <span class="synIdentifier">statusCodeString</span> <span class="synIdentifier">=</span> HTTPURLResponse.localizedString(forStatusCode<span class="synSpecial">:</span> <span class="synType">statusCode</span>) <span class="synComment">// すべてのヘッダー</span> <span class="synPreProc">let</span> <span class="synIdentifier">headers</span> <span class="synIdentifier">=</span> response.allHeaderFields <span class="synComment">// 特定のヘッダー情報を見てみる</span> <span class="synPreProc">let</span> <span class="synIdentifier">xLimit</span><span class="synSpecial">:</span> <span class="synType">Any?</span> <span class="synIdentifier">=</span> response.value(forHTTPHeaderField<span class="synSpecial">:</span> <span class="synConstant">&quot;x-ratelimit-limit&quot;</span>) <span class="synPreProc">let</span> <span class="synIdentifier">xRemain</span><span class="synSpecial">:</span> <span class="synType">Any?</span> <span class="synIdentifier">=</span> response.value(forHTTPHeaderField<span class="synSpecial">:</span> <span class="synConstant">&quot;x-ratelimit-remaining&quot;</span>) } <span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">data</span> <span class="synIdentifier">=</span> data{ <span class="synComment">// mimeTypeに応じた処理を行う</span> } }).resume() </pre> toyship UnManaged な CFString hatenablog://entry/26006613802114487 2021-08-28T14:46:59+09:00 2021-08-28T14:46:59+09:00 いつも自分で書くコードはすっかりSwiftですが、システムライブラリはまだまだObjective-Cのこともあります。 ObjCのライブラリが返してくれる CFString をSwiftの String にするメモ。 基本的には、CFString はそのままConvertできます。 var str1: CFString = "Hello!" as CFString var str2: String = str1 as String ただ、Unmanaged でかえされた場合には takeRetainedValue を使いましょう。 var propertyStr: Unmanaged<CFSt… <p>いつも自分で書くコードはすっかりSwiftですが、システムライブラリはまだまだObjective-Cのこともあります。</p> <p>ObjCのライブラリが返してくれる <code>CFString</code> をSwiftの <code>String</code> にするメモ。</p> <p>基本的には、<code>CFString</code> はそのままConvertできます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">var</span> <span class="synIdentifier">str1</span><span class="synSpecial">:</span> <span class="synType">CFString</span> <span class="synIdentifier">=</span> <span class="synConstant">&quot;Hello!&quot;</span> <span class="synStatement">as</span> CFString <span class="synPreProc">var</span> <span class="synIdentifier">str2</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> str1 <span class="synStatement">as</span> String </pre> <p>ただ、<code>Unmanaged</code> でかえされた場合には <code>takeRetainedValue</code> を使いましょう。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">var</span> <span class="synIdentifier">propertyStr</span><span class="synSpecial">:</span> <span class="synType">Unmanaged</span><span class="synSpecial">&lt;CFString&gt;</span>? <span class="synComment">//これだとダメ</span> <span class="synPreProc">let</span> <span class="synIdentifier">deviceName1</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> propertyStr <span class="synStatement">as</span>? String <span class="synStatement">??</span> <span class="synConstant">&quot;(cannot convert...)&quot;</span> <span class="synComment">//これならOK</span> <span class="synPreProc">let</span> <span class="synIdentifier">deviceName2</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synIdentifier">=</span> propertyStr?.takeRetainedValue() <span class="synStatement">as</span>? String <span class="synStatement">??</span> <span class="synConstant">&quot;(cannot convert...)&quot;</span> </pre> toyship PlaygroundSupportでAR hatenablog://entry/26006613790293036 2021-07-25T00:44:35+09:00 2021-07-25T10:40:21+09:00 前回のこちらの記事でSceneKitのビューをPlaygroundで表示する方法について書きましたが、Playgroundは通常のView表示だけでなく、ARもサポートしています。 PlaygroundSupportでSceneKitの画面を表示する - Toyship.org iPadだけでARができるのはハードルも低くなり、初心者にARを学んでもらうための教材としてもいいですね。 ARのViewの表示 基本的には前回の記事と同じく、PlaygroundSupportをインポートして、AR用のビューを作り、それをPlaygroundPage.current.liveViewにセットするだけで… <p>前回のこちらの記事でSceneKitのビューをPlaygroundで表示する方法について書きましたが、Playgroundは通常のView表示だけでなく、ARもサポートしています。</p> <p><a href="https://www.toyship.org/2021/07/24/234832">PlaygroundSupport&#x3067;SceneKit&#x306E;&#x753B;&#x9762;&#x3092;&#x8868;&#x793A;&#x3059;&#x308B; - Toyship.org</a></p> <p>iPadだけでARができるのはハードルも低くなり、初心者にARを学んでもらうための教材としてもいいですね。</p> <h2>ARのViewの表示</h2> <p>基本的には前回の記事と同じく、<code>PlaygroundSupport</code>をインポートして、AR用のビューを作り、それを<code>PlaygroundPage.current.liveView</code>にセットするだけです。</p> <p>ただし、ARViewなので、ARのセッションを開始する必要があります。 ここでは、一番シンプルな<code>ARWorldTrackingConfiguration</code>でAR sessionを開始していますが、必要に応じて変更してみてください。</p> <p>ここでは、SceneKitを使ったARSCNViewを使っていますが、おそらくRealityKitを使ったビューでも大丈夫だと思います。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> PlaygroundSupport <span class="synPreProc">var</span> <span class="synIdentifier">arView</span> <span class="synIdentifier">=</span> ARSCNView(frame<span class="synSpecial">:</span> <span class="synType">CGRect</span>(x<span class="synSpecial">:</span> <span class="synConstant">0</span>, y<span class="synSpecial">:</span> <span class="synConstant">0</span>, width<span class="synSpecial">:</span> <span class="synConstant">300</span>, height<span class="synSpecial">:</span> <span class="synConstant">300</span>)) arView.autoenablesDefaultLighting <span class="synIdentifier">=</span> <span class="synConstant">true</span> <span class="synPreProc">let</span> <span class="synIdentifier">scene</span> <span class="synIdentifier">=</span> SCNScene() arView.scene <span class="synIdentifier">=</span> scene <span class="synPreProc">let</span> <span class="synIdentifier">configuration</span> <span class="synIdentifier">=</span> ARWorldTrackingConfiguration() arView.session.run(configuration) PlaygroundPage.current.liveView <span class="synIdentifier">=</span> arView </pre> <p>iPadで実行するとこんな感じです。 iPadだけでARの実行環境を用意することができます。 (なお、Playgroundを実行するときにカメラへのアクセス許可ダイアログが表示されます。)</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20210725/20210725004041.png" alt="f:id:toyship:20210725004041p:plain" width="1200" height="838" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>全コード</h2> <p>全部のコードはこちら。 (iPadのPlaygroundアプリにそのままコピペすれば使えます。)</p> <p><script src="https://gist.github.com/5b0c9e6e4538476f579d21abdc98503a.js"> </script></p> <p><a href="https://gist.github.com/5b0c9e6e4538476f579d21abdc98503a">gist5b0c9e6e4538476f579d21abdc98503a</a></p> toyship PlaygroundSupportでSceneKitの画面を表示する hatenablog://entry/26006613790278500 2021-07-24T23:48:32+09:00 2021-07-25T10:40:10+09:00 みなさん、Xcodeのプレビュー機能は使いこなしていますか? 私の環境では、プレビュー画面が表示されないことが時々あります。 作業しながらSafariのタブを数百ページくらい開いていることもあるので、メモリー不足だとはおもうんですが。 Xcodeを再起動したら治るのかもしれないけど、治らないかもしれないし。まあ、だいたいの場合は直らないんですよね……。 ということで、せっかくiPadもあるので、iPadのPlaygroundでプレビューを表示されてみましょう。それならMacでいくらメモリーを使っていても影響されないですから。 PlaygroundSupportとは PlaygroundSupp… <p>みなさん、Xcodeのプレビュー機能は使いこなしていますか? 私の環境では、プレビュー画面が表示されないことが時々あります。</p> <p>作業しながらSafariのタブを数百ページくらい開いていることもあるので、メモリー不足だとはおもうんですが。</p> <p>Xcodeを再起動したら治るのかもしれないけど、治らないかもしれないし。まあ、だいたいの場合は直らないんですよね……。</p> <p>ということで、せっかくiPadもあるので、iPadのPlaygroundでプレビューを表示されてみましょう。それならMacでいくらメモリーを使っていても影響されないですから。</p> <h2>PlaygroundSupportとは</h2> <p>PlaygroundSupportは、Playgroundアプリ単体などで画面を表示することができるようにするサポートライブラリです。</p> <p>UIViewやNSViewはもちろん、UIViewControllerなどもそのまま表示できるようになります。</p> <p>ここでは、負荷が重そうなSceneKitのSCNViewを表示してみることにしましょう。</p> <h2>SCNViewの表示</h2> <p>まずは、<code>PlaygroundSupport</code>を使うので、importをしておきましょう。</p> <p>それから、表示したいView(ここではSCNView)を作って、それを<code>PlaygroundPage.current.liveView</code>に設定するだけ。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> PlaygroundSupport <span class="synComment">// 表示用のSCNViewを作る。</span> <span class="synPreProc">var</span> <span class="synIdentifier">sceneView</span> <span class="synIdentifier">=</span> SCNView(frame<span class="synSpecial">:</span> <span class="synType">CGRect</span>(x<span class="synSpecial">:</span> <span class="synConstant">0</span>, y<span class="synSpecial">:</span> <span class="synConstant">0</span>, width<span class="synSpecial">:</span> <span class="synConstant">300</span>, height<span class="synSpecial">:</span> <span class="synConstant">300</span>)) <span class="synPreProc">var</span> <span class="synIdentifier">scene</span> <span class="synIdentifier">=</span> SCNScene() sceneView.scene <span class="synIdentifier">=</span> scene <span class="synComment">// PlaygroundPageに作ったSCNViewを設定。</span> PlaygroundPage.current.liveView <span class="synIdentifier">=</span> sceneView PlaygroundPage.current.needsIndefiniteExecution <span class="synIdentifier">=</span> <span class="synConstant">true</span> <span class="synComment">// あとは適当に画面のパーツを作ってsceneに追加してください。</span> </pre> <p>できたPlaygroundファイルを iCloud Drive の Playgroundsフォルダーにコピーすれば、iPadのPlaygroundアプリから開いて動作確認ができます。</p> <p><figure class="figure-image figure-image-fotolife" title="PlaygroundSupport"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20210724/20210724234230.png" alt="f:id:toyship:20210724234230p:plain" width="1200" height="838" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>PlaygroundSupport</figcaption></figure></p> <p>これでMacでどんな重い処理が走っていてもプレビューできますね。</p> <h2>全コード</h2> <p>全部のソースコードはこちら。 (iPadのPlaygroundアプリにそのままコピペすれば使えます。)</p> <script src="https://gist.github.com/TachibanaKaoru/9f3c2824f0ae250f730ae27f8b8243dd.js"></script> toyship 君はAppClipを見たか hatenablog://entry/26006613639127197 2020-10-11T00:16:25+09:00 2020-10-12T12:10:11+09:00 iOS14の新機能、AppClip。なんかあのにゅっとでてくるやつです。 対応しているアプリをあまり見かけないので実装してみました。 是非、実際の端末で動かしてみてください。 実装 どうやって実装するのかの情報はWWDCでもでていましたが、どんなタイミングでどう動くのかは、実際に動くアプリで試してみたいですよね。 動作確認用に使っている個人アプリで試してみました。 AppClip 起動トリガー WWDCで説明されていた起動トリガーはこの7種類。 URL Smart App Banner (App Clip用) (普通の)QRコード (Apple独自の)QRコード NFC Map 場所 まずはU… <p>iOS14の新機能、AppClip。なんかあのにゅっとでてくるやつです。 対応しているアプリをあまり見かけないので実装してみました。 是非、実際の端末で動かしてみてください。</p> <h2>実装</h2> <p>どうやって実装するのかの情報はWWDCでもでていましたが、どんなタイミングでどう動くのかは、実際に動くアプリで試してみたいですよね。</p> <p>動作確認用に使っている個人アプリで試してみました。 <figure class="figure-image figure-image-fotolife" title="AppClip"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20201010/20201010232123.jpg" alt="f:id:toyship:20201010232123j:plain" title="f:id:toyship:20201010232123j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>AppClip</figcaption></figure></p> <h2>起動トリガー</h2> <p>WWDCで説明されていた起動トリガーはこの7種類。</p> <ul> <li>URL</li> <li>Smart App Banner (App Clip用)</li> <li>(普通の)QRコード</li> <li>(Apple独自の)QRコード</li> <li>NFC</li> <li>Map</li> <li>場所</li> </ul> <p>まずは<code>URL</code>から……といいたいところなのですが、実は私の手元では、AppClipが(安定して)起動していないんですよね。 ちょっとほかの起動トリガーから見てみましょう。</p> <p><code>Smart App Banner</code>。 下のURLをひらいて、ページの右上の「開く」をクリックしてください。下からにゅっとAppClipがでてきますね。</p> <ul> <li><a href="https://tachibanakaoru.github.io/toyjigsaw/">https://tachibanakaoru.github.io/toyjigsaw/</a></li> </ul> <p><figure class="figure-image figure-image-fotolife" title="Smart App Banner"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20201010/20201010234952.png" alt="f:id:toyship:20201010234952p:plain" title="f:id:toyship:20201010234952p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>Smart App Banner</figcaption></figure></p> <p>次は<code>普通のQRコード</code>。 こちらのコードをカメラで読み込むと、画面上に「開く」メニューが表示され、そちらからAppClipカードが表示できます。</p> <p><figure class="figure-image figure-image-fotolife" title="QRコード"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20201010/20201010235436.png" alt="f:id:toyship:20201010235436p:plain" title="f:id:toyship:20201010235436p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>QRコードその1</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="その2"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20201012/20201012114802.png" alt="f:id:toyship:20201012114802p:plain" title="f:id:toyship:20201012114802p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>QRコードその2</figcaption></figure></p> <p><code>Apple独自のQRコード</code>については、今年後半にリリースされるということなので、まだ確認ができません。</p> <p>そして、<code>NFC</code>。サンワサプライのNFCタグにこちら↓のURLを書き込んで、iPhoneを上にのせてみたところ、AppClipがちゃんと表示されました。(ただし、iOS14.2では表示されるんですが、iOS14.0.1では表示されないようです。)</p> <ul> <li><a href="https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/1">https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/1</a></li> </ul> <p>あとは、<code>Map</code>と<code>場所のトリガー</code>ですが、こちらについてはまだ確認中。(実装してみたんですが、うまく動かないので、動いたらまた記事を書きます。)</p> <h2>Default App ClipとAdvanced App Clip</h2> <p>WWDCでも触れられていましたが、App Clipには2種類あります。</p> <p><code>Default App Clip</code>と<code>Advanced App Clip</code>です。 この2種類の使い分けは、あまり大きく触れられていませんでした。</p> <p><figure class="figure-image figure-image-fotolife" title="Default App Clip and Advanced App Clip"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20201010/20201010233403.png" alt="f:id:toyship:20201010233403p:plain" title="f:id:toyship:20201010233403p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>Default App Clip and Advanced App Clip</figcaption></figure></p> <p><code>Default App Clip</code>と<code>Advanced App Clip</code>は、見た目は全く同じです。 App Clipを実装する場合、<code>Default App Clip</code>は必須ですが、<code>Advanced App Clip</code>はオプショナルです。</p> <p><code>Default App Clip</code>は、一つだけですが、<code>Advanced App Clip</code>はいくつでも実装できます。 基本的には、AppClipのルート画面を表示するのが<code>Default App Clip</code>、条件に応じた画面を表示するのが<code>Advanced App Clip</code>です。</p> <p>(<code>Default App Clip</code>も<code>Advanced App Clip</code>も固有のURLを持ち、実装面からは違いはありません。)</p> <p>今回の実装では、このURLを<code>Default App Clip</code>として実装し、ここから開いた時にはAppClipのルート画面を見せるようにしています。</p> <ul> <li><a href="https://tachibanakaoru.github.io/toyjigsaw/">https://tachibanakaoru.github.io/toyjigsaw/</a></li> </ul> <p>そして、<code>Advanced App Clip</code>としてはこのURLを実装し、個別のパズル画面が表示されるようになっています。</p> <ul> <li><a href="https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/1">https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/1</a></li> <li><a href="https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/2">https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/2</a></li> <li><a href="https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/3">https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/3</a></li> <li><a href="https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/4">https://tachibanakaoru.github.io/toyjigsaw/puzzle4x4/4</a></li> </ul> <p>また、起動トリガーによって、使えるApp Clipの種類が違います。</p> <p><code>Smart App Banner</code>は<code>Default App Clip</code>だけ、<code>QRコード</code>と<code>NFC</code>は<code>Advanced App Clip</code>だけでしか使えません。</p> <h2>アプリ本体とAppClipのインストール状態で違う動作</h2> <p>App Clipは、アプリ本体のインストール状態、AppClipのインストール状態でトリガーされた時の動作が異なります。</p> <p>下記の3つのインストール状態でみてみましょう。<a href="#f-f981fd46" name="fn-f981fd46" title="iOS14.2 beta2で確認した動作なので、バージョンが異なると違う動作になる可能性があります。">*1</a>(アプリ本体がインストールされるとAppClipは削除されるので、「App本体あし、App Clipあり」の状態はありません。)</p> <ul> <li>App本体なし、App Clipなし</li> <li>App本体なし、App Clipあり</li> <li>App本体あり、App Clipなし</li> </ul> <h3>App本体なし、App Clipなし</h3> <ul> <li>Smart App Banner から起動した場合 → AppClipカードが<code>表示され</code>、そこからApp Clipが起動</li> <li>NFCタグ → AppClipカードが<code>表示され</code>、そこからApp Clipが起動</li> <li>QRコード → AppClipカードが<code>表示され</code>、そこからApp Clipが起動</li> </ul> <h3>App本体なし、AppClipあり</h3> <ul> <li>Smart App Banner から起動した場合 → AppClipカードは<code>表示されず</code>、App Clipが直接起動</li> <li>NFCタグ → AppClipカードが<code>表示され</code>、そこからApp Clipが起動</li> <li>QRコード → AppClipカードが<code>表示され</code>、そこからApp Clipが起動</li> </ul> <h3>App本体あり、AppClipなし</h3> <ul> <li>Smart App Banner から起動した場合 → AppClipカードは<code>表示されず</code>、App本体が直接起動</li> <li>NFCタグ → AppClipカードが<code>表示され</code>、そこからApp本体が起動</li> <li>QRコード → AppClipカードが<code>表示され</code>、そこからApp本体が起動</li> </ul> <h2>開発中のApp Clipのテスト</h2> <p>App Clipの起動は、Test Flightで確認することができます。</p> <p>ただし、Test Flightからの起動ではApp Clipカードは表示されません。(App Clipが直接起動されます。)</p> <p>開発中にApp Clipカードを表示したい場合には、設定画面の <code>Developer</code> の <code>APP CLIPS TESTING</code> にある<code>Local Experiences</code>に画像・テキスト情報を設定すると、App Clipカードが表示されるようになっています。</p> <h2>App Clipの申請</h2> <p>App Clipの情報の申請は、基本的にはアプリの審査時の情報入力と同時に行います。</p> <p>ただ、現時点では、アプリをリリースしたあとでも<code>Advanced App Clip</code>の情報を追加・編集することはできるようになっています。</p> <h2>entitlement</h2> <p>App Clipの実装についてはAppleの公式情報も充実しているので、この記事では触れませんが、ハマりどころの多い設定ファイル系だけ置いておきます。</p> <p>親アプリのBundle Identifierは<code>org.toyship.toyjigsaw</code>、App Clipは<code>org.toyship.toyjigsaw.clip</code>です。</p> <p>親アプリのentitlement fileはこちら。</p> <pre class="code" data-lang="" data-unlink>&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt; &lt;!DOCTYPE plist PUBLIC &#34;-//Apple//DTD PLIST 1.0//EN&#34; &#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;&gt; &lt;plist version=&#34;1.0&#34;&gt; &lt;dict&gt; &lt;key&gt;com.apple.developer.associated-domains&lt;/key&gt; &lt;array&gt; &lt;string&gt;applinks:tachibanakaoru.github.io&lt;/string&gt; &lt;string&gt;appclips:tachibanakaoru.github.io&lt;/string&gt; &lt;/array&gt; &lt;/dict&gt; &lt;/plist&gt; </pre> <p>App Clipのentitlement fileはこちら。</p> <pre class="code" data-lang="" data-unlink>&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt; &lt;!DOCTYPE plist PUBLIC &#34;-//Apple//DTD PLIST 1.0//EN&#34; &#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;&gt; &lt;plist version=&#34;1.0&#34;&gt; &lt;dict&gt; &lt;key&gt;com.apple.developer.associated-domains&lt;/key&gt; &lt;array&gt; &lt;string&gt;appclips:tachibanakaoru.github.io&lt;/string&gt; &lt;string&gt;applinks:tachibanakaoru.github.io&lt;/string&gt; &lt;/array&gt; &lt;key&gt;com.apple.developer.parent-application-identifiers&lt;/key&gt; &lt;array&gt; &lt;string&gt;$(AppIdentifierPrefix)org.toyship.toyjigsaw&lt;/string&gt; &lt;/array&gt; &lt;/dict&gt; &lt;/plist&gt; </pre> <p>apple-app-site-associationはこんな設定としています。</p> <p><a href="https://tachibanakaoru.github.io/apple-app-site-association">https://tachibanakaoru.github.io/apple-app-site-association</a></p> <h2>まとめ</h2> <p>App Clipは、まだApple側も手探りなようで、iOSのバージョンによって微妙に動作が異なったり、Developer Documentに書いてある動作をしない場合もあります。</p> <p>iTunes ConnectでApp Clipの情報を申請するんですが、その際にApp Clipカード用の画像の登録がうまくいかないことが多いんですよね。 (バグだとは思いますが、同じ画像ファイルを設定しても登録が成功する場合と失敗する場合があります。)</p> <p>また、URLをトリガーとした機動がうまくいかないことも多い(タイミングによって異なる?)ので、そのあたりもまだ調査中です。</p> <p>App Clip は、実装難易度が高く、はまりどころも多いので、初心者Developerには難しいかもしれません。</p> <p>ただ、やっぱり「にゅっとでてくる」のは楽しいですよね。対応しているアプリは少ないですけど、いろいろ楽しいことができそうな気がします。</p> <div class="footnote"> <p class="footnote"><a href="#fn-f981fd46" name="f-f981fd46" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">iOS14.2 beta2で確認した動作なので、バージョンが異なると違う動作になる可能性があります。</span></p> </div> toyship iOSDC 2020に参加しました hatenablog://entry/26006613630894932 2020-09-22T15:47:28+09:00 2020-09-22T15:47:28+09:00 今年も 9/19 から 9/21 に開催されたiOSDC 2020に参加しました。 Synchronized iPhones, Again! 今年はこちらの内容で発表させていただきました。 複数のiPhone端末を連携させて動かすためにはどんな実装ですすめていけばいいのかというお話をデモをまじえて話しています。 speakerdeck.com 当日再生したビデオはこちら。 最初のビデオはARのデモ(?)です。 youtu.be こちらは実際のiPhoneで動かした本当のデモになります。 youtu.be ビデオからもわかるように、この程度の動作では端末間の遅延は感じられないレベルです。 ビデオ… <p>今年も 9/19 から 9/21 に開催されたiOSDC 2020に参加しました。</p> <h2>Synchronized iPhones, Again!</h2> <p>今年はこちらの内容で発表させていただきました。</p> <p>複数のiPhone端末を連携させて動かすためにはどんな実装ですすめていけばいいのかというお話をデモをまじえて話しています。</p> <p><iframe id="talk_frame_667611" src="//speakerdeck.com/player/7c151959c1a74b3d9e5b5d54915d2552" width="710" height="399" style="border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/toyship/synchronized-iphones-again">speakerdeck.com</a></cite></p> <p>当日再生したビデオはこちら。</p> <p>最初のビデオはARのデモ(?)です。</p> <p><iframe width="459" height="344" src="https://www.youtube.com/embed/ka4dRsgbG0M?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/ka4dRsgbG0M">youtu.be</a></cite></p> <p>こちらは実際のiPhoneで動かした本当のデモになります。 <iframe width="459" height="344" src="https://www.youtube.com/embed/2Gm6L9oA7YE?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/2Gm6L9oA7YE">youtu.be</a></cite></p> <p>ビデオからもわかるように、この程度の動作では端末間の遅延は感じられないレベルです。</p> <p>ビデオでは6台を使った動作ですが、実際には10台を超えても特に遅延は感じなかったので、おそらく20台くらいまではこのレベルの遅延で行けそうです。</p> <p>プレゼンの中では詳しく説明しませんでしたが、このデモアプリでは、実際には端末の <code>touchesMoved</code> が発生するたびに他のアプリにtap位置の情報を送っています。情報量的には<code>CGPoint</code> 程度なので、そのレベルの通信ならこの同期速度がだせます。</p> <h2>AR Synchronize!</h2> <p>ARのデモ画面を体験できるアプリのコードをGithubに公開しました。 お手持ちのiPhoneで動かしてみると、テーブルの上に35個のiPhoneが表示されます。 たくさんあるiPhoneの一つをクリックすると、「hello world」という文字が画面に表示されます。</p> <p><a href="https://github.com/TachibanaKaoru/SynchronizediPhones">https://github.com/TachibanaKaoru/SynchronizediPhones</a></p> <p>是非ご自宅のテーブルの上に、iPhoneをたくさん表示してみて下さい。</p> <h2>発表していろいろ</h2> <p>質疑応答、たくさん質問していただいてありがとうございました。 覚えている限り下に書いておきます。</p> <ul> <li><p>Multipeer Connectivityを使うと、電子レンジとかカンファレンス会場とかWiFiが多い場所で通信遅延がでますか?</p> <ul> <li>実際に試したことはないんですが、おそらく遅延は発生すると思います。</li> </ul> </li> <li><p>遅延はどのくらいですか?</p> <ul> <li>このデモの使い方(指で画面に文字をかく)だと気にならないレベルです。</li> </ul> </li> <li><p>デモでは2つのグループをつかっていますが、3つのグループでもできる?</p> <ul> <li>可能ですが、端末数が足りなかったので実装はしていません。</li> </ul> </li> <li><p>バックグラウンドでMultipeer Connectivityはできる?</p> <ul> <li>おそらくできないと思います。</li> </ul> </li> <li><p>もしリアル会場での開催だったら、会場のみんなで試せた?</p> <ul> <li>はい、できますね。是非やりたかったです。</li> </ul> </li> <li><p>Multipeer Connectivityを使うアプリをリリースするときに注意したほうがいいことはありますか?</p> <ul> <li>iOS14からはユーザー許諾を取る必要があるので、info.plistに説明文を追加する必要があります。</li> </ul> </li> </ul> <p>質疑応答の時に、「おととしの発表もおもしろかったです」と言ってくださった方がいらっしゃいました。(どなたか確認することはできなかったのですが……)</p> <p>実は、今回のこの発表はおととしの発表(<a href="https://www.toyship.org/2018/09/05/231525">https://www.toyship.org/2018/09/05/231525</a>)の続きでもあったんですが、覚えている方はいないだろうとプレゼンの中ではそこはスルーしていました。</p> <p>その発表をちゃんと覚えてくださった方がいて、こうやって声をかけていただけるなんて、発表してよかったなぁ……とちょっとじーんとしてました。</p> <h2>リアルアバター</h2> <p>今回発表のときにリアルアバターをかぶったんですけど、本当にたいへんでした……。</p> <p><figure class="figure-image figure-image-fotolife" title="avatar"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200922/20200922150508.jpg" alt="f:id:toyship:20200922150508j:plain" title="f:id:toyship:20200922150508j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>avatar</figcaption></figure></p> <p>本番の録画の前に、アバターをかぶって40分リハーサルをしてみたら、もう酸欠で気分が悪くなってしまい、本番の録画の時には気分が悪いのをがまんんしてしゃべるという状況になってしまいました。 (そのあと撮り直せばよかったんですが、もう疲れてアバターでしゃべる気になれなかったので……。)</p> <p>あと、リアルアバターは声がこもってしまって、聞く側も少しききとりづらいですね。聞いていた方、申し訳ありません。</p> <p>ほんと大変だったので、次回アバターをかぶるときにはもう少し工夫したいです。</p> <h2>初のリモートiOSDC</h2> <p>今年は、iOSDCは初のリモート開催でしたね。</p> <p>始まる前はどうなるんだろうと思っていましたが、自宅から参加できるのは手軽だし、無限コーヒーもあるし、複数のセッションがきけるし、朝早く起きなくてもいいし、参加のハードルもさがったし、よかった面も多かったのではと思います。</p> <p>ただ、リモートだと、「Ask the speaker」でとても質問しづらいですよね。 いつもの 「Ask the speaker」ではスピーカーの方とだけお話しするので、初心者的な質問でも躊躇なくきけたんですが、今年はDiscordで、その部屋にいる全部の方に聞こえる中で質問するので、少しハードルが高めになっていました。 (そのなかで質問していただいた方、本当にありがとうございました。)</p> <p>また、参加のハードルは低くなったけど、新しく知り合いをつくる面では難しくなってしまった部分もあると思うので、リアルが懐かしくもありますね。</p> <p>来年はどうなるかわかりませんが、リアル・リモートどちらでもまた参加したいと思います。 スタッフ、スポンサーのみなさん、ありがとうございました。</p> toyship Combine 最初の一歩 hatenablog://entry/26006613603641660 2020-07-24T19:50:01+09:00 2021-06-06T03:53:55+09:00 WWDC2019で紹介されたCombineはSwiftで使えるasync frameworkです。 iOS13以上でしか使えないのでプロダクツに導入するのをためらっていましたが、そろそろ導入できそうですね。 まだCombineを導入していない方向けに、Combineの簡単な使い方を書いてみました。 (この記事はXcode v12.0 beta 3 (12A8169g)で確認しています。今後のバージョンアップで動作が変わるかもしれません。) PublisherとSubscriber PublisherはCombineの中核となるもので、目的のデータを時系列順に送る output stream み… <p>WWDC2019で紹介されたCombineはSwiftで使えるasync frameworkです。</p> <p>iOS13以上でしか使えないのでプロダクツに導入するのをためらっていましたが、そろそろ導入できそうですね。</p> <p>まだCombineを導入していない方向けに、Combineの簡単な使い方を書いてみました。</p> <p>(この記事はXcode v12.0 beta 3 (12A8169g)で確認しています。今後のバージョンアップで動作が変わるかもしれません。)</p> <h2>PublisherとSubscriber</h2> <p><code>Publisher</code>はCombineの中核となるもので、目的のデータを時系列順に送る output stream みたいなものですね。 そして、 <code>Subscriber</code>がそのstreamを受け取る側になります。</p> <p>まずは一番単純な <code>Publisher</code>を動かしてみましょう。 素数の配列から<code>Publisher</code>を作り、<code>Subscriber</code>はそれをprintするコードです。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">sosu</span> <span class="synIdentifier">=</span> [<span class="synConstant">2</span>,<span class="synConstant">3</span>,<span class="synConstant">5</span>,<span class="synConstant">7</span>,<span class="synConstant">11</span>] <span class="synPreProc">let</span> <span class="synIdentifier">cancellable</span> <span class="synIdentifier">=</span> sosu.publisher .sink(receiveCompletion<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;completion: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }, receiveValue<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;value: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }) <span class="synComment">// (結果)</span> <span class="synComment">// value: 2</span> <span class="synComment">// value: 3</span> <span class="synComment">// value: 5</span> <span class="synComment">// value: 7</span> <span class="synComment">// value: 11</span> <span class="synComment">// completion: finished</span> </pre> <p>動かしてみると、配列の要素を順番に送り、すべての要素を送り終わったらCompletionが送られています。</p> <p>ちょっとこの書き方だとどれが<code>Publisher</code>と<code>Subscriber</code>なのかわかりにくいので、書き直してみましょう。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">sosu</span> <span class="synIdentifier">=</span> [<span class="synConstant">2</span>,<span class="synConstant">3</span>,<span class="synConstant">5</span>,<span class="synConstant">7</span>,<span class="synConstant">11</span>] <span class="synPreProc">let</span> <span class="synIdentifier">sosuPublisher</span> <span class="synIdentifier">=</span> sosu.publisher <span class="synPreProc">let</span> <span class="synIdentifier">sosuSubscriber</span> <span class="synIdentifier">=</span> Subscribers.Sink<span class="synIdentifier">&lt;</span>Int,Never<span class="synIdentifier">&gt;</span>( receiveCompletion<span class="synSpecial">:</span> {comp <span class="synStatement">in</span> print(<span class="synConstant">&quot;completion: </span><span class="synSpecial">\(comp)</span><span class="synConstant">&quot;</span>)}, receiveValue<span class="synSpecial">:</span> { val <span class="synStatement">in</span> print (<span class="synConstant">&quot;value: </span><span class="synSpecial">\(val)</span><span class="synConstant">&quot;</span>)}) sosuPublisher.subscribe(sosuSubscriber) <span class="synComment">// (結果)</span> <span class="synComment">// value: 2</span> <span class="synComment">// value: 3</span> <span class="synComment">// value: 5</span> <span class="synComment">// value: 7</span> <span class="synComment">// value: 11</span> <span class="synComment">// completion: finished</span> </pre> <p><code>sosuPublisher</code>が<code>Publisher</code>で、<code>sosuSubscriber</code>が<code>Subscriber</code>です。 これで<code>Publisher</code>と<code>Subscriber</code>がわかりやすくなりました。</p> <p><code>sosuPublisher</code>はIntのArrayを先頭から順に送り、 <code>sosuSubscriber</code>はそれを順にプリントアウトしていきます。</p> <p>最初の書き方は<code>Chained Publisher</code>とよばれ、これを使うと、下記のようにArray要素の追加も可能です。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">sosu</span> <span class="synIdentifier">=</span> [<span class="synConstant">2</span>,<span class="synConstant">3</span>,<span class="synConstant">5</span>,<span class="synConstant">7</span>,<span class="synConstant">11</span>] <span class="synPreProc">let</span> <span class="synIdentifier">cancellable</span> <span class="synIdentifier">=</span> sosu.publisher .append([<span class="synConstant">17</span>,<span class="synConstant">19</span>]) .prepend([<span class="synConstant">0</span>,<span class="synConstant">1</span>]) .sink(receiveCompletion<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;completion: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }, receiveValue<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;value: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }) <span class="synComment">// (結果)</span> <span class="synComment">// value: 0</span> <span class="synComment">// value: 1</span> <span class="synComment">// value: 2</span> <span class="synComment">// value: 3</span> <span class="synComment">// value: 5</span> <span class="synComment">// value: 7</span> <span class="synComment">// value: 11</span> <span class="synComment">// value: 17</span> <span class="synComment">// value: 19</span> <span class="synComment">// completion: finished</span> </pre> <p>(念のためですが、0と1は素数ではありません。)</p> <p>さて、ここまでは固定要素のArray のPublisherであまりasync要素が感じられないので、逐次的なPublisherを作ってみましょう。</p> <h2>Passthrough な Publisher</h2> <p><code>PassthroughSubject</code>を使って、入力されたものを逐次的に送る<code>Publisher</code>を作ってみます。 (<code>Subject</code>は<code>Publisher</code>の一種だと考えてください。)</p> <p>さきほどと同じように<code>sink subscriber</code>を使ってプリントアウトするようにセットアップします。</p> <p>ここでは文字列をどんどん流していく<code>Publisher</code>を作りましたが、作成した<code>Publisher</code>に<code>send</code>コマンドで任意のデータを流すことができます。</p> <p>ボタンアクションなどで、この<code>send</code>アクションを呼ぶようにしておくと、ボタンをおすたびに<code>Subscriber</code>がよばれるのがわかると思います。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">var</span> <span class="synIdentifier">stringPublisher</span> <span class="synIdentifier">=</span> PassthroughSubject<span class="synIdentifier">&lt;</span>String?, Never<span class="synIdentifier">&gt;</span>() <span class="synPreProc">var</span> <span class="synIdentifier">cancellables</span> <span class="synIdentifier">=</span> [AnyCancellable]() <span class="synPreProc">func</span> <span class="synIdentifier">setupStringSubscriber</span>(){ stringPublisher .sink(receiveCompletion<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;completion: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }, receiveValue<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;value: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }) .store(<span class="synStatement">in</span><span class="synSpecial">:</span> <span class="synIdentifier">&amp;</span>cancellables) } <span class="synComment">// Hello ButtonのAction</span> <span class="synPreProc">func</span> <span class="synIdentifier">pressHelloButton</span>(){ stringPublisher.send(<span class="synConstant">&quot;hello,&quot;</span>) } <span class="synComment">// World ButtonのAction</span> <span class="synPreProc">func</span> <span class="synIdentifier">pressWorldButton</span>(){ stringPublisher.send(<span class="synConstant">&quot;world!&quot;</span>) } <span class="synComment">// Hello Buttonを押すと</span> <span class="synComment">// value: Optional(&quot;hello,&quot;)</span> <span class="synComment">// World Buttonを押すと</span> <span class="synComment">// value: Optional(&quot;world!&quot;)</span> </pre> <p>ちょっとストリームっぽくなってきましたね。</p> <h2>Publisher はいつとまるのか。</h2> <p>さて、<code>Publisher</code>のストリームをとめたい場合、どうすればいいでしょうか。</p> <p>大きく分けて3つの方法があります、</p> <p>まず、一つ目は<code>Publisher</code>側から <code>Completion</code> を送る方法です。 この<code>completion</code>が送られると、 <code>Subscriber</code>側でも<code>Completion</code>ハンドラがよばれます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> stringPublisher.send(<span class="synConstant">&quot;hello,&quot;</span>) stringPublisher.send(<span class="synConstant">&quot;world!&quot;</span>) stringPublisher.send(completion<span class="synSpecial">:</span> .finished) <span class="synComment">// Completionを送ります。</span> stringPublisher.send(<span class="synConstant">&quot;again&quot;</span>) <span class="synComment">// 上の行でfinishedを送ったので、これは送られません。</span> <span class="synComment">// (結果)</span> <span class="synComment">// value: Optional(&quot;hello,&quot;)</span> <span class="synComment">// value: Optional(&quot;world!&quot;)</span> <span class="synComment">// completion: finished</span> </pre> <p>二つ目は<code>Subscriber</code>側からキャンセルする方法。 キャンセルのために必要な<code>token</code>が<code>AnyCancellable</code>になっており、<code>subscribe</code>したときに取得できるので、下記のようにstoreしておきましょう。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">var</span> <span class="synIdentifier">stringPublisher</span> <span class="synIdentifier">=</span> PassthroughSubject<span class="synIdentifier">&lt;</span>String?, Never<span class="synIdentifier">&gt;</span>() <span class="synPreProc">var</span> <span class="synIdentifier">cancellables</span> <span class="synIdentifier">=</span> [AnyCancellable]() <span class="synPreProc">func</span> <span class="synIdentifier">setupStringSubscriber</span>(){ stringPublisher .sink(receiveCompletion<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;completion: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }, receiveValue<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;value: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }) .store(<span class="synStatement">in</span><span class="synSpecial">:</span> <span class="synIdentifier">&amp;</span>cancellables) } </pre> <p>それを使ってキャンセルします。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> cancellables.first?.cancel() </pre> <p>三つ目は、ちょっと消極的な方法ですが、この<code>AnyCancellable</code>のオブジェクトの消滅です。 下記のように、 <code>AnyCancellable</code>を保持していない場合、<code>setupStringSubscriber</code>のスコープをぬけた瞬間にstreamは停止します。</p> <p>(streamが予想外に停止してしまう場合、この<code>AnyCancellable</code>がきちんと保持できているかどうかを確認しましょう。)</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">var</span> <span class="synIdentifier">stringPublisher</span> <span class="synIdentifier">=</span> PassthroughSubject<span class="synIdentifier">&lt;</span>String?, Never<span class="synIdentifier">&gt;</span>() <span class="synPreProc">func</span> <span class="synIdentifier">setupStringSubscriber</span>(){ stringPublisher .sink(receiveCompletion<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;completion: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }, receiveValue<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;value: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }) <span class="synComment">// 保持されていないので、この瞬間にstringPublisherは停止。</span> } </pre> <h2>Assign Subscriber</h2> <p><code>Subscriber</code>には、いままで使っていた<code>sink subscriber</code>の他にもう一つ<code>assign subscriber</code>というものがあります。</p> <p>この<code>assign subscriber</code> は、指定したKeyの値を変更することができるので、実行時にかなり便利です。</p> <p>下記では、<code>UILabel</code>に表示されるテキストを<code>Publisher</code>から送られたものにしています。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synType">@IBOutlet</span> weak <span class="synPreProc">var</span> <span class="synIdentifier">resultLabel</span><span class="synSpecial">:</span> <span class="synType">UILabel</span><span class="synIdentifier">!</span> <span class="synPreProc">var</span> <span class="synIdentifier">stringPublisher</span> <span class="synIdentifier">=</span> PassthroughSubject<span class="synIdentifier">&lt;</span>String?, Never<span class="synIdentifier">&gt;</span>() <span class="synPreProc">var</span> <span class="synIdentifier">cancellables</span> <span class="synIdentifier">=</span> [AnyCancellable]() <span class="synPreProc">func</span> <span class="synIdentifier">setupStringSubscriber</span>(){ stringPublisher .assign(to<span class="synSpecial">:</span> \.resultLabel.text, on<span class="synSpecial">:</span> <span class="synType">self</span>) .store(<span class="synStatement">in</span><span class="synSpecial">:</span> <span class="synIdentifier">&amp;</span>cancellables) } </pre> <h2>Notification</h2> <p>さて、ここからは、既存のコードをCombineでかきかえたらどうなるかをみていきましょう。</p> <p>まずは<code>Notification</code>。<code>UITextField</code>で文字を入力した時の<code>.textDidChangeNotification</code>で見てみましょう。</p> <p>今までは<code>addObserver</code>をしていましたね。</p> <pre class="code lang-swift" data-lang="swift" data-unlink>NotificationCenter.<span class="synStatement">default</span>.addObserver( forName<span class="synSpecial">:</span> <span class="synType">UITextField.textDidChangeNotification</span>, object<span class="synSpecial">:</span> <span class="synType">nil</span>, queue<span class="synSpecial">:</span> <span class="synType">OperationQueue.main</span>) { (note) <span class="synStatement">in</span> <span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">txf</span> <span class="synIdentifier">=</span> note.object <span class="synStatement">as</span>? UITextField{ <span class="synPreProc">let</span> <span class="synIdentifier">currentText</span> <span class="synIdentifier">=</span> txf.text print(<span class="synConstant">&quot;old way: </span><span class="synSpecial">\(currentText)</span><span class="synConstant">&quot;</span>) } }) <span class="synComment">// (結果)</span> <span class="synComment">// old way: Optional(&quot;a&quot;)</span> <span class="synComment">// old way: Optional(&quot;ab&quot;)</span> <span class="synComment">// old way: Optional(&quot;abc&quot;)</span> </pre> <p>Combineで<code>sink subscriber</code>を使うとこうなります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink>NotificationCenter .<span class="synStatement">default</span> .publisher(<span class="synStatement">for</span><span class="synSpecial">:</span> <span class="synType">UITextField.textDidChangeNotification</span>, object<span class="synSpecial">:</span> <span class="synType">inputText</span>) .map { (<span class="synIdentifier">$0</span>.object <span class="synStatement">as</span><span class="synIdentifier">!</span> UITextField).text } .sink(receiveCompletion<span class="synSpecial">:</span> { print (<span class="synConstant">&quot; comp:</span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }, receiveValue<span class="synSpecial">:</span> { print (<span class="synConstant">&quot; new way:</span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) }) .store(<span class="synStatement">in</span><span class="synSpecial">:</span> <span class="synIdentifier">&amp;</span>cancellables) <span class="synComment">// (結果)</span> <span class="synComment">// new way: Optional(&quot;a&quot;)</span> <span class="synComment">// new way: Optional(&quot;ab&quot;)</span> <span class="synComment">// new way: Optional(&quot;abc&quot;)</span> </pre> <p><code>assign subscriber</code>ではこうなります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink>NotificationCenter .<span class="synStatement">default</span> .publisher(<span class="synStatement">for</span><span class="synSpecial">:</span> <span class="synType">UITextField.textDidChangeNotification</span>, object<span class="synSpecial">:</span> <span class="synType">inputText</span>) .map { (<span class="synIdentifier">$0</span>.object <span class="synStatement">as</span><span class="synIdentifier">!</span> UITextField).text } .assign(to<span class="synSpecial">:</span> \.resultLabel.text, on<span class="synSpecial">:</span> <span class="synType">self</span>) .store(<span class="synStatement">in</span><span class="synSpecial">:</span> <span class="synIdentifier">&amp;</span>cancellables) </pre> <h2>KVO</h2> <p>次は key value observing です。</p> <p><code>UIScrollView</code>の<code>contentOffset</code>をKVOで確認するコードで見てみましょう。</p> <p>Swift3ではこうでした。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synType">@IBOutlet</span> weak <span class="synPreProc">var</span> <span class="synIdentifier">scrollView</span><span class="synSpecial">:</span> <span class="synType">UIScrollView</span><span class="synIdentifier">!</span> <span class="synPreProc">func</span> <span class="synIdentifier">setupKVO</span>(){ scrollView.addObserver(<span class="synIdentifier">self</span>, forKeyPath<span class="synSpecial">:</span> <span class="synConstant">&quot;contentOffset&quot;</span>, options<span class="synSpecial">:</span> .<span class="synStatement">new</span>, context<span class="synSpecial">:</span> <span class="synType">nil</span>) } <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">observeValue</span>(forKeyPath keyPath<span class="synSpecial">:</span> <span class="synType">String</span>?, of object<span class="synSpecial">:</span> <span class="synType">Any</span>?, change<span class="synSpecial">:</span> <span class="synPreProc">[NSKeyValueChangeKey : Any]</span>?, context<span class="synSpecial">:</span> <span class="synType">UnsafeMutableRawPointer</span>?) { print(<span class="synConstant">&quot; key: </span><span class="synSpecial">\(keyPath)</span><span class="synConstant">&quot;</span>) } </pre> <p>Swift4からはこうなりました。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synType">@IBOutlet</span> weak <span class="synPreProc">var</span> <span class="synIdentifier">scrollView</span><span class="synSpecial">:</span> <span class="synType">UIScrollView</span><span class="synIdentifier">!</span> <span class="synPreProc">private</span> <span class="synPreProc">var</span> <span class="synIdentifier">observations</span> <span class="synIdentifier">=</span> [NSKeyValueObservation]() <span class="synPreProc">func</span> <span class="synIdentifier">setupKVO</span>(){ observations.append(scrollView.observe(\.contentOffset, options<span class="synSpecial">:</span> .<span class="synStatement">new</span>){_,change <span class="synStatement">in</span> print(<span class="synConstant">&quot;contentOffset : </span><span class="synSpecial">\(change.newValue)</span><span class="synConstant">&quot;</span>) }) } </pre> <p>そして、Combineにするとこうなります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synType">@IBOutlet</span> weak <span class="synPreProc">var</span> <span class="synIdentifier">scrollView</span><span class="synSpecial">:</span> <span class="synType">UIScrollView</span><span class="synIdentifier">!</span> <span class="synPreProc">var</span> <span class="synIdentifier">cancellables</span> <span class="synIdentifier">=</span> [AnyCancellable]() <span class="synPreProc">func</span> <span class="synIdentifier">setupKVO</span>(){ KeyValueObservingPublisher(object<span class="synSpecial">:</span> <span class="synType">scrollView</span>, keyPath<span class="synSpecial">:</span> \.contentOffset, options<span class="synSpecial">:</span> .<span class="synStatement">new</span>) .receive(on<span class="synSpecial">:</span> <span class="synType">DispatchQueue.main</span>) .sink(receiveCompletion<span class="synSpecial">:</span> { completion <span class="synStatement">in</span> print(<span class="synConstant">&quot;finished&quot;</span>) }, receiveValue<span class="synSpecial">:</span> { response <span class="synStatement">in</span> print(response) }).store(<span class="synStatement">in</span><span class="synSpecial">:</span> <span class="synIdentifier">&amp;</span>cancellables) } </pre> <h2>Timer</h2> <p><code>Timer</code>の <code>Publisher</code>は<code>ConnectablePublisher</code>といって、他の <code>Publisher</code>と少し違い、<code>connect</code>しないと使えません。 すぐ発火する場合には、<code>autoconnect</code>を使うと便利です。</p> <p>1秒おきに時間を確認するコードで見てみましょう。</p> <p><code>sink subscriber</code>の場合はこうなります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink>Timer.publish(every<span class="synSpecial">:</span> <span class="synConstant">1.0</span>, on<span class="synSpecial">:</span> .main, <span class="synStatement">in</span><span class="synSpecial">:</span> .common) .autoconnect() .sink() { print (<span class="synConstant">&quot;timer fired: </span><span class="synSpecial">\($0)</span><span class="synConstant">&quot;</span>) } .store(<span class="synStatement">in</span><span class="synSpecial">:</span> <span class="synIdentifier">&amp;</span>cancellables) </pre> <p><code>assign subscriber</code>の場合はこうなります。(画面上のラベルに時間を表示するようになります。)</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">timer</span> <span class="synIdentifier">=</span> Timer.publish(every<span class="synSpecial">:</span> <span class="synConstant">1.0</span>, on<span class="synSpecial">:</span> .main, <span class="synStatement">in</span><span class="synSpecial">:</span> .<span class="synStatement">default</span>) .autoconnect() .map { String(describing<span class="synSpecial">:</span> <span class="synIdentifier">$0</span>) } .assign(to<span class="synSpecial">:</span> \.resultLabel.text, on<span class="synSpecial">:</span> <span class="synType">self</span>) </pre> <h2>Network</h2> <p>URLSessionをつかったネットワーク処理もCombineで書くことができます。</p> <p>こちらはGithubのリポジトリを取得するコードです。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">var</span> <span class="synIdentifier">cancellableNetwork</span><span class="synSpecial">:</span> <span class="synType">AnyCancellable</span>? <span class="synIdentifier">=</span> <span class="synConstant">nil</span> <span class="synPreProc">struct</span> <span class="synType">Repository</span><span class="synSpecial">:</span> <span class="synType">Codable</span> { <span class="synPreProc">let</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synPreProc">let</span> <span class="synIdentifier">html_url</span><span class="synSpecial">:</span> <span class="synType">String</span> } <span class="synPreProc">func</span> <span class="synIdentifier">fetch</span>(_ sender<span class="synSpecial">:</span> <span class="synType">Any</span>) { <span class="synPreProc">let</span> <span class="synIdentifier">url</span> <span class="synIdentifier">=</span> URL(string<span class="synSpecial">:</span> <span class="synConstant">&quot;https://api.github.com/repositories&quot;</span>)<span class="synIdentifier">!</span> <span class="synPreProc">let</span> <span class="synIdentifier">session</span> <span class="synIdentifier">=</span> URLSession(configuration<span class="synSpecial">:</span> .<span class="synStatement">default</span>) cancellableNetwork <span class="synIdentifier">=</span> session .dataTaskPublisher(<span class="synStatement">for</span><span class="synSpecial">:</span> <span class="synType">url</span>) .tryMap() { element <span class="synSpecial">-&gt;</span> <span class="synType">Data</span> <span class="synStatement">in</span> <span class="synStatement">guard</span> <span class="synPreProc">let</span> <span class="synIdentifier">httpResponse</span> <span class="synIdentifier">=</span> element.response <span class="synStatement">as</span>? HTTPURLResponse, httpResponse.statusCode <span class="synIdentifier">==</span> <span class="synConstant">200</span> <span class="synStatement">else</span> { throw URLError(.badServerResponse) } <span class="synStatement">return</span> element.data } .decode(type<span class="synSpecial">:</span> <span class="synPreProc">[Repository]</span>.<span class="synIdentifier">self</span>, decoder<span class="synSpecial">:</span> <span class="synType">JSONDecoder</span>()) .sink(receiveCompletion<span class="synSpecial">:</span> { print (<span class="synConstant">&quot;Received completion: </span><span class="synSpecial">\($0)</span><span class="synConstant">.&quot;</span>) }, receiveValue<span class="synSpecial">:</span> { value <span class="synStatement">in</span> print (<span class="synConstant">&quot;Received user: </span><span class="synSpecial">\(value)</span><span class="synConstant">.&quot;</span>)}) } </pre> <p>Apple のCombineによるネットワークアクセスのガイドもあるので、詳しくはそちらをみてください。 <a href="https://developer.apple.com/documentation/foundation/urlsession/processing_url_session_data_task_results_with_combine">Apple Developer Documentation</a></p> <h2>まとめ</h2> <p>Combine、まだまだいろいろな機能がありますが、とりあえず最初の一歩としてはこのくらいわかっていればいいかなと思います。 まずは始めてみましょう!</p> toyship Pure SwiftUI App Life Cycle hatenablog://entry/26006613594971334 2020-07-17T15:02:47+09:00 2020-07-17T15:02:47+09:00 去年のWWDC2019で発表されたSwiftUI。 待望の Swift製のUIライブラリでしたが、実際のところはUIHostingController上に新しいSwiftUIのViewをのせる方式で、過去互換性を守っていました。 そうするしかないだろうとは思ってたんですけど、中途半端さにすっきりしない気分の方も多かったと思います。 iOS14では、いよいよ、アプリのすべてのUIをSwiftUIで作ることができるようになりました。 UIViewControllerにもStoryboardにもAutoLayoutにもAppDelegateにも、window.makeKeyAndVisibleにもさ… <p>去年のWWDC2019で発表されたSwiftUI。 待望の Swift製のUIライブラリでしたが、実際のところはUIHostingController上に新しいSwiftUIのViewをのせる方式で、過去互換性を守っていました。</p> <p>そうするしかないだろうとは思ってたんですけど、中途半端さにすっきりしない気分の方も多かったと思います。</p> <p>iOS14では、いよいよ、アプリのすべてのUIをSwiftUIで作ることができるようになりました。 UIViewControllerにもStoryboardにもAutoLayoutにもAppDelegateにも、window.makeKeyAndVisibleにもさよならです。</p> <p>今までの古いコードを全部捨てて新しく書き直せるのかと思うと、ちょっとドキドキしますね。</p> <p>(この記事の内容はXcode 12.0 beta 2 (12A6163b) で調査した結果ですが、実際に14.0が正式にリリースされる際には変更になる可能性が高いです。最新のXcodeで確認してくださいね。)</p> <h2>Pure Swift UI の前提条件</h2> <p>Pure Swift UIを使うには、target version を14.0にする必要があります。</p> <p>iOS13以下のバージョンに対応する必要がある場合には使えないので、現実のプロジェクトに今すぐ適用するのは難しいかもしれません。</p> <p>ただ、Pure SwiftUIにするとどう変わるのかを確認しておくと、今からアプリの将来設計を考えておく助けになるかと思います。</p> <h2>Pure Swift UI にするには</h2> <p>新規でPure Swift UI のプロジェクトを作る場合には、Xcode12を使ってください。</p> <p>新規プロジェクトの設定ダイアログの中に<code>Life Cycle</code>という項目があります。 ここで<code>SwiftUI App</code>を選ぶと Pure SwiftUIのアプリになり、<code>UIKit App Delegate</code>を選ぶと今までの App Delegate を使った構成になります。</p> <h2>アプリの起動時の処理</h2> <p>今までは、<code>AppDelegate(UIApplicationDelegate)</code>の<code>didFinishLaunchingWithOptions</code>で起動時の処理を行なっていましたね。</p> <p>PureSwiftUIでは、<code>App Protocol</code>を継承する構造体(!)がその役目を担います。</p> <p>現状では、その構造体の<code>init()</code>で起動時処理をやるしかなさそうです。</p> <p>試してみたところ、@Environmenで取得できる値が、<code>init()</code>で取得した値とSceneやViewで取得した値とでと異なっていることもありましたので、若干注意が必要です。</p> <p>AppleのApp Life Cycle の公式ドキュメントには、SwiftUI Appについて記述がないので、このあたりの処理は今後変更される可能性もありそうです。 <a href="https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle">https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle</a></p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synType">@main</span> <span class="synPreProc">struct</span> <span class="synType">MyPureSwiftApp</span><span class="synSpecial">:</span> <span class="synType">App</span> { <span class="synType">@SceneBuilder</span> <span class="synPreProc">var</span> <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synType">some</span> Scene { WindowGroup { ContentView() } <span class="synIdentifier">init</span>(){ print(<span class="synConstant">&quot; initializing...&quot;</span>) } } </pre> <h2>アプリがバックグラウンドになった時の処理</h2> <p>バックグラウンド/フォアグラウンドの処理は、iOS13で大きく変更が入っていますよね。(以前の構成のままでも特に問題はないので、新構成に対応した人は多くはないと思いますが。)</p> <p>iOS12までは、AppDelegateでの対応、iOS13からはSceneDelegateで対応することになっていました。</p> <p>Pure SwiftUIではView、App、Sceneのどこでも処理できるようになります。</p> <p>Viewで処理する場合はこうなります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">struct</span> <span class="synType">SmallView</span><span class="synSpecial">:</span> <span class="synType">View</span> { <span class="synType">@Environment</span>(\.scenePhase) <span class="synPreProc">private</span> <span class="synPreProc">var</span> <span class="synIdentifier">scenePhase</span> <span class="synPreProc">var</span> <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synType">some</span> View { Text(<span class="synConstant">&quot;Hello World&quot;</span>) .onChange(of<span class="synSpecial">:</span> <span class="synType">scenePhase</span>) { phase <span class="synStatement">in</span> print(<span class="synConstant">&quot;user scene changed to..</span><span class="synSpecial">\(phase)</span><span class="synConstant">&quot;</span>) } } } </pre> <p>App で処理する場合はこうなります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synType">@main</span> <span class="synPreProc">struct</span> <span class="synType">MyApp</span><span class="synSpecial">:</span> <span class="synType">App</span> { <span class="synType">@Environment</span>(\.scenePhase) <span class="synPreProc">private</span> <span class="synPreProc">var</span> <span class="synIdentifier">scenePhase</span> <span class="synPreProc">var</span> <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synType">some</span> Scene { WindowGroup { MyRootView() } .onChange(of<span class="synSpecial">:</span> <span class="synType">scenePhase</span>) { phase <span class="synStatement">in</span> <span class="synStatement">if</span> phase <span class="synIdentifier">==</span> .background { print(<span class="synConstant">&quot;changed to background!&quot;</span>) } } } } </pre> <p><code>ScenePhase</code>は<code>active</code>、<code>inactive</code>、<code>background</code>の3値をとるenumです。</p> <h2>openURL</h2> <p>アプリ間連携や、その他細々したところでよく使うopenURLですが、こちらは今のところViewでしかハンドリングできないようです。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">struct</span> <span class="synType">SmallView</span><span class="synSpecial">:</span> <span class="synType">View</span> { <span class="synPreProc">var</span> <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synType">some</span> View { Text(<span class="synConstant">&quot;Hello&quot;</span>) .onOpenURL{ url <span class="synStatement">in</span> print(<span class="synConstant">&quot; called with url.. </span><span class="synSpecial">\(url)</span><span class="synConstant">&quot;</span>) } } } </pre> <h2>アプリ起動画面</h2> <p>今までは、LaunchScreen Storyboardを使っていましたね。(古くはDefault.pngという固定イメージファイルを使っていましたが……。)</p> <p>Pure Swift UIアプリでは、info.plistにファイル名を書くことでその画像を起動画面にすることができます。</p> <pre class="code" data-lang="" data-unlink> &lt;key&gt;UILaunchScreen&lt;/key&gt; &lt;dict&gt; &lt;key&gt;UIImageName&lt;/key&gt; &lt;string&gt;startScreen&lt;/string&gt; &lt;/dict&gt;</pre> <h2>viewDidAppear系</h2> <p>今までViewControllerで使っていたviewDidAppear/viewDidDisapperはViewのonAppear、 onDisapperになります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">struct</span> <span class="synType">SmallView</span><span class="synSpecial">:</span> <span class="synType">View</span> { <span class="synPreProc">var</span> <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synType">some</span> View { Text(<span class="synConstant">&quot;Hello World&quot;</span>) .onAppear { print(<span class="synConstant">&quot;use view appeared!&quot;</span>) } .onDisappear { print(<span class="synConstant">&quot;user view disappeared!&quot;</span>) } } } </pre> <h2>まとめ</h2> <p>正直なところ、Pure SwiftUIは、公式ドキュメントの整備もまだまだなので、これからまだいろいろと変更がありそうです。</p> <p>本番のコードにいれるのはまだ先かもしれませんが、プライベートプロジェクトなどで是非試してみましょう。</p> toyship NearbyInteractionで周囲の端末の位置を測定する hatenablog://entry/26006613588833140 2020-06-24T05:46:55+09:00 2020-06-24T09:24:00+09:00 NearbyInteractionとはiOS14で導入された新しいフレームワークです。 近距離無線チップを使い、複数の端末の距離や方向を測定することができます。 UWB UWBとは、iPhone 11で導入されていた近距離無線チップです。 何に使われているのかよくわからず、売りにしているんだかどうだか、いまいちわからない機能です。 (対応端末を持っている人が近くにいるとAirDropのときに優先的に表示されるそうんだけど、私は実感できたことはないですね……。) share with UWB 使用可能な端末 このフレームワークは、UWBチップがのっている端末どうしでだけ使えます。 UWBチップは… <p>NearbyInteractionとはiOS14で導入された新しいフレームワークです。 近距離無線チップを使い、複数の端末の距離や方向を測定することができます。</p> <h2>UWB</h2> <p>UWBとは、iPhone 11で導入されていた近距離無線チップです。 何に使われているのかよくわからず、売りにしているんだかどうだか、いまいちわからない機能です。</p> <p>(対応端末を持っている人が近くにいるとAirDropのときに優先的に表示されるそうんだけど、私は実感できたことはないですね……。) <figure class="figure-image figure-image-fotolife" title="share with UWB"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200624/20200624050715.png" alt="f:id:toyship:20200624050715p:plain" title="f:id:toyship:20200624050715p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>share with UWB</figcaption></figure></p> <h2>使用可能な端末</h2> <p>このフレームワークは、UWBチップがのっている端末どうしでだけ使えます。</p> <p>UWBチップは、現状では <code>iPhone 11</code>、<code>iPhone 11 Pro</code>、<code>iPhone 11 Pro Max</code> の3モデルのみに搭載されています。 この端末が複数ないと動作を試すこともできないのですが、そのあたりのことを配慮してくれたのか、シミュレーターでも動作を確認することができます。</p> <p>サンプルコードをシミュレーターで動かしてみると、だいたいの動作はわかると思います。</p> <p><a href="https://developer.apple.com/documentation/nearbyinteraction/implementing_interactions_between_users_in_close_proximity">https://developer.apple.com/documentation/nearbyinteraction/implementing_interactions_between_users_in_close_proximity</a></p> <h2>測定できるもの</h2> <p>このフレームワークでは、検知した端末の位置(distance)と相対角度(direction)を測定することができます。 <figure class="figure-image figure-image-fotolife" title="distance and angle"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200624/20200624051037.png" alt="f:id:toyship:20200624051037p:plain" title="f:id:toyship:20200624051037p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>distance and angle</figcaption></figure></p> <p>また、周囲に複数端末がある場合、それらすべての端末を認識することができます。 <figure class="figure-image figure-image-fotolife" title="detect many devices"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200624/20200624051127.png" alt="f:id:toyship:20200624051127p:plain" title="f:id:toyship:20200624051127p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>detect many devices</figcaption></figure></p> <p>ただし、このdistanceとdirectionの値は<code>optional</code>で、端末の状況によっては<code>nil</code>になることがあります。 下の図にあるように、相手の端末がカメラの視野角にはいっていればdistanceとdirectionの両方をとれますが、視野角外ではdistanceしかとることができません。</p> <p><figure class="figure-image figure-image-fotolife" title="angle may be nil"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200624/20200624051713.png" alt="f:id:toyship:20200624051713p:plain" title="f:id:toyship:20200624051713p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>angle may be nil</figcaption></figure></p> <p>正しく位置と角度をとるには、相互の端末を向かい合わせにするのが推奨されています。</p> <p><figure class="figure-image figure-image-fotolife" title="device position"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200624/20200624065131.png" alt="f:id:toyship:20200624065131p:plain" title="f:id:toyship:20200624065131p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>device position</figcaption></figure></p> <p>また、端末のあいだに壁や人や犬や猫がいた場合にもdistance やdirectionがとれなくなってしまうそうです。</p> <p><figure class="figure-image figure-image-fotolife" title="may not detect data"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200624/20200624051938.png" alt="f:id:toyship:20200624051938p:plain" title="f:id:toyship:20200624051938p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>may not detect data</figcaption></figure></p> <h2>使い方</h2> <p>使い方はわりとシンプルで、下記のように<code>NISession</code>を作って<code>delegate</code>を設定して、runすれば、delegateの <code>func session(_ session: NISession, didUpdate nearbyObjects: [NINearbyObject])</code>で周囲の端末の位置情報が取得できるようになっています。</p> <p>実行時にはユーザーのpermissionを取得するようになっています。(ただし、現状ではinfo.plistへの記述は必要ないようです。) 双方の端末でユーザーのpermissionを得ていないとsessionが張れません。</p> <p><figure class="figure-image figure-image-fotolife" title="run a session"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200624/20200624052137.png" alt="f:id:toyship:20200624052137p:plain" title="f:id:toyship:20200624052137p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>run a session</figcaption></figure></p> <p>このフレームワークでは、周囲の端末の位置検知のみだけができるので、たとえばその端末と通信などをする場合には別の方法が必要です。</p> <p>上にあげたサンプルコードの場合、<code>Multipeer Connectivity</code>をつかって、周囲の特定の1台の端末とconnectionを張り、そのあと<code>Multipeer Connectivityのsession</code>を使って<code>NearbyInteraction session</code>用のtokenを送る、というまわりくどいことをしています。</p> <p>あと、NearbyInteractionでは、周囲の複数の端末の位置情報がとれるんですが、現状ではその端末の位置情報以外の情報が全くとれないんですよね……。 0.8mと1.2mのところに端末があるんだけど、それが<code>iPhone 11</code>か<code>iPhone 11 Pro</code>かもわからないし、誰の端末なのかもわからない、という微妙な状況です。</p> <p>(これについては、今Developer Forumに質問をしているので、結果がわかったら追記します。)</p> <p>追記:どうやって端末を特定するの?と聞いたら、NINearbyObjectsのdiscoveryTokenを使ってくれ、とのことでした。 こんな感じ(↓)ですね。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">NISession</span>, didUpdate nearbyObjects<span class="synSpecial">:</span> <span class="synPreProc">[NINearbyObject]</span>) { <span class="synPreProc">let</span> <span class="synIdentifier">myToken</span> <span class="synIdentifier">=</span> session.discoveryToken <span class="synComment">// 自分のToken</span> <span class="synStatement">for</span> obj <span class="synStatement">in</span> nearbyObjects{ <span class="synPreProc">let</span> <span class="synIdentifier">foundPeerTokne</span> <span class="synIdentifier">=</span> obj.discoveryToken <span class="synComment">// 見つけた端末のToken</span> } } </pre> <p>取得した<code>NINearbyObject</code>の<code>discoveryToken</code>を取得すれば、一意なIDをとることができます。</p> <p>自分の<code>Token</code>は自分の持っている<code>session</code>の<code>discoveryToken</code>なので、この<code>Token</code>を<code>MultipeerConnectivity</code>などで交換して、どの<code>Token</code>がどの端末かを確認できますね。</p> <h2>まとめ</h2> <ul> <li>NearbyInteractionは周囲の端末の位置と角度を高性能で検知できる。</li> <li>使える端末が限定される。</li> <li>位置推定時の端末の姿勢に制限がある。</li> <li>NearbyInteractionでデータが送信できるわけではないので、データ送信用の通信路は別途用意する必要がある。</li> </ul> <p>まだβなので、使い所も実装もいろいろと微妙な部分が多いですね……。今後の発展に期待です。</p> <h2>参考</h2> <ul> <li><a href="https://developer.apple.com/documentation/nearbyinteraction">https://developer.apple.com/documentation/nearbyinteraction</a></li> <li><a href="https://developer.apple.com/documentation/nearbyinteraction/initiating_and_maintaining_a_session">https://developer.apple.com/documentation/nearbyinteraction/initiating_and_maintaining_a_session</a></li> <li><a href="https://developer.apple.com/documentation/nearbyinteraction/implementing_interactions_between_users_in_close_proximity">https://developer.apple.com/documentation/nearbyinteraction/implementing_interactions_between_users_in_close_proximity</a></li> </ul> toyship SwiftでAudio Queue Recorderを書きました。 hatenablog://entry/26006613561250832 2020-05-04T09:51:35+09:00 2020-05-04T09:51:35+09:00 ちょっと思い立って、Audio Queueを使ったAudio録音ができるコードをかいてみました。 github.com 基本的にはAppleのだしているhttps://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Introduction/Introduction.htmlを参考にしたんですが、このドキュメントは最終更新日が2013年で、サンプルコードは当然 Objective-Cで書かれています。 Audio QueueはiOS2.0から使わ… <p>ちょっと思い立って、Audio Queueを使ったAudio録音ができるコードをかいてみました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FTachibanaKaoru%2FAudioQueueRecorder" title="TachibanaKaoru/AudioQueueRecorder" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/TachibanaKaoru/AudioQueueRecorder">github.com</a></cite></p> <p>基本的にはAppleのだしている<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Introduction/Introduction.html">https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Introduction/Introduction.html</a>を参考にしたんですが、このドキュメントは最終更新日が2013年で、サンプルコードは当然 Objective-Cで書かれています。</p> <p>Audio QueueはiOS2.0から使われているとても古いフレームワークですし、いろいろとつらみも多く、思ったより手間取ってしまいました。</p> <h2>使い方</h2> <p>録音用のクラスを生成し、<code>prepare</code>などをよんでバッファー作成などをしておきます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> audioRecorder <span class="synIdentifier">=</span> AudioRecorder() audioRecorder?.prepare() audioRecorder?.prepareQueue() audioRecorder?.setupBuffer() </pre> <p>録音を開始する時にはこちらをよんでください。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> audioRecorder?.startRecord() </pre> <p>終了する時にはこちら。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> audioRecorder?.stopRecord() </pre> <h2>録音データ</h2> <p>Documentフォルダにファイルを保存します。</p> <p>録音フォーマットは <code>aiff</code> ですが、このコードの中のこのあたりで変更できます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">documentDirectories</span> <span class="synIdentifier">=</span> FileManager.<span class="synStatement">default</span>.urls( <span class="synStatement">for</span><span class="synSpecial">:</span> <span class="synType">FileManager.SearchPathDirectory.documentDirectory</span>, <span class="synStatement">in</span><span class="synSpecial">:</span> <span class="synType">FileManager.SearchPathDomainMask.userDomainMask</span>) <span class="synPreProc">let</span> <span class="synIdentifier">docDirectory</span> <span class="synIdentifier">=</span> (documentDirectories.first)<span class="synIdentifier">!</span> <span class="synPreProc">var</span> <span class="synIdentifier">audioFilePathURL</span> <span class="synIdentifier">=</span> docDirectory.appendingPathComponent(<span class="synConstant">&quot;audiotest&quot;</span>) audioFilePathURL.appendPathExtension(<span class="synConstant">&quot;aiff&quot;</span>) </pre> <p>また、サンプリングレートなどはこのあたりで変えてみてください。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">var</span> <span class="synIdentifier">currentMusicDataFormat</span> <span class="synIdentifier">=</span> AudioStreamBasicDescription( mSampleRate<span class="synSpecial">:</span> <span class="synConstant">44100</span>, mFormatID<span class="synSpecial">:</span> <span class="synType">kAudioFormatLinearPCM</span>, mFormatFlags<span class="synSpecial">:</span> <span class="synType">AudioFormatFlags</span>(kLinearPCMFormatFlagIsBigEndian<span class="synIdentifier">|</span>kLinearPCMFormatFlagIsSignedInteger<span class="synIdentifier">|</span>kLinearPCMFormatFlagIsPacked), mBytesPerPacket<span class="synSpecial">:</span> <span class="synConstant">4</span>, mFramesPerPacket<span class="synSpecial">:</span> <span class="synConstant">1</span>, mBytesPerFrame<span class="synSpecial">:</span> <span class="synConstant">4</span>, mChannelsPerFrame<span class="synSpecial">:</span> <span class="synConstant">2</span>, mBitsPerChannel<span class="synSpecial">:</span> <span class="synConstant">16</span>, mReserved<span class="synSpecial">:</span> <span class="synConstant">0</span>) </pre> <p>iOSむけにかきましたが、おそらくMacでも動くんじゃないかと思います。</p> <h2>いろいろ</h2> <p>つまったポイントとしては、 <code>AudioQueueInputCallback</code> にわたされる<code>inUserData :Optional&lt;UnsafeMutableRawPointer&gt;</code>を、bindMemoryでもとのクラスにcastしようとしたら、うまくいかなかったことでした。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">userData1</span><span class="synSpecial">:</span> <span class="synType">UnsafeMutablePointer</span><span class="synSpecial">&lt;RecorderState&gt;</span>? <span class="synIdentifier">=</span> inUserData?.bindMemory(to<span class="synSpecial">:</span> <span class="synType">RecorderState.self</span>, capacity<span class="synSpecial">:</span> <span class="synType">MemoryLayout</span><span class="synSpecial">&lt;RecorderState&gt;</span>.size) </pre> <p>念のため、</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">userData1</span><span class="synSpecial">:</span> <span class="synType">UnsafeMutablePointer</span><span class="synSpecial">&lt;RecorderState&gt;</span>? <span class="synIdentifier">=</span> inUserData?.bindMemory(to<span class="synSpecial">:</span> <span class="synType">RecorderState.self</span>, capacity<span class="synSpecial">:</span> <span class="synType">MemoryLayout</span><span class="synSpecial">&lt;RecorderState&gt;</span>.stride) </pre> <p>にしてみてもうまく動かず。</p> <p>最終的には、こんな形でcastしています。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">unManagedUserData</span> <span class="synIdentifier">=</span> Unmanaged<span class="synIdentifier">&lt;</span>RecorderState<span class="synIdentifier">&gt;</span>.fromOpaque(inUserData<span class="synIdentifier">!</span>) <span class="synPreProc">let</span> <span class="synIdentifier">receivedUserData</span> <span class="synIdentifier">=</span> unManagedUserData.takeUnretainedValue() </pre> <p>castには手間取りましたが、AudioQueueは予想していたより楽に使えますね。 簡単に録音ができるとアプリの機能もいろいろと豪華にできそうです。</p> toyship ARKit3.5で周囲の物の位置が正確にわかるようになりました。 hatenablog://entry/26006613542604995 2020-03-30T15:09:24+09:00 2020-03-30T15:09:24+09:00 AppleがARKitを発表してからもう2年以上たちました。 今回 iPad Proの発売にあわせてリリースされたARKit3.5では、また新しい進化がありました。 なにができるようになったのかみていきましょう。 Scene Geometory ARKit3.5では、新しい iPad Proに搭載された LiDARスキャナーを利用して、周囲の物体の正確な位置測定と種類判別を行えるようになりました。 この機能はLiDARスキャナー搭載デバイス(2020年3月時点では iPad Pro 11 inch/12,0 inchのみ)でしか使えないので、現時点では利用範囲が限定されますが、今までの位置判定… <p>AppleがARKitを発表してからもう2年以上たちました。 今回 iPad Proの発売にあわせてリリースされたARKit3.5では、また新しい進化がありました。 なにができるようになったのかみていきましょう。</p> <h2>Scene Geometory</h2> <p>ARKit3.5では、新しい iPad Proに搭載された LiDARスキャナーを利用して、周囲の物体の正確な位置測定と種類判別を行えるようになりました。 この機能はLiDARスキャナー搭載デバイス(2020年3月時点では iPad Pro 11 inch/12,0 inchのみ)でしか使えないので、現時点では利用範囲が限定されますが、今までの位置判定より格段に正確で、とても強力な機能です。</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/u7ZAMLQw2HY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <p>この動画でわかるように、周囲の物体を動的にメッシュとして認識し、それらの種類を判別できるようになります。</p> <p>ARKitは、複数のレンダリング方法をサポートしていますが、この機能はRealityKitでレンダリングをする場合だけ利用可能です。</p> <p>RealityKitでレンダリングをするARプロジェクトをつくり、<code>ARConfiguration</code>の<code>sceneReconstruction</code>を設定すると、自動的にメッシュ作成・判別が行われます。(<code>.mesh</code>を指定した場合には、メッシュのみの判定、<code>.meshWithClassification</code>を指定した場合にメッシュに加えて物体の種類判定も行われます。)</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">configuration</span> <span class="synIdentifier">=</span> ARWorldTrackingConfiguration() configuration.sceneReconstruction <span class="synIdentifier">=</span> .meshWithClassification </pre> <p>上の動画のようにメッシュを表示するには、ARViewのDebugOptionを設定します。 (ただし、このメッシュ表示はデバッグ用で、ユーザーに見える場所で表示するのは望ましくないようです。)</p> <pre class="code lang-swift" data-lang="swift" data-unlink>arView.debugOptions.insert(.showSceneUnderstanding) </pre> <p>メッシュを表現するクラスは、<code>ARAnchor</code>の拡張クラス、<code>ARMeshAnchor</code>です。 <code>ARView</code>の<code>session</code>から、今までに使っていた <code>ARAnchor</code>も含んだ形で取れます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink>arView.session.currentFrame.anchors </pre> <p>ARMeshAnchorは、位置情報の他に物体の種類(classification)情報を持っています。</p> <p>classificationはこの8種類。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">public</span> <span class="synPreProc">enum</span> <span class="synType">ARMeshClassification</span> <span class="synSpecial">:</span> <span class="synType">Int</span> { <span class="synStatement">case</span> none <span class="synIdentifier">=</span> <span class="synConstant">0</span> <span class="synComment">// なし</span> <span class="synStatement">case</span> wall <span class="synIdentifier">=</span> <span class="synConstant">1</span> <span class="synComment">// 壁</span> <span class="synStatement">case</span> floor <span class="synIdentifier">=</span> <span class="synConstant">2</span> <span class="synComment">// 床</span> <span class="synStatement">case</span> ceiling <span class="synIdentifier">=</span> <span class="synConstant">3</span> <span class="synComment">// 天井</span> <span class="synStatement">case</span> table <span class="synIdentifier">=</span> <span class="synConstant">4</span> <span class="synComment">// テーブル</span> <span class="synStatement">case</span> seat <span class="synIdentifier">=</span> <span class="synConstant">5</span> <span class="synComment">// 椅子</span> <span class="synStatement">case</span> window <span class="synIdentifier">=</span> <span class="synConstant">6</span> <span class="synComment">// 窓</span> <span class="synStatement">case</span> door <span class="synIdentifier">=</span> <span class="synConstant">7</span> <span class="synComment">// ドア</span> } </pre> <p>物質判定はかなり正確で、カーテンがついていても窓を認識してくれるし、テーブルや椅子も正確に認識されます。</p> <p><code>ARMeshAnchor</code>の一つ一つが「壁」とか「窓」と判定されるわけではありません。 <code>ARMeshAnchor</code>は複数の「面」をもちますが、それぞれの面に対して種類判定が行われます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">anchor</span><span class="synSpecial">:</span> <span class="synType">ARMeshAnchor</span> <span class="synStatement">for</span> face <span class="synStatement">in</span> anchor.geometry.faces{ <span class="synPreProc">let</span> <span class="synIdentifier">classification</span><span class="synSpecial">:</span> <span class="synType">ARMeshClassification</span> <span class="synIdentifier">=</span> anchor.geometry.classificationOf(faceWithIndex<span class="synSpecial">:</span> <span class="synType">index</span>) } </pre> <p>Scene Geometoryについては、Appleが提供しているこちらのサンプルを見ると参考になります。</p> <p><a href="https://developer.apple.com/documentation/arkit/world_tracking/visualizing_and_interacting_with_a_reconstructed_scene">https://developer.apple.com/documentation/arkit/world_tracking/visualizing_and_interacting_with_a_reconstructed_scene</a></p> <h2>LiDARスキャナーの精度</h2> <p>さて、今回のLiDARスキャナーの精度はどのくらいでしょうか。</p> <p>iPad Pro 11 inchで確認してみたところ、このくらいの数値でした。</p> <ul> <li>距離判定精度 : 約 1~2cm</li> <li>到達距離 : 約 3~4m</li> </ul> <p>(私が個人的に測定したものなので、正確な測定ではありませんし、対象物などによっても異なると思うので参考程度の値です。)</p> <p><figure class="figure-image figure-image-fotolife" title="測定"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20200329/20200329205617.jpg" alt="f:id:toyship:20200329205617j:plain" title="f:id:toyship:20200329205617j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>床にメジャーをおいて測定</figcaption></figure></p> <h2>端末間のCompatibility確認</h2> <p>ARKitもかなりバージョンがあがってきたので、端末間の連携アプリを作る際などに、それぞれの端末のARKitのバージョンも気にしなければいけなくなってきました。</p> <p>ARKitはiOSに含まれているので、iOSのシステムバージョンを確認すればARKitのバージョンも類推できますが、少し面倒ですよね。 端末間のARKitの互換性確認のために <code>NetworkCompatibilityToken</code> が使えるようになりました。</p> <p>自分のトークンを <code>NetworkCompatibilityToken.local</code> で取得し、他のデバイスのトークンと比較すると、<code>Compatibility. compatible</code> か<code>Compatibility . sessionProtocolVersionMismatch</code> が得られます。 <code>Compatibility . sessionProtocolVersionMismatch</code> の場合には連携処理をしないなどの対応をすれば万全です。</p> <p>なお、他のデバイスのトークンを取得する方法はデフォルトで用意されていません。 例えば <code>MultipeerConnectivity</code>を使うときには、<code>MCNearbyServiceAdvertiser</code>の<code>discoveryInfo</code>などにいれるなどの処理をしておきましょう。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">let</span> <span class="synIdentifier">localToken</span> <span class="synIdentifier">=</span> NetworkCompatibilityToken.local <span class="synPreProc">let</span> <span class="synIdentifier">anotherToken</span> <span class="synIdentifier">=</span> <span class="synIdentifier">...</span> <span class="synComment">// 他のデバイスのトークン</span> <span class="synPreProc">let</span> <span class="synIdentifier">compatibility</span> <span class="synIdentifier">=</span> localToken.compatibilityWith(anotherToken) <span class="synStatement">switch</span> compatibility { <span class="synStatement">case</span> .compatible<span class="synSpecial">:</span> <span class="synComment">// 処理を続ける</span> <span class="synStatement">case</span> .sessionProtocolVersionMismatch<span class="synSpecial">:</span> <span class="synComment">// OSのバージョンアップをしてください、などのアラートを表示する。</span> } </pre> toyship 2018年行ってよかった海外リモートワーク先まとめ hatenablog://entry/10257846132693216482 2018-12-31T19:01:57+09:00 2019-01-01T23:00:53+09:00 今年は年初のバリに始まって8カ国で海外リモートワークをしました。 バリ(インドネシア)(バリ島で開発合宿 - Toyship.org) ギリシャ(サントリーニ)(サントリーニ島で花粉症退避の開発合宿をしてきました。 - Toyship.org) バンコク フィンランド スウェーデン ノルウェー バルセロナ アイスランド (まだ全部はブログの記事にしていなんですが、お正月休みのうちに書こうと思っています……。) なぜ海外リモートワークなのか 仕事が忙しい。でも仕事は好きだし、別に休暇をとりたいわけじゃない。でもずっと仕事ばかりしていると精神的にもよくないし……なら、「仕事しながら旅行すればいいん… <p>今年は年初のバリに始まって8カ国で海外リモートワークをしました。</p> <ul> <li>バリ(インドネシア)(<a href="https://www.toyship.org/2018/02/19/102035">&#x30D0;&#x30EA;&#x5CF6;&#x3067;&#x958B;&#x767A;&#x5408;&#x5BBF; - Toyship.org</a>)</li> <li>ギリシャ(サントリーニ)(<a href="https://www.toyship.org/2018/12/31/164856">&#x30B5;&#x30F3;&#x30C8;&#x30EA;&#x30FC;&#x30CB;&#x5CF6;&#x3067;&#x82B1;&#x7C89;&#x75C7;&#x9000;&#x907F;&#x306E;&#x958B;&#x767A;&#x5408;&#x5BBF;&#x3092;&#x3057;&#x3066;&#x304D;&#x307E;&#x3057;&#x305F;&#x3002; - Toyship.org</a>)</li> <li>バンコク</li> <li>フィンランド</li> <li>スウェーデン</li> <li>ノルウェー</li> <li>バルセロナ</li> <li>アイスランド</li> </ul> <p>(まだ全部はブログの記事にしていなんですが、お正月休みのうちに書こうと思っています……。)</p> <h2>なぜ海外リモートワークなのか</h2> <p>仕事が忙しい。でも仕事は好きだし、別に休暇をとりたいわけじゃない。でもずっと仕事ばかりしていると精神的にもよくないし……なら、「仕事しながら旅行すればいいんじゃん!」ということで始めました。</p> <p>海外リモートワークをはじめてみると、短期間でも新しい場所で生活することで精神的によい刺激をうけることがわかり、全体的な仕事の能率はあがりました。</p> <p>また、荷物などを最適化したり仕事のきりをまめにつけるようにしたりなどの新しい習慣も、いい方向にはたらいているようです。</p> <p>もちろんその分出費もかさんでいるので、トータルで見ると利益をうんでいるというわけではないんですが、長い目で見て自分への投資にもなるんじゃないかなと思っています。</p> <h2>海外リモートワークの利点</h2> <p>私はだいたい10日から2週間くらいかけて一回のリモートワークをしています。</p> <p>その場所に2、3日いるだけでは、街の普段の様子は定常的にわからないですよね。</p> <p>積極的に地域コミュニティに参加するというわけではないんだけど、同じ場所に一週間いると、バスやお店での独特の作法にも慣れて、お客様ではなくそこに住んでいる住人への入り口にたつことができる気がします。</p> <p>他の国の社会制度、法制度、歴史、習慣などは、知識として知ってはいても、現地で実感としてうけとめると理解が深まります。 また、それについてもっと知りたい知識欲もでてきます。</p> <p>食事の仕方、移動の仕方、街での過ごし方の意識も少しずつ変わってきます。</p> <h2>仕事環境</h2> <p>一口に海外リモートワークといっても、どこで作業をするかによって作業効率が全然違います。</p> <h3>ホテルの自室</h3> <p>ネットワークと電源の確保が 一番確実ですが、 部屋によっては、作業ができる大きさの机がないこともあります。 私はbooking.comで部屋を予約することが多いんですが、予約前に部屋の広さと作業スペースは確認するようにしていました。 ただ、もし部屋で作業をするのが快適でも、ずっと部屋だけにこもっていると海外に行く意味が薄くなってしまうので、別の作業環境と並列にしたほうがいいと思います。</p> <p><figure class="figure-image figure-image-fotolife" title="Nordurey Hostel at Reykjavik"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181211/20181211110914.jpg" alt="f:id:toyship:20181211110914j:plain:w300" title="f:id:toyship:20181211110914j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Nordurey Hostel at Reykjavik</figcaption></figure></p> <h3>ホテルのロビーや共有スペース</h3> <p>これも、ホテルによると思います。 ロビー自体は用意されていても非常にシンプルで作業スペースがなかったり、宿泊者が自由に使える場所ではなかったりすることもあります。</p> <p>また用意されていても、人通りが多くて作業場所としてはちょっと……という場合もあります。</p> <p>ロビーの有無や広さは事前に確認することもできますが、電源が使えるかどうかとか、雰囲気などは、実際に行ってみたいとなかなかわからないものなので、あまりあてにしないほうがいいかもしれません。</p> <p><figure class="figure-image figure-image-fotolife" title="Casa Gracia at Barcelona"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181101/20181101103024.jpg" alt="f:id:toyship:20181101103024j:plain:w300" title="f:id:toyship:20181101103024j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Casa Gracia at Barcelona</figcaption></figure> <figure class="figure-image figure-image-fotolife" title="Casa Gracia at Barcelona"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181031/20181031155039.jpg" alt="f:id:toyship:20181031155039j:plain:w300" title="f:id:toyship:20181031155039j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Casa Gracia at Barcelona</figcaption></figure></p> <p>今までの経験では、ドミトリーの宿の共有スペースは、大きい机と電源が完備されていて、作業がとてもしやすいことが多かったと思います。</p> <p><figure class="figure-image figure-image-fotolife" title="Rodamon Barcelona Hostel"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181104/20181104143313.jpg" alt="f:id:toyship:20181104143313j:plain:w300" title="f:id:toyship:20181104143313j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Rodamon Barcelona Hostel</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Rodamon Barcelona Hostel"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181104/20181104145207.jpg" alt="f:id:toyship:20181104145207j:plain:w300" title="f:id:toyship:20181104145207j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Rodamon Barcelona Hostel</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="TOC Hostel Barcelona"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181031/20181031094923.jpg" alt="f:id:toyship:20181031094923j:plain:w300" title="f:id:toyship:20181031094923j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>TOC Hostel Barcelona</figcaption></figure></p> <h3>街角のカフェ</h3> <p>その街のカフェに行って作業をするのは、街の雰囲気も感じられるし、海外リモートワークの醍醐味だと思います。</p> <p>大きめの街では、電源のある新しいカフェなどもあり、いいカフェを見つけられると仕事もすすみます。</p> <p>ただ、カフェではトイレに行きたい時に荷物をどうするかなどのセキュリティの問題もあるし、1〜2時間ごとにドリンクの追加注文をするとコストがわりとかかってしまうというのもあります。 あと、田舎ではカフェ自体がない場所もありますし……。</p> <p><figure class="figure-image figure-image-fotolife" title="Cafe at Barcelona"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181102/20181102115026.jpg" alt="f:id:toyship:20181102115026j:plain:w300" title="f:id:toyship:20181102115026j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Cafe at Barcelona</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Cafe at Bali"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180202/20180202151038.jpg" alt="f:id:toyship:20180202151038j:plain:w300" title="f:id:toyship:20180202151038j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Cafe at Bali</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Reykjavik Airport"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181202/20181202113807.jpg" alt="f:id:toyship:20181202113807j:plain:w300" title="f:id:toyship:20181202113807j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Reykjavik Airport</figcaption></figure></p> <h3>コワーキングスペース</h3> <p>コワーキングスペースがあると、電源、ネットワーク、広い作業スペースを比較的確保しやすいです。 でも、街によってはコワーキングスペースが全くないところもあります。 あった場合にも、ドロップインの使用を受け付けていない場合もあるので、その街に行く前に事前に確認しておきましょう。</p> <p><figure class="figure-image figure-image-fotolife" title="バルセロナ MOB"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181030/20181030122335.jpg" alt="f:id:toyship:20181030122335j:plain:w300" title="f:id:toyship:20181030122335j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>バルセロナ MOB</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="バンコク"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180504/20180504141112.jpg" alt="f:id:toyship:20180504141112j:plain:w300" title="f:id:toyship:20180504141112j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>バンコク AIS D.C.</figcaption></figure></p> <h3>よい作業場所</h3> <p>旅をしながら仕事をするのであれば、絶対にここで仕事する!と決めるのではなく、その時の自分の気分や体調、街の天気や店の様子などで臨機応変に決めていくのが効率的だし、それが一番よい方法かなと思います。</p> <p>そのためには、作業場所の候補地を複数用意しておいて、状況に応じて臨機応変に選んでいきましょう。</p> <p>あと、自分が作業しやすい場所があるなら、それによって滞在先を決めるのもよいと思います。 コワーキングスペースで作業したいなら大都市、カフェで作業したいなら中都市以上を選ばないと難しいですね。</p> <p>また、田舎でゆっくり作業したいのであれば、ホテル自体に作業スペースがあるか、作業スペースがとれそうな大きめの部屋にとまるかにしましょう。</p> <p>また、最近は海外で活動している日本人グループのみなさんもいるので、積極的に頼っていくとよいと思います。 私もバンコクに行った時にガオガオさんのところにとめていただいて、作業環境を整えたり、情報を教えていただいたり、とても助かりました。</p> <p><a href="https://gaogao.asia/">GAOGAO (&#x30AC;&#x30AA;&#x30AC;&#x30AA;) - &#x6D77;&#x5916;&#x3067;&#x50CD;&#x304F;&#x30FB;&#x50CD;&#x304D;&#x305F;&#x3044;&#x30AF;&#x30EA;&#x30A8;&#x30A4;&#x30BF;&#x30FC;(&#x30A8;&#x30F3;&#x30B8;&#x30CB;&#x30A2;&#x30FB;&#x30C7;&#x30B6;&#x30A4;&#x30CA;&#x30FC;&#x30FB;&#x30C7;&#x30A3;&#x30EC;&#x30AF;&#x30BF;&#x30FC;)&#x3092;&#x652F;&#x63F4;&#x3059;&#x308B;&#x30DE;&#x30AC;&#x30B8;&#x30F3;</a></p> <p><figure class="figure-image figure-image-fotolife" title="Montserrat"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181103/20181103141021.jpg" alt="f:id:toyship:20181103141021j:plain:w300" title="f:id:toyship:20181103141021j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>Montserrat</figcaption></figure></p> <h2>コスト</h2> <p>海外リモートワーク、いいとは思うけどやっぱりコストかかるよね……とみなさん思われると思います。</p> <p>でも、実は調べてみると結構思ったほどではないです。</p> <p>たとえば、今格安航空券をぐぐってみたら、2019年1月のバルセロナ往復は29800円から。バンコクは22000円から。バリ(デンパサール)は42000円からでした。</p> <p>値段を安くしたいのであれば、ドミトリーに宿泊するのもおすすめです。</p> <p>先日バルセロナに行った時にいくつかドミトリーにとまったのですが、一泊1500円から5000円。 部屋は女性と男性がわけられているのでセキュリティも安心だし、予想していたよりずっと清潔でした。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181102/20181102154724.jpg" alt="f:id:toyship:20181102154724j:plain:w300" title="f:id:toyship:20181102154724j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>もちろんホテルの個室よりは気を使うので、人によっては難しいと思いますが、一度とまって確かめてみてもいいと思います。</p> <p>バルセロナに格安航空券で行って10日すごす場合。 3500円のところにとまって、食事代を1日1500円とすると、往復の航空券も入れて10日間で78900円です。</p> <p>10日間で8万円だったら、なんとか出せる気がしませんか?</p> <p>バンコクでも、一泊1000円もだせばドミトリーに泊まれます。バンコクの食費は1日1000円でもよさそうなので、同じく10日間で、42000円。</p> <p>10日間で4万円ちょっと。なんとかなりそうです。</p> <p>値段を安くしたいのであれば、食費がやすいところにいくとよいでしょう。</p> <p>アジアは一般的に安いですし、バルセロナはヨーロッパの中では格段に安いです。 (おまけにどちらもご飯が美味しい。)</p> <p>北欧(フィンランド、アイスランド)あたりは食費が高めなので、コストを追求するなら避けたほうがいいですね。</p> <h2>デメリット</h2> <p>もちろん、海外リモートワークにもデメリットがあります。</p> <p>まず、ネットワーク環境も含め、仕事環境を準備するのに一定の手間がかかること。</p> <p>また、一緒に仕事をしている方々のうちあわせのリクエストに答えられないことがあること。 明日渋谷でうちあわせがあるんですがどうでしょう?という場合にはスケジュール変更をお願いするしかありません。</p> <p>滞在先によっては、移動時間が多く取られてしまい、作業時間が少なくなってしまうこともあります。</p> <p>あと、海外で体調を悪くしてしまった時には、健康面・経済面からリスクは大きいです。</p> <p>いろいろとリスクはありますが、事前に調査したり計画や対策をたてたりして、これらのリスクを避けることが重要です。</p> <h2>おすすめ海外リモートワーク先</h2> <p>海外リモートワークをしていると、オススメをよくきかれるので、自分の今年いったなかからあげてみます。 (私が今年行った中からオススメするのでかなり限定的です……。)</p> <h3>コストを下げたい方</h3> <p>バンコクは食事が美味しくて安いです。バルセロナも食事が美味しくて安い。どちらもとっても活気がある街で、食事の重要性を実感します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20170708/20170708091932.jpg" alt="f:id:toyship:20170708091932j:plain:w300" title="f:id:toyship:20170708091932j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <h3>海外リモートワークが初めての人</h3> <p>なんといってもバンコクは日本人が多く、日本人会もあったりするので、なにかあったときにアドバイスをもらえる可能性が高いです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180512/20180512161652.jpg" alt="f:id:toyship:20180512161652j:plain:w300" title="f:id:toyship:20180512161652j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <h3>人里離れた場所でゆっくり仕事をしたい方</h3> <p>アイスランドは雄大な自然の中に人が点々と住んでいる感じの国なので、ゆっくり仕事をしたいときには最適です。冬季は雪に埋もれないように注意。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181207/20181207130905.jpg" alt="f:id:toyship:20181207130905j:plain:w300" title="f:id:toyship:20181207130905j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <h3>自然のなかでゆっくり仕事をしたい方</h3> <p>バリのウブド周辺は、緑が鬱蒼と濃く、とても気持ちがいいです。仕事の合間にバリダンスなどをみても楽しいですね。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180201/20180201190842.jpg" alt="f:id:toyship:20180201190842j:plain:w300" title="f:id:toyship:20180201190842j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <h3>美味しいものを食べながら美しいものをみたい方</h3> <p>バルセロナは食事も美味しくて安いし、街中にこんなものがゴロゴロしているので、疲れた時にすぐ美しいものを鑑賞できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181101/20181101162036.jpg" alt="f:id:toyship:20181101162036j:plain:w300" title="f:id:toyship:20181101162036j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181030/20181030112716.jpg" alt="f:id:toyship:20181030112716j:plain:w300" title="f:id:toyship:20181030112716j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>来年は今年よりは海外リモートワークの頻度はさがりそうです。 おすすめのリモートワーク先がある方はぜひ教えてください。</p> toyship サントリーニ島で花粉症退避の開発合宿をしてきました。 hatenablog://entry/10257846132663133587 2018-12-31T16:48:56+09:00 2018-12-31T16:51:12+09:00 今年の3月、サントリーニ島で花粉症退避のための開発合宿をしてきました。 私は花粉症の症状がひどく、3月の中旬には1日にボックスティッシュを一箱使ってしまうくらいの流量になっています。 当然仕事などできるわけもなく、出社してもいつもぼーっとしながら仕事をしていました。 今担当している仕事はリモートワークでやらせていただいているので、せっかくだから仕事の能率をあげるために花粉症退避の旅行をすることにしました。 長期間行くので、少し遠目の場所ということで行ったことのないサントリーニ島へ。 サントリーニといえば、青い空!白い家!ラブ!バカンス!ウェーイ!って印象だったんですが、予想よりずっと奥が深い島… <p>今年の3月、サントリーニ島で花粉症退避のための開発合宿をしてきました。</p> <p>私は花粉症の症状がひどく、3月の中旬には1日にボックスティッシュを一箱使ってしまうくらいの流量になっています。</p> <p>当然仕事などできるわけもなく、出社してもいつもぼーっとしながら仕事をしていました。</p> <p>今担当している仕事はリモートワークでやらせていただいているので、せっかくだから仕事の能率をあげるために花粉症退避の旅行をすることにしました。 長期間行くので、少し遠目の場所ということで行ったことのないサントリーニ島へ。</p> <p>サントリーニといえば、青い空!白い家!ラブ!バカンス!ウェーイ!って印象だったんですが、予想よりずっと奥が深い島でした。</p> <h2>サントリーニ島に行くには</h2> <p>サントリーニ島に行くには、アテネ経由でいくのが一般的な方法です。</p> <p>飛行機でもいけるんですが、せっかく島に行くんだから、今回はフェリーにしてみました。</p> <p>アテネの近くにピレウスという街があり、そこの港からエーゲ海の島々へのフェリーがでています。 夏の間は高速船などもありますが、冬はフェリーの本数なども少なめです。</p> <p>フェリーはかなり大きめでした。もちろん車なども運ぶことができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180312/20180312063835.jpg" alt="f:id:toyship:20180312063835j:plain:w300" title="f:id:toyship:20180312063835j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>私は席争いを避けたかったので、ビジネスクラスにしましたが、ノーマルクラスはこんな感じです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180312/20180312064249.jpg" alt="f:id:toyship:20180312064249j:plain:w300" title="f:id:toyship:20180312064249j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>ビジネルクラスはこんな感じです。ビジネスクラスといっても指定席ではなく、席自体はノーマルクラスとそれほど違いはないんですが、ビジネスクラスの方が席が豊富にあって、争わずに窓際に座ることができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180312/20180312064415.jpg" alt="f:id:toyship:20180312064415j:plain:w300" title="f:id:toyship:20180312064415j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>窓際席で電源も確保できたので、かなり快適な船旅ができました。 さすがにネットワーク環境はなかったので、そこは我慢。(有料サービスはありました) <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180312/20180312084920.jpg" alt="f:id:toyship:20180312084920j:plain:w300" title="f:id:toyship:20180312084920j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>ピレウス港を出て、いくつかの港を経由して、7時間ほどでサントリーニ島につきました。</p> <h2>イアの街</h2> <p>サントリーニ島にはいくつか集落があるんですが、一番リゾート感がある、イアという場所のホテルにとまることにしました。 こんな感じのいかにもサントリーニ島っぽい風景の場所です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180316/20180316142613.jpg" alt="f:id:toyship:20180316142613j:plain:w300" title="f:id:toyship:20180316142613j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>イアでは世界で一番美しい夕日がみられるんだそうです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180317/20180317181720.jpg" alt="f:id:toyship:20180317181720j:plain:w300" title="f:id:toyship:20180317181720j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>あと、写真撮影中のハネムーンカップルの方がいたりもしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180317/20180317175609.jpg" alt="f:id:toyship:20180317175609j:plain:w300" title="f:id:toyship:20180317175609j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180317/20180317180405.jpg" alt="f:id:toyship:20180317180405j:plain:w300" title="f:id:toyship:20180317180405j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>周囲のみんなが長袖の上着を着ているなか、ノースリーブの綺麗なウェディングドレスに身を包んで30分以上も撮影に挑む新郎新婦をみて、美しいウェディング写真の後ろに隠された根性をみた思いがしました。愛ってすごい。</p> <h2>ホテル</h2> <p>サントリーニには、断崖にほった洞窟が部屋になっている洞窟ホテルというものがあります。 断崖にあるので、眺めは素晴らしいです。</p> <p>壁は白が一般的で、洞窟なので窓はなく、ちょっと隠れ家っぽい感じです。</p> <p>私の止まったホテルはこんな感じでした。 リビングには長いソファ。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180312/20180312160913.jpg" alt="f:id:toyship:20180312160913j:plain:w300" title="f:id:toyship:20180312160913j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>あと、作業ができる小さめのダイニングテーブル。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180312/20180312160917.jpg" alt="f:id:toyship:20180312160917j:plain:w300" title="f:id:toyship:20180312160917j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>寝室はちょっと奥まっていましたが居心地がいい部屋でした。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180312/20180312160806.jpg" alt="f:id:toyship:20180312160806j:plain:w300" title="f:id:toyship:20180312160806j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>部屋の前に屋外風呂があって、いつでもエーゲ海をみながら露天風呂に入れました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180312/20180312161102.jpg" alt="f:id:toyship:20180312161102j:plain:w300" title="f:id:toyship:20180312161102j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180317/20180317104720.jpg" alt="f:id:toyship:20180317104720j:plain:w300" title="f:id:toyship:20180317104720j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <h2>サントリーニの1日</h2> <p>毎日こんな感じですごしていました。</p> <p>毎朝6時、まだ暗いうちに起床。 コーヒーを入れて、エーゲ海を見渡すベンチでコーヒーを飲みながら仕事を始めます。</p> <p>少しずつ空が明るくなってきます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180314/20180314060942.jpg" alt="f:id:toyship:20180314060942j:plain:w300" title="f:id:toyship:20180314060942j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>雲が色づいてきます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180314/20180314061655.jpg" alt="f:id:toyship:20180314061655j:plain:w300" title="f:id:toyship:20180314061655j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>雲が朝焼けで真っ赤に染まります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180314/20180314062021.jpg" alt="f:id:toyship:20180314062021j:plain:w300" title="f:id:toyship:20180314062021j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>これは別の日の朝日。エーゲ海にのぼる朝日と雲は、滞在したどの日も毎日違う表情をみせてくれました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180317/20180317061642.jpg" alt="f:id:toyship:20180317061642j:plain:w300" title="f:id:toyship:20180317061642j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>これも別の日。雲が少ないと、また違う表情が見えました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180316/20180316061445.jpg" alt="f:id:toyship:20180316061445j:plain:w300" title="f:id:toyship:20180316061445j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>エーゲ海をみわたせるベンチがあったので、毎朝朝日を眺めながら仕事をしていました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180314/20180314063613.jpg" alt="f:id:toyship:20180314063613j:plain:w300" title="f:id:toyship:20180314063613j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>朝日を見た後は、朝ごはん。 部屋にキッチンがついていたので、自分で作っていました。</p> <p>近所のスーパーで、野菜やパンを買ってきて簡単に。 ギリシャヨーグルトにギリシャの特産のはちみつをかけてたっぷりと食べていました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180314/20180314083045.jpg" alt="f:id:toyship:20180314083045j:plain:w300" title="f:id:toyship:20180314083045j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>スーパーのヨーグルトの種類は多く、1リットル単位で売ってたので、たっぷり食べても問題ありません。 島なので、野菜の種類は少なかったです。 (島の中心地のフィラでは豊富でしたが、イアには小さいスーパーしかなく、品数は貧弱でした。)</p> <p>朝ごはんのあとは、部屋でお仕事。</p> <p>昼ごはんを食べに近所のカフェにいって、帰ってきてまた仕事。</p> <p>気が向いたらジュースを飲みながら露天風呂にはいったり。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180319/20180319180957.jpg" alt="f:id:toyship:20180319180957j:plain:w300" title="f:id:toyship:20180319180957j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>夜は部屋でお仕事。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180316/20180316211354.jpg" alt="f:id:toyship:20180316211354j:plain:w300" title="f:id:toyship:20180316211354j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>部屋から見える景色が素晴らしかったので、正直それほど出かける気にならず、毎日ほとんど自分の部屋で過ごし、</p> <p>休みの日は火山を見に行ったり、博物館や遺跡を見に行ったりしていました。</p> <h2>コストと時期</h2> <p>サントリーニ島というのは基本的にはリゾートなので、夏季の料金はとても高いです。 私がとまっていたホテルも、おそらく夏季だと一泊4〜5万円くらいすると思います。</p> <p>それに対して冬季(11月から3月)の料金は半額以下なので、かなりお得にとまることができます。</p> <p>もちろん繁忙期ではないので、いろいろと不便な点もありますが、コスト面ではとても助かります。</p> <p>また、サントリーニ島は人気があるリゾート地なので、夏季の混雑はかなり凄まじいものがあるようです。 特に人気のあるイア地域では、夏季は道を歩くのも難しいくらいの混雑だと言われました。</p> <p>私がいった3月は、夏に向けてホテルなどの新築工事が始まっていたので、ちょっと騒音がうるさかったりしました。 もう少し前(1月や2月)だと騒音の問題はないと思いますが、少し寒いかもしれません。</p> <p>あと、レストランやカフェは冬季は閉店している店も多く、選択肢が限られます。 私はそれほど困りませんでしたが、美味しいものをたくさん食べたい人やお目当の店がある人は注意した方がいいかもしれません。</p> <p>宿泊費は冬季と夏季でだいぶ差がありますが、食事代は時期によって変動はありません。 リゾート地だけあって、ギリシャ本土よりは高く、一食2000円前後くらいの感じでした。</p> <h2>サントリーニ島のいいところと悪いところ</h2> <p>まず、よかったところから。</p> <p>景観が素晴らしい。 部屋から見える景色も素晴らしかったんだけど、街全体が断崖の上にあるので、公園や道端から見える景色も素晴らしかったです。 自分の部屋からエーゲ海が見渡せるのは贅沢ですよね。</p> <p>地質学的におもしろい。 溶岩のなかを登る火山見物ができたり、断崖に見える地層も興味深い。詳しくはこちらを。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.toyship.org%2F2018%2F12%2F31%2F163916" title="サントリーニ島と火山 - Toyship.org" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.toyship.org/2018/12/31/163916">www.toyship.org</a></cite></p> <p>考古学的にすごい。 (これは別にブログ記事を書く予定です。)こんな小さな島に3700年前の遺跡があるなんて知りませんでした。そしてアトランティス。さすがエーゲ海文明。今度はクレタ島も行ってみたい。</p> <p>猫が多い。詳しくはこちらを。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.toyship.org%2F2018%2F12%2F31%2F163310" title="サントリーニ島の猫とロバ - Toyship.org" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.toyship.org/2018/12/31/163310">www.toyship.org</a></cite></p> <p>天気がいい。 これは季節にもよるだろうけど、滞在したあいだ、ほぼ晴れていて気持ちよく過ごせました。</p> <p>次は悪いところをあげてみましょう。</p> <p>ネットワーク環境が悪い。 事前に調べていなかったのがいけないんですが、そもそもサントリーニ島には海底ネットワークケーブルがきていないので、島全体のネットワーク環境が悪い。 それに加えて、洞窟タイプのホテルというのは、構造上どうしても電波の通りが悪くなってしまいます。 Githubのコミットくらいは大丈夫でしたが、ビデオ会議が何回も切れてしまい、関係各所にご迷惑をかけてしまいました。</p> <p>日本から遠い。 フェリーを使ったので、結局片道2日かかってしまいました。 飛行機という手もあるけど、それでもやっぱり気軽にくるのは難しいです……。</p> <h2>まとめ</h2> <p>結論としては、サントリーニ島はあまり開発合宿向きの場所ではないんだけど、旅行場所としてはとてもおすすめです。</p> <p>でも花粉症退避合宿は本当によいですね。例年生産性が50%以下になっている3月が、生産性120%くらいになりました。 来年の花粉の量も多いという話なので、来年も絶対花粉症退避をしようと思います。(どこにいくかは未定ですが……)</p> toyship サントリーニ島と火山 hatenablog://entry/10257846132663134717 2018-12-31T16:39:16+09:00 2018-12-31T16:50:16+09:00 今年の3月にサントリーニ島に開発合宿に行ってきた(サントリーニ島で花粉症退避の開発合宿をしてきました。 - Toyship.org)んですが、サントリーニ島は、地質学的にみてもとても興味深い島でした。 サントリーニ島の形 サントリーニ島はこんな形をしています。 三日月状の島のなかほどあたりに、フィラという島の中心都市があります。 私が滞在していたのはイアという上の方の街です。 実は、この三日月状のサントリーニ島は、火山のカルデラの外輪山にあたります。 中央にある島は、ネア・カメニ島とパレア・カメニ島。これが、かつての火山口でした。 この二つの島には今は誰も住んでおらず、観光地となっています。 … <p>今年の3月にサントリーニ島に開発合宿に行ってきた(<a href="https://www.toyship.org/2018/12/31/164856">&#x30B5;&#x30F3;&#x30C8;&#x30EA;&#x30FC;&#x30CB;&#x5CF6;&#x3067;&#x82B1;&#x7C89;&#x75C7;&#x9000;&#x907F;&#x306E;&#x958B;&#x767A;&#x5408;&#x5BBF;&#x3092;&#x3057;&#x3066;&#x304D;&#x307E;&#x3057;&#x305F;&#x3002; - Toyship.org</a>)んですが、サントリーニ島は、地質学的にみてもとても興味深い島でした。</p> <h2>サントリーニ島の形</h2> <p>サントリーニ島はこんな形をしています。 三日月状の島のなかほどあたりに、フィラという島の中心都市があります。 私が滞在していたのはイアという上の方の街です。</p> <p><figure class="figure-image figure-image-fotolife" title="サントリーニ島"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20181230/20181230232825.jpg" alt="f:id:toyship:20181230232825j:plain:w300" title="f:id:toyship:20181230232825j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>実は、この三日月状のサントリーニ島は、火山のカルデラの外輪山にあたります。</p> <p>中央にある島は、ネア・カメニ島とパレア・カメニ島。これが、かつての火山口でした。 この二つの島には今は誰も住んでおらず、観光地となっています。</p> <p>サントリーニ島は、歴史上何度も噴火を繰り返しています。</p> <p>一番古い噴火は紀元前1610年のミノア噴火です。(この時の噴火によって、サントリーニがアトランティスだったのではないか、という説もあるんです。)</p> <p>その後も何回か噴火を繰り返し、最新の噴火は1950年。この時にも死者はでています。</p> <p>現在では休火山になっており、おちついた状態です。</p> <h2>火山ツアー</h2> <p>サントリーニでは、このネア・カメニ島とパレア・カメニ島に船でいくツアーが毎日出ています。 休火山とはいえ、カルデラの噴火口に行く機会はあまりないので、いってみることにしました。 (ガイドブックには火山ツアーは夏季だけと書いてありましたが、現地で確認してみたところ通年でやっているようです。)</p> <p>フィラの中心にある、Dakoutros Travelという旅行会社で申し込んで、値段は20ユーロでした。</p> <h2>フィラからオールドポートへ</h2> <p>ツアーは、フィラの近くのオールドポートという港から出発します。</p> <p>フィラからオールドポートまでは、タクシーやバスでもいけますが、ケーブルカーで行ってみることにしました。 断崖絶壁と海に挟まれたケーブルカーなので、とても眺めがよかったです。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/f7cCyxSxDOs"></iframe> <p>港に行くと、すでに船はついていました。ちょっと中世っぽい船です。 <figure class="figure-image figure-image-fotolife" title="船"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315104006.jpg" alt="f:id:toyship:20180315104006j:plain:w300" title="f:id:toyship:20180315104006j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>30人ほどの参加者が乗り込んで、島にむかって出発です。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/uoKCDh-fpfE"></iframe> <p>出航したあと、途中でふりかえるとフィラの街が断崖の上に見えました。</p> <p>海からみると、断崖の激しさがよくわかります。よくこんなところにうちをつくろうと考えるなあ、というくらいの断崖です。 <figure class="figure-image figure-image-fotolife" title="フィラの街"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315135129.jpg" alt="f:id:toyship:20180315135129j:plain:w300" title="f:id:toyship:20180315135129j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>しばらくすると、ネア・カメニ島の港に到着。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/nFZwNsUfzdI"></iframe> <h2>ネア・カメニ島</h2> <p>港には、ほかのツアーの船もありました。 船をおいて頂上に出発。</p> <p><figure class="figure-image figure-image-fotolife" title="船"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315123713.jpg" alt="f:id:toyship:20180315123713j:plain:w300" title="f:id:toyship:20180315123713j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>といっても、それほど長い距離ではないです。</p> <p><figure class="figure-image figure-image-fotolife" title="港から登る"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315114104.jpg" alt="f:id:toyship:20180315114104j:plain:w300" title="f:id:toyship:20180315114104j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>周りが海なので、普通の山よりかなり開放感があって、楽しく登れます。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/BXThXDbkZZw"></iframe> <p><figure class="figure-image figure-image-fotolife" title="歩きます。"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315113844.jpg" alt="f:id:toyship:20180315113844j:plain:w300" title="f:id:toyship:20180315113844j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>勾配もそれほどではないし、周りの風景をみていると楽しく登れます。</p> <p><figure class="figure-image figure-image-fotolife" title="まだまだ登る"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315114039.jpg" alt="f:id:toyship:20180315114039j:plain:w300" title="f:id:toyship:20180315114039j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>見渡してみると、溶岩だらけ。かなり迫力のある風景です。</p> <p>ちょっと日本ではこんな風景はみることができませんよね。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/flnPey0pymA"></iframe> <p><figure class="figure-image figure-image-fotolife" title="歩いているところ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315115919.jpg" alt="f:id:toyship:20180315115919j:plain:w300" title="f:id:toyship:20180315115919j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>また、途中には火山活動の観測機器もありました。</p> <p><figure class="figure-image figure-image-fotolife" title="観測機器"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315120835.jpg" alt="f:id:toyship:20180315120835j:plain:w300" title="f:id:toyship:20180315120835j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>港から30分ほど歩くと、頂上に到着。 頂上といっても、平坦な感じです。 <figure class="figure-image figure-image-fotolife" title="頂上"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315120620.jpg" alt="f:id:toyship:20180315120620j:plain:w300" title="f:id:toyship:20180315120620j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>頂上付近では、少し蒸気がでていました。 火山活動は休止しているだけなのを実感できます。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/yrWXcKjRtIw"></iframe> <p>しばらく頂上でいろいろまわり、また港まで戻ります。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/LeGLiGonDWI"></iframe> <h2>海中温泉</h2> <p>ネア・カメニ島の港をでると、船はパレア・カメニ島にある海中温泉へ向かいました。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/BIh3jTk3pCg"></iframe> <p>海中温泉は、泳ぎたい人だけ水着でおよぎ、他の人はそれを眺めている感じです。</p> <p>温泉といっても、ちょっとぬるめの温水プールくらいの温度でした。</p> <p>私は荷物をみてもらう人がいなかったので、見るだけでしたが、みんなが泳いでいるのをみるだけでも楽しかったです。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/5VE6wHH2PTw"></iframe> <p><figure class="figure-image figure-image-fotolife" title="温泉で停止中"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315131907.jpg" alt="f:id:toyship:20180315131907j:plain:w300" title="f:id:toyship:20180315131907j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/BSigiev3t2g"></iframe> <p>20分ほど温泉で遊んだ後、船はまたオールドポートに向かいました。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/8exKpVYAjGQ"></iframe> <h2>オールドポートからフィラへ</h2> <p>さて、オールドポートからフィラへは、また断崖を登る必要があります。 <figure class="figure-image figure-image-fotolife" title="帰りの道"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315135610.jpg" alt="f:id:toyship:20180315135610j:plain:w300" title="f:id:toyship:20180315135610j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></figure></p> <p>行きと同じケーブルカーでもいいんですが、実はこの道はロバで登ることができます。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/OiIw3iFL-wg"></iframe> <p>徒歩と同じくらいのスピードですが、かなり楽なのと、視点が高くて眺めがいいのがよかったです。</p> <p>ロバはあまりこちらの言うことをきいてくれず、途中で止まってしまったりするんですけど、それもまた生き物っぽくて楽しいですよね。 のんびりゆっくり断崖を登って、フィラに戻りました。</p> toyship サントリーニ島の猫とロバ hatenablog://entry/10257846132692887345 2018-12-31T16:33:10+09:00 2018-12-31T16:50:28+09:00 今年の3月に、サントリーニ島で花粉回避開発合宿をしてきました。(サントリーニ島で花粉症退避の開発合宿をしてきました。 - Toyship.org) 今年はヨーロッパに何カ国かいったんですが、サントリーニ島は群をぬいて猫の数が多かったです。 塀の上の猫 数が多いだけじゃなくて、猫の可愛さと愛想良さも抜群でした。 お嬢様猫 毛並みはいいし、可愛いし、栄養満点で可愛がられている猫ばかり。 塀の向こうの猫 黒猫 岩合さんの世界猫あるきでもサントリーニ島を撮影されていますが、岩合さんに何回でも来て欲しいくらい猫が多いです。 カフェの前の猫 街をあるいていても、可愛い猫が多くて困りました。 花壇の前の猫 … <p>今年の3月に、サントリーニ島で花粉回避開発合宿をしてきました。(<a href="https://www.toyship.org/2018/12/31/164856">&#x30B5;&#x30F3;&#x30C8;&#x30EA;&#x30FC;&#x30CB;&#x5CF6;&#x3067;&#x82B1;&#x7C89;&#x75C7;&#x9000;&#x907F;&#x306E;&#x958B;&#x767A;&#x5408;&#x5BBF;&#x3092;&#x3057;&#x3066;&#x304D;&#x307E;&#x3057;&#x305F;&#x3002; - Toyship.org</a>)</p> <p>今年はヨーロッパに何カ国かいったんですが、サントリーニ島は群をぬいて猫の数が多かったです。</p> <p><figure class="figure-image figure-image-fotolife" title="塀の上の猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180313/20180313153046.jpg" alt="f:id:toyship:20180313153046j:plain" title="f:id:toyship:20180313153046j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>塀の上の猫</figcaption></figure></p> <p>数が多いだけじゃなくて、猫の可愛さと愛想良さも抜群でした。</p> <p><figure class="figure-image figure-image-fotolife" title="お嬢様猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180313/20180313153150.jpg" alt="f:id:toyship:20180313153150j:plain" title="f:id:toyship:20180313153150j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>お嬢様猫</figcaption></figure></p> <p>毛並みはいいし、可愛いし、栄養満点で可愛がられている猫ばかり。</p> <p><figure class="figure-image figure-image-fotolife" title="塀の向こうの猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180313/20180313153158.jpg" alt="f:id:toyship:20180313153158j:plain" title="f:id:toyship:20180313153158j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>塀の向こうの猫</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="黒猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180313/20180313153204.jpg" alt="f:id:toyship:20180313153204j:plain" title="f:id:toyship:20180313153204j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>黒猫</figcaption></figure></p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/zAJ8zGYaC5o"></iframe> <p>岩合さんの世界猫あるきでもサントリーニ島を撮影されていますが、岩合さんに何回でも来て欲しいくらい猫が多いです。</p> <p><figure class="figure-image figure-image-fotolife" title="カフェの前の猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180318/20180318144541.jpg" alt="f:id:toyship:20180318144541j:plain" title="f:id:toyship:20180318144541j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>カフェの前の猫</figcaption></figure></p> <p>街をあるいていても、可愛い猫が多くて困りました。</p> <p><figure class="figure-image figure-image-fotolife" title="花壇の前の猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180317/20180317135613.jpg" alt="f:id:toyship:20180317135613j:plain" title="f:id:toyship:20180317135613j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>花壇の前の猫</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="階段の黒猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180318/20180318144341.jpg" alt="f:id:toyship:20180318144341j:plain" title="f:id:toyship:20180318144341j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>階段の黒猫</figcaption></figure></p> <p>ホテルの部屋で仕事をしていても、猫がきてミルクをねだっていったり……。</p> <p><figure class="figure-image figure-image-fotolife" title="部屋に来た猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180320/20180320072712.jpg" alt="f:id:toyship:20180320072712j:plain" title="f:id:toyship:20180320072712j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>部屋に来た猫</figcaption></figure></p> <p>仕事の邪魔をしたり……。</p> <p><figure class="figure-image figure-image-fotolife" title="仕事の邪魔をする猫"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180313/20180313094532.jpg" alt="f:id:toyship:20180313094532j:plain" title="f:id:toyship:20180313094532j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>仕事の邪魔をする猫</figcaption></figure></p> <p>ほんと、猫はけしからんですね。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/AqsN8REg77M"></iframe> <h2>ロバ</h2> <p>街を歩いていると、ロバも目につきました。</p> <p>サントリーニ島は、主要道路は車が走る普通の道路ですが、場所によっては道が細くて石畳になっていて、さらに階段だらけなので、荷物を運ぶ手段がロバしかないところもあります。</p> <p>こちらは工事用の荷物を運んでいるロバたち。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/CoGJdZAJKE8"></iframe> <p>こちらは、観光客用の人をのせるためのロバです。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/PKnDEfjEIn4"></iframe> <p>3月は工事の時期のようで、かなり重めの荷物を運んだロバが多かったです。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/EWNAkFbm7r4"></iframe> <p><figure class="figure-image figure-image-fotolife" title="おじさんとロバ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180315/20180315140504.jpg" alt="f:id:toyship:20180315140504j:plain" title="f:id:toyship:20180315140504j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>おじさんとロバ</figcaption></figure></p> <p>旅行中に動物にあうと和みますね。</p> toyship わけあって weak self hatenablog://entry/10257846132683699268 2018-12-12T07:01:58+09:00 2018-12-12T07:01:58+09:00 Closureを使っていたら、ちょっと予想外の動作になってしまっていました。 超・基本的なことですが、動作確認したのでメモ。 closureの循環参照と weak self weak self をなぜ使うかというと、循環参照によるメモリーリークを避けるためですよね。 closureを保持するクラスを作って見てみましょう。 下の Personクラスは、生成・消滅した時がわかりやすいように init と deinit にログをいれておきます。 そシンプルに Hello, I am [name]! と表示してくれる normalHello というメソッドを作り、 keepAndDo メソッドで、cl… <p>Closureを使っていたら、ちょっと予想外の動作になってしまっていました。 超・基本的なことですが、動作確認したのでメモ。</p> <h2>closureの循環参照と weak self</h2> <p><code>weak self</code> をなぜ使うかというと、循環参照によるメモリーリークを避けるためですよね。</p> <p>closureを保持するクラスを作って見てみましょう。</p> <p>下の Personクラスは、生成・消滅した時がわかりやすいように <code>init</code> と <code>deinit</code> にログをいれておきます。 そシンプルに <code>Hello, I am [name]!</code> と表示してくれる <code>normalHello</code> というメソッドを作り、 <code>keepAndDo</code> メソッドで、closureを保持するようにしておきます。</p> <p><script src="https://gist.github.com/TachibanaKaoru/fe0551e48fcd0eadc3e1ed19ec9c0078.js"> </script><cite class="hatena-citation"><a href="https://gist.github.com/TachibanaKaoru/fe0551e48fcd0eadc3e1ed19ec9c0078">gist.github.com</a></cite></p> <p>このPersonクラスをつかって、Closureを保持して実行してみましょう。 <code>strongHello</code> では <code>weak self</code> を使わず、 <code>weakHello</code> では <code>weak self</code> を使っています。</p> <p><script src="https://gist.github.com/TachibanaKaoru/e5c7252120f819c0e7558b5d9443f6a6.js"> </script><cite class="hatena-citation"><a href="https://gist.github.com/TachibanaKaoru/e5c7252120f819c0e7558b5d9443f6a6">gist.github.com</a></cite></p> <p>実行してみると、🦁の <code>deinit</code> だけよばれません。 <code>weak self</code> をよんでいないから <code>self</code> が循環参照をおこして解放されず、メモリーリークの原因になっていますね。</p> <h2>escapingとnonescape</h2> <p>で、ここで <code>escaping</code> と <code>nonescape</code> についても復習しましょう。</p> <p>対象となるclosureがスコープから抜けても存在するときには <code>@escaping</code> が必要になります。</p> <p>上の <code>keepAndDo</code> メソッドでは <code>@escaping</code>をつけていますが、これを省略するとコンパイルエラーになります。 上の場合は、<code>keepAndDo</code> のスコープから抜けても、(selfで保持しているので)closureが存在するから必要なんですね。</p> <p>で、似たような処理でnoescapeのものを付け加えてみましょう。</p> <p><script src="https://gist.github.com/TachibanaKaoru/38db2a26403bfa5878e864c476e27d64.js"> </script><cite class="hatena-citation"><a href="https://gist.github.com/TachibanaKaoru/38db2a26403bfa5878e864c476e27d64">gist.github.com</a></cite></p> <p>さきほどの Personクラスに、closureを実行するだけで保持しない <code>justDo</code> メソッドをつけてみました。 その <code>justDo</code> を よぶ <code>noescapeHello</code> には <code>weak self</code> はついていません。</p> <p>(なお、Swift3からデフォルトは <code>@noescape</code> になったので、最近は <code>@noescape</code> を書くことはないと思います。)</p> <p><script src="https://gist.github.com/TachibanaKaoru/648b92fb9c971f685cd685ae7fc3de7d.js"> </script><cite class="hatena-citation"><a href="https://gist.github.com/TachibanaKaoru/648b92fb9c971f685cd685ae7fc3de7d">gist.github.com</a></cite></p> <p>実行してみると、<code>weak self</code> をつけなくても <code>deinit</code> がよばれています。循環参照は発生していません。</p> <h2>escapingで非同期実行</h2> <p>で、さらにここで非同期実行をかけてみましょう。 それぞれのclosureのなかに、1秒後に自分の名前のログを出す処理をいれておきます。</p> <p>さらに、 <code>noescapeAndWeakHello</code> として、非同期実行のclosureで <code>weak self</code> つきのメソッドも追加。</p> <p><script src="https://gist.github.com/TachibanaKaoru/fb65884b0050756d4559d7b2e77c1700.js"> </script><cite class="hatena-citation"><a href="https://gist.github.com/TachibanaKaoru/fb65884b0050756d4559d7b2e77c1700">gist.github.com</a></cite></p> <p>で、これを実行すると……。</p> <p><script src="https://gist.github.com/TachibanaKaoru/dc512ce163dc361bbaaaf93a7e490969.js"> </script><cite class="hatena-citation"><a href="https://gist.github.com/TachibanaKaoru/dc512ce163dc361bbaaaf93a7e490969">gist.github.com</a></cite></p> <p>weakで実行した場合、当然解放済みなので非同期実行で名前が表示されないんですが、noescapeだと名前が表示されるのに、noescapeかつweakだと名前が表示されない……。</p> <p>ログを見ればわかることですが、noescapeの場合、非同期処理の完了と同時にPersonのインスタンスが削除されますが、selfにweakがついていると、非同期処理が実行される前にインスタンスが削除されてしまうんですね。 <code>weak self</code> つけてるんだから当然ですけど。</p> <p>ステップで一つ一つ確認すれば当然の動作なんだけど、惰性で <code>weak self</code> つけちゃって予想外の動作になっちゃっていました。</p> <p>なんとなく <code>weak self</code> 、じゃなくてちゃんと理由を考えて使わないとな。 (とおもいつつ、closureの処理が深くなってくると、だんだんわからなくなるんですよね……。)</p> <p>基本的には closureは nonescapeでとりまわすのが一番事故らない気はしますが、いろいろな事情でclosureをkeepしたいこともあり。難しいところです。</p> toyship iOSと新しい元号 hatenablog://entry/10257846132678495147 2018-12-02T04:19:01+09:00 2018-12-02T04:19:01+09:00 これはiOSアドベントカレンダーの2日目の記事です。 毎年アドベントカレンダーでは、カレンダーに関係あることを書くことにしています。 今回は来年5月の改元。 iOS始まって以来の改元ですね。 改元前にバグになりそうなポイントを確認しておきましょう。 <p>これは<a href="https://qiita.com/advent-calendar/2018/ios">iOSアドベントカレンダー</a>の2日目の記事です。</p> <p>毎年アドベントカレンダーでは、カレンダーに関係あることを書くことにしています。 今回は来年5月の改元。 iOS始まって以来の改元ですね。</p> <p>改元前にバグになりそうなポイントを確認しておきましょう。</p> <h2>元号</h2> <p>この業界にいると元号って年に数回くらいしか使わないですよね。 私は使うタイミングになるたびに毎回毎回「今年って平成何年だっけ?」って繰り返していて、結局平成が終わるまで、「今年って平成何年だっけ?」から卒業できませんでした。 きっと次の元号でもずっとそうなんだろうなと思います。</p> <p>新しい元号になるのは2019年5月。 2019年4月30日までは「平成」、2019年5月1日から新しい元号が適用されます。</p> <h2>iOSエンジニアは改元でなにをすべきか</h2> <p>さて、この改元にともなってiOSエンジニアがすべきことはなんでしょうか。</p> <p>iOSで和暦がUIに表示されるのは、ユーザーが設定の「一般」>「言語と地域」>「カレンダー」で和暦を選んでいる時だけです。</p> <p>これを設定すると、カレンダーアプリなどの日付の表示が和暦になり、DatePickerで年を選ぶときにも西暦ではなく和暦の年が表示されるようになります。 レイアウト上の都合からユーザーが和暦を選択していても西暦で表示しているアプリも多いので、和暦を設定していても、表示上は関係ないアプリも多いと思います。</p> <p>元号についての処理は、FoundationフレームワークのCalendarに含まれています。 元号が正式に決定されたあとの iOSのバージョンアップで、新元号が含まれたフレームワークになるので、基本的にはエンジニアがしなければいけないことはなにもありません。</p> <p>ただし、日時の計算で正しくCalendarクラスを使っていないとバグが発生することがあります。</p> <h2>改元のタイミングで発生しがちなバグ</h2> <p>改元のタイミングで発生しがちなバグを、昭和から平成になったときの場合で確認してみましょう。</p> <p>昭和から平成への移行、私は記憶があるんですけど、たぶんこの記事を読んでいる人は覚えていない人の方が(というかそもそも生まれていない人の方が)多いんでしょうね……。</p> <p>1989年のお正月早々の1月7日に、昭和天皇が崩御しました。 年末から天皇陛下の具合がだいぶ悪いという話だったのでそのこと自体に驚く人はいませんでしたが、やはり時代の節目として社会に大きな衝撃をもたらしました。 すぐに新しい元号の「平成」が発表され、1989年の1月7日まで昭和、1月8日から平成になりました。</p> <p>さて、このときの DateComponentの処理を見てみましょう。 例として、1/7の3日後の日付を取得する処理を書いてみます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">currentCalendar</span> <span class="synIdentifier">=</span> Calendar(identifier<span class="synSpecial">:</span> .gregorian) <span class="synComment">// 1989年1月7日のDateです。</span> <span class="synPreProc">let</span> <span class="synIdentifier">now_19890107</span> <span class="synIdentifier">=</span> Date(timeIntervalSinceReferenceDate<span class="synSpecial">:</span> <span class="synIdentifier">-</span><span class="synConstant">378172800</span>) <span class="synComment">// 上のDateからDateComponentを作ります。</span> <span class="synPreProc">let</span> <span class="synIdentifier">nowComponent</span> <span class="synIdentifier">=</span> currentCalendar.dateComponents([.year,.month,.day], from<span class="synSpecial">:</span> <span class="synType">now_19890107</span>) <span class="synPreProc">let</span> <span class="synIdentifier">year</span> <span class="synIdentifier">=</span> (nowComponent.year)<span class="synIdentifier">!</span> <span class="synPreProc">let</span> <span class="synIdentifier">month</span> <span class="synIdentifier">=</span> (nowComponent.month)<span class="synIdentifier">!</span> <span class="synPreProc">let</span> <span class="synIdentifier">day</span> <span class="synIdentifier">=</span> (nowComponent.day)<span class="synIdentifier">!</span> <span class="synComment">// 上のDateComponentの3日後のDateComponentを作ります。</span> <span class="synPreProc">let</span> <span class="synIdentifier">newDay</span> <span class="synIdentifier">=</span> day <span class="synIdentifier">+</span> <span class="synConstant">3</span> <span class="synPreProc">var</span> <span class="synIdentifier">newComponent</span> <span class="synIdentifier">=</span> DateComponents() newComponent.year <span class="synIdentifier">=</span> year newComponent.month <span class="synIdentifier">=</span> month newComponent.day <span class="synIdentifier">=</span> newDay <span class="synComment">// 上で作ったDateComponentから3日後のDateを作成</span> <span class="synPreProc">let</span> <span class="synIdentifier">newDate</span> <span class="synIdentifier">=</span> currentCalendar.date(from<span class="synSpecial">:</span> <span class="synType">newComponent</span>) print(<span class="synConstant">&quot;newDate:</span><span class="synSpecial">\(String(describing: newDate)</span><span class="synConstant">)&quot;</span>) </pre> <p>DateからDateComponentsを作成し、それに3を加えて新しいDateComponentsを作り、それをDateにして 計算結果はこうなります。</p> <pre class="code" data-lang="" data-unlink>newDate:Optional(1989-01-10 00:00:00 +0000)</pre> <p>1/7の3日後なので、1/10になってしますね。</p> <p>さて、上記の計算を和暦でやってみましょう。 次のコードは、上のコードの1行目のCalendarを変えただけです。(あとはコピペです)</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">currentCalendar</span> <span class="synIdentifier">=</span> Calendar(identifier<span class="synSpecial">:</span> .japanese) <span class="synComment">// 1989年1月7日のDateです。</span> <span class="synPreProc">let</span> <span class="synIdentifier">now_19890107</span> <span class="synIdentifier">=</span> Date(timeIntervalSinceReferenceDate<span class="synSpecial">:</span> <span class="synIdentifier">-</span><span class="synConstant">378172800</span>) <span class="synComment">// 上のDateからDateComponentを作ります。</span> <span class="synPreProc">let</span> <span class="synIdentifier">nowComponent</span> <span class="synIdentifier">=</span> currentCalendar.dateComponents([.year,.month,.day], from<span class="synSpecial">:</span> <span class="synType">now_19890107</span>) <span class="synPreProc">let</span> <span class="synIdentifier">year</span> <span class="synIdentifier">=</span> (nowComponent.year)<span class="synIdentifier">!</span> <span class="synPreProc">let</span> <span class="synIdentifier">month</span> <span class="synIdentifier">=</span> (nowComponent.month)<span class="synIdentifier">!</span> <span class="synPreProc">let</span> <span class="synIdentifier">day</span> <span class="synIdentifier">=</span> (nowComponent.day)<span class="synIdentifier">!</span> <span class="synComment">// 上のDateComponentの3日後のDateComponentを作ります。</span> <span class="synPreProc">let</span> <span class="synIdentifier">newDay</span> <span class="synIdentifier">=</span> day <span class="synIdentifier">+</span> <span class="synConstant">3</span> <span class="synPreProc">var</span> <span class="synIdentifier">newComponent</span> <span class="synIdentifier">=</span> DateComponents() newComponent.year <span class="synIdentifier">=</span> year newComponent.month <span class="synIdentifier">=</span> month newComponent.day <span class="synIdentifier">=</span> newDay <span class="synComment">// 上で作ったDateComponentから3日後のDateを作成</span> <span class="synPreProc">let</span> <span class="synIdentifier">newDate</span> <span class="synIdentifier">=</span> currentCalendar.date(from<span class="synSpecial">:</span> <span class="synType">newComponent</span>) print(<span class="synConstant">&quot;newDate:</span><span class="synSpecial">\(String(describing: newDate)</span><span class="synConstant">)&quot;</span>) </pre> <p>さて、上記の結果はこうなります。</p> <pre class="code" data-lang="" data-unlink>newDate:Optional(2052-01-10 00:00:00 +0000)</pre> <p>上記の計算は、わかりやすくというと昭和64年1月7日の3日後を平成64年1月10日で計算しちゃった感じですね。</p> <p>Calendarオブジェクトを作るときに、下記のようにcurrentの要素を使ってしまうと、端末で設定しているカレンダーとなります。 端末のカレンダーを西暦にしているユーザーのところでは予想通りに動作しますが、端末を和暦にしているユーザーのところでは正しい日付が取れません。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">currentCalendar</span> <span class="synIdentifier">=</span> Calendar.current </pre> <p>たとえアプリの表示に和暦要素がなくても、日付系の計算をするときに、Calendar.currentを使っていたらバグが発生する可能性があります。 日付計算をするときには、Calendar.currentは絶対に使わないこと。 改元まで時間がある今の時期に、あらためて確認してみましょう。</p> <p>でも実は上の計算、わざわざDateComponentsのプロパティを操作する必要はないんです。 下記のように、CalendarクラスにDateComponentsベースで加算減算を行えるメソッドがあるので、これを使えば currentCalendarが西暦でも和暦でも問題はでません。 (この方が短くて済みますし。)</p> <pre class="code lang-swift" data-lang="swift" data-unlink>currentCalendar.date(byAdding<span class="synSpecial">:</span> .day, value<span class="synSpecial">:</span> <span class="synConstant">3</span>, to<span class="synSpecial">:</span> <span class="synType">now_19890107</span>) </pre> <h2>和暦の元号</h2> <p>和暦の元号はCalendarクラスのなかにどのように保存されているんでしょうか。</p> <p>CalendarにerasとlongErasというプロパティがあり、このなかにStringの配列として保存されています。 (和暦の場合にはerasとlongErasの中身は同じです。)</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">eras</span> <span class="synIdentifier">=</span> japaneseCalendar.eraSymbols <span class="synPreProc">let</span> <span class="synIdentifier">longEras</span> <span class="synIdentifier">=</span> japaneseCalendar.longEraSymbols </pre> <p>このStringの配列は、Localeが日本語だとこちら。</p> <pre class="code" data-lang="" data-unlink>[&#34;大化&#34;, &#34;白雉&#34;, &#34;白鳳&#34;, &#34;朱鳥&#34;, &#34;大宝&#34;, &#34;慶雲&#34;, &#34;和銅&#34;, &#34;霊亀&#34;, &#34;養老&#34;, &#34;神亀&#34;, &#34;天平&#34;, &#34;天平感宝&#34;, &#34;天平勝宝&#34;, &#34;天平宝字&#34;, &#34;天平神護&#34;, &#34;神護景雲&#34;, &#34;宝亀&#34;, &#34;天応&#34;, &#34;延暦&#34;, &#34;大同&#34;, &#34;弘仁&#34;, &#34;天長&#34;, &#34;承和&#34;, &#34;嘉祥&#34;, &#34;仁寿&#34;, &#34;斉衡&#34;, &#34;天安&#34;, &#34;貞観&#34;, &#34;元慶&#34;, &#34;仁和&#34;, &#34;寛平&#34;, &#34;昌泰&#34;, &#34;延喜&#34;, &#34;延長&#34;, &#34;承平&#34;, &#34;天慶&#34;, &#34;天暦&#34;, &#34;天徳&#34;, &#34;応和&#34;, &#34;康保&#34;, &#34;安和&#34;, &#34;天禄&#34;, &#34;天延&#34;, &#34;貞元&#34;, &#34;天元&#34;, &#34;永観&#34;, &#34;寛和&#34;, &#34;永延&#34;, &#34;永祚&#34;, &#34;正暦&#34;, &#34;長徳&#34;, &#34;長保&#34;, &#34;寛弘&#34;, &#34;長和&#34;, &#34;寛仁&#34;, &#34;治安&#34;, &#34;万寿&#34;, &#34;長元&#34;, &#34;長暦&#34;, &#34;長久&#34;, &#34;寛徳&#34;, &#34;永承&#34;, &#34;天喜&#34;, &#34;康平&#34;, &#34;治暦&#34;, &#34;延久&#34;, &#34;承保&#34;, &#34;承暦&#34;, &#34;永保&#34;, &#34;応徳&#34;, &#34;寛治&#34;, &#34;嘉保&#34;, &#34;永長&#34;, &#34;承徳&#34;, &#34;康和&#34;, &#34;長治&#34;, &#34;嘉承&#34;, &#34;天仁&#34;, &#34;天永&#34;, &#34;永久&#34;, …, &#34;宝暦&#34;, &#34;明和&#34;, &#34;安永&#34;, &#34;天明&#34;, &#34;寛政&#34;, &#34;享和&#34;, &#34;文化&#34;, &#34;文政&#34;, &#34;天保&#34;, &#34;弘化&#34;, &#34;嘉永&#34;, &#34;安政&#34;, &#34;万延&#34;, &#34;文久&#34;, &#34;元治&#34;, &#34;慶応&#34;, &#34;明治&#34;, &#34;大正&#34;, &#34;昭和&#34;, &#34;平成&#34;]</pre> <p>英語だとこちらの値が入っています。</p> <pre class="code" data-lang="" data-unlink>[&#34;Taika (645–650)&#34;, &#34;Hakuchi (650–671)&#34;, &#34;Hakuhō (672–686)&#34;, &#34;Shuchō (686–701)&#34;, &#34;Taihō (701–704)&#34;, &#34;Keiun (704–708)&#34;, &#34;Wadō (708–715)&#34;, &#34;Reiki (715–717)&#34;, &#34;Yōrō (717–724)&#34;, &#34;Jinki (724–729)&#34;, &#34;Tenpyō (729–749)&#34;, &#34;Tenpyō-kampō (749–749)&#34;, &#34;Tenpyō-shōhō (749–757)&#34;, &#34;Tenpyō-hōji (757–765)&#34;, &#34;Tenpyō-jingo (765–767)&#34;, &#34;Jingo-keiun (767–770)&#34;, &#34;Hōki (770–780)&#34;, &#34;Ten-ō (781–782)&#34;, &#34;Enryaku (782–806)&#34;, &#34;Daidō (806–810)&#34;, &#34;Kōnin (810–824)&#34;, &#34;Tenchō (824–834)&#34;, &#34;Jōwa (834–848)&#34;, &#34;Kajō (848–851)&#34;, &#34;Ninju (851–854)&#34;, &#34;Saikō (854–857)&#34;, &#34;Ten-an (857–859)&#34;, &#34;Jōgan (859–877)&#34;, &#34;Gangyō (877–885)&#34;, &#34;Ninna (885–889)&#34;, &#34;Kanpyō (889–898)&#34;, &#34;Shōtai (898–901)&#34;, &#34;Engi (901–923)&#34;, &#34;Enchō (923–931)&#34;, &#34;Jōhei (931–938)&#34;, &#34;Tengyō (938–947)&#34;, &#34;Tenryaku (947–957)&#34;, &#34;Tentoku (957–961)&#34;, &#34;Ōwa (961–964)&#34;, &#34;Kōhō (964–968)&#34;, &#34;Anna (968–970)&#34;, &#34;Tenroku (970–973)&#34;, &#34;Ten’en (973–976)&#34;, &#34;Jōgen (976–978)&#34;, &#34;Tengen (978–983)&#34;, &#34;Eikan (983–985)&#34;, &#34;Kanna (985–987)&#34;, &#34;Eien (987–989)&#34;, &#34;Eiso (989–990)&#34;, &#34;Shōryaku (990–995)&#34;, &#34;Chōtoku (995–999)&#34;, &#34;Chōhō (999–1004)&#34;, &#34;Kankō (1004–1012)&#34;, &#34;Chōwa (1012–1017)&#34;, &#34;Kannin (1017–1021)&#34;, &#34;Jian (1021–1024)&#34;, &#34;Manju (1024–1028)&#34;, &#34;Chōgen (1028–1037)&#34;, &#34;Chōryaku (1037–1040)&#34;, &#34;Chōkyū (1040–1044)&#34;, &#34;Kantoku (1044–1046)&#34;, &#34;Eishō (1046–1053)&#34;, &#34;Tengi (1053–1058)&#34;, &#34;Kōhei (1058–1065)&#34;, &#34;Jiryaku (1065–1069)&#34;, &#34;Enkyū (1069–1074)&#34;, &#34;Shōho (1074–1077)&#34;, &#34;Shōryaku (1077–1081)&#34;, &#34;Eihō (1081–1084)&#34;, &#34;Ōtoku (1084–1087)&#34;, &#34;Kanji (1087–1094)&#34;, &#34;Kahō (1094–1096)&#34;, &#34;Eichō (1096–1097)&#34;, &#34;Jōtoku (1097–1099)&#34;, &#34;Kōwa (1099–1104)&#34;, &#34;Chōji (1104–1106)&#34;, &#34;Kashō (1106–1108)&#34;, &#34;Tennin (1108–1110)&#34;, &#34;Ten-ei (1110–1113)&#34;, &#34;Eikyū (1113–1118)&#34;, …, &#34;Hōreki (1751–1764)&#34;, &#34;Meiwa (1764–1772)&#34;, &#34;An’ei (1772–1781)&#34;, &#34;Tenmei (1781–1789)&#34;, &#34;Kansei (1789–1801)&#34;, &#34;Kyōwa (1801–1804)&#34;, &#34;Bunka (1804–1818)&#34;, &#34;Bunsei (1818–1830)&#34;, &#34;Tenpō (1830–1844)&#34;, &#34;Kōka (1844–1848)&#34;, &#34;Kaei (1848–1854)&#34;, &#34;Ansei (1854–1860)&#34;, &#34;Man’en (1860–1861)&#34;, &#34;Bunkyū (1861–1864)&#34;, &#34;Genji (1864–1865)&#34;, &#34;Keiō (1865–1868)&#34;, &#34;Meiji&#34;, &#34;Taishō&#34;, &#34;Shōwa&#34;, &#34;Heisei&#34;]</pre> <p>上記では途中省略されていますが、どちらも236個のStringが入っています。 来年の5月にはこの配列の要素数が一つ増えるわけですね。</p> <p>上記の一番古い要素は「大化」です。</p> <p>Wikipediaを見ると一番古い年号が大化なので、それに従っています。 <a href="https://ja.wikipedia.org/wiki/元号一覧_(日本)">&#x5143;&#x53F7;&#x4E00;&#x89A7; (&#x65E5;&#x672C;) - Wikipedia</a></p> <p>大化の改新で天皇中心の政治になったと同時に元号制が始まったわけですね。</p> <p>でも、ちょっとDatePickerで確認してみたところ、DatePickerに表示される一番古い年号って"白雉"になってるんですよね。 eraには"大化"も入っているのになんで表示されないんでしょう……?</p> <p>2013年のアドベントカレンダーでもCalendarネタをとりあげていたんですが、その時にはDatePickerで"大化"が表示されているのは確認してるんですよね。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.toyship.org%2Farchives%2F1665" title="NSCalendar on iOS Advent Calendar 2013 - Toyship.org" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.toyship.org/archives/1665">www.toyship.org</a></cite></p> <p>ちょっと謎……。</p> toyship iOSDC 2018とiOS業界の変化 hatenablog://entry/10257846132620841217 2018-09-05T23:15:25+09:00 2018-09-05T23:15:25+09:00 iOSDC 2018、去年と同じく、今年もスタッフ兼スピーカーで参加しました。 (記事中の写真は、すべてiOSDC写真チームのみなさんの写真です。毎年素敵な写真をありがとう!) 去年のiOSDCも楽しかったんですが、今年はそれをうわまわる充実感でした。 www.toyship.org Synchronized iPhones! Synchronized iPhones!というタイトルで発表させていただきました。 いろいろ資料につめこみすぎて、2日前の練習では50分超。 その後推敲して、最終的に資料が完成したのが発表1時間前。 speakerdeck.com もともと、プレゼンのテーマはBlue… <p>iOSDC 2018、去年と同じく、今年もスタッフ兼スピーカーで参加しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180901/20180901090121.jpg" alt="f:id:toyship:20180901090121j:plain" title="f:id:toyship:20180901090121j:plain" class="hatena-fotolife" itemprop="image"></span></p> <p>(記事中の写真は、すべてiOSDC写真チームのみなさんの写真です。毎年素敵な写真をありがとう!)</p> <p>去年のiOSDCも楽しかったんですが、今年はそれをうわまわる充実感でした。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.toyship.org%2Farchives%2F2373" title="iOSDC 2017と個人的なファミリーデー - Toyship.org" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.toyship.org/archives/2373">www.toyship.org</a></cite></p> <h3>Synchronized iPhones!</h3> <p>Synchronized iPhones!というタイトルで発表させていただきました。</p> <p>いろいろ資料につめこみすぎて、2日前の練習では50分超。 その後推敲して、最終的に資料が完成したのが発表1時間前。</p> <p><iframe id="talk_frame_462043" src="//speakerdeck.com/player/8044b506ff7846ddb8f269b414611012" width="710" height="463" style="border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe><cite class="hatena-citation"><a href="https://speakerdeck.com/toyship/synchronized-iphones">speakerdeck.com</a></cite></p> <p>もともと、プレゼンのテーマはBluetoothを使った端末間同期だったんですが、時刻系について調べていくうちにそちらの方がおもしろくなってきてしまいました。</p> <ul> <li>GPSから配信されている時刻系はUTCではない。</li> <li>現行UTCの前の旧UTCでは、秒の長さが変動していた。</li> <li>GPSに搭載されている時計は、一般相対論と特殊相対論の補正がされて遅くなっている。</li> </ul> <p>このあたりは、iOSからは少しはなれてしまうことではあるんですが、カンファレンスにきて今までしらなかったことを知るのも楽しいかな、ということで発表資料にいれました。 幸い好評いただいたようでよかったです。</p> <p>聞いていただいた方、質問してくださった方、みなさまありがとうございました!</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180901/20180901150920.jpg" alt="f:id:toyship:20180901150920j:plain" title="f:id:toyship:20180901150920j:plain" class="hatena-fotolife" itemprop="image"></span></p> <h3>IRT</h3> <p>今年のiOSDCでは、セッション以外にも新しいコミュニケーションの試みがいくつかありました。</p> <p>一つ目がIRT。 IRTは、決まったテーマでテーブルを囲んで司会者と討論するという形式のものです。</p> <p>始まる前はどうなるかよくわからなかったのですが、終わって見ると盛況でした</p> <p>1日目は<a href="https://twitter.com/tarappo">tarappo (@tarappo) | Twitter</a>さんのテスト相談会、2日目は<a href="https://twitter.com/inamiy">Yasuhiro Inami (@inamiy) | Twitter</a>さんのStoryboard/AutoLayout相談会 、3日目は<a href="https://twitter.com/1amageek">nori &#x6751;&#x672C;&#x7AE0;&#x61B2; - Firebase&#x306E;&#x4EBA; (@1amageek) | Twitter</a>さんのFirebase相談会 。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180901/20180901132407.jpg" alt="f:id:toyship:20180901132407j:plain" title="f:id:toyship:20180901132407j:plain" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180901/20180901135132.jpg" alt="f:id:toyship:20180901135132j:plain" title="f:id:toyship:20180901135132j:plain" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180901/20180901134851.jpg" alt="f:id:toyship:20180901134851j:plain" title="f:id:toyship:20180901134851j:plain" class="hatena-fotolife" itemprop="image"></span></p> <p>IRTは、参加者の人の積極的な参加がないと成功しなかったと思います。 積極的な参加で司会者賞をとった<a href="https://twitter.com/takasek">takasek (@takasek) | Twitter</a>さん、<a href="https://twitter.com/orga_chem">Kuniwak@A man using Vanilla DI/Mock (@orga_chem) | Twitter</a>さん、をはじめ、参加者の皆さんありがとうございました。</p> <h3>アンカンファレンス</h3> <p>もう一つの新コミュニケーションは、アンカンファレンス。 アンカンファレンスは空き部屋を使って自由なセッションしていただくというものです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180901/20180901131731.jpg" alt="f:id:toyship:20180901131731j:plain" title="f:id:toyship:20180901131731j:plain" class="hatena-fotolife" itemprop="image"></span></p> <p>わいわいswiftcのみなさんや、「iOSアプリ設計パターン」のみなさんがアンカンファレンスを開催してくださいました。</p> <p><blockquote class="twitter-tweet" data-lang="HASH(0xcca54b8)"><p lang="ja" dir="ltr">人めっちゃきた! <a href="https://twitter.com/hashtag/%E3%82%8F%E3%81%84%E3%82%8F%E3%81%84swiftc?src=hash&amp;ref_src=twsrc%5Etfw">#わいわいswiftc</a> <a href="https://twitter.com/hashtag/iosdc?src=hash&amp;ref_src=twsrc%5Etfw">#iosdc</a> <a href="https://twitter.com/hashtag/e?src=hash&amp;ref_src=twsrc%5Etfw">#e</a> <a href="https://t.co/OswpsOcevE">pic.twitter.com/OswpsOcevE</a></p>&mdash; VRChatで論理女の子良い (@hiragram) <a href="https://twitter.com/hiragram/status/1036089077609652224?ref_src=twsrc%5Etfw">September 2, 2018</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p> <p><a href="https://twitter.com/hak">Hak Matsuda (@hak) | Twitter</a>さんの「ゲーム機のアーキテクチャを語る」もありました。(聞きたかった……!)</p> <p>IRTとアンカンファレンス、どちらも本家WWDCでも味わえないようなよいイベントだったのでは、と思います。</p> <h3>企業ブース</h3> <p>今年は企業ブースも盛況でした。 企業ブースのみなさん、屋台みたいに楽しいブースをありがとうございました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180901/20180901131158.jpg" alt="f:id:toyship:20180901131158j:plain" title="f:id:toyship:20180901131158j:plain" class="hatena-fotolife" itemprop="image"></span></p> <p>Cookpadさんのブースでは、Cookpad製ゲームができたそうです。やりたかった。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180901/20180901112451.jpg" alt="f:id:toyship:20180901112451j:plain" title="f:id:toyship:20180901112451j:plain" class="hatena-fotolife" itemprop="image"></span></p> <h3>今年のiOSDCで感じたこと</h3> <p>たぶん長くiOS業界にいる方は強く感じていると思いますが、この3年のiOS業界の変化は、それ以前とは大きく異なるものです。</p> <p>iOSDCの発表テーマにも年々変化が見られます。</p> <p>そして、その変化の主役は、まさに若手の開発者のみなさんだな、というのを今回のiOSDCでひしひしと感じました。 (あ、すみません、私も含めて、年配の開発者もがんばってますよね!もちろん!)</p> <p>あと、参加者にも発表者にも年々女性エンジニアが増えていること。 これは間違いなくよい変化で、本当にうれしく思っています。</p> <p><blockquote class="twitter-tweet" data-lang="HASH(0xcca54b8)"><p lang="ja" dir="ltr">今年はすごくコミュニティイベントとしての充実度が増していた気がする。<br>会場のどこにいても楽しくてすごいよかった<br> <a href="https://twitter.com/hashtag/iosdc?src=hash&amp;ref_src=twsrc%5Etfw">#iosdc</a></p>&mdash; にわタコ (@niwatako) <a href="https://twitter.com/niwatako/status/1036261756803244033?ref_src=twsrc%5Etfw">September 2, 2018</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p> <p>たくさんグッズを配っても、美味しいランチを出しても、きれいな場所で整然と開催しても、それだけでは決してよいカンファレンスにはなりません。 スタッフだけでは決して用意できないなにかを、参加者のみなさんにつくってもらったなぁ、というのが、今年のiOSDCのまぎれもない実感です。</p> <p>人も、業界も、カンファレンスも変化していきます。 来年もお楽しみに!</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180902/20180902190226.jpg" alt="f:id:toyship:20180902190226j:plain" title="f:id:toyship:20180902190226j:plain" class="hatena-fotolife" itemprop="image"></span></p> toyship ARKit 2.0 でAR経験を共有できるようになりました。あと、アプリを実装しなくてもARできます。 hatenablog://entry/17391345971651294483 2018-06-05T19:34:35+09:00 2018-06-05T19:35:53+09:00 今朝発表された iOS 12では、ARがいままでになく身近になりました。 実は、わざわざアプリを作らなくてもARを実現できるようになっています。 iOS12でなにができるようになったかみてみましょう。 <p>今朝発表された iOS 12では、ARがいままでになく身近になりました。 実は、わざわざアプリを作らなくてもARを実現できるようになっています。</p> <p>iOS12でなにができるようになったかみてみましょう。</p> <h2>ARKit 2.0 とiOS 12</h2> <p>iOS 12 と同時に、ARKit 2.0が発表され、おおざっぱに言うと、こんなことができるようになりました。</p> <ul> <li>アプリをつくらなくてもARができるようになった。(iOS12)</li> <li>みんなでARを共有できるようになった。(ARKit 2.0)</li> </ul> <h2>アプリをつくらなくてもAR!</h2> <p>今朝 AppleがUSDZファイルを発表しました。 これは、新しいARのオブジェクトのファイルフォーマットです。</p> <p>実は iOS 12 では、独自のアプリを使わなくても、このファイルをSafariなどでそのままARとして表示することができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180605/20180605182320.png" alt="f:id:toyship:20180605182320p:plain:w200" title="f:id:toyship:20180605182320p:plain:w200" class="hatena-fotolife" style="width:200px" itemprop="image"></span></p> <p>(↑これ、ARになっているので、椅子の後ろの背景はカメラでみている画像です)</p> <p>これって結構すごいと思うんですよね。</p> <p>iPhoneアプリもAndroidアプリも作らずに、ただファイルをウェブサイトに置くだけでARができちゃうんです。</p> <p>ファイルをサイトに置くのではなく、メールで送ったりしても大丈夫。</p> <p>ARに対応した商品カタログを作ろうとすると、今までは、商品の3Dモデルをつくって、アプリを作って、アプリのなかからよびだす、というかなり大変な作業がありました。</p> <p>でも、iOS 12 なら、商品カタログサイトにUSDZファイルをおいておくだけで、その商品をARで自分の部屋に表示できるようになるんです。</p> <p>アプリを作らずにARができると、ARの活用方法がいろいろと広がります。</p> <p>例えば、新製品のUSDZファイルをつくって、登録したユーザーにメールで送るだけで、ユーザーはAR素材として使えます。 新しい商品を拡散してもらいやすくなり、販促キャンペーンに使えそうです。</p> <p>また、例えば恐竜博覧会で、恐竜のUSDZファイルをつくってウェブサイトにおいておくと、アプリをつくらなくても、その恐竜を自分の部屋に表示することができます。 USDZファイルはアニメーションも含めることができるので、恐竜が部屋の中をあるいている風景もアプリなしでつくることができます。 USDZファイルをつくって、展覧会場にiPhoneかiPadを何台か置いておけば、それだけで来場者がARを体験できますね。</p> <p>アプリをつくる予算やノウハウがなくても大丈夫です。</p> <h2>USDZとは</h2> <p>USDZとは、3Dオブジェクトを表示するための新しいファイルフォーマットです。</p> <p>仕様書はこちらです。</p> <p><a href="https://graphics.pixar.com/usd/files/USDZFileFormatSpecification.pdf">https://graphics.pixar.com/usd/files/USDZFileFormatSpecification.pdf</a></p> <p>USDZのサンプルファイルはこちらにあります。(このページにiOS 12 の端末でアクセスするだけで、3DオブジェクトをARでみることができます。)</p> <p><a href="https://developer.apple.com/arkit/gallery/">ARKit - AR Quick Look Gallery - Apple Developer</a></p> <p>USDZファイルは、Adobeを含む複数の会社のツールで作ることができるようになるそうです。</p> <h2>USDZのQuick Look</h2> <p>Safariなどの標準アプリでAR表示することも簡単ですが、自分のアプリでこのUSDZファイルをAR表示するのもとても簡単です。</p> <p>ARKitは必要ありません。</p> <p>pdfや画像ファイルなどを簡単に表示できる QLPreviewController というクラスがあり、USDZファイルもサポートしています。</p> <p>下記のコードだけで表示することができます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> QuickLook <span class="synPreProc">class</span> <span class="synType">ViewController</span><span class="synSpecial">:</span> <span class="synType">UIViewController</span> { <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">viewDidLoad</span>() { <span class="synIdentifier">super</span>.viewDidLoad() } <span class="synComment">// ボタンを押したらここを呼んでください。</span> <span class="synType">@IBAction</span> <span class="synPreProc">func</span> <span class="synIdentifier">showQuickLook</span>(_ sender<span class="synSpecial">:</span> <span class="synType">Any</span>) { <span class="synPreProc">let</span> <span class="synIdentifier">quickViewCon</span> <span class="synIdentifier">=</span> QLPreviewController() quickViewCon.dataSource <span class="synIdentifier">=</span> <span class="synIdentifier">self</span> <span class="synIdentifier">self</span>.present(quickViewCon, animated<span class="synSpecial">:</span> <span class="synType">true</span>, completion<span class="synSpecial">:</span> <span class="synType">nil</span>) } } <span class="synPreProc">extension</span> <span class="synType">ViewController</span><span class="synSpecial">:</span> <span class="synType">QLPreviewControllerDataSource</span> { <span class="synPreProc">func</span> <span class="synIdentifier">numberOfPreviewItems</span>(<span class="synStatement">in</span> controller<span class="synSpecial">:</span> <span class="synType">QLPreviewController</span>) <span class="synSpecial">-&gt;</span> <span class="synType">Int</span>{ <span class="synStatement">return</span> <span class="synConstant">1</span> } <span class="synPreProc">func</span> <span class="synIdentifier">previewController</span>(_ controller<span class="synSpecial">:</span> <span class="synType">QLPreviewController</span>, previewItemAt index<span class="synSpecial">:</span> <span class="synType">Int</span>) <span class="synSpecial">-&gt;</span> <span class="synType">QLPreviewItem</span>{ <span class="synStatement">return</span> MyARItem() } } <span class="synPreProc">class</span> <span class="synType">MyARItem</span><span class="synSpecial">:</span> <span class="synType">NSObject</span>, QLPreviewItem{ <span class="synPreProc">var</span> <span class="synIdentifier">previewItemURL</span><span class="synSpecial">:</span> <span class="synType">URL</span>? { <span class="synStatement">return</span> Bundle.main.url(forResource<span class="synSpecial">:</span> <span class="synConstant">&quot;coffee&quot;</span>, withExtension<span class="synSpecial">:</span> <span class="synConstant">&quot;usdz&quot;</span>) } } </pre> <p>これで、自分のアプリでもUSDZファイルをARとして表示することができます。</p> <p>(上記の実装ではなく、WKWebViewを使っても表示することができます。)</p> <p>このQuickLookのサポートで、ほとんどのアプリではARKitの実装をする必要はなくなったと思います。 とても簡単なので、なにかをAR的に表示したいアプリは今すぐにでも実装するべきです。</p> <h2>みんなでARを共有できるようになった</h2> <p>さて、今回できるようになった二つ目の機能は、ARが完全に共有できるようになったこと、です。</p> <p>「ARを完全に共有する」とはなんでしょうか。</p> <p>たとえば、いままでもポケモンGOなどでARの経験自体は共有できていました。 街角でそこにいる全員でホウオウを倒したりするのも、ARの共有の一つです。</p> <p>ただ、その時にその場にいる全員のiPhoneのなかでホウオウの3Dデータが全く同じ動きをしていた(同時に羽ばたいたり、同時に声をあげたり)していたわけではありません。 ホウオウのHPは共有されていても、3Dモデルの動きはiPhoneごとに少しづつ異なっています。</p> <p>ポケモンGOなら、3Dモデルのデータを正確に同期する必要はありません。 みんなで一緒にホウオウを倒せばAR経験は共有できます。</p> <p>でも、3Dパズルなどでは、3Dモデルのデータの状態が正確に同期できないとAR経験が共有できません。</p> <p>今回のARKit 2.0では、ARで表示している座標系と3Dモデル情報をまとめてアーカイブできる機能が追加されました。</p> <p>まとめてアーカイブした情報を保存しておけば、ARの状態をSave/Loadでき、この情報をシェアすれば大勢でARの情報を完全に共有することができます。</p> <p>実は、これは今までもがんばって実装すればできないことはなかったんですが、認識したWorld座標、表示している3Dオブジェクトすべての情報をアーカイブするのはそこそこ大変な実装です。</p> <p>ARKit 2.0では、それをARWorldMapクラスが全部サポートしてくれるようになりました。</p> <p>ARWorldMapの使い方は下記に開設されています。</p> <p><a href="https://developer.apple.com/documentation/arkit/arworldmap/archiving_world_map_data_for_persistence_or_sharing">Archiving World Map Data for Persistence or Sharing | Apple Developer Documentation</a></p> <p>データのsaveはこんな感じです。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">writeWorldMap</span>(_ worldMap<span class="synSpecial">:</span> <span class="synType">ARWorldMap</span>, to url<span class="synSpecial">:</span> <span class="synType">URL</span>) throws { <span class="synPreProc">let</span> <span class="synIdentifier">data</span> <span class="synIdentifier">=</span> try NSKeyedArchiver.archivedData(withRootObject<span class="synSpecial">:</span> <span class="synType">worldMap</span>, requiringSecureCoding<span class="synSpecial">:</span> <span class="synType">true</span>) try data.write(to<span class="synSpecial">:</span> <span class="synType">url</span>) } </pre> <p>loadはこちらになります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">loadWorldMap</span>(from url<span class="synSpecial">:</span> <span class="synType">URL</span>) <span class="synSpecial">-&gt;</span> <span class="synType">ARWorldMap</span> throws { <span class="synPreProc">let</span> <span class="synIdentifier">mapData</span> <span class="synIdentifier">=</span> try Data(contentsOf<span class="synSpecial">:</span> <span class="synType">mapURL</span>) <span class="synStatement">guard</span> <span class="synPreProc">let</span> <span class="synIdentifier">worldMap</span> <span class="synIdentifier">=</span> try NSKeyedUnarchiver.unarchivedObject(of<span class="synSpecial">:</span> <span class="synType">ARWorldMap.classForKeyedUnarchiver</span>(), from<span class="synSpecial">:</span> <span class="synType">mapData</span>) <span class="synStatement">as</span>? ARWorldMap <span class="synStatement">else</span> { throw ARError.invalidWorldMap } <span class="synStatement">return</span> worldMap } </pre> <p>楽ですね……。</p> <h2>AR Multiuser Sample</h2> <p>Appleから、ARWorldMapのシンプルなサンプルが提供されているので、それをみてみるといいでしょう。</p> <p><a href="https://developer.apple.com/documentation/arkit/creating_a_multiuser_ar_experience">Creating a Multiuser AR Experience | Apple Developer Documentation</a></p> <p>このサンプルアプリは、機能がシンプルでわかりやすいです。</p> <p>起動すると、AR画面が表示され、画面をクリックすると、このたぬき(?)が画面に何体も表示されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180605/20180605181323.png" alt="f:id:toyship:20180605181323p:plain:w200" title="f:id:toyship:20180605181323p:plain:w200" class="hatena-fotolife" style="width:200px" itemprop="image"></span></p> <p>あとから起動したデバイスに、先に起動したデバイスのタヌキが共有されるはずです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180605/20180605174208.png" alt="f:id:toyship:20180605174208p:plain:w500" title="f:id:toyship:20180605174208p:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>ちなみに、ファイル情報の共有にはMultipeerConnectivityを使っていますが、通信プロトコルは問わないので、別の方法でも大丈夫です。</p> <h2>SwiftShot Sample</h2> <p>Appleからは、もう一つARの共有サンプルが提供されています。</p> <p><a href="https://developer.apple.com/documentation/arkit/swiftshot_creating_a_game_for_augmented_reality">SwiftShot: Creating a Game for Augmented Reality | Apple Developer Documentation</a></p> <p>こちらも、基本的にはMultipeerConnectivityを使ってARWorldMapの情報を共有するアプリですが、商業レベルのゲームとして作られているのでコードの量が非常に多いです。</p> <p>認識した平面の上に木のブロックが表示され、パチンコで撃ち合うアクションゲームですね。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180605/20180605181302.png" alt="f:id:toyship:20180605181302p:plain:w500" title="f:id:toyship:20180605181302p:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>WWDC会場で動作しているこのアプリのデモの様子です。</p> <iframe width="560" height="315" frameborder="0" allowfullscreen="" src="//www.youtube.com/embed/jD8Z1nhcIdY"></iframe> <p><br><a href="https://youtube.com/watch?v=jD8Z1nhcIdY">iOS 12 multiplayer AR gaming first look</a></p> <h2>Scan</h2> <p>また、ARKit 2.0では、3D オブジェクトのスキャンもできるようになっています。</p> <p>こちらのサンプルアプリは、bounding boxを表示して、その中のオブジェクトをスキャンするようになっています。</p> <p><a href="https://developer.apple.com/documentation/arkit/scanning_and_detecting_3d_objects">Scanning and Detecting 3D Objects | Apple Developer Documentation</a></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180605/20180605190527.png" alt="f:id:toyship:20180605190527p:plain" title="f:id:toyship:20180605190527p:plain" class="hatena-fotolife" itemprop="image"></span></p> <p>使い方がちょっと複雑で、何かを一回スキャンし、その物体から ARReferenceObject を作成し、そのファイルをもう一度Projectに追加して、その ARReferenceObject と同じ物体を検知するようになっています。</p> <p>まだベータだからか、認識率は少し悪いようで、ARReferenceObject の作成に何回も失敗してしまいました。</p> <p>下記のことを注意すれば、認識率があがるようです。</p> <ul> <li>ARKitは背景がクリアな背景でオブジェクトを確認します。オブジェクトは模様があったほうが検知しやすいです。</li> <li>オブジェクトのスキャンは、テーブルの上にのるような小さいものがよいでしょう。</li> <li>検知するオブジェクトは、ARReferenceObjecと全く同じ形でなくてはいけません。柔らかいものより、固いもののほうがよいでしょう。</li> <li>検知は照明の条件がよいところにしましょう。安定した室内照明がよいでしょう。</li> <li>高品質のオブジェクトスキャンは、デバイスのパフォーマンスを必要とします。高性能のiPhoneだとよりよい品質となります</li> </ul> <h2>まとめ</h2> <p>iOS 12ではアプリをつくらなくてもARができます!</p> <p>イベントや販促、教育などに是非つかってみてください。</p> toyship バリ島で開発合宿 hatenablog://entry/8599973812344341622 2018-02-19T10:20:35+09:00 2018-03-01T20:05:00+09:00 フリーランスエンジニアをはじめて1年半たちました。 お仕事を紹介してくださる方々のおかげに加えて、ますます逼迫してきているモバイルエンジニア不足もあり、そのあいだお仕事が途切れることはありませんでした。 ただ、1週間単位の休暇がとれないと、ほぼ一年中仕事している感じでリフレッシュできないんですよね……。 かといって、仕事は好きなので、長期休暇をとりたいわけではない。 なら、仕事を休んで旅行に行くんじゃなく、旅行しながら仕事すれば全部解決じゃん!ということで、バリのプライベートヴィラを借りて、一人開発合宿をしてきました。 プライベートヴィラ 私が行ったのはDisini Luxury Spa Vi… <p>フリーランスエンジニアをはじめて1年半たちました。</p> <p>お仕事を紹介してくださる方々のおかげに加えて、ますます逼迫してきているモバイルエンジニア不足もあり、そのあいだお仕事が途切れることはありませんでした。</p> <p>ただ、1週間単位の休暇がとれないと、ほぼ一年中仕事している感じでリフレッシュできないんですよね……。</p> <p>かといって、仕事は好きなので、長期休暇をとりたいわけではない。</p> <p>なら、仕事を休んで旅行に行くんじゃなく、旅行しながら仕事すれば全部解決じゃん!ということで、バリのプライベートヴィラを借りて、一人開発合宿をしてきました。</p> <h2>プライベートヴィラ</h2> <p>私が行ったのはDisini Luxury Spa Villasという、プール付きのヴィラです。</p> <p>バリの中でもショッピングの中心、スミニャックにあり、2、3分あるけばショッピングモールや飲食店、マッサージ店がたくさんあり、食事に困ることはありませんでした。</p> <p>一つ一つの部屋が高い塀でしきられていたので、自宅感覚ですごせました。 広さは195 m²で、机やソファの数も多く、飽きずに仕事ができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180130/20180130175920.jpg" alt="f:id:toyship:20180130175920j:plain:w500" title="f:id:toyship:20180130175920j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <h2>開発合宿の日課</h2> <p>朝8時か9時くらいにおきて、庭で朝ごはん。 ヴィラの従業員さんが朝食をもってきてくれます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180203/20180203101123.jpg" alt="f:id:toyship:20180203101123j:plain:w500" title="f:id:toyship:20180203101123j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>そのあとは庭のソファで仕事。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180130/20180130180004.jpg" alt="f:id:toyship:20180130180004j:plain:w500" title="f:id:toyship:20180130180004j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>仕事にあきたら庭のプールで泳いだり、プールサイドのソファで仕事をしたり。</p> <p>お昼になると、外のカフェでお昼ごはん。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180202/20180202151512.jpg" alt="f:id:toyship:20180202151512j:plain:w500" title="f:id:toyship:20180202151512j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>そのあと、街角のマッサージ屋さんで30分くらいフットマッサージをしてもらって帰宅。</p> <p>夜は部屋の中で10時くらいまでお仕事。</p> <p>そのあとはベッドで本を読んだりテレビをみたりしてごろごろと過ごしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180130/20180130175930.jpg" alt="f:id:toyship:20180130175930j:plain:w500" title="f:id:toyship:20180130175930j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>また、夜はバリダンスも見に行ったりもしていました。</p> <h2>どこでもWiFi</h2> <p>バリ島は、仕事がとてもしやすい環境だったんですが、その一番の理由がWifi。</p> <p>ホテルやレストランはもちろん、どんな小さい喫茶店でもWiFiがありました。 驚いたことに、マッサージ店にも必ずありました。</p> <p>東京だと、WiFiがある店の方が少ないので、外で仕事をするとき困ることが多いですが、バリ島にはWiFiがつながらない店は一件もありませんでした。</p> <p>どこでもWiFiがつながるので、現地のSIMなども買わずにすませることができました。</p> <p>2020年のオリンピックでは東京にも環境客がたくさんおしよせると思いますが、今の東京の状況では観光客の皆さんはかなり困るのでは……と思います。</p> <h2>英語が通じる。</h2> <p>バリは観光客が多いので、レストランや販売店の人は必ず英語がしゃべれます。 私はインドネシア語もバリ語もまったく話せないんですが、言葉が通じなくて困ることは一度もありませんでした。</p> <p>ホテルなどでは、日本語がしゃべれる人がいることも多いようです。</p> <h2>マッサージ</h2> <p>バリ島にはマッサージ店がたくさんあります。</p> <p>高級スパなどのマッサージはかなりお値段も高いんですが、街中にあるマッサージ店は気軽に入れてお値段も安くなっています。 だいたい30分のフットマッサージで500円ほど。 1時間のボディーマッサージをうけても1000円くらいしかかかりません。</p> <p>この値段なら気軽に入れるし、仕事でこわばった肩をほぐすとストレスもなくなりますよね。まあバグがなくなるわけじゃないですけど。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180203/20180203143600.jpg" alt="f:id:toyship:20180203143600j:plain:w300" title="f:id:toyship:20180203143600j:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <h2>食事</h2> <p>食事の値段は日本とそれほど変わらず、一食1000円強くらいでした。 味は洗練されており、どこの店にはいっても美味しかったです。</p> <p>バリ島は観光地化がすすんでいるので、インドネシア料理だけではなく、フランス料理やイタリア料理なども美味しいらしいです。</p> <p>一番有名なご当地料理、ナシゴレン。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180201/20180201191209.jpg" alt="f:id:toyship:20180201191209j:plain:w500" title="f:id:toyship:20180201191209j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>黒米を煮たデザート。ココナッツアイス添え。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180201/20180201194849.jpg" alt="f:id:toyship:20180201194849j:plain:w500" title="f:id:toyship:20180201194849j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>名前はわからないんですが、美味しかったカニ。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180203/20180203210211.jpg" alt="f:id:toyship:20180203210211j:plain:w500" title="f:id:toyship:20180203210211j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <h2>バリ島の開発合宿</h2> <p>プライベートヴィラは、外の声があまり聞こえず、他の人にみられることもないので、日本の旅館内など開発合宿をするよりも安心に仕事ができました。</p> <p>ファミリー向けの広いヴィラもあるので、人数が多いと合宿費用もかなり安くすることができると思います。</p> <p>集中できるし、ほどよく気分転換もできるので、バリ島開発合宿おすすめします。</p> <p>追記:帰ってきてあらためて調べてみたら、バリ島は、ノマドワーキングの場所としてかなり有名なようです。そのあたり、また行って調べてみるつもりです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180131/20180131143530.jpg" alt="f:id:toyship:20180131143530j:plain:w500" title="f:id:toyship:20180131143530j:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> toyship ARKit 1.5で画像認識ができるようになりました。 hatenablog://entry/8599973812340902301 2018-01-26T11:57:05+09:00 2018-01-26T19:14:45+09:00 先日、iOS 11.3のベータがリリースされました。 ARKitがアップデートされて、ARKit1.5となり、画像認識や垂直面の認識などができるようになりました。 (この記事は、公開されている情報に基づいて書いています。) ARKit 1.5 ARKit1.5でのアップデート内容はこちらです。 垂直面の平面認識の追加 ARKit内での画像認識 ビデオ系処理の改善 それぞれの項目についてみてみましょう。 垂直面の平面認識 今までのARKitでも平面認識はできましたが、認識できたのは床などの水平面だけでした。 ARKit1.5からは、壁などの垂直面も認識できるようにになりました。 コードでみてみま… <p>先日、iOS 11.3のベータがリリースされました。 ARKitがアップデートされて、ARKit1.5となり、画像認識や垂直面の認識などができるようになりました。</p> <p>(この記事は、公開されている情報に基づいて書いています。)</p> <h2>ARKit 1.5</h2> <p>ARKit1.5でのアップデート内容はこちらです。</p> <ul> <li>垂直面の平面認識の追加</li> <li>ARKit内での画像認識</li> <li>ビデオ系処理の改善</li> </ul> <p>それぞれの項目についてみてみましょう。</p> <h2>垂直面の平面認識</h2> <p>今までのARKitでも平面認識はできましたが、認識できたのは床などの水平面だけでした。 ARKit1.5からは、壁などの垂直面も認識できるようにになりました。</p> <p>コードでみてみましょう。</p> <p>まず、Xcodeで新規プロジェクトを選び、<code>Augmented Reality App</code> を選択します。</p> <p>このプロジェクトはデフォルトでARが使えるようになっているので、初期設定を変えるだけで平面認識ができます。</p> <p><code>viewWillAppear</code>に下記の1行を追加しましょう。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">viewWillAppear</span>(_ animated<span class="synSpecial">:</span> <span class="synType">Bool</span>) { <span class="synIdentifier">super</span>.viewWillAppear(animated) <span class="synPreProc">let</span> <span class="synIdentifier">configuration</span> <span class="synIdentifier">=</span> ARWorldTrackingConfiguration() <span class="synComment">// この行を追加</span> configuration.planeDetection <span class="synIdentifier">=</span> [.horizontal,.vertical] sceneView.session.run(configuration) } </pre> <p>平面が認識されると、下記のメソッドがよばれます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">renderer</span>(_ renderer<span class="synSpecial">:</span> <span class="synType">SCNSceneRenderer</span>, didAdd node<span class="synSpecial">:</span> <span class="synType">SCNNode</span>, <span class="synStatement">for</span> anchor<span class="synSpecial">:</span> <span class="synType">ARAnchor</span>) { <span class="synStatement">guard</span> <span class="synPreProc">let</span> <span class="synIdentifier">planeAnchor</span> <span class="synIdentifier">=</span> anchor <span class="synStatement">as</span>? ARPlaneAnchor <span class="synStatement">else</span> { <span class="synStatement">return</span> } print(<span class="synConstant">&quot;✈️Plain detected...?&quot;</span>) } </pre> <p>ちなみに、iPhoneXにiOS11.3(β1)をいれて試してみましたが、私の部屋ではうまく垂直面が認識されませんでした。 まだ初期βなので、認識精度についてはこれから改善されていくでしょう。</p> <p>(1/26追記:<a href="https://twitter.com/tokorom">&#x6240; &#x53CB;&#x592A; / ToKoRo Yuta (@tokorom) | Twitter</a>さんに教えてもらって壁にポスターをはったら垂直画面が認識されるようになりました。なにもない白い壁は認識しづらいんですね。)</p> <h2>ARKit内での画像認識</h2> <p>さて、次は画像認識です。</p> <p>技術的には今までもARを使いながらの画像認識処理は可能でした。</p> <p>ARKitでは、認識時のカメラ画像がそのまま取得できるので、その画像でVisionフレームワークのQRコード認識処理や顔認識処理をしたり、CoreMLで画像認識モデルで処理をしたりすることができました。</p> <p>今回のARKit1.5の変更では、他のライブラリを使わずにARKit単体で画像処理ができるので、かなり便利になっています。</p> <p>これもコードで試してみましょう。</p> <p>まず、同じようにXcodeで新規プロジェクトを選び、<code>Augmented Reality App</code>を選択します。</p> <p>次に認識用の画像リソースを作成します。</p> <p>アセットカタログの左下の <code>+</code> を押すと、アセット追加用のメニューが表示されるので、このなかから <code>New AR Resouce Group</code> というのを選びます。</p> <p>(Xcodeβの画面キャプチャーはNDAのため載せられないので、これはXcode9.2の画面キャプチャーです。この画像にはありませんが、Xcode9.3でひらくと、<code>New AR Resouce Group</code> を選ぶことができます。) <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toyship/20180126/20180126111812.png" alt="f:id:toyship:20180126111812p:plain" title="f:id:toyship:20180126111812p:plain" class="hatena-fotolife" itemprop="image"></span></p> <p><code>New AR Resouce Group</code>を作ったら、ファインダー上で認識したい画像ファイルをいくつか選び、Drag&amp;Dropでいれてみましょう。</p> <p>このAR用のリソースには、認識されるはずの実物の物理サイズを設定することができます。 サイズを設定しておくと認識率が向上するので、入力しておきましょう。</p> <p>それから、<code>viewWillAppear</code>に画像認識用のコードを追加しましょう。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">viewWillAppear</span>(_ animated<span class="synSpecial">:</span> <span class="synType">Bool</span>) { <span class="synIdentifier">super</span>.viewWillAppear(animated) <span class="synPreProc">let</span> <span class="synIdentifier">configuration</span> <span class="synIdentifier">=</span> ARWorldTrackingConfiguration() <span class="synComment">// ARリソースをよびだす</span> <span class="synStatement">guard</span> <span class="synPreProc">let</span> <span class="synIdentifier">referenceImages</span> <span class="synIdentifier">=</span> ARReferenceImage.referenceImages(inGroupNamed<span class="synSpecial">:</span> <span class="synConstant">&quot;AR Resources&quot;</span>, bundle<span class="synSpecial">:</span> <span class="synType">nil</span>) <span class="synStatement">else</span> { fatalError(<span class="synConstant">&quot;Missing expected asset catalog resources.&quot;</span>) } <span class="synComment">// ARリソースを認識用画像として設定する</span> configuration.detectionImages <span class="synIdentifier">=</span> referenceImages sceneView.session.run(configuration) } </pre> <p>これで、先ほどの平面認識と同様に、AR空間内のイメージを認識することができます。</p> <p>うまく画像認識できると、こちらがよばれます。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">renderer</span>(_ renderer<span class="synSpecial">:</span> <span class="synType">SCNSceneRenderer</span>, didAdd node<span class="synSpecial">:</span> <span class="synType">SCNNode</span>, <span class="synStatement">for</span> anchor<span class="synSpecial">:</span> <span class="synType">ARAnchor</span>) { <span class="synStatement">guard</span> <span class="synPreProc">let</span> <span class="synIdentifier">imageAnchor</span> <span class="synIdentifier">=</span> anchor <span class="synStatement">as</span>? ARImageAnchor <span class="synStatement">else</span> { <span class="synStatement">return</span> } <span class="synPreProc">let</span> <span class="synIdentifier">referenceImage</span> <span class="synIdentifier">=</span> imageAnchor.referenceImage print(<span class="synConstant">&quot;image is detected: </span><span class="synSpecial">\(String(describing: referenceImage.name)</span><span class="synConstant">)&quot;</span>) } </pre> <p>自分のiPhoneX(β1)で試してみたところ、ロゴイメージなどはかなり高精度で認識されています。 他のフレームワークを使うのにくらべると実装が圧倒的に楽ですね。</p> <p>AppleからARKitの画像認識のサンプルコードとガイド記事もでています。 <a href="https://developer.apple.com/documentation/arkit/recognizing_images_in_an_ar_experience">Recognizing Images in an AR Experience | Apple Developer Documentation</a></p> <p>上記のガイド記事には、画像認識しやすくするための注意点が記載されています。</p> <ul> <li>ハイコントラストの画像を選ぼう</li> <li>ARリソースに設定した時に、物理サイズを設定しておこう</li> <li>平面でなく、曲面に表示されたもの(ワインボトルのラベルなど)は認識されにくい</li> <li>光の状態にも注意。光を反射するものにイメージが表示されている場合、光の反射で認識されにくくなることがある。</li> </ul> <h2>ARKit 1.5その他</h2> <p>今回から、ARKitでオートフォーカスが使えるようになりました。 画像認識しているときなど、特定の面にオートフォーカスできるようになり、認識精度が向上しています。</p> <p>あと、個人的には、今回から認識された座標軸を再設定することができるようになった点なども試してみようかと思っています。</p> toyship はてなブログにひっこしました hatenablog://entry/8599973812334696512 2018-01-07T21:21:45+09:00 2019-01-17T10:00:23+09:00 このブログは、今までさくらレンタルサーバーのWordpressをで配信していたんですが、はてなブログにひっこしました。 なぜひっこしたのか Wordpressでは、デフォルトではマークダウンを使った編集ができません。 プラグインをいれると、マークダウン編集を含めていろいろできることは広がるんですが、本体とプラグインのバージョンアップが頻繁にあるので更新が面倒なんですよね。 業務じゃないので、そこまで手のかけるのはつらいなあ、と。 また、Wordpressは高機能でよいCMSですが、パフォーマンスチューニングなどはそれなりに手間がかかります。 もう個人サイトで使うものでもないのかなという気もしま… <p>このブログは、今までさくらレンタルサーバーのWordpressをで配信していたんですが、はてなブログにひっこしました。</p> <h2>なぜひっこしたのか</h2> <p>Wordpressでは、デフォルトではマークダウンを使った編集ができません。 プラグインをいれると、マークダウン編集を含めていろいろできることは広がるんですが、本体とプラグインのバージョンアップが頻繁にあるので更新が面倒なんですよね。 業務じゃないので、そこまで手のかけるのはつらいなあ、と。</p> <p>また、Wordpressは高機能でよいCMSですが、パフォーマンスチューニングなどはそれなりに手間がかかります。 もう個人サイトで使うものでもないのかなという気もしますね。</p> <p>でも、ひっこしの一番の理由は、https化でした。</p> <p>自分で証明書をとることもできるんですが、AppleのApp Transport Security (ATS)に対応した証明書にしないといけないし……とか考えると、これもまた面倒だな、と。</p> <p>はてなブログでは、独自ドメインでのhttps化もマイルストーンにいれてくれているということなので、もうおまかせしてしまうことにしました。</p> <p>来年でweb serverがうまれて30年。 もうそろそろ素人がホスティングする時代はおわりつつあるのかも。</p> <h2>今までの歴史</h2> <p>このブログも、実はかなり長くやっています。</p> <p>whoisでドメイン取得日時をみてみたら、2002年でした。 独自ドメインをとる前にも、レンタルサーバーで配信していたので、下手するともう20年近くhttpサーバーをたてていたことになります。</p> <ol> <li>レンタルサーバーで静的htmlファイルで運営。(たぶん20世紀くらいから)</li> <li>レンタルサーバーでNucleus CMSで運営。独自ドメイン。(2002年から)</li> <li>レンタルサーバーでWordpress 2系で運営。独自ドメイン。(2005 or 2006年くらいから)</li> <li>レンタルサーバーでWordpress 3系で運営。独自ドメイン。(2011年から)</li> <li>はてなブログで運営。独自ドメイン。(2018年から)</li> </ol> <p>20年近くhttpサーバーをたてているのに、全く素人の領域から卒業できず、途中からはじめたiOSでお仕事できてるのはよく考えると少し不思議です。 やっぱり経験年数より愛ですかね。</p> toyship Xcode9でのMarkdownレンダリング hatenablog://entry/8599973812334281335 2018-01-04T23:42:24+09:00 2018-04-12T15:36:32+09:00 Xcode9ではマークダウンファイルはデフォルトではレンダリングされません。レンダリングするためにはどうしたらよいでしょうか。 マークダウンファイルのレンダリング Xcodeのマークダウンファイルでソースコードへのリンクができるときいてやってみようとしたところ…… Xcodeのソースコメントに- Tag:をいれると、マークダウンファイルなどからソースコードへのリンクが簡単に作れるというお話。これで、気になったところのリストアップとかドキュメンテーションがはかどる。 https://t.co/7lwfbvRvMb— Kaoru (@TachibanaKaoru) 2018年1月3日 Xcodeで… <p>Xcode9ではマークダウンファイルはデフォルトではレンダリングされません。レンダリングするためにはどうしたらよいでしょうか。</p> <h2>マークダウンファイルのレンダリング</h2> <p>Xcodeのマークダウンファイルでソースコードへのリンクができるときいてやってみようとしたところ……</p> <p><blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Xcodeのソースコメントに- Tag:をいれると、マークダウンファイルなどからソースコードへのリンクが簡単に作れるというお話。これで、気になったところのリストアップとかドキュメンテーションがはかどる。 <a href="https://t.co/7lwfbvRvMb">https://t.co/7lwfbvRvMb</a></p>&mdash; Kaoru (@TachibanaKaoru) <a href="https://twitter.com/TachibanaKaoru/status/948379647368024064?ref_src=twsrc%5Etfw">2018年1月3日</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p> <p>Xcodeでは、そもそもマークダウンファイルはレンダリングされず、生のマークダウンファイルのままで表示されてしまいました。 (Playgroundファイルでは、ファイルの中のマークダウンレンダリングをon/offする「Editor>Show Rendered Markup」メニューがあるんですが、通常のファイルではこのメニューが表示されません。)</p> <p>でも、最近のAppleが配布しているサンプルコードだと、プロジェクトファイルに含まれたマークダウンファイルはレンダリングされてますよね。それはどうやっているんでしょう。</p> <h2>Xcodeでのレンダリングon/off</h2> <p>Appleのサンプルファイルを調べてみると、レンダリングのon/offは、プロジェクトファイルのなかに特定のplistファイルがあるかどうかで決定されていました。</p> <p>ファイルパスはこちら。(プロジェクトファイル名を「MyFirst」とします) MyFirst/MyFirst.xcodeproj/.xcodesamplecode.plist</p> <p>ファイルの中身はこちらです。</p> <pre class="code" data-lang="" data-unlink>&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt; &lt;!DOCTYPE plist PUBLIC &#34;-//Apple//DTD PLIST 1.0//EN&#34; &#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;&gt; &lt;plist version=&#34;1.0&#34;&gt; &lt;array/&gt; &lt;/plist&gt;</pre> <p>このファイルをおくと、自分のプロジェクトでもマークダウンファイルがちゃんとレンダリングされました。</p> <h2>ソースコードへのリンク</h2> <p>このマークダウンファイルには、通常のマークダウン文だけでなく、ソースコードへのリンクも記述できます。</p> <p>マークダウンファイルにこちらをかき、</p> <pre class="code" data-lang="" data-unlink>[View in Source](x-source-tag://AddIdle)</pre> <p>ソースコードにこちらをかいておくと、</p> <pre class="code" data-lang="" data-unlink> /// - Tag: AddIdle</pre> <p>リンクをクリックした時にその場所にジャンプします。</p> <h2>ハイパーリンク</h2> <p>また、通常のハイパーリンクなども実現できます。</p> <pre class="code" data-lang="" data-unlink>アプリをつくるときには、 [iOS Human Interface Guidelines][0] をよくよみましょう。 [0]:https://developer.apple.com/ios/human-interface-guidelines/</pre> <h2>画像の表示</h2> <p>画像の表示もできます。 ファイルパスは、プロジェクトの中の相対パスで指定してください。</p> <pre class="code" data-lang="" data-unlink>![神崎蘭子](idleimage/ranko.jpg)</pre> <h2>コードの表示</h2> <p>コードの表示もできます。言語の指定をしても、コードハイライトはしてくれないようです。</p> <pre class="code" data-lang="" data-unlink> ``` swift func addIdle(name: String, id: Int){ let idle = Idol(name) } ```</pre> <h2>まとめ</h2> <p>プロジェクト内に特定のplistファイルをおくと、Xcodeでマークダウンファイルがレンダリングされるようになります。</p> <p>なお、レンダリングをファイル単位でon/offするのは(いまのところ)できないようです。</p> <p>便利な機能ではあると思いますが、現状ではレンダリングをon/offするのに手間がかかるのでちょっと使いどころに迷いますね。 Githubにはこのファイルをcommitしておいて、マークダウンファイルの編集をするときだけローカルで削除するなどの作業フローがいいかもしれません。</p> <p>また、この記事の内容はXcodeVersion 9.2 (9C40b)で調べた情報なので、他のバージョンでは動作が異なるかもしれません。 できれば、Xcodeのバージョンアップでon/offが簡単になるといいですね。  </p> <h2>参考リンク</h2> <p>下記のサイトを参考にさせていただきました。</p> <ul> <li><a href="https://dev.to/danielinoa_/rendering-markdown-in-xcode-9">Rendering Markdown in Xcode 9</a></li> <li><a href="https://cocoaengineering.com/2018/01/01/some-useful-url-schemes-in-xcode-9/">Some useful URL schemes in Xcode 9</a></li> </ul> toyship