20191222のSwiftに関する記事は18件です。

AVAudioEngineでリモートの音楽ファイルを再生する

やりたいこと

リモートにある音楽ファイルをiOS端末で再生する。
iOSの音声再生APIは複数存在するが、信号処理をしたいのでAVAudioEngineを使う。
XambmPaQ.png

環境

手順

  1. 音楽ファイルをダウンロードする
  2. パケットに分割する
  3. パケットをPCMに変換する
  4. PCMをAVAudioEngineを使って再生する

実装

step1. 音楽ファイルをダウンロードする

URLSessionを使って指定したURLからDataをダウンロードする。
downloaderを外から入れられるようにRemoteAudioDownloaderのprotocolを定義

RemoteAudioDownloader.swift
public protocol RemoteAudioDownloader {
    func request(url: URL, completionHandler: @escaping (_ data: Data) -> Void)
}

internal class DefaultRemoteAudioDownloader: RemoteAudioDownloader {
    var task: URLSessionDataTask?
    var completionHandler: ((_ data: Data) -> Void)?
    func request(url: URL, completionHandler: @escaping (Data) -> Void) {
        self.completionHandler = completionHandler
        let request = URLRequest(url: url)
        task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data: Data?, response: URLResponse?, error: Error?) in
            guard let self = self, let data = data else { return }
            self.completionHandler?(data)
        })
        task?.resume()
    }
}

step2. パケットに分割する

step2-1. AudioFileStreamOpenを使ってDataを開く

ダウンロードしたDataをAudio File Stream Servicesでパケットに分割する。
最初にDataをAudioFileStreamOpenを使って開く。
分割したパケットはAudioFileStreamServiceで処理する。
propertyの処理にはAudioFileStreamService.propertyListenerProcedureが呼ばれる。
packetのdataの処理にはAudioFileStreamService.packetsProcedureが呼ばれる。
inClientDataのポインタとして自信のポインタを渡す。
AudioFileStreamService.propertyListenerProcedure, AudioFileStreamService.packetsProcedure内でinClientDataをAudioFileStreamServiceにキャストすることで、自信を呼び出すことができる。

AudioFileStreamService.swift
public class AudioFileStreamService {
    private var streamID: AudioFileStreamID?
    public init() {
        let inClientData = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
        _ = AudioFileStreamOpen(inClientData,
                                { AudioFileStreamService.propertyListenerProcedure($0, $1, $2, $3) },
                                { AudioFileStreamService.packetsProcedure($0, $1, $2, $3, $4) },
                                kAudioFileMP3Type,
                                &streamID)
    }
}

step2-2. AudioFileStream_PropertyListenerProcに届いたpropertyを処理する

AudioFileStream_PropertyListenerProcとして指定したpropertyListenerProcedure内でAVAudioFormatを取得する。

AudioFileStreamService.swift
extension AudioFileStreamService {
    static func propertyListenerProcedure(_ inClientData: UnsafeMutableRawPointer, _ inAudioFileStream: AudioFileStreamID, _ inPropertyID: AudioFileStreamPropertyID, _ ioFlags: UnsafeMutablePointer<AudioFileStreamPropertyFlags>) {
        let audioFileStreamService = Unmanaged<AudioFileStreamService>.fromOpaque(inClientData).takeUnretainedValue()
        switch inPropertyID {
        case kAudioFileStreamProperty_DataFormat:
            var description = AudioStreamBasicDescription()
            var propSize: UInt32 = 0
            _ = AudioFileStreamGetPropertyInfo(inAudioFileStream, inPropertyID, &propSize, nil)
            _ = AudioFileStreamGetProperty(inAudioFileStream, inPropertyID, &propSize, &description)
            print("format: ", AVAudioFormat(streamDescription: &description))
        default:
            print("unknown propertyID \(inPropertyID)")
        }
    }
}

step2-3. AudioFileStream_PacketsProcに届いたpacketsを処理する

AudioFileStream_PacketsProcとして指定したpacketsProcedure内でpacketのDataとAudioStreamPacketDescriptionを取得する。

AudioFileStreamService.swift
extension AudioFileStreamService {
    static func packetsProcedure(_ inClientData: UnsafeMutableRawPointer, _ inNumberBytes: UInt32, _ inNumberPackets: UInt32, _ inInputData: UnsafeRawPointer, _ inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {
        let audioFileStreamService = Unmanaged<AudioFileStreamService>.fromOpaque(inClientData).takeUnretainedValue()
        var packets: [(data: Data, description: AudioStreamPacketDescription)] = []
        let packetDescriptionList = Array(UnsafeBufferPointer(start: inPacketDescriptions, count: Int(inNumberPackets)))
        for i in 0 ..< Int(inNumberPackets) {
            let packetDescription = packetDescriptionList[i]
            let startOffset = Int(packetDescription.mStartOffset)
            let byteSize = Int(packetDescription.mDataByteSize)
            let packetData = Data(bytes: inInputData.advanced(by: startOffset), count: byteSize)
            packets.append((data: packetData, description: packetDescription))
        }
        print("packets: ", packets)
    }
}

step3. パケットをPCMに変換する

AVAudioConverterを使ってAVAudioCompressedBufferからAVAudioPCMBufferに変換する。

step3-1. DataからAVAudioCompressedBufferを生成する

AVAudioCompressedBufferを作ってDataをコピーする。

CompressedBufferConverter.swift
let compBuff = AVAudioCompressedBuffer(format: srcFormat, packetCapacity: 1, maximumPacketSize: Int(data.count))
_ = data.withUnsafeBytes({ (ptr: UnsafeRawBufferPointer) in
    memcpy(compBuff.data, ptr.baseAddress!, data.count)
})
compBuff.packetDescriptions?.pointee = AudioStreamPacketDescription(mStartOffset: 0, mVariableFramesInPacket: 0, mDataByteSize: UInt32(data.count))
compBuff.packetCount = 1
compBuff.byteLength = UInt32(data.count)

step3-2. AVAudioPCMBufferを生成する

出力用のAVAudioPCMBufferを作成する。

CompressedBufferConverter.swift
let pcmBuff = AVAudioPCMBuffer(pcmFormat: dstFormat, frameCapacity: frames)!
pcmBuff.frameLength = pcmBuff.frameCapacity

step3-3. AVAudioConverterを使って変換する

AVAudioConverterを使って変換する。

CompressedBufferConverter.swift
converter.convert(to: pcmBuff, error: &error, withInputFrom: { [weak self] (count: AVAudioPacketCount, input: UnsafeMutablePointer<AVAudioConverterInputStatus>) -> AVAudioBuffer? in
    input.pointee = .haveData
    let buff = self.audioCompressedBuffer[self.index]
    self?.index += 1
    return buff
})

step4. PCMをAVAudioEngineを使って再生する

ViewController.swift
        engine.attach(player)
        engine.connect(player, to: engine.mainMixerNode, format: internalFormat)
        engine.prepare()
        try! engine.start()
        player.play()
        if let ret = converter?.read(frames: frameCountPerRead) {
            self.scheduleBuffer(ret)
        }

さいごに

Audio File Stream Servicesは古いAPIなので使いにくいと感じました。
mp3は最初の数百フレームが無音として出力されるので、正しく出力できているのか、鳴らしてみるまで疑心暗鬼でしたw

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

実務経験4ヶ月のインターン生がGASを用いて勤務報告を自動化してみた

初めに

閲覧していただき、ありがとうございます!
現在からくり株式会社でiOSエンジニアとしてインターンしております。
今回勤務報告を自動化してみたので、そちらを記事にしたいと思います?

背景

インターン生(アルバイト)は月に一回勤務報告をSlackでしないといけないのですが、
いつも手入力で報告しており、これが滅茶苦茶めんどくさい、、、
「月1の作業くらい頑張れよ?」という野次が飛んできそうですが、
なんせエンジニアなので楽したい性分なのです?

そもそもGASとは??

Google Apps Script(GAS)とは、Googleが提供するサーバーサイド・スクリプト環境のことです。
スプレッドシートやGoogleフォームなどのサービスを、
JavaScriptをベースとしたプログラム言語を使って操作することができます。

実装方針

スクリーンショット 2019-12-22 21.10.04.png

インターン生は上記写真のように出勤日時をGoogleカレンダーに記入しなくてはいけないので、
これらイベント情報を抽出して出勤時間を計算していきたいと思います。

1.カレンダーから出勤のイベントを抽出

const myCalendar = CalendarApp.getCalendarById('自分のカレンダーID');
const attendanceDays = myCalendar.getEvents(getFirstDate(new Date()), getLastDate(new Date()), {search: "出勤"});
//月の初めを取得
function getFirstDate (date) {
  return new Date(date.getFullYear(), date.getMonth(), 1);
}

//月の最後を取得
function getLastDate (date) {
  return new Date(date.getFullYear(), date.getMonth() + 1, 0);
}

getCalendarByIdメソッドを用いて自分のGoogleカレンダーを取得後、
getEventsメソッドを用いて出勤のイベントだけを抽出します。

2.勤務時間を計算

for(var i = 0; i < attendanceDays.length; i++) {
   var attendanceTime = attendanceDays[i].getStartTime()
   var leavingTime = attendanceDays[i].getEndTime()
   var actualWorkingHours = getActualWorkingHours(leavingTime.getHours(), attendanceTime.getHours())
}
//実働時間を取得
function getActualWorkingHours(leavingTime, attendanceTime) {
  actualWorkingHours = leavingTime - attendanceTime
  if (actualWorkingHours == 9) {
    return actualWorkingHours - 1
  } else {
    return actualWorkingHours
  }
}

//休憩時間を取得
function getBreakTime(workingHours) {
  if (workingHours == 9) {
    return 1
  } else {
    return 0
  }
}

取得してきた各々のイベントの終了時刻getEndTime()と開始時刻getStartTime()を取得し、
その差分を計算します。
また、フル勤務の際(10:00~19:00)は1時間休憩をとる必要があり、
実働時間を正確に計算する為、差分から1時間引きます。

3.Slackに投稿

function postMessage() {

  var url = "https://slack.com/api/chat.postMessage";

  var payload = {
    "token" : "アクセストークン",
    "channel" : "投稿したいチャンネルのID",
    "text" : "<@メンションしたい相手のID>投稿するメッセージ,
    "as_user" : true
  };

  var params = {
    "method" : "post",
    "payload" : payload
  };

  UrlFetchApp.fetch(url, params);
}

SlackAPIのchat.postMessageに関してはコチラの記事がわかりやすいです。

4.Google Apps Scriptにトリガーを追加する

最後に月の初めに自動で実行されるようにGoogle Apps Scriptにトリガーを追加します。

プロジェクトのトリガーボタンからトリガーの設定をします。
スクリーンショット 2019-12-23 1.39.16.png
トリガーの一覧画面右下のトリガーを追加ボタンを選択します。
スクリーンショット 2019-12-23 1.39.31.png

イベントのソースを選択で定期的に実行したい時間を適宜設定します。
今回は月の初めに実行してほしいので、下記のように設定しました。

スクリーンショット 2019-12-23 11.04.52.png

これで月の初めに自動で実行されるようになりました!

成果物

スクリーンショット 2019-12-22 21.52.56.png
これで毎月の勤務報告を手入力をしなくても
自動で勤務報告ができるようになりました!(送り忘れることもなし)

まとめ

普段Swiftを書いていて、今回初めてJavaScript(GAS)を触ったのですが、
なんとか形にすることができてとりあえずホッとしてます笑(なんせこの土日で勉強して書いた為、、、ギリギリ?)
まだ交通費の記入や15分単位での計算に対応できてないので、
その辺りを改善していきたいと思います。
また、コードを書いている内にあれも自動化したいな〜?
みたいなのが色々出てきたので、社員の方がより重要な業務に時間を割けるように
無駄な作業はインターン生で自動化していければなと思います!
(社員さんは忙しくてなかなか手が回らないので、、、)

最後に

僕がインターンをしているからくり株式会社では現在エンジニアを募集しているそうです!
(インターン、新卒、中途問わず)
興味がありましたら、是非一度遊びに来てくださいね〜!!
Wantedly

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

【Swift】複数のフラグを管理する場合に Set<Enum> を使う

この記事は

複数のフラグを管理する場合、データサイズを固定長にする目的で OptionSet を使うことがあります。
しかしフラグの数が少なく、データサイズを気にしない状況であれば、OptionSet のかわりに Set<Enum> を使うことで実装を少しシンプルにできます。

データ型の定義

OptionSet

OptionSet はビット集合を表現したプロトコルです。
true | false )を整数型の各ビットの( 1 | 0 )に対応させることで、複数のフラグを1つの 整数型で表現します。

