SwiftUI List selection doesn’t show If I add a NavigationLink and a .contextMenu to the list. Is this a known bug? - ios

List selection doesn’t show If I add a NavigationLink and a .contextMenu to the list, when I select a row, the selection disappears.
struct ContentView: View {
#State private var selection: String?
let names = ["Cyril", "Lana", "Mallory", "Sterling"]
var body: some View {
NavigationView {
List(names, id: \.self, selection: $selection) { name in
NavigationLink(destination: Text("Hello, world!")) {
Text(name)
.contextMenu {
Button(action: {}) {
Text("Tap me!")
}
}
}
}
.toolbar {
EditButton()
}
}
}
}

We can disable context menu button(s) for the moment of construction in edit mode (because the button is a reason of issue).
Here is a possible approach - some redesign is required to handle editMode inside context menu (see also comments inline).
Tested with Xcode 13.2 / iOS 15.2
struct ContentViewSelection: View {
#State private var selection: String?
let names = ["Cyril", "Lana", "Mallory", "Sterling"]
var body: some View {
NavigationView {
List(names, id: \.self, selection: $selection) { name in
// separated view is needed to use editMode
// environment value
NameCell(name: name)
}
.toolbar {
EditButton()
}
}
}
}
struct NameCell: View {
#Environment(\.editMode) var editMode // << !!
let name: String
var body: some View {
NavigationLink(destination: Text("Hello, world!")) {
Text(name)
}
.contextMenu {
if editMode?.wrappedValue == .inactive { // << !!
Button(action: {}) {
Text("Tap me!")
}
}
}
}
}

Related

Passing List between Views in SwiftUi

I'm making a ToDo list app on my own to try to get familiar with iOS development and there's this one problem I'm having:
I have a separate View linked to enter in a new task with a TextField. Here's the code for this file:
import SwiftUI
struct AddTask: View {
#State public var task : String = ""
#State var isUrgent: Bool = false // this is irrelevant to this problem you can ignore it
var body: some View {
VStack {
VStack(alignment: .leading) {
Text("Add New Task")
.bold()
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
TextField("New Task...", text: $task)
Toggle("Urgent", isOn: $isUrgent)
.padding(.vertical)
Button("Add task", action: {"call some function here to get what is
in the text field and pass back the taskList array in the Content View"})
}.padding()
Spacer()
}
}
}
struct AddTask_Previews: PreviewProvider {
static var previews: some View {
AddTask()
}
}
So what I need to take the task variable entered and insert it into the array to be displayed in the list in my main ContentView file.
Here's the ContentView file for reference:
import SwiftUI
struct ContentView: View {
#State var taskList = ["go to the bakery"]
struct AddButton<Destination : View>: View {
var destination: Destination
var body: some View {
NavigationLink(destination: self.destination) { Image(systemName: "plus") }
}
}
var body: some View {
VStack {
NavigationView {
List {
ForEach(self.taskList, id: \.self) {
item in Text(item)
}
}.navigationTitle("Tasks")
.navigationBarItems(trailing: HStack { AddButton(destination: AddTask())})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
I need a function to take the task entered in the TextField, and pass it back in the array in the ContentView to be displayed in the List for the user
-thanks for the help
You can add a closure property in your AddTask as a callback when the user taps Add Task. Just like this:
struct AddTask: View {
var onAddTask: (String) -> Void // <-- HERE
#State public var task: String = ""
#State var isUrgent: Bool = false
var body: some View {
VStack {
VStack(alignment: .leading) {
Text("Add New Task")
.bold()
.font(.title)
TextField("New Task...", text: $task)
Toggle("Urgent", isOn: $isUrgent)
.padding(.vertical)
Button("Add task", action: {
onAddTask(task)
})
}.padding()
Spacer()
}
}
}
Then, in ContentView:
.navigationBarItems(
trailing: HStack {
AddButton(
destination: AddTask { task in
taskList.append(task)
}
)
}
)
#jnpdx provides a good solution by passing Binding of taskList to AddTask. But I think AddTask is used to add a new task so it should only focus on The New Task instead of full taskList.

How to use NavigationView and NavigationLink in SwiftUI?

Piece of code that I made for this question that runs in Playground:
import SwiftUI
struct Player {
let name: String
let surname: String
}
struct DetailView2: View {
var player: Player
var body: some View {
Text("\(player.name) \(player.surname)")
}
}
struct PlaygroundView: View {
// MARK: - Propertiers
#State private var selection = 0
private var players = [
Player(name: "Lionel", surname: "Messi"),
Player(name: "Diogo", surname: "Jota"),
]
// MARK: - View
var body: some View {
TabView(selection: $selection) {
NavigationView {
VStack {
Text("Settings").font(.title)
List(0..<players.count, id: \.self) { index in
NavigationLink(destination: DetailView2(player: players[index])) {
Text("\(index)")
}
}
}
.navigationTitle("Players")
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(true)
}
.background(Color.white)
.tabItem {
Image(systemName: "house.fill")
Text("Players")
}
.tag(0)
VStack {
Text("Settings").font(.title)
List(0..<2, id: \.self) { index in
Text("#\(index)")
}
}
.tabItem {
Image(systemName: "book.fill")
Text("Foo")
}
.tag(1)
}
}
}
struct Playground_Previews: PreviewProvider {
static var previews: some View {
PlaygroundView()
}
}
Current Players bar:
Current Settings bar:
How can I fix the code such that "Players" bar will look like Settings bar (it terms of the styling of the title). It seems like I've got the tab item and navigation working already.
It seems you're looking something like below (prepared & tested with Xcode 12.1 / iOS 14.1)
var body: some View {
TabView(selection: $selection) {
VStack {
Text("Settings").font(.title)
NavigationView {
List(0..<players.count, id: \.self) { index in
NavigationLink(destination: DetailView2(player: players[index])) {
Text("\(index)")
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(true)
}
}
.background(Color.white)
.tabItem {
Image(systemName: "house.fill")
Text("Players")
}
.tag(0)
VStack {
Text("Settings").font(.title)
List(0..<2, id: \.self) { index in
Text("#\(index)")
}
}
.tabItem {
Image(systemName: "book.fill")
Text("Foo")
}
.tag(1)
}
}

How to push many views into a NavigationView in SwiftUI [duplicate]

In my navigation, I want to be able to go from ContentView -> ModelListView -> ModelEditView OR ModelAddView.
Got this working, my issue now being that when I hit the Back button from ModelAddView, the intermediate view is omitted and it pops back to ContentView; a behaviour that
ModelEditView does not have.
There's a reason for that I guess – how can I get back to ModelListView when dismissing ModelAddView?
Here's the code:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List{
NavigationLink(
destination: ModelListView(),
label: {
Text("1. Model")
})
Text("2. Model")
Text("3. Model")
}
.padding()
.navigationTitle("Test App")
}
}
}
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing:
NavigationLink(
destination: ModelAddView(modelViewModel: $modelViewModel), label: {
Image(systemName: "plus")
})
)
}
}
struct ModelEditView: View {
#Binding var model: Model
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelAddView: View {
#Binding var modelViewModel: ModelViewModel
#State var model = Model(id: UUID(), titel: "")
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelViewModel {
var modelValues: [Model]
init() {
self.modelValues = [ //mock data
Model(id: UUID(), titel: "Foo"),
Model(id: UUID(), titel: "Bar"),
Model(id: UUID(), titel: "Buzz")
]
}
}
struct Model: Identifiable, Equatable {
let id: UUID
var titel: String
}
Currently placing a NavigationLink in the .navigationBarItems may cause some issues.
A possible solution is to move the NavigationLink to the view body and only toggle a variable in the navigation bar button:
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
#State var isAddLinkActive = false // add a `#State` variable
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
}
)
}
.background( // move the `NavigationLink` to the `body`
NavigationLink(destination: ModelAddView(modelViewModel: $modelViewModel), isActive: $isAddLinkActive) {
EmptyView()
}
.hidden()
)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: trailingButton)
}
// use a Button to activate the `NavigationLink`
var trailingButton: some View {
Button(action: {
self.isAddLinkActive = true
}) {
Image(systemName: "plus")
}
}
}

