Issue with NavigationLink ID in List - list

I'm trying to enable the edit functionality for list using a NavigationLink. I have the following:
EditView - want to use this view to edit the list items:
import SwiftUI
import Combine
struct EditView: View {
#Binding var index: String
#EnvironmentObject var taskStore: TaskStore
var body: some View {
Text("Hello, World!")
}
}
struct EditView_Previews: PreviewProvider {
#State static var index = ""
static let taskStore = TaskStore()
static var previews: some View {
EditView(index: $index)
}
}
And the list that should use the NavigationLink to edit the item:
if(self.taskStore.tasks.count != 0){
List {
ForEach(self.taskStore.tasks) {
task in
NavigationLink(destination: EditView(index: task.$id)){
VStack(alignment: .leading, spacing: 0){
HStack{
VStack{
Text(task.description)
.font(.headline)
Text(String(task.outputYears) + "YRS" + String(task.outputMonths) + "MTHS")
Spacer()
}
}
}
}
}
.onDelete(perform: self.onDelete)
}
.navigationBarTitle("Menu")
.listStyle(PlainListStyle())
.padding(.top, 5)
}
This line seem to be the issue:
NavigationLink(destination: EditView(index: task.$id))
How do I pass the task id to the EditView to enable me to edit the right time in the task store?

You don't need binding for index in this case - just pass it as value
struct EditView: View {
var index: String // << here !!
//...
}
and
NavigationLink(destination: EditView(index: task.id)){ // << here !!

Related

Hiding the chevron for navigation link [duplicate]

How to delete Navigation Link > symbol in SwiftUI?
Navigation code
NavigationView{
List{
ForEach(messages) { item in
NavigationLink(destination: MessageDetailView()){
ChatRowView(chat: item)
.padding(.vertical,3)
}
}
}
Chevron image of link is injected by List for automatically detected NavigationLink. This is default behavior of List.
The possible solution is to replace NavigationLink inside List with Button and activate NavigationLink programmatically.
Here is a demo of approach. Tested with Xcode 12.4 / iOS 14.4
struct Message: Identifiable, Hashable {
let id: String
}
struct ContentView: View {
let messages = [Message(id: "1"), Message(id: "2"), Message(id: "3")]
#State private var tappedItem: Message?
var body: some View {
NavigationView{
List{
ForEach(messages) { item in
Button(action: { tappedItem = item }) { // << activate !!
Text("Message \(item.id)")
.padding(.vertical,3)
}
}
}
.background(
NavigationLink(destination: Text("MessageDetailView \(tappedItem?.id ?? "")"),
isActive: Binding(
get: { tappedItem != nil }, // << handle !!
set: { _,_ in tappedItem = nil }
)){
EmptyView()
}
)
}
}
}
You should not use List in this case:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
VStack {
ForEach(0..<10) { item in
NavigationLink(destination: Text("some text here!")){
HStack { Text("Link " + item.description); Spacer() }.padding(.horizontal)
}
Divider()
}
Spacer()
}
.navigationTitle("Hello World!")
}
}
}

How to delete Navigation Link > symbol in SwiftUI?

How to delete Navigation Link > symbol in SwiftUI?
Navigation code
NavigationView{
List{
ForEach(messages) { item in
NavigationLink(destination: MessageDetailView()){
ChatRowView(chat: item)
.padding(.vertical,3)
}
}
}
Chevron image of link is injected by List for automatically detected NavigationLink. This is default behavior of List.
The possible solution is to replace NavigationLink inside List with Button and activate NavigationLink programmatically.
Here is a demo of approach. Tested with Xcode 12.4 / iOS 14.4
struct Message: Identifiable, Hashable {
let id: String
}
struct ContentView: View {
let messages = [Message(id: "1"), Message(id: "2"), Message(id: "3")]
#State private var tappedItem: Message?
var body: some View {
NavigationView{
List{
ForEach(messages) { item in
Button(action: { tappedItem = item }) { // << activate !!
Text("Message \(item.id)")
.padding(.vertical,3)
}
}
}
.background(
NavigationLink(destination: Text("MessageDetailView \(tappedItem?.id ?? "")"),
isActive: Binding(
get: { tappedItem != nil }, // << handle !!
set: { _,_ in tappedItem = nil }
)){
EmptyView()
}
)
}
}
}
You should not use List in this case:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
VStack {
ForEach(0..<10) { item in
NavigationLink(destination: Text("some text here!")){
HStack { Text("Link " + item.description); Spacer() }.padding(.horizontal)
}
Divider()
}
Spacer()
}
.navigationTitle("Hello World!")
}
}
}

ContentView redraw from change in modalView not triggered

