20190829のSwiftに関する記事は10件です。

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の編集

スクリーンショット 2019-08-29 22.43.59.png
1 まず最初にプロジェクト作成時に自動で作成されるinfo.plistを開きます。そして、上記画像のinformation Property Listの右部の+マークをクリックします。
スクリーンショット 2019-08-29 22.50.15.png
2 次に+ボタンを押したら表示される欄の中から、App Transport Security Settingsを選択します。
スクリーンショット 2019-08-29 22.54.00.png
3次に先ほど選択したApp Transport Security Settingsの左にある、▶︎を上記画像のように▼にクリックして下向きにしてください。その後App Transport Securityの右にある+を押し、Allow Arbitary Loadsを選択してください。
スクリーンショット 2019-08-29 22.58.45.png
4最後に上記画像のように、Allow Arbitary LoadsのValue部分をデフォルトのNOから、YESに変更したら設定完了です。

最後に

今回はHTTP通信を可能にする方法を紹介しましたが、iOS9移行で通信が暗号化されるHTTPS通信が推奨されており、セキュリティー面なども考慮するともちろんHTTPS通信を使用することが良いと思います。
そのため、できればapiを用意する場合にはhttpsに対応させることが勧められます。

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

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 AViewController BViewController Cと3つあったり、コンテナであったりそういう場面でも使えるし、そしてViewControllerに限定せずViewでも使えるのがいい。

これによって複数人でも入力と出力だけやり方を揃えてぶらさず、そこを重点的にチェックすれば良くなるはず。

また、入力と出力さえ公開されていればよくて他はすべてprivateでいい。人のコードをチェックする際に考えることが減りそうなのもいい。

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

FirebaseでiOS版簡易SNSを作成する。(メール認証編)

はじめに

今回、Firebaseを使ってiOS版簡易SNSを作成する記事になっています。
この記事ではアカウント作成までの部分を行なっています。

対象者

  • Xcodeがインストールされている。
  • GoogleAccountを所持している。
  • Cocoapodsの環境が整っている。

やっておいてもらうこと

  • Xcodeプロジェクトの作成 (当記事ではSNSAppという名前で進めていきます。)
  • Firebaseプロジェクトの作成
  • Cocoapodsのインストール・セットアップ

Firebaseの事前準備

FirebaseプロジェクトにiOSアプリを追加する。

  • ここから事前に作成していただいたFirebaseのプロジェクトを選択します。

  • 以下の画面のiOSを選択します。

スクリーンショット 2019-08-29 11.46.22.png

  • 手順に沿って進めていきます。注意としてBundle IdentifierはXcodeから以下の画面で確認します。 スクリーンショット 2019-08-29 11.49.20.png

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と打ってください。

スクリーンショット 2019-08-29 11.51.58.png

  • SNSApp.xcworkspaceを開きます。

スクリーンショット 2019-08-29 11.57.38.png

FirebaseAuthenticationの有効化

  • Web上のFirebaseコンソールから以下のように選択していきます。

スクリーンショット 2019-08-29 12.16.30.png

スクリーンショット 2019-08-29 12.21.57.png

スクリーンショット 2019-08-29 12.17.15.png

スクリーンショット 2019-08-29 12.17.27.png


Firebaseの初期設定 (Xcode編)

  • iOSプロジェクトを追加した段階でやっておいてもらったFirebaseの初期設定をするコードを書きます。既に書いた方は大丈夫です。
AppDelegate.swift
import 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.swift
import 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.swift
import UIKit
import Firebase

class AccountViewController: UIViewController {

    (省略)
    var auth: Auth // 追加

    override func viewDidLoad() {
        super.viewDidLoad()
        auth = Auth.auth() // 追加
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }

    (省略)

}

次に、アカウント登録する処理を書いていきます。
- registerAccountというメソッドを定義してユーザーがボタンを押した時にアカウント登録を行うような実装をします。

アカウント作成のコード.swift
auth.createUser(withEmail: "メールアドレス", password: "パスワード") { (result, error) in
    // アカウント登録後に呼ばれる。
    // error変数が nil -> 成功
    //            nilではない -> 失敗
    // result変数 ... user情報などをプロパティとして格納している。 
}

実装したコードとしては、

AccountViewController.swift
import 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に変更する。

スクリーンショット 2019-08-29 12.57.18.png

  • 関連付けを行う。

スクリーンショット 2019-08-29 12.57.33.png

  • 新しくViewControllerを作成して、クラスをTimelineViewControllerに設定する。

  • セグエをAccountViewControllerTimelineViewControllerに繋げて作成する。

  • セグエのidentifierをTimelineに設定する。
    スクリーンショット 2019-08-29 13.01.20.png

現在、このようにアカウントを作成するところまで進めました。

demo2

AccountViewController.swiftを編集

  • 一度ログインしていたらアカウント作成画面を飛ばす。
  • メール認証機能を実装する。

この2つを最後に実装していきます。


一度ログインしていたらアカウント作成画面を飛ばす。

