20200125のiOSに関する記事は4件です。

CoreNFCのFelica(NFC Type F)

はじめに

前回までで一応、NFC Type F のリードまではできるようになっていました。
今回はちょっとだけ補足とまとめをしますね。

処理の流れ

Session

まずは CoreNFC のセッションを作成します。

let session = NFCTagReaderSession(pollingOption: [.iso14443, .iso18092],
                                  delegate: self)
session?.begin()

このとき、Type F 以外にも Type A/B も同時にスキャンしたい時はこんな感じになります。
もちろん、Type F だけで良い場合は .iso18092 を指定すれば OK です。

スキャンして NFC タグが見つかったら、デリゲートが呼び出されます。
Type A/B/F でスキャンしている場合は、TypeA/B と Type F でアクセスする実装が違ってくるので、ここで判別することになります。
大雑把にはこんな感じです。

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    let t : NFCTag = tags.first!
    switch t {
    case .feliCa:
        // Type F
    case .iso7816:
        // Type A/B
    default:
        return
    }
}

Service

タグに接続できたらアクアスしたいサービスを指定します。
サービスコードはリトルエンディアンになります。

let serivces = [Data([0x8b, 0x00])]
feliCaTag.requestService(nodeCodeList: serivces,
                         completionHandler: {
    (nodes, error) -> Void in
})

Read

サービスを指定したらリードできます。
アクセスしたいブロックを指定します。Type F ではブロック単位でのリード・ライトになるんですが、1ブロックは16バイトのバイト列になります。swift なら Data 型ですけどね。
ただ、この時もサービスをしている必要があるのと、ブロックの指定もリトルエンディアンです。

let serivces = [Data([0x8b, 0x00])]
let blockList = [Data([0x80, 0x00])]
feliCaTag.readWithoutEncryption(serviceCodeList: serivces,
                                blockList: blockList,
                                completionHandler: {
    (status1, status2, dataList, error) in
})

ステータスフラグとデータとエラーが取得できます。
ちなみに本来の NFC の通信なら、ここでレスポンスコードとか PICC とかも取得できるみたいなんですが、端折られるみたいなんです。
ま、普通はそこまで不要だとは思うんですけど、取得する方法はあるんでしょうかね?

Write

サービスを指定したらライトもできます。
ほぼリードと同じですね。ライトしたいデータを渡せるようになっているのが違いです。

feliCaTag.writeWithoutEncryption(serviceCodeList: services,
                                 blockList: blockList,
                                 blockData: blockData,
                                 completionHandler: {
    (status1, status2, error) in
})

まとめ

処理の流れをまとめてみました。
Type A/B はちょっとだけ厄介ですが、Type F はずいぶんスッキリした API でアクセスできるのが良いですね。
今回は暗号なしでしたが、暗号ありはサービス提供者との契約が必要になるみたいです。

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

iosのgoodreaderでsftpのprivate-keyが読み込めない問題

これはmacのssh-keygenで作成されるコマンドの出力が変更されているので、昔作ったキーでは読み込めますが、新しく作ったキーは読み込めません。よって、キーを作り直すか、キー自体を編集しましょう。

# 公開鍵、秘密鍵の作成
$  ssh-keygen -t rsa -b 4096 -m PEM -f ~/.ssh/goodreader

# serverにpublic-keyを登録
$ ssh-copy-id -i ~/.ssh/goodreader.pub user@192.168.1.11 -p 22

# ~/.ssh/goodreader
# (private-key)をiosのgoodreaderに持っていく
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Lazyプロパティの使い方に気をつける(ドキュメントの確認は大事)

普段使っているものの
実は注意しないと予期せぬ動作を起こしてしまう可能性があるものは
意外とたくさんあると感じています。

そういう時
Swiftのドキュメントを見てみると
注意書(NOTE)があったりして役に立ちます。

今回は一つの例として
lazyのついたプロパティについて見てみたいと思います。

Lazy Stored Property(遅延格納プロパティ)とは?

Swiftでは
lazyという修飾子を使うことで
最初に利用されるまで
初期化処理を走らなくさせることができます。

これを
Lazy Stored Property(遅延格納プロパティ)
と呼びます。

どういうときに使う?

下記のような場合に役に立ちます。

  • 初期値がインスタンス生成後の状態(他のプロパティなど)に依存している
  • セットアップが複雑で重く、使われるまでは生成する必要がない

例えば
下記のようなCalendarクラスを考えてみます。

class Calendar {
    private lazy var formatter: DateFormatter = {
        print("call")
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.locale = Locale.init(identifier: "ja_JP")
        return formatter
    }()

    var today: String {
        formatter.string(from: Date())
    }
}

let c = Calendar()

formatterの初期化処理は
Calendarインスタンスを生成した時点では
呼ばれませんが

