20190727のiOSに関する記事は5件です。

【iOS】iOS開発初心者の個人的に参考になったswift記事まとめ

趣旨

swift関連でお世話になった、参考になった記事のまとめ。
個人的に詰まった時にために参考にするためにまとめます。
勉強初めて1月程度の初心者なのでswift関連のおすすめ記事などあれば教えていただけると助かります。
多くなってきたら分類します・・・たぶん
随時更新していくはず

記事一覧

デリゲートの概要

デリゲートってそもそも何ってなった際に以下の記事を読んだ。
この記事を理解して初心者向けの記事読んだほうが個人的には良いと思った。
参考先

iOSの設定画面への遷移方法

アプリ側からiOS本体側への設定画面への遷移方法。
参考先

Slider

スライダーの記事。
参考先
参考先

地図

基本的なとこ
参考先

地図でのピンのたてかた

swift5では地味に違ってる気がする。
あとピンを複数立てる場合は配列で渡すようになってるみたい
参考先

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

画像の読み込み時に「Thread 1: EXC_BAD_INSTRUCTION 」エラーが出る

UIImageを読み込む際、
Buildは成功するが、画面描画の際に、下記エラーが出ていた。

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) image literal

UIImageViewで直接読み込もうとした際も下記エラーが出ていた。

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

解決法:
Assetsフォルダ内のContents.jsonで
「properties」
を丸ごと削除したら描画した。

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

FlutterViewControllerからiOSのUIViewControllerをpresentした際、下のFlutterがイベントを拾ってしまう件

FlutterでiOSのネイティブの画面をモーダル表示したい時、FlutterViewControllerからiOSのUIViewControllerをpresent()していたのだが、この際ハンドラが定義されてない部分タップすると後ろに隠れてるFlutter側のイベントが発火する挙動に困ってた。↓こういうの

Jul-27-2019 12-01-04.gif

問題の再現

原因調査

FlutterViewControllerの実装(engineのv1.5.4-hotfixesブランチ)を見ると、以下のようにtouchesBeganメソッド等の4メソッドをoverrideしており、ここからFlutter側にイベントを伝播させていることがわかる
https://github.com/flutter/engine/blob/v1.5.4-hotfixes/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#L649-L663

touchesBeganがどう呼び出されるかについては以下のappleのドキュメントを参照。iOS側のtouchイベントはResponder Chainを駆け上っていくようになっている
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events

今回の挙動に関係ある部分をまとめると

  • タッチイベントなら、タッチされたビューがファーストレスポンダとなる
  • 各ビューのnextプロパティにresponder chain上の次のレスポンダが入っているので、それで確認できる
  • UIViewオブジェクト
    • ビューがView Controllerのルートビューの場合、次のレスポンダはView Controller
    • それ以外の場合、次のレスポンダはビューのスーパービュー
  • UIViewController オブジェクト
    • View Controllerのビューがウィンドウのルートビューの場合、次のレスポンダはウィンドウオブジェクト
    • View Controllerが別のView Controllerによって表示されている場合、次のレスポンダは表示側のView Controller

つまり、今回のケースは以下のようになっていた

  • イベントハンドラがないUIの部分をタップする
  • タップイベントがiOSのResponder Chainを駆け上っていき、FlutterViewControllerのtouchesBegan()まで伝播
  • Flutter側のイベントハンドラが呼び出される

スクリーンショット 2019-07-27 11.31.09.png

解決策

FlutterViewControllerのtouchesBegan()等が呼び出されないようにすればよい。以下2つの方法が考えられる

1. FlutterViewControllerからpresent()したUIViewControllerのtouchesBegan等の4メソッドをoverrideする

FlutterViewControllerからpresentされるUIViewControllerに以下を追記する。これにより、FlutterViewControllerにアンハンドルなイベントを伝播しないようにする

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {}

参考:UIKitのヘッダファイルの該当箇所

// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

2. FlutterViewControllerからpresent()するのをやめる

iOS側のrootViewControllerをFlutterViewControllerではなくNavigationControllerにするなどして、FlutterViewControllerからUIViewControllerを直接present()しないようにする。
rootViewControllerを変更する方法はこちらの記事が参考になる
https://qiita.com/najeira/items/fa52ca4a9f544ae08300

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

【Swift】Swift5.1の新機能 Collection Diffing入門

Collection Diffingとは?

Swift5.1で導入される新しい機能です。

