20201023のiOSに関する記事は11件です。

【Xcode 12.1】line 144: ARCHS[@]: unbound variable Command PhaseScriptExecution failed with a nonzero exit code

Xcode12.1で以下のエラーで実機ビルドとarchiveできなくなり数時間ハマった。

line 144: ARCHS[@]: unbound variable
Command PhaseScriptExecution failed with a nonzero exit code

以下の方法で解決できました。

やった

  • プロジェクトのBuildSetting -> User Defined にVALID_ARCHSを追加。値は$(ARCHS_STANDARD)
  • pod install後にPodsプロジェクトのBase SDKをmacOSからiOSに変更(macOSだとARCHS_STANDARDの値がiOSと異なるので)

やらない

以下は解決方法としてぐぐると出てくるが実施しない
- Excluded Architectureにarm64を追加する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]UIProgressView を利用して進捗状況を表示 + UIAlertController を利用して処理完了を通知

書くこと

・UIProgressView を利用して進捗状況を表示
・UIAlertController を利用して処理完了を通知

処理のイメージ

開発環境

PC MacBook Air(13-inch,2017)
PC OS macOS Catalina(ver 10.15.6)
IDE Xcode(ver 12.0.1)
iPhone SE(2nd Generation)
iPhone OS ver 14.0.1
Swift ver 5.3

前提条件

・Xcode を利用してデスクトップ上に Sample というプロジェクトアプリを作成する
・今回は、 Sample にある ViewController.swift にコードを記述する
UIProgressViewMain.storyboard 上に配置
UIProgressView は画面中央に配置されるよう制約を設定している
・下記コード例はリファクタリングを行っていない

コード例

ViewController.swift
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var progressView: UIProgressView!

    override func viewDidLoad() {

        DispatchQueue.main.asyncAfter(deadline: .now() + 1){
            self.progressView.setProgress(1/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 2){
            self.progressView.setProgress(2/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 3){
            self.progressView.setProgress(3/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 4){
            self.progressView.setProgress(4/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 5){
            self.progressView.setProgress(5/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 6){
            self.progressView.setProgress(6/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 7){
            self.progressView.setProgress(7/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 8){
            self.progressView.setProgress(8/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 9){
            self.progressView.setProgress(9/10, animated: true)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 10){
            self.progressView.setProgress(10/10, animated: true)
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 11){
            let alert:UIAlertController = UIAlertController(title: "お知らせ", message: "処理が終了しました", preferredStyle: .alert)
            let action:UIAlertAction = UIAlertAction(title: "OK", style: .default, handler: nil)
            alert.addAction(action)
            self.present(alert, animated: true, completion: nil)
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        progressView.progress = 0
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発:タイマーアプリ(8.プログレスバーの実装)

内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、カウントダウンタイマーの残り時間を示す円形のプログレスバーの作成について掲載します。

環境

  • OS: macOS 10.15.7 (Catalina)
  • エディタ: Xcode 12.1
  • 言語: Swift
  • 主な使用ライブラリ: SwiftUI

手順

  1. プログレスバーの View を作成する
  2. プログレスバーの背景用の円を作成する
  3. プログレスバー用の円を作成する
  4. プログレスバーの長さを経過時間に連動させる
  5. MainView にプログレスバーを配置する
  6. プログレスバーの動きを滑らかにする

1. プログレスバーの View を作成する

ProgressBar.swift という名前で新規ファイルを作成します。この View でも TimeManager クラスからプロパティの値を参照しますので、var の前に @EnvironmentObject プロパティラッパーをつけて TimeManager クラスのインスタンスを作成しておきます。

ProgressBar.swift
import SwiftUI

struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Text("Hello, World!")
    }
}

2. プログレスバーの背景用の円を作成する

プログレスバーは、時間経過とともに短くなっていく円と、その背景となる円の2つが必要です。背景の円は、時間が経過してもずっと同じ大きさで長さが変化しません。まず先に、背景の円から作成していきます。

body{} の中に円を配置します。SwiftUI には Circle() という図形のコンポーネントが用意されていますので、これを利用します。

ProgressBar.swift
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Circle()
    }
}

図形は、輪郭線と面で構成されていますので、中空の円にするには、面を表示せず、輪郭線だけ表示し、その輪郭線の太さや長さ、色を調整します。

Circle() のモディファイアを追加して望んだ形状にしていきます。

.stroke モディファイアで、引数に Color() を入れて、さらにその引数を .darkGray にして、背景らしいグレーの色にします。

.stroke モディファイアで、style 引数の lineWidth を 20 にしてプログレスバーの太さを指定します。

.scaledToFit モディファイアで、円のサイズをスクリーンサイズいっぱいに合わせ、 .padding モディファイアでスクリーン端との余白を調整します。

ProgressBar.swift
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Circle()
                .stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
                .scaledToFit()
                .padding(25)
    }
}

3. プログレスバー用の円を作成する

これから作成するプログレスバー用の円は、手順2で作成した背景用の円とレイヤー状に重なるので、プログレスバー用の Circle() コンポーネントをもう一つ追加したら、 ZStack{} で2つの円を囲います。

ProgressBar.swift
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        ZStack {
            //背景用の円
            Circle()
                .stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
                .scaledToFit()
                .padding(25)

            //プログレスバー用の円
            Circle()
        }
    }
}

背景用の円と同様に、プログレスバー用の円もモディファイアを追加して形状を調整していきます。

.stroke モディファイアで、引数にColor() を入れ、プログレスバーの色をひとまず .cyan を指定しました。

.stroke モディファイアで、style 引数の StrokeStyle に細かな指定を入れます。lineWidth で幅を 20 に指定、lineCap で .round を指定して線の端の角に丸みを出し、lineJoin で .round を指定して線の端を線幅の1/2の長さで超えて丸みを出します。

.rotationEffect モディファイアを追加し、引数に Angle(degrees: -90) と入れます。これにより、デフォルトの円の輪郭線の開始位置が3時の方向であるのを、12時の方向に変更できます。

その他は背景用の円と同様です。

ProgressBar.swift
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        ZStack {
            //背景用の円
            Circle()
                .stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
                .scaledToFit()
                .padding(25)

            //プログレスバー用の円
            Circle()
                .stroke(Color(.cyan), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .bevel))
                .scaledToFit()
                //輪郭線の開始位置を12時の方向にする
                .rotationEffect(Angle(degrees: -90))
                .padding(25)
        }
    }
}

