20190405のSwiftに関する記事は6件です。

[iOS]UIBezierPath, CAShapeLayer, UIColor.init(patternImage:) でUITabBarの形、色、外観を変える

Swift4.2とSwift5 で動作確認済

実現したかったUITabBar
IMG_1664的副本.PNG

https://itunes.apple.com/sg/app/pripara/id1039257927?mt=8
(香港のAppStoreじゃないとダウンロードできません)

実現できたUITabBar(アイコンは今回の記事と関係ないです)
屏幕快照 2019-04-05 23.15.49.png

https://github.com/Satoru-PriChan/ChangeUITabBarAppearance

方法

⑴UIBezierPathを使って、どんなグラフィックを描きたいかを定義します。今回は、スタート地点のY座標をマイナスとし、縦に少し拡張しているだけです。

MyTabBarController.swift
    func createLengthExpandShape() -> CGPath {
        let path = UIBezierPath()

        //Starting point (left top) then draw lines until the it returns to the starting point with close().
        let startX: CGFloat = 0
        let startY: CGFloat = -37

        path.move(to: CGPoint.init(x: startX, y: startY))
        path.addLine(to: CGPoint.init(x: self.tabBar.frame.width, y: startY))
        path.addLine(to: CGPoint.init(x: self.tabBar.frame.width, y: self.tabBar.frame.height))
        path.addLine(to: CGPoint.init(x: startX, y: self.tabBar.frame.height))
        path.close()

        return path.cgPath
    }

参考 https://medium.com/@philipp307/draw-a-custom-ios-tabbar-shape-27d298a7f4fa

(2)CAShapeLayerに(1)のパスを追加し、レイヤーの形を変えます。

MyTabBarController.swift
   override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        //中略 ここでUITabBarControllerの他の設定を色々することになると思います

        //Change tabbar shape
        self.changeTabBarShape()

    }

    func changeTabBarShape() {
        let shapeLayer = CAShapeLayer()

        //Get CGPath
        shapeLayer.path = self.createLengthExpandShape()

        //Set image
        shapeLayer.fillColor = UIColor.init(patternImage: UIImage.init(named: "MyImage.png")!).cgColor

        //Set layer
        self.tabBar.layer.insertSublayer(shapeLayer, at: 0)
    }

(3)UIColor.init(patternImage:) でセットしたい画像を用意し、Xcodeにドラッグ&ドロップで追加してください。その際Copy Items If Neededにチェックを入れることをお勧めします。

今回作った画像
lace-1174578_640的副本8.png

画像の準備が一番わかりにくい点です。
画像はランタイム時上下逆にして使われるので、画像編集ソフトなどで画像の内容をあらかじめ上下逆にしておいてください。また画像が貼られ始めるのは「拡張前の」レイヤーの左上角からという点に注意してください。

今回、画像下のピンク色で雑に塗りつぶしただけの部分がUITabBarの本来の左上から貼られます。

その上のCAShapeLayerで拡張した部分には、画像上のレースのひらひら部分が貼られます。

ひらひら部分と、塗りつぶしただけの部分の間をpngで透過しておくと、UITabBarが画像に合わせて複雑に形を変える(ように見える)ため、デザイン性が増します。

CAShapeLayerの拡張法をもっと複雑に変えたい場合は、画像の中身も調整する必要があると思います。今回は縦長にしただけなので、簡単に済んでいます。

全プロジェクト
https://github.com/Satoru-PriChan/ChangeUITabBarAppearance

気に入ったら、GitHubの方に星もらえると助かります。

もっとエクセレントなやり方があると思うので、誰か教えてください・・・

以上

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

iOSの照度センサーを使って画面を暗くしたり明るくしたりする

iOSの照度センサーを使って、なにかできないか模索していたときのメモ

iPhoneのカメラ部分を触ると画面を暗くできる

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // 近接監視が有効(true)か無効かを示すブール値
        UIDevice.current.isProximityMonitoringEnabled = true
        //照度センサーを監視
        NotificationCenter.default.addObserver(self,
                                               selector: Selector("proximityChanged"),
                                               name:UIDevice.proximityStateDidChangeNotification,
                                               object: nil)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @objc func proximityChanged() {
        //状態を表示
        print("\(UIDevice.current.proximityState)")
    }
}

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