c.today 

// call

とtodayプロパティ経由で
formatterに始めてアクセスした時点で
初期化処理が呼ばれるようになります。

そして一度初期化処理が走ると
値がメモリに保存されるため
再度処理は走りません。

let c = Calendar()
c.today
c.today

// call ※1回しか出力されない

実は複数回呼ばれる場合がある

しかし
これには例外があります。

Swiftのドキュメントによると

NOTE
If a property marked with the lazy modifier is accessed by multiple threads simultaneously 
and the property has not yet been initialized, 
there is no guarantee that the property will be initialized only once.

↓のLazy Stored Propertiesの項
https://docs.swift.org/swift-book/LanguageGuide/Properties.html

マルチスレッドで同時にアクセスされた場合に
まだ初期化処理が終わっていないと
1回しか呼ばれないことは保証されない。

とあります。

先ほどのソースを少し変更してみます。

class Calendar {
    private lazy var formatter: DateFormatter = {
        sleep(1)
        print("call")
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.locale = Locale.init(identifier: "ja_JP")
        return formatter
    }()

    var today: String {
        formatter.string(from: Date())
    }
}

初期化処理で少しsleepします。

そして2つのキューからアクセスするようにしてみると

let q1 = DispatchQueue(label: "1")
let q2 = DispatchQueue(label: "2")

let c = Calendar()
q1.async { print(c.today) }
q2.async { print(c.today) }

// call
// call
...

初期化処理が2回走ることがわかりました。

この中で
もし値の変更など
副作用を起こすような処理が入っていたら
2回値を変更してしまった結果
予期せぬ不具合を引き起こす可能性があるので
注意が必要です。

Stored Type Property(格納型プロパティ)は1回しか呼ばれない

ここで似たようなものとして
Stored Type Property(格納型プロパティ)
があります。

Stored Type Propertyはインスタンスに属しておらず
classstaticという修飾子がついた
型自体に属しているプロパティです。

そしてSwiftのドキュメントに
下記の記載があります。

Stored type properties are lazily initialized on their first access. 
They are guaranteed to be initialized only once, 
even when accessed by multiple threads simultaneously, 
and they do not need to be marked with the lazy modifier.

↓のType Propertiesの項
https://docs.swift.org/swift-book/LanguageGuide/Properties.html

つまり
Stored Type Propertyの場合には
マルチスレッドで同時にアクセスされたとしても
必ず1回だけ処理が走るということを保証しています。

先ほどのformatterプロパティを
Type Propertyに変えて
同じことをやってみます。

class Calendar {
    private static var formatter: DateFormatter = {
        sleep(1)
        print("call")
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.locale = Locale.init(identifier: "ja_JP")
        return formatter
    }()

    var today: String {
        Calendar.formatter.string(from: Date())
    }
}

let q1 = DispatchQueue(label: "1")
let q2 = DispatchQueue(label: "2")

let c = Calendar()
q1.async { print(c.today) }
q2.async { print(c.today) }

// call ※ 1回だけしか出力されない

1回しか初期化処理が走らないことがわかりました。

まとめ

lazyなプロパティは
不要なメモリの消費やパフォーマンスの低下を防ぐことができますが
使い方を誤ると
わかりづらい不具合を混ぜてしまう可能性があることがわかりました。

このlazyのように
Swiftのドキュメントには
実は気をつけなければいけないことなどの
情報が書いてありますので
一度全てに目を通しておくことや
定期的にチェックすることは大事ですね?

もし何か間違いなどございましたら
ご指摘いただけますと嬉しいです??‍♂️

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

UIKitでNeumorphismのデザインを構築するライブラリ

Neumorphism(ニューモーフィズム)は、UIデザインの次のトレンドになるかもしれないと昨年末あたりから言われ出している表現手法です。本当にそうなるかどうかは分かりませんが、実験としてiOS用のライブラリ EMTNeumorphicView を作ってみました。ここではその実装アプローチについて書きます。

Neumorphismとは

フラットデザインやMaterial Designから、かつて流行したスキューモーフィックデザインに少し回帰したようなデザインといえばわかりやすいと思います。
EMTNeumorphicView screenshot

詳細は下記にあります。

自分もnoteを書きました。

凸型 (convex) のUIを作成する

Neumorphismのデザインを実現するには、要素に角Rを付けたうえで明暗2色のシャドウを落とさなければなりません。

実装方法はいろいろあると思いますが、EMTNeumorphicViewではシャドウ用のサブレイヤーを生成するカスタムCALayerを作りました。ライブラリではそれをlayerClassにしたUIView、UIButton、UITableViewCellを提供します。

EMTNeumorphicView.swift
    public override class var layerClass: AnyClass {
        return EMTNeumorphicLayer.self
    }

