Why my SwiftUI List is not getting updated? - binding

I use a SwiftUI List and pass a String to a different view via a Binding.
But the list get not updated when I went back.
Here is my example:
struct ContentView: View {
#State private var list = ["a", "b", "c"]
#State private var item: String?
#State private var showSheet = false
var body: some View {
List {
ForEach(list.indices) { i in
Button(action: {
item = list[i]
showSheet.toggle()
}) {
Text(list[i])
}
}
}
.sheet(isPresented: $showSheet, content: {
DetailView(input: $item)
})
}
}
And the detail page:
struct DetailView: View {
#Binding var input: String?
var body: some View {
Text(input ?? "")
.onDisappear{
print("changed to changed")
input = "changed"
}
}
}
What I want to achieve is, that on every Item I click, I see the detail page. After that the element should change to "changed". But this does not happen. Why?

You update item but not list, so don't see any result. Here is corrected variant - store selected index and pass binding to list by index.
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
#State private var list = ["a", "b", "c"]
#State private var item: Int?
var body: some View {
List {
ForEach(list.indices) { i in
Button(action: {
item = i
}) {
Text(list[i])
}
}
}
.sheet(item: $item, content: { i in
DetailView(input: $list[i])
})
}
}
extension Int: Identifiable {
public var id: Self { self }
}
struct DetailView: View {
#Binding var input: String
var body: some View {
Text(input)
.onDisappear{
print("changed to changed")
input = "changed"
}
}
}

I recommend you use .sheet(item:content:) instead of .sheet(isPresented:content:)
struct ContentView: View {
#State private var items = ["a", "b", "c"]
#State private var selectedIndex: Int?
var body: some View {
List {
ForEach(items.indices) { index in
Button(action: {
selectedIndex = index
}) {
Text(items[index])
}
}
}
.sheet(item: $selectedIndex) { index in
DetailView(item: $items[index])
}
}
}
struct DetailView: View {
#Binding var item: String
var body: some View {
Text(item)
.onDisappear {
print("changed to changed")
item = "changed"
}
}
}
This will, however, require the selectedIndex to conform to Identifiable.
You can either create an Int extension:
extension Int: Identifiable {
public var id: Int { self }
}
or create a custom struct for your data (and conform to Identifiable).

Related

Removing multiple sheets after swiping down the last one swiftui

I currently have two sheets in a row and I want them to be dismissed to the view that called the first sheet once the last sheet is dismissed by the user. I am open to not pulling up views as sheets its just the way I learned how to easily pull up new views.
BookView is what I want to be returned after the PickDefinition sheet view has been dismissed.
BookView pulls up AddWord as a sheet.
AddWord is pulled up as a sheet and then in it PickDefinition is pulled up as a sheet.
After PickDefinition is dismissed I would like for it to go back to the BookView
struct BookView: View {
#ObservedObject var book: Book
#State var addingWord = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
List(Array(zip(book.words, book.definitions)), id: \.self.0) { (word, definition) in
Text("\(word) - \(definition)")
}
}
.onAppear(perform: {
DB_Manager().openBook(name: book.name, book: self.book)
})
.navigationBarTitle("\(book.name)")
.navigationBarItems(trailing: Button(action: {
self.addingWord = true
}) {
Image(systemName: "plus")
}
).sheet(isPresented: $addingWord) {
AddWord(book: self.book)
}
}
}
struct AddWord: View {
#ObservedObject var book: Book
#StateObject var currentArray = SaveArray()
#State var addingDefinition = false
#State var word = ""
#State var definition = ""
#State var allDefinitions: [String] = []
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
TextField("Word: ", text: $word)
}
.navigationBarTitle("Add word")
.navigationBarItems(trailing: Button("Add") {
if self.word != "" {
book.words.append(self.word)
getDef(self.word, book, currentArray)
addingDefinition = true
}
}).sheet(isPresented: $addingDefinition) {
PickDefinition(definitions: currentArray, book: book, word: self.word)
}
}
}
struct PickDefinition: View {
#ObservedObject var definitions: SaveArray
var book: Book
var word: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
List {
ForEach(0 ..< definitions.currentArray.count) { index in
Button("\(self.definitions.currentArray[index])", action: {
print("hello")
DB_Manager().addWords(name: self.book.name, word: self.word, definition: self.definitions.currentArray[index])
book.definitions.append(self.definitions.currentArray[index])
self.presentationMode.wrappedValue.dismiss()
})
}
}
}
.navigationTitle("Choose")
}
}
}
You need to pass addingWord into the AddWord view, and then into the PickDefinition view, using Binding. When PickDefinition disappears, set the passed property to false to hide the AddWord sheet.
struct BookView: View {
var body: some View {
// ...
.sheet(isPresented: $addingWord) {
AddWord(book: self.book, presentAddingWord: $addingWord)
}
}
}
struct AddWord: View {
#Binding var presentAddingWord: Bool
// ...
var body: some View {
// ...
.sheet(isPresented: $addingDefinition) {
PickDefinition(/* ... */, presentAddingWord: $presentAddingWord)
}
}
}
struct PickDefinition: View {
#Binding var presentAddingWord: Bool
// ...
var body: some View {
// ...
.onDisappear {
presentAddingWord = false
}
}
}