FirebaseをCarthageで設定する時のXcodegenのyml

こちらを参考にxcodegenのymlを書きました。
https://qiita.com/lovee/items/09d33b7c8b1e3ff3dcc6

targets:
  App:
    settings:
      base:
        OTHER_LDFLAGS: $(inherited) $(OTHER_LDFLAGS) -ObjC
    dependencies:
      - framework: Carthage/Build/iOS/Firebase.framework
        embed: false
        link: false
      - framework: Carthage/Build/iOS/FIRAnalyticsConnector.framework
        embed: false
      - framework: Carthage/Build/iOS/FirebaseAnalytics.framework
        embed: false
      - framework: Carthage/Build/iOS/FirebaseAuth.framework
        embed: false
      - framework: Carthage/Build/iOS/FirebaseCore.framework
        embed: false
      - framework: Carthage/Build/iOS/FirebaseCoreDiagnostics.framework
        embed: false
      - framework: Carthage/Build/iOS/FirebaseInstanceID.framework
        embed: false
      - framework: Carthage/Build/iOS/GoogleAppMeasurement.framework
        embed: false
      - framework: Carthage/Build/iOS/GoogleUtilities.framework
        embed: false
      - framework: Carthage/Build/iOS/GTMSessionFetcher.framework
        embed: false
      - framework: Carthage/Build/iOS/nanopb.framework
        embed: false
      - sdk: libc++.tbd
      - sdk: libsqlite3.tbd
      - sdk: CoreTelephony.framework
      - sdk: StoreKit.framework

FirebaseAnalyticsとAuthのサンプル

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

【Swift】オセロ(リバーシ)

概要

友人に 本気で始めるiPhoneアプリ作り という本を頂いたので、Swiftでオセロゲームを作りました。このページで紹介するものはオフライン対戦です。サーバーとの通信を実装したものは他のページで紹介します。

環境

OS
・ macOs
開発環境
・ Xcode10
・ Swift4

完成画像

スクリーンショット 2019-04-05 16.56.22.png

ファイル一覧

  1. ViewController.swift
  2. board.swift   オセロボード
  3. player.swift   対戦するCPU
  4. board.png
    board.png

  5. black.png
    black.png

  6. white.png
    white.png

この 4 ~ 6 の画像を使ってオセロ盤を作ります。

難しいところはないと思うのでコメントを読んで理解してください。わからないところがあればコメントお願いします。記事を修正します。

board.swift

board.swift
import Foundation

class Board{

    var SIZE: Int = 0
    let DIRECTIONS_XY = [[-1, -1], [+0, -1], [+1, -1],
                         [-1, +0],           [+1, +0],
                         [-1, +1], [+0, +1], [+1, +1]]
    let BLACK = -1
    let WHITE = 1
    let BLANK = 0
    var square:[[Int]] = []

    // オセロを開始する際に呼ばれ、オセロ盤を初期化する
    func start(size: Int){
        self.SIZE = size
        let center = size / 2
        for _ in 0..<self.SIZE{
            var array:[Int] = []
            for _ in 0..<self.SIZE{
                array += [BLANK]
            }
            square += [array]
        }
        square[center-1][center-1] = self.WHITE
        square[center-1][center] = self.BLACK
        square[center][center-1] = self.BLACK
        square[center][center] = self.WHITE
    }

    // 盤上にある石の個数を返す
    func returnStone() -> (Int,Int) {
        var black = 0
        var white = 0
        var blank = 0
        for y in 0..<SIZE{
            for x in 0..<SIZE{
                switch square[y][x]{
                case BLACK:
                    black += 1
                case WHITE:
                    white += 1
                default:
                    blank += 1
                }
            }
        }
        return (black, white)
    }

    // 対戦終了時もう一度対戦する際にボード板をリセットする
    func reset(){
        var _square:[[Int]] = []
        let size = SIZE
        let center = size / 2
        for _ in 0..<SIZE{
            var array:[Int] = []
            for _ in 0..<SIZE{
                array += [BLANK]
            }
            _square += [array]
        }
        _square[center-1][center-1] = self.WHITE
        _square[center-1][center] = self.BLACK
        _square[center][center-1] = self.BLACK
        _square[center][center] = self.WHITE
        square = _square
    }

