Swiftui: ForEach button, wrong parameter is passed in a view [duplicate] - swiftui

I have an issue using a sheet inside a ForEach. Basically I have a List that shows many items in my array and an image that trigger the sheet. The problem is that when my sheet is presented it only shows the first item of my array which is "Harry Potter" in this case.
Here's the code
struct ContentView: View {
#State private var showingSheet = false
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.showingSheet = true
}
.sheet(isPresented: self.$showingSheet) {
Text(self.movies[movie])
}
}
}
}
}
}

There should be only one sheet, so here is possible approach - use another sheet modifier and activate it by selection
Tested with Xcode 12 / iOS 14 (iOS 13 compatible)
extension Int: Identifiable {
public var id: Int { self }
}
struct ContentView: View {
#State private var selectedMovie: Int? = nil
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie) {
Text(self.movies[$0])
}
}
}
}

I changed your code to have only one sheet and have the selected movie in one variable.
extension String: Identifiable {
public var id: String { self }
}
struct ContentView: View {
#State private var selectedMovie: String? = nil
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(movies) { movie in
HStack {
Text(movie)
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie, content: { selectedMovie in
Text(selectedMovie)
})
}
}
}

Wanted to give my 2 cents on the matter.
I was encountering the same problem and Asperi's solution worked for me.
BUT - I also wanted to have a button on the sheet that dismisses the modal.
When you call a sheet with isPresented you pass a binding Bool and so you change it to false in order to dismiss.
What I did in the item case is I passed the item as a Binding. And in the sheet, I change that binding item to nil and that dismissed the sheet.
So for example in this case the code would be:
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie) {
Text(self.movies[$0])
// My addition here: a "Done" button that dismisses the sheet
Button {
selectedMovie = nil
} label: {
Text("Done")
}
}
}
}

Related

Changing a TextField text for items in a foreach NavigationLink SwiftUI and saving it