Forumスレッド一覧
https://forums.swift.org/search?q=SE-0240

Original Pitch
https://forums.swift.org/t/se-0240-ordered-collection-diffing/19514/34

Proposal
https://github.com/apple/swift-evolution/blob/master/proposals/0240-ordered-collection-diffing.md

Review
https://forums.swift.org/t/se-0240-ordered-collection-diffing/19514/42
https://forums.swift.org/t/amendment-se-0240-ordered-collection-diffing/26084

これはdifference(from:)difference(from:by:)を呼び出すことで
2つのコレクションの差分を計算して
何がどう変わっているのかを教えてくれます。

difference(from:)
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L188

またCollection Diffingは全てSwiftで書かれています。

今回はCollection Diffingがどういうものなのか
とても参考になる記事がありましたので
そちらを参考に実際の実装などを見ながら
理解をしていきたいと思います。

参考記事
https://www.fivestars.blog/code/swift-5-1-collection-diffing.html

最初に使用される要素を見ていき
その後にメソッドを見ていきたいと思います。

CollectionDifference

GenericなCollection
2つの整列されているコレクションの
差分(どの要素が削除され、どの要素が挿入されているのか)
を保持しています。

A collection of insertions and removals 
that describe the difference between two ordered collection states

CollectionDifference
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L16

ChangeElement

比較している要素の型でこれには何の制約も付いていません。
.insert.removeという2つのケースを持った
CollectionDifference.Changeというenumになっており
個々の要素の変化を表します。(詳細は後ほど記載します)

CollectionDifference.Change
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L19

CollectionDifferenceの特徴

CollectionDifferenceには下記のような特徴があります。

CollectionDifferenceが返す値の順番が決まっている

まず最初に削除された要素のコレクション
その次に追加された要素のコレクションという順番になっています。

これは内部のinitで保証されており
必ず適切な順番になって返ってきます。

init
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L168

CollectionDifferenceの要素はユニークである

initのコメントに下記のような記載があります。

The collection of changes passed as `changes` must meet these
requirements: 
- All insertion offsets are unique
- All removal offsets are unique
- All associations between insertions and removals are symmetric

つまり
削除や挿入された要素のオフセットは全てユニークでなければなりません。
また削除と挿入のこ関係は対称でなければなりません。

同じインデックスに対して削除の処理をしていたりした場合などはできません。
挿入に関しても同じです。

移動した要素は削除と挿入に対になる要素が一つ存在している。
どちらか片方だけというようなものは存在できません。

これは内部でバリデーションをしており
initで不適切なコレクションが渡ってきた場合はnilが返却されます。

_validateChanges
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L89

insertionsとremovalsプロパティ

これらの2つのプロパティを使用することで
元のコレクションの要素に直接アクセスすることができます。

insertions

挿入された要素がオフセットが小さいものから順番に並んでいます。

insertions
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L70

removals

削除された要素がオフセットが小さいものから順番に並んでいます。

removals
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L73

CollectionDifference.Change

2つのコレクションを比較した時に
変化としては下記の3つのパターンがあります。

  • 新しい要素が追加された
  • 要素が削除された
  • 移動して位置が変わった(元の位置から削除され、新しい位置に挿入された)

そのため
CollectionDifference.Change
insertとremoveの2つのenumで表現できます。

CollectionDifference.Change
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L19

それぞれのcaseはoffset element associatedWithという
3つのassociated valuesを持っています。

offset

挿入または削除された位置です。

削除された場合

let oldArray = ["a", "b", "c", "d"]
let newArray = ["a", "b", "d"]

// "c"という要素がindex 2から削除されました

let difference = newArray.difference(from: oldArray) 
// differenceは1つの要素を含んだ配列です。
// これは`.remove`で`offset`は2となります。

挿入された場合

let oldArray = ["a", "b", "c", "d"]
let newArray = ["a", "b", "c", "d", "e"]

// "e"という要素がindex 4に挿入されました

let difference = newArray.difference(from: oldArray) 
// differenceは1つの要素を含んだ配列です。
// これは`.insert`で`offset`は4となります。

element

実際の要素です。
これがCollectionDifferenceがジェネリックになっている要因です。

let oldArray = ["a", "b", "c", "d"]
let newArray = ["a", "b", "d", "e"]

// "c"という要素をindex 2から削除
// "e"という要素をindex 3に追加