    // ボード盤を返す
    func return_board() -> [[Int]]{
        return square
    }

    // 呼ばれた段階で Game Over であるかどうかを判定する
    func gameOver() -> Bool {
        var black = 0
        var white = 0
        var blank = 0
        for y in 0..<SIZE{
            for x in 0..<SIZE{
                switch square[y][x]{
                case BLACK:
                    black += 1
                case WHITE:
                    white += 1
                default:
                    blank += 1
                }
            }
        }
        if( blank == 0 || black == 0 || white == 0 ){
            return true
        }
        if( self.available(stone: BLACK).count == 0 && self.available(stone: WHITE).count == 0){
            return true
        }
        return false
    }

    func is_available( x: Int, y:Int, stone: Int) -> Bool {
        if ( square[x][y] != BLANK ){
            return false
        }
        for i in 0..<8 {
            let dx = DIRECTIONS_XY[i][0]
            let dy = DIRECTIONS_XY[i][1]
            if( self.count_reversible(x: x, y: y, dx: dx, dy: dy, stone: stone) > 0 ){
                return true
            }
        }
        return false
    }

    // 引数で与えられた石の次に打てる場所を返す
    func available(stone: Int) -> [[Int]]{
        var return_array:[[Int]] = []
        for x in 0..<SIZE{
            for y in 0..<SIZE{
                if( self.is_available( x: x, y: y, stone: stone) ){
                    return_array += [[x,y]]
                }
            }
        }
        return return_array
    }

    // ボードに石を置く
    func put( x: Int, y:Int, stone: Int){
        square[x][y] = stone
        for i in 0..<8 {
            let dx = DIRECTIONS_XY[i][0]
            let dy = DIRECTIONS_XY[i][1]
            let n = self.count_reversible( x: x, y: y, dx: dx, dy: dy, stone: stone)
            for j in 1..<(n+1){
                square[x + j * dx][y + j * dy] = stone
            }
        }
    }

    func count_reversible( x: Int, y: Int, dx: Int, dy: Int, stone: Int) -> Int {
        var _x = x
        var _y = y
        for i in 0..<SIZE{
            _x = _x + dx
            _y = _y + dy
            // 0 <= x < 4 : can't write <- Annoying!!!!
            if !( 0 <= _x && _x < SIZE && 0 <= _y && _y < SIZE ){
                return 0
            }
            if (square[_x][_y] == BLANK){
                return 0
            }
            if (square[_x][_y] == stone){
                return i
            }
        }
        return 0
    }
}

player.swift

今回は簡単なランダム君だけです。

player.swift
import Foundation

class Player {

    func play(board: Board, stone: Int) -> (Int,Int) {
        return Random(available: board.available(stone: stone))
    }

    // 打てるところにランダムで打ちます。
    // swift には random.choice() みたいなものありますか?
    func Random(available: [[Int]]) -> (Int,Int) {
        let int = Int.random(in: 0..<available.count)
        return (available[int][0], available[int][1])
    }
}

ViewController.swift

ViewController.swift
class ViewController: UIViewController {

    // BOARDSIZE で ボードのサイズを変更できます。
    let BOARDSIZE = 8
    var board = Board()
    var player = Player()

    var player_name = "Random"
    var Stone_count = 0

    // -1: 黒
    //  1: 白
    let User_color = -1
    let Cpu_color = 1

    // ボードはボタンを行列配置して表現される
    var buttonArray: [UIButton] = []

    // board.png, white.png, black.png
    let baseBoard = UIImage(named: "board")
    let white = UIImage(named: "white")
    let black = UIImage(named: "black")

    var resetButton = UIButton()
    var passButton = UIButton()
    var viewStoneCount = UILabel()

    // オセロ盤を表現するボタン
    class buttonClass: UIButton{
        let x: Int
        let y: Int
        init( x:Int, y:Int, frame: CGRect ) {
            self.x = x
            self.y = y
            super.init(frame:frame)
        }
        required init?(coder aDecoder: NSCoder) {
            fatalError("error")
        }
    }

