20210611のiOSに関する記事は6件です。

[WWDC 2021] Meet in-app events on the App Store メモ

[WWDC 2021] Meet in-app events on the App Store のメモを残しておきますので、皆様の参考になれば幸いです。 Meet in-app events on the App Store - WWDC 2021 - Videos - Apple Developer 重要: 本記事は2021年06月時点の in-app events ローンチ前の情報になるため、サービス開始時には変更があるかもしれません。 Introduction ディープリンクで指定したアプリ内イベントを App Store で見つけられるようになる 今秋後半(later this fall)から iOS, iPadOS で利用可能 アプリ内イベントは App Store でアプリのイベントを紹介し、そのリーチを拡大する全く新しい方法 App Store ユーザが App Store でアプリ内イベントを見つける方法 イベントカード アプリ内イベントは App Store のイベントカードに表示される 画像かビデオをサポート イベントカードにはイベント名、説明、時間が表示される イベントが近づいたり始まると自動的にイベントカードが更新される アプリをインストールしているユーザならタップしてすぐにイベントに参加できる イベント詳細ページ イベントカードをタップするとイベントの詳細ページが表示される 詳細ページには詳細な説明と大きなメディアセットを使える アプリの App Store URL にイベントIDを追加して、イベント詳細ページに直接リンクすることもできる イベント開始の2週間前から宣伝(公開)することが可能 通知 ユーザは App Store 内の通知ボタンからイベント開始の通知を簡単に受け取れる 通知を設定するとイベントの(プッシュ)通知が届き、通知からアプリ内イベントを直接起動できるようになる App Store での検索 App Store 内の検索でイベントを見つけることができる アプリを持っているユーザなら、アプリと一緒にイベントカードが表示される アプリを持っていないユーザなら、スクリーンショットが表示される ゲームタブとアプリタブには持っているアプリの最新のイベント情報が表示される パーソナライズされたおすすめのイベントが表示される(機械学習がここでも使われるのか) Appleの編集チームがエキサイティングで革新的なイベントをキュレーションします App Store でのアプリ内イベントは iOS と iPadOS で利用できるようになる App Store Connect App Store Connect でアプリ内イベントを設定する方法 イベントを作成する イベントを新規作成 App Store Connect にログインして、アプリに移動 左側の[Feature]下にある[In-App Events]オプションをクリック [Create In-App Event]をクリック イベントの参照名を入力して[Create] 参照名は App Store Connect でのみ利用される 最大文字数は64文字 イベントが作成されたら、イベントページに移動する [Event Information] 現在の言語から利用可能な全ての言語を選択する Event Information の全ての項目はローカライズされる [Name]にイベント名を入力 最大30文字 ここで入力したイベント名は App Store 全体で使われる 左側に項目を入力すると右側にイベントカードと詳細ページのプレビューが表示される [Short Description]にカードで使う説明文を入力 最大50文字 [Event Card Media]に画像かビデオを登録する (画像とビデオの対象フォーマットは不明) [Long Description]に詳細ページで使う説明文を入力 最大120文字 [Event Detail Page Media]に詳細ページで使うメディアを登録する カードと詳細ページの両方で使うビデオはループする ビデオは30秒以内 [Badge]ではドロップダウンからイベントタイプを選択する このバッジはカードと詳細ページで表示される [Country or Region Availability] [Availability]から対象となる国か地域を選択する 地域ごとにイベントを用意できる [Start/End Date and Time]にイベント開始と終了日を入力 イベント期間は最長31日、最短15分 [Publish Start Date and Time]にイベント公開日を入力 イベント開始日から最大14日前 [Customize Dates and Times]で地域ごとに時間をカスタマイズ カスタム日付はデフォルト設定から48時間以内 時差を考慮して国ごとに時刻をずらす場合などに利用できる [Scheduling] イベントが終了すると App Store でイベントカードは表示されなくなる ただし、イベント終了後も30日間は App Store のURLから詳細ページを開ける イベント終了から30日後にアーカイブされる ※30日間の日数は変更できないが、アーカイブをもっと早くすることはできる [Additional Information] [Reference Name]で参照名を編集 [Event Deep Link]にイベント用のディープリンクを入力 ユニバーサルリンク or カスタムURL 不要なリダイレクトを避けるため短縮URLなどは避ける [Event Purpose]でイベントの目的をチェックボックスから選択する ここで選択する目的は App Store がおすすめするアプリに使用される 選択した目的は App Store での検索には影響しない [Priority]でイベントの優先度を指定 Normal or High [Requires In-App Purchase or Subscription]でイベントにIAPか定期購読が必要かチェックする この情報はイベントカードと詳細ページに表示される [Primary Language]で第一言語を選択 最後に[save]ボタンで保存する 保存すると[Draft]に表示される イベントを App Store に公開する 重要: アプリ内イベントを App Store で公開するには、Appleがレビューする必要がある レビュー用にイベント追加する [Draft]からイベントページ右上の[Add for Review]ボタンをクリック 必要なメタ情報を入力したら、レビュー用にイベントを追加できる イベントのためにアプリを用意する必要はない レビュー後 レビューを通過したら、承認状態になり自動で公開される [Set to Public]:未来のイベント [Published]:公開中のイベント [past]:終わったイベント [Archived]:アーカイブされたイベント リンクからアクセスできなくなる (リジェクトされた時にどうなるのかは不明) 制限 Max of 10 approved 承認されたイベントは10個まで Max of 5 published 一度に公開できるイベントは5個まで 制限を超える場合、レビュー用のイベントを追加できない App Analytics App Analytics でイベントのパフォーマンスを測定する方法 イベントが公開されたら App Analytics でイベントの計測ができるようになる App Store Connect のイベントページから App Analytics にリンクする予定 App Analytics で重要な指標(metrics)を確認できるようになる Impressions App Storeでイベントを見たユーザ数やイベントを見た場所 Opens イベントからアプリを開いたユーザ数やイベント詳細ページを開いたユーザ数 Downloads イベントの結果としてアプリを(新規/再)ダウンロードしたユーザ数 Notifications アプリ内イベントの通知を有効にしたユーザ数 App Store Connect API アプリ内イベント機能が App Store Connect API でサポートされる仕様は今年後半(later this year)に公開 アプリ内イベントは今秋後半(later this fall)に公開予定 感想: スピーカがアプリ内イベントは「App Store でアプリのイベントを紹介し、そのリーチを拡大する全く新しい方法」と言っていたのを視聴後に思い出した。 初めはまたいつものセリフかと思っていたが、優秀なビデオの影響で視聴後には「これは確かにアプリマーケティングの方法が変わるかもしない」と開発者ながらも思ってしまった。(単純) 本セッションは開発者よりも、企画マーケティングや広告会社の方に見ていただいた方が良い気がするが、開発者としても将来 in-app events をやりたいと言われた時に対応出来るように予備知識を備えておくのが良いと思っている。 イベント内容によっては開発者が追加でやることをゼロにすることも出来るし。 個人的には in-app events は有効に使った方が良いと思っているので、企画の方にも早めに in-app events を紹介し、多くのアプリが今秋には in-app events を導入できるようになって、日本のiOSアプリが盛り上がると良いなと思っている。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]音声ファイルの位相を反転させる

