Swiftで配列の要素を別の位置に移動させたい場面は、アプリ開発でよく遭遇します。TODOリストの優先順位変更、プレイリストの曲順変更、お気に入りの並び替えなど、ユーザーが自由に順序を変更できる機能は、使いやすいアプリには欠かせません。
本記事では、Swift初心者の方にもわかりやすく、配列のmove
メソッドの使い方を基本から実践まで徹底的に解説します。
配列のmoveとは何か
move(fromOffsets:toOffset:)
は、配列内の要素を指定した位置から別の位置へ移動させるメソッドです。SwiftのCollectionプロトコルに含まれており、配列の並び替え機能を簡単に実装できます。
基本的な構文
配列.move(fromOffsets: 移動元のインデックス, toOffset: 移動先のインデックス)
最もシンプルな例
var fruits = ["りんご", "バナナ", "みかん", "ぶどう", "メロン"]
// "バナナ"(インデックス1)を最後(インデックス5)に移動
fruits.move(fromOffsets: IndexSet(integer: 1), toOffset: 5)
print(fruits)
// 結果: ["りんご", "みかん", "ぶどう", "メロン", "バナナ"]
このコードでは、インデックス1にある”バナナ”を配列の最後に移動させています。
IndexSetとは
move
メソッドで使うIndexSet
について理解しましょう。
IndexSetの基本
IndexSet
は、整数のインデックスの集合を表す型です。1つまたは複数のインデックスをまとめて扱えます。
// 単一のインデックス
let singleIndex = IndexSet(integer: 2)
// 複数のインデックス
let multipleIndices = IndexSet([0, 2, 4])
// 範囲指定
let rangeIndices = IndexSet(integersIn: 1...3)
なぜIndexSetを使うのか
通常の整数型(Int)ではなくIndexSetを使う理由は、複数の要素を同時に移動できるためです。
var numbers = [1, 2, 3, 4, 5, 6, 7, 8]
// インデックス1と3の要素を同時に移動
numbers.move(fromOffsets: IndexSet([1, 3]), toOffset: 7)
print(numbers)
// 結果: [1, 3, 5, 6, 7, 8, 2, 4]
moveメソッドの詳しい仕組み
パラメータの意味
func move(fromOffsets source: IndexSet, toOffset destination: Int)
- fromOffsets(移動元): 移動したい要素のインデックス集合
- toOffset(移動先): 移動先の位置(挿入位置)
移動先のインデックスの考え方
toOffset
は「移動後の挿入位置」を指します。少しわかりにくいので、図で確認しましょう。
var items = ["A", "B", "C", "D", "E"]
// インデックス: 0 1 2 3 4
// Bをインデックス4の位置に移動
items.move(fromOffsets: IndexSet(integer: 1), toOffset: 4)
// 結果: ["A", "C", "D", "B", "E"]
ポイント:
- 元の配列から”B”が取り除かれる
- 残った配列 [“A”, “C”, “D”, “E”] のインデックス4の位置(”E”の前)に挿入される
- 実際には”E”の直前に配置される
配列の最後に移動する場合
var colors = ["赤", "青", "緑", "黄"]
// "青"を最後に移動
colors.move(fromOffsets: IndexSet(integer: 1), toOffset: colors.count)
print(colors)
// 結果: ["赤", "緑", "黄", "青"]
配列の最後に移動するには、toOffset
に配列.count
を指定します。
実践的な使用例
例1: 配列の先頭に移動
var tasks = ["メール確認", "会議準備", "資料作成", "報告書提出"]
// "報告書提出"を最優先(先頭)に移動
tasks.move(fromOffsets: IndexSet(integer: 3), toOffset: 0)
print(tasks)
// 結果: ["報告書提出", "メール確認", "会議準備", "資料作成"]
例2: 隣の要素と入れ替え
var playlist = ["曲A", "曲B", "曲C", "曲D"]
// "曲C"と"曲D"を入れ替え(曲Cを1つ後ろへ)
playlist.move(fromOffsets: IndexSet(integer: 2), toOffset: 4)
print(playlist)
// 結果: ["曲A", "曲B", "曲D", "曲C"]
例3: 複数要素を同時に移動
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// インデックス0, 2, 4の要素(1, 3, 5)を配列の最後に移動
numbers.move(fromOffsets: IndexSet([0, 2, 4]), toOffset: numbers.count)
print(numbers)
// 結果: [2, 4, 6, 7, 8, 9, 10, 1, 3, 5]
例4: 範囲指定で移動
var letters = ["A", "B", "C", "D", "E", "F", "G"]
// インデックス1〜3の要素(B, C, D)をまとめて最後に移動
letters.move(fromOffsets: IndexSet(integersIn: 1...3), toOffset: letters.count)
print(letters)
// 結果: ["A", "E", "F", "G", "B", "C", "D"]
例5: カスタム型の配列で使用
struct Student {
let name: String
var rank: Int
}
var students = [
Student(name: "田中", rank: 1),
Student(name: "佐藤", rank: 2),
Student(name: "鈴木", rank: 3),
Student(name: "高橋", rank: 4)
]
// 田中さん(インデックス0)を3番目に移動
students.move(fromOffsets: IndexSet(integer: 0), toOffset: 3)
// 順位を更新
for (index, _) in students.enumerated() {
students[index].rank = index + 1
}
print(students.map { $0.name })
// 結果: ["佐藤", "鈴木", "田中", "高橋"]
SwiftUIでの実装方法
SwiftUIのListビューで並び替え機能を実装する方法を見ていきましょう。
基本的な並び替えリスト
import SwiftUI
struct TodoListView: View {
@State private var todos = [
"朝食を作る",
"メールをチェック",
"会議に参加",
"レポートを書く",
"買い物に行く"
]
var body: some View {
NavigationStack {
List {
ForEach(todos, id: \.self) { todo in
HStack {
Image(systemName: "line.3.horizontal")
.foregroundColor(.gray)
Text(todo)
}
}
.onMove(perform: moveTodo)
}
.navigationTitle("TODOリスト")
.toolbar {
EditButton()
}
}
}
func moveTodo(from source: IndexSet, to destination: Int) {
todos.move(fromOffsets: source, toOffset: destination)
}
}
EditButtonの役割
EditButton()
は、SwiftUIが提供する編集モード切り替えボタンです。このボタンをタップすると:
- リストが編集モードに入る
- 各行の左側にドラッグハンドル(三本線)が表示される
- 行をドラッグして並び替えできるようになる
.onMove
で指定した処理が実行される
優先順位付きTODOリスト
struct Task: Identifiable {
let id = UUID()
var title: String
var priority: Int
}
struct PriorityTodoView: View {
@State private var tasks = [
Task(title: "緊急の報告書", priority: 1),
Task(title: "メール返信", priority: 2),
Task(title: "資料作成", priority: 3),
Task(title: "企画書作成", priority: 4)
]
var body: some View {
NavigationStack {
List {
ForEach(Array(tasks.enumerated()), id: \.element.id) { index, task in
HStack {
// 順位表示
ZStack {
Circle()
.fill(priorityColor(index))
.frame(width: 35, height: 35)
Text("\(index + 1)")
.font(.headline)
.foregroundColor(.white)
}
VStack(alignment: .leading, spacing: 4) {
Text(task.title)
.font(.body)
Text("優先度: \(task.priority)")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.leading, 8)
}
.padding(.vertical, 4)
}
.onMove { source, destination in
tasks.move(fromOffsets: source, toOffset: destination)
updatePriorities()
}
}
.navigationTitle("優先順位管理")
.toolbar {
EditButton()
}
}
}
func updatePriorities() {
for (index, _) in tasks.enumerated() {
tasks[index].priority = index + 1
}
}
func priorityColor(_ index: Int) -> Color {
switch index {
case 0: return .red
case 1: return .orange
case 2: return .yellow
default: return .gray
}
}
}
カスタム編集モード制御
EditButtonを使わず、独自のボタンで編集モードを制御する方法です。
struct CustomEditView: View {
@State private var items = ["アイテム1", "アイテム2", "アイテム3", "アイテム4"]
@State private var editMode: EditMode = .inactive
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.self) { item in
HStack {
if editMode == .active {
Image(systemName: "line.3.horizontal")
.foregroundColor(.blue)
.font(.title3)
}
Text(item)
}
}
.onMove(perform: moveItem)
}
.navigationTitle("カスタムリスト")
.environment(\.editMode, $editMode)
.toolbar {
Button(editMode == .active ? "完了" : "並び替え") {
withAnimation {
editMode = editMode == .active ? .inactive : .active
}
}
.foregroundColor(.blue)
}
}
}
func moveItem(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
}
}
並び替えと削除の組み合わせ
実際のアプリでは、並び替えと削除を両方実装することが多いです。
struct FullFeaturedListView: View {
@State private var fruits = [
"りんご", "バナナ", "みかん", "ぶどう", "メロン", "いちご"
]
var body: some View {
NavigationStack {
List {
ForEach(fruits, id: \.self) { fruit in
HStack {
Image(systemName: "leaf.fill")
.foregroundColor(.green)
Text(fruit)
}
}
.onMove(perform: moveFruit)
.onDelete(perform: deleteFruit)
}
.navigationTitle("フルーツリスト")
.toolbar {
EditButton()
}
}
}
func moveFruit(from source: IndexSet, to destination: Int) {
fruits.move(fromOffsets: source, toOffset: destination)
print("並び替え後: \(fruits)")
}
func deleteFruit(at offsets: IndexSet) {
fruits.remove(atOffsets: offsets)
print("削除後: \(fruits)")
}
}
編集モードに入ると:
- 左側に赤い削除ボタン(マイナスアイコン)が表示される
- 右側にドラッグハンドルが表示される
- ドラッグで並び替え、削除ボタンで削除ができる
データ永続化との組み合わせ
並び替えた結果を保存する方法を見ていきましょう。
UserDefaultsで保存
struct PersistentListView: View {
@AppStorage("savedItems") private var savedItemsData: Data = Data()
@State private var items: [String] = []
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onMove(perform: moveItem)
}
.navigationTitle("保存されるリスト")
.toolbar {
EditButton()
}
.onAppear {
loadItems()
}
}
}
func moveItem(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
saveItems()
}
func loadItems() {
if let decoded = try? JSONDecoder().decode([String].self, from: savedItemsData) {
items = decoded
} else {
items = ["初期アイテム1", "初期アイテム2", "初期アイテム3"]
}
}
func saveItems() {
if let encoded = try? JSONEncoder().encode(items) {
savedItemsData = encoded
}
}
}
SwiftDataで保存
import SwiftUI
import SwiftData
@Model
class TodoItem {
var title: String
var order: Int
var createdAt: Date
init(title: String, order: Int) {
self.title = title
self.order = order
self.createdAt = Date()
}
}
struct SwiftDataTodoView: View {
@Environment(\.modelContext) var modelContext
@Query(sort: \TodoItem.order) private var todos: [TodoItem]
var body: some View {
NavigationStack {
List {
ForEach(Array(todos.enumerated()), id: \.element.id) { index, todo in
Text(todo.title)
}
.onMove { source, destination in
moveTodo(from: source, to: destination)
}
}
.navigationTitle("SwiftData TODO")
.toolbar {
EditButton()
}
}
}
func moveTodo(from source: IndexSet, to destination: Int) {
var todosArray = todos
todosArray.move(fromOffsets: source, toOffset: destination)
// 順序を更新
for (index, todo) in todosArray.enumerated() {
todo.order = index
}
try? modelContext.save()
}
}
よくある間違いと対処法
間違い1: 直接インデックスを指定
var items = ["A", "B", "C"]
// ❌ これはエラー
// items.move(from: 1, to: 2)
// ✅ 正しい書き方
items.move(fromOffsets: IndexSet(integer: 1), toOffset: 2)
move
メソッドはfromOffsets
パラメータでIndexSetを受け取ります。整数を直接渡すことはできません。
間違い2: 範囲外のインデックス
var colors = ["赤", "青", "緑"]
// ❌ インデックス5は存在しない
// colors.move(fromOffsets: IndexSet(integer: 5), toOffset: 0)
// 実行時エラーが発生
// ✅ 有効なインデックスを指定
if colors.indices.contains(2) {
colors.move(fromOffsets: IndexSet(integer: 2), toOffset: 0)
}
存在しないインデックスを指定すると実行時エラーが発生します。必ず配列の範囲内のインデックスを使いましょう。
間違い3: 編集モードを有効にしていない
// ❌ 編集モードが有効になっていない
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onMove(perform: moveItem)
}
// ドラッグしても何も起こらない
// ✅ EditButtonで編集モードを有効に
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onMove(perform: moveItem)
}
.toolbar {
EditButton()
}
SwiftUIで.onMove
を使う場合、編集モードを有効にする必要があります。
間違い4: IDが重複している
// ❌ IDが重複している
struct Item {
let id = 1 // すべて同じID
let name: String
}
// ✅ ユニークなIDを持たせる
struct Item: Identifiable {
let id = UUID()
let name: String
}
ForEach
で使用する要素は、ユニークな識別子を持つ必要があります。
パフォーマンスの考慮事項
大量のデータを扱う場合
// 配列のサイズが大きい場合は、moveの前に検証
func moveItem(from source: IndexSet, to destination: Int) {
guard source.allSatisfy({ $0 < items.count }),
destination <= items.count else {
print("無効なインデックス")
return
}
items.move(fromOffsets: source, toOffset: destination)
}
アニメーションの制御
func moveItem(from source: IndexSet, to destination: Int) {
withAnimation(.easeInOut(duration: 0.3)) {
items.move(fromOffsets: source, toOffset: destination)
}
}
実践的なTips
Tip 1: 並び替え後のコールバック
func moveItem(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
// 並び替え後の処理
onOrderChanged()
}
func onOrderChanged() {
print("新しい順序: \(items)")
// サーバーに保存、分析イベント送信など
}
Tip 2: アンドゥ機能の実装
@State private var items = ["A", "B", "C", "D"]
@State private var itemsHistory: [[String]] = []
func moveItem(from source: IndexSet, to destination: Int) {
// 現在の状態を履歴に保存
itemsHistory.append(items)
items.move(fromOffsets: source, toOffset: destination)
}
func undo() {
guard let previousState = itemsHistory.popLast() else { return }
items = previousState
}
Tip 3: 条件付き並び替え
func moveItem(from source: IndexSet, to destination: Int) {
// 特定の条件でのみ並び替えを許可
guard canReorder else {
print("並び替えが許可されていません")
return
}
items.move(fromOffsets: source, toOffset: destination)
}
まとめ
配列のmove
メソッドは、Swiftで並び替え機能を実装する際の基本的なツールです。
重要なポイント
- 基本構文 –
move(fromOffsets:toOffset:)
でIndexSetを使用 - IndexSet – 単一または複数のインデックスをまとめて扱える
- toOffsetの意味 – 移動先の挿入位置を指定
- SwiftUIでの使用 –
.onMove
とEditButton
の組み合わせ - データ永続化 – UserDefaultsやSwiftDataと連携
- エラー処理 – 範囲外のインデックスに注意
使い分けの目安
- 単一要素の移動 →
IndexSet(integer: インデックス)
- 複数要素の移動 →
IndexSet([インデックス1, インデックス2, ...])
- 範囲指定の移動 →
IndexSet(integersIn: 開始...終了)
- 配列の最後へ →
toOffset: 配列.count
- 配列の先頭へ →
toOffset: 0
move
メソッドを理解することで、ユーザーが自由に順序をカスタマイズできる、使いやすいアプリを作ることができます。まずは簡単なリストの並び替えから始めて、徐々に複雑な機能に挑戦していきましょう。