4. プログレスバーの長さを経過時間に連動させる

プログレスバーの円に、さらにモディファイアを追加して、カウントダウンタイマーの経過時間に連動してプログレスバーが短くなっていくようにします。

.trim モディファイアを追加します。これにより、プログレスバーを必要な長さにトリムすることができます。

開始位置はいつでも12時の方向で固定ですので、引数 from には 0 を入れます。時間経過とともにプログレスバーは短くなっていく必要があるので、終端位置を常に残り時間と連動させる必要があります。また、引数 from も to も入れる値は 0 ~ 1 の間である必要があります。

ここで少し算数です。TimeManager クラスには、Picker で設定した最大時間を格納するプロパティ maxValue と 残り時間を格納するプロパティ duration が用意してあります。この2つの値を使って、カウントダウン開始時に最大値 1 カウントダウン終了時に最小値 0 になる計算式は duration / maxValue です。

引数 to の値のデータ型は CGFloat する必要がありますので、最終的に引数 to に入れる値は以下になります。

CGFloat(self.timeManager.duration / self.timeManager.maxValue)

よって、コードは以下のようになります。

ProgressBar.swift
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        ZStack {
            //背景用の円
            Circle()
                //(モディファイア省略)

            //プログレスバー用の円
            Circle()
                .trim(from: 0, to: CGFloat(self.timeManager.duration / self.timeManager.maxValue))
                .stroke(Color(.cyan), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                .scaledToFit()
                //輪郭線の開始位置を12時の方向にする
                .rotationEffect(Angle(degrees: -90))
                .padding(25)
        }
    }
}

5. MainView にプログレスバーを配置する

MainView の body{} 内一番外側の ZStack{} の一番上に ProgressBarView のインスタンスを追加します。ZStack のコード上一番上ということは、UI コンポーネントのレイヤー階層では一番後ろになります。イメージとしては、残り時間の表示や時間設定のPickerのほうが、画面上、手前にある状態です。

また、先に 設定画面である SettingView の項目に、プログレスバーの表示/非表示のトグルスイッチを用意していますので、その設定と連動している TimeManager クラスの isProgressBaron プロパティが true の場合だけプログレスバーを表示するように if 文で記述します。

MainView.swift
struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        ZStack {
            if timeManager.isProgressBarOn {
                ProgressBarView()
            }

            if timeManager.timerStatus == .stopped {
                PickerView()
            } else {
                TimerView()
            }

            VStack {
                Spacer()
                ZStack {
                    ButtonsView()
                        .padding(.bottom)

                    SettingButtonView()
                        .padding(.bottom)
                        .sheet(isPresented: $timeManager.isSetting) {
                            SettingView()
                                .environmentObject(self.timeManager)
                        }
                }
            }
        }
        .onReceive(timeManager.timer) { _ in
            //(onReceive の中の記述省略)
        }
    }
}

6. プログレスバーの動きを滑らかにする

プログレスバーは実装できましたが、Xcode Canvas や Simulator で MainView の実際の動きを確認してみると、タイマーの設定時間が短いほど、プログレスバーが1秒毎に短くなる(カクカクした)動きがよくわかると思います。プログレスバーとして失敗ではありませんが、視覚的に滑らかなほうが洗練された印象を受けますので、ここを少し拘って修正していきます。

このカクカクした動きの原因は2つありますので、それぞれ修正していきます。

まず1つめは、TimeManager クラスの timer プロパティです。このプロパティには、Timer クラスの publish メソッドが格納されていますが、その引数 every の値が 1 になっています。これは1秒毎に発動するという意味になります。この値を 0.05 くらいに変更しておきます。検証の結果 0.01 を切るあたりから、現実の時間経過とタイマーアプリの残り時間の更新に誤差が出てくるので、0.05 くらいが限界かと思います。

TimeManager クラスで以下のように更新します。

TimeManager.swift
class TimeManager: ObservableObject {
    //(他のプロパティ省略)

    //1秒ごとに発動するTimerクラスのpublishメソッド
    var timer = Timer.publish(every: 0.05, on: .main, in: .common).autoconnect()

    //(メソッド省略)

2つ目は、MainView の onReceive モディファイア内の記述です。このモディファイアが先に修正した TimeManager クラスの timer プロパティをトリガーにして、クロージャ {} 内のコードを実行しています。トリガーを 0.05 秒更新にしたので、onReceive モディファイアのクロージャ {} 内に記述された TimeManager クラスの duration プロパティ(残り時間)の更新もまた 0.05 秒ずつマイナスされる必要があります。

MainView で以下のように更新します。

MainView.swift
struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        ZStack {
            //(省略)
        }
        //指定した時間(1秒)ごとに発動するtimerをトリガーにしてクロージャ内のコードを実行
        .onReceive(timeManager.timer) { _ in
            //タイマーステータスが.running以外の場合何も実行しない
            guard self.timeManager.timerStatus == .running else { return }
            //残り時間が0より大きい場合
            if self.timeManager.duration > 0 {
                //残り時間から -0.05 する
                self.timeManager.duration -= 0.05 //ここを更新!
                //残り時間が0以下の場合
            } else {
                //タイマーステータスを.stoppedに変更する
                self.timeManager.timerStatus = .stopped
                //アラーム音を鳴らす
                AudioServicesPlayAlertSoundWithCompletion(self.timeManager.soundID, nil)
                //バイブレーションを作動させる
                AudioServicesPlayAlertSoundWithCompletion(SystemSoundID(kSystemSoundID_Vibrate)) {}
            }
        }
    }
}

