【Swift】配列のmove関数を調査

Swiftではこれまで配列要素に対して位置の移動をさせる簡潔なメソッドがなく、こちらの[Stackoverflow](https://stackoverflow.com/questions/36541764/how-to-rearrange-item-of-an-array-to-new-position-in-swift)の記事のように、配列から取り除いて、取り除いた要素を任意の箇所に挿入するという手間をかけていました。

```swift:temp.swift
let element = arr.remove(at: 3)
arr.insert(element, at: 2) 
```

しかし、iOS13のランタイム依存関数ではありますが、今では**以下の関数を利用することでシンプルに配列要素の移動を行うことが出来ます。**

```swift:move.swift
mutating func move(fromOffsets source: IndexSet, toOffset destination: Int)
```

[この関数](https://developer.apple.com/documentation/swift/mutablecollection/3348325-move)は[MutableCollection](https://developer.apple.com/documentation/swift/mutablecollection)プロトコルに宣言されています。

ただ、ドキュメントを見ても記事投稿時点では解説がまだ何も書いていないので、少しサンプルコードとテストコードを利用しながら挙動を探っていこうと思います。

# 用語
オフセット: 先頭要素からの距離

# 挙動調査
気になるのは、`fromOffsets`を`IndexSet`で渡して、`toOffset`を`Int`で渡しているところです。

IndexSetは整数で値被りのないコレクションなのですが、複数インデックスを渡して、対象のオフセットに移動させる...。という操作のイメージが今ひとつ湧かないので、その辺りも明らかにしていきます。

## サンプル1.ひとつだけIndexを渡して移動させる
文字列の"a"から"e"までを配列にして、中身の要素を動かしながら検証していきます。今回の検証では"b"を動かします。



```swift:tests.swift

    func testMoveItemToOffset0() {
        var items = ["a", "b", "c", "d", "e"]
        items.move(fromOffsets: IndexSet([1]), toOffset: 0)
        XCTAssertEqual(items.joined(), "bacde")
    }
    
    func testMoveItemToOffset1() {
        var items = ["a", "b", "c", "d", "e"]
        items.move(fromOffsets: IndexSet([1]), toOffset: 1)
        XCTAssertEqual(items.joined(), "abcde")
    }

    func testMoveItemToOffset2() {
        var items = ["a", "b", "c", "d", "e"]
        items.move(fromOffsets: IndexSet([1]), toOffset: 2)
        
        // expected
        // XCTAssertEqual(items.joined(), "abcde")

        // actual
        XCTAssertEqual(items.joined(), "abcde")
    }
    
    func testMoveItemToOffset3() {
        var items = ["a", "b", "c", "d", "e"]
        items.move(fromOffsets: IndexSet([1]), toOffset: 3)
        
        // expected
        // XCTAssertEqual(items.joined(), "acdbe")

        // actual
        XCTAssertEqual(items.joined(), "acbde")
    }
    
    func testMoveItemToOffset4() {
        var items = ["a", "b", "c", "d", "e"]
        items.move(fromOffsets: IndexSet([1]), toOffset: 4)
        
        // expected
        // XCTAssertEqual(items.joined(), "acdeb")

        // actual
        XCTAssertEqual(items.joined(), "acdbe")
    }
```

**自分が期待した挙動をしません。なぜだろう**

### SwiftUIでどう使われるかを見る
このmoveメソッドは、SwiftUIの`onMove`と`onDelete`に対応するように作られたようなので、実際に`List`の移動でどのように出力されるか検証します。

コードはこちらからお借りしました。よければお手元の環境でお試しください。
https://www.hackingwithswift.com/quick-start/swiftui/how-to-let-users-move-rows-in-a-list


```swift:ContentView.swift
struct ContentView: View {
    @State private var users = ["a", "b", "c", "d", "e"]

    var body: some View {
        NavigationView {
            List {
                ForEach(users, id: \.self) { user in
                    Text(user)
                }
                .onMove(perform: move)
            }
            .navigationBarItems(trailing: EditButton())
        }
    }

    func move(from source: IndexSet, to destination: Int) {
        print("source:\(source.first!)")
        print("destination:\(destination)")
        
        users.move(fromOffsets: source, toOffset: destination)
    }
}

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

```

#### bをbの位置のまま離す

<img width="200" alt="スクリーンショット 2020-01-30 16.40.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/2d0e3d5e-dc77-29f8-2f44-8cfcdc3c5cb8.png">

```
source:1
destination:1
```

#### bを先頭にする

<img width="200" alt="スクリーンショット 2020-01-30 16.41.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/5f8ed24a-2efe-7f5b-c798-0aec8fddfea5.png">

```
source:1
destination:0
```

#### bをcの次に持ってくる

<img width="200" alt="スクリーンショット 2020-01-30 16.40.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/6e5801b6-82f3-443d-63dd-49ddae0610d2.png">

```
source:1
destination:3
```

### bをdの次にもってくる
<img width="200" alt="スクリーンショット 2020-01-30 16.43.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/1b6663e1-9c25-c0a5-38b6-8d215f89c6c3.png">

```
source:1
destination:4
```

#### bを最後に持ってくる
<img width="200" alt="スクリーンショット 2020-01-30 16.44.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/2000fc93-27aa-cca0-5ac5-0add18863e19.png">

```
source:1
destination:5
```

## 実験結果
自分では

1. `fromOffsets`で指定した要素を`remove`
2. `fromOffsets`で指定した要素を`toOffset`位置にinsert

という解釈で進めていたのですが、**実際の挙動のイメージとしては、自分の想定と実行順が逆で以下のような形です。**

1. `fromOffsets`で指定した要素を`toOffset`位置にinsert
2. `fromOffsets`で指定した要素を`remove`する。

※あくまでイメージなので、実際の内部挙動と異なる場合があります。

例えば、"c"の次に"b"を持ってきたい場合、以下のようになります。

```swift:変更前配列.swift
var items = ["a", "b", "c", "d", "e"]
items.move(fromOffsets: IndexSet([1]), toOffset: 3)
// a is at 0
// b is at 1
// c is at 2
// d is at 3 ← この位置のindexを指定
// e is at 4

// result: items[2] is "b"
```

この違いについては、**自分の事前イメージが誤っており、正しい挙動の方が正しいAPIデザインである**と感じます。

理由としては、指定した要素を取り除いた後の配列に対しての追加先index指定だと`同じ関数の引数なのに、後者は副作用が出た前提でデザインされている`となるからです。

正しい挙動では、両方の引数が副作用が出ていない状態の配列に対する指定になるため、きれいに揃っています。

# サンプル2. 複数のIndexを渡して移動させる
一つ前のサンプルで`toOffset`として渡す引数の法則が分かったので、このサンプルはそれを前提に進めていきます。

```swift:moveItems.swift

    func testMoveItemsToOffset3() {
        var items = ["a", "b", "c", "d", "e"]
        items.move(fromOffsets: IndexSet([0, 1]), toOffset: 3)
        
        XCTAssertEqual(items.joined(), "cabde")

    }

```

このテストケースでは`IndexSet([0, 1])`を渡しています。なので、"c"の次に"a", "b"が挿入され、`cabde`という文字列が得られることを期待しましたが、期待通りの結果を得ることが出来ました。

# まとめ
いくつかコードを書いて実験しながら調査を進めることで、MutableCollectionのmoveメソッドの挙動について理解を深めることが出来ました。ざっくりまとめるために、Apple風ドキュメントを書いて終わろうと思います。

```swift:Doc.swift
/// 指定した要素を、指定した場所に移動させる。
/// - Parameters:
///   - source: 移動させる要素のオフセットのIndexSet
///   - destination: 移動先のオフセット。要素移動を適応していないインデックスを指定する
///
///    var items = ["Alice", "Bob", "Chales", "David", "Elen"]
///    items.move(fromOffsets: IndexSet([0, 1]), toOffset: 3)
///     items[0]: Chales
///     items[1]: Alice
///     items[2]: Bob
///     items[3]: David
///     items[4]: Elen
mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) 
```