let difference = newArray.difference(from: oldArray)
// differenceは2つの要素の配列
// 最初の要素は.removeでoffset 2、element "c"
// 次の要素は.insertでoffset 3、element "e"

associatedWith

要素を移動した時にこの値が入ります。
insertの場合は移動後のオフセット
removeの場合は移動前のオフセット
が入ります。

これは計算に時間がかかるため
デフォルトでは全てnilとなっており
inferringMoves()を呼び出す必要があります。

inferringMoves
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L323

let oldArray = ["a", "b", "d", "e", "c"]
let newArray = ["a", "b", "c", "d", "e"]

// "c"をindex 4から2へ移動
// - index 4から削除
// - index 2へ追加

let difference = newArray.difference(from: oldArray)
// differenceは2つの要素の配列
// - 最初の要素は`.remove` offset 4 associatedWith nil
// - 次の要素は`.insert` offset 2 associatedWith nil

let differenceWithMoves = difference.inferringMoves()
// differenceWithMovesは2つの要素の配列
// - 最初の要素は`.remove` offset 4 associatedWith 2
// - 次の要素は`.insert` offset 2 associatedWith 4

InferringMoves

CollectionDifferenceインスタンスをスキャンして
削除と挿入のオフセットがマッチした(移動した)要素をコレクションに格納していきます。
もし存在しなければ元のコレクションと同じ要素が含まれたコレクションを返します。

CollectionDifferenceの要素の並び順

initの中を見てみるとわかるのですが
要素の順番は

削除が先 挿入があと

になります。

挿入された要素はinsertions配列に入り
削除された要素はremovals配列に入ります。

しかしsubscriptを見てみると
また違った順番で要素を参照しています。

removalsは大きいオフセットから小さいオフセットへ
insertionsは小さいオフセットから大きいオフセットに向かっています。

subscript
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L253

これは順番に取得した要素を元のコレクション(変化前のコレクション)に適用していくと
新しいコレクションになるようになっており
変化を再現することができます。

let oldArray = ["a", "b", "c", "d"]
let newArray = ["x", "a", "e", "c"]

var anotherArray = oldArray

let difference = newArray.difference(from: oldArray)
// differenceは4つの要素の配列
// 1. `.remove` at offset 3
// 2. `.remove` at offset 1 
// 3. `.insert` at offset 0
// 4. `.insert` at offset 2

for change in difference {
  switch change {
  case let .remove(offset, _, _):
    anotherArray.remove(at: offset)
  case let .insert(offset, newElement, _):
    anotherArray.insert(newElement, at: offset)
  }
}
// この時点で`anotherArray`は`newArray`と等しくなる 

遷移をたどると下記のようになります。

["a", "b", "c", "d"] // 初期状態
["a", "b", "c"] // "d"をindex 3から削除
["a", "c"] // "b"をindex 1から削除
["x", "a", "c"] // "x"をindex 0へ挿入
["x", "a", "e", "c"] // "e"をindex 2へ挿入

applying

上記ではdifference(from:)で取得した要素をコレクションに順番に適用してきましたが
applying(_:)メソッドを使用することで変化を別のコレクションに適用できます。

applying
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L68

let oldArray = ["a", "b", "c", "d"]
let newArray = ["x", "a", "e", "c"]

var anotherArray = oldArray

let difference = newArray.difference(from: oldArray)

// ここでdifferenceをapplyingしている
anotherArray = anotherArray.applying(difference)!

// この時点で`anotherArray`は`newArray`と等しくなる 

このメソッドは制約に適合している限りはどんなコレクションにも適用できます。
戻り値としてdifferenceを適用したOptionalなコレクションを返します。

なぜOptionalなのかは
differenceの要素のオフセットが
適用対称のコレクションのオフセットを超えた時に
クラッシュを避けてnilを返すようにしています。

difference(from:)

これまでも出てきましたCollection Diffingの中心となるメソッドです。

extension BidirectionalCollection where Element : Equatable {
  public func difference<C: BidirectionalCollection>(
    from other: C
  ) -> CollectionDifference<Element> where C.Element == Self.Element {
    return difference(from: other, by: ==)
  }
}

difference(from:)difference(from: by:)を呼び出しているだけで
要素がEquatableの時に使用できます。

difference(from: by:)

byに等しくなる条件を指定することでEquatableに適合していない要素にも
differenceを取得することができるようになります。

public func difference<C: BidirectionalCollection>(
  from other: C,
  by areEquivalent: (Element, C.Element) -> Bool
) -> CollectionDifference<Element>
where C.Element == Self.Element 

