SwiftUI - Section style difference when embedding List within a VStack - ios

It seems that there is a difference in showing a List Section header when you embed the List in a VStack. Does anybody know why this happens?
struct ContentView: View {
#State var toggle: Bool = false
let items: [String] = ["Apple", "Pear", "Banana"]
var body: some View {
NavigationView {
if toggle {
VStack {
list
}
} else {
list
}
}
}
var list: some View {
List {
Section {
ForEach(items, id: \.self) { item in
Text(item)
}
} header: {
Text("Fruit")
}
}
.navigationTitle(toggle ? "VStack" : "No VStack")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
toggle.toggle()
} label: {
Text("Toggle")
}
}
}
}
}

Related

How to make ScrollViewReader scroll to top of List?

I have List within a TabView and allowing the user to scroll to the top when they double tap a tab. I'm using a ScrollViewReader to scroll to a specific anchor. However, it is not fully scrolling to the top of the list because of the navigation title, see the title overlapping the content:
I'm using the technique from this blog post for more context. Below is a working sample:
struct ContentView: View {
#State private var _selectedTab: SelectedTab = .one
#State private var tabbedTwice = false
var selectedTab: Binding<SelectedTab> {
Binding(
get: { _selectedTab },
set: {
if $0 == _selectedTab {
tabbedTwice = true
}
_selectedTab = $0
}
)
}
enum SelectedTab: String {
case one
case two
}
var body: some View {
ScrollViewReader { proxy in
TabView(selection: selectedTab) {
NavigationView {
List {
Section {
ForEach(1...50, id: \.self) { index in
Text("Item \(index.formatted())")
}
}
.id(SelectedTab.one.rawValue)
Section {
Text("Section 2")
}
}
.navigationTitle("First")
}
.tabItem {
Label("One", systemImage: "clock.arrow.circlepath")
}
.tag(SelectedTab.one)
NavigationView {
List {
Section(header: Text("Header").id(SelectedTab.two.rawValue)) {
ForEach(50...100, id: \.self) { index in
Text("Item \(index.formatted())")
}
}
Section {
Text("Section 2")
}
}
.navigationTitle("Second")
}
.tabItem {
Label("Two", systemImage: "list.bullet")
}
.tag(SelectedTab.two)
}
.onChange(of: tabbedTwice) {
guard $0 else { return }
withAnimation { proxy.scrollTo(_selectedTab.rawValue, anchor: .top) }
tabbedTwice = false
}
}
}
}
Is there a better place to put the anchor identifier? I tried putting on the first section and also the section header which worked better but still not scrolling to the very top. How can this be achieved?

Picker conflicts with NavigationLink gesture

I am trying to create the following card view.
With the following code to achieve it.
struct SimpleGame: Identifiable, Hashable {
var id = UUID()
let name: String
}
enum PlayingStatus: String {
case In = "I"
case Out = "O"
case Undecided = "U"
}
struct TestView: View {
let games: [SimpleGame] = [
.init(name: "First"),
.init(name: "Second")
]
#State private var currentStatus: PlayingStatus = .Undecided
var body: some View {
NavigationView {
List(games) { game in
Section {
VStack {
NavigationLink(value: game) {
Text("\(game.name)")
}
Divider()
Picker("Going?", selection: $currentStatus) {
Text("No Response")
.tag(PlayingStatus.Undecided)
Text("Going")
.tag(PlayingStatus.In)
Text("Not going")
.tag(PlayingStatus.Out)
}
.font(.body)
}
}
}
.navigationDestination(for: Game.self) { game in
Text("Detail View")
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Upcoming")
}
}
}
But a tap on element wrapped by NavigationLink is registering as a tap on the Picker. Anyone know of a way around this?
iOS 16/Xcode 14
you could try this:
List {
ForEach(games, id: \.name) { game in
Section {
NavigationLink(value: game) {
Text("\(game.name)")
}
// -- here
VStack {
Divider()
Picker("Going?", selection: $currentStatus) {
Text("No Response").tag(PlayingStatus.Undecided)
Text("Going").tag(PlayingStatus.In)
Text("Not going").tag(PlayingStatus.Out)
}
.font(.body)
}
}
}
}
What often works for me is extending the view.
struct TestView: View {
var body: some View {
List {
ForEach(games, id: \.name) { game in
Section {
NavigationLink(value: game) {
Text("\(game.name)")
}
// -- here
VStack {
Divider()
picker
}
}
}
}
}
}
Extension TestView {
private var picker: some View {
Picker("Going?", selection: $currentStatus) {
Text("No Response").tag(PlayingStatus.Undecided)
Text("Going").tag(PlayingStatus.In)
Text("Not going").tag(PlayingStatus.Out)
}
.font(.body)
}
}

Creating two sidebars in iPadOS application using SwiftUI

I have created one sidebar with NavigationView, which by default appends to the left of the landscape view of application. However I wanted to have another on the right side.
NavigationView {
List {
Label("Pencil", systemImage: "pencil")
Label("Paint", systemImage: "paintbrush.fill")
Label("Erase", systemImage: "quote.opening")
Label("Cutter", systemImage: "scissors")
Label("Eyedropper", systemImage: "eyedropper.halffull")
Label("Draw Line", systemImage: "line.diagonal")
}
.listStyle(SidebarListStyle())
}
Here's a simple working example made with SwiftUI:
struct ContentView: View {
var body: some View {
NavigationView{
TagView()
Text("Default second View")
Text("Default Third View")
}
}
}
struct TagView: View {
let tags = ["Apple", "Google"]
var body: some View {
List{
ForEach(tags, id: \.self) { name in
NavigationLink {
ProductView(tag: name)
} label: {
Text(name)
}
}
}
}
}
struct ProductView: View {
var tag: String
var products: [String] {
if tag == "Apple" {
return ["iPhone", "iPad", "MacBook"]
} else {
return ["resuable stuff"]
}
}
var body: some View {
List{
ForEach(products, id: \.self) { name in
NavigationLink {
DetailsView()
} label: {
Text(name)
}
}
}
}
}
struct DetailsView: View {
var body: some View {
Text("Detailed explanation about product")
}
}

Unwanted list item indent SwiftUI List after deleting items

After removing all the items from a list and then adding the items back to the list, each list item is indented like it is in edit mode and swipe actions are unavailable. I only see the issue when I have the conditional checking if the array is empty.
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
} else {
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
categories.remove(at: i)
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
}
}
}
}
Before removing items from array:
After removing items and adding new items to array:
here is an example that shows that deletion is not the problem
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
} else {
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
// .swipeActions(allowsFullSwipe: false) {
// Button(role: .destructive) {
// categories.remove(at: i)
// } label: {
// Label("Delete", systemImage: "trash.fill")
// }
// }
.onTapGesture {
categories.remove(at: i)
}
}
}
}
}
}}
the problem is that after deleting an element from the list with swipeActions the list is supposed to reposition itself, doing so just after deleting the last element from the list with swipeActions you decide to disappear the list so it will not have the time to finish his action.
I suggest the following code which works fine
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
}
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
categories.remove(at: i)
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
// don't display if categories.isEmpty
.frame(height: categories.isEmpty ? 0 : nil)
}
}}
Here's a possible solution. You could try using onDelete for this, documentation is here. I also included onMove if needed and added a button which is only active when the array is empty.
struct ContentView: View {
#State private var animals = [
"Dog",
"Cat",
]
var body: some View {
NavigationView {
List {
ForEach(animals, id: \.self) { animal in
Text(animal)
}
.onDelete { self.delete(at :$0) }
.onMove { self.move(from: $0, to: $1) }
}
.navigationTitle("Animals")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.animals = [
"Dog",
"Cat",
]
}, label: {
Label("Add", systemImage: "plus")
.labelStyle(.iconOnly)
}).disabled(!animals.isEmpty)
}
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
}
func delete(at: IndexSet) {
for i in at {
animals.remove(at: i)
}
}
func move(from: IndexSet, to: Int) {
animals.move(fromOffsets: from, toOffset: to)
}
}