これで、例えばタイマーを5秒に設定してもプログレスバーが比較的滑らかな動きをしてくれます。
次回は、少しおまけ的要素ですが、プログレスバーの色をより美しく表示していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]Realmファイル内容の確認方法

1. この記事を書いた理由

iOSアプリを作るぞ!
→ Realm Swiftを使ってみよう!
 → 実装してみたけどRealmファイルがちゃんと作られているかわからないよ〜!
  → 確認しよう! (今ここ)

2. 実行環境

  • MacOS Catalina : 10.15.7(19H2)
  • Xcode : Version 12.0.1 (12A7300)
  • Realm Studio : バージョン5.0.1 (5.0.1.6)

(2020年10月23日辺り)

3. 手順

目次
3.1. 前提条件
3.2. Realm Studioのインストール
3.3. Realmファイルの居場所を確認
3.4. Realmファイルの確認

3.1.前提条件

  • シミュレータ上で実行。実機では試していません。(実機手順は3.3章の公式ドキュメントに記載あり)
  • Realm Swiftを用いて、Realmファイルの生成までは済んでいる状態。以下は生成の例。
example.swift
            let realm =  try! Realm()
            let data = "save data"

            try! realm.write {
                realm.add(data)
            }

3.2. Realm Studioのインストール

3.2.1. インストール

公式HPよりRealm Studio-5.x.x.dmgをダウンロード。実行してインストール。

3.2.2. 起動

Launchpadより Realm Studioを起動する。

※「発行元が信頼されていないため、起動できません」という旨の警告が表示されて起動できない場合は、以下手順を実施。

「システム環境設定」
 →「セキュリティとプライバシー」
  →「一般」
   →「ダウンロードしたアプリケーションの実行許可」
     にRealm Studioが表示されているため、許可する。

3.2.3. 起動後

サインアップを求められるので、メールアドレスを登録。
スクリーンショット 2020-10-23 20.46.22.png

3.3.Realmファイルの居場所を確認

公式情報によると、以下フォルダに生成されるとのこと。

/Users/<username>/Library/Developer/CoreSimulator/Devices//data/Containers/Data/Application/<application-uuid>/Documents/default.realm

引用元:公式ドキュメントHow to find my realm file?

ファイルの場所を確認する方法は、シミュレータ使用時にコマンド実行することで可能。
以下に説明していく。

3.3.1. Realmファイル生成後のタイミングにbreakポイントを張ってアプリを実行

スクリーンショット 2020-10-23 7.59.56.png

停止すると下部に(lldb)というコンソールが出る。

スクリーンショット 2020-10-23 8.03.42.png

3.3.2. lldbコンソールにてコマンド実行
po Realm.Configuration.defaultConfiguration.fileURL

スクリーンショット 2020-10-23 8.12.17.png

するとファイルパスが出力されます。

3.4. Realmファイルの確認

ターミナルを開き、以下のコマンドを実行する。
(前手順で調べたRalmファイルパスをopenコマンドで開く)

open /Users/<username>/Library/Developer/CoreSimulator/Devices/<simulator-uuid>/data/Containers/Data/Application/<application-uuid>/Documents/default.realm

すると Realm Studio画面が起動して・・・
ファイル内容が見れた!!! こうなってるんですね〜。
スクリーンショット 2020-10-23 6.33.01.png

4. おわりに

お読みいただきありがとうございました。
iOSアプリ開発(勉強)を初めてまだまだ新米で分からないことだらけですが、少しずつ出来ていくのが楽し〜です。
本記事もどこかの同じようなアプリ開発者の参考になれば幸いです。

またQiita記事も初投稿でしたが、こちらもこれからもっと発信していきたいな〜と思います。
もし良かったらLGTM/フォロー等のアクションいただけるとスゴい嬉しいです!?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Shift + EnterでUITextViewを改行

SmartKeyboardなども発売され、iPadにはずっとキーボードを接続して利用している。というユーザーも多いのではないかと思いますが、そのような市場の様子を見て「enterを確定や直下の新規作成操作とし、shift + enterで改行操作」と思った場合、素直に対応したAPIがあれば良いですがUITextViewDelegateには該当するものが存在しないため、少し工夫をする必要があります。

この記事ではその実現方法について解説します。

方針

こちらのStackoverflowの質問が参考にしましたが、

「UIKeyCommandをoverrideしてインターセプトし、shift + enterで独自の改行アクションを実行する」という方針になります。

実装

コードはこちらのgistに投稿しましたので参考にしてください。

今回はUITextViewのサブクラスを利用して実現を図りました。

ポイント1: keyCommandsのoverride

システム定義以外のあるキーが押されたとき、システムはresponderChainを探して該当するキーに対応するUIKeyCommandが実装されているか確認し、存在すればそのアクションを実行します。

以下の実装では、shift + return(enter)を受け取るとnewLine(sender:)というアクションを実行するようなコマンドを返しています。

override var keyCommands: [UIKeyCommand]? {
// キーボードへのinputは、returnなので"\r". うっかり"\n"としてしまう事があるが、それは押下したキーではないため.
        return [UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(newLine(sender:)))]
 }

UITextViewDelegatetextView(_:shouldChangeTextIn:replacementString:)を実装している場合であっても、このkeyCommandsの実装がインターセプトするため、テキスト入力まで辿り着かずデリゲートメソッドは呼ばれません。

ポイント2: アクションの実装

UITextViewtextプロパティに直接\nを代入したいところですが、firstResponderが外れてしまう上に予期しない挙動を引き起こしてしまいます。

そのため、現在アクティブなUITextView/UITextFieldのテキストに変更を加える場合は、UIKeyInputプロトコルに属しているinsertText(_:)メソッドを利用しましょう。

