[SwiftUI] OutlineGroup で選択可能にする

SwiftUI2021

     

TAGS:

⌛️ 3 min.
SwiftUI での Outline 表示を調べていて気づいたことのメモ

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.2
  • Xcode 13.2.1
  • iOS 15.2

OutlineGroup

SwiftUI で アウトライン表示をしようと考えた時に、最初に候補にあがるのが、OutlineGroup です。

Apple のドキュメントは、こちら

OutlineGroup 使い方

Apple のドキュメントにもありますが、シンプルに使うのは、非常に簡単です。

OutlineGroup


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/01/27
//  © 2022  SmallDeskSoftware
//

import SwiftUI

struct FileItem: Hashable, Identifiable, CustomStringConvertible {
    var id: Self { self }
    var name: String
    var children: [FileItem]? = nil
    var description: String {
        switch children {
        case nil:
            return "📄 \(name)"
        case .some(let children):
            return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
        }
    }
}

let data = FileItem(name: "users", children:
    [FileItem(name: "user1234", children:
      [FileItem(name: "Photos", children:
        [FileItem(name: "photo001.jpg"),
         FileItem(name: "photo002.jpg")]),
       FileItem(name: "Movies", children:
         [FileItem(name: "movie001.mp4")]),
          FileItem(name: "Documents1", children: [])
      ]),
     FileItem(name: "newuser", children:
       [FileItem(name: "Documents2", children: [])
       ])
    ])

struct ContentView: View {
    @State private var selection: FileItem?
    var body: some View {
        VStack {
            OutlineGroup(data, children: \.children) { item in
                Text("\(item.description)")
            }
        }
            .padding()
    }
}

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

OutlineGroupの難しい点

シンプルに使う時には、直感的でわかりやすいのですが、以下のことをしようとすると、徐々に難しくなっていきます。

・要素の選択
・Drag&Drop

OutlineGroup での要素選択

Apple のサンプルのままでは、要素を選択できるようになりません。

OutlineGroup には、selection を引数として持つ initializer が存在しないのです。

以下のように OutlineGroup を List に内包するようにすると、selection できるようになります。

SelectableOutlineGroup


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/01/27
//  © 2022  SmallDeskSoftware
//

import SwiftUI

struct FileItem: Hashable, Identifiable, CustomStringConvertible {
    var id: Self { self }
    var name: String
    var children: [FileItem]? = nil
    var description: String {
        switch children {
        case nil:
            return "📄 \(name)"
        case .some(let children):
            return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
        }
    }
}

let data = FileItem(name: "users", children:
    [FileItem(name: "user1234", children:
      [FileItem(name: "Photos", children:
        [FileItem(name: "photo001.jpg"),
         FileItem(name: "photo002.jpg")]),
       FileItem(name: "Movies", children:
         [FileItem(name: "movie001.mp4")]),
          FileItem(name: "Documents1", children: [])
      ]),
     FileItem(name: "newuser", children:
       [FileItem(name: "Documents2", children: [])
       ])
    ])

struct ContentView: View {
    @State private var selection: FileItem?
    var body: some View {
        VStack {
            List(selection: $selection) {
                OutlineGroup(data, children: \.children) { item in
                    Text("\(item.description)")
                }
            }
        }
        .environment(\.editMode, .constant(.active))
            .padding()
    }
}

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

OutlineGroup の root 要素選択

スクリーンショットでも気づくのですが、一番 root に存在する要素 (users) を選択することができません。

OutlineGroup では、root 要素は選択対象にできないようです。

今回の例で言うと、users は、root 要素ですので、選択対象となりません。

以下のように、root 要素を [FileItem] とすることで、選択対象とすることができます。

SelectableRoot


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/01/27
//  © 2022  SmallDeskSoftware
//

import SwiftUI

struct FileItem: Hashable, Identifiable, CustomStringConvertible {
    var id: Self { self }
    var name: String
    var children: [FileItem]? = nil
    var description: String {
        switch children {
        case nil:
            return "📄 \(name)"
        case .some(let children):
            return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
        }
    }
}

let data = [  // 👈 最初の FileItem も配列に入れるのがポイント
  FileItem(name: "users", children:
    [FileItem(name: "user1234", children:
      [FileItem(name: "Photos", children:
        [FileItem(name: "photo001.jpg"),
         FileItem(name: "photo002.jpg")]),
       FileItem(name: "Movies", children:
         [FileItem(name: "movie001.mp4")]),
          FileItem(name: "Documents1", children: [])
      ]),
     FileItem(name: "newuser", children:
       [FileItem(name: "Documents2", children: [])
       ])
    ])]

struct ContentView: View {
    @State private var selection: FileItem?
    var body: some View {
        VStack {
            List(selection: $selection) {
                OutlineGroup(data, children: \.children) { item in
                    Text("\(item.description)")
                }
            }
        }
        .environment(\.editMode, .constant(.active))
            .padding()
    }
}

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

OutlineGroup の ドラッグ&ドロップ

OutlineGroup の子要素として作成する View に .onDrag を設定することで、ドラッグは可能になるのですが、ドロップ可能になる方法がわかりません・・・

通常は、onInsert を ForEach に設定することで対応しますが、OutlineGroup の場合は、対応する ForEach は存在しません。

もちろん、子 View に設定することはできませんし、List に設定することもできません。

既存の OutlineGroup を使うことでは、ドラッグ&ドロップ操作は実装できないようです。

つまり、自分で onInsert や onDrag を使って実装する必要があります。

このような実装は、OutlineGroup のままではできないので、ForEach に分解する必要があります。

まとめ

OutlineGroup を使う時に役立ちそうなことをまとめてきました。

OutlineGroup を使う時に役立ちそうなメモ
  • List に内包させることで、selection はできる
  • root 要素も、Array に入れることで selection 可能になる
  • Drag&Drop は、自分で”1から”実装する必要がある

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です