- 投稿日:2019-08-29T23:52:05+09:00
Github Actions で Xcode プロジェクトをビルドしてみる
これは何
GitHub Actions で Xcode プロジェクトをビルドする方法のメモです。そもそも GitHub Actions って何よ?という方は下記サイトを御覧ください。
GitHub Actionsについて - GitHub ヘルプ
利用申請
GitHub Actions はまだベータ版であり、利用するには下記URLから申し込む必要があります。
https://github.com/features/actions
何日か待つと「使えるようにしたよ〜」といった内容のメールがGitHubから届きます。
とりあえずビルドしてみる
リポジトリのトップページに「Actions」タブが追加されているので「Set up Actions」をクリックしましょう。
スタートページが表示されます。ここでサンプルの workflow を動かすこともできますが、今回は右上の「Set up workflow youself」をクリックします。
workflow を編集する画面が表示されるので下記の yml を入力してください。
name: CI on: [push] # git push をトリガーとする jobs: build: runs-on: macos-latest steps: - uses: actions/checkout@v1 # ソースコードをcheckout - name: Select Xcode version # Xcode 10.3 を使う run: sudo xcode-select -s '/Applications/Xcode_10.3.app/Contents/Developer' - name: Show Xcode version run: xcodebuild -version - name: Build # シミュレーター向けにビルド run: xcodebuild -sdk iphonesimulator -configuration Debug buildせっかくなので上記内容を適当なブランチに push してプルリクエストを作成してみましょう。「Checks」タブで workflow が動いていることが確認できます。
これで Xcode プロジェクトをビルドできました!
この要領でテストを実行したり、IPAを書き出したりもできそうですね。
xcodebuild
については下記サイトで丁寧に解説されています。CocoaPods や Carthage を使う
GitHub Actions の仮想環境にはすでに CocoaPods や Carthage がインストールされているので
- name: Install CocoaPods run: pod installのように利用できます。
リンク
- 投稿日:2019-08-29T23:05:22+09:00
iOSアプリのhttp通信を許可する方法
目的
XcodeでiOSアプリを開発するとき、api通信を挟んでデータを取得する機会がよくあります。しかし、iOS9以降ではhttps通信しかデフォルトで許可されていません。
いざ実際にhttp通信を試みても、下記のようなエラーメッセージが表示され、通信がリジェクトされます。App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app’s Info.plist file.設定方法
info.plistの編集
1 まず最初にプロジェクト作成時に自動で作成されるinfo.plistを開きます。そして、上記画像のinformation Property Listの右部の+マークをクリックします。
2 次に+ボタンを押したら表示される欄の中から、App Transport Security Settingsを選択します。
3次に先ほど選択したApp Transport Security Settingsの左にある、▶︎を上記画像のように▼にクリックして下向きにしてください。その後App Transport Securityの右にある+を押し、Allow Arbitary Loadsを選択してください。
4最後に上記画像のように、Allow Arbitary LoadsのValue部分をデフォルトのNOから、YESに変更したら設定完了です。最後に
今回はHTTP通信を可能にする方法を紹介しましたが、iOS9移行で通信が暗号化されるHTTPS通信が推奨されており、セキュリティー面なども考慮するともちろんHTTPS通信を使用することが良いと思います。
そのため、できればapiを用意する場合にはhttpsに対応させることが勧められます。
- 投稿日:2019-08-29T20:06:05+09:00
MicroViewControllerのInjectableとInteractableプロトコルを使ってみたら良かった話
最近少し入力が多くて画面が多く複雑な画面を実装することになったので、iOSDC2018で発表されてたMicroViewControllerを思い出して、ViewController間のやりとりを入力と出力のプロトコルで縛る方式で行うようにした。
「MicroViewControllerのやり方良いよね」というと、正しく相手に通じないかもしれないので何が良いかというのを書いておこうと思います。別にButtonとかCellとかをViewControllerにしたいわけじゃない。単に入力と出力のプロトコルが良いというのが結論です。
入力
https://github.com/mercari/Mew/blob/master/Sources/Mew/Protocols/Injectable.swift
public protocol Injectable { associatedtype Input func input(_ input: Input) }具体的には
ViewController A
からViewController B
に情報を渡すような際にも使える。class ViewControllerB: UIViewController { struct Input { let userId: String } ... } extension ViewControllerB: Injectable { func input(_ input: Input) { // self.input = input として使い回すときもあるけどしなかったりもする if isViewLoaded { label.text = input.userId } } }
func input(_ input: Input)
が単なるメソッドなのが良い。これをvar input: Input
にしてしまうと入力をget
できてしまう。get
して誰かが困るかっつうと困らない。困らないんだけど入力をget
する意味がないのでできない方が良い。出力
https://github.com/mercari/Mew/blob/master/Sources/Mew/Protocols/Interactable.swift
public protocol Interactable { associatedtype Output func output(_ handler: ((Output) -> Void)?) }具体的には、入力によって出力が取得できる場合に使える
class ViewControllerB: UIViewController { struct Input { let userId: String } typealias Output = User private var outputHandler: ((Output) -> Void)? ... func 何かやる() { 取得しに行くやつ(input.userId).fetch { [weak self] user in self?.outputHandler?(user) } } } extension ViewControllerB: Interactable { func output(_ handler: ((Output) -> Void)?) { outputHandler = handler } }これも入力と同じで出力がメソッドなのがいい。
その他の良い点
その他の良い点も書いておく。
例えば
ViewController A
、ViewController B
、ViewController C
と3つあったり、コンテナであったりそういう場面でも使えるし、そしてViewControllerに限定せずViewでも使えるのがいい。これによって複数人でも入力と出力だけやり方を揃えてぶらさず、そこを重点的にチェックすれば良くなるはず。
また、入力と出力さえ公開されていればよくて他はすべて
private
でいい。人のコードをチェックする際に考えることが減りそうなのもいい。
- 投稿日:2019-08-29T13:17:54+09:00
AVFoundationで動画に音声を追加する
[Swift]AVFoundationで動画に音声を追加する
関わっているアプリ開発のプロジェクトで無音動画に音声を結合する必要がありました。AVFoundationまわりの情報が少なく特にSwiftのサンプルコードなどがあまりないなと思い備忘録をかねてまとめます。
サンプルコードでは無音声動画と音声の結合をしていますが、音声ありでも処理は変わりません。その場合は差し替えの処理も可能です。前提としてAVAsset/AVMutableCompositionをある程度理解しておく必要はあります。
AVFoundation Programming Guide以下サンプルコードです。(必要箇所だけ抜き出しているのでコンパイルが通るかは?)
MovieMakerViewController.swiftimport UIKit import Foundation import AVFoundation import AVKit class MovieMakerViewController: UIViewController { let dispatchQueue = DispatchQueue(label: "queue") let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first let videoUrl = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("videotmp.mp4") let soundUrl = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("soundtmp.caf") let movieUrl = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("sample.mp4")//output override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let composition = AVMutableComposition.init() //-------------------- //source video let asset = AVURLAsset.init(url: self.videoUrl) let range = CMTimeRangeMake(start: CMTime.zero, duration: asset.duration) let videoTrack = asset.tracks(withMediaType: .video).first // let audioTrack = asset.tracks(withMediaType: .audio).first//無音性動画の場合エラーになる let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) // let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) try? compositionVideoTrack?.insertTimeRange(range, of: videoTrack!, at: CMTime.zero) // try? compositionAudioTrack?.insertTimeRange(range, of: audioTrack!, at: CMTime.zero) let instruction = AVMutableVideoCompositionInstruction.init() instruction.timeRange = range let layerInstruction = AVMutableVideoCompositionLayerInstruction.init(assetTrack: compositionVideoTrack!) //-------------------- //source sound let soundAsset = AVURLAsset.init(url: self.soundUrl) let soundTrack = soundAsset.tracks(withMediaType: .audio).first let compositionSoundTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) try? compositionSoundTrack?.insertTimeRange(range, of: soundTrack!, at: CMTime.zero) //-------------------- //composite let transform = videoTrack!.preferredTransform layerInstruction.setTransform(transform, at: CMTime.zero) instruction.layerInstructions = [layerInstruction] let videoComposition = AVMutableVideoComposition.init() videoComposition.renderSize = videoTrack!.naturalSize videoComposition.instructions = [instruction] videoComposition.frameDuration = CMTime.init(value: 1, timescale: 60) if FileManager.default.fileExists(atPath: self.movieUrl.path) { try? FileManager.default.removeItem(at: self.movieUrl) } let session = AVAssetExportSession.init(asset: composition, presetName: AVAssetExportPresetHighestQuality) session?.outputURL = self.movieUrl session?.outputFileType = .mp4 session?.videoComposition = videoComposition session?.exportAsynchronously(completionHandler: { if session?.status == AVAssetExportSession.Status.completed { DispatchQueue.global(qos: .default).async { DispatchQueue.main.async { print("finished") } } } }) DispatchQueue.global(qos: .default).async { DispatchQueue.main.async { Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer: Timer) in print("\(Int((session?.progress ?? 0) * 100.0))%") //完了したらtimer.invalidate()を実行 }) } } } }もともとの無音声動画と音声は同じdurationという前提なので再生時間はそのまま抜き出しています。
AVの時間情報
MovieMakerViewController.swiftlet asset = AVURLAsset.init(url: self.videoUrl) let range = CMTimeRangeMake(start: CMTime.zero, duration: asset.duration)AVAssetで動画でも音声でも時間情報を取り出すことができます。CMTimeを操作すること自由に再生時間を制御できますが、動画と音声で考え方が違います。CMTimeは動画であればフレームレート、音声であればサンプルレートを時間基準を算定します。(このあたりは機会があれば)
またこのサンプルではViewがLoadされてからメインキュー内で動画が作成されていますが、本来はメインとは別で行われるべきです。実際には自分でキューを作ってその中で処理をしていますが、サンプルコードでは省略しています。以下がその名残です。
またその場合、ユーザーに処理状況をお知らせする必要があります。UIまわりはメインキューでないと反映されないのでDispatchQueue.main.async内で処理状況を知らせるUI要素をするとよいです。進行状況はsession.progressで取得できます。MovieMakerViewController.swiftlet dispatchQueue = DispatchQueue(label: "queue") (略) DispatchQueue.global(qos: .default).async { DispatchQueue.main.async { Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer: Timer) in print("\(Int((session?.progress ?? 0) * 100.0))%") //完了したらtimer.invalidate()を実行 }) } }手順としては
- AVAssetで再生時間に関する情報を抜き出す
- AVMutableCompositionにVideoTrackとAudioTrackを追加する
- AVAssetExportSessionに合成(composition)のための情報を渡してエクスポートする
これを応用すれば音声を差し替えたり、動画同士を連結することもできるようになります。それはまたの機会に。
- 投稿日:2019-08-29T09:53:36+09:00
XCTestで「待つ」ためのサンプルコード集
任意のタイミングまで待つ
任意のタイミングで
expectation.fulfill()
する。let expectation = XCTestExpectation(description: "view hidden") DispatchQueue.main.asyncAfter(deadline: .now() + 1) { XCTAssertEqual(view.isHidden, true) expectation.fulfill() } XCTWaiter().wait(for: [expectation], timeout: 10)NSPredicateを使って待つ
let predicate = NSPredicate(format: "isHidden == true") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: view) let result = XCTWaiter().wait(for: [expectation], timeout: 10) XCTAssertEqual(result, .completed)KVOを使って待つ
let expectation = XCTKVOExpectation(keyPath: "isHidden", object: view, expectedValue: true) let result = XCTWaiter().wait(for: [expectation], timeout: 10) XCTAssertEqual(result, .completed)Notificationを待つ
let expectation = XCTNSNotificationExpectation(name: notificationName) let result = XCTWaiter().wait(for: [expectation], timeout: 10) XCTAssertEqual(result, .completed)環境
Swift 5
Xcode 10.2参考
- 投稿日:2019-08-29T09:53:36+09:00
XCTestで非同期処理を待つためのサンプルコード集
任意のタイミングまで待つ
任意のタイミングで
expectation.fulfill()
する。let expectation = XCTestExpectation(description: "view hidden") DispatchQueue.main.asyncAfter(deadline: .now() + 1) { XCTAssertEqual(view.isHidden, true) expectation.fulfill() } XCTWaiter().wait(for: [expectation], timeout: 10)NSPredicateを使って待つ
let predicate = NSPredicate(format: "isHidden == true") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: view) let result = XCTWaiter().wait(for: [expectation], timeout: 10) XCTAssertEqual(result, .completed)KVOを使って待つ
let expectation = XCTKVOExpectation(keyPath: "isHidden", object: view, expectedValue: true) let result = XCTWaiter().wait(for: [expectation], timeout: 10) XCTAssertEqual(result, .completed)Notificationを待つ
let expectation = XCTNSNotificationExpectation(name: notificationName) let result = XCTWaiter().wait(for: [expectation], timeout: 10) XCTAssertEqual(result, .completed)環境
Swift 5
Xcode 10.2参考
- 投稿日:2019-08-29T08:23:12+09:00
iPhone で FeliCa を読み取るライブラリを作りました
この記事は potatotips #64 で発表した内容をテキスト化したものです。
今から発表する資料ですー
— たなたつ (@tanakasan2525) August 27, 2019
iPhoneでFeliCaを読み取ってみたhttps://t.co/kX4j3SZ6QL
作ったライブラリhttps://t.co/gnz0eoNqcf#potatotipsTL;DR
サクッと FeliCa の IC カードを読み取れるライブラリを作ったよ。
今のところ下記のカードに対応してるよ。
- Suica, Pasmo, Kitaca, ICOCA, TOICA、manaca、PiTaPa、nimoca、SUGOCA、はやかけん
- nanaco、Edy、WAON
- カスタムタグ
https://github.com/tattn/NFCReader
iOS 13 から FeliCa の読み書きができるように
WWDC 19 で CoreNFC で FeliCa が読み書きできるようになったことが発表されました。
FeliCa は Suica、Pasmo などの交通系 IC や nanaco、WAON などの電子マネーが採用している非接触 IC カードの技術方式です。https://developer.apple.com/videos/play/wwdc2019/715/
Suica を例に読み取り方を紹介
例として Suica の乗降履歴の読み取り手順を紹介します。
CoreNFC で FeliCa を読み込むときのフロー
https://www.sony.co.jp/Products/felica/business/tech-support/index.html
https://developer.apple.com/documentation/corenfcプロジェクト設定
Info.plist に Privacy 設定と System Code の追加が必要です。
System Code はシステムごとに割り当てられた 2 バイトのコードです。 plist に追加していないカードは読み取ることができません。
Suica の場合は 0003 になります。また、Capabilities に Near Field Communication Tag Reading を追加する必要があります。
Session の作成 & ポーリングの開始
let session = NFCTagReaderSession( pollingOption: NFCTagReaderSession.PollingOption.iso18092, delegate: self ) session.alertMessage = "iPhoneをSuicaに近づけてください" session.begin()FeliCa を読み込む時は ISO18092 を指定します。これは PollingOption のドキュメントコメントにも記載されています。
alertMessage
を指定すると、読み込みが始まったタイミングで表示される View にそのテキストが表示されます。
begin
を呼び出すと、タグの読み取り (ポーリング) が開始されます。タグとの接続
public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { guard case .feliCa(let tag) = tags.first else { return } session.connect(to: tag) { error in guard error == nil else { return } // Tagの読み込み (次のセクションに続く) } }タグが検出されると、delegate (
NFCTagReaderSessionDelegate
) が呼ばれます。
タグは同時に複数検出される場合がありますが、今回はその時のハンドリングは省略します。
NFCFeliCaTag
が取得できたら、connect
メソッドを呼び出して、タグに対してコマンドを送れるようにします。Suica のブロックデータの読み込み
let serviceCodeList = [Data([0x0f, 0x09])] // サービス(データ)を特定するコード let blockList = (0..<UInt8(10)).map { Data([0x80, $0]) } // データの取得方法/位置を決める tag.requestService(nodeCodeList: serviceCodeList) { nodes, error in guard error == nil, nodes.first == Data([0xff, 0xff]) else { return } tag.readWithoutEncryption( serviceCodeList: serviceCodeList, blockList: blockList) { status1, status2, dataList, error in guard error == nil, status1 == 0, status2 == 0 else { return } // dataの読み込み (次のセクションに続く) } }まずは、
requestService
を呼び出して、そのタグに読み取ろうとしているデータ (サービス) があるかどうかを確認します。
サービスが存在しない場合は 0xFF, 0xFF が返ります。 (FeliCa の仕様に基づく)サービスが存在した場合は、
readWithoutEncryption
を呼び出して、データの取得をします。 (今回読み取るデータが認証不要なサービスのため、readWithoutEncryption
になります)FeliCa の仕様で、ステータスフラグがともに 0 のときのみ、正しいデータが取得可能なため、念の為チェックしておくと良さそうです。
Suica データのデコード
for data in dataList { let year = data[4] >> 1 let month = UInt16(bytes: data[4...5]) >> 5 & 0b1111 let day = data[5] & 0b11111 print("利用日: \(year)/\(month)/\(day)") // 19/8/27 let entrance = UInt16(bytes: data[6...7]) let exit = UInt16(bytes: data[8...9]) print("入場駅: \(entrance), 出場駅: \(exit)") let balance = UInt16(bytes: data[10...11].reversed()) print("残高: ", balance) }下記のページを参考にバイナリデータから必要なデータを読み取ります。
https://www.wdic.org/w/RAIL/サイバネ規格%20(ICカード)(
year
が 2000 年基準なのが面白いです)Swift でバイナリデータを数値型に変換するのは少し手間なので上記では以下のようなエクステンションを利用しています。
extension FixedWidthInteger { init(bytes: UInt8...) { self.init(bytes: bytes) } init<T: DataProtocol>(bytes: T) { let count = bytes.count - 1 self = bytes.enumerated().reduce(into: 0) { (result, item) in result += Self(item.element) << (8 * (count - item.offset)) } } }↓のような感じで使えるので便利です。
XCTAssertEqual(UInt16(bytes: 0x35, 0x0B), 13579) XCTAssertEqual(Int(bytes: 0x07, 0x5B, 0xCD, 0x15), 123456789)便利なライブラリを作りました
前述のように読み取りには結構実装が必要で、面倒です。(エラーハンドリングなどを含めるとより手間)
そこで、サクッと使えるライブラリにしてみました。https://github.com/tattn/NFCReader
let reader = Reader<Suica>() reader.read(didBecomeActive: { _ in print("読み込み開始") }, didDetect: { reader, result in switch result { case .success(let suica): let balance = suica.boardingHistories.first?.balance ?? 0 reader.setMessage(balance) case .failure(let error): reader.setMessage("読み込みに失敗しました") } })このような感じで
Reader
の型パラメータに読み取りたい NFC タグを指定するだけでstruct
にマッピングされたデータを取得できます。Suica の他にも以下のようなタグを読み取れます。
ぜひ使ってみてください。
NFC タグの追加プルリクエストなども大募集中です。まとめ・所感
CoreNFC を使って FeliCa (Suica) を読み込む方法と作ったライブラリを紹介しました。
Suica の残高領域は 2 bytes しか用意されてないので 入金できる上限を簡単には増やせなさそうだなという発見もありました。
FeliCa の仕様はソニーが日本語で 丁寧に書いているのでとてもわかりやすかったです。
一方 Apple のドキュメントには現時点では全然情報がなく、FeliCa の仕様を知っている人でないと読めない感じでした。参考文献
- https://developer.apple.com/videos/play/wwdc2019/715
- https://developer.apple.com/documentation/corenfc
- https://www.sony.co.jp/Products/felica/business/tech-support/index.html
- https://www.wdic.org/w/RAIL/サイバネ規格%20(ICカード)
- http://www014.upp.so-net.ne.jp/SFCardFan/
- https://ja.osdn.net/projects/felicalib/wiki/FrontPage
- 投稿日:2019-08-29T01:16:00+09:00
Swift ナビゲーションバーにUITextVIewの文字数をリアルタイムにカウント
完成形
ナビゲーションバーのタイトル下にTextViewに入力された文字数をリアルタイムにカウント
実装の流れ
1. UITextViewDelegateを継承
ViewController: UIViewController, UITextViewDelegate {2. 文字数をカウントしたいUITextViewにデリゲート設定
MemoTextView.delegate = self3. ナビゲーションバーのタイトル・カウントの作成
イメージは下記のような形
青い四角:全体のUIView
黄色の四角:タイトルラベル
緑の四角:カウントラベル//青い四角 UIView let titleView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) //黄色の四角 タイトルラベル let titleLabel = UILabel() titleLabel.text = "メモ" titleLabel.font = UIFont.boldSystemFont(ofSize: 17) titleLabel.frame = CGRect(x: 30, y: 0, width: 50, height: 20) //緑の四角 カウントラベル countLabel.text = "0" countLabel.font = UIFont.systemFont(ofSize: 14) countLabel.frame = CGRect(x: 40, y: 20, width: 50, height: 20) //UIViewに追加 titleView.addSubview(countLabel) titleView.addSubview(titleLabel) //ナビゲーションに青い四角のUIViewを追加 navigationItem.titleView = titleView4. TextViewの文字数をカウントラベルに挿入
デリゲートメソッドであるtextViewDidChangeを用意し、下記のように文字列にキャストして、カウントラベルに値を入れます。
func textViewDidChange(_ textView: UITextView) { let MemoCount = MemoTextView.text.count countLabel.text = String(MemoCount) }