このメソッドは、テキストの現在カーソルが当たっているindexに対してStringの挿入をして、その後テキストを表示し直してくれます。(バッキングストアなどについて気になる場合は、TextKitをご参照ください)

@objc func newLine(sender: UIKeyCommand) {
    // プログラム経由のテキスト更新はデリゲートメソッドは呼ばれない
    insertText("\n")
}

プログラム経由でテキスト更新をした場合にはデリゲートメソッドは呼ばれないことに注意してください。

まとめ

以上のような2つのポイントを抑えて実装することで、Shift + Enterでの改行操作が実現可能となります。

もしもっとスマートな実装方法などご存知の場合は、ご教授頂けると助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Mapbox入門】アカウント登録〜サンプルをビルドしてみよう(iOS編)

はじめに

みなさんこんにちは、Mapbox Japan(マップボックス・ジャパン)です。「Mapbox」を聞いたことがありますか?名前は知っているよ、聞いたことがないな、使い勝手はどうなんだろう?など様々あるかと思いますが、Mapboxについて知っていただこうと、この度Qiitaアカウントを開設しました。SDKのチュートリアルや関連ツールの紹介をはじめ、簡単なアプリ開発事例など、これからアプリ開発に地図機能を搭載したい、既存プロダクトの拡充を考えている皆さんの助けになればと思っています。

Mapboxについて

Mapboxは地図情報サービスの開発プラットフォームを提供しています。2010年にCEOであるEric Gundersenによって米国にて創業されました。Web、iOS、Android、Unityなど多岐に渡るプラットフォームに対応したSDKおよびAPIをオープンソースで開発・提供しています。日本へは2019年11月に参入後、2020年3月にソフトバンク株式会社との合弁会社「マップボックスジャパン合同会社」を設立しローカライズなど各種事業展開を進めています。

MapboxはGoogle MapsやApple Maps(MapKit)に比べ知名度がまだ低いかもしれませんが、ヤフーマップやPayPayなど国内でも採用事例が増えています。

noteに私達について紹介した記事がありますので、ぜひ御覧ください。

Hello World. こんにちは、Mapboxです。

さっそくMapboxを使ってみよう

Mapboxではモバイル向けSDKを利用したサンプルプロジェクトが用意されています。まずはサンプルプロジェクトをビルドしてシミュレーターでMapboxの地図を表示させてみるところから始めてみましょう。

始めるにあたり、SDKを使用してMapboxの地図を表示するには専用のアクセストークンが必要です。Mapboxへアクセスし会員登録をしてください(できれば二段階認証も設定しましょう)。

会員登録やSDKダウンロード、サンプルのビルドと実行は全て無料で実行できます(料金体系については後述)。

会員登録

1)Mapbox へアクセスし、サインアップを押す。Create your Mapbox account の画面が表示されたら、Username、Email、Passwordを入力し、Mapboxの利用規約とプライバシーポリシー(Terms of Service and Privacy Policy)に同意してGet Startedボタンを押します。

2)Check your email 画面が表示されますので、メールボックスに認証用メールが届いているのを確認します。

3)メールボックスに、no-reply@mapbox.com から Verify your Mapbox email address というメールが届いてるのを確認し、本文のリンクをクリックします。

4)設定したパスワードを入力し、ログインします。

5)ログインに成功すると Dashboard ページが表示されます。

アクセストークンを確認する

会員登録を終えてアカウントページにアクセスすると、pk. から始まる文字列が確認できます。これはDefault public token と呼ばれるアクセストークンです。この文字列をメモ帳などに控えておきましょう。後述するサンプルのビルドでこのトークンを利用します。

アクセストークンについて

Mapbpxはアクセストークンごとに各機能の利用(スコープ)を設定し管理することができます。Default public tokenは地図の閲覧やスタイル切り替えなど基本的な閲覧機能を提供するトークンです。各種スコープを変更することはできません。

それに対し、Secret Tokenと呼ばれるアクセストークンでは、Mapboxで提供されている各サービスの利用可否を設定できます。使わない機能を外しておくことで、例えば不用意なアクセスにより意図しない料金が発生するなどの事故を防ぐことができます。

今後の記事でSecret Tokenの作成について説明する予定です。

アカウントの二段階認証の設定をしておきましょう

Mapboxはサイトにログインする時の 二段階認証 が設定できるようになっています。Google AuthenticatorやMicrosoft Authenticatorを使って設定をしておきましょう。

1)ログインして画面右上から Settings を選択します。

2)左メニューから Security を選択し、Two-factor authentication のスイッチをONにします。

3)QRコードが表示されるのでGoogle AuthenticatorやMicrosoft Authenticatorを使って認証コードを用意し、画面に入力してください。

サンプルをビルドしよう(iOS編)

事前準備

本記事執筆時は下記環境で実行しています。
- MacOS X 10.15.7(19H2)
- Xcode12

プロジェクトはCocoaPodsを使います。インストールされてない場合は事前にインストールしてください。

プロジェクトのクローン

会員登録を済ませてアクセストークンを用意しましたらサンプルプロジェクトをGitHubからクローンしましょう。

git clone git@github.com:mapbox/ios-sdk-examples.git

関連ライブラリのインストール

pod コマンドを実行して関連ライブラリをインストールします。
pod install でエラーが出る場合は --repo-update オプションをつけてください。

$ cd ios-sdk-examples

$ pod install --repo-update

Analyzing dependencies
Downloading dependencies
Installing Mapbox-iOS-SDK (6.2.1)
Installing MapboxMobileEvents (0.10.4)
Installing SwiftLint (0.40.3)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `Examples.xcworkspace` for this project from now on.
Pod installation complete! There are 2 dependencies from the Podfile and 3 total pods installed.

プロジェクトのビルド

ios-sdk-examples フォルダにExamples.xcworkspaceが生成されますのでダブルクリックしてXcodeを起動してください。
ファイルを開いたら、Examplesスキームと任意のシミュレーターを選択します。このまま実行ボタンを押してみると、ビルドエラーとなり、下記メッセージがログに表示されます。
009_xcode_build1-2.png

