OutlineGroup is an analogue to NSOutlineView. NSOutlineView supports single/multiple node selection and we can obtain them by querying on NSOutlineView. Though obtaining selection on NSOutlineView is O(n), but this can be optimized to O(1) if the view tracks selection and provide them in proper interface.
How to obtain selections from OutlineGroup? Especially for multiple node selections. I checked out the manual entry, but couldn't find any mention about selection. What am I missing here?
Here is the code.
import SwiftUI
struct ContentView: View {
#State var selection = Set<FileItem.ID>()
var body: some View {
NavigationView {
VStack {
List(selection: $selection) {
OutlineGroup(data, children: \.children) { item in
Text("\(item.description)")
}
.onTapGesture {
print(selection)
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Files")
//.toolbar { EditButton() }
.environment(\.editMode, .constant(.active))
.onTapGesture {
// Walkaround: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
print(selection)
})
}
Text("\(selection.count) selections")
}
}
}
}
// Sample data:
struct FileItem: Hashable, Identifiable, CustomStringConvertible {
var id: Self { self }
var name: String
var header: 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", header: "Header 1", children:
[FileItem(name: "photo001.jpg", header: "Header 2"),
FileItem(name: "photo002.jpg")]),
FileItem(name: "Movies", children:
[FileItem(name: "movie001.mp4")]),
FileItem(name: "Documents", children: [])
]),
FileItem(name: "newuser", children:
[FileItem(name: "Documents", children: [])
])
])
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Documentation is not completely finished as it looks. Use direct SwiftUI autogenerated interfaces in Xcode 12 to find updates.
Especially for asked OutlineGroup there are several constructors with selection parameter, like below:
/// Creates a hierarchical list that computes its rows on demand from an
/// underlying collection of identifiable data, optionally allowing users to
/// select multiple rows.
///
/// - Parameters:
/// - data: The identifiable data for computing the list.
/// - selection: A binding to a set that identifies selected rows.
/// - rowContent: A view builder that creates the view for a single row of
/// the list.
#available(iOS 14.0, OSX 10.16, *)
#available(tvOS, unavailable)
#available(watchOS, unavailable)
public init<Data, RowContent>(_ data: Data, children: KeyPath<Data.Element, Data?>,
selection: Binding<Set<SelectionValue>>?, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) where Content == OutlineGroup<Data, Data.Element.ID, HStack<RowContent>, HStack<RowContent>, DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable
You need to put NavigationLink/Button for the item which do not have children.
Here is how it could look based on Apple source code.
var body: some View {
OutlineGroup(data, children: \.children) { item in
Group {
if item.children == nil {
NavigationLink(
destination: Text("\(item.name)"),
label: {
Text ("\(item.description)")
})
} else {
Text ("\(item.description)")
}
}
}
}
The data comes from an Apple example. Sometimes links gets broken. So, here is source code:
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.count > 0 ? "📂 \(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:"Documents", children: [])
]),
FileItem(name: "newuser", children:
[FileItem (name: "Documents", children: [])
])
])
Using a NavigationLink as the view to represent the children of the tree actually do work
Related
We're using Hierarchical lists in SwiftUI. The List takes an optional array for the children argument children: \.children. We would like to however use a 'non-optional' array
Example from https://developer.apple.com/documentation/swiftui/list
struct ContentView: View {
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 fileHierarchyData: [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: "Documents", children: [])
]),
FileItem(name: "newuser", children:
[FileItem(name: "Documents", children: [])
])
]),
FileItem(name: "private", children: nil)
]
var body: some View {
List(fileHierarchyData, children: \.children) { item in
Text(item.description)
}
}
}
so instead of using
var children: [FileItem]? = nil
we'd like to use
var children: [FileItem] = []
This of course produces a compiler error
Key path value type '[FileItem]' cannot be converted to contextual type '[FileItem]?'
How can one cast from [FileItem] to [FileItem]?
A possible approach is force-unwrapped optional (to fulfil List contract) and initial value and guard setter, so in all places it can be used directly w/o ?. It is the same approach as is used for IBOutlet declarations.
Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
struct FileItem: Hashable, Identifiable, CustomStringConvertible {
var id: Self { self }
var name: String
var children: [FileItem]! = [] { // with default !!
didSet {
if children == nil { // guard !!
children = []
}
}
}
var description: String {
children.isEmpty ? "📂 \(name)" : "📁 \(name)" // << direct use !!
}
}
let fileHierarchyData: [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: "Documents")
]),
FileItem(name: "newuser", children:
[FileItem(name: "Documents")
])
]),
FileItem(name: "private")
]
var body: some View {
List(fileHierarchyData, children: \.children) { item in
Text(item.description)
}
}
}
What I got from your question is you want to use non-optional array you can update like this.
var children: [FileItem]! = [] {
didSet {
if children.isEmpty {
children = []
}
}
}
Currently, .itemProvider is not working properly with OutlineGroup.
OutlineGroup in List: Not Work
Hierarchical List: Not Work
Pure List: Work
I think it is a bug or SwiftUI doesn't support it yet.
Is there any workaround? or should I use cocoa OutlineView for this feature?
Reproduce:
Copy and Paste code below, and drag items.
import SwiftUI
import PlaygroundSupport
var greeting = "Hello, playground"
struct FileItem: Hashable, Identifiable {
var id: Self { self }
var name: String
var children: [FileItem]? = nil
}
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: "Documents", children: [])]),
FileItem(name: "newuser", children:
[FileItem(name: "Documents", children: [])])])
struct ContentView: View {
var body: some View {
VStack {
Text("Not Work - OutlineGroup in List")
List {
OutlineGroup(data, children: \.children) { item in
Text("\(item.name)")
.itemProvider { NSItemProvider(object: item.name as NSString) }
}
}
Text("Not Work - Hierarchical List")
List {
OutlineGroup(data, children: \.children) { item in
Text("\(item.name)")
.itemProvider { NSItemProvider(object: item.name as NSString) }
}
}
Text("Work without item info")
List {
OutlineGroup(data, children: \.children) { item in
Text("\(item.name)")
}
.itemProvider { NSItemProvider() }
}
Text("Work - Pure List")
List([
FileItem(name: "Documents", children: []),
FileItem(name: "Files", children: [])
]) { item in
Text("\(item.name)")
.itemProvider { NSItemProvider(object: item.name as NSString) }
}
}
.frame(width: 320, height: 800)
}
}
PlaygroundPage.current.setLiveView(ContentView())
i have this code.
unfortunately the pickerview is not centered horizontally and there is too much space between the buttons and the pickerview (vertically), i now i can use offset, but is there a better way?
var body: some View {
NavigationView {
Form {
Section(header: Text("Hi"), content: {
Button("Alphabet") {
}.frame(alignment: .center)
Button("Ok") {
}.frame(alignment: .center)
HStack {
Picker(selection: $sortedBy,
label: Text(""),
content: {
ForEach(p, id: \.self) { category in
Text(category)
}
}).pickerStyle(WheelPickerStyle())
}
})
}
}
}
#Reiner Fischer...here is the result of your proposol (unfortunately not centered)
The problem is your empty label
label: Text(""),
even if the label is empty it takes some space on the left side of the picker. You can check by just adding some text to the label.
To get rid of the label, adjust your code like this:
.pickerStyle(WheelPickerStyle())
.labelsHidden()
That will center your picker selections
Update 21.02.2020
Hi Chris, enclosed is the code, i tested and that centers the picker:
struct PickerView: View {
let p:[Vital] = [
.init(name: "0"),
.init(name: "1"),
.init(name: "2"),
.init(name: "3")
]
#State private var sortedby = 0
var body: some View {
Picker(selection: $sortedby, label: Text("")) {
ForEach(p) { post in
Text(post.name)
}
}.pickerStyle(DefaultPickerStyle())
.labelsHidden()
}
}
In a small sample SwiftUI app, I have a settings view that shows a couple of option selections, implemented as segmented controls. The text in these segmented controls visibly moves when an alert is presented or dismissed. Is there a way to get rid of this glitch?
Paste this in a Playground to reproduce:
import SwiftUI
import PlaygroundSupport
struct FlickeringSegmentsView: View {
#State var option = 0
#State var alerting = false
var body: some View {
VStack(alignment: .center, spacing: 120) {
Picker("options", selection: $option) {
Text("Option A").tag(0)
Text("Option B").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding(16)
Button(action: { self.alerting.toggle() },
label: { Text("Show Alert") }
)
.alert(isPresented: $alerting) {
Alert(title: Text("Alert"))
}
}
}
}
PlaygroundPage.current.setLiveView(FlickeringSegmentsView())
This issue is resolved in Xcode 12 beta using the included iOS 14 simulator (and hopefully stays that way).
I hope code below should help you:
public protocol SegmentedPickerViewElementTraits: Hashable {
var localizedText: String { get }
}
public struct SegmentedPickerView<Value, Data, ID, Label>: View
where
Value: SegmentedPickerViewElementTraits,
Data: RandomAccessCollection,
Data.Element == Value,
ID: Hashable,
Label: View {
public let data: Data
public let id: KeyPath<Data.Element, ID>
public let selection: Binding<Value>
public let label: Label
public init(data: Data,
id: KeyPath<Data.Element, ID>,
selection: Binding<Value>,
label: Label) {
self.data = data
self.id = id
self.selection = selection
self.label = label
}
public var body: some View {
Picker(selection: selection, label: label) {
ForEach(data, id: id) {
Text($0.localizedText).tag($0)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
and lets modify your code:
enum Options: UInt8, CaseIterable {
case optionA
case optionB
}
extension Options: SegmentedPickerViewElementTraits {
var localizedText: String {
switch self {
case .optionA:
return "Option A"
case .optionB:
return "Option B"
}
}
}
struct FlickeringSegmentsView: View {
#State
var option: Options = .optionA
#State
var alerting = false
var body: some View {
VStack(alignment: .center, spacing: 120) {
SegmentedPickerView(
data: Options.allCases,
id: \.self,
selection: $option,
label: Text("options")
)
.padding(16)
Button(
action: { self.alerting.toggle() },
label: { Text("Show Alert") }
)
.alert(isPresented: $alerting) {
Alert(title: Text("Alert"))
}
}
}
}
I'm using SwiftUI to animate an expand and collapse in a list.
How can I get the height expansion of the section to animate smoothly like it would in UIKit with a tableview?
struct Rows: View {
let rows = ["Row 1", "Row 2", "Row 3", "Row 4", "Row 5"]
var body: some View {
Section {
ForEach(rows.identified(by: \.self)) { name in
Text(name)
.lineLimit(nil)
}
}
}
}
struct Header: View {
#State var isExpanded: Bool = false
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.isExpanded.toggle()
}) {
Text(self.isExpanded ? "Collapse Me" : "Expand Me")
.font(.footnote)
}
if self.isExpanded {
Rows().animation(.fluidSpring())
}
}
}
}
struct ContentView : View {
var body: some View {
List(0...4) { _ in
Header()
}
}
}
The animation seems to only apply to the text in the rows not the actual height or separator line growing to accommodate the new rows. The row text also seems to start animating from the very top of the row rather than where it appears in the view hierarchy. I need a smooth animation.
I implemented it like this: (It is with proper animation)
struct ExpandCollapseList : View {
#State var sectionState: [Int: Bool] = [:]
var body: some View {
NavigationView {
List {
ForEach(1 ... 6, id: \.self) { section in
Section(header: Text("Section \(section)").onTapGesture {
self.sectionState[section] = !self.isExpanded(section)
}) {
if self.isExpanded(section) {
ForEach(1 ... 4, id: \.self) { row in
Text("Row \(row)")
}
}
}
}
}
.navigationBarTitle(Text("Expand/Collapse List"))
.listStyle(GroupedListStyle())
}
}
func isExpanded(_ section: Int) -> Bool {
sectionState[section] ?? false
}
}
Thanks to Aakash Jaiswal's answer I was able to expand upon this implementation to suit my need to expand to three tiers, i.e., Section, Subsection, and Lesson. The compiler failed to compile the whole implementation in a single View, which is why I separated it out.
import SwiftUI
struct MenuView: View {
var body: some View {
HStack {
List {
ToggleableMenuItemsView(sections: menuItems)
.padding()
}
}
.background(Color("Gray"))
.cornerRadius(30)
.padding(.top, 30)
.padding(.trailing, bounds.width * 0.2)
.padding(.bottom, 30)
.shadow(radius: 10)
}
#State var menuItemState = [String: Bool]()
private var bounds: CGRect { UIScreen.main.bounds }
private func isExpanded(_ menuItem: MenuItem) -> Bool {
menuItemState[menuItem.id] ?? false
}
}
struct ToggleableMenuItemsView: View {
let sections: [MenuItem]
var body: some View {
ForEach(sections) { section in
Section(
header: Text(section.title)
.font(.title)
.onTapGesture { self.menuItemState[section.id] = !self.isExpanded(section) },
content: {
if self.isExpanded(section) {
ForEach(section.children) { subsection in
Section(
header: Text(subsection.title)
.font(.headline)
.onTapGesture { self.menuItemState[subsection.id] = !self.isExpanded(subsection) },
content: {
if self.isExpanded(subsection) {
LessonsListView(lessons: subsection.children)
}
}
)
}
}
}
)
}
}
#State var menuItemState = [String: Bool]()
private func isExpanded(_ menuItem: MenuItem) -> Bool {
menuItemState[menuItem.id] ?? false
}
}
struct LessonsListView: View {
let lessons: [MenuItem]
var body: some View {
ForEach(lessons) { lesson in
Text(lesson.title)
.font(.subheadline)
}
}
}
class MenuItem: Identifiable {
var id: String
let title: String
var children: [MenuItem]
init(id: String, title: String, children: [MenuItem] = []) {
self.id = id
self.title = title
self.children = children
}
}
let menuItems = [
MenuItem(
id: "01",
title: "The Land in its World",
children: [
MenuItem(
id: "01A",
title: "North and South",
children: [
MenuItem(
id: "01A01",
title: "Between Continents"
),
MenuItem(
id: "01A02",
title: "The Wet North"
),
MenuItem(
id: "01A03",
title: "The Dry South"
),
MenuItem(
id: "01A04",
title: "Between Wet and Dry"
)
]
),
MenuItem(
id: "01B",
title: "East and West",
children: [
MenuItem(
id: "01B01",
title: "Sea and Desert"
),
MenuItem(
id: "01B02",
title: "Exchange in Aram"
),
MenuItem(
id: "01B03",
title: "Exchange in Egypt"
),
MenuItem(
id: "01B04",
title: "A Bypass Between"
)
]
),
MenuItem(
id: "01C",
title: "Between Empires",
children: [
MenuItem(
id: "01C01",
title: "Imperial Dreams"
),
MenuItem(
id: "01C02",
title: "Egypt Marches"
),
MenuItem(
id: "01C03",
title: "Taking Egypt's Wealth"
),
MenuItem(
id: "01C04",
title: "The Land Between"
)
]
)
]
)
]
struct MenuView_Previews: PreviewProvider {
static var previews: some View {
MenuView()
}
}
Here's a demo
try to implement it like this:
struct ContentView : View {
#State var expanded:[Int:Bool] = [:]
func isExpanded(_ id:Int) -> Bool {
expanded[id] ?? false
}
var body: some View {
NavigationView{
List {
ForEach(0...80) { section in
Section(header: CustomeHeader(name: "Section \(section)", color: Color.white).tapAction {
self.expanded[section] = !self.isExpanded(section)
}) {
if self.isExpanded(section) {
ForEach(0...30) { row in
Text("Row \(row)")
}
}
}
}
}
}.navigationBarTitle(Text("Title"))
}
}
struct CustomeHeader: View {
let name: String
let color: Color
var body: some View {
VStack {
Spacer()
HStack {
Text(name)
Spacer()
}
Spacer()
Divider()
}
.padding(0)
.background(color.relativeWidth(1.3))
.frame(height: 50)
}
}