    // ボタンなどを生成
    func createUI(){
        board.start(size: BOARDSIZE)
        var y = 83
        let boxSize = 84 / (BOARDSIZE/4)
        viewStoneCount.frame = CGRect(x: 18, y: 430, width: 330, height: 60)
        viewStoneCount.textAlignment = NSTextAlignment.center
        viewStoneCount.font = UIFont.systemFont(ofSize: 25)
        self.view.addSubview(viewStoneCount)
        for i in 0..<BOARDSIZE{
            var x = 19
            for j in 0..<BOARDSIZE{
                let button: UIButton = buttonClass(
                    x: i,
                    y: j,
                    frame:CGRect(x: x,y: y, width: boxSize,height: boxSize))
                button.addTarget(self, action: #selector(ViewController.pushed), for: .touchUpInside)
                self.view.addSubview(button)
                button.isEnabled = false
                buttonArray.append(button)
                x = x + boxSize + 1
            }
            y = y + boxSize + 1
        }
        resetButton.frame = CGRect(x: 125, y: 575, width: 125, height: 45)
        resetButton.addTarget(self, action: #selector(ViewController.pushResetButton), for: .touchUpInside)
        resetButton.isEnabled = false
        resetButton.isHidden = true
        resetButton.setTitle("RESET", for: .normal)
        resetButton.titleLabel?.font = UIFont.systemFont(ofSize: 25)
        resetButton.setTitleColor(.white, for: .normal)
        resetButton.backgroundColor = UIColor(red: 0.3, green: 0.7, blue: 0.6, alpha: 1)
        resetButton.layer.cornerRadius = 25
        resetButton.layer.shadowOpacity = 0.5
        resetButton.layer.shadowOffset = CGSize(width: 2, height: 2)
        self.view.addSubview(resetButton)

        passButton.frame = CGRect(x: 150, y: 500, width: 80, height: 30)
        passButton.addTarget(self, action: #selector(ViewController.pushPassButton), for: .touchUpInside)
        passButton.isEnabled = false
        passButton.isHidden = true
        passButton.setTitle("Pass", for: .normal)
        passButton.titleLabel?.font = UIFont.systemFont(ofSize: 25)
        passButton.setTitleColor(.white, for: .normal)
        passButton.backgroundColor = UIColor(red: 0.3, green: 0.7, blue: 0.6, alpha: 1)
        passButton.layer.cornerRadius = 25
        passButton.layer.shadowOpacity = 0.5
        passButton.layer.shadowOffset = CGSize(width: 2, height: 2)
        self.view.addSubview(passButton)
        drawBoard()
    }

    // passButton を押された時の処理
    @objc func pushPassButton() {
        CpuTurn()
        passButton.isEnabled = false
        passButton.isHidden = true
    }

    // resetButton を押された時の処理
    @objc func pushResetButton() {
        board.reset()
        drawBoard()
        resetButton.isEnabled = false
        resetButton.isHidden = true
        passButton.isEnabled = false
        passButton.isHidden = true
    }

    // ボード盤をタッチされた時の処理
    @objc func pushed(mybtn: buttonClass){
        mybtn.isEnabled = false
        board.put(x: mybtn.x, y: mybtn.y, stone: User_color)
        drawBoard()
        if( board.gameOver() == true ){
            resetButton.isEnabled = true
            resetButton.isHidden = false
        }
        self.CpuTurn()
    }

    // CPU
    func CpuTurn() {
        if( board.available(stone: Cpu_color).count != 0 ){
            let xy = player.play(board: board, stone: Cpu_color)
            board.put(x: xy.0, y: xy.1, stone: Cpu_color)
            drawBoard()
            if( board.gameOver() == true ){
                resetButton.isHidden = false
                resetButton.isEnabled = true
            }
        }
        if( board.gameOver() == true ){
            resetButton.isHidden = false
            resetButton.isEnabled = true
        }
        if( board.available(stone: User_color).count == 0){
            passButton.isHidden = false
            passButton.isEnabled = true
        }
    }

    // 画面にオセロ盤を表示させる
    func drawBoard(){
        let stonecount = board.returnStone()
        viewStoneCount.text = "● Uer: " + String(stonecount.0) + "     ○ CPU: " + String(stonecount.1)
        var count = 0
        var _board = board.return_board()
        for y in 0..<BOARDSIZE{
            for x in 0..<BOARDSIZE{
                if( _board[y][x] == User_color ){
                    buttonArray[count].setImage(black, for: .normal)
                } else if( _board[y][x] == Cpu_color ){
                    buttonArray[count].setImage(white, for: .normal)
                } else {
                    buttonArray[count].setImage(baseBoard, for: .normal)
                }
                buttonArray[count].isEnabled = false
                count += 1
            }
        }
        let availableList = board.available(stone: User_color)
        for i in 0..<(availableList.count){
            let x = availableList[i][0]
            let y = availableList[i][1]
            buttonArray[x*BOARDSIZE+y].isEnabled = true
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.createUI()
    }
}

まとめ

kotlinと似ていてとても書きやすかったです。今回はランダム君としか対戦できませんが、サーバーと通信することで強いCPUと対戦することができます。それについては次回書きます。改善点や疑問点などがありましたらコメントお願いします。kotlin版もあとで書きます。

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

UIFeedbackGeneratorを簡単に使えるようにする

開発環境

  • Xcode10.2
  • Swift 5.0

UIFeedbackGeneratorとは

Apple公式から引用

概要
このクラスのインスタンスを自分でサブクラス化したり作成したりしないでください。代わりに、提供されている具象サブクラスの1つをインスタンス化してください。

UIImpactFeedbackGenerator。影響が発生したことを示すには、影響フィードバックジェネレータを使用します。たとえば、ユーザーインターフェイスオブジェクトが何かにぶつかったり、所定の位置に固定されたときに、インパクトフィードバックを引き起こすことがあります。

UISelectionFeedbackGenerator。選択の変化を示すために選択フィードバックジェネレータを使用します。

UINotificationFeedbackGenerator。成功、失敗、および警告を示すために通知フィードバックジェネレータを使用します。

何かのアクションのフィードバックとしてユーザさんの端末をブルッてさせてフィードバックするやつです。

通常の実装方法

UIFeedbackGeneratorの使い方と便利に使えるライブラリから引用させていただきました。

class ViewController: UIViewController {

    private let feedbackGenerator: Any? = {
        if #available(iOS 10.0, *) {
            let generator: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
            generator.prepare()
            return generator
        } else {
            return nil
        }
    }()

    @IBAction private func light() {
        if #available(iOS 10.0, *), let generator = feedbackGenerator as? UIImpactFeedbackGenerator {
            generator.impactOccurred()
        }
    }

各VCに記述するにはしんどいなという印象でした。

簡単に使う方法

以下の構造体でラップして利用します。

/// UIFeedbackGeneratorを簡単に利用するためのラッパー
struct Feedbacker {

  static func notice(type: UINotificationFeedbackGenerator.FeedbackType) {
    if #available(iOS 10.0, *) {
      let generator = UINotificationFeedbackGenerator()
      generator.prepare()
      generator.notificationOccurred(type)
    }
  }

  static func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) {
    if #available(iOS 10.0, *) {
      let generator = UIImpactFeedbackGenerator(style: style)
      generator.prepare()
      generator.impactOccurred()
    }
  }