error: Missing Mapbox access token
error: Get an access token from <https://www.mapbox.com/studio/account/tokens/>, then create a new file at {プロジェクトのディレクトリ}/ios-sdk-examples/mapbox_access_token that contains the access token.

アクセストークンが認識できない為ビルドエラーを起こしています。
このエラーはios-sdk-examples/Examples/insert-mapbox-token.sh スクリプトによって出力されています。プロジェクトのBuild PhasesInsert Mapbox Access Token で設定されています。どのようなスクリプトなのか確認してみましょう。

token_file=$SRCROOT/mapbox_access_token

# First check the above path, then the user directory.
# Ignore exit codes from `cat`.
token="$(cat $token_file 2> /dev/null)" || token="$(cat ~/.mapbox 2> /dev/null)"

if [ "$token" ]; then
  plutil -replace MGLMapboxAccessToken -string $token "$TARGET_BUILD_DIR/$INFOPLIST_PATH"
else
  echo 'error: Missing Mapbox access token'
  open 'https://www.mapbox.com/studio/account/tokens/'
  echo "error: Get an access token from <https://www.mapbox.com/studio/account/tokens/>, then create a new file at $token_file that contains the access token."
  exit 1
fi

insert-mapbox-token.shは、プロジェクトディレクトリ($SRCROOT)にmapbox_access_token 、または ~/.mapbox というファイルからアクセストークンを取得し、Info.plistMGLMapboxAccessToken をキーにを書き込んで保存していることがわかります。いずれのファイルも設置されていなかったことからエラーになっていることがわかります。

複数のプロジェクトで個別のアクセストークンを扱う場合を想定し、今回は前者を採用します。
ios-sdk-examples/mapbox_access_token を作成・アクセストークンをペーストして保存し、再ビルドしましょう。

プロジェクトのビルド その2

今度は無事にビルドが成功しシミュレーターが起動・サンプルが表示されたと思います。

Getting started グループにある Add a marker to a map をタップしてみましょう。

無事にマップが表示されたと思います(おめでとうございます!)
マウスでドラッグしてスクロールしたり、オプションキーを押しながらピンチ操作やドラッグして拡大・回転させるなど操作してみましょう。

動作がなんか遅い……

シミュレーターでサンプルを動かしてみると動作がもっさりして心配になるかもしれません(シミュレーターで稼働させる都合上仕方ありません……)。ストレスになる場合は実機にアプリを転送することをおすすめします。

各サンプルの説明サイト

ビルドしたサンプルを実行すれば、どのようなことが実現できるかを知ることができますが、下記サイトでも各サンプルについて解説しています。

https://docs.mapbox.com/ios/maps/examples/

画面キャプチャが掲載されていますので、特に非エンジニアの方へ説明する際に活用できます。

料金体系

Mapboxの料金体系です。APIごとに一定のラインまでは無料となっています。SDKを組み込んでシミュレーターや実機確認程度の利用であれば無料枠内に収まると思います(クレカ登録の必要もありません)。

https://www.mapbox.jp/pricing

各種リンク

Twitter:https://twitter.com/mapbox_jp

Note:https://note.com/mapbox_japan

GitHub:https://github.com/mapbox

SDKドキュメント:https://docs.mapbox.com/ios/maps/overview/

各種お問い合わせ:https://www.mapbox.jp/contact

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【未経験の方必見】アプリエンジニアが狙い目な理由

こんにちは。
この記事では、
「なぜアプリエンジニアが狙い目なのか」
「なぜアプリエンジニアが少ないのか」
にフォーカスを置いて書いていきます。

なぜアプリエンジニアが狙い目なのか

理由は簡単です。
母数が少ないからです。
この理由から、需要に対しての供給が足りていません。
経験者の採用を諦め、未経験者の教育に力を入れている会社もあるようです。

実際、弊社では全ポジションを募集していますが、
アプリエンジニアの面談回数はサーバーサイドやフロントに比べてかなり少ないです。

サーバーサイドやフロントに対してアプリエンジニアは
大体5分の1〜10分の1ほどしか応募がきません。
アプリエンジニアの中でもAndroidエンジニアは特に、iOSエンジニアの10分の1ほどしか
来ないため、Androidエンジニアの希少性はかなり強いと言えます。

なぜアプリエンジニアが少ないのか

Twitterを通していろいろな意見を見たり、実際に勉強会で交流を通して意見を聞いてきた中で、
アプリエンジニアが少ない理由がいくつか見えてきました。

苦手意識

初学者の人はまず、「どの言語をやるか」「どのポジションをやるか」
について選択が迫られます。
この時、「フロントの方がすぐに結果が見えてわかりやすい」
という意見がよく見られました。

また、「プログラミングの勉強を始めるならまずHTML・CSS
という暗黙の何かがあるように思えました。
(「HTML・CSSはプログラミング言語じゃない」という気持ちはわかります)
そこから派生してJavascript・jQuery・Vue.jsなどフロントエンジニアの道を歩む人が
多いのではないでしょうか。

アプリエンジニアを勧める環境が少ない

初学者の言語選択に対して、「PHP×Javascript」「Rails」
を勧める環境が多いように感じられました。
特に「PHP×Javascript」は、「初学者が最短で仕事につくのに最適だ」「一番簡単だ」
と勧める人をよく見かけました。

Railsが多い理由はスクールの影響が大きいかと思います。
確かにわかりやすいですし、レール(rail)にのっとった書き方ができるため初学者に優しいかもしれませんが、
逆に言えば経験者が爆速開発するためのフレームワークで少数精鋭でやるものだと言う声もあり、
初学者に本当に優しいのかと聞かれると
はっきりとYesと言えない言語です。

これらから分かるように、初学者にとってウケがいい言語は「手頃(そう)な言語」で、
またウケを狙った(?)スクールによってRails学習者が増え、
バランスが偏ってしまっているのではないかと推測しました。