struct ShippingOptions: OptionSet {
    let rawValue: Int

    static let nextDay   = ShippingOptions(rawValue: 1 << 0)
    static let secondDay = ShippingOptions(rawValue: 1 << 1)
    static let priority  = ShippingOptions(rawValue: 1 << 2)
    static let standard  = ShippingOptions(rawValue: 1 << 3)
}

Set<Enum>

Set の要素である列挙体はシンプルに次のように定義するだけです。

enum ShippingOptions {
    case nextDay
    case secondDay
    case priority
    case standard
}

Set<Enum> は列挙型の値を要素をもつ集合です。集合内の要素の有無によってフラグを表現します。
OptionSet の場合とは異なり、要素をビットに格納する方法を用いないので、raw-value-style-enum である必要はありません。もちろん、raw-value-style-enum でも良いです。
rawValue の型は何でも良く、Int である必要もありません。

全体集合

OptionSet では全体集合を自前で定義する必要がありますが、
列挙型であれば CaseIterable で自動合成可能です。

OptionSet

struct ShippingOptions: OptionSet {
    let rawValue: Int

    static let nextDay   = ShippingOptions(rawValue: 1 << 0)
    static let secondDay = ShippingOptions(rawValue: 1 << 1)
    static let priority  = ShippingOptions(rawValue: 1 << 2)
    static let standard  = ShippingOptions(rawValue: 1 << 3)

    /// 全体集合
    static let all: ShippingOptions = [nextDay, secondDay, priority, standard]
}

Set<Enum>

enum ShippingOptions: CaseIterable {
    case nextDay
    case secondDay
    case priority
    case standard
}

allCases から Set を作ることで全体集合になります。

let universe = Set(ShippingOptions.allCases)
// true
universe.isSuperset(of: [.nextDay, .secondDay])

使い方

OptionSetSet<Enum> はともに SetAlgebra に適合しているので、どちらも集合演算を使用することができます。フラグの設定・取得はほとんど同じ操作です。

OptionSet

var options = ShippingOptions()
options.formUnion([.nextDay, .priority])

if (options == [.nextDay, .priority]) {
    print("お急ぎ")
}

Set<Enum>

var options = Set<ShippingOptions>()
options.formUnion([.nextDay, .priority])

if (options == [.nextDay, .priority]) {
    print("お急ぎ")
}

複数のフラグを OptionSet で定義する理由

Set<Enum> の方がシンプルに定義できたりと優位性があるように見えますが、UIKit などのフレームワークでは OptionSet で定義されています。

Swift で作った Set<Enum> では Objective-C にブリッジすることができないからです。
しかし、Objective-C で作った NS_OPTIONS であれば OptionSet な構造体として Swift にブリッジすることができます。

typedef NS_OPTIONS(NSInteger, ShippingOptions) {
    ShippingOptionsNextDay   = 1 << 0,
    ShippingOptionsSecondDay = 1 << 1,
    ShippingOptionsPriority  = 1 << 2,
    ShippingOptionsStandard  = 1 << 3,

    ShippingOptionsAll = 0b1111,
};

Objective-C で上記の定義をすることで、Swift では次のようにインポートされます。

public struct ShippingOptions: OptionSet {
    public init(rawValue: Int)

    public static var nextDay:   ShippingOptions { get }
    public static var secondDay: ShippingOptions { get }
    public static var priority:  ShippingOptions { get }
    public static var standard:  ShippingOptions { get }

    public static var all: ShippingOptions { get }
}

Objective-C と Swift の両方をサポートする場合は、NS_OPTIONS で定義することになります。

まとめ

  • OptionSet は複数のフラグを固定長データに詰め込むことができる
  • Set でも OptionSet と同じ操作ができる
  • Set で実装した方がシンプル
  • Objective-C と Swift の両方をサポートしたい場合は NS_OPTIONS で実装する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メモ

//呼び出し元
@objc enum Status: Int {
    case web
    case normal
    case none

    func get1(str: String) -> Status {
        switch str {
        case "WEB会員":
            return .web
        case "ノーマル会員「":
            return .normal
        default:
            return .none
        }
    }
}

class Enum {

    func get() {
        // 文字列取得
        let sta = "WEB会員"

        // 初期値取得
        let status: Status = .none

        // 文字列をもとに数字を取得今回は0
        let i = status.get1(str: sta).rawValue

        // dictに詰める
        var dict: [String: Any]
        dict = ["rank": i]

        let a = Tamu()
        a.consoleWrite(dict)
    }
}

呼び出し先(objective-c側)

- (void)consoleWrite:(NSDictionary *)dict {
    // integerValueで 型変換を行う。でないと正式にint型になってないから
    NSInteger ka = [dict[@"rank"] integerValue];
    if(StatusWeb == ka) {
        NSLog(@"a");
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITableViewCellのボタンをどのCellのものか判別する

自分へのリマインドを主として書いています。
分かりにくかったら申し訳ないです。

UITableViewCellにボタンを追加

  • addTargetを追加
sample.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomTableViewCell
       let button = UIButton()
       button.addTarget(self, action: #selector(self.buttonEvent(_: )), for: UIControl.Event.touchUpInside)

       return cell
}

- タップされたときのメソッド

sample.swift
 @objc func buttonEvent(_ sender: UIButton) {
        print("tapped.")
    }

tapされたボタンのCellを判定する

今回はbuttonにタグを設定する方法を紹介します。
- buttonにtagを追加
UIButtonにはInt型のtagをつけることができます、これをcellを追加する際にボタンに設定しておくことでボタンがどのセルのボタンなのかを判別できるようになります。

sample.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomTableViewCell
       let button = UIButton()
       button.addTarget(self, action: #selector(self.buttonEvent(_: )), for: UIControl.Event.touchUpInside)
       //タグの設定
       cell.button.tag = indexPath.row

       return cell
}
  • メソッドの書き換え
sample.swift
 @objc func buttonEvent(_ sender: UIButton) {
        print("tapped: \([sender.tag])番目のcell"
 }

Reference

CustomCellのボタンをどのCellのものか判別する

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

RxSwift + MVVM の学習をしよう(サンプルアプリ付き)

この記事は フラーAdventCalendar2019 の22日目の記事です。
前回の21日目は@sacchoさんで AndroidでKotlin Coroutinesを使ってソケット通信をしてみる でした。

私は現在、フラー株式会社でiOSエンジニアとしてアルバイトをさせて頂いています。
今年を振り返るとRxSwift,MVVMをある程度まで習得するのが一番大変だったなと感じています。(現在もですが、、笑)
なので今回は「RxSwift,MVVMを学習するのに参考になった記事を紹介する」 + 「RxSwift,MVVMを使って何か簡単なアプリを作成する」の2点について書いていきたいと思います。
そして、RxSwift初学者の人の参考になれば嬉しいです。

1.RxSwift,MVVMを学習するのに参考になった記事

RxSwift

用途別に紹介していきます。
まず、RxSwiftを最近触り始めたけど、よく理解できていない方にオススメです。

また、2019年のiOSDC自作して理解するリアクティブプログラミングフレームワークはとても参考になりました。(URLは動画となっています。)

次はオペレーターを理解する上で参考にさせて頂いた記事です。

最後は、実践的な使用方法を学習する上で参考になった記事です。

MVVM

自分はまず、アーキテクチャって言葉自体聞いた事ない状態からだったので以下の記事は分かりやすく、ありがたかったです。

また、最近はiOSアプリ設計パターン入門 を購入して勉強しています。

2.RxSwift + MVVMで簡単なStopWatchアプリを作成する

概要

下記の画面の画像にあるように、Startボタンを押すと0.1秒ごとにカウントされ、Stopボタンが出ます。そして、Stopボタンを押すとカウントが止まってStartボタンとResetボタンが出てきます。
このように普通のStopWatchアプリです。
タイトルの方にもMVVMって書いてあるんですが、今回のアプリではModelは使う機会がありませんでした。なので、ViewControllerViewModelのみの実装となります。
また、StoryBoardを使わずにCodeのみで作成しました。

GithubRepository

 https://github.com/SUGIYOSI/StopWatchSample

画面の画像

sample1.png   sample2.png

使用ライブラリ
  • RxSwift
  • RxCocoa
  • SnapKit(今回はあまり重要ではない)
環境
  • Xcode 11.2.1
  • Swift 5.1.2

ViewController

今回使ったUI部品は以下の3つです。(上の画像を参考にしてください)
- timerLabel (UILabel)
- startStopButton (UIButton)
- resetButton (UIButton)

RxSwiftの処理を追うのは初めのうちは結構大変なので、今回はViewとViewModelの動きを以下のように細かく書きました。正確じゃないところもありますが、参考程度にはなると思います。ViewModelとViewControllerを照らし合わせて見てください。あと字が汚くてすみませんw

IMG_1336.jpg

下記のコードはViewを作成しているコードを抜かしているのでコピペしていじってみたい方は、Githubの方からしてください。

StopWatchViewController.swift
class StopWatchViewController: UIViewController {
    private var viewModel: StopWatchViewModelType
    private let disposeBag = DisposeBag()

    init(viewModel: StopWatchViewModelType) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupSubView()
        bind(viewModel)
        viewModel.inputs.isPauseTimer.accept(false)
    }
}

extension StopWatchViewController {
    func bind(_ viewModel: StopWatchViewModelType) {
        startStopButton.rx.tap.asSignal()
            .withLatestFrom(viewModel.outputs.isTimerWorked)
            .emit(onNext: { [weak self] isTimerStop in
                self?.viewModel.inputs.isPauseTimer.accept(!isTimerStop)
            })
            .disposed(by: disposeBag)

        resetButton.rx.tap.asSignal()
            .emit(to: viewModel.inputs.isResetButtonTaped)
            .disposed(by: disposeBag)

        viewModel.outputs.isTimerWorked
            .drive(onNext: { [weak self] isWorked in
                if isWorked {
                    self?.startStopButton.backgroundColor = UIColor(red: 255/255, green: 110/255, blue: 134/255, alpha: 1)
                    self?.startStopButton.setTitle("Stop", for: UIControl.State.normal)
                } else {
                    self?.startStopButton.backgroundColor = UIColor(red: 173/255, green: 247/255, blue: 181/255, alpha: 1)
                    self?.startStopButton.setTitle("Start", for: UIControl.State.normal)
                }
            })
            .disposed(by: disposeBag)

        viewModel.outputs.timerText
            .drive(timerLabel.rx.text)
            .disposed(by: disposeBag)

        viewModel.outputs.isResetButtonHidden
            .drive(resetButton.rx.isHidden)
            .disposed(by: disposeBag)
    }
}

ViewModel

ViewModelInputOutputを分ける事で可読性をあげたり、バグを未然に防ぐ事ができます。
また、InputOutputは必ず DriverSignal を使用しています。

このViewModelを作成するにあたって、以下の3つの記事を参考にしました。

また、Observable<Int>.interval(0.1, scheduler: MainScheduler.instance)を使用する際にこの記事と同じように躓いてしまったので参考にさせてもらいました。

StopWatchViewModel.swift
protocol StopWatchViewModelInputs: AnyObject {
    var isPauseTimer: PublishRelay<Bool> { get }
    var isResetButtonTaped: PublishRelay<Void> { get }
 }

protocol StopWatchViewModelOutputs: AnyObject {
    var isTimerWorked: Driver<Bool> { get }
    var timerText: Driver<String> { get }
    var isResetButtonHidden: Driver<Bool> { get }
}

protocol StopWatchViewModelType: AnyObject {
    var inputs: StopWatchViewModelInputs { get }
    var outputs: StopWatchViewModelOutputs { get }
}

final class StopWatchViewModel: StopWatchViewModelType, StopWatchViewModelInputs, StopWatchViewModelOutputs {
    var inputs: StopWatchViewModelInputs { return self }
    var outputs: StopWatchViewModelOutputs { return self }

    // MARK: - Input
    let isPauseTimer = PublishRelay<Bool>()
    var isResetButtonTaped = PublishRelay<Void>()

    // MARK: - Output
    let isTimerWorked: Driver<Bool>
    let timerText: Driver<String>
    let isResetButtonHidden: Driver<Bool>

    private let disposeBag = DisposeBag()
    private let totalTimeDuration = BehaviorRelay<Int>(value: 0)

    init() {
        isTimerWorked = isPauseTimer.asDriver(onErrorDriveWith: .empty())

        timerText = totalTimeDuration
            .map { String("\(Double($0) / 10)") }
            .asDriver(onErrorDriveWith: .empty())

        isResetButtonHidden = Observable.merge(isTimerWorked.asObservable(), isResetButtonTaped.map { _ in true }.asObservable())
            .skip(1)
            .asDriver(onErrorDriveWith: .empty())

        isTimerWorked.asObservable()
            .flatMapLatest { [weak self] isWorked -> Observable<Int> in
                if isWorked {
                    return Observable<Int>.interval(0.1, scheduler: MainScheduler.instance)
                        .withLatestFrom(Observable<Int>.just(self?.totalTimeDuration.value ?? 0)) { ($0 + $1) }
                } else {
                    return Observable<Int>.just(self?.totalTimeDuration.value ?? 0)
                }
             }
            .bind(to: totalTimeDuration)
            .disposed(by: disposeBag)

        isResetButtonTaped.map { _ in 0 }
            .bind(to: totalTimeDuration)
            .disposed(by: disposeBag)
    }
}

最後に

StopWatchアプリの方で改善した方がいい箇所があればコメントしていただければ嬉しいです。
これからも自分のペースで楽しく学習していければなと思います。
やっぱりRxSwiftは難しいなと思いました。

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

テスト記事

qiitaのAPI検証

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

Vision.frameworkでカメラ画像のテキスト認識を行う

前回の記事では、 Vision.framework をつかって顔認識を行いました。
今度はテキスト認識をやってみます。
ちなみに、テキストの文字認識はiOS13からの機能みたいです。

概要

カメラ画像からテキストを検出し、テキスト部分に矩形を表示。
さらにその部分に検出したテキストを出力します。

現在のところ、対応言語が英語のみのようです。
また、今回のサンプルでは端末を横にしないと、文字をうまく認識しません。

試した環境

Xcode 11.3
iOS 13.2
swift 5

実行サンプル

動画なので速度を出すため、検証精度を落として確認しています。
画面をアップにするとそこそこの精度は出ていそうです。

検証に使ったサイトのURLは以下です。
https://en.wikipedia.org/wiki/Apple

コード説明

手順的には、顔認証とほぼ同じで、リクエストを VNDetectFaceRectanglesRequest から VNRecognizeTextRequest に変更します。
VNRecognizeTextRequest で画像から検出した文字情報を [VNRecognizedTextObservation] として受け取ります。
ちなみに、 VNDetectTextRectanglesRequest でも文字の矩形取得はできるのですが、こちらの場合文字情報を取得することができません。

また、リクエストにプロパティを設定することで、文字取得条件を変更できます。

recognitionLevel = 文字の取得制度設定。 fastaccurate があり、動画で検出する場合は fast が良いみたい
recognitionLanguages = 認識する言語。 現在は英語のみ。
usesLanguageCorrection = 認識した文字を自動修正する機能。 スペルミス防止などにつかえそう?

    /// 文字認識情報の配列取得 (非同期)
    private func getTextObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNRecognizedTextObservation])->())) {
        let request = VNRecognizeTextRequest { (request, error) in
            guard let results = request.results as? [VNRecognizedTextObservation] else {
                completion([])
                return
            }
            completion(results)
        }

        request.recognitionLevel = recognitionLevel
        request.recognitionLanguages = supportedRecognitionLanguages
        request.usesLanguageCorrection = true

        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        try? handler.perform([request])
    }