はじめに ノイズキャンセリングやボーカル抽出には音声の位相反転を用います。 今回はSwiftを用いて位相反転を行う方法を紹介します。 逆位相に関する記事はこちら 本編 使用するフレームワークはAVFoundationとAccelerateです。 AVFoundationで音声ファイルを読み込み、Accelerateで位相反転処理を行う、といった流れです。 今回は簡単にPlaygroundを使用して実装していきます。 1. 位相反転させたい音声ファイルを用意 位相反転させたい音声ファイルを用意します。今回はwav形式で用意しました。 2. 音声ファイルの読み込み 音声ファイルを読み込みます。 AVFoundationを使用するのでimportしてください。 Urlは各自読み替えてください。 import AVFoundation let inputUrl = URL(fileURLWithPath: NSHomeDirectory()+"/Desktop/"+"input.wav") let outputUrl = URL(fileURLWithPath: NSHomeDirectory()+"/Desktop/"+"output.wav") let input = try! AVAudioFile(forReading: inputUrl,commonFormat: .pcmFormatFloat32, interleaved: false) 3. 音声データをバッファに読み込む 読み込んだ音声ファイルの中身を読み込むためのバッファと、処理を行った後に出力するためのバッファを用意します。 バッファを定義した後、実際にinputBufferには、先ほど読み込んだ音声ファイルから中身の音声データを読み込みます。 guard let inputBuffer = AVAudioPCMBuffer(pcmFormat: input.processingFormat, frameCapacity: AVAudioFrameCount(input.length)), let outputBuffer = AVAudioPCMBuffer(pcmFormat: input.processingFormat, frameCapacity: AVAudioFrameCount(input.length)) else{ fatalError() } do{ try input.read(into: inputBuffer) }catch{ print(error.localizedDescription) } 4.バッファから音声データ配列([[Float32]])を取り出す。 取り出します。(正確にはUnsafePointer<UnsafeMutablePointer<Float>>型です) 配列はDouble型の二次元配列となっており、チャンネルごとに音声データの振幅値が入っています。 let inputFloat32ChannelData = inputBuffer.floatChannelData! let outputFloat32ChannelData = outputBuffer.floatChannelData! 5.音声データを逆位相に変換 4で取り出した二次元配列に対し、チャンネルごとに音声データの配列のそれぞれの要素に-1をかければ、逆位相になります。 for文などで配列を走査してもできないことはありませんが、計算量が多く時間がかかりすぎるので、Accerelateフレームワークの関数を使うこととします。 vDSP_vsmulはFloat32型の配列に対してスカラー倍を行う関数です。 この関数の第4引数が出力したい配列ポインタを表すので、outputを指定します。 for channel in 0 ..< Int(inputBuffer.format.channelCount) { let input: UnsafeMutablePointer<Float32> = inputFloat32ChannelData[channel] let output: UnsafeMutablePointer<Float32> = outputFloat32ChannelData[channel] var scalar:Float = -1.0 vDSP_vsmul(input, 1, &scalar, output, 1, vDSP_Length(inputBuffer.frameLength)) } outputBuffer.frameLength = inputBuffer.frameLength 6.出力のフォーマットの設定 let settings: [String: Any] = [ AVFormatIDKey: outputBuffer.format.settings[AVFormatIDKey] ?? kAudioFormatLinearPCM, AVNumberOfChannelsKey: outputBuffer.format.settings[AVNumberOfChannelsKey] ?? 2, AVSampleRateKey: outputBuffer.format.settings[AVSampleRateKey] ?? 44100, AVLinearPCMBitDepthKey: outputBuffer.format.settings[AVLinearPCMBitDepthKey] ?? 16 ] 7.出力 先程のフォーマット設定に従って出力をおこないます。 すると、1で指定したoutputUrlの場所にファイルが生成されます。 do{ let output = try AVAudioFile(forWriting: outputUrl, settings: settings, commonFormat: .pcmFormatFloat32, interleaved: false) try output.write(from: outputBuffer) } catch{ print(error.localizedDescription) } 本当に逆位相になっているか確認 前の記事でも使用した、Audacityというソフトを使います。(winでもmacでも使えます) Audacityにinputファイルと生成されたoutputファイルをインポートして再生します。 きちんと反転できていれば、音が打ち消しあい、何も聞こえません。 さいごに 読んでいただきありがとうございました。 今回作成したコードは、以下のリポジトリにあります。 https://github.com/p-x9/InvertAudioPhase
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よく見るSF Symbolsの名前をまとめてみた