difference(from:by:)
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L134

シンタックスを見てみると2つのコレクションは
BidirectionalCollectionに適合している必要があります。

つまり前から後からもコレクションの要素へ行ったり来たり参照できることが必要になります。
また2つのコレクションはassociated typeの型は同じである必要があります。

byの条件によってdifferenceで取得できる要素が変わってきます。

例えば全てに対してfalseを返すようにすると

let oldArray = ["a", "b", "c"]
let newArray = ["a", "b", "c"] // 同じ要素

let difference = newArray.difference(from: oldArray, 
                                     by: { _, _  in false })
// `difference`は6つの要素を持つ配列:
// - removalsに3要素
// - insertionsに3要素

これは全ての要素の比較がfalseになるので
要素を全部入れ替えたとみなされているからです。

difference(from: by:) の戻り値

Optional*ではない*CollectionDifferenceを返します。

上記でCollectionDifferenceのinitは
nilを返す可能性があることを紹介しましたが
なぜ違うのか?

これは内部でnilにならないinitを保持しており
そちらを使用しているからです。

これは妥当だと証明されたコレクションを使用しているので
Swift的にもnon-optionalで問題ありません。

init
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/CollectionDifference.swift#L168

difference(from: by:)の内部

まず_CountingIndexCollectionのコレクションを2つ作成し
これを_CollectionChangesへの変換しなおしています。

let source = _CountingIndexCollection(other)
let target = _CountingIndexCollection(self)

let result = _CollectionChanges(from: source, to: target, by: areEquivalent)

_CountingIndexCollectionは実際のコレクションを包んで
開始のindexからのoffsetへアクセスしやすくします。

let originalCollection = ["a", "b", "c", "d"]
let offsetCollection = _CountingIndexCollection(originalCollection)
if let index = offsetCollection.index(of: "d") {
  print(index.offset) // 3
  print(offsetCollection[index]) // d
}

_CollectionChangesはCollectionDifference.Changeインスタンスの配列です。

その後取得したChangeインスタンスの配列から
CollectionDifferenceのインスタンスを生成して返却しています。

このメソッドないでは実際の差分計算はしておらず
_CollectionChangesの初期化の中で行っています。

_CollectionChanges

2つのコレクションの要素を辿っていき、下記のようなことを行っています。

この2つは表裏一体のような関係となっており
片方が見つかるともう片方も見つかるようになっています。

挿入や削除は非常にコストの高い操作であるため
このプロセスで最も効率差分更新するための方法を導き出すことが
Collection Diffingの根幹になります。

_CollectionChanges
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L247

ある状態からある状態への遷移方法は複数見つかる中で
もっとも効率の良いものを_CollectionChangesは導き出すことを保証します。

Endpoint

_CollectionChangesの定義の冒頭にあるtypealiasです。

typealias Endpoint = (x: SourceIndex, y: TargetIndex)

これはsourceコレクションとtargetコレクションの関係を表します。
例えばsourceコレクションの最初の要素とtargetコレクションの2番目の要素を関連づけたい場合
Endpointは(1,2)と定義します。

pathStorage

ここからは_CollectionChangesが
具体的にどういう計算をしているのかを見ていきます。

下記のような2次元のチャートがあります。

x軸がsourceのコレクションを
y軸はtargetのコレクションを表します。

  | • | X | A | B | C | D |
 • |   |   |   |   |   |   |
 X |   |   |   |   |   |   |
 Y |   |   |   |   |   |   |
 C |   |   |   |   |   |   |
 D |   |   |   |   |   |   |

左上を(0,0)として一番右下の(lastIndexOfTarget, lastIndexOfSource)を目指します。

下記のようなイメージです。
```
| • | X | A | B | C | D |
• | S | | | | | |
X | | | | | | | // S: 開始
Y | | | | | | | // T: 目的地
C | | | | | | |
D | | | | | | T |

// 開始位置 S: (0,0)
// 目的位置 T: (4,5)
```

ルール
- 左から右にしか移動できない
- 上から下にしか移動できない
- 左右と上下は同じ距離だけ同時に動かして良い(左に1、下に1など)
- 左右と上下を同時に動かして良いのはxとyが同じ時だけ((A,A)の時など)

この中でもっとも少ない数で対角線の位置に移動する方法を探します。

これが最長共通部分列問題です。