iOSエンジニアはMacBookが必要

正確に言えば、MacBookがなくてもOSをぶち込めれば良いのですが、
いきなりの初学者がそんなことを思いつきませんし、できません。
勉強を始めるためにMacBookを買うと言うフィルターがあるために、
アプリエンジニアの頭数が少ないのだと推測しました。

Androidユーザーが少ない

アプリエンジニアの道に進んだとしても、iOSかAndroidかの分岐があります。
ここでは、多くの人がiOSエンジニアを選択することでしょう。
なぜなら日本では、世界でも稀にiPhoneユーザーが多いためです。
Android端末を使ったことがないのにAndroidの勉強をする人はかなり稀です。

結局のところアプリエンジニアは少ない

これらの理由から、エンジニア市場には偏りが見られます。
iOSエンジニアをやるにしても高価なMacbookが必要で、
Androidエンジニアへの道も国内OSシェアからその道に進む人があまりいません。

サーバー・インフラ>>アプリエンジニア(iOS >> Android)

のような構図が見られるため、ただでさえアプリエンジニアは狙い目であるのに
Androidエンジニアとなれば更に母数が減るのです。

これからプログラミングの勉強を始めようと考えている方がいましたら、
ぜひアプリエンジニアを検討してみてはいかがでしょうか。

誤解してはいけない

誤解してはいけないのが、「供給が足りないのだから簡単なんだ!」と言うことです。
あくまでも一定レベルのスキルは必要であり、それなりの勉強時間が必要になります。
はっきり言ってエンジニアになるには大変です。
数ヶ月でなれる人は天才だと思います。

最後まで記事を読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5]少数点第二位や第三位の四捨五入・切り捨て・切り上げを行う方法

必要とする関数

四捨五入・切り捨て・切り上げを行う場合は以下の関数を使います。

round() → 四捨五入
fool() → 切り捨て
ceil() → 切り上げ

使用例

ViewController.swift
let num = 7.5

let numRound = round(num)   
// 8(四捨五入)

let numFloor = floor(num)   
// 7切り捨て)

let numCeil  = ceil(num)    
// 8(切り上げ)

少数点第二位や第三位の四捨五入・切り捨て・切り上げ

ViewController.swift
let num = 3.1415

let numRound = round(num*10)/10   
// 3.1(小数第2位で四捨五入)

let numFloor = floor(num*100)/100   
// 3.14(小数第3位で切り捨て)

let numCeil =ceil(num*1000)/1000   
// 3.142(小数第4位で切り上げ)

以上です。参考にしてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

App Distributionで招待された時にアプリをインストールする方法

AppDistributionをで招待された側の操作方法をまとめる。

手順

1.招待メールについているダウンロードリンクをタップする

2.Sign in with Googleから、Googleにサインインする

3.UDIDの提供に同意し、「Start testing on this device」を押下

4.「Register Device」を押下

5.「Download profile」から配布アプリインストール用プロファイルを端末にインストールする

6.「許可」を押下

7.「設定アプリ」を開き、「一般」を押下

8.「プロファイル」を押下

9.「Firebase App Distribution」を押下

10.「インストール」を押下

11.「インストール」を押下

ここからは、アプリ管理者がUDIDをプロビジョニングに追加後操作可能

12.ホーム画面上にこのようなAppDistributionのショートカットが追加されているので押下する

13.配布アプリが表示されているので、押下

14.「Download」を押下

15.「インストール」を押下すると、アプリのインストールが開始されます

備考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebase Crashlytics SDKのアップデートでテストクラッシュが上手くいかなかった時の解決方法

クラッシュレポートのためのFirebase Crashlytics SDK を アップグレードしないと2020/11/15から使えなくなるのでFirebaseのドキュメントを参考にアップグレードの作業を進めました。
Firebaseのドキュメント通りに作業を進めればすんなりと終わるはずだが自分はそうもいかなかったので記事にさせていただきます。
後ほど説明しますが、自分はコードの修正を行いテスト用で強制クラッシュさせる際にかなりてこずりました。
ios環境をアップデートしたのでXcodeを使っていきます。

そもそもFirebase Crashlyticsとは

アプリのクラッシュ時にレポートを管理してくれるFirebaseの機能

アップデートの手順

  • podfile追加&pod install
  • コード修正
  • 実機でテスト用強制クラッシュ

テスト用強制クラッシュの手順(参考サイト)

  1. Xcode にて Build and then run
  2. Xcode にて Stop running
  3. 実機でもう一回アプリの立ち上げ
  4. 自分が前もって設定してたfatalErrorを仕込んである画面に遷移
  5. fatalErrorでクラッシュレポート発信
  6. Firebaseのコンソールで確認
  7. 実装されたfatalErrorを削除する

これをやれば大丈夫!強制クラッシュの実装方法

アプリを立ち上げた時には呼び出すことができないようなViewController内のviewload関数内にfatalErrorを入れる

  • つまり実機でアプリを立ち上げて自分の手でスマホを操作してのち、fatalErrorを起こせればOK
  • アプリを立ち上げて即fatalErrorになるとレポートが送られない
  • 参考サイトでは、プロジェクト内でタップするとfatalErrorが起きるボタンを実装している

なぜ僕がテスト用強制クラッシュで手こずったのか

ずばりXcode にて Stop runningののち、実機でもう一回アプリの立ち上げをしないとクラッシュレポートが送れないということを知らなかったから
何回も修正したコードを見直しました笑

一連の作業が終わって大変だなって思ったこと

Pods関連はcocoapodsを使ったことがある方は一瞬で終わるでしょう。
自分が大変だなって思ったのはコード修正です。
Firebaseのドキュメントをみてもらえればわかりますが、修正項目が14箇所あり間違えればテスト用の強制クラッシュさせる時にエラーが出るため抜け漏れなく修正する必要があります。
僕は各項目それぞれ修正点と懸念点を細かくメモしながら修正しました。修正点を見つけるときはXcodeのプロジェクト内検索機能を使うと楽です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発:タイマーアプリ(7.アラーム音選択の実装)