  static func selection() {
    if #available(iOS 10.0, *) {
      let generator = UISelectionFeedbackGenerator()
      generator.prepare()
      generator.selectionChanged()
    }
  }

}

使い方

フィードバックをさせたいところでstaticメソッドをコールします。

/// 例
override func viewDidLoad() {
  // ViewControllerの読み込みが終わったらフィードバック
  Feedbacker.notice(type: .success)
}

コールの仕方は以下になりますので用途によって使い分けてください。

// notice
Feedbacker.notice(type: .success)
Feedbacker.notice(type: .warning)
Feedbacker.notice(type: .error)

// impact
Feedbacker.impact(style: .heavy)
Feedbacker.impact(style: .light)
Feedbacker.impact(style: .medium)

// selection
Feedbacker.selection()

おわり

間違えてる部分や編集リクエストありましたらよろしくお願いいたします

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

Swift で RTMP Handshake を実装する

RTMPとは

RTMPは、Adobe が開発している Adobe Flash プレーヤーとサーバーの間で、音声・動画・データをやりとりするストリーミングのプロトコルです。
RTMP の主要な利用法は Flash Video を再生することでしたが、低遅延でのストリーミングを実現できることから、ストリーミングサーバーへの映像伝送に使われたりもします。

ネットで調べると大体のLive配信アプリはRTMPで配信し、HLSで視聴しているものが多いようです。