Creating controls at runtime in SwiftUI

The following code creates new controls every time a button is pressed at runtime, the problem is that the picker selection is set to the same state.
How can I create new controls with different state variables so they can operate separately ?
struct ContentView: View {
#State private var numberOfControlls = 0
#State var selection: String="1"
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
Picker(selection: self.$selection, label:
Text("Picker") {
Text("1").tag(1)
Text("2").tag(2)
}
}
}
}
}
How can I create new controls with different state variables so they can operate separately ?
Separate control into standalone view with own state (or view model if/when needed).
Here is a demo:
struct ContentView: View {
#State private var numberOfControlls = 0
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
ControlView()
}
}
}
}
struct ControlView: View {
#State var selection: String="1"
var body: some View {
Picker(selection: self.$selection, label:
Text("Picker")) {
Text("1").tag(1)
Text("2").tag(2)
}
}
}

SwiftUI: "Fatal error: Index out of range" after deleting last item of a list and switching to another tabview

I try to implement a dynamic created list into an tabview in SwiftUI using an EnvironmentObject and a Binding (which may not be needed in the example-code below but it is in my real project) for the SubView of the row.
The list itself (add item, delete item) is working fine as well as the switch to the other "page" of the tabview. But if you are deleting the last item of the list, and only the last (if you are deleting the first one or any item between it also works fine) the app crashes with the Error: "Thread 1: Fatal error: Index out of range" as soon you are trying to switch to the other tab after deleting this item.
Heres the (example)-code:
import SwiftUI
struct PickerRowData: Equatable, Identifiable, Hashable {
var id = UUID()
var pickerValueString = ""
}
class GlobalStates: ObservableObject, Identifiable {
#Published var pickerRowData = [PickerRowData]()
var id = UUID()
}
struct MeasurePickerRow: View, Identifiable {
#Binding var pickerRowData: PickerRowData
var id = UUID()
var body: some View {
return VStack {
Text("Just a test. ID: \(pickerRowData.id)")
}
}
}
struct MeasurePickerView: View {
#EnvironmentObject var globalStates: GlobalStates
var body: some View {
return NavigationView {
List {
ForEach(self.globalStates.pickerRowData) { rowData in
MeasurePickerRow(pickerRowData: self.$globalStates.pickerRowData[self.globalStates.pickerRowData.lastIndex(of: rowData) ?? 0])
}
.onDelete(perform: deleteMeasurePicker)
}
.listStyle(GroupedListStyle())
.padding(0)
.navigationBarTitle(Text("Input"), displayMode: .inline)
.navigationBarItems(
trailing: HStack {
Button(action: {
self.globalStates.pickerRowData.append(PickerRowData())
}) {
Image(systemName: "gauge.badge.plus")
}
})
}
}
func deleteMeasurePicker(at offsets: IndexSet) {
self.globalStates.pickerRowData.remove(atOffsets: offsets)
}
}
struct ContentView: View {
var body: some View {
TabView {
MeasurePickerView()
.tabItem {
Image(systemName: "gauge")
HStack {
Text("Tab 1")
}
}
Text("Tab 2 View")
.tabItem {
Image(systemName: "equal.circle")
HStack {
Text("Tab 2")
}
}
}
}
}
Did I miss something? I'm quite new in SwiftUI (and Swift) so I hope someone has a hint.
Regards,
DZ

Resources