サポートしている言語一覧は以下手続きで取得。

    /// サポートしている言語リストを取得 (現在は英語のみ)
    private lazy var supportedRecognitionLanguages : [String] = {
        return (try? VNRecognizeTextRequest.supportedRecognitionLanguages(
        for: recognitionLevel,
        revision: VNRecognizeTextRequestRevision1)) ?? []
    }()

文字取得とは直接関係ありませんが、画面上に取得した文字を表示するため、
CGContext に文字を書き込んでいます。
普通に書き込むと座標系の関係で文字列が逆転してしまうみたいで、
何気にここが一番面倒くさい処理となりました・・・

   /// コンテキストに矩形を描画
    private func drawRect(_ rect: CGRect, text: String, context: CGContext) {

        context.setLineWidth(4.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.stroke(rect)
        context.setFillColor(UIColor.black.withAlphaComponent(0.6).cgColor)
        context.fill(rect)

        drawText(text, rect: rect, context: context)

    }

    /// コンテキストにテキストを描画  (そのまま描画すると文字が反転するので、反転させる必要あり)
    private func drawText(_ text: String, rect: CGRect, context: CGContext) {

        context.saveGState()
        defer {
            context.restoreGState()
        }

        let transform = CGAffineTransform(scaleX: 1, y: 1)
        context.concatenate(transform)

        guard let textStyle = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle else {
            return
        }
        let font = UIFont.boldSystemFont(ofSize: 20)
        let textFontAttributes = [
            NSAttributedString.Key.font: font,
            NSAttributedString.Key.foregroundColor: UIColor.white,
            NSAttributedString.Key.paragraphStyle: textStyle
        ]

        let astr = NSAttributedString(string: text, attributes: textFontAttributes)
        let setter = CTFramesetterCreateWithAttributedString(astr)
        let path = CGPath(rect: rect, transform: nil)
        let frame = CTFramesetterCreateFrame(setter, CFRange(), path, nil)

        context.textMatrix = CGAffineTransform.identity
        CTFrameDraw(frame, context)

    }

VNRecognizedTextObservation から文字や検出範囲を取得する手続きは以下部分です。
topCandidates に検出文字候補が評価が高い順に入ってるみたいなので、一番最初の物を決め打ちで取るようにしています。

        textObservations.forEach{
            let rect = getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize)
            let text = $0.topCandidates(1).first?.string ?? "" // topCandidates に文字列候補配列が含まれている
            self.drawRect(rect, text: text, context: newContext)
        }

コード全体

import UIKit
import AVFoundation
import Vision

class TextObservationViewController: UIViewController {

    @IBOutlet weak var previewImageView: UIImageView!

    private let avCaptureSession = AVCaptureSession()

    /// 認識制度を設定。 リアルタイム処理なので fastで
    private let recognitionLevel : VNRequestTextRecognitionLevel = .fast

    /// サポートしている言語リストを取得 (現在は英語のみ)
    private lazy var supportedRecognitionLanguages : [String] = {
        return (try? VNRecognizeTextRequest.supportedRecognitionLanguages(
        for: recognitionLevel,
        revision: VNRecognizeTextRequestRevision1)) ?? []
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupCamera()
    }

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

    /// カメラのセットアップ
    private func setupCamera() {
        avCaptureSession.sessionPreset = .photo

        let device = AVCaptureDevice.default(for: .video)
        let input = try! AVCaptureDeviceInput(device: device!)
        avCaptureSession.addInput(input)

        let videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)]
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.setSampleBufferDelegate(self, queue: .global())

        avCaptureSession.addOutput(videoDataOutput)
        avCaptureSession.startRunning()
    }

    /// コンテキストに矩形を描画
    private func drawRect(_ rect: CGRect, text: String, context: CGContext) {

        context.setLineWidth(4.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.stroke(rect)
        context.setFillColor(UIColor.black.withAlphaComponent(0.6).cgColor)
        context.fill(rect)

        drawText(text, rect: rect, context: context)

    }

    /// コンテキストにテキストを描画  (そのまま描画すると文字が反転するので、反転させる必要あり)
    private func drawText(_ text: String, rect: CGRect, context: CGContext) {

        context.saveGState()
        defer {
            context.restoreGState()
        }

        let transform = CGAffineTransform(scaleX: 1, y: 1)
        context.concatenate(transform)

        guard let textStyle = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle else {
            return
        }
        let font = UIFont.boldSystemFont(ofSize: 20)
        let textFontAttributes = [
            NSAttributedString.Key.font: font,
            NSAttributedString.Key.foregroundColor: UIColor.white,
            NSAttributedString.Key.paragraphStyle: textStyle
        ]

        let astr = NSAttributedString(string: text, attributes: textFontAttributes)
        let setter = CTFramesetterCreateWithAttributedString(astr)
        let path = CGPath(rect: rect, transform: nil)
        let frame = CTFramesetterCreateFrame(setter, CFRange(), path, nil)

        context.textMatrix = CGAffineTransform.identity
        CTFrameDraw(frame, context)

    }

    /// 文字認識情報の配列取得 (非同期)
    private func getTextObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNRecognizedTextObservation])->())) {
        let request = VNRecognizeTextRequest { (request, error) in
            guard let results = request.results as? [VNRecognizedTextObservation] else {
                completion([])
                return
            }
            completion(results)
        }

        request.recognitionLevel = recognitionLevel
        request.recognitionLanguages = supportedRecognitionLanguages
        request.usesLanguageCorrection = true

        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        try? handler.perform([request])
    }

    /// 正規化された矩形位置を指定領域に展開
    private func getUnfoldRect(normalizedRect: CGRect, targetSize: CGSize) -> CGRect {
        return CGRect(
            x: normalizedRect.minX * targetSize.width,
            y: normalizedRect.minY * targetSize.height,
            width: normalizedRect.width * targetSize.width,
            height: normalizedRect.height * targetSize.height
        )
    }

    /// 文字検出位置に矩形を描画した image を取得
    private func getTextRectsImage(sampleBuffer :CMSampleBuffer, textObservations: [VNRecognizedTextObservation]) -> UIImage? {

        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return nil
        }

        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        guard let pixelBufferBaseAddres = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else {
            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
            return nil
        }

        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)
        let bitmapInfo = CGBitmapInfo(rawValue:
            (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
        )

        guard let newContext = CGContext(
            data: pixelBufferBaseAddres,
            width: width,
            height: height,
            bitsPerComponent: 8,
            bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer),
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: bitmapInfo.rawValue
            ) else
        {
            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
            return nil
        }

        let imageSize = CGSize(width: width, height: height)

        textObservations.forEach{
            let rect = getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize)
            let text = $0.topCandidates(1).first?.string ?? "" // topCandidates に文字列候補配列が含まれている
            self.drawRect(rect, text: text, context: newContext)
        }

        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        guard let imageRef = newContext.makeImage() else {
            return nil
        }
        let image = UIImage(cgImage: imageRef, scale: 1.0, orientation: UIImage.Orientation.right)

        return image
    }
}


extension TextObservationViewController : AVCaptureVideoDataOutputSampleBufferDelegate{

    /// カメラからの映像取得デリゲート
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        getTextObservations(pixelBuffer: pixelBuffer) { [weak self] textObservations in
            guard let self = self else { return }
            let image = self.getTextRectsImage(sampleBuffer: sampleBuffer, textObservations: textObservations)
            DispatchQueue.main.async { [weak self] in
                self?.previewImageView.image = image
            }
        }
    }
}

github

becky3/text_observation: Vision.frameworkでカメラ画像のテキスト認識を行う
https://github.com/becky3/text_observation

参考サイト

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

TextFieldタップでDataPickerを呼ぶ

下図のように編集開始時にDatePickerを起動させ,時間変更時に変更時間をTextFieldに反映させます.

ViewController.swift
var toolBar:UIToolbar!
@IBOutlet var wakeTimeTextField: UITextField!

