20200708のSwiftに関する記事は7件です。

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!
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 の順番に戻るようになる。

こんな感じで好きなようにカスタマイズ出来る、と思う。多分。

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

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() を忘れないように

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

[個人アプリ開発記]スクショ・画面録画がバレるトークアプリ

はじめに

こんにちは。先日2つ目となるアプリをリリースしましたので、その機能や流れについて書こうと思います。一つ目についてはこちら。ちなみにリジェクトは4回されました。匿名投稿ができるアプリが審査を通るために持たせなければならない機能についても書いています。

アプリの紹介

機能としてはLINEのようなトークアプリなのですが、友達追加やアカウント登録という概念をなくし、ルーム名とパスワードさえお互い分かっていれば誰とでも話すことができるようにしました。
IMG_0344.PNG IMG_0347.PNG
トーク画面にはスクショ・画面録画を行うとメッセージが自動送信される仕組みになっています。
IMG_0351.PNG IMG_0390.PNG
審査を通るために追加した処理ですが、不適切な内容を投稿するユーザーをブロックすることや通報することもできます。

Simulator Screen Shot - iPhone SE (2nd generation) - 2020-07-08 at 02.40.48.png Simulator Screen Shot - iPhone SE (2nd generation) - 2020-07-08 at 02.30.14.png
報告された内容は開発者の元に届き、不適切な利用が確認された場合はユーザーの利用を停止させることもできます。
スクリーンショット 2020-07-08 2.32.10.jpg

アプリを作ろうと思った理由

インスタでストーリをスクショすると相手にバレる?みたいな噂を聞き、そもそもスクショを検出することは可能なのか?と思ったことがきっかけです。この記事で可能であることを知り、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には"ルーム名"+"パスワード"をドキュメント名に持つドキュメントが作成されます。
スクリーンショット 2020-07-08 8.50.51.jpg
そして、メッセージが送信されると"messages"コレクションにデータがセットされます。
ユーザーが通報を行った際には各ルームドキュメントに"report"コレクションが作成されると同時に、同じ内容が"Reports"ドキュメントにも追加されます。
スクリーンショット 2020-07-08 8.51.17.jpg
利用規約に同意することで匿名登録が行われます。アカウントはuidというユーザーが一意に定まる文字列を持つため、ブロック機能の際にはこのuidを利用しています。
スクリーンショット 2020-07-03 8.04.47.jpg

終わりに

画像・動画を送れる機能やブデザインの向上などまだまだ実装したい内容はたくさんあります。これからも開発を継続していきます。
teratailで回答・コメントしてくださった方を始め様々な方のおかげでアプリを完成させることができました。ありがとうございました。

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

練習のために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.swift
import 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)
    }   
}

次にやること

  • 上記コードの内容を理解する
  • 次のアプリを作ってみる

がんばります。

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

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 で動作確認すると githubnil になって落ちちゃいます。ちなみにこのエラーもメッセージがわかりにくい(どのプロパティーが nil なのかが教えられていない)ため、私はこれの原因を突き止めるのにかなり時間かかりました。

これらの悪いところで更に次に悪かったところを引き出します:githubnil になっちゃうのに、割と多くのデータ取得が 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.swiftimport の行の最後に、コメントで // 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.")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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で渡しているパスが正しいかどうか確認してください)。

output.gif

コード補完できてる〜!!!!

ちなみに、先程指定した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でもそれなりに開発できる…かも!?!?

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