- 投稿日:2020-07-08T23:09:07+09:00
SVProgressHUDが左上に固定される場合
SVProgressHUDがなぜか左上に表示される場合
SVProgressHUDの中心が(0, 0)になってしまうことがあるようです。
その場合の応急処置。func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. SVProgressHUD.setOffsetFromCenter( UIOffset(horizontal: UIScreen.main.bounds.width/2, vertical: UIScreen.main.bounds.height/2) ) }オフセットを調整して中央に来るようにする。
あまりよろしくないが、急ぎの場合はこれで。よくあるコードをSwiftで
あまり関係ないかもしれないけど、、、ずれを検出するコード
extension SVProgressHUD { static func adjustSVProgressHuDHudViewCenter() -> CGFloat { var keyboardWindow: UIWindow? for window in UIApplication.shared.windows { if window.nameOfClass == UIWindow.nameOfClass { keyboardWindow = window break } } if let keyboardWindow = keyboardWindow { for possibleKeyboard in keyboardWindow.subviews { if possibleKeyboard.nameOfClass == "UIPeripheralHostView" || possibleKeyboard.nameOfClass == "UIKeyboard" { return possibleKeyboard.bounds.height } else { if possibleKeyboard.nameOfClass == "UIInputSetContainerView" { for possibleKeyboardSubview in possibleKeyboard.subviews { if possibleKeyboardSubview.nameOfClass == "UIInputSetHostView" { return possibleKeyboardSubview.bounds.height } } } } } } return 0 } } extension NSObject { static var nameOfClass: String { return NSStringFromClass(self).components(separatedBy: ".").last! } var nameOfClass: String { return NSStringFromClass(type(of: self)).components(separatedBy: ".").last! } }
- 投稿日: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:02:31+09:00
UIPickerView 文字のサイズを合わせる
UIPickerViewの選択肢が長くて入らない場合
UIPickerView上のラベルの文字サイズを調整してピッカー内に入るようにする。
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { let label = (view as? UILabel) ?? UILabel() label.text = self.addresses[row] label.textAlignment = .center label.adjustsFontSizeToFitWidth = true return label }view が取れない場合もあるので
?? UILabel()
を忘れないように
- 投稿日:2020-07-08T11:21:37+09:00
[個人アプリ開発記]スクショ・画面録画がバレるトークアプリ
はじめに
こんにちは。先日2つ目となるアプリをリリースしましたので、その機能や流れについて書こうと思います。一つ目についてはこちら。ちなみにリジェクトは4回されました。匿名投稿ができるアプリが審査を通るために持たせなければならない機能についても書いています。
アプリの紹介
機能としてはLINEのようなトークアプリなのですが、友達追加やアカウント登録という概念をなくし、ルーム名とパスワードさえお互い分かっていれば誰とでも話すことができるようにしました。
![]()
![]()
トーク画面にはスクショ・画面録画を行うとメッセージが自動送信される仕組みになっています。
![]()
審査を通るために追加した処理ですが、不適切な内容を投稿するユーザーをブロックすることや通報することもできます。
![]()
報告された内容は開発者の元に届き、不適切な利用が確認された場合はユーザーの利用を停止させることもできます。
アプリを作ろうと思った理由
インスタでストーリをスクショすると相手にバレる?みたいな噂を聞き、そもそもスクショを検出することは可能なのか?と思ったことがきっかけです。この記事で可能であることを知り、LINEなどのトークアプリに実装したら面白そうだなと思ったのが制作を始めた理由です。その上で、LINEなど既存のトークアプリと差別化を図るために、メールアドレスや電話番号による会員登録不要、ルーム名&パスワード入力によるチャットルームの製作・参加機能を取り入れることにしました。
使用ライブラリ
Firebase
MessageKit
SVProgressHUD苦労したところ
App Storeの審査がなかなか通らない
匿名投稿機能を持つアプリが審査をパスするためには利用規約への同意、不適切な投稿の通報&ブロック機能、危険なユーザーの利用停止機能、管理者と直接コンタクトを取る手段の提供などの機能を持っている必要があります。以下は最初に出したリジェクトの一部です。
A method for filtering objectionable content
A mechanism for users to flag objectionable content
A mechanism for users to block abusive users
A mechanism for users to immediately remove posts from the feed
Developer must act on objectionable content reports within 24 hours by removing the content and ejecting the user who provided the offending content
Developer must provide contact information in the app itself, giving users the ability to report inappropriate activityトーク機能よりもユーザーがブロックした内容を非表示にする機能や利用停止機能をつけるのに苦労しました。FireStoreから削除してしまうとブロックしていない他のユーザーの画面からも消えてしまうので、削除ではなく非表示にする必要があります。
Cloud FireStoreの利用
Cloud Firestore(以下Firestore)とはFirebaseが提供しているデータベースの一つです。FirebaseはデータベースとしてRealtime Databse(古いほう)とFirestore(新しい方)の二つを提供しているのですが、どうやらFirebaseが推奨しているのはFirestoreの方っぽい。参考一応Realtime Databaseも使ってみたのですが、ブラウザ上でのデザインから管理しやすそうという印象を受けたのでFirestoreを利用することにしました。が、ネット上の記事はほとんどRealtime Databaseで書かれたものであったため製作において苦労しました。
Cloud Firestoreで具体的にどう使ったのかは後述します。NotificationCenterの管理
スクショ・画面録画の検出はNotificationCenterによって実装しています。また、別のアプリからマルチタスク画面(ホームボタンを2回押す)を利用してスクリーンショットを撮られると検出できません。それを防ぐためにNotificationCenterを利用しホームボタンを押すことでトーク画面から別の画面へ遷移するようにしました。NotificationCenterが望まないタイミングで発火することがあり、処理に苦労しました。
ルーム名・パスワードの入力欄
ルーム名やパスワードに文字数の制限がないと、ユーザー同士に意図しない接触が発生してしまいます。ルーム名は1文字以上、パスワードには10文字以上という制限を加え、それらの自動作成ボタンを作ることで安全性と利便性を向上させました。ルーム名のTextFieldに文字が入力されており、かつパスワードのTextFieldに文字数が10文字以上入力されている時のみ(①)ボタンを押せるように、またそれぞれの自動作成ボタンを押されたときに押されていない方のTextFieldに①の条件が満たされている時のみボタンを押せるようにするという複雑な処理を実装するのに苦労しました。
技術を得るために
Qiitaでのアウトプット
調べればわかることでも、何度もそのサイトに訪れることは結構ストレスを感じます。例外的に自分の記事は何度訪れてもストレスを感じないので、些細なことや既出の内容でも記事にするようにしています。
Xcodeで画面遷移とともにデータを渡す
Notificationによる通知
複数のTextFieldにそれぞれ文字数の制限をつける
NotificationCenter.default.removeObserverの大切さを痛感した話
スクショ・画面録画されたことを通知する方法
NavigationControllerの下の色を変える方法
ラジオボタン(選択肢)を作る(xcode)
NavigationControllerからモーダル遷移をする時に出たエラーCould not cast value of type..
アプリを再審査に提出する際にYour package contains a file ..と出た
Firebase/Authenticationで匿名ログインにする方法
初回起動時のみ画面遷移する方法
UserDefaultsに配列を追加・リセット
メッセージに含まれる特定の単語を*で置き換える
実行した時刻を数字として取得することで時系列でソートできるようにするTeratailでの質問
調べても解決できない時はTeratailで質問しました。回答やコメントしてくださった方々にはとても感謝しています。
func applicationWillResignActiveが呼ばれない
Database.database().reference() と Firestore.firestore().collection().document()の違い
UI部品の重なりを解消したい
UIKitの中にSwiftUIを入れた時Buttonが反応しない
willTerminateNotificationでマルチタスクからアプリを消去した時に特定の処理ができない
Firestoreから取得したデータの順番を時系列にしたい
Could not cast value of type..というエラーが解決できないFireStoreの構造
Databaseには"Rooms"と"Reports"の二つのコレクションを持っています。アプリ内でルームが作成されると、コレクションRoomsには"ルーム名"+"パスワード"をドキュメント名に持つドキュメントが作成されます。
そして、メッセージが送信されると"messages"コレクションにデータがセットされます。
ユーザーが通報を行った際には各ルームドキュメントに"report"コレクションが作成されると同時に、同じ内容が"Reports"ドキュメントにも追加されます。
利用規約に同意することで匿名登録が行われます。アカウントはuidというユーザーが一意に定まる文字列を持つため、ブロック機能の際にはこのuidを利用しています。
終わりに
画像・動画を送れる機能やブデザインの向上などまだまだ実装したい内容はたくさんあります。これからも開発を継続していきます。
teratailで回答・コメントしてくださった方を始め様々な方のおかげでアプリを完成させることができました。ありがとうございました。
- 投稿日:2020-07-08T10:39:01+09:00
練習のためにSwiftをUdemyで勉強してみた(1日目)
まず、誰?
どこにでもいる社会人です。
普段、職場ではGASを使って仕事の効率化を図りながら日々を過ごしております。
もともとVBA、GASと業務効率化のためにプログラミングを行っておりましたが、職場内だけで使えるものではなくて、世界中の人が触れるものを作りたいとと思い、iPhoneアプリを作ろうと思い至りました。
なんでWebアプリじゃないのかって?
覚えることが多岐にわたって面倒くさそうだったので。
iPhoneアプリだったらとりあえずSwiftができればいいだろうと。
将来的にはWebアプリにも手を伸ばしていきたいです。何で勉強するの?
Udemyの以下の講座を受講しました。
ちょうどセールで安かったので。色々Swiftの言語の基本を学ぶよりかは、早速実物を作りたいと思い、実践重視のこれにしました。
【6日で速習】iOS 13アプリ開発入門決定版 20個のアプリを作って学ぼう(Xcode 11, Swift 5対応中)
https://www.udemy.com/course/ios11basics/1日目
2つ作りました。
FirstMap
https://github.com/blumemond10/FirstMap
Swiftの使い方確認も兼ねて、単純にMap Kit Viewを配置しただけです。
プログラミングのプもしていませんw
後々、現在地を表示したり、指定の住所の地図を出せるようにしたいです。FirstCamera
https://github.com/blumemond10/FirstCamera
一気にコーディングしていったので、何が起きたのかよくわからないまま完成に至りましたw
あとでしっかりコードを読んで、何をしたのか把握したいです。
以下を記述したけど。。。ViewController.swiftimport UIKit class ViewController: UIViewController,UINavigationControllerDelegate,UIImagePickerControllerDelegate { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBOutlet weak var photoImage: UIImageView! @IBAction func cameraLaunchAction(_ sender: Any) { if UIImagePickerController.isSourceTypeAvailable(.camera){ print("Camera can be usd.") let ipc = UIImagePickerController() ipc.sourceType = .camera ipc.delegate = self present(ipc,animated:true,completion:nil) }else{ print("Camera is not avaiable.") } } @IBAction func shareAction(_ sender: Any) { if let sharedimage = photoImage.image{ let sharedItems = [sharedimage] let controller = UIActivityViewController(activityItems: sharedItems, applicationActivities: nil) controller.popoverPresentationController?.sourceView = view present(controller,animated:true,completion: nil) } } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { photoImage.image = info[UIImagePickerController.InfoKey.originalImage]as? UIImage dismiss(animated:true,completion: nil) } }次にやること
- 上記コードの内容を理解する
- 次のアプリを作ってみる
がんばります。
- 投稿日:2020-07-08T06:44:09+09:00
Danger から Danger-Swift 移行への手引き
なぜ Danger-Swift に移行(しようと)したか
以前の記事にも書いたとおり、弊社はソースコードの品質担保の一環として Danger による PR の機械チェックが導入されています。通常のコードフォーマットが合ってるかどうか、ビルドワーニングが発生してるかどうか以外にも、テストのカバレッジがどれくらいか、(バージョンリリース時に)バージョン番号や Changelog がちゃんと更新されているかどうか、更にブランチの設定が正しいかどうかや、そのブランチ名からチケット番号を抽出して自動で PR ページにチケットリンクへ書き込むなどの機能も Danger で実現させています。割と Danger のヘビーユーザです。
ところが、そんな Danger ですが、もちろん多くの機能はプラグインによって実現されており、例えばその「テストのカバレッジがどれくらいか」というのはまさに Danger-slather というプラグインによって実現されています。
そして最近、社内では純粋 SwiftPM プロジェクトが新しくできています。文字通り純粋 SwiftPM 案件なので、通常の Xcode プロジェクトでお馴染みの
.xcodeproj
ファイルや Scheme の設定などはありません。しかし残念ながら、Danger で Xcode プロジェクトのテストカバレッジを取得してるこの Danger-slather も danger-xcov も、.xcodeproj
ファイルと Scheme の設定に強く依存しています。特に Danger-slather は 4 年以上更新されておらず、danger-xcov も以前導入を試みたときはっきり覚えていませんがなぜかカバレッジをうまく読み取れなかったことが何回かあったため結局断念した記憶があります。ここで思い出したのが Danger-Swift の存在です。Danger-Swift は Danger と全く同じ目的のものですが、Swift で実装されており、Dangerfile も Ruby ではなく Swift で記述されます。そのため Swift 信者の筆者はその当時から導入したかったのですが、Danger と Danger-Swift はプラグインがお互い互換性が全くなく、そして当時の Danger-Swift プラグインライナップは非常に貧弱だったため断念しました。
ところで今調べ直してみたら、SwiftLint はもちろん、当時まだなかったカバレッジを取得する DangerSwiftCoverage やビルドワーニングを取得する DangerXCodeSummary も揃えられたので、必要なものはとりあえず一通り揃えたと感じて今回の導入に踏み切りました。
移行してみてどうか
まだ移行したばかりなので深いところまでは突っ込めませんが、ひとまず今現在の感想を良かったところと悪かったところを分けて述べます。
良かったところ
Swift で Dangerfile 書けるので、Swift 信者に私にとっては非常にやりやすい気持ちです。
まず Swift には型がちゃんとあるので、その型に応じたプロパティーやメソッドの参照が可能になり、「この属性ってどんな型だっけ?」とか「このデータを取得するにはどこ経由でどう取ればいいんだっけ?」と言った仕様に基づいた使用は Ruby と比べて非常に楽です。何より大抵のことは型で保証してくれてますので。
そして Swift の表現力にも非常に助かります。後述しますが例えば共通の出力フォーマットを定義したいとき、とりあえず
struct
で定義しとけば、あとで算出プロパティーとかメソッドとかクロージャの力でいろんなことが実現可能です。(もちろんプロパティーとかは Ruby もできますが、何せ型がないのでね…)最後はやはり使い慣れてる言語で書けるのもとてもいいところですね。正規表現の抽出とか配列の操作とか、これは普段の業務でもいつもやってることなのでどう書けばいいかすぐわかりますから、調べながら動作テストしながら書いていくしかない Ruby と比べると作業効率爆上げです(もちろんそもそも Ruby に慣れてる人でしたら Ruby の方が作業効率が高いでしょう)。
ちなみに Danger-Swift は
Dangerfile.swift
を編集するための Xcode プロジェクト環境を自動で生成できるので、本体プロジェクトに影響を及ぼさずに Dangerfile の編集が可能です。悪かったところ
残念ながら Danger-Swift は実は罠もたくさん潜んでありますね。まあ Swift はそもそもスクリプト言語ではないのに無理やりスクリプトとして動かしてるから仕方ないのかもしれません。
まず意外なところですが、実は Danger-Swift は JavaScript 製の Danger ツール Danger-JS に依存しています。まだ具体的に Danger-Swift の実装を見ていないのでどこで何のために使ってるかまでは把握していませんが、ただそのせいで時々 Danger-JS のエラーが吐き出されて何が何だかよくわからないことが稀によくあります。そしてこの Danger-JS のコマンドは Ruby 製の Danger ツールと同じ
danger
のため、Danger-JS と Danger の共存ができず、すでに Ruby 製の Danger をインストール済みの場合は先にこれをアンインストールしなくてはいけないのです。まあ Swift は所詮スクリプト言語ではなくコンパイル言語なので、それは仕方ないのかもしれません(Danger-Swift の詳しい実装見てないので適当に言ってみただけです)。次に Danger-JS 抜きにしても、Danger-Swift のエラーも比較的にわかりにくいことがあり、しかも Ruby 製 Danger と比べて情報が圧倒的に少ないので、何か躓いたら割とかなり難航します。
さらには Danger では
git
にあったいくつかのもの、例えば修正量を抽出するlines_of_code
とか、は Danger-Swift の場合なぜかgithub
とかのサービス依存のプロパティーに移動されてしまいました;その上github
プロパティーは IUO ですので、それが当たり前のように存在してる気持ちでDangerfile.swift
書いてたら、danger-swift local
で動作確認するとgithub
がnil
になって落ちちゃいます。ちなみにこのエラーもメッセージがわかりにくい(どのプロパティーがnil
なのかが教えられていない)ため、私はこれの原因を突き止めるのにかなり時間かかりました。これらの悪いところで更に次に悪かったところを引き出します:
github
がnil
になっちゃうのに、割と多くのデータ取得がgithub
に依存してしまうため、完全にローカルだけで完結できる動作確認(danger-swift local
)が非常に限られており、結局 PR つくってそれに基づいたローカル動作確認(danger-swift pr [PR URL]
)が必要になります。そして機能面でも、実は Ruby 製 Danger より低いところがあります。例えば Danger-Swift は直接特定ファイルの diff を抽出できません、それをやりたい場合は残念ながら自分で Swift から shell コマンドを呼び出す必要があります。(そして残念ながら今のところ私はまだこれを使った機能の動作確認ができていないので、本当にうまくいくかどうかの確証がまだないのです…)
最後にまた後述しますが、良かったところでは
Dangerfile.swift
を編集するための Xcode プロジェクト環境を自動で生成できると書いてありますが、そのための手順があるのでそれが抜けちゃうと編集が反映されないことがあります。実際の移行手順
Danger-Swift のインストール
Danger-Swift は基本 CI 側で実行されるものですが、Dangerfile.swift ファイルの正規性を確認するためにもローカルで実行できる方が嬉しいです。
ところでここでいきなり最初の罠にハマります:悪かったところにも書きましたが Ruby 製 Danger との共存ができませんが、タイトルで書いたとおり筆者は「移行」でしたので、元々ローカルには Ruby 製の Danger がインストールされているのです。そのためこのまま
brew install danger/tap/danger-swift
でインストールしても、「すでにdanger
がありますよ」的なことが言われてインストール失敗します。そのためまず最初は既存の Ruby 製 Danger をアンインストールする必要があります。Dangerfile.swift の作成
Danger-Swift がインストールされたら、次は確認スクリプトの Dangerfile.swift を作る必要があります。Ruby 製 Danger の場合は自分で
Dangerfile
を勝手につくっちゃえばいいから、Danger-Swift もまあ同じ方法でやっても問題はないのですが、実は Danger-Swift は Dangerfile.swift の簡易な構築環境も用意してくれています。danger-swift edit
コマンドを叩けば、Dangerfile.swift
の Xcode プロジェクトが立ち上がりますので、この時点でもしDangerfile.swift
がまだなかったら自動で最低限のものを生成してくれますし、あったらそれをそのまま使ってくれます。Swift は Playground とmain.swift
以外で直接関数の呼び出しができないので、この構築環境は自動でプロジェクト内でmain.swift
をつくってくれます。またdanger-swift edit
コマンドは自動終了しないので、編集が終わったらmain.swift
を保存すればその内容が自動的にDangerfile.swift
に反映されてくれます。そして完全に作業が終わったらdanger-swift edit
を叩いたターミナルに行ってreturn
キーを押せば終了できます。ただし注意しないといけないのは、この構築環境のプロジェクトは
Dangerfile.swift
のみに基づいて自動生成されるものなので、このプロジェクト内でmain.swift
以外のソースファイルとか追加してもDangerfile.swift
に反映されませんし実行できません。必要なものは全てmain.swift
内で完結する必要があります。また悪かったところにも軽く書いてありますが、その作業が終わったらターミナルだけ終了して Xcode プロジェクトを開いたままにすると、今度は
danger-swift edit
を叩かずにそのまま開いた状態の Dangerfile プロジェクト編集しちゃっても、それを自動でDangerfile.swift
に反映される術はありません。またその修正された状態でdanger-swift edit
を叩くと、反映される前の古いコードのDangerfile.swift
の内容がmain.swift
に適用されちゃいますので、今までの編集が全て失われてしまいます。なのでここでの TIPS としては、Dangerfile プロジェクトは保存すると修正が自動的にDangerfile.swift
に反映されるので、作業が本当に終わるまではdanger-swift edit
をそのまま閉じずに動かしてください。そして作業が終わったら必ず Dangerfile プロジェクトを閉じちゃった方が無難です。プラグインの利用
Danger-Swift はあくまで CI 環境で動かすものであってプロジェクト自体にとって必須ではありません。そのため私は基本的に Danger-Swift を
Package.swift
とかのパッケージ管理ファイルには追加しない方が好きです。しかしそうすると今度は逆に自分のDangerfile.swift
が依存しているプラグインの管理が難しくなります。でもご安心ください、幸いなことに Danger-Swift は Marathon(すでに Deprecated にはなってますが)を使って依存解決が可能です。方法としては
Dangerfile.swift
のimport
の行の最後に、コメントで// package: PackageURL
を書いとけばいいです。例えば DangerSwiftCoverage の導入は、下記のようなコードで書けばいいです:import DangerSwiftCoverage // package: https://github.com/f-meloni/danger-swift-coverage.gitこうすると、
danger-swift edit
コマンドを叩くと、Danger-Swift は自動的に DangerSwiftCoverage を落としてきてプロジェクト内で使える状態にしてくれます。非常に便利ですし、ライブラリー配布の場合も Danger によるPackage.swift
の不必要な依存が増える心配もないです。ちなみにもしそれ書いても
No such module
のエラーが出るなら、一回 Xcode を終了してもう一回danger-swift edit
やり直してみてください、私の場合はこれで解決しました。Dangerfile.swift の動作確認
悪かったところにも書いてありますが、残念ながら
danger-swift local
で確認できる動作は割と限られています。必要最小限の動作確認(例えば修正があるファイルのパスだけ出力してみる確認とか)ならdanger-swift local
で確認できますが、github
に依存している動作確認は必ずdanger-swift pr PR_URL
で確認する必要があります。CI で導入
Danger-Swift は CI 側で使うツールなので、最終的に CI に導入しないと意味がありません。
まずは CI での Danger-Swift のインストールですが、これはローカルでのインストールと全く同じです。ただし場合によってはキャッシュに Ruby 製の Danger が残ってる可能性がありますので、Danger-Swift をインストールする前に Ruby 製 Danger をアンインストールするスクリプトを組み込むか、キャッシュを削除して作り直す必要があります(個人的にはキャッシュの作り直しがいいと思います)。
次に Ruby 製 Danger と同じく、GitHub のリポジトリーの場合
DANGER_GITHUB_API_TOKEN
の環境変数を設定する必要があります。これは Ruby 製の Danger のものを流用すればいいですし、もしそもそも Ruby 製 Danger を今まで使ったことがなければ、GitHub で Personal access token を新たにつくって CI にそれを設定すればいいです。プラグインは先ほどにも書いた通り、Marathon 形式で書いていれば Danger-Swift が自動で解決してくれるので、Ruby 製の Danger みたいにわざわざ手動でインストールする必要は特にありません。
そして最後は実行ですが、CI で実行するときのコマンドは
danger-swift ci
です。サンプル Dangerfile
当然ながら流石に業務のプロジェクトを見せるわけにはいけませんので、その Dangerfile だけひとまずサンプルとしてあげておきます。プロジェクトに依存している情報は適当に書き換えていますので、自分のニーズに合わせて直せばいいです。Swift コードなので多分読むのはそんなに難しくないはずです。チェックする項目が多いのでクッソ長いですが(時間あったら個人プロジェクトにこれを組み込みますので、終わったらそのプロジェクトもこの記事に入れる予定です)。
import Foundation import Danger import DangerXCodeSummary // package: https://github.com/f-meloni/danger-swift-xcodesummary.git import DangerSwiftCoverage // package: https://github.com/f-meloni/danger-swift-coverage.git // swiftlint:disable file_length function_body_length // MARK: - Variables those may change according to each project let changelogPath = "CHANGELOG.md" let versionSpecifyingFile = "Sources/Agen/main.swift" let versionSpecifyingText = "let version = " // MARK: - A structure to introduce checking result struct CheckResult { let title: String private(set) var warningsCount = 0 private(set) var errorsCount = 0 enum Result { case good case acceptable case rejected var markdownSymbol: String { switch self { case .good: return ":tada:" case .acceptable: return ":thinking:" case .rejected: return ":no_good:" } } } typealias Message = (content: String, result: Result) private var messages: [Message] = [] private var todos: [String] = [] init(title: String) { self.title = title } mutating func askReviewer(to taskToDo: String) { todos.append(taskToDo) } mutating func check(_ item: String, execution: () -> Result) { let result = execution() messages.append((item, result)) switch result { case .good: break case .acceptable: warningsCount += 1 case .rejected: errorsCount += 1 } } var markdownTitle: String { "### " + title } var markdownMessage: String { let chartHeader = """ Checking Item | Result | ---| --- | """ let chartContent = messages.map { "\($0.content) | \($0.result.markdownSymbol)" } .joined(separator: "\n") return chartHeader + chartContent } var markdownTodos: String { let todoContent = todos.map { "- [ ] \($0)" } return todoContent.joined(separator: "\n") } } // MARK: - DangerDSL computed properties extension DangerDSL { private var headBranch: String { github.pullRequest.head.ref } private var baseBranch: String { github.pullRequest.base.ref } private var additions: Int { github.pullRequest.additions ?? 0 } private var deletions: Int { github.pullRequest.deletions ?? 0 } private var diffLinesCount: Int { additions + deletions } private func hasModifiedFile(at filepath: String) -> Bool { git.modifiedFiles.contains(where: { $0 == filepath }) } var githubIssue: String? { headBranch.substring(of: #"issue/(\d+)"#, options: .regularExpression) } var hasBootstrapSHFileBeenModified: Bool { hasModifiedFile(at: "bootstrap.sh") } var hasBrewfileBeenModified: Bool { hasModifiedFile(at: "Brewfile") } var hasChangelogBeenModified: Bool { hasModifiedFile(at: changelogPath) } var hasMarketingVersionBeenModified: Bool { let diff = Process.runShell("git diff -- \(versionSpecifyingFile)").components(separatedBy: "\n") let additions = diff.filter({ $0.hasPrefix("+") }) return additions.contains(where: { $0.contains(versionSpecifyingText) }) } var isDevelopPR: Bool { // Treat PRs merging into develop branch as a develop PR baseBranch == "develop" } var isReleasePR: Bool { // Treat PRs merging into master branch as a release PR baseBranch == "master" } } // MARK: - DangerDSL PR content check extension DangerDSL { enum BranchType { case master case develop case feature case refactor case fix case issue case version case ci static func parsed(from branchName: String) -> BranchType? { switch branchName { case "master": return .master case "develop": return .develop case let feature where feature.hasPrefix("feature/"): return .feature case let refactor where refactor.hasPrefix("refactor/"): return .refactor case let fix where fix.hasPrefix("fix/"): return .fix case let issue where issue.hasPrefix("issue/"): return .issue case let version where version.hasPrefix("version/"): return .version case let ci where ci.hasPrefix("ci/"): return .ci case _: return nil } } } func isHeadBranch(_ branchType: BranchType) -> Bool { BranchType.parsed(from: headBranch) == branchType } func isHeadBranch(anyOf branchTypes: [BranchType]) -> Bool { branchTypes.contains(where: { isHeadBranch($0) }) } func isBaseBranch(_ branchType: BranchType) -> Bool { BranchType.parsed(from: baseBranch) == branchType } } // MARK: - DangerDSL modifications content check extension DangerDSL { // Auto PR created by CI func checkCIAutoPRModification(into result: inout CheckResult) { result.askReviewer(to: "Check if the automatically-created-by-CI content is correct.") warn("This PR is created by CI automatically. Please check if the content is correct.") } func checkDevelopmentModification(into result: inout CheckResult) { // It's encouraged to edit changelog in a develop PR let doChangelogModificationCheckTitle = "Changelog Modification Check" result.check(doChangelogModificationCheckTitle) { if hasChangelogBeenModified { return .good } else { warn("This PR doesn't contain any modifications to changelog. Please consider if it's necessary to edit it.") return .acceptable } } } func checkReleaseModification(into result: inout CheckResult) { // It's encouraged to check if there's any issue left before releasing a version result.askReviewer(to: "Check open issues") warn("This is a release PR. Please check if there's any issue left to be resolved.") // It's required to change version number. let doVersionModificationCheckTitle = "Version Modification Check" result.check(doVersionModificationCheckTitle) { if hasMarketingVersionBeenModified { warn("This is a release PR. Please check if the version has been correctly modified.") return .acceptable } else { fail("This is a release PR, but it seems there's no version modification, which is requried.") return .rejected } } if hasMarketingVersionBeenModified { result.askReviewer(to: doVersionModificationCheckTitle) } // It's strongly encouraged to edit changelog in a release PR. let doChangelogModificationCheckTitle = "Changelog Modification Check" result.check(doChangelogModificationCheckTitle) { if hasChangelogBeenModified { warn("This is a release PR. Please check if the changelog has been correctly modified.") return .acceptable } else { fail("This is a release PR, but it seems there's no changelog modification, which is strongly encouraged.") return .rejected } } if hasChangelogBeenModified { result.askReviewer(to: doChangelogModificationCheckTitle) } } } // MARK: - DangerDSL PR review flow extension DangerDSL { func doDevelopPRCheck() -> CheckResult { var result = CheckResult(title: "Develop PR Check") // Develop PR should be created from a branch which begins with either `feature/`、`refactor/` 、`fix/`、`issue/`, `version/` or `ci/`. let doHeadBranchCheckTitle = "PR Head Branch Check" let validBranches: [BranchType] = [ .feature, .refactor, .fix, .issue, .version, .ci, ] result.check(doHeadBranchCheckTitle) { if isHeadBranch(anyOf: validBranches) { return .good } else { fail("Please create a develop PR from either a feature, refactor, fix, issue or version branch.") return .rejected } } // Develop PR should be created into develop branch let doBaseBranchCheckTitle = "PR Base Branch Check" result.check(doBaseBranchCheckTitle) { if isBaseBranch(.develop) { return .good } else { fail("Please create a develop PR into develop branch.") return .rejected } } // Develop PR shouldn't contain any merge commits. let doNoMergeCommitsCheckTitle = "Merge Commits Excluded Check" result.check(doNoMergeCommitsCheckTitle) { if github.commits.allSatisfy({ $0.commit.parents == nil }) { return .good } else { fail("Develop PR should not contain any merge commits. Please consider rebasing if needed.") return .rejected } } // The volume of diff should not be over 1,000 lines. let doDiffAmountCheckTitle = "Diff Volume Check" result.check(doDiffAmountCheckTitle) { if diffLinesCount <= 1000 { return .good } else { warn("There's too much diff. Please consider splitting this PR.") return .acceptable } } if isHeadBranch(.ci) { // If the PR is created from the branch created by CI, do the CI auto PR modification check. checkCIAutoPRModification(into: &result) } else if isHeadBranch(.version) { // If the PR is created from a version branch, do the release modification check. checkReleaseModification(into: &result) } else { // Otherwise, do the develop modification check. checkDevelopmentModification(into: &result) } return result } func doReleasePRCheck() -> CheckResult { var result = CheckResult(title: "Release PR Check") // Release PR should be created from develop branch. let doHeadBranchCheckTitle = "PR Head Branch Check" result.check(doHeadBranchCheckTitle) { if isHeadBranch(.develop) { return .good } else { fail("Please create a release PR from develop branch.") return .rejected } } // Develop PR should be created into master branch let doBaseBranchCheckTitle = "PR Base Branch Check" result.check(doBaseBranchCheckTitle) { if isBaseBranch(.master) { return .good } else { fail("Please create a release PR into master branch.") return .rejected } } // Do the release modification check. checkReleaseModification(into: &result) return result } enum RoutineError: Error { case failedToFindCheckRoutine } func checkPR() throws -> CheckResult { if isDevelopPR { return doDevelopPRCheck() } else if isReleasePR { return doReleasePRCheck() } else { throw RoutineError.failedToFindCheckRoutine } } } // MARK: - Other convenient extensions private extension Git { var diffFiles: [File] { createdFiles + deletedFiles + modifiedFiles } } private extension XCodeSummary { convenience init(filePath: String, onlyShowSummaryInDiffFiles: Bool) { if onlyShowSummaryInDiffFiles { let diffFiles = Danger().git.diffFiles self.init(filePath: filePath) { [diffFiles] in guard let path = $0.file else { return false } return diffFiles.contains(path) } } else { self.init(filePath: filePath) } } } private extension String { func substring <S: StringProtocol> (of string: S, options: CompareOptions) -> String? { guard let range = range(of: string, options: options) else { return nil } return String(self[range]) } } private extension Process { static func runShell(_ commands: String...) -> String { let task = Process() task.launchPath = ProcessInfo().environment["SHELL"]! task.environment = ProcessInfo().environment task.arguments = ["-c"] + commands let pipe = Pipe() task.standardOutput = pipe task.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)! return output } } // MARK: - Check routine let danger = Danger() // If there's any modification in bootstrap.sh file, ask the reviewer to check if he needs to update Bitrise workflows. if danger.hasBrewfileBeenModified { markdown("- [ ] Check bitrise workflow") warn("There's modification in bootstrap.sh file. Please remember to check if needed to update Bitrise workflow.") } // If the head branch refers to a GitHub issue, comment it in the PR page. if let githubIssue = danger.githubIssue { message("Resolve #\(githubIssue)") } // SwiftLint format check. SwiftLint.lint(.modifiedAndCreatedFiles(directory: nil), inline: true) // Xcode summary warnings check. XCodeSummary(filePath: "result.json", onlyShowSummaryInDiffFiles: true).report() // Xcode test coverage check. Coverage.spmCoverage(minimumCoverage: 60) // PR routine check. do { let result = try danger.checkPR() markdown(result.markdownTitle) if !result.markdownMessage.isEmpty { markdown(result.markdownMessage) } if !result.markdownTodos.isEmpty { markdown(result.markdownTodos) } if result.warningsCount == 0 && result.errorsCount == 0 { message("Well Done :white_flower:") } } catch { fail("Failed to find out the correct check routine. Please check if your PR is created from or into a correct branch.") }
- 投稿日: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でもそれなりに開発できる…かも!?!?