シャドウをCALayerで実装しているため、ボタンの状態変更にはCore Animationが効いています。
toggle buttons

カスタムCALayer内では、次のような感じで要素色、明るいシャドウ、暗いシャドウの3つのサブレイヤーを使っています。

EMTNeumorphicLayer.swift
masksToBounds = false

colorLayer = CALayer()
shadowLayer = EMTShadowLayer()
lightLayer = EMTShadowLayer()

insertSublayer(colorLayer!, at: 0)
insertSublayer(lightLayer!, at: 0)
insertSublayer(shadowLayer!, at: 0)

colorLayer?.masksToBounds = true
shadowLayer?.masksToBounds = false
lightLayer?.masksToBounds = false

明るいシャドウと暗いシャドウのレイヤーに、shadowPathを使って影をつけます。それぞれ左上方向と右下方向にオフセットし、Neumorphismのシャドウを表現します。

ShadowLayer.swift
let cornerRadius: CGFloat = 24
let shadowRadius: CGFloat = 8
let offsetWidth: CGFloat = shadowRadius / 2
let cornerRadii: CGSize = CGSize(width: cornerRadius - offsetWidth, 
                                 height: cornerRadius - offsetWidth)

var shadowX: CGFloat = offsetWidth
var shadowY: CGFloat = offsetWidth

// reverse offset direction
if mode == isLightSide {
    shadowX *= -1
    shadowY *= -1
}

// add shadow
let shadowBounds = bounds
let path = UIBezierPath(roundedRect: shadowBounds.insetBy(dx: offsetWidth, dy: offsetWidth),
                        byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight],
                        cornerRadii: cornerRadii)
shadowPath = path.cgPath
shadowOffset = CGSize(width: shadowX, height: shadowY)

凹型 (concave) のUIを作成する

凹型はトグルボタンやテーブルなどを表現するのに向いていますが、明暗2色をインナーシャドウで表現しなければならないため、凸型よりもずっと実装が面倒です。

要点は次のような感じです

  • 明暗2色のシャドウを2つのレイヤーで表現するのは凸型と同じ
  • 必要な部分のみ表示されるようにマスクをかけ、明暗が綺麗に合成されるようにする

ポイントとなる部分のみ説明します。まずインナーシャドウを作成します。
EMTNeumorphicViewでは、要素の形状に沿った中空のUIBezierPathを作成し、それをレイヤーのshadowPathとして使うことでインナーシャドウを描画しています。

ShadowLayer.swift
masksToBounds = true
cornerRadius = 24

let gap: CGFloat = 1
let cornerRadii = CGSize(width: cornerRadius + gap, height: cornerRadius + gap)
let innerRadius = cornerRadius - gap
let cornerRadiiInner = CGSize(width: innerRadius, height: innerRadius)

let outerPath = UIBezierPath(roundedRect: bounds.insetBy(dx: -gap, dy: -gap),
                             byRoundingCorners: corners,
                             cornerRadii: cornerRadii)
let innerPath = UIBezierPath(roundedRect: bounds.insetBy(dx: gap, dy: gap),
                             byRoundingCorners: corners,
                             cornerRadii: cornerRadiiInner).reversing()

outerPath.append(innerPath)
shadowPath = path.cgPath

問題は明暗2つのシャドウの合成です。凸型の場合はシャドウの重なる部分は要素の後ろ側に隠れてしまうので合成する必要はありませんでした。
凹型の場合、明暗が重なって入れ替わる部分(要素の右上と左下の角R部分)がうまく合成されるように個別にマスクをかけます。
マスク領域を説明するために色をつけると次のようになります。マゼンタがダークシャドウ用、シアンがライトシャドウ用です。
mask area

マスクを生成するためのカスタムレイヤーを作り、drawメソッド内でマスク領域を描画します。
以下はライトシャドウの右上の角R部分をマスクするためのコードです(黒→透明色のグラデーションを右下→左上へ描画)。
ダークシャドウの場合は同じ領域をグラデーションの方向を逆にして描画します。

GradientMaskLayer.swift
override func draw(in ctx: CGContext) {
    // top-right corner
    let cornerRect = CGRect(x: frame.size.width - cornerRadius,
                            y: 0, 
                            width: cornerRadius, 
                            height: cornerRadius)

    let bottomRight = CGPoint(x: cornerRect.maxX, y: cornerRect.maxY)

    guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                    colors: [UIColor.black.cgColor, UIColor.clear.cgColor] as CFArray,
                                    locations: [0, 1]) else { return }
    ctx.saveGState()
    ctx.addRect(cornerRect)
    ctx.clip()
    ctx.drawLinearGradient(gradient, start: bottomRight, end: cornerRect.origin, options: [])
    ctx.restoreGState()

まとめ

もっといい方法がある気もしますがとりあえずこんな感じです。

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