記事内容

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、アラーム音の選択画面の実装について掲載します。

開発環境

  • OS: macOS 10.15.7 (Catalina)
  • エディタ: Xcode 12.1
  • 言語: Swift
  • 主な使用ライブラリ: SwiftUI

手順概要

  1. アラーム音の構造体に必要な要素(プロパティ)を検討する
  2. Data にアラーム音の Struct を作成する
  3. アラーム音のリストを作成する
  4. アラーム音を試聴できるようにする
  5. 選択したアラーム音をTimeManagerクラスのプロパティに反映させる
  6. 設定画面、アラーム音リスト間の画面遷移を実装する

1. アラーム音の構造体に必要な要素(プロパティ)を検討する

現時点では、アラーム音がデフォルトで指定されたものになっており、変更できません。これをいくつかのアラーム音を用意し、設定画面から変更可能にしていきます。

アラーム音のオブジェクトとして必要な要素は何か考えると、以下の2つがあれば成立します。

  • サウンドID:AudioToolbox ライブラリのメソッドで音を鳴らすために必要
  • サウンド名:設定画面やリスト画面に表示するために必要

2. Data にアラーム音の Struct を作成する

Data.swift ファイルに Sound という名前の構造体を作成し、そのプロパティとして、手順1で考えた2つの要素を含めます。

このとき、サウンドIDプロパティのデータ型は SystemSoundID でなければなりません。このデータ型は AudioToolbox ライブラリに含まれますので、必ずインポートします。

サウンドIDによって、この構造体から作成されたインスタンスが一意である必要があるので、 Identifiable プロトコル を継承します。

Data.swift
import SwiftUI
import AudioToolbox //追加import

//(他のenum省略)

struct Sound: Identifiable {
    let id: SystemSoundID
    let soundName: String
}

3. アラーム音のリストを作成する

SoundListView という名前の新しい Swift ファイルを SwiftUI テンプレートから作成します。システムサウンドを利用するため、AudioToolbox ライブラリをインポートしておきます。

ほかの View と同様に、TimeManager クラスのプロパティを常に参照したいので、そのインスタンスを作成します。@EnvironmentObject プロパティラッパーを var の前につけます。

SoundListView
import SwiftUI
import AudioToolbox

struct SoundListView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Text("Hello, World!")
    }
}

次に、アラーム音のデータを用意します。

手順1で作成した Sound 構造体から id プロパティと soundName プロパティを指定しながら、インスタンスを作成し、それを Sounds と名付けた Array に格納します。具体的なサウンド ID は以下のリソースを参考にします。soundName もリソースにあるファイル名を参考に、わかりやすいものにします。
https://github.com/TUNER88/iOSSystemSoundsLibrary

SoundListView
struct SoundListView: View {
    @EnvironmentObject var timeManager: TimeManager

    let sounds: [Sound] = [
        Sound(id: 1151, soundName: "Beat"),
        Sound(id: 1304, soundName: "Alert"),
        Sound(id: 1309, soundName: "Glass"),
        Sound(id: 1310, soundName: "Horn"),
        Sound(id: 1313, soundName: "Bell"),
        Sound(id: 1314, soundName: "Electronic"),
        Sound(id: 1320, soundName: "Anticipate"),
        Sound(id: 1327, soundName: "Minuet"),
        Sound(id: 1328, soundName: "News Flash"),
        Sound(id: 1330, soundName: "Sherwood Forest"),
        Sound(id: 1333, soundName: "Telegraph"),
        Sound(id: 1334, soundName: "Tiptoes"),
        Sound(id: 1335, soundName: "Typewriterst"),
        Sound(id: 1336, soundName: "Update")
    ]

    var body: some View {
        Text("Hello, World!")
    }
}

body{} 内の一番外枠には List{} を用意します。この List は SettingView で利用した Form に似ていて、表形式でたくさんのアイテムを罫線で区切って表示するときに利用します。

List{} 内に、Array 内のアイテムを順番に並べるときは、ForEach が便利です。ForEach(sounds) とすることで、sounds 内のアイテムに対して、順番にループ処理してくれます。処理内容は{}内に記述します。

まずは {} 内にテキストでサウンド名を表示するように記述します。

SoundListView
struct SoundListView: View {
    @EnvironmentObject var timeManager: TimeManager

    let sounds: [Sound] = [
        //(各サウンドオブジェクト省略)
    ]

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //soundNameの値でリストに表示する
                Text(("\(sound.soundName)"))
            }
        }
    }
}

4. アラーム音を試聴できるようにする

実際のアプリの操作として、アラームを選択するときにどんな音なのかを確認したいものです。

試聴用のアイコンをリストの各行の左端に表示させて、それをタップするとサウンドが再生される、という仕様にしていきます。

ForEach{} 内に HStack{} を追加して、各行ごとにコンポーネントが横並びになるようにします。

HStack{} の一番最初に、アイコンとして Image コンポーネントを追加します。SF Symbols から "speaker.2.fill" を選びました。そして、.onTapGesture {} モディファイアを追加し、クロージャ {} 内に AudioToolbox ライブラリのメソッド AudioServicesPlayAlertSoundWithCompletion() を記述します。引数を sound.id とすることで、タップした行の id を soundID として拾って再生されます。第二引数は nil で構いません。

SoundListView
struct SoundListView: View {
    //(プロパティ省略)

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //リストの行に関する記述
                HStack {
                    //試聴用のアイコン
                    Image(systemName: "speaker.2.fill")
                        .onTapGesture {
                            AudioServicesPlayAlertSoundWithCompletion(sound.id, nil)
                    }           
                    //soundNameの値でリストに表示する
                    Text(("\(sound.soundName)"))
                }
            }
        }
        .navigationBarTitle("Alarm Sound Setting", displayMode: .automatic)
    }
}