SwiftUI MVVM Binding List Item

I am trying to create a list view and a detailed screen like this:
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
LazyVStack {
// https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
ForEach(viewModel.items.identifiableIndicies) { index in
MyListItemView($viewModel.items[index])
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = []
...
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item), label: {
...
})
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
#StateObject var viewModel: MyListViewItemDetailModel
init(item: Binding<Item>) {
viewModel = MyListViewItemDetailModel(item: item)
}
var body: some View {
...
}
}
class MyListViewItemDetailModel: ObservableObject {
var item: Binding<Item>
...
}
I am not sure what's wrong with it, but I found that item variables are not synced with each other, even between MyListItemDetailView and MyListItemDetailViewModel.
Is there anyone who can provide the best practice and let me know what's wrong in my implmentation?
I think you should think about a minor restructure of your code, and use only 1
#StateObject/ObservableObject. Here is a cut down version of your code using
only one StateObject source of truth:
Note: AFAIK Binding is meant to be used in View struct not "ordinary" classes.
PS: what is identifiableIndicies?
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Item: Identifiable {
let id = UUID().uuidString
var name: String = ""
}
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
var body: some View {
LazyVStack {
ForEach(viewModel.items.indices) { index in
MyListItemView(item: $viewModel.items[index])
}
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = [Item(name: "one"), Item(name: "two")]
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item)){
Text(item.name)
}
}
}
class MyAPIModel {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your fetching here
completion(Item(name: "new data from api"))
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
let myApiModel = MyAPIModel()
var body: some View {
VStack {
Button(action: fetchNewData) {
Text("Fetch new data")
}
TextField("edit item", text: $item.name).border(.red).padding()
}
}
func fetchNewData() {
myApiModel.fetchItemData() { itemData in
item = itemData
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyListView()
}.navigationViewStyle(.stack)
}
}
EDIT1:
to setup an API to call some functions, you could use something like this:
class MyAPI {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your stuff
}
}
and use it to obtain whatever data you require from the server.
EDIT2: added some code to demonstrate the use of an API.

SwiftUI NavigationLink in the list doesn't get the right detail page with isActive

I just want to simply navigate to a detail page from a List if press any cell. I have a list like this:
When I click cell c it gets d or others. Rather than this page.
Here is my code:
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var isCellSelected = false
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: Text("\(items[index])"),
isActive: $isCellSelected,
label: {
RowView(text: items[index])
})
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct RowView: View {
var text: String
var body: some View {
VStack {
Text(text)
}
}
}
If I remove isActive: $isCellSelected then it works as expected. I need to use isCellSelected to pop back to root view. Not sure how to fix this issue.
Any help? thanks!
EDIT
Update removed isActive and try set selection = nil
truct DetailView: View {
var text: String
#Binding var isCellSelected: Int?
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = nil
}
}
}
}
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: DetailView(text: "\(items[index])", isCellSelected: $selectedTag),
tag: index,
selection: $selectedTag,
label: {
RowView(text: items[index])
})
}
}
}
}
}
When press Back button, doesn't go back.
It is not recommended to share a single isActive state among multiple NavigationLinks.
Why don't you use selection instead of isActive?
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil //<-
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: Text("\(items[index])"),
tag: index, //<-
selection: $selectedTag, //<-
label: {
RowView(text: items[index])
})
}
}
}
}
}
You can set nil to selectedTag to pop back. Seems NavigationLink in List does not work as I expect. Searching for workarounds and update if found.
A dirty workaround:
(Tested with Xcode 12.3/iPhone simulator 14.3. Please do not expect this to work on other versions of iOS including future versions.)
struct DetailView: View {
var text: String
#Binding var isCellSelected: Bool
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = false
}
}
}
}
struct Item {
var text: String
var isActive: Bool = false
}
struct ContentView: View {
#State var items = ["a", "b", "c", "d"].map {Item(text: $0)}
#State var listId: Bool = false //<-
var body: some View {
// Text(items.description) // for debugging
NavigationView {
List {
ForEach(items.indices) { index in
NavigationLink(
destination:
DetailView(text: "\(items[index].text)",
isCellSelected: $items[index].isActive)
.onAppear{ listId.toggle() } //<-
,
isActive: $items[index].isActive,
label: {
RowView(text: items[index].text)
})
}
}
.id(listId) //<-
}
}
}
Another workaround:
struct DetailView: View {
var text: String
#Binding var isCellSelected: Int?
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = nil
}
}
}
}
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil
var body: some View {
NavigationView {
ZStack {
ForEach(items.indices) { index in
NavigationLink(
destination:
DetailView(text: "\(items[index])",
isCellSelected: $selectedTag),
tag: index,
selection: $selectedTag,
label: {
EmptyView()
})
}
List {
ForEach(items.indices) { index in
Button(action: {
selectedTag = index
}) {
HStack {
RowView(text: items[index])
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(Color.secondary)
}
}
}
}
}
}
}
}