_CollectionChangesに当てはめると

  • 垂直への移動が挿入
  • 平行への移動が削除

になります。

結果は下記になります。

  | • | X | A | B | C | D |
 • | S |   |   |   |   |   | 
 X |   | x | x | x |   |   | // S: 開始 
 Y |   |   |   | x |   |   | // T: 目的地
 C |   |   |   |   | x |   |
 D |   |   |   |   |   | T |

// 経路:
(0,0) → (1,1) → (1,2) → (1,3) → (2,3) → (3,4) → (4,5)

これを_CollectionChangesで考えると

  • (1,1) → (1,2)、(1,2) → (1,3)は2つの削除
  • (1,3) → (2,3)は1つの挿入

になります。

この一つ一つの地点がEndpointであり
pathはEndpointの配列になります。

これがpathStorageプロパティになります。

pathStorage
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L276

pathStartIndex

_CollectionChangesが持つもう一つのプロパティで
差分計算のパスを探す開始位置を表します。

これは内部最適化のために使われ
例えばAEOSとAEFTという文字列があった場合
AEは共通なのでこの分の計算は無駄になります。
そこでpathStartIndexを見てこの無駄をスキップします。

pathStartIndex
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L279

enum Element

internalな要素で上記の計算で見つけたパスを
CollectionDifference.Changeに変換するときに使用します。

enum Element {
    case removed(Range<SourceIndex>)
    case inserted(Range<TargetIndex>)
    case matched(Range<SourceIndex>, Range<TargetIndex>)
}

_CollectionChanges.Element
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L300

※ なぜRangeなのか?

上記の問題の例では一つ一つEndpointを分けて考えましたが
(1,1) → (1,2) → (1,3)は連続した平行移動で
(1,1) → (1,3)と一緒にできます。
これを表現するためにRangeとなっています。

differenceメソッドでは
このElementを一つ一つ取り出してCollectionDifference.Changeへと変換していきます。

_CollectionChangesの内部

まず中心となるinitの形です。

fileprivate
init<Source: BidirectionalCollection, Target: BidirectionalCollection>(
    from source: Source,
    to target: Target,
    by areEquivalent: (Source.Element, Target.Element) -> Bool
) where
    Source.Element == Target.Element,
    Source.Index == SourceIndex,
    Target.Index == TargetIndex
{
    self.init()
    formChanges(from: source, to: target, by: areEquivalent)
}

空のinitは下記になります。

private init() {
    self.pathStorage = []
    self.pathStartIndex = 0
}

init()
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L291

formChangesメソッドでpathStorageEndpointを追加していきます。

formChanges
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L372

下記ような処理を行っています。

最初のチェック

最初のチェックとして下記の2つを行います。

  1. 両方のコレクションが空の場合

すぐに処理を中断し、diffrenceの結果も空になります。

  1. 共通の接頭辞を見つけ、かつ1つのコレクションがもう1つのコレクションの全要素を網羅している場合

上記の最短経路問題で考えると
右か下にぶつかるまで斜めに移動することができ
その後平行か垂直移動して対角線にいきます。

いくつかの例を見てイメージしてみます。

// source = ASDFO 
// target = ASD
// targetはsourceの接頭辞になっています:

   | • | A | S | D | F | O |
 • | S |   |   |   |   |   | 
 A |   | x |   |   |   |   | // S: 開始 
 S |   |   | x |   |   |   | // T: 目的地
 D |   |   |   | x | x | T |
                 ↑
        ここで端にぶつかって平行移動している

// 最終経路:
(0,0) → (3,3) → (3,5)

// - 3つが一致 (0,0) → (3,3)
// - 2つ削除 (FとO), (3,3) → (3,5)

// source = ASD
// target = ASDFO
// sourceがtargetの接頭辞になっています:

   | • | A | S | D |
 • | S |   |   |   | // S: 開始
 A |   | x |   |   | // T: 目的地
 S |   |   | x |   | 
 D |   |   |   | x | ← ここで端にぶつかって垂直移動している
 F |   |   |   | x |
 O |   |   |   | T |

// 最終経路:
(0,0) → (3,3) → (5,3)

// - 3つが一致 (0,0) → (3,3)
// - 2つ挿入 (FとO), (3,3) → (5,3)

// source = ASD
// target = ASD
// 両方同じ要素

   | • | A | S | D |
 • | S |   |   |   | // S: 開始 
 A |   | x |   |   | // T: 目的地
 S |   |   | x |   | 
 D |   |   |   | T | ← 端にぶつかる