override func viewDidLoad() {
    super.viewDidLoad()

    wakeTimeTextField.delegate = self
    setupToolbar()
}
func setupToolbar() {
        //datepicker上のtoolbarのdoneボタン
        toolBar = UIToolbar()
        toolBar.sizeToFit()
        let toolBarBtn = UIBarButtonItem(title: "DONE", style: .plain, target: self, action: #selector(doneBtn))
        toolBar.items = [toolBarBtn]
        wakeTimeTextField.inputAccessoryView = toolBar
}

func textFieldDidBeginEditing(_ textField: UITextField) {
    let datePickerView:UIDatePicker = UIDatePicker()
    datePickerView.datePickerMode = UIDatePicker.Mode.time
    textField.inputView = datePickerView
    datePickerView.addTarget(self, action: #selector(datePickerValueChanged(sender:)), for: UIControl.Event.valueChanged)
}

    //datepickerが選択されたらtextfieldに表示
@objc func datePickerValueChanged(sender:UIDatePicker) {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat  = "H:mm";
    wakeTimeTextField.text = dateFormatter.string(from: sender.date)
}

//toolbarのdoneボタン
@objc func doneBtn(){
    wakeTimeTextField.resignFirstResponder()
}

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

TextFieldタップでDatePickerを呼ぶ

下図のように編集開始時にDatePickerを起動させ,時間変更時に変更時間をTextFieldに反映させます.

まずTextFieldのDelegateを追加します.

ViewController.swift
class ViewController: UIViewController, UITextFieldDelegate 

次に以下のコードを追加hします

ViewController.swift
var toolBar:UIToolbar!
@IBOutlet var wakeTimeTextField: UITextField!

override func viewDidLoad() {
    super.viewDidLoad()

    wakeTimeTextField.delegate = self
    setupToolbar()
}
func setupToolbar() {
        //datepicker上のtoolbarのdoneボタン
        toolBar = UIToolbar()
        toolBar.sizeToFit()
        let toolBarBtn = UIBarButtonItem(title: "DONE", style: .plain, target: self, action: #selector(doneBtn))
        toolBar.items = [toolBarBtn]
        wakeTimeTextField.inputAccessoryView = toolBar
}

func textFieldDidBeginEditing(_ textField: UITextField) {
    let datePickerView:UIDatePicker = UIDatePicker()
    datePickerView.datePickerMode = UIDatePicker.Mode.time
    textField.inputView = datePickerView
    datePickerView.addTarget(self, action: #selector(datePickerValueChanged(sender:)), for: UIControl.Event.valueChanged)
}

    //datepickerが選択されたらtextfieldに表示
@objc func datePickerValueChanged(sender:UIDatePicker) {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat  = "H:mm";
    wakeTimeTextField.text = dateFormatter.string(from: sender.date)
}

//toolbarのdoneボタン
@objc func doneBtn(){
    wakeTimeTextField.resignFirstResponder()
}

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

extensionの条件付き適合の謎

ことの発端

条件付き適合を使用して処理を分けつつ共通化していたとき
なぜか条件付き適合ではない方の処理を実行していました。

環境

swift 5
Xcode 11.3

ソースコード

protocol Hoge {
    associatedtype Value
    func value(_ value: Value?) -> Value?
}

struct HogeImple<Value> {
    // jQuery風に value が届いたら保存、なければ読込
    func value(_ value: Value? = nil) -> Value? {
        if let value = value {
            self.store(value)
            return value
        }
        return self.read(value)
    }
}

private extension Hoge {
    func store(_ value: Value?) {
        // 保存
        /* !!! String, Codable もこちらにくる!? !!! */
    }

    func read() -> Value? {
        // 読込
    }
}

private extension Hoge where Value == String {
    func read(_ value: Value?) {
        // 文字列の読込
    }
}

private extension Hoge where Value: Codable {
    func store(_ value: Value?) {
        // Codableの保存
    }

    func read() -> Value? {
        // Codableの読込
    }
}

実験

条件付き適合が動作する条件は何か実験してみました。

protocol Hoge {
    associatedtype Value
}

struct HogeImple<Value>: Hoge {
    func execute(_ value: Value) {
        self.hoge(value)
    }
}

extension Hoge {
    func hoge(_ value: Value) {
        print("Hoge", value)
    }
}

extension Hoge where Value == String {
    func hoge(_ value: Value) {
        print("String", value)
    }
}

extension Hoge where Value: Numeric {
    func hoge(_ value: Value) {
        print("Numeric", value)
    }
}

let boolHoge = HogeImple<Bool>()
boolHoge.execute(true)
boolHoge.hoge(true)

let strHoge = HogeImple<String>()
strHoge.execute("Value")
strHoge.hoge("Value")

let intHoge = HogeImple<Int>()
intHoge.execute(3)
intHoge.hoge(3)

結果

Bool型 : 適合条件なし
String型 : where Value == Stringに適合
Int型 : where Value: Numericに適合

execute
struct HogeImple内の関数
hoge
extention Hoge内の関数

関数 結果
Bool execute Hoge true
Bool hoge Hoge true
String execute Hoge Value
String hoge String Value
Int execute Hoge 3
Int hoge Numeric 3

まとめ

  • 具象型からは extension の適合条件で分岐することができない

これはバグだと思うんですが、どうなんでしょうか?

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

Swift 5.1 Tour まとめ

概要

Swift開発経験0.001程度のプログラマーが少しでもSwift理解を深めるためにA Swift Tourの内容をまとめました。

ただ、そもそもツアーだけでは理解をすることができない点が多々あったので、自分で追記したり、参考記事のリンクを添付しています。
そのため完全なSwiftTourのただのまとめではなく自分なりにアレンジを加えたものになります。
まとめといったな?あれは嘘だ

※Tourで使われているコードはそのまま添付しています。検証の際はMyPlaygroundのご利用をおすすめします。

本文

Simple Values

変数と定数

変数はvarで、定数はletで定義する。

// 変数を定義
var myVariable = 42
myVariable = 50 // 上書き

// 定数を定義
let myConstant = 42
myConstant = 11 // エラーになる

以下の形式で、変数や定数に型を明示することができる。

let explicitDouble: Double = 70 // コロンの後のDoubleが型の定義。

※型の一覧と意味はこちらを参照。

下記のように変数の結合で、型がマッチしていない場合、エラーになるので注意。

let label = "The width is " // 文字列はダブルクオーテーションで囲む
let width = 94
let widthLabel = label + String(width) // widthがInt型のため、string型に変換

文字列内で変数を展開する場合は\()で変数を囲う。

let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

配列と辞書

配列型と辞書型の値は[]で囲う。 (配列と辞書について)

// 配列
var shoppingList = ["catfish", "water", "tulips"]
shoppingList[1] = "bottle of water"
// 配列に追加
shoppingList.append("blue paint")

// 辞書
var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
]
// 辞書の追加
occupations["Jayne"] = "Public Relations"

※辞書型はPHPでいう連想配列といったところか。
※辞書型についてはこちらも参照。

空の配列型、辞書型の定義もできる。

// 空配列
let emptyArray = [String]() // 値の型を定義
// 空辞書
let emptyDictionary = [String: Float]() // キーと値の型をそれぞれ定義

すでに定義されている配列・辞書の初期化は以下のように書く。

// 配列の初期化
shoppingList = []
// 辞書の初期化
occupations = [:]

Control Flow

条件はifswitchで、ループはfor-inwhilerepeat-whileで書く。

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
// inの後にループさせる変数を置く
// forの後のteamScoreにループの結果取得できる値が格納される
for score in individualScores { 
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
print(teamScore)
// Prints "11"

詳しくはこちらも参照。

オプショナル型とif let

オプショナル型は、変数の型のひとつで、nilの可能性がある値を指す。
nilが考えられるような変数をチェックする場合、if letを使うと良い。

var optionalName: String? = "John Appleseed" // オプショナル型変数
var greeting = "Hello!"
// optionalNameがnilのとき、条件節内は通らない。(falseのため)
if let name = optionalName {
    greeting = "Hello, \(name)"
}

optionalNameの定義で使われているStringの後の?で、オプショナル型を定義。

下記のように??を使って文字列内でnilかどうかの判定をすることもできる。

let nickName: String? = nil
let fullName: String = "John Appleseed"
// nickNameがnilなら、fullNameが使用される
let informalGreeting = "Hi \(nickName ?? fullName)"

switch構文

switch caseを使えば、いろいろなかたちでの比較ができる。

let vegetable = "red pepper"
switch vegetable {

    // 文字列の比較。一致していたら「Add some raisins and make ants on a log.」が表示される。
    case "celery":
        print("Add some raisins and make ants on a log.")

    // 同じ結果を出力する場合、比較対象をカンマで区切って書くこともできる。
    case "cucumber", "watercress":
        print("That would make a good tea sandwich.")

    // 変数xが「pepper」という文字を含む場合、letの後の定数xに「pepper」を格納する
    case let x where x.hasSuffix("pepper"):
        print("Is it a spicy \(x)?")

    // defaultを設定しないとエラーになる
    default:
        print("Everything tastes good in soup.")
}
// Prints "Is it a spicy red pepper?"
// 上記いずれかのcaseに合致すれば、switch構文は終了する。
// ※caseに一致しない場合、defaultが出力される。
// (続くcaseが比較されることはない)

for-in構文

for inを使えば、ループ対象の配列・辞書のキーと値のそれぞれを取り出せる。

// 辞書型
let interestingNumbers = [
    "Prime": [2, 3, 5, 7, 11, 13],
    "Fibonacci": [1, 1, 2, 3, 5, 8],
    "Square": [1, 4, 9, 16, 25],
]
var largest = 0
// kindで「Prime」などのキーを、numbersで配列を取り出せる
for (kind, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}
print(largest)
// Prints "25"

while構文

while構文は、while後に書かれた条件に該当する限り構文内を通る。

var n = 2
while n < 100 {
    n *= 2
}
print(n)
// Prints "128"

for 変数 in 開始値 ..< 終了値で、+1ずつ変数に格納され、変数が終了値と同値の場合、条件節内を通らない。

var total = 0
for i in 0..<4 {
    total += i
}
print(total)
// Prints "6"

for 変数 in 開始値 ... 終了値は、終了値と同値でも条件節内を通り、超過した場合falseとなり、条件節内を通らない。

Functions and Closures

functionはfuncで始めて定義する。

func greet(person: String, day: String) -> String {
    return "Hello \(person), today is \(day)."
}
// functionの実装
greet(person: "Bob", day: "Tuesday")

persondayが引数にあたるラベルであり、それぞれのラベルのコロンの後のStringが値の型を表す。
->後のStringは、返り値(戻り値)の型を表す。
また、_のみ記述してラベルを省略することも可能。

func greet(_ person: String, on day: String) -> String {
    return "Hello \(person), today is \(day)."
}
// 「_」を用いることで、第一引数はラベルが不要。
greet("John", on: "Wednesday")

タプル

返り値はタプルを使って複数指定することも可能。

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
    var min = scores[0]
    var max = scores[0]
    var sum = 0

    for score in scores {
        if score > max {
            max = score
        } else if score < min {
            min = score
        }
        sum += score
    }

    return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
// Prints "120"
print(statistics.2)
// Prints "120"

ネスト

関数をネスト(入れ子)構造で書くこともできる。

func returnFifteen() -> Int {
    var y = 10
    func add() {
        // 上位の関数で定義された変数にアクセスすることもできる
        y += 5
    }
    add()
    return y
}
print(returnFifteen())
// 15

ファーストクラスオブジェクト

Swiftでは関数は「ファーストクラスオブジェクト」として定義されている。
よって、値として関数を用いることもできる。

func makeIncrementer() -> ((Int) -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
increment(7)

また、引数に関数を入れることもできる。

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
    for item in list {
        if condition(item) {
            return true
        }
    }
    return false
}
func lessThanTen(number: Int) -> Bool {
    return number < 10
}
var numbers = [20, 19, 7, 12]
// 第二引数でlessThanTenという関数を用いている
hasAnyMatches(list: numbers, condition: lessThanTen)

クロージャ

クロージャは、たとえば次のように書く。

var numbers = [20, 19, 7, 12]
let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)
// Prints "[60, 57, 21, 36]"

他にも、inなどを省略することができる。

var numbers = [20, 19, 7, 12]
let sortedNumbers = numbers.sorted { $0 > $1 } // $0と$1については下記のクロージャの記法について参照を
print(sortedNumbers)
// Prints "[20, 19, 12, 7]"

※クロージャの記法についてはこちらを参照

Objects and Classes

クラスの定義はclassで始める。

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

定義したクラスは以下のように用いる。

var shape = Shape()
// ShapeクラスのnumberOfSides変数に 7 を格納
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()
print(shapeDescription)
// A shape with 7 sides.

イニシャライザ

インスタンスの生成時、イニシャライザinitでの初期化をする必要がある。
※その理由についてはこちら

class NamedShape {
    var numberOfSides: Int = 0
    var name: String
    // name初期化 イニシャライザのため、funcは不要
    init(name: String) {
        // selfで自クラス内の変数を指示する。(initのnameを指していない)
        self.name = name
    }

    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

オブジェクトのクリーンアップ処理をする場合、deinitデイニシャライザを生成する。

継承

継承の概念があり、親クラスの属性を引き継ぐためには、子クラスの後に: 親クラスを記載する。

// SquareがNamedShapeクラスを継承
class Square: NamedShape {
    var sideLength: Double

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 4
    }

    func area() -> Double {
        return sideLength * sideLength
    }

    override func simpleDescription() -> String {
        return "A square with sides of length \(sideLength)."
    }
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()

親クラスにある関数と同一の関数名を用いる場合、overrideをつけて定義。(なければエラーになる。)

ゲッター・セッター

ゲッターとセッターを利用することもできる。

class EquilateralTriangle: NamedShape {
    var sideLength: Double = 0.0

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 3
    }

    var perimeter: Double {
        // 値の取得
        get {
            return 3.0 * sideLength
        }
        // 値のセット
        set {
            sideLength = newValue / 3.0
        }
    }

    override func simpleDescription() -> String {
        return "An equilateral triangle with sides of length \(sideLength)."
    }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
// Prints "9.3" ゲッター
triangle.perimeter = 9.9
print(triangle.sideLength)
// Prints "3.3000000000000003" セッター

上記のEquilateralTriangleクラスでは、3つのステップが踏まれている。

  • サブクラスが宣言するプロパティの値をセット
  • サブクラスのイニシャライザの呼び出し
  • スーパークラス(親クラス)で定義されたプロパティの値を変更。

willSet と didSet

willSet didSetでプロパティの変更前、変更後で何らかの処理を行わせることができます。

class TriangleAndSquare {
    // 先のEquilateralTriangleクラスを定義
    var triangle: EquilateralTriangle {
        willSet {
            // newValueはwillSetの定数
            square.sideLength = newValue.sideLength
        }
    }
    // 先のSquareクラスを定義
    var square: Square {
        didSet {
            // oldValueはdidSetの定数
            triangle.sideLength = oldValue.sideLength
        }
    }
    init(size: Double, name: String) {
        square = Square(sideLength: size, name: name)
        triangle = EquilateralTriangle(sideLength: size, name: name)
    }
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
// Prints "10.0"
print(triangleAndSquare.triangle.sideLength)
// Prints "10.0"
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)
// Prints "10.0"

結果がオプショナル型の場合があるとき、Square?のように?をつける。
オプショナル型の変数ないし定数を利用する際は、optionalSquare?のように対象のプロパティの後に?をつける。

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

Enumerations and Structures

enum

列挙型を書く際は、enumで始める。

enum Rank: Int {
    case ace = 1
    case two, three, four, five, six, seven, eight, nine, ten
    case jack, queen, king
    // 列挙型でも関数を書くこともできる
    func simpleDescription() -> String {
        switch self {
        case .ace:
            return "ace"
        case .jack:
            return "jack"
        case .queen:
            return "queen"
        case .king:
            return "king"
        default:
            return String(self.rawValue)
        }
    }
}

let ace = Rank.ace
print(ace)
// ace

let aceRawValue = ace.rawValue // rawValueで要素の値を取得
print(aceRawValue)
// 1

列の要素それぞれの値は、先頭から012...というように順番に割り当てられる。
(例:case a, b, cという列なら、aには0が、bには1cには2が割り当てられる。)

ただ、case ace = 1のように要素に対して明示的に値を割り当てることによって、要素の値を変更することができる。
(例:case ace = 1では明示的に1を要素の値に割り当てていますが、そうでなければ0が入る。)

また、続く要素の値は2、3...というように明示された値に続く。

if let convertedRank = Rank(rawValue: 3) {
    let threeDescription = convertedRank.simpleDescription()
}

上記のようにインスタンス生成時にrawValueを指定して列挙子を取得する際、自動的にイニシャライザーinit(rawValue:)が付与される。

enum Suit {
    case spades, hearts, diamonds, clubs

    func simpleDescription() -> String {
        switch self {
        case .spades:
            return "spades"
        case .hearts:
            return "hearts"
        case .diamonds:
            return "diamonds"
        case .clubs:
            return "clubs"
        }
    }
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()

自クラス外の特定の要素にアクセスする場合は、Suit.heartsのように要素の前にSuitをつける必要があるが、クラス内では.heartsのようにクラスを省略できる。
switch文のselfで、引数自体にアクセスできる。

下記のように要素を定数に格納し、要素と一致する値を抽出することもできる。

enum ServerResponse {
    case result(String, String)
    case failure(String)
}

let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")

switch success {
case let .result(sunrise, sunset):
    print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
    print("Failure...  \(message)")
}
// Prints "Sunrise is at 6:00 am and sunset is at 8:09 pm."

構造体

structをつけて構造体を生成できる。イニシャライザを持つことができるが、継承することはできず、クラスが参照型であるのに対し、構造体は値型。

struct Card {
    var rank: Rank
    var suit: Suit
    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()

Protocols and Extensions

プロトコルは以下のように定義する。

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

クラスも列挙も構造体もプロトコルを継承することができる。

class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    var anotherProperty: Int = 69105
    func adjust() {
        simpleDescription += "  Now 100% adjusted."
    }
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription

mutatingstructenumにおいて自身の値を変更する場合にfuncの前につける。(変更できることを明示する必要がある)
classではつける必要がない。クラスは常に変更できるから。

extension

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}
print(7.simpleDescription)
// Prints "The number 7"

extensionをつけると既存の機能を拡張できる。
上記の例でいうと、ExampleProtocolの型プロパティをIntで拡張し、adjustメソッドをExampleProtocolに追加している。

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// Prints "A very simple class.  Now 100% adjusted."
// print(protocolValue.anotherProperty)  // Uncomment to see the error

プロトコル名は型としても利用できる。
また上記の例では、protocolValue変数はすでにSimpleClassクラスで利用されているが、コンパイラがExampleProtocolの変数として認識するため問題ない。クラスが実装するプロパティにアクセスできない。

Error Handling

エラーを取得する際はErrorプロトコルを用いる。

enum PrinterError: Error {
    case outOfPaper
    case noToner
    case onFire
}

エラーハンドリングはthrowを用い、エラーハンドリングをする関数を用いる場合はthrowsを用いる。

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

下記の例ではIf節を通った場合、即座にエラーを返す。

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

エラーハンドリングの方法は他にもある。

do-catch

do {
    let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
    print(printerResponse)
} catch {
    print(error)
}
// Prints "Job sent"

do-catchでは、doブロック内でtry文を書くことで、エラー時にcatchのエラーコードが自動的に出力される。

下記のように複数のcatchブロックを用意することも可能。


do {
    let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
    print(printerResponse)
} catch PrinterError.onFire {
    print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
    print("Printer error: \(printerError).")
} catch {
    print(error)
}
// Prints "Job sent"

try?

try?と書けばオプショナル型で結果を返せる。関数がエラーを返せば、結果ではnilが返る。

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

defer

deferを使ってブロック要素を書くことで最終的には関数内のコードをすべて通す。(returnするまで)

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String) -> Bool {
    fridgeIsOpen = true
    defer {
        fridgeIsOpen = false
    }

    let result = fridgeContent.contains(food)
    return result
}
fridgeContains("banana")
print(fridgeIsOpen)
// Prints "false"

Generics

ジェネリクスで、IntやStringなどの予め指定された型とは異なり、任意の指定した型パラメータを用いることができる。
<>(山括弧)で括ることで、ジェネリクスの関数・型を定義することができる。
※下記でいうと<Item>がジェネリクス。

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
    var result = [Item]()
    for _ in 0..<numberOfTimes {
        result.append(item)
    }
    return result
}
makeArray(repeating: "knock", numberOfTimes: 4)

ジェネリクスは、クラス、列挙、構造体でも用いることができる。

// Reimplement the Swift standard library's optional type
enum OptionalValue<Wrapped> {
    case none
    case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)

where

下記のようにwhereを関数内部の前に記述することで、2つの型が同じであること、またはあるクラスがスーパークラスを継承していることを明示することができる。

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
    where T.Element: Equatable, T.Element == U.Element
{
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
    return false
}
anyCommonElements([1, 2, 3], [3])

<T: Equatable>と、<T> ... where T: Equatableは同じ意味。

End

最後に

先人の知恵を勝手にたくさん拝借しました。ありがとうございます。
(リンク先の記事がどれもわかりやすく、理解に際して非常にありがたかったです…)

修正や参考資料などございましたらご指定いただけますと幸いです。

ご覧くださいましてありがとうございました。

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

AWS Amplifyを使ってCognito User PoolsログインするSwiftUIのサンプル

はじめに

最近Amplifyを利用し始めたのですが、Amplify CLIでちょっとコマンドを実行するだけで、アプリからAWSのリソースを利用できるようになり、すごく便利に感じています。

ここでは、Amplifyを使って、Cognito User Poolsログインする処理をSwiftUIで行う方法を説明します。

ドキュメントには、ログインやログアウトなど一つ一つの例はあるのですが、具体的にアプリにどのように組み込むかまでは記載されていません。そこで、このサンプルが具体的にアプリにどう組み込むか悩んでいる方に役に立つのではないかと思い、書きました。

前提

バージョンは次の通りです。

  • Xcode 11.2.1
  • Amplify iOS SDK 2.0

また、この記事では、サインインの処理を具体的にどう組み込むかのみを説明します。

CocoaPodsに依存関係を設定して、$ amplify add authして、...のような基本的な手順は割愛します。基本的な利用手順については、以下のドキュメントをざっと読んでいただければと思います。
https://aws-amplify.github.io/docs/sdk/ios/authentication

イメージ

アプリを起動して、ユーザーがログイン済で出ない場合、ログイン画面を開きます。
ログインしたら、ホーム画面に遷移します。

ログインサンプル.gif

Podfile

target 'MyApp' do
  use_frameworks!

  pod 'AWSMobileClient', '~> 2.12.0'

  target 'MyAppTests' do
    inherit! :search_paths
  end

  target 'MyAppUITests' do
  end

end

SceneDelegate

ポイントは以下の2つです。

  • AWSMobileClient.initialize(completionHandler:)でAWSMoblieClientを初期化し、結果に応じてホーム画面またはログイン画面を開く
  • AWSMobileClient.addStateListener(object:callback:)でuserStateをobserveし、userStateの変化に応じてホーム画面またはログイン画面を開く
SceneDelegate.swift
import UIKit
import SwiftUI

import AWSAppSync
import AWSMobileClient

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)

            AWSMobileClient.default().initialize { [weak self, window] (userState, error) in
                guard let self = self else { return }

                if let error = error {
                    print("### AWSMobileClient.initialize.error", error.localizedDescription)
                    return
                }

                if let userState = userState {
                    switch (userState) {
                    case .signedIn:
                        self.showHomeView(in: window) // ホームを開く
                    case .signedOut:
                        self.showSignInView(in: window) // ログインページを開く
                    default:
                        AWSMobileClient.default().signOut()
                        self.showSignInView(in: window) // ログインページを開く
                    }
                }
            }

            AWSMobileClient.default().addUserStateListener(self) { [weak self, window] (userState, info) in
                guard let self = self else { return }

                switch (userState) {
                case .signedIn:
                    self.showHomeView(in: window) // ホームを開く
                case .signedOut, .signedOutUserPoolsTokenInvalid:
                    self.showSignInView(in: window) // ログインページを開く
                default:
                    AWSMobileClient.default().signOut()
                    self.showSignInView(in: window) // ログインページを開く
                }
            }
        }
    }

    private func showHomeView(in window: UIWindow) {
        DispatchQueue.main.async {
            window.rootViewController = UIHostingController(rootView: HomeView())
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    private func showSignInView(in window: UIWindow) {
        DispatchQueue.main.async {
            window.rootViewController = UIHostingController(rootView: SignInView(viewModel: SignInViewModel()))
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

SignInView

パスワードの初期化とサインアップは割愛させていただきます。

SignInView.swift
import SwiftUI

struct SignInView: View {
    @ObservedObject var viewModel: SignInViewModel
    var body: some View {
        VStack(spacing: 20) {
            Spacer()

            Text("MyApp")
                .bold()
                .font(.title)

            Spacer()

            TextField("Username", text: $viewModel.userName)
                .autocapitalization(.none)
                .padding()
                .overlay(
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(Color.gray, lineWidth: 1)
                )

            SecureField("Password", text: $viewModel.password)
                .padding()
                .overlay(
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(Color.gray, lineWidth: 1)
                )

            Button(action: {
                print("### signinButton did tap")
                self.viewModel.signIn()
            }) {
                HStack {
                    Spacer()
                    Text("Sign in")
                    Spacer()
                }
                .padding()
            }
            .accentColor(.white)
            .background(Color.green)
            .cornerRadius(6)
            .alert(isPresented: $viewModel.showAlert) {
                Alert(title: Text("Sign in error"), message: Text(viewModel.errorMessage))
            }

            HStack {
                Spacer()
                Button(action: {
                    fatalError("forget password hasn't be implemented.") // 省略
                }) {
                    Text("Forget password?")
                }
            }

            Spacer()

            HStack {
                Spacer()
                Text("Don't have an account?")
                Button(action: {
                    fatalError("sign up hasn't be implemented.") // 省略
                }) {
                    Text("Sign up")
                }
                Spacer()
            }

            Spacer()
        }
        .padding(EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15))
    }
}

struct SignInView_Previews: PreviewProvider {
    static var previews: some View {
        SignInView(viewModel: SignInViewModel())
    }
}

SignInViewModel

SignInViewModel.swift
import Foundation

import AWSMobileClient

class SignInViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var password: String = ""
    @Published var showAlert = false
    @Published var errorMessage: String = ""

    func signIn() {
        AWSMobileClient.default().signIn(username: userName, password: password) { (result, error) in
            if let error = error {
                print("### signin error:", error.localizedDescription)
                self.showAlert = true
                self.errorMessage = error.localizedDescription
            }

            print("### Signin success")
        }
    }
}

HomeView

HomeView.swift
import SwiftUI

struct HomeView: View {
    var body: some View {
        Text("Home")
            .font(.title)
    }
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView()
    }
}

以上です。

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

GithubActionsでプライベートリポジトリをsubmoduleとして取り込む

GithubActionsでプライベートリポジトリをsubmoduleとして取り込む方法はsubmodule addコマンドでhttps方式で追加したかssh方式で追加したかによって変わります。それぞれのメリットデメリットを説明しつつ、実現方法を記しておきます。

https方式
git submodule add https://github.com/u-nation/GITHUB_REPOSITORY.git
ssh方式
git submodule add git@github.com:u-nation/GITHUB_REPOSITORY.git

https方式

手軽なのはhttps方式です。
以下の権限を持つパーソナルアクセストークンを作成し、secretsに登録してワークフロー内でwithオプションに渡してあげるだけで済みます。
※パーソナルアクセストークンはhttps方式しか使えません1

.github/workflows/sample.yml
      - name: submodule
        uses: actions/checkout@v1
        with:
          submodules: true
          token: ${{ secrets.PERSONAL_TOKEN }}
  • メリット 手軽さ
  • デメリット パーソナルアクセストークンは発行したアカウントがアクセスできるリポジトリ全てに権限が及ぶのでトークンが漏れた場合のセキリュティリスクが大きい。Bot用のアカウントを作成やトークン作成の依頼・共有など運用がしんどい。

ssh方式

  1. ssh-keygenコマンドで秘密鍵公開鍵を作成
  2. submodule先のリポジトリのDeploy Keysに公開鍵を登録(必要に応じてwrite権限を付与)
  3. 取り込む方のリポジトリのsecretsに秘密鍵を登録
  4. ワークフローでsshの設定とsubmodule update --initをする
.github/workflows/sample.yml
      - uses: actions/checkout@v2
      - name: SSH Setting
        env:
          TOKEN: ${{ secrets.TOKEN }}
        run: |
          mkdir -p /home/runner/.ssh/
          echo -e "$TOKEN" > /home/runner/.ssh/id_rsa
          chmod 600 /home/runner/.ssh/id_rsa
      # https://github.com/actions/checkout#checkout-submodules
      - name: Checkout submodules
        shell: bash
        run: |
          git submodule sync --recursive
          git submodule update --init --force --recursive
  • メリット 鍵の権限はそのリポジトリに限定されるので漏れた時のセキュリティリスクが軽減される。必要に応じて各々が鍵の設定を追加できるので運用が楽。
  • デメリット 鍵作ったり設定するのがちょっとだけめんどい。

所感

企業でGithubを使う場合は大抵プライベートなリポジトリだと思うので、誰かのお役に立てると嬉しいです。

ssh方式を実現するのに6時間位ハマって辛かったですが、運用が楽になるのでssh方式がオススメです。

注釈

注釈


  1. GithubActionsに移行する前は、AWSのCodeBuildを使用していました(GithubAcitonsの方がめちゃくちゃ早いです)。その時は秘密鍵をSystem Managerで用意してssh方式をしていましたが、途中でhttps方式のsubmoduleオプションが登場しました。 

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

FeliCa が遅いしフルスキャンも不可能 後編【iOS 13 Core NFC】

iOS 13 Core NFC での FeliCa に関する制限を紹介した前編、今回はそれの続きです。

iOS での FeliCa の制限

  • FeliCa の読み取りで得られる最初の currentSystemCode は Info.plist での FeliCa システムコードの記述する順番に依存する → 前編で紹介
  • FeliCa の読み取り速度は Info.plist での FeliCa システムコードの記述する順番に依存する → 前編で紹介
  • FeliCa のフルスキャンを iOS で行いたい場合、「じゃあ Info.plist に存在する全ての FeliCa システムコードを記載すればいいのでは…?」と考えつくが、それは現実的な解決策ではない

対応する FeliCa システムコードを増やすと最悪どれくらい遅くなるのか

前編では

iOS 13 の Core NFC の場合は検出された FeliCa カードにあるシステムのうち、Info.plist に記載され 最も順番が前にある FeliCa システムコードに一致するシステム が検出、currentSystemCode および currentIDm にはそれが入ることになります。

そのため、多数の FeliCa システムコードに対応しようとした場合、Info.plist に記載する順番が非常に重要になります。

と記事の最後に述べました。では具体的にどれくらい速度に変化があるのでしょうか。実際に測定してみます。

ここでの "読み取り速度" の定義

そもそも FeliCa は NFC の中で読み取り速度がめちゃ速いです。NFC-B の運転免許証の読み取りに比べても速度が段違いです。

しかし、iOS での Core NFC で私が述べたい "速度" はこのことではありません。

事例紹介をスキップして早く本題に行きたい方はこちらからジャンプ

各 App での読み取り時間に関する表示

iOS 13 のリリース以降、多くの電子マネーカードリーダー系の App が登場しましたが、各 App のスキャン画面等にこんな記載がありませんか?

Japan NFC Reader ICリーダー CardPort
IMG_6487.jpg IMG_6486.jpg IMG_6485.jpg
読み取りに時間がかかる場合があります。 読み込みまで少し時間がかかることがあります。 読み取りには時間がかかることがあります。

(個人的)ド定番 iOS カードリーダー 御三家のどれもに 読み取りに時間がかかる という記載があります。
本記事を読んでいただくと、なぜわざわざこのように表示しているのかがわかり、また文脈としては ICリーダー さんの 読み込みまで少し時間がかかることがある が最も正しい表現であることがわかると思います。

Japan NFC Reader の場合、このあと次のような表示になります。

読み取り中…
…ここで思うことはありませんか?別に「読み取り中…」の文言が表示されるなら、読み取り開始前の画面にわざわざ「読み取りに時間がかかる」ことを表示しなくてもいいんじゃないでしょうか。これは各 App 開発者の親切心で……?

……(少なからず親切心もあるでしょうが)それは違います。

コードを見てみる

ここで少しコードを見てみることにしましょう。

var session: NFCTagReaderSession?

// …

func scan() {
    // …
    self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
    self.session?.alertMessage = "カードの上に iPhone の上部を載せてください。読み取りまでに時間がかかることがあります。"
    self.session?.begin()
}

// …

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    let tag = tags.first!
    session.connect(to: tag) { (error) in
        // …
        session.alertMessage = "カードを読み取っています…"
        // …
    }
}