Ever since the advent of swiftUI 2.0, I have been unable to update a view according to a change done in another modally-presented view (the settings view).
I display a string on my main ContentView that derives its content from a segmented Picker value on the SettingsView.
The problem is that after the user changes the setting and discards the SettingsView, the string in ContentView is not updated. The body is not redrawn.
I am making use of #ObservableObject and #StateObject so every change to it should trigger a redraw, but I can't make it work...
I created a class that conforms to the ObservableObject protocol : AppState
I am using that class to try and pass data and -more importantly- data changes between the views in order to have my ContentView redrawn according to the the user's settings.
In order to instantiate this class, I registered a single UserDefaults in my AppDelegate file.
I also imported the Combine Framework into my project and added the import Combine line in each and every file !
I've simplified my code as much as possible, in order to illustrate the issue, so the following might seem a bit circumvolutated, but it is derived from a much more complex app, sorry about that.
Here is my ContentView code :
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var appState: AppState
#State var modalViewCaller = 0 // used to present correct modalView
#State var modalIsPresented = false // to present the modal views
var body: some View {
let stringArray = generateString() // func to generate string according to user's pref
let recapString = stringArray[0]
return ZStack {
NavigationView {
VStack {
// MARK: - texts :
VStack {
Text(recapString)
.bold()
.multilineTextAlignment(/*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
} // end of VStack
.padding()
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color(UIColor.systemBlue), lineWidth: 4))
.padding()
} // END of VStack
.onAppear() {
self.modalViewCaller = 0
print("\n\n*********** Content View onAppear triggered ! ************\n")
}
.navigationBarTitle("DataFun", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 1 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.onAppear() {
self.appState.updateValues()
}
} // End of ZStack
.sheet(isPresented: $modalIsPresented) {
sheetContent(modalViewCaller: $modalViewCaller, appState: AppState())
}
.navigationViewStyle(StackNavigationViewStyle())
}
// MARK: - struct sheetContent() :
struct sheetContent: View {
#Binding var modalViewCaller: Int // Binding to the #State modalViewCaller variable from ContentView
#StateObject var appState: AppState
var body: some View {
if modalViewCaller == 1 { // The settings view is called
SettingsView(appState: AppState())
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
} else if modalViewCaller == 2 { // the "other view" is called
OtherView()
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
}
}
} // END of func sheetContent
// MARK: - generateString()
func generateString() -> [String] {
var recapString = "" // The recap string
var myArray = [""]
// We create the recap string :
if UserDefaults.standard.integer(forKey: "rules selection") == 0 { // ICAO
recapString = "User chose LEFT"
} else if UserDefaults.standard.integer(forKey: "rules selection") == 1 { // AF Rules
recapString = "User chose RIGHT"
}
myArray = [recapString]
return myArray
} // End of func generateString()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(appState: AppState())
}
}
Here is my AppState code :
import Foundation
import SwiftUI
import Combine
class AppState: ObservableObject {
#Published var rulesSelection: Int = UserDefaults.standard.integer(forKey: "rules selection")
func updateValues() { // When the user changes a setting, the UserDefault is updated. Here, we align the AppState's value with what is now in the UserDefaults
self.rulesSelection = UserDefaults.standard.integer(forKey: "rules selection")
print("\nappState value (ruleSelection) updated from Appstate class func \"updateValues")
}
}
Here is my SettingsView code :
import SwiftUI
import Combine
struct SettingsView: View {
#Environment(\.presentationMode) var presentationMode // in order to dismiss the Sheet
#StateObject var appState: AppState
#State private var rulesSelection = UserDefaults.standard.integer(forKey: "rules selection") // 0 is LEFT, 1 is RIGHT
var body: some View {
NavigationView {
VStack {
Spacer()
Text("Choose a setting below")
.padding()
Picker("", selection: $rulesSelection) {
Text("LEFT").tag(0)
Text("RIGHT").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
Spacer()
}
.navigationBarItems(
leading:
Button("Done") {
self.saveDefaults() // We set the UserDefaults
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
// self.modalViewCaller = 0
}
) // END of NavBarItems
} // END of NavigationBiew
} // END of body
func saveDefaults() {
UserDefaults.standard.set(rulesSelection, forKey: "rules selection")
self.appState.updateValues() // This is a func from the AppState class that will align the appState's value to the UserDefaults
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView(appState: AppState())
}
}
And a working project if anyone has the time to check this "live" :
https://github.com/Esowes/dataFun
Thanks for any pointers.
Regards.
Well... it was... in short many changes, so here is complete ContentView.swift with fixes.
Note: you need only one StateObject, and one instance set into it, and you need to have published property of observable object in view otherwise it is not refreshed, and changes in UserDefaults do not refresh view until you use AppStorage, etc.
Verified with Xcode 12.1 / iOS 14.1
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var appState: AppState
#State var modalViewCaller = 0 // used to present correct modalView
#State var modalIsPresented = false // to present the modal views
var body: some View {
return ZStack {
NavigationView {
VStack {
// MARK: - texts :
VStack {
RecapStringView(appState: appState)
} // end of VStack
.padding()
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color(UIColor.systemBlue), lineWidth: 4))
.padding()
} // END of VStack
.onAppear() {
self.modalViewCaller = 0
print("\n\n*********** Content View onAppear triggered ! ************\n")
}
.navigationBarTitle("DataFun", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 1 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.onAppear() {
self.appState.updateValues()
}
} // End of ZStack
.sheet(isPresented: $modalIsPresented) {
sheetContent(modalViewCaller: $modalViewCaller, appState: appState)
}
.navigationViewStyle(StackNavigationViewStyle())
}
// MARK: - struct sheetContent() :
struct sheetContent: View {
#Binding var modalViewCaller: Int // Binding to the #State modalViewCaller variable from ContentView
#ObservedObject var appState: AppState
var body: some View {
if modalViewCaller == 1 { // The settings view is called
SettingsView(appState: appState)
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
} else if modalViewCaller == 2 { // the "other view" is called
OtherView()
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
}
}
} // END of func sheetContent
}
struct RecapStringView: View {
#ObservedObject var appState: AppState
var body: some View {
Text("User chose " + "\(appState.rulesSelection == 0 ? "LEFT" : "RIGHT")")
.bold()
.multilineTextAlignment(.center)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(appState: AppState())
}
}

SwiftUI Reload View

I have a struct which shuffles and Lists records from CoreData.
I would like to reload / Refresh the List view with a Button.
I tried to use a function from within the Button.
Is there a way I can do this?
var body: some View {
VStack {
List {
ForEach(dictionary.shuffled().prefix(upTo: 10),id: \.self) { word in
HStack {
Text("\(word.englishWord)")
.foregroundColor(Color.blue)
Text("| \(word.urhoboWord) |")
.foregroundColor(Color.green)
Image(word.imageName)
.resizable()
.frame(width:40, height: 40)
}//HStack
}//End of ForEach
}//End of List
//Button to reload and shuffle list
Button(action: {}) {
Text("Shuffle")
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.cornerRadius(6)
}
.navigationBarTitle(Text("Begin Learning"),displayMode: .inline)
Just trigger any value of the #State or #Published of #ObservableObject.
If you do not have such, just create one:
#State var refresh: Bool = false
func update() {
refresh.toggle()
}
You should move this dictionary.shuffled().prefix(upTo: 10) to your ViewModel and your view just reload base on the data.
Take a look at this code for reference:
struct SampleShuffleView : View {
#ObservedObject var viewModel : ShuffleViewModel = ShuffleViewModel()
var body : some View {
VStack {
List(self.viewModel.listData, id: \.self) { str in
Text(str)
}
Button(action: self.shuffle) {
Text("Shuffle me").padding()
}.background(Color.white).padding()
}
}
func shuffle() {
self.viewModel.shuffle()
}
}
class ShuffleViewModel : ObservableObject {
#Published var listData = ["one", "two", "three", "four"]
func shuffle() {
listData.shuffle()
//or listData = dictionary.shuffled().prefix(upTo: 10)
}
}
Note: All view's components will be reloaded when #ObservedObject changes, so consider to separate smaller view-viewmodel(s), or using #State variable.
Hope this helps.
Think about. To show array and shuffle on tap, do exactly what you would like to see. first show us the array in some "list" like manner and next shuffle it on user action.
struct ContentView: View {
#State var arr = ["ALFA", "BETA", "GAMA", "DELTA"]
var body: some View {
VStack {
VStack {
Divider()
ForEach(arr, id: \.self) { element in
VStack {
Text(element)
Divider()
}
}
}
Spacer()
Button(action: {
self.arr.shuffle()
}) {
Text("Shuffle")
}
Spacer()
}
}
}
arr.shuffle() changed the #State of View and force SwiftUI to "reload it" automatically.

SwiftUI - List gives error "Type of expression is ambiguous without more context"

I am trying to implement a List with Multiple selection.
This is the Code:
import SwiftUI
struct MultiSelectionWithEditButton : View {
var items = ["One","Two", "Three"]
#State var selectedRows = Set<String>()
var body: some View {
NavigationView {
List((items, id: \.self), selection: $selectedRows) { name in
Text(name)
.font(.title)
.fontWeight(.bold)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle(Text("Selected \(selectedRows.count) rows"))
}
}
}
#if DEBUG
struct MultiSelectionWithEditButton_Previews : PreviewProvider {
static var previews: some View {
MultiSelectionWithEditButton()
}
}
#endif
But I get error bellow:
"Type of expression is ambiguous without more context"
Appreciate if anyone can help! : )
Problem is in yours Lists init. Change it to List with ForEach inside, like below:
struct MultiSelectionWithEditButton : View {
var items = ["One","Two", "Three"]
#State var selectedRows = Set<String>()
var body: some View {
NavigationView {
List(selection: $selectedRows){
ForEach(items, id: \.self) { name in
Text(name)
.font(.title)
.fontWeight(.bold)
}
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle(Text("Selected \(selectedRows.count) rows"))
}
}
}