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

UIViewとUITableCellの共通化

はじめに こんにちは。 今回は、私がTableViewを使うプロジェクトでよく使っている、UIViewとUIViewCellの共通化について書きます。 UIViewとUIViewCellで同じようなデザインを使うことが多くて、この方法を見つけたときは感動しました。 TableViewがあるプロジェクトでは、ほぼ必ず使ってます。 ViewとCellの共通化が有効な場面 UITableViewのヘッダーにテーブルの項目タイトルをつけたい時 通常はViewとして使う部品をTableViewのCellとしても利用したい 実装 仕組み作り こんな感じで、プロトコルを1つ作ります。 CellとViewで共通利用したいカスタムビューにこのプロトコルを継承させればOKです。 extensionに定義したidentifierは、Cellをインスタンス化するときに使えるので、実装しておきます。 public protocol CellViewBinder { associatedtype ContentView: UIView static func contentView() -> ContentView static var identifier: String { get } } extension CellViewBinder { static var identifier: String { return String(describing: self) } } こっちがUIViewを継承したカスタムビューをUICellとして利用するために作るクラスです。 class GenericTableViewCell<View>: UITableViewCell where View: CellViewBinder, View.ContentView == View { let customView: View = .init(frame: .zero) override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) configure() } required init?(coder: NSCoder) { super.init(coder: coder) configure() } private func configure() { contentView.addSubview(customView) customView.translatesAutoresizingMaskIntoConstraints = false customView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true customView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true customView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true customView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true } } customViewに自分の使いたいカスタムビューが入る構造です。 CellViewBinderプロトコルを継承しているカスタムビューだけが、GenericTableViewCellにビューを突っ込めるという仕様ですね。やはりプロトコル優秀すぎる。 実際に使う時 // Viewで使う時(例えばTableのヘッダーなどで) func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { return CustomView() } // Cellで使う時 guard let cell = tableView.dequeueReusableCell(withIdentifier: CustomView.identifier) as? GenericTableViewCell<CustomView> else { return UITableViewCell() } // 実際のViewにアクセスするのは、customView経由 let customView = cell.customView 参考文献 GenericなUITableViewCellを使う https://medium.com/eureka-engineering/thought-about-creating-view-component-ios-3cb8e525461a
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IOSの管理

はじめに CCNA試験の勉強をしていて、IOSの管理について勉強したものをまとめています。 ご指摘等ございましたらご教授下さい。 IOSには第1世代と第2世代に分かれています。 IOSイメージファイルによって、使用できる機能とできない機能があります。 第1世代の機器では用途に合わせて、適宜IOSイメージをインストールする必要がありました。 しかし、第2世代ではIOSユニバーサルイメージがインストールされており、 用途別にインストールせずに、ライセンスを購入するだけでアップグレードできます。 ルータのメモリ領域 ROM NVRAM フラッシュメモリ RAMの4つがあります。 この内、RAMだけが電源が消えると消えます。 これらのメモリを使って、ルータの起動手順を見ていきましょう。 ①ROMでPOST(自己診断プログラム)を起動します。 これはインターフェイスやハードウェアは大丈夫かを調べます。 ②起動モードは何にするか? これはROMのブートストラップがNVRAMのコンフィギュレーションレジスタに 起動モードを確認します。 ③どのIOSを起動するのか? ROMのブートストラップがNVRAMのstartup-configのboot systemコマンドに どのIOSを起動させて良いのかを確認します。 ④IOSイメージはフラッシュメモリに保管されているので、 フラッシュメモリから検索をかけます。 ⑤IOSイメージが検索できたらRAMにロードします。 ⑥ROMのブートストラップからNVRAMのstartup-config boot systemコマンドに ルータの設定情報はあるか確認します。 ⑦startup-configをRAMにロードし、running-configとして 実行します。 コンフィギュレーションレジスタンス値 たくさんありますが、まずは3パターンだけ覚えるようにしましょう。 0x2100はROMモニターモードで起動。 0x2102はstartup-configのboot systemコマンドで定義されたIOSを起動。 0x2142はNVRAMの内容を無視。 IOSイメージファイルの読み込み boot systemコマンドにIOSファイルが指定されていない時、 フラッシュメモリの中の1番目のIOSを起動するが、 もし無いとROMのmini IOSで起動。 しかし、それも無いとTFTPサーバからIOSイメージファイルを読み込む。 これも無いと、ROMモニタモードで起動する。 IOSのバックアップ RT#copy running-config tftp これはrunning-configをTFTPサーバにコピーするコマンドです。 RT#copy flash tftp これはフラッシュメモリをTFTPサーバに保管します。 RT#copy tftp flash これは反対にTFTPサーバからフラッシュメモリをダウンロードします。 パスワードリカバリーの方法 パスワードを忘れた時の方法 ①ルータの電源をOFF/ONにする。 ②60秒以内にブレーク信号を送信する。 ③コンフィギュレーションレジスタ値を変更 0x2142にすることでNVRAMを無視する。 ④ルータの再起動 ⑤startup-configの読み込み パスワードは変更したいが、他の設定情報はそのまま ⑥パスワードの変更 ⑦コンフィギュレーションモードレジスタ値を元に戻す。 ⑧startup-configへ保存する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSで 光学迷彩

