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

[Swift] UITextViewに画像を添付する方法

はじめに

GameWith AdventCalendar 2019 の14日目の記事になります。

普段はiOSエンジニアをやっていて、最近はWebの業務を行なっているのでその辺りについても来週のアドベントカレンダーで話したいと思います。

今回の記事ではUITextViewNSTextAttachmentクラスを用いて画像を添付する方法をまとめています。
入力中の文字のpositionなどを取得して、その上下にUIImageViewを配置するなどといった事をしなくても簡単に実装できるので具体例をまじえて紹介したいと思います。

具体例

SNSサービスなどでカメラロールから画像を選択し、テキストと一緒に投稿などを行う場合のUIイメージを想定しています。

カメラロールから画像を取得

シンプルにUIImagePickerControllerを使うケースです。

let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .photoLibrary
present(picker, animated: true, completion: nil)

UIImagePickerControllerDelegateを設定する

extension SampleViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let image = info[.originalImage] as? UIImage {

            let fullString = NSMutableAttributedString(string: textView.text)

            let imageWidth = image.size.width
            // 画像の幅を調整したい場合paddingなどをframeから引く
            let padding: CGFloat = 16

            let scaleFactor = imageWidth / (textView.frame.size.width - padding)

            let imageAttachment = NSTextAttachment()
            imageAttachment.image = UIImage(cgImage: image.cgImage!, scale: scaleFactor, orientation: .up)

            let imageString = NSAttributedString(attachment: imageAttachment)
            fullString.append(imageString)

            // TextViewに画像を含んだテキストをセット
            textView.attributedText = fullString
        } 
        dismiss(animated: true, completion: nil)
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }

}

終わりに

NSTextAttachment

今回利用している NSTextAttachment ですが、こちらを使う事でUILabelにも画像を適用する事ができるので非常に便利なクラスになっています。
今後文字と画像を結合するUIを実装する際には是非使ってみてください!

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

Swift勉強日記 〜Part 1〜

はじめに

やっとmacを手に入れたのでSwiftの勉強を始めました。
HackingWithSwiftのProject1を進めるにあたって今後のためにメモしておきたいと思ったことを綴ります。

コマンドのCheat Sheet

・Cmd + R : Simulatorの起動
・Cmd + . : Simulatorの終了
・Cmd + L : Libraryの表示

用語集

FileManager.default : ファイル操作をするために必要なデータタイプ。
Bundle : コンパイルされたプログラムや全ての要素を含むディレクトリである。
UITableViewController : スクロールや選択できる複数行のデータを表示できる。

データファイルの追加方法

データファイルを左側のNavigation AreaにDrag&Dropする。
スクリーンショット 2019-12-14 0.41.06.png

と以下の図のような画面が出現する。
スクリーンショット 2019-12-14 0.31.55.png

この際
・Copy items if needed
・Create groups
という項目がチェックされているか確認すること

ファイル名の取得方法

pathで取得したディレクトリ内のファイル名を全て取得する。

let path = Bundle.main.resourcePath!
let contents = FileManager.default.contentsOfDirectory(atPath: path)

列の表示方法

TableViewにおいて表示する列の数を設定する

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return //ここに表に表示させたい列の数
}

UITableViewControllerへの変更方法

まず

class ViewController: UIViewController 

class ViewController: UITableViewController 

に変更する。

次にMain.storyboard内において、デフォルトで設定されている View Controller Scene を Table View Controller に変更する。
スクリーンショット 2019-12-14 1.26.33.png

Navigation Controllerとは?

Table View Controller を Navigation Controller に Embed In すると、iPhoneの設定上部で見られる画面遷移のための Navigation Bar が表示される。
IMG_A41E31204DB4-1.jpeg

参考Webサイト
https://www.hackingwithswift.com/100

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

SILOptimizerに軽く入門する

SILOptimizerに軽く入門する

こんにちは、freddiです。Swiftを初めて1年位経ちましたが、今までの登壇内容などのせいか一部の人から「リテラルおじさん」と呼ばれるなど、リテラルハラスメントを受けています。