5. 選択したアラーム音をTimeManagerクラスのプロパティに反映させる

行をタップすると、そのサウンドが選択されるようにします。

具体的には、HStack{} がリスト内の行に相当するので、そのモディファイアとして .onTapGesture{} を追加します。.onTapGesture{} のクロージャ {} 内には、TimeManagerクラスのすでに用意されている soundID プロパティと soundName プロパティに選択した行のサウンドIDとサウンド名がそれぞれ代入されるようにします。

ただし、これだけだと、サウンドリスト上のサウンド名の表示をタップしないと選択できません。行の空白部分をタップしても無反応です。細かいですが、ここも修正していきます。

HStackのモディファイアとして、.onTapGesture{} より前に、.contentShape(Rectangle()) を追加します。これにより、HStackが四角いオブジェクトとみなされる、つまり行の全体がひとつのオブジェクトになり、空白箇所をタップしても、アラーム音を選択できるようになります。

SoundListView
struct SoundListView: View {
    //(プロパティ省略)

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //リストの行に関する記述
                HStack {
                    //試聴用のアイコン
                    Image(systemName: "speaker.2.fill")
                        .onTapGesture {
                            AudioServicesPlayAlertSoundWithCompletion(sound.id, nil)
                    }           
                    //soundNameの値でリストに表示する
                    Text(("\(sound.soundName)"))
                }
                //HStackを四角いオブジェクトとみなす
                .contentShape(Rectangle())
                //行をタップでサウンド選択(IDと名前をTimeManagerへ反映)
                .onTapGesture {
                    self.timeManager.soundID = sound.id
                    self.timeManager.soundName = sound.soundName
                }
            }
        }
    }
}

さらに、現在選択されているサウンド名の右側にはチェックマークが表示されるように、 if 構文で記載します。チェックマークは行の右端に寄せたいので、サウンド名とチェックマークの間に Spacer() を入れます。

SoundListView
struct SoundListView: View {
    //(プロパティ省略)

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //リストの行に関する記述
                HStack {
                    //試聴用のアイコン
                    Image(systemName: "speaker.2.fill")
                        .onTapGesture {
                            AudioServicesPlayAlertSoundWithCompletion(sound.id, nil)
                    }           
                    //soundNameの値でリストに表示する
                    Text(("\(sound.soundName)"))

                    Spacer()

                    //現在選択されているサウンドの場合はチェックマークを表示
                    if self.timeManager.soundID == sound.id {
                        Image(systemName: "checkmark")
                    }
                }
                //HStackを四角いオブジェクトとみなす
                .contentShape(Rectangle())
                //行をタップでサウンド選択(IDと名前をTimeManagerへ反映)
                .onTapGesture {
                    self.timeManager.soundID = sound.id
                    self.timeManager.soundName = sound.soundName
                }
            }
        }
    }
}

6. 設定画面、アラーム音リスト間の画面遷移を実装する

メインの設定画面 SettingView にアラーム音選択の項目を追加し、サウンドリスト画面 SoundListView へ遷移するようにします。

SettingView の body{} 内の一番外側は NavigationView{} で囲った状態になっています。これにより、その中の Form{} 内に NavigationLink{} を追加することで画面をさらに次の階層に遷移させることができます。

設定項目名を Text() にて "Sound Selection" とし、その行の右端に、現在選択中のアラーム音の名前が表示されるように Spacer() を間に挟んで、 Text() にて TimeManagerクラスのプロパティ soundName を指定します。

Form のモディファイアとして、以下の2つを追加し、NavigationBar(画面上部)の表示と、NavigationView全体の表示スタイルを指定します。

.navigationBarTitle()
.navigationViewStyle()

SettingView.swift
struct SettingView: View {
    //(プロパティ省略)

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Alarm:")) {
                    Toggle(isOn: $timeManager.isAlarmOn) {
                        Text("Alarm Sound")
                    }
                    Toggle(isOn: $timeManager.isVibrationOn) {
                        Text("Vibration")
                    }
                    //サウンド選択画面へ画面遷移
                    NavigationLink(destination: SoundListView()) {
                        HStack {
                            //設定項目名
                            Text("Sound Selection")
                            Spacer()
                            //現在選択中のアラーム音
                            Text("\(timeManager.soundName)")
                        }
                    }
                }
                Section(header: Text("Animation:")) {
                    //(Animationセクションの内容省略)
                }
                Section(header: Text("Save:")) {
                    //(Saveセクションの内容省略)
                }
            }
            .navigationBarTitle("Setting", displayMode: .automatic)
            .navigationViewStyle(DefaultNavigationViewStyle())
        }
    }
}

そして、SoundListView の List{} に .navigationBarTitle モディファイアを追加し、画面のタイトル "Alarm Sound Setting" をつけます。

SoundListView
struct SoundListView: View {
    //(プロパティ省略)

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //リストの行に関する記述
                HStack {
                    //試聴用のアイコン
                    Image(systemName: "speaker.2.fill")
                        .onTapGesture {
                            AudioServicesPlayAlertSoundWithCompletion(sound.id, nil)
                    }           
                    //soundNameの値でリストに表示する
                    Text(("\(sound.soundName)"))

                    Spacer()

                    //現在選択されているサウンドの場合はチェックマークを表示
                    if self.timeManager.soundID == sound.id {
                        Image(systemName: "checkmark")
                    }
                }
                //行をタップでサウンド選択(IDと名前をTimeManagerへ反映)
                .onTapGesture {
                    self.timeManager.soundID = sound.id
                    self.timeManager.soundName = sound.soundName
                }
            }
        }
        .navigationBarTitle("Alarm Sound Setting", displayMode: .automatic)
    }
}

これで、アラーム音選択を含めた設定画面の実装ができあがりました。次回は視覚的に楽しいプログレスバーの実装について掲載します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む