移動する物体を除いた背景を作り、それをベースに物体に光学迷彩エフェクトをかける方法の紹介です。 <完成イメージ> 人物 ボトル 物体のセグメンテーションには DeeplabV3 を使っています。 1. 光学迷彩画像の作り方 この記事では物体を透かして表示することで光学迷彩を表現しています(つまり物体の位置に、物体の裏にある背景色を出力)。その際、物体の真後ろの背景色ではなく、物体の法線方向にずらした位置の背景色を取得して出力することで、それっぽく物体が歪んで見せています。 ここで、物体が3Dモデルであれば法線は自明ですが、カメラ画像から物体の法線を推定するのは大変です。LiDAR搭載機種であれば深度情報から法線を作れますが、未搭載機種で難しそうです。 そこで、本記事ではCIFilterの『CIHeightFieldFromMask』と『CIShadedMaterial』を使って擬似的に物体の形を作る方法を紹介します。 【参考】3Dモデルで光学迷彩を実現する方法『ARKit+SceneKit+Metalで光学迷彩①』 【参考】LiDAR搭載機種で取得できる深度情報『ARKit+CoreML+LiDAR で 物体のコピー』 2. 背景画像の作り方 上記の光学迷彩を実現するには、物体が無い状態の画像(背景画像)が必要です。 画像からどうやって物体を取り除くかというと、動画の各フレームを保存しておき、各フレームの画素値の中央値(≠平均値)を背景色とすることで移動する物体を取り除きます。 次のWWDC2017のビデオがわかりやすいです。 【参考】WWDC2017 Advances in Core Image: Filters, Metal, Vision, and More ※ちなみに、このビデオで画素値の中央値を取得する前にVNHomographicImageRegistrationRequestでフレーム間の位置の差を整える方法が紹介されています。2枚の画像を一致させる射影変換の行列を生成してくれるのですが試したところピーキーな動作で、わずかな手振れ程度であれば移動・回転等が補正されるものの、画像に占める移動体のサイズが大きかったり、画像の特徴点(?)が少なかったりすると変換結果が不安定&ガタガタにズレまくるのでこの記事での採用は見送りました。 3. 処理の流れ ソースコードはGithubに置いています。 背景画像を作る 物体を切り出す 切り出し結果をCGImageに変換 モルフォロジー処理で穴や小さな物体を除去(マスク画像生成) 光学迷彩画像生成 3-1) 背景画像を作る 背景画像は過去の動画フレームの中央値から作成します。 下図は直近の5つのフレームから画素毎の中央値を選択して1枚の画像を生成している時のCIImageのPreviewです。 5枚中2枚にはボールペンが写っていますが、残りの3枚には写っていないため、画像の各画素値をソートして中央に位置する画素値を選択すると、ボールペンが消えることになります。 ここでフレーム5枚を貯める時間は15fpsだと0.33秒という短い時間です。物体を取り除くにはさらに短い時間しか写り込みは許されません(高速に移動する物体でないと除去できません)。そこで、本サンプルでは5枚分の中央値を5回ためて、その中央値を計算して、さらにそれを5回ためて、その中央値を計算して、それを背景とする、という方法で時間稼ぎをしています(サンプルプログラムのcreateMedianImage())。 中央値の取得にはCIColorKernelによる独自フィルターを利用しています。 MedianImageFilter.swift override var outputImage : CIImage? { get { guard let inputList = inputList, inputList.count == Const.imageListCount, let kernel = Self.ciKernel else { return nil } let roiCallback: CIKernelROICallback = { (index, destRect) in return destRect } return kernel.apply(extent: inputList[0].extent, roiCallback: roiCallback, arguments: [ inputList[0],inputList[1],inputList[2],inputList[3],inputList[4] ]) } } inputList[0]〜inputList[4]が5枚分の画像です。 この画像を受け取って、中央値を計算するのはMSL側です。 shader.metal inline void swap(thread float4 &a, thread float4 &b) { float4 tmp = a; a = min(a,b); b = max(tmp, b); } extern "C" { namespace coreimage { float4 median(sample_t v0,sample_t v1,sample_t v2,sample_t v3,sample_t v4, destination dest) { swap(v0, v1); swap(v1, v2); swap(v2, v3); swap(v3, v4); swap(v0, v1); swap(v1, v2); swap(v2, v3); swap(v0, v1); swap(v1, v2); return v2; } } } median()が独自に実装したもので、引数のv0からv4がswift側から渡された各画像の画素値(float4)です。ここで何をやっているかというとバブルソートです。5つの値の内、3番目(中央値)がわかればいいので、3番目を決めるまでソートします。 ※前述のWWDC2017のビデオではBose-Nelson sorting networkという聞いたことがないソートアルゴリズムを使っているので(不勉強なので)、わかりやすくバブルソートにしています。 3-2) 物体を切り出す Vison+CoreML(DeepLabV3) を使って物体のセグメンテーション を行います。 やり方はこの記事『Vision+CoreMLを使って①をセグメンテーション』 と同じです。 次の20個の物体のセグメンテーションができます。 3-3) 切り出し結果をCGImageに変換 セグメンテーション結果(ラベルの値)はInt32の配列(513x513)となるのですが、ここからCGImageを作る方法は単純な方法をとっています(GPUに渡してMTLTextureにするとか、もっと早い方法がありそうですが)。 func createMaskImage(segmentedMap: MLMultiArray) -> CIImage? { let size = segmentedMap.shape[0].intValue * segmentedMap.shape[1].intValue var pixels = [UInt8](repeating: 0, count: size) for i in 0 ..< size { pixels[i] = segmentedMap[i].intValue == Const.objectLabel ? 255 : 0 } guard let segmentedImage = createCGImage(from: &pixels, width: Const.imageSize, height: Const.imageSize) else { return nil } func createCGImage(from: inout [UInt8], width: Int, height: Int) -> CGImage? { return from.withUnsafeMutableBufferPointer { pixelPointer in // 画素値配列をvImage_Bufferの形にする let sourceBuffer = vImage_Buffer(data: pixelPointer.baseAddress!, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: width) // 画像のピクセルフォーマットを定義 guard let format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 8, colorSpace: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGBitmapInfo(rawValue: 0)) else { return nil } // CGImageに変換 return try? sourceBuffer.createCGImage(format: format) } } まず、光学迷彩対象としたいラベルを白(255)、それ以外を黒(0)とする画素値の配列を作ります。 次にAccelerateフレームワークを使ってCGImageに変換します。 画素値の配列からCGImageにする方法はこちらの記事『画素値の配列からCGImageを作る』で解説しています。 3−4) モルフォロジー処理で穴や小さな物体を除去(マスク画像生成) DeeplabV3で認識した物体には穴やギザギザした部分があるため、これをモルフォロジー処理で除去します。 加工前 クロージング後 オープニング後 モルフォロジー処理もCIFilterのCIMorphologyMinimum、CIMorphologyMaximumを使っています。 使い方についてはこちらの記事『iPhone のGPUで物体の重心をリアルタイムに計算』で解説しています。 ここで得られた白黒画像をこの後の処理でマスク画像として利用します。 3−5) 光学迷彩画像作成 光学迷彩画像の作成過程は次の通りです。 ①ハイトマップ生成 ②透かした画像生成 ③物体切り出し ④背景と合成 ①ハイトマップ作成 CIFIlterのCIHeightFieldFromMaskを使います。 マスク画像をインプットとし、エッジに近いほど暗く(低く)、エッジから離れるほと明るく(高く)なる画像が得られます。この画像が物体の奥行き方向の形状となります(擬似的に作った形状で、LiDARを使えば正確に得られたであろう情報)。 // マスク画像からハイトマップ生成 heightFieldFilter.setValue(mask, forKey: kCIInputImageKey) heightFieldFilter.setValue(NSNumber(value: Const.heightFieldRadius), forKey: kCIInputRadiusKey) guard let heightFieldImage = heightFieldFilter.outputImage else { return nil } ②透かした画像生成 背景画像と①のハイトマップを入力として、背景を歪めた画像を作ります。 これもCIFilterでCIShadedMaterialを使います。 // 光学迷彩化(背景とハイトマップをミックス) shaderMaterialFilter.setValue(heightFieldImage, forKey: kCIInputImageKey) shaderMaterialFilter.setValue(CIImage(cgImage: median), forKey: kCIInputShadingImageKey) shaderMaterialFilter.setValue(NSNumber(value: Const.shaderMaterialScale), forKey: kCIInputScaleKey) guard let shaderOutput = shaderMaterialFilter.outputImage else { return nil } ハイトマップから法線が計算され、その法線の方向から背景画像の色が選択され、画像を出力してくれる、という便利なフィルタです。 ③物体の切り出し ②の結果ではマスク画像の黒い部分に対応する画素に色ついてしまうため、②の結果にマスク画像をあてて物体部分だけ切り出します。 これもCIFilterでCIMultiplyCompositingを使います。 // 光学迷彩画像のマスク部分を抽出 multiplyCompositingFilter.setValue(shaderOutput, forKey: kCIInputImageKey) multiplyCompositingFilter.setValue(mask, forKey: kCIInputBackgroundImageKey) guard let opticalObject = multiplyCompositingFilter.outputImage else { return nil } ②の結果とマスク画像を乗算することで、マスク画像の白い部分だけ残した画像を生成できます。 ④背景と合成 ここでやりたいことは、③の切り出し部分は③の画素値、それ以外は背景画像の画素値を採用した画像を作ることです。標準のCIFilterを見たところ一度にこの処理ができるようなFilterが見当たらなかったので、これもCIColorKernelによる独自フィルターで解決しています。 (CompositeFilter.swift/shader.metal) 4. 最後に 物体の切り出しにDeeplabV3を使いましたが、遅いし綺麗なセグメンテーションとは言えないかもしれません。 人物に限定するのであればARKitのARMatteGeneratorの方が高速で綺麗にセグメンテーションしてくれそうです。 【参考】ARMatteGeneratorを使った例『ARKit+Metal で みんな超サイヤ人』
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift JSON+Codableで不正なURL表現の文字列があった場合のデコードエラーを防ぐ

URLのCodable JSONなどのデータをSwiftのStructに落とし込む時、Codableが使われます。 structの各プロパティをCodableに対応したクラスにすることで、JSONをstructに変換することができます。 Codableに対応したクラスですが、Appleのドキュメントに、Encoding and Decoding Custom Types というものがあります。 ここには、String, Int, Double, Date, Data, そしてURLが対応していると列挙されています。 なので、struct中にURLがあっても、その文字列がURLで表現される場合、正しくURLとして変換されます。 以下はURLを含む文字列の例です。 JSONデータ let data = """ { "id": 1234, "name": "Hello World!", "url": "http://example.com" } """.data(using: .utf8) struct MyData: Codable { let id: Int let name: String let url: URL } let ret = try! JSONDecoder().decode(MyData.self, from: data!) print(ret) //MyData(id: 1234, name: "Hello World!", url: http://example.com) 問題 しかし、この文字列→URL変換処理は、URL(string:)で変換できることが条件となっているので、URLの部分の文字列が空文字や日本語を含むURL、またはURLとは看做さない文字列がJSONに含まれていると、JSONそのものがDecodingErrorになってしまいます。 この挙動はstructのURLがURL?とOptionalで定義してあっても同様です。 JSONデータ let data = """ { "id": 1234, "name": "Hello World!", "url": "" } """.data(using: .utf8) let ret = try! JSONDecoder().decode(MyData.self, from: data!) // Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "url", intValue: nil)], debugDescription: "Invalid URL string.", underlyingError: nil)) 対処 この問題を対処するために、一度Codableに対応した別クラス SafeCodableUrl を挟んでデコードを行うようにしてみます。 URLにできる文字列がJSONに入っていた場合、SafeCodableUrlのvalueにURLが存在し、URLにできない文字列の場合、valueはnilとなり、より安全にJSONが扱えるようになります。 struct SafeCodableUrl: Codable { let value: URL? init(from decoder: Decoder) throws { let container = try? decoder.singleValueContainer() if let string = try? container?.decode(String.self) { self.value = URL(string: string) } else { self.value = nil } } } JSONデータ let data = """ { "id": 1234, "name": "Hello World!", "url": "" } """.data(using: .utf8) struct MyData: Codable { let id: Int let name: String let url: SafeCodableUrl //文字列がURLではない場合もデコードエラーにならない } let ret = try! JSONDecoder().decode(MyData.self, from: data!) print(ret.url.value) //nil
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】UIAlertViewControllerの実装

はじめに ユーザーに確認を求めたりエラー表示をしたりとよくアラートを出したい場面があると思います。 そんな時に毎回アラートを出すコードを書くのは効率が悪いので、どこでもすぐ呼び出せるようにアラート処理用のファイルを作成しておきます。 コピーして使用できるので特に初学者の方のお役に立てればと思います。 アラートを実装 import UIKit final class Alert { // OKアラート static func okAlert(vc: UIViewController, title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) { let okAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) okAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: handler)) vc.present(okAlertVC, animated: true, completion: nil) } // OK&キャンセルアラート static func cancelAlert(vc: UIViewController, title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) { let cancelAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) cancelAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: handler)) cancelAlertVC.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)) vc.present(cancelAlertVC, animated: true, completion: nil) } // TextField付きアラート static func textFieldAlert(vc: UIViewController, title: String, message: String, placeholder: String, securyText: Bool, handler: ((String?) -> Void)? = nil) { let textFieldAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) textFieldAlertVC.addTextField { (textField) in textField.placeholder = placeholder textField.isSecureTextEntry = securyText } textFieldAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in handler?(textFieldAlertVC.textFields?.first?.text) })) textFieldAlertVC.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)) vc.present(textFieldAlertVC, animated: true, completion: nil) } // 自動で消えるアラート static func autoCloseAlert(vc: UIViewController, title: String, message: String) { let autoCloseAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) vc.present(autoCloseAlertVC, animated: true) { // 2秒後に消える DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { autoCloseAlertVC.dismiss(animated: true, completion: nil) } } } } 呼び出しコード // OKアラートを表示 Alert.okAlert(vc: self, title: "タイトル", message: "任意のメッセージ", handler: { (_) in // OKボタンを押した後の処理を書いて下さい。 }) // OK&キャンセルアラートを表示 Alert.cancelAlert(vc: self, title: "タイトル", message: "任意のメッセージ", handler: { (_) in // OKボタンを押した後の処理を書いて下さい。 }) // TextField付きアラートを表示 Alert.textFieldAlert(vc: self, title: "タイトル", message: "任意のメッセージ", placeholder: "任意の文字", securyText: false, handler: { (text) in // TextFieldに入力した文字の処理を書いて下さい。 }) // 自動で消えるアラートを表示 Alert.autoCloseAlert(vc: self, title: "タイトル", message: "任意のメッセージ")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新 App Store 審査ガイドライン 翻訳&差分ガイド 2021年10月号

はじめに これは2021年10月22日付けで変更された App Store 審査ガイドラインの翻訳&差分ガイドです。 前回からの主な変更点は、課金方法の紹介と iOS 15 の In-App Events についてです。あくまで、課金方法の "紹介" が緩和されるだけであり、アプリ内の制約は変わらず。 旧規約では「メールで他の課金方法を紹介」を禁止していましたが、該当の文章は削除され、さらに連絡先情報の取得が可能となりました。これには、米国開発者による集団訴訟の合意内容が反映されているようです。 他方、日本公正取引委員会との合意内容の「リーダーアプリ内に決済リンクを1つ設置できる」は、今回は未反映です。この内容は、2022年初めに適用予定。 また、Epic Games との訴訟は最終的な決着がついていないため、この内容も未反映です。現状、2021年12月9日までにアプリ決済の見直しを命じられているため、延期の訴えが認められない限り、12月辺りに大きな変化がありそうです。 https://developer.apple.com/app-store/review/guidelines/ In-App Events In-App Events は iOS 15 の新機能で、2021年10月27日開始が告知されています。WWDC21 では Meet in-app events on the App Store が該当します。 App Store 内のプロモーション機能のため、基本的には App Store Connect で情報を入力するだけですが、ディープリンクを実装していない場合はアプリ側にも対応が必要そう。2021年後半には、アプリ内イベント登録の API サポートや効果測定を提供予定。 https://developer.apple.com/app-store/in-app-events/ 新規項目 2.3.13 アプリ内イベントのメタデータは正確に。「3. Business」に違反しなければ収益化が可能。 In-app events are timely events that happen within your app. To feature your event on the App Store, it must fall within an event type provided in App Store Connect. All event metadata must be accurate and pertain to the event itself, rather than the app more generally. Events must happen at the times and dates you select in App Store Connect, including across multiple storefronts. You may monetize your event so long as you follow the rules set forth in Section 3 on Business. And your event deep link must direct users to the proper destination within your app. Read In-App Events for detailed guidance on acceptable event metadata and event deep links. 「3. Business」ありきなので、デジタルコンテンツは要注意。 5.1.1 (x) オプションとして連絡先情報の要求が可能。 Apps may request basic contact information (such as name and email address) so long as the request is optional for the user, features and services are not conditional on providing the information, and it complies with all other provisions of these guidelines, including limitations on collecting information from kids. 連絡先情報の提供有無による機能制限は駄目。 修正項目 3.1.3 Other Purchase Methods アプリ内課金を阻害するような施策は不可。 The following apps may use purchase methods other than in-app purchase. Apps in this section cannot, within the app, encourage users to use a purchasing method other than in-app purchase. Developers can send communications outside of the app to their user base about purchasing methods other than in-app purchase. Developers cannot use information obtained within the app to target individual users outside of the app to use purchasing methods other than in-app purchase (such as sending an individual user an email about other purchasing methods after that individual signs up for an account within the app). を削除。 「メールで他の課金方法を紹介」という具体例が消滅し、容認へ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む