App がカードのスキャンを行えるようになった後に表示されるメッセージは、scan() の中にある session.alertMessage です。そして、実際にカードを検出し、プログラム側にその情報がやってくるのが tagReaderSession(_:didDetect:) の中の session.alertMessage です。

ここで大きな問題になるのが、FeliCa カードに iPhone を載せてから tagReaderSession(_:didDetect:) が呼ばれるまでに、ものすごく時間がかかる場合があるということです。その条件が 検出された FeliCa カードに存在するシステムが、Info.plist に記載されている FeliCa システムコードの順番の後ろの方にあるとき です。

ここでの読み取り速度とは、tagReaderSession(_:didDetect:) が呼ばれるまでの時間のことを示します。

検証 ~ FeliCa システムコードを増やすとどれだけ遅くなるか ~

今回は以下のコードを使って、Dateの差がどれだけ増えるかを見ていきます。

func scan() {
    // …
    self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
    self.session?.alertMessage = "カードの上に iPhone の上部を載せてください。読み取りまでに時間がかかることがあります。"
    self.session?.begin()
}

func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
    self.start = Date()
    print("tagReaderSessionDidBecomeActive(_:)")
}

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    let elapsedTime = Date().timeIntervalSince(self.start)
    print("elapsed time:", elapsedTime)
    // …
    session.alertMessage = "完了"
    session.invalidate()
}