AccountViewController.swift
import 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はセグエによる画面遷移が行われる際に呼ばれます。

  • この実装で、既にログインを行なっていれば、画面遷移時が行われ、サインアップをする画面をスキップできます。

demo1


メール認証機能を実装する。

  • ここでいうメール認証機能とは 入力したメールアドレス宛に確認メールが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であればメールボックスを確認してもらうようにユーザーに促すようにアラートを表示します。

demo3

次に、メールボックスから送信されたメールを見つけ、リンクを開きます。

demo4

その後、アプリをもう一度開くと、画面遷移が行われるはずです。

demo5

コードの説明に戻りますが、

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を使用して、タイムライン画面を作成していきます。

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

Swift/スライダーを作ろう!(2)

*この記事ではSwift4.0を使っています。
*Swiftを勉強していく中で備忘録用に超絶初歩的なことを投稿しています。

さあ、今回は前回に引き続き、作った静的なスライダーをアニメーションで
ゆっくりと動くスライダーを作成していきます。

第1回はこちらから。
https://qiita.com/tarunn/items/a5de5179d6f2b827e3c7

スライダーを動かしてみよう!!

hoge.swift
import UIKit

var slider = UISlider()

slider.setValue(0.5,animated: true)

生成した「slider」インスタンスに対して「setValue」メソッドで0.5の位置(中央)までアニメーションしながらスライダーを動かす。という内容です。

簡単ではありますが、以上です。
次回以降も自分のアウトプットように簡単な実装からどんどん投稿していきます!!

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

用 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 的這篇文章

走訪每一個字元並以指定間隔插入指定字串

在這裡就用我自己的話說明看看。

因為 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: 鯛 ? 魚 ? 燒

以上!

參考資料

環境

  • Xcode 10.3 (10G8)
  • Swift 5

最後

如果內文需要更新、有錯誤或容易誤解的地方,歡迎發修改或是留言告訴我!

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

AVFoundationで動画に音声を追加する

[Swift]AVFoundationで動画に音声を追加する

関わっているアプリ開発のプロジェクトで無音動画に音声を結合する必要がありました。AVFoundationまわりの情報が少なく特にSwiftのサンプルコードなどがあまりないなと思い備忘録をかねてまとめます。
サンプルコードでは無音声動画と音声の結合をしていますが、音声ありでも処理は変わりません。その場合は差し替えの処理も可能です。

前提としてAVAsset/AVMutableCompositionをある程度理解しておく必要はあります。
AVFoundation Programming Guide

以下サンプルコードです。(必要箇所だけ抜き出しているのでコンパイルが通るかは?)

MovieMakerViewController.swift

import 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.swift
let 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.swift
    let 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)のための情報を渡してエクスポートする

これを応用すれば音声を差し替えたり、動画同士を連結することもできるようになります。それはまたの機会に。

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

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

参考

http://masilotti.com/xctest-waiting/

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

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

参考

http://masilotti.com/xctest-waiting/

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

iPhone で FeliCa を読み取るライブラリを作りました

この記事は potatotips #64 で発表した内容をテキスト化したものです。

TL;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 を読み込むときのフロー

image.png

https://www.sony.co.jp/Products/felica/business/tech-support/index.html
https://developer.apple.com/documentation/corenfc

プロジェクト設定

Info.plist に Privacy 設定と System Code の追加が必要です。

image.png

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 の他にも以下のようなタグを読み取れます。

image.png

ぜひ使ってみてください。
NFC タグの追加プルリクエストなども大募集中です。

まとめ・所感

CoreNFC を使って FeliCa (Suica) を読み込む方法と作ったライブラリを紹介しました。

Suica の残高領域は 2 bytes しか用意されてないので
入金できる上限を簡単には増やせなさそうだなという発見もありました。

FeliCa の仕様はソニーが日本語で
丁寧に書いているのでとてもわかりやすかったです。
一方 Apple のドキュメントには現時点では全然情報がなく、FeliCa の仕様を知っている人でないと読めない感じでした。

参考文献

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

Swift ナビゲーションバーにUITextVIewの文字数をリアルタイムにカウント

完成形

ナビゲーションバーのタイトル下にTextViewに入力された文字数をリアルタイムにカウント

count.gif

実装の流れ

1. UITextViewDelegateを継承

ViewController: UIViewController, UITextViewDelegate {

2. 文字数をカウントしたいUITextViewにデリゲート設定

MemoTextView.delegate = self

3. ナビゲーションバーのタイトル・カウントの作成

イメージは下記のような形
青い四角:全体のUIView
黄色の四角:タイトルラベル
緑の四角:カウントラベル

qiita_material_count1.png

        //青い四角 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 = titleView

4. TextViewの文字数をカウントラベルに挿入

デリゲートメソッドであるtextViewDidChangeを用意し、下記のように文字列にキャストして、カウントラベルに値を入れます。

    func textViewDidChange(_ textView: UITextView) {
        let MemoCount = MemoTextView.text.count
        countLabel.text = String(MemoCount)
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む