SF Symbolとは? SF SymbolsはAppleが提供しているシンボルフォントです。iOSなどでの使用を念頭においてデザインされており、開発の場面で使う事も多いかと思います。 SwiftUIであれば、以下のようにして使うことができます。 SymbolSample.swift import SwiftUI struct SymbolTest: View { var body: some View { Image(systemName: "face.smiling") } } 各種Modifierを使って色や大きさを変えることができますし、そのままダークモードにも対応できるのが強みです。さらに数字を含むシンボルはローカライズに対応しているので、海外展開を考えるなら積極的に採用してもいいかもしれません。 基本的には公式サイトから一覧のアプリを入手して、目的のものを探すことになると思います。iOSのバージョンによっては使えない物もあるとはいえ、続々とシンボルが追加されています。 ただ、UIとして使う場合には多くの人が見慣れたものを使う方が操作しやすいのは明らかです。 ということで独断ではありますがよく見るシンボルの名前をまとめてみました。 共有ボタン square.and.arrow.up 主に何かを共有するときに使われているシンボルです。SF Symbolsの多くはこれに限らず、名前の末尾に.fillを加えることで塗りつぶしたものに変更できます。左の、塗りつぶしなしの方が多く使われている気がしますね。 送信ボタン paperplane TextFieldの横に配置されたりする送信ボタンに使われていることが多いです。こちらは塗りつぶしありの方がメジャーかもしれません。 ダウンロード arrow.down.to.line こちらもよく目にしますね。実は2種類あって、わかりにくいですが右の.altの方が若干矢印が長くなっています。 設定 gear こちらは設定画面を表すときによく使われる歯車のデザインです。ただ、より単純なデザインのgearshapeはiOS14以上でしか使えない模様。 検索 magnifyingglass 検索を表す時の虫眼鏡のようなデザイン。画像以外にも多くのバリエーションがあるので、検索するコンテンツごとに使い分けてもいいかもしれませんね。 履歴、最近の項目 clock よくある機能ですが、主に時計のデザインで表現されることが多いです。例えばMacのFinderの「最近の項目」にはこれが使われています。一番右のclock.arrow.circlepathはiOS14以上対応ではありますが、似たものがSpotifyで使われていました。 最後に アイコンの画像をプロジェクトに追加したりしなくても良いのは便利ですね。また、デフォルトのアプリと同じ意味合いで使えば操作しやすさにもつながるかと思います。 SF Symbolsは今後も追加されていくそうなので楽しみです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】別のViewControllerのViewを使いたい!(ContainerView編)