対象とするカード、今回は楽天Edyを用います。このようにカードを iPhone の NFC 読み取り部をピッタリと置いた状態で Scan ボタンをタップします。

tagReaderSession(_:didDetect:)print でコンソールに出力される elapsed time の値がどれだけ変化するか測定します。

楽天Edy カードが持っている FeliCa システムコードは 0x8B610xFE00 です。Info.plist に記述する 0xFE00 の順番を段々と後ろにしていきます。
スクリーンショット 2019-12-22 2.14.49.png
スクリーンショット 2019-12-22 2.38.40.png

結果


このグラフのもとになった表はページ下部に記載しました。
FeliCa システムコードの順番が1つ後ろにずれると約0.3秒、tagReaderSession(_:didDetect:) が呼ばれるまでの時間が延びることがわかりました。

また、NFCReaderSession には時間制限があるのですが、その45秒となる Item 148 = 149 個目よりも FeliCa システムコードを多くしてしまうと、そもそも tagReaderSession(_:didDetect:) が呼ばれないことがわかりました。

各カードが持っている FeliCa システムコードはそれぞれ異なる場合があるので、1つの App で多数のカードに対応させようとすると、それだけ Info.plist に記載しなければならない FeliCa システムコードの数も増え、順番によってカードが検出されるまでの時間が左右されるのは使う側としてとても不便です。
また、tagReaderSession(_:didDetect:) が呼ばれないとプログラム側からもカードが iPhone に載せられているのかすら分からないため、各 App では「読み込みまで時間がかかる」という記載がスキャン前の段階で表示されることにつながっています。

Info.plist に 存在する全ての FeliCa システムコードを記載するのは現実的な解決策ではない

もし、iOS で FeliCa のフルスキャンを行おうとしたときに、0x0001 から 0xFEFE までの全ての FeliCa システムコードをInfo.plist に載せれば…?と思いつきますが、そもそも検出のみで149個が限界なので、それはいい方法とは言えないという結論になりました。

そもそもなぜ iOS (Apple) は検出にこれほど時間をかけるのでしょうか。
「ショートカット」のオートメーションに「NFC タグ」が使えますが、これは Apple 製ということもあってか FeliCa も即検出、登録することができています。確実にサードパーティのみを締め出しているということになりますが……。FeliCa システムの IDm による切り替えができないのと合わせて、理由がわかりません。

iOS 14…ではさらなる Core NFC の機能開放を希望します…。

結果(表)

"Item x" の列は 0 は 0xFE00 を Item 0 に記述した場合、1 は Item 1 に記述した場合…というような感じです。Item 7 = 8個目で2秒を超え始めています。実用ではもっと早く検出されたほうがいいですよね。

