- 投稿日: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-29T19:44:29+09:00
FirebaseでiOS版簡易SNSを作成する。(メール認証編)
はじめに
今回、Firebaseを使ってiOS版簡易SNSを作成する記事になっています。
この記事ではアカウント作成までの部分を行なっています。対象者
- Xcodeがインストールされている。
- GoogleAccountを所持している。
- Cocoapodsの環境が整っている。
やっておいてもらうこと
- Xcodeプロジェクトの作成 (当記事ではSNSAppという名前で進めていきます。)
- Firebaseプロジェクトの作成
- Cocoapodsのインストール・セットアップ
Firebaseの事前準備
FirebaseプロジェクトにiOSアプリを追加する。
ここから事前に作成していただいたFirebaseのプロジェクトを選択します。
以下の画面のiOSを選択します。
CocoaPodsからFirebaseをインストールする
- Terminalを使用して自分のプロジェクトまで潜ります。その後、
pod init
と打ってください。cd ~/Download/iPhone_Dev/SNSApp pod init
- Terminalに
open Podfile
と入力します。これを打つと適切なアプリケーション(今回はテキストエディタ)でPodfile
を開いてくれます。その後、以下のようにPodfileを編集していきます。# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'SNSApp' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for SNSApp pod 'Firebase/Core' pod 'Firebase/Firestore' pod 'Firebase/Auth' end# この3行を追加する。 pod 'Firebase/Core' pod 'Firebase/Firestore' pod 'Firebase/Auth'
⌘ + S
で保存をしてテキストエディタを終了します。- Podfileに書いたライブラリは
pod install
を行うことでインストールされます。Terminalにpod install
と打ってください。
SNSApp.xcworkspace
を開きます。FirebaseAuthenticationの有効化
- Web上のFirebaseコンソールから以下のように選択していきます。
Firebaseの初期設定 (Xcode編)
- iOSプロジェクトを追加した段階でやっておいてもらったFirebaseの初期設定をするコードを書きます。既に書いた方は大丈夫です。
AppDelegate.swiftimport UIKit import Firebase @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() // ここを追加します。 return true } }TimelineViewController.swiftをコーディング
Xcodeから
File
->New File
->Cocoa Touch Class
を選択し、TimelineViewController
というクラスを作ります。中身は以下のように編集しておきます。この
user
はアカウント登録成功後に前の画面(AccountViewController
)から値渡しで受け取る変数です。TimelineViewController.swiftimport UIKit import Firebase // 追加 class TimelineViewController: UIViewController { var user: User! // 追加 override func viewDidLoad() { super.viewDidLoad() } }AccountViewController.swiftをコーディング
Xcodeから
File
->New File
->Cocoa Touch Class
を選択し、AccountViewController
というクラスを作ります。UIKitの宣言・デリゲートメソッドの記述を行います。
import UIKit import Firebase // Firebaseをインポート class AccountViewController: UIViewController { @IBOutlet var emailTextField: UITextField! @IBOutlet var passwordTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self passwordTextField.delegate = self } // 登録ボタンを押したときに呼ぶメソッド。 @IBAction func registerAccount() { } } // デリゲートメソッドは可読性のためextensionで分けて記述します。 extension AccountViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } }いよいよ、Firebaseを使用してユーザーを作成していきます。
- プロパティとして、FirebaseAuthのクラスを宣言します。
AccountViewController.swiftimport UIKit import Firebase class AccountViewController: UIViewController { (省略) var auth: Auth // 追加 override func viewDidLoad() { super.viewDidLoad() auth = Auth.auth() // 追加 emailTextField.delegate = self passwordTextField.delegate = self } (省略) }次に、アカウント登録する処理を書いていきます。
-registerAccount
というメソッドを定義してユーザーがボタンを押した時にアカウント登録を行うような実装をします。アカウント作成のコード.swiftauth.createUser(withEmail: "メールアドレス", password: "パスワード") { (result, error) in // アカウント登録後に呼ばれる。 // error変数が nil -> 成功 // nilではない -> 失敗 // result変数 ... user情報などをプロパティとして格納している。 }実装したコードとしては、
AccountViewController.swiftimport UIKit import Firebase class AccountViewController: UIViewController { (省略) @IBAction func registerAccount() { let email = emailTextField.text! let password = passwordTextField.text! auth.createUser(withEmail: email, password: password) { (result, error) in if error == nil, let result = result { // errorが nil であり、resultがnilではない == user情報がきちんと取得されている。 self.performSegue(withIdentifier: "Timeline", sender: result.user) // 遷移先の画面でuser情報を渡している。 } } } }Main.storyboardを編集
UITextFieldを2つとUIButtonを1つずつ用意する。
ViewControllerのクラスを
AccountViewController
に変更する。
- 関連付けを行う。
新しくViewControllerを作成して、クラスを
TimelineViewController
に設定する。セグエを
AccountViewController
→TimelineViewController
に繋げて作成する。現在、このようにアカウントを作成するところまで進めました。
AccountViewController.swiftを編集
- 一度ログインしていたらアカウント作成画面を飛ばす。
- メール認証機能を実装する。
この2つを最後に実装していきます。
一度ログインしていたらアカウント作成画面を飛ばす。
AccountViewController.swiftimport UIKit import Firebase class AccountViewController: UIViewController { var auth: Auth! (省略) override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if auth.currentUser != nil { // もし既にユーザーにログインができていれば、タイムラインの画面に遷移する。 // このときに、ユーザーの情報を次の画面の変数に値渡ししておく。(直接取得することも可能。) performSegue(withIdentifier: "Timeline", sender: auth.currentUser!) } } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let nextViewController = segue.destination as! TimelineViewController let user = sender as! User nextViewController.user = user } @IBAction func registerAccount() { let email = emailTextField.text! let password = passwordTextField.text! auth.createUser(withEmail: email, password: password) { (result, error) in if error == nil, let result = result { self.performSegue(withIdentifier: "Timeline", sender: result.user) } } } }ポイントは、
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if auth.currentUser != nil { performSegue(withIdentifier: "Timeline", sender: auth.currentUser!) } }
viewDidAppearで 既にユーザーを所持している == ログインしたことがある ならば、
performSegue
で画面遷移を実行しています。senderにユーザー情報を渡していますが、これは最新の状態を
Auth.auth().currentUser
で取得できるのでこれの限りではないです。override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let nextViewController = segue.destination as! TimelineViewController let user = sender as! User nextViewController.user = user }
prepare
はセグエによる画面遷移が行われる際に呼ばれます。この実装で、既にログインを行なっていれば、画面遷移時が行われ、サインアップをする画面をスキップできます。
メール認証機能を実装する。
- ここでいうメール認証機能とは 入力したメールアドレス宛に確認メールがFirebaseから送られてきて、それに応じれば正しいアカウントと見なせる という意味でのメール認証機能です。
コード全体です。
import UIKit import Firebase class AccountViewController: UIViewController { var auth: Auth! @IBOutlet var emailTextField: UITextField! @IBOutlet var passwordTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() auth = Auth.auth() emailTextField.delegate = self passwordTextField.delegate = self } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if auth.currentUser != nil { auth.currentUser?.reload(completion: { error in if error == nil { if self.auth.currentUser?.isEmailVerified == true { self.performSegue(withIdentifier: "Timeline", sender: self.auth.currentUser!) } else if self.auth.currentUser?.isEmailVerified == false { let alert = UIAlertController(title: "確認用メールを送信しているので確認をお願いします。", message: "まだメール認証が完了していません。", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) } } }) } } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let nextViewController = segue.destination as! TimelineViewController let user = sender as! User nextViewController.user = user } @IBAction func registerAccount() { let email = emailTextField.text! let password = passwordTextField.text! auth.createUser(withEmail: email, password: password) { (result, error) in if error == nil, let result = result { result.user.sendEmailVerification(completion: { (error) in if error == nil { let alert = UIAlertController(title: "仮登録を行いました。", message: "入力したメールアドレス宛に確認メールを送信しました。", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) } }) } } } } // デリゲートメソッドは可読性のためextensionで分けて記述します。 extension AccountViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } }ポイントは、
auth.createUser(withEmail: email, password: password) { (result, error) in if error == nil, let result = result { result.user.sendEmailVerification(completion: { (error) in if error == nil { let alert = UIAlertController(title: "仮登録を行いました。", message: "入力したメールアドレス宛に確認メールを送信しました。", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) } }) } }ここの部分の
result.user.sendEmailVerification(compeltion: @escaping (Error?) -> ())で入力したメールアドレスに対して確認用メールを送信します。
コールバックで指定されている引数は確認用メールの送信に成功していれば
nil
で失敗していればError型
が入っています。もしエラーがnilであればメールボックスを確認してもらうようにユーザーに促すようにアラートを表示します。
次に、メールボックスから送信されたメールを見つけ、リンクを開きます。
その後、アプリをもう一度開くと、画面遷移が行われるはずです。
コードの説明に戻りますが、
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if auth.currentUser != nil { auth.currentUser?.reload(completion: { error in if error == nil { if self.auth.currentUser?.isEmailVerified == true { self.performSegue(withIdentifier: "Timeline", sender: self.auth.currentUser!) } else if self.auth.currentUser?.isEmailVerified == false { let alert = UIAlertController(title: "まだメール認証が完了していません。", message: "確認用メールを送信しているので確認をお願いします。", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) } } }) } }ここの部分で、ポイントは
self.auth.currentUser?.isEmailVerified
これで現在のユーザーがメール認証済みかのチェックが可能です。
また、メール認証を実際に行なったかをアプリ側で自動更新はしてくれないので、以下のメソッドを呼んでわざと更新処理を行います。
auth.currentUser?.reload(completion: @escaping (Error?) -> ())
- その後
if self.auth.currentUser?.isEmailVerified == true
のようにif文でチェックします。最後に
この記事では、
- FirebaseAuthを利用してアカウント登録を行う。
- ユーザーログイン済みの場合、勝手に画面遷移を行わせることでUXの向上
- 確認用メールを送信
この3つを実装しました。
次の記事では続編としてFirestoreを使用して、タイムライン画面を作成していきます。
- 投稿日:2019-08-29T18:38:44+09:00
Swift/スライダーを作ろう!(2)
*この記事ではSwift4.0を使っています。
*Swiftを勉強していく中で備忘録用に超絶初歩的なことを投稿しています。さあ、今回は前回に引き続き、作った静的なスライダーをアニメーションで
ゆっくりと動くスライダーを作成していきます。第1回はこちらから。
https://qiita.com/tarunn/items/a5de5179d6f2b827e3c7スライダーを動かしてみよう!!
hoge.swiftimport UIKit var slider = UISlider() slider.setValue(0.5,animated: true)生成した「slider」インスタンスに対して「setValue」メソッドで0.5の位置(中央)までアニメーションしながらスライダーを動かす。という内容です。
簡単ではありますが、以上です。
次回以降も自分のアウトプットように簡単な実装からどんどん投稿していきます!!
- 投稿日:2019-08-29T18:20:23+09:00
用 Swift 實作隔幾個字插入某個字串
以信用卡卡號來舉例
4242424242424242每隔四個數字插入一個空白,像是這樣:
4242 4242 4242 4242實作
原先有嘗試過用 map 或是 reduce 等等高階函式的方式實作,
但是在建置時間和型別推論的時間都太長,再來是必須要反覆取得記憶體空間,不是很有智慧(笑)。
於是就去找了一下有沒有比較「環保」的做法。接著就在 Stack Overflow 找到一個解法 (這裡),
這個解法簡單來說,是從 String 「內部」 來修改它的內容,這樣可以在不用額外索取記憶體空間的情形下達成。簡化這個解答到必要的程式碼之後如下:
extension StringProtocol where Self: RangeReplaceableCollection { mutating func insert(separator: Self, every count: Int) { for index in indices.reversed() where index != startIndex && distance(from: startIndex, to: index) % count == 0 { insert(contentsOf: separator, at: index) } } }詳細原理的解說可以參照 Swift Blog 的這篇文章
- Strings and Characters | Swift Blog 的
Accessing and Modifying a String
章節走訪每一個字元並以指定間隔插入指定字串
在這裡就用我自己的話說明看看。
因為 Swift 的 String 無法以單純的整數定位,所以必須要使用
String.Index
。
在這邊就是透過 Collection protocol 提供的 indices 來走訪每個位置。(RangeReplaceableCollection 有繼承 Collection protocol)
先反轉再走訪的原因是可以避免在插入字串的過程中,長度改變造成 index 不好計算的問題
for index in indices.reversed()觸發條件就是當前的 index 還沒抵達 startIndex :
index != startIndex以及與第一個字元的距離的餘數為 0
distance(from: startIndex, to: index) % count == 0符合條件之後,就可以用 RangeReplaceableCollection 提供的方法,來在指定位置插入指定字串
insert(contentsOf: separator, at: index)
RangeReplaceableCollection#insert(contentsOf:at:)
- 文件使用方法
var cardNumber = "4242424242424242" cardNumber.insert(separator: " ", every: 4) print(cardNumber) // output: 4242 4242 4242 4242當然也可以拿來插入複數字元:
var content = "鯛魚燒" content.insert(separator: " ? ", every: 1) print(content) // output: 鯛 ? 魚 ? 燒以上!
參考資料
- How add separator to string at every N characters in swift? | Stack Overflow
- Strings and Characters | Swift Blog
- Collection#indices
- RangeReplaceableCollection#insert(contentsOf: at:)
環境
- Xcode 10.3 (10G8)
- Swift 5
最後
如果內文需要更新、有錯誤或容易誤解的地方,歡迎發修改或是留言告訴我!
- 投稿日: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) }