はじめに 前回このような記事を投稿しました。親Viewに子Viewを追加したり消去したりして別のViewControllerのViewを使うというやり方です。 しかし、このやり方だと表示したいViewControllerの高さを変えたいとなった時にレイアウトがうまくいきません。(レイアウトを張っていないからですが、、、) 以下の緑のViewに赤いView Controllerの赤いView、青いView Controllerの青いViewを表示させようとすると、真ん中のlabelが中央に表示されません。これは、表示したいViewControllerのトップと緑のViewのトップが同じ設定になってるからですね。 無理矢理やる方法もありますが、もう少し上手い方法があるので、今回はそれを紹介します。 (僕の名誉のために、レイアウトをしっかり張って対応したものは以下に載せておきます(そもそも名誉なんかない)) 今回はcenterXAnchor必要ないですけどね。 では、本題に入りましょう。 今回作るもの 赤いところと青いところがContainerViewです。ボタンを押したらテーブルビューにそれぞれ値を追加していくというシンプルなアプリを考えていきましょう。 GitHub 実装 Storyboardの作成 階層は以下のようになっています。(オートレイアウトなどはGitHubを参考にしてください) ソースコード ContainerViewを切り替えたいのでContainerAとContainerBの親Viewを用意して、ContainerAとContainerをセグメントで切り替えます。 TopViewController import UIKit final class TopViewController: UIViewController { @IBOutlet private weak var containerView: UIView! @IBOutlet private weak var containerA: UIView! @IBOutlet private weak var containerB: UIView! @IBOutlet private weak var tableView: UITableView! private var containers = [UIView]() private var texts = [String]() override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self containers.append(containerA) containers.append(containerB) containerView.bringSubviewToFront(containerA) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch segue.identifier { case "ASegueID": let redVC = segue.destination as! RedViewController redVC.delegate = self case "BSegueID": let blueVC = segue.destination as! BlueViewController blueVC.delegate = self default: fatalError() } } @IBAction private func segmentDidTapped(_ sender: UISegmentedControl) { let currentContainerView = containers[sender.selectedSegmentIndex] containerView.bringSubviewToFront(currentContainerView) } } extension TopViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { texts.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell")! let text = texts[indexPath.row] cell.textLabel?.text = text return cell } } extension TopViewController: ContainerViewDelegate { func input(_ text: String) { texts.append(text) tableView.reloadData() } } RedViewController protocol ContainerViewDelegate: AnyObject { func input(_ text: String) } final class RedViewController: UIViewController { weak var delegate: ContainerViewDelegate? @IBAction private func inputButtonDidTapped(_ sender: Any) { delegate?.input("AAAAA") } } BlueViewController final class BlueViewController: UIViewController { weak var delegate: ContainerViewDelegate? @IBAction private func inputButtonDidTapped(_ sender: Any) { delegate?.input("BBBBB") } } 解説 テーブルビューやデリゲート周りの解説は省きます。 Containerを切り替えたいので、それを格納する配列を用意します。 private var containers = [UIView]() 先ほど用意した配列にContainerを格納し、初期起動した時はcontainerViewにcontainerAを一番前に表示させます。(bringSubviewToFront) override func viewDidLoad() { super.viewDidLoad() containers.append(containerA) containers.append(containerB) containerView.bringSubviewToFront(containerA) } そして、セグメントが選択された時に表示するcontainerを配列から持ってきて先ほどの(bringSubviewToFrontを使って一番前に表示させます。 @IBAction private func segmentDidTapped(_ sender: UISegmentedControl) { let currentContainerView = containers[sender.selectedSegmentIndex] containerView.bringSubviewToFront(currentContainerView) } あとはデリゲートやらプロトコルやらを使ってテーブルビューを更新してあげれば完成です。 ちなみに、ビューヒエラルキーはこのようになっています。 伝えたかったこと 今回、ContainerViewを使ってViewControllerの切り替えを行いましたが、addChildを使ったものよりも簡単ではなかったでしょうか。addChildは追加する前にremoveする必要があったり(addSubViewせずにinsertSubViewを使えばいいだけですが)、オートレイアウトを考えなければいけませんでした。ContainerViewを使うことで、簡単にViewControllerの切り替えができることが伝えたかったことです。(コードでやるならaddChildを使うのが良さそうですね) おわりに ドキュメントは目を通しておいてください。 ContainerView
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】Metal Best Practicesの解説(2) 永続オブジェクト

Metal Best Practicesは、iOS/MacOS/tvOSのAPIであるMetalを用いた設計のベストプラクティスガイドです。 本稿では、何回かに分けてこのガイドを読み解き、コード上での実験を交えて解説していきます。 読んでそのまま理解できそうなところは飛ばしますので、原文を読みながら原文のガイドとしてご利用下さい。 過去の記事一覧 Persistent Objects (永続オブジェクト) 永続オブジェクトを早期に作成して再利用します。すべてのレンダリングや計算のループでこれらのオブジェクトを作成するのは非効率です。次に、オブジェクトとそれぞれの作成タイミングを示します。 Metalに関するオブジェクトと作成のタイミング一覧 オブジェクト名 作成タイミング MTLDevice アプリ開始時にGPUごとに1つ作成する。 MTLCommandQueue アプリ開始時にGPUごとに最低1つ作成。各キューが異なる作業をする場合は、それぞれ作成する。 MTLLibrary なるべくビルド時に構築する。Xcodeはアプリのビルド時に自動的に.metalをコンパイルして、デフォルトライブラリにビルドする。newDefaultLibraryで取得できる。 MTLFunction 作成はライブラリのビルド時と同じ。複数のパイプラインで使用する場合も同じものを再利用する。 MTLRenderPipelineState, MTLComputePipelineState アプリ開始時に必要なパイプライン毎に作成する。 MTLBuffer, MTLTexture 静的データの場合は、最初に作ったものをなるべく再利用する。動的データの場合も、最初に領域を確保しておき、データを更新するようにする。トリプルバフッファリングを用いると、フレーム毎に新しいバッファを作成せずにプロセッサのアイドル時間を最小限に抑えることができる。 コードで試してみる こちらのリポジトリにサンプルコードがあります。 今回の記事用に、PersistentObjectsというサンプルがあるのでこれを使って計測していきます。 (実行イメージ) 毎フレームごとにオブジェクトを生成する場合 効率が悪いとされるオブジェクト作成処理を毎フレームごとに入れています。 ソースコードは、Pattern1_GenerateEveryFrameです。 MTLDevice、MTLCommandQueue、MTLRenderPipelineState, MTLTexture, MTLBufferなどベストプラクティスで再利用せよと書かれているものをほとんど再作成してみます。実際のコードは処理時間を計測するために、前後でos_signpost関数を実行しています。 PersistentObjects1MetalView.swiftv func draw(in view: MTKView) { guard let drawable = view.currentDrawable else {return} // wasteful processing start let metalDevice = MTLCreateSystemDefaultDevice()! let metalCommandQueue = metalDevice.makeCommandQueue()! buildPipeline() initTexture() makeBuffers() // wasteful processing end 計測してみます、 os_signpost関数で取得したパフォーマンスデータは、InstrumentsのMetal System Traceで確認できます。(Time Profilerでも確認できますが、Metal System Traceを使うとGPUの処理と並べて比較できます) Metal System Traceの使い方はこちらの記事をご覧ください。 計測結果は次のようになりました。 フレームごとの処理は次の順番になっています。 MetalPerformance(2.44ms) ・・・ 今回計測した処理 => Metal Application(452μs) ・・・ コマンドバッファの作成処理 => GPU(11.21ms) ・・・ GPU側の処理(VertexシェーダーやFragmentシェーダーの処理など) => Built-in Display(33.33ms) ・・・ 描画 60FPSを実現するためには、16.67ms間隔で描画する必要がありますが、今回は33msかかっていました。これは、描画のためのCPU+GPUの処理が16.67msに間に合わなかったためです。このうち、今回計測した処理が2.44ms占めているのでかなりコストが大きい処理です。 内訳をみていくと次のようにっていました。所要時間の大きい順に並べています。 作成コストの内訳 オブジェクト 所要時間 MTLCommandQueue 1,100μs MTLRenderPipelineState 887μs MTLTexture 328μs MTLBuffer 204μs MTLDevice 45μs 合計 2.86ms 1つ1つは短い処理ですが、『塵が積もれば山となる』ですね。 フレームレートはだいたい30FPSぐらいになっていました。 オブジェクトを再利用した場合を試してみる 今度はオブジェクトを最初に作成したら再利用しつづけるパターンで計測してみます。 ソースコードは、Pattern2_Reuseです。 計測結果は次のようになりました。 こちらは毎フレームにオブジェクトを作成しないので、その分早くなります。ところどころ16.67msで表示できていることがわかります。フレームレートはだいたい40FPSぐらいでした。 (実はGPUの処理もだいぶ時間がかかるので、60FPSは出せない作りになっています。) 結論 ベストプラクティスにあるとおり、永続オブジェクトを早期に作成して、再利用したほうが効率的ということがわかりました。 最後に iOSを使ったARやML、音声処理などの作品やサンプル、技術情報を発信しています。 作品ができたらTwitterで発信していきますのでフォローをお願いします? Twitterは作品や記事のリンクを貼っています。 https://twitter.com/jugemjugemjugem Qiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。 https://qiita.com/TokyoYoshida Noteでは、連載記事を書いています。 https://note.com/tokyoyoshida Zennは機械学習が多めです。 https://zenn.dev/tokyoyoshida
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iPhone/iPad判別とiPadの向き判定

iPhone/iPad判別とiPadの向き判定 -iPhoneとiPadを判別 -iPadの場合はPortrait/Landscapeどちらか判定する。 if UIDevice.current.userInterfaceIdiom == .phone { // for iPhone } if UIDevice.current.userInterfaceIdiom == .pad { // for iPad let deviceOrientation: Int? = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.rawValue switch deviceOrientation { case 1, 2: // 1:Portrait, 2:Portrait Upside Down debugPrint("interfaceOrientation: Portrait") case 3, 4: // 3:Landscape Left, 4:Landscape Right debugPrint("interfaceOrientation: Landscape") default: debugPrint("unknown") } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む