// 最終経路:
(0,0) → (3,3)

// - 3つが一致 (0,0) → (3,3)

上記では2か3つの要素を持ったpathStorageができます。

formChangesCore

上記の条件に合わない場合は
下記のような状態になっています。

  • 2つのコレクションの要素が等しくない(byで示した条件ですべてtrueにならない)
  • 2つのコレクションが空ではない
  • 共通の接頭辞がある可能性がある

この際にはformChangesCoreメソッドを呼び出します。

formChangesCore
https://github.com/apple/swift/blob/a0421124f0de37d696e503c17c05afba19f74cf2/stdlib/public/core/Diffing.swift#L411

引数には2つのコレクションと等しいとみなすための条件に加えて
最初のロジックで見つけた共通の接頭辞のindexです。
これを渡すことで不要な計算をスキップします。

このメソッド内では
Greedy LCS/SES Algorithmを実装しています。
http://www.xmailserver.org/diff2.pdf

詳細は割愛しますが簡単に何をしているのかを示すと

  1. まずはもっとも時間がかかるように斜め移動なしでのDまでの移動手順を見つけます。
  2. この時点で目的地に到着したら計算を終了します。
  3. 到着していない場合、Dの値を一つずつ増やして再び最初から計算を開始します。

この際により最適化するために前のパスをロジックに従った形で保存しておくことで
できる限り計算の無駄をなくすようにされています。

ご興味のある方は冒頭の参照記事をご覧ください。

まとめ

Collection Diffingについて見てきました。
使う側としては非常にシンプルに使えますが

内部の複雑なアルゴリズムに支えられ
安全に効率良く差分計算を実現されていることもわかりました。

参照記事の筆者も結論で述べていますが
これはCollectionViewTableViewでは使用しない方が良く
代わりにDiffable Data Sourcesというより最適化されたAPIが
iOS13で用意されています。

詳細は下記にまとめましたのでもしよろしければご参照ください
https://qiita.com/shiz/items/a6032543a237bf2e1d19

Swiftはまだまだ発展途上で
たくさんのプロポーサルが出てきています。

これからもどんな新しい機能が増えていくのか楽しみですね:smiley:

もし間違いなどございましたら教えていただけると嬉しいです:bow_tone1:

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

WebAuthnでBLEデバイスを使う方法

iOSデバイスを外部認証機器と使うためにはCTAPのBLEの実装が必要ですが、どこのWebAuthnのデモサイトにアクセスしてもBluetooth機器の認証を目にすることができませんでした。

調べてみたところ、Bluetoothの認証が出るか否かはWebAuthnのサーバ側の設定ではなく、アクセス元のブラウザがWeb Bluetooth APIに対応しているか否かで決まるそうです。

Web Bluetooth APIとは

  • WebブラウザがBluetoothを制御するためのAPI
  • ChromeやOperaでは利用できるが、FirefoxやIEでは対応は微妙らしい

ChromeでWeb Bluetooth APIを有効にする方法

Web Bluetooth APIは隠し機能のため、通常の設定メニューには出てきません。

アドレスバーに chrome://flags/ を入力し、「Web Authentication API BLE support」をenableにします。

スクリーンショット 2019-07-27 0.47.21.png

設定を行った上で、Webauthn.ioにアクセスすると、次のようにBluetoothセキュリティキーが選択肢に現れます。

スクリーンショット 2019-07-27 0.46.20.png

注意点

iOSのBluetoothアプリを作成し、chromeのWebauthnと通信が行われるかを確認しましたが、OSのバージョンやBluetoothのバージョンによっては通信ができませんでした。

クライアント側

OS

  • Windows10 → NG(ChromeでWeb BluetoothをONにしても選択詞が出てこない)
  • macOS 10.14.3 → OK
  • macOS 10.15 beta4 → NG(選択詞は表示されるが、PeripheralであるiOSデバイスに通信が飛ばない)

BlueTooth IF

  • 4.0 → NG(PeripheralであるiOSデバイスに通信が飛ばない)
  • 4.2 → OK

認証機器側

OS

  • iOS 12.4 → OK
  • iOS 13 beta 4 → NG(Central側からの通信が受信されない)

今のところ、アプリの実装が試せそうなのはBlueTooth4.2を搭載のmacOS 10.14とiOS12端末の組み合わせのみのようです。

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