SwiftUI set a Binding from the String result of a picker

I am tearing out my hair trying to figure out how to bind the picked value in my SwiftUI view:
The picker needs to be bound to the Int returned from the tags. I need to covert this Int to the String and set the Binding. How?
struct ContentView: View {
#Binding var operatorValueString:String
var body: some View {
Picker(selection: queryType, label: Text("Query Type")) {
ForEach(0..<DM.si.operators.count) { index in
Text(DM.si.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}
How and where can I set my operatorValueString ?
operatorValueString = DM.si.operators[queryType] //won't compile.
You can achieve the result, using your own custom binding that sets the string, whenever the picker's selection changes:
struct ContentView: View {
#State private var operatorString = ""
var body: some View {
VStack {
Subview(operatorValueString: $operatorString)
Text("Selected: \(operatorString)")
}
}
}
struct Subview: View {
#Binding var operatorValueString: String
#State private var queryType: Int = 0
let operators = ["OR", "AND", "NOT"]
var body: some View {
let binding = Binding<Int>(
get: { self.queryType },
set: {
self.queryType = $0
self.operatorValueString = self.operators[self.queryType]
})
return Picker(selection: binding, label: Text("Query Type")) {
ForEach(operators.indices) { index in
Text(self.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}

SwiftUI Picker in a Form doesn't show the selected row

I am trying to have a Picker that shows which option is currently selected.
Try out the following code which correctly selects the right option but the picker does not show which option is selected:
import SwiftUI
struct ContentView: View {
#State var selectedIndex: Int = 0
let strings: [String] = {
var strings: [String] = []
for i in 0..<10 {
strings.append("\(i)")
}
return strings
}()
var body: some View {
NavigationView {
VStack {
Form {
Picker(selection: $selectedIndex,
label: Text("Selected string: \(strings[selectedIndex])")) {
ForEach(0..<strings.count) {
Text(self.strings[$0]).tag($0)
}
}
}
}
.navigationBarTitle("Form Picker",
displayMode: NavigationBarItem.TitleDisplayMode.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Anyone know what could be wrong? It's observed using Xcode 11.1 and iOS 13.1
I created the simple picker I call "ListPicker" which should fit the bill. I've written it so it works well in a Form; if you need it outside of a Form you will have to tinker with it. If you see any way to improve the code, please add a comment; this is still a learning experience for all of us.
// MARK: - LIST PICKER (PUBLIC)
struct ListPicker<Content: View>: View {
#Binding var selectedItem: Int
var label: () -> Content
var data: [Any]
var selectedLabel: String {
selectedItem >= 0 ? "\(data[selectedItem])" : ""
}
var body: some View {
NavigationLink(destination: ListPickerContent(selectedItem: self.$selectedItem, data: self.data)) {
ListPickerLabel(label: self.label, value: "\(self.selectedLabel)")
}
}
}
// MARK: - INTERNAL
private struct ListPickerLabel<Content: View>: View {
let label: () -> Content
let value: String
var body: some View {
HStack(alignment: .center) {
self.label()
Spacer()
Text(value)
.padding(.leading, 8)
}
}
}
private struct ListPickerContentItem: View {
let label: String
let index: Int
let isSelected: Bool
var body: some View {
HStack {
Text(label)
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}.background(Color.white) // so the entire row is selectable
}
}
private struct ListPickerContent: View {
#Environment(\.presentationMode) var presentationMode
#Binding var selectedItem: Int
var data: [Any]
var body: some View {
List {
ForEach(0..<data.count) { index in
ListPickerContentItem(label: "\(self.data[index])", index: index, isSelected: index == self.selectedItem).onTapGesture {
self.selectedItem = index
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
Then you can use it like this:
#State var selectedCar: Int = 0
let cars = ["Jaguar", "Audi", "BMW", "Land Rover"]
Form {
ListPicker(
selectedItem: self.$selectedCar,
label: {
Text("Cars")
},
data: self.cars
)
}

Resources