There is a list in on the Main View that has navigation links that bring you to a an Edit Birthday View where the textFieldName is saved with the onAppear method. I need help in allowing the user to change the text in the text field on the Edit Birthday View and having it save when the user dismisses and returns to that particular item in the foreach list. I have tried onEditingChanged and on change method but they don't seem to work. (Also, in my view model i append birthday items when they are created in the Add Birthday View). If you would like to see more code i will make updates. Thank you.
/// MAIN VIEW
import SwiftUI
struct MainView: View {
#EnvironmentObject var vm: BirthdayViewModel
#State var nameTextField: String = ""
var body: some View {
VStack(spacing: 20) {
List {
ForEach(vm.searchableUsers, id: \.self) { birthday in
NavigationLink(destination: EditBirthdayView(birthday: birthday)) {
BirthdayRowView(birthday: birthday)
}
.listRowSeparator(.hidden)
}
.onDelete(perform: vm.deleteBirthday)
}
}
.toolbar {
ToolbarItem {
NavigationLink(destination: AddBirthdayView(textfieldName: $nameTextField)) {
Image(systemName: "plus.circle")
}
}
}
}
}
/// EDIT BIRTHDAY VIEW
import SwiftUI
import Combine
struct EditBirthdayView: View {
#EnvironmentObject var vm: BirthdayViewModel
#State var textfieldName: String = ""
#Environment(\.presentationMode) var presentationMode
var birthday: BirthdayModel
var body: some View {
NavigationView {
VStack {
TextField("Name...", text: $textfieldName)
}
Button {
saveButtonPressed()
} label: {
Text("Save")
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()) {
textfieldName = birthday.name
}
}
}
}
func saveButtonPressed() {
vm.updateItem(birthday: birthday)
presentationMode.wrappedValue.dismiss()
}
func updateTextField() {
textfieldName = birthday.name
}
}
struct MainView: View {
#EnvironmentObject var store: BirthdayStore
var body: some View {
List {
ForEach($store.birthdays) { $birthday in
NavigationLink(destination: EditBirthdayView(birthday: $birthday)) {
BirthdayRowView(birthday: birthday)
}
}
.onDelete(perform: deleteBirthday)
}
.listRowSeparator(.hidden)
.toolbar {
ToolbarItem {
NavigationLink(destination: AddBirthdayView() {
Image(systemName: "plus.circle")
}
}
}
}
}
struct EditBirthdayView: View {
#EnvironmentObject var store: BirthdayStore
#Binding var birthday: Birthday
...
TextField("Name", text: $birthday.name)

SwiftUI List rows with INFO button

UIKit used to support TableView Cell that enabled a Blue info/disclosure button. The following was generated in SwiftUI, however getting the underlying functionality to work is proving a challenge for a beginner to SwiftUI.
Generated by the following code:
struct Session: Identifiable {
let date: Date
let dir: String
let instrument: String
let description: String
var id: Date { date }
}
final class SessionsData: ObservableObject {
#Published var sessions: [Session]
init() {
sessions = [Session(date: SessionsData.dateFromString(stringDate: "2016-04-14T10:44:00+0000"),dir:"Rhubarb", instrument:"LCproT", description: "brief Description"),
Session(date: SessionsData.dateFromString(stringDate: "2017-04-14T10:44:00+0001"),dir:"Custard", instrument:"LCproU", description: "briefer Description"),
Session(date: SessionsData.dateFromString(stringDate: "2018-04-14T10:44:00+0002"),dir:"Jelly", instrument:"LCproV", description: " Description")
]
}
static func dateFromString(stringDate: String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter.date(from:stringDate)!
}
}
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(session: session )
}
}
.navigationTitle("Session data")
}
// without this style modification we get all sorts of UIKit warnings
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SessionRow: View {
var session: Session
#State private var presentDescription = false
var body: some View {
HStack(alignment: .center){
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
Spacer()
// SessionGraph is a place holder for the Graph data.
NavigationLink(destination: SessionGraph()) {
// if this isn't an EmptyView then we get a disclosure indicator
EmptyView()
}
// Note: without setting the NavigationLink hidden
// width to 0 the List width is split 50/50 between the
// SessionRow and the NavigationLink. Making the NavigationLink
// width 0 means that SessionRow gets all the space. Howeveer
// NavigationLink still works
.hidden().frame(width: 0)
Button(action: { presentDescription = true
print("\(session.dir):\(presentDescription)")
}) {
Image(systemName: "info.circle")
}
.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: SessionDescription(),
isActive: $presentDescription) {
EmptyView()
}
.hidden().frame(width: 0)
}
.padding(.vertical, 4)
}
}
struct SessionGraph: View {
var body: some View {
Text("SessionGraph")
}
}
struct SessionDescription: View {
var body: some View {
Text("SessionDescription")
}
}
The issue comes in the behaviour of the NavigationLinks for the SessionGraph. Selecting the SessionGraph, which is the main body of the row, propagates to the SessionDescription! hence Views start flying about in an un-controlled manor.
I've seen several stated solutions to this issue, however none have worked using XCode 12.3 & iOS 14.3
Any ideas?
When you put a NavigationLink in the background of List row, the NavigationLink can still be activated on tap. Even with .buttonStyle(BorderlessButtonStyle()) (which looks like a bug to me).
A possible solution is to move all NavigationLinks outside the List and then activate them from inside the List row. For this we need #State variables holding the activation state. Then, we need to pass them to the subviews as #Binding and activate them on button tap.
Here is a possible example:
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
// create state variables for activating NavigationLinks
#State private var presentGraph: Session?
#State private var presentDescription: Session?
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(
session: session,
presentGraph: $presentGraph,
presentDescription: $presentDescription
)
}
}
.navigationTitle("Session data")
// put NavigationLinks outside the List
.background(
VStack {
presentGraphLink
presentDescriptionLink
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var presentGraphLink: some View {
// custom binding to activate a NavigationLink - basically when `presentGraph` is set
let binding = Binding<Bool>(
get: { presentGraph != nil },
set: { if !$0 { presentGraph = nil } }
)
// activate the `NavigationLink` when the `binding` is `true`
NavigationLink("", destination: SessionGraph(), isActive: binding)
}
#ViewBuilder
var presentDescriptionLink: some View {
let binding = Binding<Bool>(
get: { presentDescription != nil },
set: { if !$0 { presentDescription = nil } }
)
NavigationLink("", destination: SessionDescription(), isActive: binding)
}
}
struct SessionRow: View {
var session: Session
// pass variables as `#Binding`...
#Binding var presentGraph: Session?
#Binding var presentDescription: Session?
var body: some View {
HStack {
Button {
presentGraph = session // ...and activate them manually
} label: {
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button {
presentDescription = session
print("\(session.dir):\(presentDescription)")
} label: {
Image(systemName: "info.circle")
}
.buttonStyle(PlainButtonStyle())
}
.padding(.vertical, 4)
}
}

Popover displaying inaccurate information inside ForEach

I'm having a problem where I have a ForEach loop inside a NavigationView. When I click the Edit button, and then click the pencil image at the right hand side on each row, I want it to display the text variable we are using from the ForEach loop. But when I click the pencil image for the text other than test123, it still displays the text test123 and I have absolutely no idea why.
Here's a video. Why is this happening?
import SwiftUI
struct TestPopOver: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var showThemeEditor = false
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.showThemeEditor = true
}
}
}
}
}
.popover(isPresented: $showThemeEditor) {
CustomPopOver(isShowing: $showThemeEditor, text: text)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}
}
}
struct CustomPopOver: View {
#Binding var isShowing: Bool
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.isShowing = false
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
This is a very common issue (especially since iOS 14) that gets run into a lot with sheet but affects popover as well.
You can avoid it by using popover(item:) rather than isPresented. In this scenario, it'll actually use the latest values, not just the one that was present when then view first renders or when it is first set.
struct EditItem : Identifiable { //this will tell it what sheet to present
var id = UUID()
var str : String
}
struct ContentView: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var editItem : EditItem? //the currently presented sheet -- nil if no sheet is presented
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.editItem = EditItem(str: text) //set the current item
}
}
}
}
}
.popover(item: $editItem) { item in //item is now a reference to the current item being presented
CustomPopOver(text: item.str)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct CustomPopOver: View {
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
I also opted to use the presentationMode environment property to dismiss the popover, but you could pass the editItem binding and set it to nil as well (#Binding var editItem : EditItem? and editItem = nil). The former is just a little more idiomatic.

Sidebar with different columns in SwiftUI

I'm playing with the new Sidebar that has come with SwiftUI 2 and the possibility to navigate in large screens with three columns. An example about how it works can be found here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-sidebar-for-ipados
It works fine, but I would like to go one step forward and make some options of my main menu that show the three columns but other options just two.
Here an example of some demo code.
import SwiftUI
struct ContentView: View {
var body: some View{
NavigationView{
List{
Section(header: Text("Three columns")){
NavigationLink(
destination: ItemsView(),
label: {
Label("Animals",systemImage: "tortoise")
})
NavigationLink(
destination: ItemsView(),
label: {
Label("Animals 2",systemImage: "hare")
})
}
Section(header: Text("Two columns")){
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label("Settings",systemImage: "gear")
})
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label("Settings 2",systemImage: "gearshape")
})
}
}
.listStyle(SidebarListStyle())
.navigationBarTitle("App Menu")
ItemsView()
DetailView(animal: "default")
}
}
}
struct ItemsView: View{
let animals = ["Dog", "Cat", "Lion", "Squirrel"]
var body: some View{
List{
ForEach(animals, id: \.self){ animal in
NavigationLink(
destination: DetailView(animal: animal)){
Text(animal)
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Animals")
}
}
struct DetailView: View{
var animal: String
var body: some View{
VStack{
Text("🐕")
.font(.title)
.padding()
Text(animal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
If you run the code in, for example, an iPad Pro (12,9-inch) in landscape mode, you can see the tree columns. First the App Menu (sidebar). If you click on one of the first two options (animals, and animals 2), you can see a list of animals (second column) and when you click on some animal, you reach the third column (detail view).
However, I want to have only two columns when I click on the last two options of the menu (Settings and Settings 2). Any clue how to achieve it?
I've tried to hide that section if some of the first options in menu are not selected (with the selected parameter in NavigationLink), but without luck. It seems it is not possible (or I don't know) to know which option is selected in the sidebar.
Any idea is welcome!
It took me a few days to figure it out.
Test on different iPad device & multiple tasking mode, all works as expected.(iOS14+, haven't test on iOS13)
Minimal Example:
extension UISplitViewController {
open override func viewDidLoad() {
super.viewDidLoad()
self.show(.primary) // show sidebar, this is the key, toke me days to find this...
self.showsSecondaryOnlyButton = true
}
}
struct ContentView: View {
#State var column: Int = 3
var body: some View {
switch column {
case 3: // Triple Column
NavigationView {
List {
HStack {
Text("Triple")
}
.onTapGesture {
column = 3
}
HStack {
Text("Double")
}
.onTapGesture {
column = 2
}
}
Text("Supplementary View")
Text("Detail View")
}
default: // Double Column
NavigationView {
List {
HStack {
Text("Triple")
}
.onTapGesture {
column = 3
}
HStack {
Text("Double")
}
.onTapGesture {
column = 2
}
}
Text("Supplementary View")
}
}
}
}
My another answer: set sidebar default selected item.
Combine with this two solution, I have built an 2&3 column co-exist style's app.
SwiftUI, selecting a row in a List programmatically
Here is a solution which uses a custom ViewModifier. It's working on iOS 14.2, 15.0 and 15.2. Since you are using SidebarListStyle and Label I didn't test for prior versions.
Testproject:
enum Item: Hashable {
case animals, animals2, settings, settings2
static var threeColumns = [Item.animals, .animals2]
static var twoColumns = [Item.settings, .settings2]
var title: String {
switch self {
case .animals:
return "animals"
case .animals2:
return "animals2"
case .settings:
return "settings"
case .settings2:
return "settings2"
}
}
var systemImage: String {
switch self {
case .animals:
return "tortoise"
case .animals2:
return "hare"
case .settings:
return "gear"
case .settings2:
return "gearshape"
}
}
}
struct ContentView: View {
#State var selectedItem: Item?
var body: some View {
NavigationView {
List {
Section(header: Text("Three columns")) {
ForEach(Item.threeColumns, id: \.self) { item in
NavigationLink(tag: item, selection: $selectedItem) {
ItemsView()
} label: {
Label(item.title.capitalized, systemImage: item.systemImage)
}
}
}
Section(header: Text("Two columns")) {
ForEach(Item.twoColumns, id: \.self) { item in
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label(item.title, systemImage: item.systemImage)
})
}
}
}
.listStyle(SidebarListStyle())
.navigationBarTitle("App Menu")
}
}
}
struct ItemsView: View {
let animals = ["Dog", "Cat", "Lion", "Squirrel"]
var body: some View {
List {
ForEach(animals, id: \.self) { animal in
NavigationLink(
destination: DetailView(animal: animal)) {
Text(animal)
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Animals")
}
}
struct DetailView: View {
var animal: String
var body: some View {
VStack {
Text("🐕")
.font(.title)
.padding()
Text(animal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
private struct ColumnModifier: ViewModifier {
let item: Item?
func body(content: Content) -> some View {
if item == .settings || item == .settings2 {
content
EmptyView()
} else {
content
EmptyView()
DetailView(animal: "default")
}
}
}
Suppose you want to create a triple-column view (with two sidebars and a detail view). Here is an example to show Projects in the first column, Files in second column and File Content in the last column.
The core step is to add three View()s in NavagationView { ... }.
import SwiftUI
struct MainView: View {
var body: some View {
ProjectsSidebar()
}
}
struct ProjectsSidebar: View {
var body: some View {
NavigationView {
List {
ForEach([1, 2, 3], id: \.self) { project_id in
VStack {
NavigationLink {
FilesSidebar(project_id: project_id)
.navigationTitle("Project \(project_id)")
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.columns)
} label: {
Text("Project \(project_id)")
}
}
}
}
.listStyle(.sidebar)
.navigationTitle("Projects")
.navigationBarTitleDisplayMode(.inline)
FilesSidebar.DefaultView()
DetailView.DefaultView()
}
.navigationViewStyle(.columns)
}
}
struct FilesSidebar: View {
var project_id: Int
var body: some View {
List {
ForEach([1, 2, 3, 4], id: \.self) { file_id in
NavigationLink {
DetailView(project_id: project_id, file_id: file_id)
.navigationTitle("File")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("File \(file_id)")
}
}
}
.listStyle(.sidebar)
}
struct DefaultView: View {
var body: some View {
VStack {
Text("Please select a project.")
}
}
}
}
struct DetailView: View {
var project_id: Int
var file_id: Int
var body: some View {
VStack {
Text("Project \(project_id) - File \(file_id)")
}
}
struct DefaultView: View {
var body: some View {
VStack {
Text("Please select a file.")
}
}
}
}
first launch:
triple columns:
select project and file:

SwiftUI: Picker content not refreshed when adding Element

I have a Picker Element in a VStack, but when its content changes by adding a new Element, the Picker does not refresh.
After hiding and showing the Picker, the new Element is visible.
Does anybody have any idea how to refresh the content of a Picker, without needing to hide / show it?
You can reproduce this by creating a new SwiftUI project and copying the following code instead of the "ContentView" struct.
class ContentModel {
#Published var pickerData: [String] = ["1"]
func addPickerData() {
pickerData.append("\(pickerData.count + 1)")
}
}
struct ContentView: View {
let contentModel = ContentModel()
#State private var showPicker = false
#State private var selectedPickerValue = ""
var body: some View {
VStack(spacing: 8) {
Text("Adding a new Element to the Picker does not refresh its content :-(")
Button(action: {
self.contentModel.addPickerData()
}) {
Text("Add Picker Data")
}
Button(action: {
self.showPicker.toggle()
}) {
Text("Show / Hide 2nd Picker")
}
Picker("Select",selection: $selectedPickerValue) {
ForEach(contentModel.pickerData, id: \.self) { data in
Text(data)
}
}
if (showPicker) {
Picker("Select",selection: $selectedPickerValue) {
ForEach(contentModel.pickerData, id: \.self) { data in
Text(data)
}
}
}
Text("Selected Value: \(selectedPickerValue)")
}
}
}
Thanks in advance for any help!
Here is the trick of reactive and always use two copies of same thing when you need to refresh something.
class ContentModel{
#Published var pickerData: [String] = ["1"]
func addPickerData() {
pickerData.append("\(pickerData.count + 1)")
}
}
struct ContentSSView: View {
let contentModel = ContentModel()
#State private var showPicker = false
#State private var selectedPickerValue = ""
var body: some View {
VStack(spacing: 8) {
Text("Adding a new Element to the Picker does not refresh its content :-(")
Button(action: {
self.contentModel.addPickerData()
self.showPicker.toggle()
}) {
Text("Add Picker Data")
}
Button(action: {
self.showPicker.toggle()
}) {
Text("Show / Hide 2nd Picker")
}
if (showPicker) {
Picker("Select",selection: $selectedPickerValue) {
ForEach(contentModel.pickerData, id: \.self) { data in
Text(data)
}
}
}else{
Picker("Select",selection: $selectedPickerValue) {
ForEach(contentModel.pickerData, id: \.self) { data in
Text(data)
}
}
}
Text("Selected Value: \(selectedPickerValue)")
}
}
}
I have a GitHub repo with this issue. I don't think having two Pickers is a viable solution.
Picker Update Bug GitHub Repo