Item x 1 2 3 Ave.
0 0.046976 0.045720 0.045037 0.045911
1 0.349753 0.346604 0.345791 0.347383
2 0.647677 0.649241 0.646586 0.647835
3 0.947898 0.950009 0.952775 0.950227
4 1.250441 1.250477 1.250740 1.250553
5 1.550706 1.551337 1.552101 1.551381
6 1.854309 1.857909 1.855477 1.855898
7 2.156484 2.155968 2.155585 2.156012
8 2.459123 2.457329 2.458836 2.458429
9 2.759697 2.762735 2.758815 2.760416
10 3.058142 3.060277 3.059271 3.059230
15 4.566140 4.568311 4.569674 4.568042
20 6.072544 6.079473 6.078423 6.076813
25 7.583499 7.585000 7.584232 7.584244
30 9.094974 9.094320 9.096275 9.095190
40 12.111050 12.109540 12.105498 12.108696
50 15.123403 15.119864 15.120571 15.121279
100 30.202593 30.200774 30.201501 30.201623
101 30.512402 30.505607 30.501605 30.506538
102 30.811206 30.814952 30.807862 30.811340
103 31.109441 31.106589 31.107631 31.107887
140 42.270938 42.267985 42.274794 42.271239
145 43.787478 43.777525 43.775778 43.780260
148 44.683454 44.679594 44.683043 44.682030
149 NaN
150 NaN
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS13で新しく追加されたMetricKitがとてもすごい件

この記事は、ZOZOテクノロジーズ #5AdventCalendar2019の記事です。

昨日は@EnKUMAさんの「GKE上でgcloudコマンドを叩く為のPodをDeployしたら詰まった話」の記事でした。

ZOZOテクノロジーズ iOSエンジニアの@ahiruです。

本稿ではiOS13 Xcode11より新しく追加されたフレームワークの一つであるMetricKitについて調べたことなどをまとめていきます。

WWDC 2019の「Improving Battery Life and Performance」にてMetricKitの詳細が説明されておりますので、このセッションをベースにMetricKitとは何か、どんなことができるのかなどを中心に書いていきます。

What's MetricKit?

Appleのドキュメントを見てみると以下のように記述されています。

Aggregate and analyze per-device reports on power and performance metrics

MetricKitはデバイス上のアプリの電力やパフォーマンスなどのメトリックを受け取るためのフレームワークです。

MetricKitを使うことでバッテリーとパフォーマンスへの影響を定量化できるようになります。

すなわち、アプリのパフォーマンスを向上させる上でどの機能がボトルネックになっているのか、何が原因で起動時間が遅いのかなどの原因の特定をかなり簡単に調べることができるようになりました。

メトリックスの種類

Battery MetricsPerformance Metricsの2のメトリックが存在します。

Battery Metrics

  • Processing
  • Location
  • Display
  • Networking
  • Accessories(Bluetooth)
  • Multi Media
  • Camera

Performance Metrics

  • Hangs
  • Disk
  • Application Launch
  • Memory
  • Custom Intervals

メトリックスを取得するための3つのツール

iOS13 Xcode11よりメトリックスを収集するための3つの機能が追加されました。

下記で紹介する3つの機能をうまく使うことで各開発段階(開発・テスト / ベータ版 / リリース版)のそれぞれでメトリックスを収集することが可能となります。

1. XCTest Metrics

これまでもXCTestからメトリックスを取得することは可能でしたが、取得できるメトリックが増えました。

UIテスト・ユニットテスト両方で使用可能です。

measure(metrics: [XCTMetric], block: () -> Void)

  • 【 iOS13 Xcode11未満 】では実行時間のみが計測可能だった
func testPhotoUploadPerformance() {
        let app = XCUIApplication()
        // 実行にかかる時間を測定
        measure() {
            app.buttons["Apply Effect"].tap()
            app.dialogs["alert"].buttons["OK"].tap()
        }
}
  • 【 iOS13 Xcode11以降 】では取得したいメトリックを指定することが可能 (取得できるメトリックについてはこちらを参照)
func testPhotoUploadPerformance() {
        let app = XCUIApplication()
        // 実行にかかる時間 / メモリ使用量 / CPU使用率 を測定する
        measure(metrics: [XCTClockMetric(),
                          XCTMemoryMetric(application: app),
                          XCTCPUMetric(application: app)]) {
                            app.buttons["Apply Effect"].tap()
                            app.dialogs["alert"].buttons["OK"].tap()
        }
}

testApplicationLaunchTime

また、新しいUIテストの各ターゲットにはアプリケーションの起動テストが自動でテストを追加されるようになりました。

func testApplicationLaunchTime() {
        measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) {
            XCUIApplication().launch()
        }
}

実際にこのテストを実行してみた方はわかると思うのですが、5回くらいアプリが立ち上がります。

その後テストの実行ボタンの下に詳細ボタンが表示されるのでそれをクリックすると下のようなポップアップが表示されます。
スクリーンショット 2019-12-22 2.06.01.png

5回の起動の平均時間がAverageとして表示され、それをBaselineに設定することができます。いわば、そのアプリの起動時間の基準値です。

自身が開発中のアプリに新しい機能の追加などを行った際に、起動時間が基準値よりも遅くなった場合にはその機能のパフォーマンスが適切とは言えません。
パフォーマンスの側面からリグレッションテストが可能となりました。

パフォーマンステストを行う際の注意点として以下の点がセッションの中で挙げられていました。

  • 負荷がかかるのでデバッガーは加えない
  • 全ての診断オプションは切っておく
  • 独立したスキームを使うかテストプラン機能を使うことで診断オプションは簡単にオフにできる

2. MetricKit

メトリックを収集するためのオンデバイスフレームワークです。

各ユーザによるアプリケーション使用時の状況を理解するのに役立ちます。

主にベータ版・リリース版に対して詳細なメトリックスを収集するために使用します。

実装方法

実装方法はとても簡単です。

クラスを作成しメトリックスの受信を許可するだけです。するとデータ(メトリックス)の収集が開始されます。

class MySubscriber: NSObject, MXMetricManagerSubscriber {

    var metricManager: MXMetricManager?
    override init() {
        super.init()

        metricManager = MXMetricManager.shared
        // メトリックスの受信を許可する
        metricManager?.add(self)
    }

    deinit {
        metricManager?.remove(self)
    }

    func didReceive(_ payloads: [MXMetricPayload]) {
        for metricPayload in payloads {
            // 各自、ペイロードをサーバーに送ったり、ファイルに保存したりする
        }
    }
}

MetricKitを使うことでアプリがいつどこでどれくらい使われたのかを簡単に知ることができます。
スクリーンショット 2019-12-22 2.57.58.png
24時間が経過するとそのサマリが作成されてデバイスに返されます。上記の実装をすると1日1回自動でメトリックスの収集が行われます。

mxSignPost

アプリ内のある機能やあるアクションに関するメトリックスを収集することができます。

実装方法はとても簡単で対象の箇所をmxsignPostで囲むだけです。

let photosLogHandle: OSLog = MXMetricManager.makeLogHandle(category: "Photos")

mxSignpost(.begin, log: photosLogHandle, name: "SavePhoto")

// パフォーマンス計測したい処理をここに記述

mxSignpost(.end, log: photosLogHandle, name: "SavePhoto")

上記のように実装すると、MetricKitが自動でメトリックスを収集してくれます。

3. Xcode Metrics Organizer

コードを変更することなくOrganizerから集約されたデータの確認ができます。

主にリリース版で生じた問題を収集するために使用します。

Window -> Organizer -> Metricsのタブより確認ができます。

個人開発のアプリで確認してみた結果がこちらです。(アップデートしたら起動時間が短縮されてて嬉しい...!!!)
スクリーンショット 2019-12-22 3.21.18.png
起動時間やバッテリーの使用量などのデータを見ることができます。

バージョン毎に確認できるので、テストの時はパフォーマンスに問題なかったのにリリース版ではパフォーマンスが悪くなっているなどの確認も簡単に行えます。

いろんなメトリックがグラフで可視化されていて面白いです。

皆さんの携わっているプロダクトでもぜひ見てみてください。

まとめ

WWDC 2019は内容が盛り沢山だったためMetrics周りに言及された国内の記事はあまり見かけませんが、実は結構な進化を遂げていました。

アプリのパフォーマンス向上はアプリ開発者なら誰しもが意識するところだと思います。

XCTestでいろんなメトリックからパフォーマンス計測を簡単に行えたり、MetricKitを使ってリリース版のメトリックスを簡単に取得できるのはハイクオリティのアプリケーションを作る上でとても大きな恩恵だと思います。

MetricKitを使って質の高いアプリを作っていきましょう!!

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

SwiftUIとSpriteKitでクリスマスツリー作る

こんにちは、
オークファンAdvent Calendar 24日担当のRickyです。

19新卒として入社して半年強が経ちました。
先週人生初の日本忘年会を体験しました。

そういえば明日クリスマスですね。
この前Advent Calendar 12日の記事を書き終わったら、
うちの同期に24日の記事でなんかクリスマスに関する記事を書いてくださいよと
煽られましたから今日はSwiftUIとSpriteKitでクリスマスツリーを作りましょう。:fist:

ちなみに、
持っているMacBookAirのOSは10.14.6なのでSwiftUIのCanvasは使えない。。。。

環境

Xcode 11.2
Swift 5
IOS 13.2
スクリーンショット 2019-12-22 2.00.46.png
スクリーンショット 2019-12-22 2.01.07.png

流れ

1、雪背景SpriteKitParticleとイリュミネイションSpriteKitParticle作成
2、雪のUIView作成
3、クリスマスツリーUIView作成
4、ギフトUIView作成
5、文字UIView作成
6、SwiftUIのContentViewに纏める

1、雪背景SpriteKitParticleライトイリュミネイションSpriteKitParticle作成

SnowParticle.sks作成

スクリーンショット 2019-12-22 0.50.10.png
スクリーンショット 2019-12-22 0.49.26.png
スクリーンショット 2019-12-22 0.51.54.png
スクリーンショット 2019-12-22 2.26.07.png

StarParticle.sks作成

スクリーンショット 2![スクリーンショット 2019-12-22 0.51.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538468/cdd7da74-99e8-10f5-20c3-2be924ee1856.png)<br>
019-12-22 0.50.10.png
スクリーンショット 2019-12-22 0.49.26.png
スクリーンショット 2019-12-22 0.50.43.png

値設定

スクリーンショット 2019-12-22 0.51.27.png
スクリーンショット 2019-12-22 2.25.56.png

Light1Particle.sks作成

スクリーンショット 2![スクリーンショット 2019-12-22 0.51.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538468/cdd7da74-99e8-10f5-20c3-2be924ee1856.png)<br>
019-12-22 0.50.10.png
スクリーンショット 2019-12-22 0.49.26.png
スクリーンショット 2019-12-22 0.50.43.png

値設定

スクリーンショット 2019-12-22 0.51.08.png
スクリーンショット 2019-12-22 2.25.29.png

Light2Particle.sks作成

スクリーンショット 2![スクリーンショット 2019-12-22 0.51.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538468/cdd7da74-99e8-10f5-20c3-2be924ee1856.png)<br>
019-12-22 0.50.10.png
スクリーンショット 2019-12-22 0.49.26.png
スクリーンショット 2019-12-22 0.50.43.png

値設定

スクリーンショット 2019-12-22 0.51.17.png
スクリーンショット 2019-12-22 2.25.44.png

2、雪のUIView作成

スクリーンショット 2019-12-22 0.50.10.png
スクリーンショット 2019-12-22 1.28.51.png

SnowBackgroundView.swift
import SwiftUI
import SpriteKit

struct SnowBackgroundView: UIViewRepresentable {
       class Coordinator: NSObject {
          var scene: SKScene?
       }
       func makeCoordinator() -> Coordinator {
           return Coordinator()
       }
       func makeUIView(context: Context) -> SKView {
           //SkView作成
           let skView = SKView()
           //FPSの表示
           skView.showsFPS = true
           //ノード数表示
           skView.showsNodeCount = true
           //シーン作成
           let scene = SKScene()
           //spriteの中心を設定する
           scene.anchorPoint = CGPoint(x: 0.5, y: 1)
           //ノードを作成
           let emitterNode = SKEmitterNode(fileNamed: "SnowParticle")
           //シーンにノード入れる
           scene.addChild(emitterNode!)
           scene.scaleMode = .resizeFill
           //シーンをこのviewの内容に設定
           context.coordinator.scene = scene

           return skView
       }

       func updateUIView(_ view: SKView, context: Context) {

          view.presentScene(context.coordinator.scene)
       }
}

struct SnowBackgroundView_Previews: PreviewProvider {
    static var previews: some View {
        SnowBackgroundView()
    }
}

3、クリスマスツリーUIView作成

スクリーンショット 2019-12-22 0.50.10.png
スクリーンショット 2019-12-22 1.28.51.png

TreeView.swift
import SwiftUI
import SpriteKit