SwiftUI Form Cell losing selection UI when drilling into details?

I have the following code:
enum SelectedDetails:Int, CaseIterable {
case d0
case d1
}
struct CellSelectionTestView : View {
#State var selection:SelectedDetails? = .d0
var body: some View {
NavigationView {
Form {
Section(header: Text("Section 0")) {
NavigationLink(destination: D0DetailsView(),
tag: .d0,
selection: $selection) {
D0CellView().frame(height: 80)
}
NavigationLink(destination: D1CellView(),
tag: .d1,
selection: $selection) {
D1CellView().frame(height: 80)
}
}
}
}
}
}
struct D0CellView: View {
var body: some View {
Text("D0")
}
}
struct D0DetailsView: View {
var body: some View {
VStack {
List {
ForEach(0..<10) { n in
NavigationLink.init(destination: OptionsDetailsView(index:n)) {
Text("show \(n) details")
}
}
}
.refreshable {
}
}
}
}
struct OptionsDetailsView: View {
let index:Int
var body: some View {
Text("OptionsDetailsView \(index)")
}
}
struct D1CellView: View {
var body: some View {
Text("D1")
}
}
When I tap on D0 cell, it shows this:
D0 cell correctly shows the selected state UI.
Then I tap on one of the show <n> details cells and the selection goes away:
How do I keep D0 cell selected UI stated active until I tap on another cell like D1 for example regardless of what I do in the details view to the right? I need to keep UI context as the user does what is needed within the details shown when D0 is tapped. Why is that selection going away if I didn't even tap on D1?
Strange, but it seems like NavigationView can only keep one selection. I found a workaround by integrating a second NavigationView with .stacked style in your child view:
struct D0DetailsView: View {
var body: some View {
NavigationView {
VStack {
List {
ForEach(0..<10) { n in
NavigationLink {
OptionsDetailsView(index:n)
} label: {
Text("show \(n) details")
}
}
}
.refreshable {
}
}
}
.navigationViewStyle(.stack)
}
}
Another approach: save the last active selection and set the select background color manually:
struct CellSelectionTestView : View {
#State private var selection: SelectedDetails? = .d0
#State private var selectionSaved: SelectedDetails = .d0
var body: some View {
NavigationView {
Form {
Section(header: Text("Section 0")) {
NavigationLink(tag: .d0, selection: $selection) {
D0DetailsView()
} label: {
D0CellView().frame(height: 80)
}
.listRowBackground(selectionSaved == .d0 ? Color.gray : Color.clear)
NavigationLink(tag: .d1, selection: $selection) {
D1CellView()
} label:{
D1CellView().frame(height: 80)
}
.listRowBackground(selectionSaved == .d1 ? Color.gray : Color.clear)
}
}
}
.onChange(of: selection) { newValue in
if selection != nil { selectionSaved = selection! }
}
}
}

Resources