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

iPhoneアプリ学習:ストップウォッチ

はじめに

某プログラミング学習サイトでの学習記録を記します。

No1:割愛

No2:部品配置

・複数の部品にまとめて制約を付ける場合は、
 ・対象となる部品を全て選ぶ
 ・Embed InからStack Viewに登録する
 ・View Controller Scene > View Controller > View > Stack Viewを選択する
 ・位置の制約を付ける

No3:部品をコードに接続する

・時間表示用のラベル
・ボタン3つ(Start,Stop, Reset)

No4:ボタン押下でタイマーを開始する

@IBAction func (ボタン名) (_ sendor:Any) {
 Timer.scheduledTimer(
  timeInterval: 0.01, // 実行間隔[秒]
  target: self, // タイマーで実行するメソッドのある場所
  selector: #selector(self.update), // 実行するメソッド名
  userinfo:nil, // selectorに渡す情報。なければnil
  repeats:true) // 繰り返し実行するかどうか

 // selectorはobjective-cの仕様のため先頭に@objcが必要らしい
@objc func update() {
  print(Date.timeIntervalSinceReferenceDate);
  // 2001年からの経過秒数を取得
 }

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

[iOS]project.pbxprojなどのマージコンフリクト解消を最速でやるには

おすすめツール

  • Visual Studio Code

project.pbxprojファイルに限らないが、ファイル内にコンフリクトマーカーがある場合、そこを色付けで表示し、一発で飛べる。その上、「Accept Incoming Change」「Accept Current Change」「Accept Both Changes」の各ボタンが用意されており、ワンタッチで「マージされてきたブランチ」「現在のブランチ」「両方のブランチ」の変更を適用する事ができる。その他、軽く使いやすいためおすすめ。

解消の考え方

project.pbxprojファイルは、プロジェクト内にあるファイルの定義およびそれらのフォルダ階層構造などを定義している。そのため、マージ(またはチェリーピック)するブランチ同士でファイルの位置が違ったり、新規ファイルを追加したりしていると必ずマージコンフリクトを起こしてしまう。

まず、現在のブランチと、マージによって持ってきたブランチで、ファイル・フォルダの階層構造がどう違っているかを確認する。

その上で、マージ後はどういった階層構造にしたいのかを決める。

その後は、その通りになるようにpbxprojの記述を編集していけば良い。

どういった状態にしたいかによって、
「現在のブランチの変更を受け入れる」
「持ってきたブランチの変更を受け入れる」
「両方の変更を受け入れる」
で済むこともあるし、変更を手動で調整する必要がある場合もある。

トラブルシューティング

コンフリクトを全て解消したはずだが、エラーが出る

エラーメッセージを読むと、pbxprojファイルの何に問題があるのか基本的には書いてある。

よくあるのは、同じ名前のファイルが二箇所で重複定義されている、逆に消してしまったはずの存在していないファイルを参照しているなど。マージコンフリクトがあった箇所とは違う箇所にこうしたエラーが生ずるのもよくある。

また、文法が間違っていると言われることがあるが、これはpbxprojファイルを編集中に誤って不要な文字を入れてしまったり、必要な文字まで消してしまったりして、記述の形式がおかしくなっていることが多い。問題のある箇所が何行目かもエラー文で教えてもらえるはずなので、そこを直す。

.xcworkspaceファイルが開けなくなる

  • Xcodeのクリーン、Derived Dataの削除、再起動などを試す。
  • .xcworkspaceを一回消してから作り直す(pod installをもう一回叩くなど)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITabBarItemにシステムアイコンをセットする

はじめに

掲題の通り、UITabBarItemにシステムアイコンをセットする方法がなかなか見つからなかったため、備忘録として残す

設定方法

以下のように引数Imageに UIImage(systemName: "アイコン名")を設定するだけ
アイコン名は以下から取得可能
https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/system-icons/

v.tabBarItem = UITabBarItem(title: R.string.localizable.bookList(), image: UIImage(systemName: "book"), tag: 0)

以上!!!

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

【Flutter】GoogleDriveへのバックアップ・リストア機能を実装するまでの道のり

トップ2.jpg

7月にFlutter開発を始めてから2作目のメモアプリ「IdeaShuffleMemo」をリリースしました。
■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

■アプリの詳細記事
https://yukio.site/idea_shuffle_memo/

3つの機能を実装しようと取り組みました。この記事では、その中のバックアップ機能を実装する方法を書いていきたいと思います。

■パスワードロック機能実装ついてはこちら
https://qiita.com/YuKiO-OO/items/bf2d1d107d1a66211619

■アプリ内課金についてはこちら
https://qiita.com/YuKiO-OO/items/a0fe8e0a256afbb69fc7

実装する機能

ユーザーがGoogleアカウントでログイン。
バックアップボタンを押すと、ユーザーのGoogleドライブにDBの情報を保存。
リストアボタンを押すと、バックアップしたデータが復元される。

Googleドライブ採用の理由

とりあえずでもGoogleアカウントを持っている人が多いと考えたからです。特に全世界のスマホシェアではAndroidが多く、AndroidユーザーはすべてGoogleアカウント持っていますしね。
その他ストレージはユーザーが限定されるので、選択肢に入れませんでした。
Firebaseを使って、こちら側でストレージを用意してあげる方法もありますが、サービスの維持、仮にサービスを止めることになった場合に、データの扱いが難しいことや、ユーザーデータの管理の観点でもリスクがあるので、今回は採用しませんでした。

参考記事

この記事を参考に構築しました。

GoogleAPI利用の設定:参考
Google Firebase Email/Password And Google Login In Flutter
Googleドライブの実装:参考
https://www.c-sharpcorner.com/article/google-drive-integration-in-flutter-upload-download-list-files/

特に2番目の記事は、部分部分を抜き出しているだけなので、分かりづらいと思います。
こちらの構成をみて、全体像を把握しておくことをオススメします。
Github
https://github.com/myvsparth/flutter_gdrive/blob/master/lib/main.dart

今回はこの記事を元にして書いていきますので、上記、記事を教科書だと思ってください。

この記事と異なる点は、データの取り扱い方です。

教科書では画像ファイルの管理が目的なので、ユーザーが保存データのリストからデータを選んでダウンロードする機能が実装されています。

ただバックアップ機能では、ユーザーにファイルを触らせないので、今回の実装ではバックグランドでファイルの選択をしていきます。

ユーザーは指定された操作のボタンを押すだけになります。

全体の流れ

今回は、参考記事を中心に、実装について詰まったところにフォーカスして書いていきます。

-FirebaseとGoogleドライブのAPIの設定
-Googleアカウントでログイン
-DBのデータを抜き出して、Googleドライブに保存
-Googleドライブに保存されているリストを取得
-保存されているデータをダウンロード、DBを上書き

知っておいたほうがいい知識

Googleドライブの仕様について

今回、Googleドライブを使用しますが、今回の方法は普通にGoogleドライブに保存するわけではありません。

アプリ専用の隠しフォルダが作られます。

その隠しフォルダにデータ保存するのですが、保存したデータのリストも同時に生成されます。
リストにはファイル名は、ファイル固有のIDなどが記載されています。
そのリストのIDからデータをダウンロードしたりします。
ファイル名を指定して、ダウンロードするわけでないので、ご注意ください。

またユーザーはファイルを見る事も、触ることもできません。

DBについて

今回は、DBの操作についてパッケージのMoorを使用しています。
バックアップ実装では、DBの設定が完了しているものします。。。。と初心者の方は、言われても困りますよね。

時間はかかりますが書こうと思うので、取り急ぎ下記の公式ドキュメントに挑戦してみてください。
公式ドキュメント通りにやれば、できるはず。
あなたならできる!
何やっているか、分からないところもあるかもしれませんが、それも学びだったりしますし 汗

https://moor.simonbinder.eu/docs/getting-started/
この方が書いている記事を参考にすれば行けるかなと思います。
https://qiita.com/niusounds/items/e4d731af58201ad5fe6f
Youtubeにもやり方がありました。
https://www.youtube.com/watch?v=zpWsedYMczM&t=387s

他のパッケージでDBを構築されている方は、DBのデータが保存されているディレクトリのパスとファイル名がわかれば、大丈夫だと思います。(だいたい同じところかな?)

注意事項

※20年6月時点での情報をもとに作成しています。
※試行錯誤の結果、まだリファクタリング等できていません。処理が冗長的なところや一部無駄な処理もありますので、ご了承ください。

※iOS側の設定の記述が少ない理由ですが、資料があまり残ってませんでした。おそらくiOSでは、そこまでハマりポイントがなかったと記憶しています。エラー表示を解決するだけで、問題なく進めただけかもしれないですが・・・。記述少なめですが、ご了承ください。

もし、分かりづらいところがあれば遠慮なくコメントなどに残して頂けると助かります。
みなさんと一緒に、Flutterを学習している人のためになる記事を作っていければと思います。

バックアップの実装

firebaseの設定

まず、教科書はこれを使います。
Google Firebase Email/Password And Google Login In Flutter

Firebaseとは、難しいことをしなくても、オンラインでサーバーと連携する必要がある機能を簡単に構築できるサービスです。

それで、Firebaseを使用する理由は、Googleアカウントのログイン状態を管理するためです。

Firebase側が、端末を識別してログインしている状態を判断してくれるので、楽なんです。

今回使うFirebaseのAuthenticationという機能は無料で使えるのですが、月1万(たぶん回数、単位がない)を超えると従量課金にする必要があるようです。
Firebase Pricing

ちなみにFirebase以外に、GoogleDriveのAPIを使うために「google cloud platform」、通称GCPも使っていきます。GoogleのAPIの中には有料なものがありますが、GoogleDriveは無料のようです。

教科書ではEmailの登録をしていますが、Emailの登録はしないので今回はGoogleアカウントだけをオンにすれば大丈夫です。

Emailの実装に関しては飛ばしてください。

教科書ではAndroidしか触れてませんよね。

ただ私の場合、他の機能でもFirebaseと連携するので、iOS側でもFirebaseに登録しています。(必須かは不明)

実装はこちらを参考にしてください。
https://firebase.google.com/docs/flutter/setup?hl=ja

Android側の設定で、追加する場所がいくつか分かりづらかったところを補足します。

Firebaseの設定途中でAndroidは「google-services.json」をダウンロードしますが、
android/app/src/のところに保存してください。

android/build.gradle
    dependencies {
        classpath com.android.tools.build:gradle:3.5.0
        classpath org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version
        classpath com.google.gms:google-services:4.3.3
   }
android/app/src/build.gradle
  dependencies {
     implementation org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version
     implementation com.google.firebase:firebase-analytics:17.2.2
  }
//下記の位置に追加することになると思います。
  apply plugin: com.google.gms.google-services

にそれぞれ記載します。

教科書では、ログインの処理まで書かれていますが、これは次のGoogleDriveの実装記事にもしっかりと書かれているので、とりあえずFirebaseの設定までで大丈夫です。

Google Drive APIの設定

ここからは、下記の教科書を使います。

Googleドライブの実装:参考
https://www.c-sharpcorner.com/article/google-drive-integration-in-flutter-upload-download-list-files/

補足でこのブログの作者のGithubを見ておくと、全体像が掴みやすいと思います。
オリジナルで画面を作りたい場合、とりあえずGithub通りに作ってから、部分部分のデザインを変更したほうが楽かもしれません。
https://github.com/myvsparth/flutter_gdrive/blob/master/lib/main.dart

実装方法は、STEPで順に説明されていますが、補足していきたいと思います。

STEP3の補足事項

さらっとAPIの設定しておいてね!と言われますが、補足だけしておきます。
firebaseを登録して、同じアカウントでGCP(Google Cloud Platfom APIのサービス)に登録すると、Firebaseのプロジェクトが紐づいています。
そのプロジェクトを選択して、下記の記事の通りすれば問題なくできると思います。

https://www.virment.com/google-drive-api-activate/

OAuth 同意画面などは設定しました。しかし、Outhクライアントをどうしても作ることができませんでした。ただ問題なく動作できているので、この操作は不要なのかもしれません。(おそらくテスト用するためのアカウントなのだと思いますが、自分のGoogleアカウントを利用して問題ありませんでした)

STEP4の補足事項

一応念のため、パッケージは最新のものをインストール。6月時点ではパッケージのバージョンによる不具合はありませんでした。

STEP5の補足

アンドロイド側で、STEP5を追加する場合、SdkVersionが対応していないというエラーが出るかもしれません。この場合minSdkVersionのバージョンをあげることで、エラーが解消されました。

android/app/src/build.gradle
    defaultConfig {
        applicationId "アプリのIDが入ります"
        minSdkVersion 21 //ここが21以上でエラーが消えました。
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

STEP6の補足

これはStatefulウィジェットの外側に記載するので、くれぐれもClass内に書かないようにご注意ください。

STEP7 ログインの補足

基本この通りに書いて行けば問題ありません。この処理ではfirebaseを使ってユーザーのログイン状態を確認しています。
通常Webであればセッション等でログイン中を確認していますが、firebaseでは端末を識別して、ログイン状態を把握しているそうです。
https://firebase.google.com/docs/auth?hl=ja

なので、一度ログインしてアプリを閉じても、ログイン状態を維持します。

STEP7 ログアウトの補足

ログアウト処理についても、そのまま使用させてもられば大丈夫です。

STEP7のその他について

この記事に書いてあるその他実装については、バックアップとは関係ないためそのまま流用できません。下記に修正した処理を記載していきます。

バックアップ機能の実装(SETP7を改良)

バックアップ機能は、全体の流れで説明した通りです。保存されたファイルにはそれぞれ固有のIDがふられていて、リストで管理されています。ここでは直接ファイルを触るというよりは、保存されているファイルのリストを元に、ファイルを管理する形になります。
まず、リストを取得するメソッドを作成します。

Googleドライブ内のアプリ専用領域から保存されているファイル情報をリストで取得

  Future<void> _listGoogleDriveFiles() async {
    var client = GoogleHttpClient(
        await googleSignInAccount.authHeaders); //承認情報を取得
    var drive = ga.DriveApi(client); //GoogleDrive APIにアクセス
//このアプリ専用のフォルダスペースから保存しているファイル情報(データじゃない)を取得。
    drive.files.list(spaces: 'appDataFolder', $fields: 'files(id, name, modifiedTime)').then((value) {
      setState(()  {
        list =   value; //ファイルのリスト情報を取得
        lastUpdateTime = list.files[0].modifiedTime.toLocal();//最新更新日付をセットします。
      });
    }); 
  }

教科書ではログイン後にボタンを押したらこの処理を呼ぶようになっていますが、このリストはバックグランドで取得できればいいので、ログインが確認できた時点で呼ぶようにしています。

つぎはアップロードですが、DB情報を保存してるディレクトリを取得してファイルをアップロードしていきます。教科書を元に変更しています。

Googleドライブへファイルをアップロード

//これは関数でclass外に書いてます。
//DBが保存されているディレクトリのパスを取得する処理
Future<String> get getDbPath async {
  final dbDir = await getApplicationDocumentsDirectory();//ファイルが保存されている領域のパスを取得
  final dbPath = join(dbDir.path, 'defult.db');//保存されているDBのファイル名を調べて、取得したパスと合体させて絶対パスにする。
  return dbPath;
}
//class外処理終了

//ここらからstatefulウィジェット内の処理
  _uploadFileToGoogleDrive() async {
//ローディング画面を表示。別の操作をされたくないので。
    showGeneralDialog(
        context: context,
        barrierDismissible: false,
        transitionDuration: Duration(seconds: 2),
        barrierColor: Colors.black.withOpacity(0.5),
        pageBuilder: (BuildContext context, Animation animation,
            Animation secondaryAnimation) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
    ); 
   //ファイルのアップロードを始めます。
    var client = GoogleHttpClient(await googleSignInAccount.authHeaders);//承認情報を取得
    var drive = ga.DriveApi(client);//APIヘアクセス
    drive.files.list(spaces: 'appDataFolder');
    ga.File fileToUpload = ga.File();//ドライブ用のファイルのインスタンスを作成
    var filePath = await getDbPath;//DBファイルのパスを関数で取得
    await _listGoogleDriveFiles();//Googleドライブのリストを取得
    //保存するファイルの加工
    var file = await File(filePath);//ファイルをセット
    fileToUpload.name = path.basename(filePath);//ファイルの名前をセット
    fileToUpload.modifiedTime = DateTime.now().toUtc();//アップロードの日付
    fileToUpload.parents = ["appDataFolder"];//アプリ専用フォルダを指定

//常に保存されるバックアップファイルは一つにしたいので、以下の処理を入れています。
//すでにファイルがある場合
//すべてのファイルを事前に削除。エラーで複数ある場合もあるので。
    if (list.files.length > 0) {
      for (var i = 0; i < list.files.length; i++) {
        ga.Media file = await drive.files //削除用
            .delete(list.files[i].id);
      }
      print('ファイルを削除しました。');
    }
    //ファイルのアップロード処理
    var response;
    response = await drive.files.create(
      fileToUpload,
      uploadMedia: ga.Media(file.openRead(), file.lengthSync()),
    );

    await _listGoogleDriveFiles();//リストの再取得
    await Future.delayed(Duration(seconds: 1));
    Navigator.pop(context);//ローディング画面を消す
    //デバック用
    print("ファイル保存が完了しました。レンスポンスは$response");
  }

ここではDBのファイルがあるディレクトリのパスを指定してから、ファイルをアップロードするために情報を加工してアップロードしています。
教科書では日付を取得していませんが、今回バックアップした日付を表示したかったので、日付も保存しています。
どのような項目があるかはこちらでチェックできます。
https://developers.google.com/drive/api/v3/reference/files
「toUtc()」で世界協定時にしているのは、googleのapiが受け付ける形式が世界協定時だからです。

Googleドライブからダウンロード

Future<void> _downloadGoogleDriveFile() async {
//処理中は触ってほしくないのでローディング画面を表示
    showGeneralDialog(
        context: context,
        barrierDismissible: false,
        transitionDuration: Duration(seconds: 1),
        barrierColor: Colors.black.withOpacity(0.5),
        pageBuilder: (BuildContext context, Animation animation,
            Animation secondaryAnimation) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
    );
//ダウンロードを開始します。
//いつもの処理がスタートします。

    var client = GoogleHttpClient(await googleSignInAccount.authHeaders);

//常にファイルは1つのなので、リストの先頭のIDで取得。
//IDはファイルそれぞれ固有の番号がふられています。
    var drive = ga.DriveApi(client);
    ga.Media file = await drive.files
        .get(list.files[0].id,
        downloadOptions: ga.DownloadOptions.FullMedia);


    final filePath = await getDbPath; //ディレクトり取得
    final saveFile = File(filePath); //ファイルの保存場所
    //ここは教科書通りに。
    List<int> dataStore = [];
    file.stream.listen((data) {
      dataStore.insertAll(dataStore.length, data);
    }, onDone: () async {
      await saveFile.writeAsBytes(dataStore);
      _listGoogleDriveFiles();//リストを再取得
      await Future.delayed(Duration(seconds: 1));
      Navigator.pop(context); //ローディングを閉じる
//バックアップをダウンロードして、保存が成功した場合の処理
    }, onError: (error) {
      //エラー処理
    });
  }

バックアップファイルは常に1つしかないので、リストの先頭のみを引っ張ってきます。
処理はアップロードと共通でDBのファイルパスをとってきて、そこに保存します。

GoogleDriveのデータを削除

ユーザー自らデータを削除したい場合もあると思うので、一応削除ボタンも作っておきます。

  Future<void> _deleteFileToGoogleDrive() async{
//実行中は触らせたくないので、ローディング画面
    showGeneralDialog(
        context: context,
        barrierDismissible: false,
        transitionDuration: Duration(seconds: 1),
        barrierColor: Colors.black.withOpacity(0.5),
        pageBuilder: (BuildContext context, Animation animation,
            Animation secondaryAnimation) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
    );
    _listGoogleDriveFiles(); //保存リストを取得。
    var client = GoogleHttpClient(await googleSignInAccount.authHeaders);
    var drive = ga.DriveApi(client); //GoogleDriveのAPIに接続
//繰り返しでリストの全ファイルを削除
    for (var i = 0; i < list.files.length; i++) {
      ga.Media file = await drive.files 
          .delete(list.files[i].id);
    }
    await _listGoogleDriveFiles();//ファイルを再取得
    setState(() {
      lastUpdateTime = null; //表示用更新日付の空に
    });
    await Future.delayed(Duration(seconds: 1));
    Navigator.pop(context); //ローディング画面を閉じる

   //削除完了後の処理を書く

}

基本的にリストで取得して、その中にあるファイルをすべて繰り返し処理で削除するようにしています。

まとめ

いかがでしたか?
バックアップの概要がわかるまでは意味不明かと思いますが、どのように動作しているかわかってくれば、そこまで難しいものではないと思います。(改めてコードを見返してみると、リファクタリングしないとなと実感してます笑)

分からないところがあれば、コメントください。

どのように動作しているかは、アプリをダウンロードしてチェックしてみてください。
■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

またツイッターでもFlutterや個人開発についていろいろ呟いています。
チェックください!

https://twitter.com/oo_forward

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

【超初心者】WebデザイナーがiOSネイティブアプリ開発に挑戦して学んだこと共有

人類(というか自分)の大きな進歩についての感動を共有。

iOSネイティブアプリがシミュレーター&実機で動作するまでの取り組み。主にWebデザイナーから範囲広げたり、なんとなく学習考えている人向けの共有。

前提スペック

・某大手企業勤務の雇われ(使い捨てとも言う)Webデザイナー
・HTML、CSS、CMS業務レベル

取り組んだこと

・MacBookAir2020でXcode(Swift)を使って、iOSネイティブアプリの開発
・シミュレータとローカルの実機で、何となく動作させるまで(iPad)

大変だったこと

・全部英語w
・Appleは互換性を大事にしてそうな気がしたけど、実際は数ヶ月くらい?のハイペースでコロッコロ仕様を変えてやがる。参考書やWebの情報が少し古いだけで、あまり役に立たない
・XcodeやSwiftにもバージョンがあって、コロッコロ変わる
・環境構築が思いのほか、難しくて大変すぎる。1日仕事どころじゃない

意外だったこと

・画面のデザインはCSSとかでやると思ってたけど、ネイティブアプリ開発にCSSなんて全く登場しない(たぶん)
・アップル独自(swift)方式?みたいなパラメータでデザインする
・パワポで資料作るイメージ(パワポよりは難しい)
・細かく機種ごとの見え方の調整するのは、かなり難しそうで未学習

プログラミング(Swift)

・初歩レベルであれば、HTMLとかでコーディングしているイメージとそんなに変わらない。調べながらやれば何とかなる(たぶん)
・HTMLやCSSで格闘した経験は、RPGで言うと「二刀流」スキルを初めから持っている「強くてニューゲーム」な状態で有利
・やってみるとWebデザインより、世の中的に旬だし色々できそうで楽しい

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

Xcode11 プロキシ設定

プロキシ環境化で作業をしているため、設定をすることが多いのですが、
Xcode11でプロキシの設定が変わっていたので、残しておこうと思います。

変更ファイル

Xcode11以前
/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/lib/net.properties

Xcode11
/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/itms/java/lib/net.properties

変更内容

パスワード設定がある場合でも、パスワードの指定は不要でした。

動かない:disappointed_relieved:
http.proxyHost=http://username:p%40ssword@myproxy
http.proxyPort=8080

動いた:smiley:
http.proxyHost=http://myproxy
http.proxyPort=8080

悩み

simulatorのプロキシ設定方法がわからないです。:sob:

独り言

プロキシ環境での作業は色々ストレスがあって大変です。

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