この記事は「Swift Advent Calendar 2019」の15日目の記事です。皆さんが簡単に、Swiftコンパイラの最適化フェーズであるSILOptimizerを学べるようになるための、準備になる知識の内容を書いています。少々長いですが、ゆっくりお付き合いください。

なぜSILOptimizerに入門すべきなのか?(ポエム)

さて、なぜ私たちは貴重なSwift Advent Calendarの一枠を使ってまで、SILOptimizer入門にすべきなのでしょうか?
あるとしたら、Swiftのコードの「理不尽な動かない」に対しての、理解や考察の糧をある程度持ってもらいたいからです。

詳しくはこのあと話しますが、SILOptimizerでは最適化オプションによっては特定のコードを削除したりします。また、iOSアプリケーション開発でもDebugReleaseでは最適化オプションも違います。

つまり、コードによってはiOSアプリでDebugReleaseで挙動が違ってくるかもしれません。それは一概に最適化のせい、とは言えません。しかし、例えば最適化によってコードが消えた結果、DebugReleaseでは全然挙動は違うということは無きにしもあらずです。私もプロダクト開発では何度か出くわしました。

それを「勝手に挙動が変わった」と思うのと「最適化に問題があるな」と思うのでは、全然視点とモチベーションが違います。「理不尽な動かない」に直面したとき、SILOptimizerなどの最適化の知識を一つの考察の糧にすると、もしかしたら「勝手に挙動が変わった」と思う世界線の自分とは違った、またスマートで面白い打開策を見つけられるかもしれません。

今回の解説記事だけではそこまでのレベルに達するのは難しいですが、他の資料などを見るときなどにの補助になる知識になると思います。

Swiftコンパイラのコンパイルの流れ

まずはじめに、事前知識としてSwiftコンパイラのコンパイルのフローを復習します。知っている人は飛ばしても構いません。

私達が書いたSwiftコードはSwiftコンパイラにコンパイルされ、目的の成果物になります。それは、アプリになったり、frameworkだったり、オブジェクトファイル1だったり。

さて、私達のSwiftコードは、目的の成果物になるまでは、コンパイラの中ではいくつかのフェーズを経ています。
Swiftコンパイラのフロー図を利用して紹介します。

image.png
(引用元: https://www.slideshare.net/kitasuke/sil-for-first-time-leaners/1 by kitasuke)

まず、SwiftのコードはParseのフェーズで抽象構文木(AST)になり、Semaのフェーズでその抽象構文木に型(Type)情報が付きます。
その後、型情報付きのASTはSILGenで中間言語であるSwift Intermediate Language(SIL)のコードになり、ここでSILOptimizerによって最適化が入ります。
最後に、最適化されたSILはIRGenによってLLVM IRのコードになった後、目的の成果物になります。

Swiftコンパイラの最適化のフェーズ SILOptimizer

SwiftコンパイラはSILGenのフェーズで、Swift Intermediate Language(以下SIL)と呼ばれる中間言語に変換して、LLVM IRに変換します。
その、LLVM IRに変換する前に、そのSILを最適化を行うフェーズがあります。それが今回話すSILOptimizer2です。

SILOptimizerの役割

Optimizerとの名のある通り、SILOptimizerはSwiftのプログラムの最適化3を行います。

どのような最適化を行うか、簡単な例を少しだけ出すと、

  • 不必要なコードの削除4
    • 使われていない変数・定数を見つけ出して削除し、メモリに不必要なものが乗らないようにする
  • できる限りHeapをStackにする5
    • プログラムの中でStackで確保できそうなところはすべてStackに変えて、プログラムのパフォーマンスを上げる

このように、私達のSwiftのプログラムをより良くするために、中間言語であるSILのコードをSILOptimizerは最適化してくれています。

SILの最適化

ここからは最適化対象である、SILについてフォーカスしたお話をします。

実は、SILといえども、2種類のSILが存在しています。おおまかに、SILOptimizerから出たか(最適化された) or 出てないか6、で分けられています。

raw SIL

まず、SILGenから出たばかりの最適化がされていないSILは、raw SILと呼ばれています。

では、試しに次のコードのraw SILをみてみましょう。

value.swift
let value = 10
print(10)

raw SILはswiftc-emit-silgenオプションで見ることができます。

swiftc value.swift -emit-silgen > value.sil

そして出力されたraw SILのコードがこちらです。(クリックで展開)

※ 無理やりSwiftのSyntaxハイライトを入れています。
value.sil
sil_stage raw

import Builtin
import Swift
import SwiftShims

@_hasStorage @_hasInitialValue let value: Int { get }

// value
sil_global hidden [let] @$s5valueAASivp : $Int

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s5valueAASivp                    // id: %2
  %3 = global_addr @$s5valueAASivp : $*Int        // user: %8
  %4 = integer_literal $Builtin.IntLiteral, 10    // user: %7
  %5 = metatype $@thin Int.Type                   // user: %7
  // function_ref Int.init(_builtinIntegerLiteral:)
  %6 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %7
  %7 = apply %6(%4, %5) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %8
  store %7 to [trivial] %3 : $*Int                // id: %8
  %9 = integer_literal $Builtin.Int32, 0          // user: %10
  %10 = struct $Int32 (%9 : $Builtin.Int32)       // user: %11
  return %10 : $Int32                             // id: %11
} // end sil function 'main'

