- 投稿日:2020-07-08T20:54:41+09:00
NavigationControllerで戻る画面を自在に書き換える
NavigationControllerを利用したページ移動をしていて、戻るページのスタックを自分の好きなように書き換えたくなった時があった。2ページ前に戻るとか、ある条件を満たした時だけ戻れるページの上限が変わるなど。
NavigationController.viewControllerの仕組み
navigationController?.viewControllers // [1pページ目, 2pページ目, 3pページ目, 今のページ]NavigationControllerは上記のような感じで配列としてこれまで遷移したページを持っている。
基本的にはこれをpopしたりpushしたりする事で1画面遷移する。ViewControllerが入った配列
ViewControllerが入った配列なので、この配列を好きなように操作してやれば戻るページを書き換えられる。
ただ、基本的にはスタックとして使う事を想定しているはずなので行儀は良くない気がする。
例
UIViewControllerクラス内で
let fugaView = self.storyboard?.instantiateViewController(withIdentifier: "FugaViewWrapController") as! FugaViewController let hogeView = self.storyboard?.instantiateViewController(withIdentifier: "MainViewController") as! MainViewController self.navigationController?.viewControllers = [fugaView, hogeView, self]こうしてやると、これまでどんな遷移をしていようが次 hogeView → fugaView の順番に戻るようになる。
こんな感じで好きなようにカスタマイズ出来る、と思う。多分。
- 投稿日:2020-07-08T14:56:57+09:00
Mobile Safari は再起動時に POST リクエストする場合がある
Rails アプリケーションで、iPhone の Safari からのリクエストでだけ
ActionController::InvalidAuthenticityToken
例外が発生するので調査していたところ、Mobile Safari は再起動後、開いていたタブをロードする際に、POST リクエストで得たページであっても再度リクエストを送信しているらしいことがわかった。※ iOS 13.5.1 の iPhone 11 で検証
検証方法
下記の Ruby スクリプトを作成して
ruby server.rb -o 0.0.0.0
で起動# gem install sinatra が必要 require "sinatra" html = <<~HTML <!doctype html> <form action="/" method="post"> <input type="text" name="email"> <button type="submit">Submit</button> </form> HTML get "/" do html end post "/" do p params html end
- Mobile Safari で
http://[上記スクリプト実行したPCのアドレス]:4567
にアクセス- 適当に入力して submit
- Mobile Safari を終了
- Mobile Safari を起動 (確認などなしでページが表示される)
この手順でログを見ると、4 で 2 と同じリクエストが送られていることが確認できた。
x.x.x.x - - [08/Jul/2020:14:24:37 +0900] "GET / HTTP/1.1" 200 131 0.0072 {"email"=>"foo"} x.x.x.x - - [08/Jul/2020:14:24:41 +0900] "POST / HTTP/1.1" 200 131 0.0022 {"email"=>"foo"} x.x.x.x - - [08/Jul/2020:14:24:48 +0900] "POST / HTTP/1.1" 200 131 0.0007Rails で InvalidAuthenticityToken 例外が起こる理由と対策
Rails で...
- Cookie をセッションストアとして使っていて、かつ有効期限が Session (デフォルト)
protect_from_forgery with: :exception
を指定している場合に、上記のように 2 度 POST リクエストが送られると、2 度めのリクエストでは Cookie がクリアされているので
session[:_csrf_token]
が nil になり、params[:authenticity_token]
の検証が失敗し、InvalidAuthenticityToken 例外が発生してしまう。解決策としては下記のものが考えられる。
- POST でページを render せず、redirect してかならず GET させるようにする
- protect_from_forgery で null_session などにする
- Cookie の有効期限を伸ばす
3 の場合は config/initializers 下に適当なファイルを作って下記の内容を記述すればよい。
# key の部分はなんでもいいがデフォルトは "_#{アプリケーション名}_session" # ここでは有効期限を 2 週間にしている Rails.application.config.session_store :cookie_store, key: "_foo_session", expire_after: 2.weeks
- 投稿日:2020-07-08T13:18:43+09:00
アプリ�APIの負荷テストを自動生成したいと思った
概要
iOSアプリのバックエンドAPIに対して、負荷テストをしたい
アプリの通信内容をキャプチャして、テストシナリオを自動で生成できないか?
- 経験則からテスト対象APIを絞ることはできるが、意図しない組合せでAPIシステムが落ちることを防げない
- アプリのテストケースはない前提
プロトタイプ的にかんたんに動くものを作ってみた
注意事項
- 負荷テストは自身が管理するシステム以外に実行しないでください
登場人物
- APIコールをするやつ
- アプリの通信内容をキャプチャするアプリ
- アプリのバックエンド: REST APIサーバ
- 負荷試験ツール
- 許されることなら何もしたくない
やりたいこと
概要
詳細
負荷テストシナリオを生成する
charles for iosでアプリの通信内容(chlsj)をキャプチャし、エクスポートする
-> charles session json(chlsj)
- 参考: [iOS] Charles for iOSがリリースされたので試してみた
- アプリからcharles session をエクスポートしてpcに送る
charles session json(chlsj)からシナリオテンプレート(CSV)を生成する
変換スクリプト$python chlsj_2_scenario_csv.py {シナリオ名}.chlsj -> {シナリオ名}_template.csvが生成されるchlsj_2_scenario_csv.pyimport json import sys import csv import dateutil.parser import os initial_time = None def session_json_2_scenario_csv(data): request = data['request'] request_body = '' if ('body' in request) and ('text' in request['body']): request_body = request['body']['text'] # 呼び出し開始msecを計算 run_at = dateutil.parser.parse(data['times']['requestBegin']) global initial_time initial_time = initial_time or run_at run_msec_at = int((run_at-initial_time).total_seconds()*1000) # headerを{"name":"Authorizatoin","value":"~~~"}から{"Authorizatoin": "~~~"}方式に変換 request_header={} for kv in data['request']['header']['headers']: request_header[kv['name']] = kv['value'] # 不要なヘッダーを削除 del request_header['Host'] return { "start_msec_at": run_msec_at, "method": data['method'], "request": '?'.join( filter(None,[data['path'],data['query']])), "request_header":json.dumps(request_header), "request_body":request_body, "response_to_variable": None } def chlsj_2_scenario(chlsj_path): scenario = [] with open(chlsj_path) as in_file: for request in json.load(in_file): scenario.append( session_json_2_scenario_csv(request) ) out_file_path = os.path.basename(chlsj_path).split('.', 1)[0] + "_template.csv" with open(out_file_path, 'w') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=scenario[0].keys(),quotechar="'",quoting=csv.QUOTE_NONNUMERIC) writer.writeheader() writer.writerows(scenario) if __name__ == '__main__': chlsj_2_scenario(sys.argv[1])
- シナリオテンプレ(CSV)からシナリオ(CSV)を作る
- 必要に応じてシナリオ_テンプレの項目を変更し、「シナリオ」を作成する
start_msec_at method request request_header request_body response_to_replace テスト開始から何秒でコールするか
※プロトタイプでは未実装HTTP メソッド
GET/POST/PATCHREST path+クエリ HTTP header HTTP request body レスポンス内容を以降のリクエストで使用したい場合、
{変数名: '値の場所'}のjsonで変数をセットする
- 例
- 「POST /api/user_devices」をたたき、response bodyの「oauth_access_token.access_token」を「auth_token」として以降のリクエストで使用する
- header/bodyに"auth_token"の文字列が含まれる場合、「oauth_access_token.access_token」の内容にリプレースされる
start_msec_at method request request_header request_body response_to_replace 0 POST' /api/user_devices' {"X-LANGUAGE": "ja_JP", "X-TIME-ZONE": "Asia/Tokyo", "Authorization": "Bearer ~~~", "Accept": "/"} {"auth_token": "oauth_access_token.access_token"}' 負荷テストシナリオを実行する
- シナリオからlocustテストを実行する
locust起動## 初回のみlocustをインストールする $pip install locust $script_csv={シナリオ}.csv locust -f scenario.py -H https://{サーバアドレス}scenario.pyfrom locust import HttpUser,task, between import os import csv import json def is_json(string): try: json_object = json.loads(string) except ValueError as e: return False return True def xpath_get(mydict, path): elem = mydict try: for x in path.strip(".").split("."): elem = elem.get(x) except: pass return elem class ScenarioLoadTest(HttpUser): wait_time = between(0.500, 1) @task def test(self): api_list_csv_path = os.environ['script_csv'] replaces = {} with open(api_list_csv_path) as f: replace_map={} for row in csv.DictReader(f,quotechar="'",quoting=csv.QUOTE_NONNUMERIC): _request = row['request'] _header = row['request_header'] _request_body = row['request_body'].encode('utf8') # 変数のレンダー(文字リプレース) for key in replace_map: _request = _request.replace(key,replace_map[key]) _header = _header.replace(key,replace_map[key]) request = _request header = json.loads(_header) request_body = _request_body response = None if row['method']=='GET': self.client.get(request,headers=header) elif row['method']=='POST': raw_response = self.client.post(request,headers=header,data=request_body) response = raw_response.json() elif row['method']=='PATCH': self.client.patch(request,headers=header,data=request_body) else: print('called else ' + str(row)) # 変数の保存 if is_json(row['response_to_replace']): replace_map = json.loads(row['response_to_replace']) for key in replace_map: replace_map[key] = xpath_get(response,replace_map[key])今後やりたいこと
- aws device farmでアプリの表示テストを動かしたい
- そのついでにスクリプトを自動生成できない?
- 投稿日:2020-07-08T11:45:44+09:00
Danger用にxcpretty-json-formatterで テストを実行して ld: symbol(s) not found for architecture x86_64 になった時に対応したこと
前段
- BitriseのWorkflow内でDangerを回している
- danger-xcode_summaryを利用
- xcpretty-json-formatterも利用
- Bitriseの
XcodeTest for iOS
は利用せずスクリプトでテスト実行- FirebaseはCocoaPodsで入れる
- Xcode11.3.1, CocoaPods1.5.3, FirebaseCore6.6.6
現象
xcpretty-json-formatterの出力を指定した以下のスクリプトでテストビルドを実行
set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild -workspace $WORKSPACE -scheme $SCHEME \ clean build test -destination "platform=iOS Simulator,name=iPhone Xs,OS=12.4" \ GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | \ XCPRETTY_JSON_FILE_OUTPUT=xcodebuild.json xcpretty -f `xcpretty-json-formatter`CocoaPodsにてFirebase/Coreを入れたタイミングで上記スクリプトにて表題のエラーが発生
対応内容
Build Settings > Other Linker Flags
に-fprofile-arcs
を追加スクリプトは以下に変更
set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild -workspace $WORKSPACE -scheme $SCHEME \ clean build test -destination "platform=iOS Simulator,name=iPhone Xs,OS=12.4" \ "GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES" "GCC_GENERATE_TEST_COVERAGE_FILES=YES" \ "OTHER_LDFLAGS=\$(OTHER_LDFLAGS) -fprofile-arcs" | \ XCPRETTY_JSON_FILE_OUTPUT=xcodebuild.json xcpretty -f `xcpretty-json-formatter`番外編
Dangerとは関係ないのですがUITestでもCocoaPodsにてFirebase/Coreを入れたタイミングでエラーが発生したので合わせて記載します。
UITest実行時に
「バンドル“アプリ名UITests”は、壊れているか必要なリソースがないため読み込めませんでした。 …」
が吐かれていたのですが以下の対応で通るようになりました
Build Settings > Runpath Search Paths
に$(FRAMEWORK_SEARCH_PATHS)
を追加参考リンク
助けて頂きました。ありがとうございます?
- 投稿日:2020-07-08T02:08:49+09:00
UIKitに依存するSwift PackageをVSCodeで開発する
XcodeがクソすぎてVSCodeが好きすぎてSwiftのコードを書くのにもVSCodeを使いたい!
と思って調べてみたところ、どうやらSwift PackageのプロジェクトならVSCodeでもそれなりにコード補完できるようになる、ということがわかりました。こちらにあるのがApple公式のsourcekit-lsp(Swiftの構文補完のためのLanguage Server)リポジトリで、中を見るとVSCode用の拡張機能のコードもあるみたい。
なので、まずはそちらのREADMEに従ってVSCodeのsourcekit-lsp拡張機能をインストールします。上記のsourcekit-lspのリポジトリをクローンしてきて、
$ cd Editors/vscode $ npm run createDevPackage $ code --install-extension out/sourcekit-lsp-vscode-dev.vsixとするだけです!
※ Xcodeのインストールが必要です。
※sourcekit-lsp
コマンドが実行できるようになっている必要があります。最近のXcodeなら入ってる…?もしくは上記リポジトリからビルドする。
※ npm コマンドを使うために、Node.jsのインストールが必要です。
※ code コマンドを使うために、VSCodeでCmd+Shift+P
からInstall code command in PATH
を実行する必要があります。ではさっそくプロジェクトを作ります。
swift package initプロジェクトができたら、さっそくコードを書いてみましょう。
Sources/my-swift-ios(作ったディレクトリにより異なる)
の中にMyViewController.swiftというファイルを作って次のようなコードを書きます。import UIKit…おや、
import UIKit
のところに赤い線が引かれて no such module UIKit と言われていますね。そしたらVSCodeの設定画面を開き(
Cmd+,
)、右上の「設定(JSON)を開く」アイコンを押してsettings.jsonを開きます。
そして、その中に以下のような指定を加えます。{ "sourcekit-lsp.serverArguments": [ "-Xswiftc", "-sdk", "-Xswiftc", "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk", "-Xswiftc", "-target", "-Xswiftc", "x86_64-apple-ios13.5-simulator", ] }iPhoneSimulator13.5とかios13.5-simulatorとかいう部分はXcodeのバージョンにより正しい指定が異なるので、実際に
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs
の場所をFinderで開いて番号を確認してください。設定ができたら、VSCodeを再起動(もしくは
Cmd+Shift+P
→Reload Window)します。改めて先程のswiftファイルを開いてみましょう。赤い線が消えていると思います(消えていない場合、上記の
-Xswiftc
で渡しているパスが正しいかどうか確認してください)。コード補完できてる〜!!!!
ちなみに、先程指定した
sourcekit-lsp.serverArguments
の設定は、swift build
をするときにも同様に必要になります。
なので、.vscode/tasks.json
の中にこんな感じで指定すると良いでしょう。{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "swift", "args": [ "build", "-Xswiftc", "-sdk", "-Xswiftc", "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk", "-Xswiftc", "-target", "-Xswiftc", "x86_64-apple-ios13.5-simulator", ], "group": { "kind": "build", "isDefault": true } } ] }これでVSCodeのビルド(
Cmd+Shift+B
)をするだけでビルドできるようになります。テスト
ここまで書いて気付いたのですが、テスト(
swift test
)が動かない…orz
.vscode/tasks.json
に以下のテスト実行の追記をしてみましたが…{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "swift", "args": [ "build", "-Xswiftc", "-sdk", "-Xswiftc", "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk", "-Xswiftc", "-target", "-Xswiftc", "x86_64-apple-ios13.5-simulator", ], "group": { "kind": "build", "isDefault": true } }, { "label": "test", "type": "shell", "command": "swift", "args": [ "test", "-Xswiftc", "-sdk", "-Xswiftc", "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk", "-Xswiftc", "-target", "-Xswiftc", "x86_64-apple-ios13.5-simulator", ], "group": { "kind": "test", "isDefault": true } } ] }error: module 'XCTest' was created for incompatible target と言われて実行できない。
2020/07/09追記:
ですが、xcodebuildコマンドを使うことでiPhone Simulator上でテストが可能であるということがわかりました!.vscode/tasks.json{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "swift", "args": [ "build", "-Xswiftc", "-sdk", "-Xswiftc", "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk", "-Xswiftc", "-target", "-Xswiftc", "x86_64-apple-ios13.5-simulator", ], "group": { "kind": "build", "isDefault": true } }, { "label": "test", "type": "shell", "command": "xcodebuild", "args": [ "-scheme", "my-swift-ios", "test", "-destination", "name=iPhone 8" ], "group": { "kind": "test", "isDefault": true } } ] }これでiOSのコードがVSCodeでもそれなりに開発できる…かも!?!?