RTMP Handshakeとは

RTMPはTCP上で動きます。
TCPによる接続が確立すると、RTMPの接続をハンドシェイクから行っていきます。
RTMPのハンドシェイクは他のプロトコルとは異なります。
クライアント(接続を開始したエンドポイント)とサーバーはそれぞれ3つの固定長のチャンクを送信します。
これらのチャンクは、クライアントから送信されるものはC0、C1、C2、サーバーから送信されるものは0、S1、S2と呼ばれています。

ハンドシェイクは、クライアントがC0およびC1チャンクを送信することから始まります。
クライアントは、C2を送信する前にS1が受信されるまで待つ必要があります。
また、その他のデータ(音声や映像など)を送信するためにはS2の受信を待つ必要があります。
逆にサーバーはS0、S1を送る前にC0が受け取られる必要があります。(C1の受信まで待機してもOK)
S2を送信するにはC1が受信されるまで待つ必要があります。
そしてクライアントと同様その他のデータを送信するにはC2の受信を待つ必要があります。
図にするとこんな感じです。

handshake.png

しかし、C0とかS0とか言われても正直良くわかりません。
実際の通信を見ると、なるほど、ほんとにC0とかが送られているんだなとわかります。

capture.png

ほんとにそういう名前なんですねー。

これらのチャンクには決まったフォーマットがありますが、フォーマットに関する説明は 仕様書 の「5.2.2. C0 and S0 Format」「5.2.3. C1 and S1 Format」「5.2.4. C2 and S2 Format」に詳しく書いてあるので割愛します。
こちらの記事も仕様書の日本語版くらいわかりやすかったので参考にしてみてください。
https://developers.cyberagent.co.jp/blog/archives/13739/#handshake

これも実際の通信を見てみると当たり前ですがフォーマット通りのチャンクが送受信されていることがわかります。

スクリーンショット 2019-04-03 14.51.05.png
スクリーンショット 2019-04-03 14.51.17.png
スクリーンショット 2019-04-03 14.51.30.png

ハンドシェークの実装

では実装していきます。
ソケット接続自体は NSStream を使えば簡単です。

let inputQueue = DispatchQueue(label: "NetSocket.input")
let outputQueue = DispatchQueue(label: "NetSocket.output")

var inputStream: InputStream?
var outputStream: OutputStream?

func open(hostname: String, port: Int) {
    Stream.getStreamsToHost(
        withName: hostname,
        port: port,
        inputStream: &self.inputStream,
        outputStream: &self.outputStream
    )

    inputQueue.async {
        self.inputStream?.delegate = self
        self.inputStream?.schedule(in: .main, forMode: .default)

        self.outputStream?.delegate = self
        self.outputStream?.schedule(in: .main, forMode: .default)

        self.inputStream?.open()
        self.outputStream?.open()
    }
}

読み込みと書き込みについてはそれぞれ InputStream#readOutputStream#write を使って以下のように書けます。

func read() {
    outputQueue.async {
        let maxLength = Int(UInt16.max)

        let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxLength)
        let count = self.inputStream?.read(buffer, maxLength: maxLength) ?? 0

        var data = Data()
        if 0 < count {
            data.append(buffer, count: count)
            self.listen(data) // listen(_:)ついては後述
        }
    }
}

func write(data: Data) {
    DispatchQueue.main.async {
        let length = data.count
        var leftLength = length
        data.withUnsafeBytes { (buffer: UnsafePointer<UInt8>) -> Void in
            while leftLength > 0 {
                let count = self.outputStream?.write(buffer + (length - leftLength), maxLength: length) ?? 0
                leftLength -= count
            }
        }
    }
}