// Int.init(_builtinIntegerLiteral:)
sil [transparent] [serialized] @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int

初見だと、もとのSwiftのコードより非常に複雑だと思ってしまうようなコードが出てきましたね。

canonical SIL

SILOptimizerから出て最適化がされたSILは、canonical SILと呼ばれています。

では、最適化されたcanonical SILのコードを見たいと思います。raw SILをcanonical SILにするのは簡単で、swiftc-emit-silオプションを指定して、先程出力したvalue.silを渡せばよいです。

swiftc value.sil -emit-sil > value-canonical.sil

swiftコードから直接canonical SILを出すこともできます。

swiftc value.swift -emit-sil > value-canonical.sil

そして出力されたcanonical SILのコードがこちらです。(クリックで展開)

※ 無理やりSwiftのSyntaxハイライトを入れています。
value-canonical.sil
sil_stage canonical

import Builtin
import Swift
import SwiftShims

@_hasStorage @_hasInitialValue let value: Int { get }

// value
sil_global hidden [let] @$s5valueAASivp : $Int

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s5valueAASivp                    // id: %2
  %3 = global_addr @$s5valueAASivp : $*Int        // user: %6
  %4 = integer_literal $Builtin.Int64, 10         // user: %5
  %5 = struct $Int (%4 : $Builtin.Int64)          // user: %6
  store %5 to %3 : $*Int                          // id: %6
  %7 = integer_literal $Builtin.Int32, 0          // user: %8
  %8 = struct $Int32 (%7 : $Builtin.Int32)        // user: %9
  return %8 : $Int32                              // id: %9
} // end sil function 'main'

// Int.init(_builtinIntegerLiteral:)
sil public_external [transparent] [serialized] @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int {
// %0                                             // user: %2
bb0(%0 : $Builtin.IntLiteral, %1 : $@thin Int.Type):
  %2 = builtin "s_to_s_checked_trunc_IntLiteral_Int64"(%0 : $Builtin.IntLiteral) : $(Builtin.Int64, Builtin.Int1) // user: %3
  %3 = tuple_extract %2 : $(Builtin.Int64, Builtin.Int1), 0 // user: %4
  %4 = struct $Int (%3 : $Builtin.Int64)          // user: %5
  return %4 : $Int                                // id: %5
} // end sil function '$sSi22_builtinIntegerLiteralSiBI_tcfC'

なんかところどころ違いますが、もとのSwiftのコードより複雑なのは変わりないですね。

raw SILとcanonical SILの違いを見る

さて、raw SILとcanonical SILの違いは一体何でしょうか?
気づいた人もいるかも知れませんが、まず一行目が違います。