struct TreeView: UIViewRepresentable {
       class Coordinator: NSObject {var scene: SKScene?
       }
       func makeCoordinator() -> Coordinator {
           return Coordinator()
       }
       func makeUIView(context: Context) -> SKView {
           //SkView作成
           let skView = SKView()
           //シーン作成
           let scene = SKScene()
           //クリスマスてツリーノードを作成
           let treeNode = SKSpriteNode(imageNamed: "tree")
           treeNode.position = CGPoint(x: UIScreen.main.bounds.size.width/2, y: UIScreen.main.bounds.size.height/3)
           //スターノードを作成
           let starNode = SKSpriteNode(fileNamed: "StarParticle")
           starNode?.position = CGPoint(x: 0, y: treeNode.position.y-50)
           //ライトノードを作成
           let lightNode1 = SKSpriteNode(fileNamed: "Light1Particle")
           lightNode1?.position = CGPoint(x: 0, y: starNode!.position.y-120)
           let lightNode2 = SKSpriteNode(fileNamed: "Light2Particle")
           lightNode2?.position = CGPoint(x: 0, y: starNode!.position.y-200)
           //クリスマスツリーノードにスターノードとライトノードを入れる
           treeNode.addChild(starNode!)
           treeNode.addChild(lightNode1!)
           treeNode.addChild(lightNode2!)
           //シーンクリスマスツリーノードを入れる
           scene.addChild(treeNode)
           scene.scaleMode = .resizeFill
           //シーン背景色を透明で黒色にする
           scene.backgroundColor = UIColor.init(displayP3Red: 0, green: 0, blue: 0, alpha: 0.1)
           //シーンをこのviewの内容に設定
           context.coordinator.scene = scene
           return skView
       }

       func updateUIView(_ view: SKView, context: Context) {
          view.presentScene(context.coordinator.scene)
       }
}

struct TreeView_Previews: PreviewProvider {
    static var previews: some View {
       TreeView()
    }
}

4、ギフトUIView作成

スクリーンショット 2019-12-22 0.50.10.png
スクリーンショット 2019-12-22 1.28.51.png

GiftView.swift
import SwiftUI

struct GiftView: View {
    var body: some View {
        ZStack{
            //重ねるギフトの絵作成
            Image("gift")
           .position(x: 130, y: 440)
            Image("gift1")
           .position(x: 150, y: 450)
            Image("gift2")
           .position(x: 220, y: 450)
        }
    }
}

struct GiftView_Previews: PreviewProvider {
    static var previews: some View {
        GiftView()
    }
}

5、文字UIView作成

スクリーンショット 2019-12-22 0.50.10.png
スクリーンショット 2019-12-22 1.28.51.png

TextView.swift
import SwiftUI

struct TextView: View {
    var body: some View {
        //文字作成
        Text("Merry Christmas")
            //フォント設定
            .font(.largeTitle)
            //フォント太さ設定
            .fontWeight(.bold)
            .padding()
            //文字の色
            .foregroundColor(Color.white)
            //枠の線
            .background(Color.red)
            //文字と距離設定
            .padding(10)
            //枠の線の太さと色設定
            .border(Color.gray, width: 5)

    }
}

struct TextView_Previews: PreviewProvider {
    static var previews: some View {
        TextView()
    }
}

6、SwiftUIのContentViewにまとめる

スクリーンショット 2019-12-22 0.50.10.png
スクリーンショット 2019-12-22 1.28.51.png

ContrntView.swift
import UIKit
import SwiftUI
import SpriteKit
struct ContentView: View {
    //@Stateプロパティを使ってUIの状態自動に同期
    @State private var displayTree = false

    var body: some View {
        //Z軸にビューを並べるコンポーネント
        ZStack{
            //縦軸にビューを並べるコンポーネント
            VStack{
                if(displayTree) {
                    ZStack{
                        //クリスマスツリーとギフトviewを呼び出す
                        TreeView()
                        GiftView()
                    }
                }
                Button(action: {
                    //ON/OFFを制御するコンポーネント
                    self.displayTree.toggle()
                }, label: {
                       TextView()
                })
            }
            //雪のviewを呼び出す
            SnowBackgroundView()
            //雪のviewを半透明にする
            .opacity(0.5)
        }.background(SwiftUI.Color.black.edgesIgnoringSafeArea(.all))

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()

    }
}

Merry Christmas :santa:

イメージ.gif

ソースコード

https://github.com/Ricky-yu/XmasTree/tree/master

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

ARでマンガを覗き読み【iOS】

はじめに

AR、いいですよね。素敵な世界。
マンガ、いいですよね。素敵な世界。

ということで、今回はiOSのARKitを利用して、紙のマンガを覗き読みできるアプリ
nekonote1-compressor.gif

を実装してみました。

デモに利用させていただいているマンガは、
マンガボックスで連載中の「ネコの手、借りてます。」?
作家の遥那もより様から許可をいただき動画掲載させていただいてます???
最高オブ最高の癒しマンガなのでみなさん是非読んでください???

いつでも、どこでも、だれもが、この素敵な世界を実現できるよう、
実装を、コメント多めで紹介します✨

前準備

1.お高めなiPhone(A9以降のプロセッサを搭載したもの)を購入?します

2.プロジェクトを作ります
[ File > New > Project > Single View App ]

3.Storyboard に ARSCNView を貼り付け、ViewController と紐付けます

4.カメラ利用の許可を取るために、Info.plistに追加します
[ Privacy - Camera Usage Description ]

5.Assets.xcassetsを開き、[ New AR Resource Group ]を追加。
フォルダの中にマーカーとして検出したい画像(今回はマンガの表紙)を入れます。
スクリーンショット 2019-12-21 21.36.21.png

この時、画像の名前現実世界での大きさを入力します。
マーカーにふさわしい画像についてはこちらを参考にしてください。

6.ARオブジェクトとして表示したい画像(今回はマンガの原稿)を[ New Image Set ]で追加します。

実装

1.設定

ARImageTrackingConfigurationを設定します。

import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController {

    @IBOutlet weak var sceneView: ARSCNView!

    var session: ARSession {
        return sceneView.session
    }

    let updateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".serialSceneKitQueue")

    override func viewDidLoad() {
        super.viewDidLoad()
        sceneView.delegate = self
        resetTracking()
    }

    func resetTracking() {
        // Assetsの読み込み
        guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
            fatalError("Missing expected asset catalog resources.")
        }

        // 既知の2D画像を追跡するconfigの設定
        let configuration = ARImageTrackingConfiguration()
        configuration.trackingImages = referenceImages
        // ARオブジェクトの手前に人が映り込む時、オクルージョン処理してくれる設定
        configuration.frameSemantics = .personSegmentation

        // session開始
        // .resetTracking: デバイスの位置をリセットする
        // .removeExistingAnchors: 配置したオブジェクトを取り除く
        session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // session停止
        sceneView.session.pause()
    }
}

configuration.frameSemantics = .personSegmentationは、
ARKit3から追加されたピープルオクルージョンの設定で、設定するとこのように

nekonote2-compressor.gif

人の指が原稿の手前にくるので、マンガの中を覗き読んでいる!という世界をよりリアルに実現できます。
ただ、まだ少し精度が甘くチリチリすることは否めないので、お好みでオフにして下さい?

2.マーカーを検知してオブジェクトを表示

表紙を検知し、ARSCNViewDelegateで通知を受け取ったら原稿を表示します。

// MARK: - ARSCNViewDelegate
extension ViewController: ARSCNViewDelegate {
    // 新しいARアンカーに対応するノードが追加されたことを通知
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let imageAnchor = anchor as? ARImageAnchor else {
            return
        }
        // 表示したいオブジェクトをARアンカーの名前から決定
        let objectImage: UIImage
        switch imageAnchor.referenceImage.name {
        case "nekonote" :
            objectImage = nekonotes[nekonoteIndex]
        case "hanakaku":
            objectImage = hanakakus[hanakakuIndex]
        default:
            return
        }

        updateQueue.async {
            // sceneにノードを追加
            node.addChildNode(self.createNode(image: objectImage, name: imageAnchor.referenceImage.name ?? "no name"))
        }
    }

    private func createNode(image: UIImage, name: String) -> SCNNode {
        // 検出されたARアンカーの位置を視覚化する平面に合わせて、長方形のノード(SCNPlane)を作成
        let scale: CGFloat = 0.2
        let plane = SCNPlane(width: image.size.width * scale / image.size.height,
                             height: scale)
        // firstMaterial:平面の最初のマテリアル
        // diffuse: 表面から拡散反射される光の量、拡散光はすべての方向に等しく反射されるため、視点に依存しない、contentsに画像をset
        plane.firstMaterial?.diffuse.contents = image
        let planeNode = SCNNode(geometry: plane)
        planeNode.name = name
        // SCNPlaneはローカル座標空間で垂直方向を向いているが、ARアンカーは画像が水平であると想定しているため
        // 一致するように回転させる
        planeNode.eulerAngles.x = -.pi / 2
        // アニメーション
        planeNode.position = SCNVector3(0.0, 0.0, -0.15)
        planeNode.scale = SCNVector3(0.1, 0.1, 0.1)
        planeNode.runAction(self.imageAction)
        return planeNode
    }
}

3.アニメーション

ARはかっこよく表示したい、ですよね、ですよね。
ということで、はじめに検知した時にフェードアニメーションを追加します?

    var imageAction: SCNAction {
        return .sequence([
            .scale(to: 1.5, duration: 0.3),//スケール
            .scale(to: 1, duration: 0.2),
            .fadeOpacity(to: 0.8, duration: 0.5),//フェード
            .fadeOpacity(to: 0.1, duration: 0.5),
            .fadeOpacity(to: 0.8, duration: 0.5),
            .move(to: SCNVector3(0.0, 0.0, 0.0), duration: 1),//移動
            .fadeOpacity(to: 1.0, duration: 0.5),
        ])
    }

.sequenceで連続したアニメーションを処理することができます。
センスがないとか言わないで?

4.タップ

画面をタップでページをめくりましょう。

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard
            let location = touches.first?.location(in: sceneView),
            let result = sceneView.hitTest(location, options: nil).first else {
                return
        }
        // ノードの名前を取得し画像変更
        let node = result.node
        let objectImage: UIImage
        switch node.name {
        case "nekonote" :
            nekonoteIndex += 1
            objectImage = nekonotes[nekonoteIndex % 4]
        case "hanakaku":
            hanakakuIndex += 1
            objectImage = hanakakus[hanakakuIndex % 4]
        default:
            return
        }
        // アニメーションしながら画像差し替え
        node.runAction(self.pageStartAction, completionHandler: {
            node.geometry?.firstMaterial?.diffuse.contents = objectImage
            node.runAction(self.pageEndAction, completionHandler: nil)
        })
    }

    var pageStartAction: SCNAction {
        return .fadeOpacity(to: 0.0, duration: 0.1)
    }
    var pageEndAction: SCNAction {
        return .fadeOpacity(to: 1.0, duration: 0.2)
    }

完成?

nekonote3-compressor.gif

これで、本屋さんでマンガに封がしてあっても、中身を覗き読みすることができちゃいますね?

サンプルアプリはこちらに公開しています✨
koooootake/MangaTrialAR-ios

動画はこちら

もうひと作品お借りしたのは個人利用可能で作品を公開してくださっている「ハナカク」様?
とても絵が綺麗いいい本当にありがとうございますすす✨

おわりに

やりたいこと

ここから、マンガのコマをVisionで分解してアニメーションさせたり
指先のジェスチャーを検知してページをめくったりして、現実を拡張したいいい

AppleのARグラスが売り出され?
コンテンツの表現が紙やディスプレイに留まらず拡張される素敵な世界が楽しみです?

おまけ

DeNAアドベントカレンダー終盤を担当したので
この記事を読んだ、ものづくり好きな人にオススメの記事を勝手に紹介?

【3日で実装・公開】エモいアートな画像生成アプリ開発
エモい!!!じんむのアイコンもエモくなりました

【Electron+GCP+Slack App】Slackのコメントをニコニコ動画風にプレゼンで流す方法
「Slack」のコメントというところが、社会人には超超超実用的✨

退職者を送る技術 - Twilio と Socket.IO で作る電話マルチプレイシステムの小ネタ
電話を掛けて、キーパットを利用して、みんなで操作するゲームの作り方?発想良すぎぎぎ

来年もべしべしものづくり楽しもううう?

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