Input/OutputのStreamの状態が変化するとStreamDelegate#stream(Stream, handle: Stream.Event) が呼ばれます。
これで通信可能になったとき( .openCompleted )と何かを受信したとき( .hasBytesAvailable )がわかるのでここからチャンクの送受信をおこなっていきます。

ソケットをオープンして通信が可能になったらクライアントからC0とC1を送信します。
C1まで送信するとサーバーからS0、S1、S2が送られてくるので先程の read() メソッドを使って受信したチャンクを読みます。

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case .openCompleted:
        guard inputStream?.streamStatus == .open, outputStream?.streamStatus == .open else { break }
        if aStream == inputStream {
            sendC0C1packet()
        }
    case .hasBytesAvailable:
        if aStream == inputStream {
            read()
        }
    case .errorOccurred:
        print("errorOccurred")
    default:
        break
    }
}

まずはC0とC1の送信です。
※整数のバイト配列への変換については HaishinKit.swiftExpressibleByIntegerLiteral+Extension.swift を使わせてもらってます。

func sendC0C1packet() {
     var data = Data()

     // C0
     let protocolVersion: UInt8 = 3
     data.append(protocolVersion.data)

     // C1
     let time = UInt32(0)
     data.append(time.bigEndian.data)

     let zero = Data([0x00, 0x00, 0x00, 0x00])
     data.append(zero)

     let size = 1536 - 8 // C1の固定長 - time+zero
     var random = Data(count: size)
     var _ = random.withUnsafeMutableBytes { bytes in
         SecRandomCopyBytes(kSecRandomDefault, size, bytes)
     }
     data.append(random)
     write(data: data)
     handshakeStatus = .c0c1
}

次にS0、S1、S2の読み取りです。
先程 read() メソッドの中に登場した listen(_:) を実装していきます。
HaishinKit.swift を参考にしています。

ハンドシェイクのステータスをenumで管理しています。
先程の sendC0C1packet() を実行した時点でステータスは .c0c1 になっています。
ハンドシェイクは上述の通りルールが決まっているので、C0とC1を送ったならサーバからはS0、S1が来るはずです。
というわけで、 .c0c1 の状態でサーバーからチャンクを受け取ったらそれはS0、S1だろうということで次はC2を送ります(ステータスは .c2 )。
.c2 の状態でサーバーからチャンクを受け取ったらそれはS1だろうということでこれでハンドシェイクは完了です。

enum HandshakeStatus {
    case none
    case c0c1
    case c2
    case done
} 

func listen(_ data: Data) {
    switch handshakeStatus {
    case .none:
        break
    case .c0c1:
        if data.count <= 1536 {
            break
        }
        sendC2packet(data)
        handshakeStatus = .c2

        var data = data
        data.removeSubrange(0... 1536) // S0、S1、S2はいっぺんに送られてくることもあるのでS0、S1を取り除いてS2を渡して再起実行します
        if 1536 <= data.count {
            listen(data)
        }
    case .c2:
        handshakeStatus = .done // ハンドシェイク完了
    case .done:
        break
    }
}
func sendC2packet(_ s0s1packet: Data) {
    var data = Data()

    let time = s0s1packet.subdata(in: 1..<5)
    data.append(time)

    let time2 = UInt32(Date().timeIntervalSince1970)
    data.append(time2.bigEndian.data)

    let random = s0s1packet.subdata(in: 9..<1536+1)
    data.append(random)
    write(data: data)
    handshakeStatus = .c2
}

まとめ

RTMP配信の序章を実装してみましたが、ハンドシェイクの仕様を理解するのが一番のハードルでした。
通信処理自体は NSStream を使えば簡単に書けてしまうので実装自体はそう難しくなかったと思います。
ハンドシェイクの段階ではまだ受信したデータのパース処理が無いので(比較的)楽ですね。
しかしハンドシェイクが終わっても、このあとクライアントからconnectコマンドメッセージを送信してサーバーからresultを受信して今度はクライアントから...みたいな感じで音声や映像を送信するまではまだ道が長いです。。
自前で実装するのはかなりめんどくさいですが、仕様書や実際の通信をキャプチャして仕様を理解していくのは面白いなと思いました。

参考

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