value.sil
// 1行目
sil_stage raw
value-canonical.sil
// 1行目
sil_stage canonical

みたとおりなのですが、SILのコードは一行目でこのSILがraw SILかcanonical SILを示しています。
さて、それだけでしょうか?

実は、Intの定数valueを宣言して、10を代入している部分も大きく違います。

value.sil
// 15行目以降
  alloc_global @$s5valueAASivp                    // id: %2
  %3 = global_addr @$s5valueAASivp : $*Int        // user: %8
  %4 = integer_literal $Builtin.IntLiteral, 10    // user: %7
  %5 = metatype $@thin Int.Type                   // user: %7
  // function_ref Int.init(_builtinIntegerLiteral:)
  %6 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %7
  %7 = apply %6(%4, %5) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %8
  store %7 to [trivial] %3 : $*Int                // id: %8
value-canonical.sil
// 15行目以降
  alloc_global @$s5valueAASivp                    // id: %2
  %3 = global_addr @$s5valueAASivp : $*Int        // user: %6
  %4 = integer_literal $Builtin.Int64, 10         // user: %5
  %5 = struct $Int (%4 : $Builtin.Int64)          // user: %6
  store %5 to %3 : $*Int                          // id: %6

明らかに、canonical SILのほうが、raw SILよりも行数も少ないですね。これはraw SILから不必要なコードが削除されたり、別のコードに交換されたりしたことを示しており、これはSILOptimizerが行っています。

最適化のPassとPipeLine

さて、ここからはSILOptimizerについてフォーカスしたお話をします

Passとは?

SILOptimizerという一つの大きなモジュールが、すべての最適化を担っているわけではありません。
SILOptimizerは、Pass7という細かいモジュールが多数存在しており、そのPass達がそれぞれの役割の最適化を行っています。

例えば、先程話した「できる限りHeapをStackにする」のも実はAllocBoxToStackと呼ばれるPassが行っています。

オプションとPass

Swiftコードをコンパイルするときに、SILOptimizerは必ず呼び出され、その中のいくつかのPassも必ず呼ばれます
例えば、先程話したAllocBoxToStackは必ず呼ばれます。

しかし、「いくつかのPassも必ず呼ばれます」と言ったとおり、SILOptimizerの中のすべてのPassが、意思表示もなしに必ず呼ばれるわけではないです

わかり易い例として、assertprecondition最適化オプション次第で消えるというものがあります。これはSILOptimizerのあるPassが該当のコードを削除しているからです。

image.png
(引用元: http://safx-dev.blogspot.com/2015/02/swift-assert-precondition-and-fatalerror.html by Safx)

つまり、最適化オプションだけが、Passを呼ぶか呼ばないかの判断になっているのでしょうか?
皆さんは馴染みがないと思いますが、たとえばswiftcに-assume-single-threadedというオプションを付けると、Single Thread向けの成果物を出力します。

swiftc -assume-single-threaded value.swift

これは、Single Thread向けに最適化するPassであるAssumeSingleThreaded8を呼び出すオプションでもあります。

つまり、

  • SILOptimizerのPassは、最適化オプションによっては呼ばれるものと呼ばれないものがある
  • AssumeSingleThreadedのような、オプションで呼び出すPassもある

という事がわかります。

実際にはどうやっているかと言うと、Swiftコンパイラは、オプションなどの状況からPassの通り道であるPipelineというものを作り上げます。
このPipelineにどのPassが通るかを設定し、raw SILはPipelineどおりに最適化されます。

ここまでまとめると、SILOptimizerのオーバービューはこんな感じになります。

image.png
(引用元: https://www.slideshare.net/YukiAki/tutorial-for-developing-siloptimizer-pass by freddi)

SILOptimizerはコードの「診断」もやっている

さて、ここまでSILOptimizerの最適化の話をしてきましたが、実はSILOptimizerはコードの「診断(Diagnostic)」を行うPassもあります。
代表的なものとして、数値系のオーバーフローのチェックをするPassがあります。このPassは、数値系のオーバーフローがあったらその場でエラーを出します。

ここで、皆さんに宿題を出します。

overflow.swift
let value = 100000000000000000000 //コンパイルできない

この、overflow.swiftは普通にコンパイルしようとすると、

$ swiftc overflow.swift
value.swift:1:13: error: integer literal '100000000000000000000' overflows when stored into 'Int'
let value = 100000000000000000000 //コンパイルできない

とコンパイルエラーになります。これは100000000000000000000Intだとオーバーフローするから当然ですね。

では、-emit-silgen-emit-silのオプションをつけたときに、それぞれどのようにエラーが変化するでしょうか?

実際にこのことをご自身の手を動かしてみて確かめてみてください。Swiftコンパイラのフローの中にあるSILOptimierについて、実際に体感することができます。9

もっと学びたい人向けに

ここからSILOptimizerが興味がある人向けに、勉強になるマテリアルをいくつか紹介します。
これらのSILOptimizerに関する資料は、だいたいSwiftコンパイラのコードをリーディングしたりいじったりするものなので、これらをこなすと更に深くSILOptimizerについて勉強することができます。

SIL入門

近年はSILの解説資料が多くなってきており、よりSILに入門しやすくなりました。

SIL for First Time Learners(SIL入門)

Swiftの中間言語SILを読む シリーズ

https://blog.waft.me/2018/01/09/swift-sil-1/

SILを読もう

https://www.youtube.com/watch?v=jih0ljNpODs

Swift Compilerの最適化入門 - AllocBoxToStack編

この文章の中で何回か言及したAllocBoxToStackに関して、この動画では詳しい解説をしています。
https://www.youtube.com/watch?v=hsO3PWH9Rcw

また、発表者のkitasukeさんんはAllocBoxToStack編に関する書籍も販売しているのでぜひ読んでみてください。
https://kitasuke.booth.pm/items/1034691

SILOptimizerを自分で作って遊んで見る

SILOptimizerのPassを自分で作って遊んでみようという題材の発表です。SILOptimizerが何をやっているのか、SILOptimizerのコードを読み歩き方などを解説しています。
https://www.youtube.com/watch?v=QBXcCTfwyZ8

また、SIL以外にもSwiftコンパイラのディープな話題に興味が出てきた方は、ぜひ、わいわいswiftc10という勉強会に参加するといいかもしれません。

まとめ

SILOptimizerのPassなどを含んだ簡単なオーバービューや、SILOptimizerがどのような最適化を行っているかについて話しました。
冒頭にも言ったとおり、これはベースになる知識のみの解説記事なので、もし興味が出たら「もっと学びたい人向けに」にある資料などをみて勉強してみてください。

ご精読(?)ありがとうございました。明日の16日目のhomunuzさんのネタに期待しています!


  1. オブジェクトファイルってなんぞや、って思う人がいるかも知れませんが、その人はオブジェクトファイルというものから知るよりも、基本的にプログラミング言語のコンパイラがやっていることから知るのがいいかもしれません。こちらにC言語のコンパイルの解説がありますのでご参照ください。http://aoking.hatenablog.jp/entry/20121109/1352457273 

  2. SILOptimizerは、フロー図のようにSILGenとひとくくりにされていることがあります。 

  3. ここでSwiftコードと書かなかった理由は、あくまでSILOptimizerはraw SILを最適化しているのであり、Swiftコードを直接最適化しているわけではないからです。 

  4. 英語だと、Code Eliminationと言います。たとえば、機能していないコードを最適化で消すことをDead Code Elimination(DCE)といいます。DCEと言う略し方はSwiftコンパイラのコードでもよく使われており、単語として覚えていて損はありません。https://github.com/apple/swift/blob/62ccf81f7748e3e2c8626354d1ecb3adbd26b063/lib/SILOptimizer/SILCombiner/SILCombine.cpp#L51 こんなとことか。 

  5. StackとHeapには利点と不利な点があります。しかし、StackのほうがHeapよりも好まれている、と言われてピンとこない方は https://keens.github.io/blog/2017/04/30/memoritosutakkutohi_puto/ あたりを見ると良いかもしれません。https://developer.apple.com/videos/play/wwdc2016/416/ ではSwiftにフォーカスした話が聞けます。 

  6. General Optimizationとよばれる、canonical SILをさらにOptimizeするフェーズがあるのですが、今回は割愛しています。詳しくは https://blog.waft.me/2018/01/09/swift-sil-1/ 

  7. SILGenの後のフェーズのLLVMの最適化のモジュールもPassと呼ばれるものです。https://www.ibm.com/developerworks/jp/opensource/library/os-createcompilerllvm2/index.html 

  8. 昔私が少々解説した資料があるので、ご興味があればご参照ください。https://www.slideshare.net/YukiAki/siloptimizercode-reading 

  9. もっと知りたい人は、これでコンパイルしてみてください → swiftc -Xllvm -sil-print-pass-name overflow.swift。「SILOptimizerを自分で作って遊んで見る」という動画でも詳しく説明していますが、SILが通ったPassを見ることができます。 

  10. connpassはこちら → https://iosdiscord.connpass.com/ 。中継もやっており、私はたまにそちらで見ています。また、いままでの中継はアーカイブとして残っています。https://www.youtube.com/results?search_query=%E3%82%8F%E3%81%84%E3%82%8F%E3%81%84swiftc 

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

[Xcode]AppIconを間違えて削除してしまった時のにすること

プロジェクトを作り始めているときに

とりあえず画像を放り込んでさあ作業!って時にこっちの画像がいいや!ってなって画像を一回全部消してからもう一度アップ!

ってなった時に間違えてAppIconまで削除してしまいエラーが出てしまった人たちへ捧ぐ記事です。

エラーの出現

エラーはこんな感じです

スクリーンショット 2019-12-14 10.51.24.png

いやあ。初心者にありがちなんですが、エラーが出ると勝手にあ、終わった。となってしまうのですが、実は大したことない。というパターンが多いです。

落ち着いてエラー内容を見てググってみればだいたい解決します。

対処法

アセット下部にある

スクリーンショット 2019-12-14 10.53.12.png

から+をクリックし、

スクリーンショット 2019-12-14 10.53.53.png

New iOS App Iconをクリックすれば作成できます。

一件落着です。

まとめ

なんかコードを書いていたら出てくるエラーじゃないからびっくらこきました。

同じ境遇の人が救われることを願って!

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

大学祭でシステムを作った話(レシートプリンタ・受付システム編)

どんな背景でシステムを開発・運用したかは、昨日のアドベントカレンダーをご覧ください

何を作ったのか

来場者の属性(性別・年代・職業など)をあらかじめ取得しておき、訪れた企画の履歴を紐づけることで、どのような属性の人がどのような企画に行く傾向があるのかや、来場人数のカウント、混雑の度合いなどを分析するするためのシステムです。

そして、企画を訪れた際にタイムスタンプを付与するための媒体として、QRコードを発行し、読み取るためのシステムがあります。

ハードウェア構成

昨年度の受付システムでも、同様のシステムが稼働していました。
その際に購入したレシートプリンタがあったので、今年もそれを使用しました。
メーカー公式サイト

タブレットと接続できるようなタイプのレシートプリンタは、最近軽減税率やキャッシュレスなどで、Airレジのようなタブレットを使ったPOSレジで見たことあるような方もいらっしゃるのではないでしょうか。

大体、1台につき5万円くらいするみたいです(参考リンク)
それが、大学祭実行委員会には3台あったので、15万円・・・ どんだけ予算余ってたんだ
このレシートプリンタは、法人だけでなく、個人でも使えるSDKが提供されているのが特徴です。

レシートプリンタは購入でしたが、タブレットは昨年度レンタルで、既になかったので、iPadを調達しました。
Androidで用意するなり、ラズパイとかで開発すればもっと安く済んだのかもわかりませんが、如何せん時間がなかったので、開発が慣れているiPadを採用することにしました。

レシートプリンタとiPadとの接続方法はいくつかあります。
IMG_0689.png

  • Lightningケーブルで有線接続
  • Bluetoothで無線接続
  • LAN接続

Bluetoothが一番接続はスッキリしていますが、今回調達したiPadはアプリのデプロイをローテーションしながら行ったり、企画での受付に拠出するために合計6台あり共通運用なのでペアリングの動作が大変でした。
LANも大学の制約で事前の申請が必要且つ自分でネットワークを構築するとなかなか大変なので、今回Lightningケーブルで有線接続としました。
ペアリングする手間なく、APIでコネクションを確立すればそのまま印刷が可能な上、iPadに給充電することができます。
使うコンセントがiPadとプリンタのペアで1本で済むので、ケーブルが気にならないなら最も簡単で確実じゃないかと思います。
IMG_0688.png
これが有線の場合の最小構成です。
レシートプリンタには、電源ケーブルが繋がっています。

SDKの使い方(Swift)

SDKの導入方法は、公式のドキュメントを参照してください(SDKのファイルと一緒にPDFで同梱されています)
Objective-Cで書かれているため、Swiftで使うにはブリッジングヘッダなるファイルを作成しなければならないようです。

レシートプリンタとの接続は、AppDelegateでインスタンスを作成して行います。
ViewControllerで保持する形式にしたかったのですが、やっぱり時間がなかったので

AppDelegate.swift
import UIKit
import Alamofire
import KeychainAccess
import SwiftyJSON

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, StarIoExtManagerDelegate {
    var manager:StarIoExtManager!

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        manager = StarIoExtManager.init(type: .standard, portName: "BT:mC-Print3", portSettings: "", ioTimeoutMillis: 10000)!
        manager.delegate = self
        manager.connectAsync()

・・・以下略・・・

managerが今回のミソで、各ViewControllerなどから呼び出すことになります。
StarIoExtManagerを初期化する際に与える引数のうち、portNameは、Bluetoothを使う場合でも有線を使う場合でも同じものを使用するようです。
AppDelegateには、StarIoExtManagerDelegateを継承させておきます。
(今回はあまり出番がありませんでしたが、プリンタのカバーが開いたり、ロール紙が切れた際の処理を記述することができます。)

実際の印刷のコードがこちらです。

ReceptionViewController.swift
func printQrCode(user_id:String){
        let ap = UIApplication.shared.delegate as! AppDelegate 
        //AppDelegateで保持しているプリンタとのコネクションのインスタンスを取得する

        do{
            let builder = StarIoExt.createCommandBuilder(.starPRNT)! //プリンタへ送信する命令の構築用

            //第一引数に与えたdataの文字列からQRコードを生成して命令へ追加する
            builder.appendQrCodeData("https://app.iniadfes.com/visitor?user_id=\(user_id)".data(using: .utf8)!, model: .no2, level: .L, cell: 10)

            //行ごとに、印刷する文字列をデータ化して命令へ追加する
            builder.appendLineFeed() //空白で若干下げる
            builder.appendData(withLineFeed: "QRコードを受付で提示してください".data(using: .shiftJIS))

            let formatter = DateFormatter()
            formatter.dateFormat = "MM月dd日"
            builder.appendData(withLineFeed: "来場日:\(formatter.string(from: Date()))".data(using: .shiftJIS))
            builder.appendData(withLineFeed: "ーーーーーーーーーーーーーーーー".data(using: .shiftJIS))
            builder.appendData(withLineFeed: "お帰りの際、受付でのアンケートにご協力をお願いいたします".data(using: .shiftJIS))
            builder.appendData(withLineFeed: "ーーーーーーーーーーーーーーーー".data(using: .shiftJIS))

            //用紙カット
            builder.appendCutPaper(.fullCutWithFeed)

            var command = [UInt8]()
            let command_data = NSData.init(bytes: builder.commands.mutableBytes, length: builder.commands.length)
            command = [UInt8](Data(command_data))

            var total: UInt32 = 0
            while total < UInt32(command.count) {
                var written: UInt32 = 0
                // 印刷データを送信し続ける
                try ap.manager.port.write(writeBuffer: command, offset: total, size: UInt32(command.count) - total, numberOfBytesWritten: &written)
                total += written
            }
        }catch{
            let alert = UIAlertController(title: "Error", message: "QRコードの印字に失敗しました", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            alert.addAction(UIAlertAction(title: "リトライ", style: .destructive, handler: {action in
                self.printQrCode(user_id: user_id)
            }))

            self.present(alert, animated: true, completion: nil)
            return
        }

    }

命令構築用のStarIoExt.commandBuilderの初期化の際の引数には、対象のデバイスの種類を指定します。
今回は.starPRNTを指定しましたが、キャッシュドロワーのような他の種類のデバイスも存在しているようでした。
1行ずつデータを追加していくというのが、何となく直感的でした。

文字コードはShift-JISだったりUTF-8だったり、どっちにするべきなのかイマイチよく分からなかったです・・・
最初Shift-JISで印字していたところ、なぜか文字化けするようになってUTF-8に変更して解決→再び文字化けするようになって戻したということがあったので、その辺ちゃんと仕様見返します・・・
そしてなんでまだNSDataとか生きてるんだよ

IMG_0691.png

こんな感じで出力されました。

アプリ周り

全体受付用のiPadアプリです。
また、昨年と同じ紙のQRコードの他、公式アプリでもQRコードを表示できるようにしました。(その関係は明日のアドベントカレンダーで書きます)

IMG_0003.PNG

アプリのQRであっても、来場したフラグを持っていないと実際の来場者数をカウントできない(アプリ側はリセット操作でQRコードを何度でも発行できてしまうので)ので、そのための画面です。

IMG_0004.PNG

紙のQRが必要な場合は、ここで属性情報を入力します。

IMG_FDFA53B3508C-1.jpeg

その他、企画の受付では、公式アプリに内蔵している機能を使って受付します。(本来はQRを表示するところを、権限のあるアカウントでログインすることで読み取り機能をアクティベートしています)
QRを検出すると、自動でAPIに受付のリクエストを送信し、すぐに次のQRを読み取れるようになっています。

権限管理

今回、このシステムを使うにあたってのユーザー管理は、Googleアカウントを使いました。
もちろん、Gmailとかのフリーアドレスでなく、大学で使われているG Suiteアカウントです。(他組織のG Suiteなどでもログインはできないように検証処理をしています)
(本当なら大学のOpenAMを使いたかったけども)予めどのIDでログインを許可するかや、どのサークルの読み取りができるかのポリシーを用意しています。(そこだけサークルの代表者に一覧を提出させました)

なお、実行委員は実行委員用のroleを割り当てて全て読み取り可にしています。

その辺りの管理も、企画管理システムと一体になっていて、Webから編集が可能です。
スクリーンショット 2019-12-14 1.52.02.png
スクリーンショット 2019-12-14 1.53.02.png

来場者レポート

最終的に集まったデータは、グラフ化して表示できます(実物のデータになっちゃうのでスクショはごめんなさい)
受付の権限の範囲で、サークル参加者も一部閲覧できるようにして、共有しました。

運用してみて

QRコードも、サードパーティーのもので読み取ってログイン済みのブラウザで開き・・・というルーチンをやめて、ネイティブで連写できるようにしたことで、オペレーションもかなり改善したと思います。
また、後からGUIで諸々の設定を変更可能にしているので、トラブルになったとしても臨機応変に対応できたのかなと思います。

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

UIColor から CIColor に変換するときの注意

前提

UIColor から CIColor にする方法は2種類あります

# 変数 color は UIColor のインスタンスとする

# initializer
CIColor(color: color)

# property
color.ciColor

注意

color.ciColor で取得する時、 color が Core Image でイニシャライズされたものでない (= CIColor で作成されてない) 場合は例外を返されます。
公式ドキュメントにはかいてありますが、抜け穴だと思います。

This property throws an exception if the color object was not initialized with a Core Image color.

結論

基本的に CIColor(color:) の方を使っておけば問題ない。

環境

  • Swift 5

参考

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