#ObservedObject does not get updated on updating the array - swift

Before I lose my mind over this. May someone please tell me, why it is not working. I am already hours in trying to make this work. ObservedObject just refuses to update my view.
struct ContentView: View {
#ObservedObject var viewModel = ListViewModel()
var body: some View {
NavigationView {
List(){
ForEach(viewModel.items, id:\.self) { item in
Text(item)
}
}
.navigationBarTitle("Test List", displayMode: .inline)
.navigationBarItems(
trailing: Button(action: { ListViewModel().addItem()}) { Image(systemName: "info.circle")}.accentColor(.red))
}
}
}
class ListViewModel: ObservableObject, Identifiable {
#Published var items: Array<String> = ["1", "2", "3", "4", "5","6"]
func addItem(){
items.append("7")
}
}

You're creating a new ListViewModel by using an initializer (ListViewModel()) in your button action.
Instead, refer to the one that you've set up as property in your view: viewModel.addItem()

Related

NavigationLink destination immediately disappear after loading

I'm quite new to Swift and I'm struggling with this implementation. I moved my code to playground so it's easier for you to debug if you copy/paste on your Xcode.
Basically once loading View3 the view immediately disappear and you are pushed back to View2.
I identify the issue to be with the categories array on the View2. If I separate the 3 categories in 3 NavigationLink (instead of looping) the code works just fine.
I just don't understand why this doesn't work and I'd love if you help me find out what's wrong with this implementation?
import UIKit
import Foundation
import SwiftUI
import PlaygroundSupport
struct Article: Identifiable {
var id = UUID()
var title : String
}
struct Category: Identifiable {
var id = UUID()
var name : String
}
class DataManager: ObservableObject{
#Published var articles : [Article] = []
func getArticles(){
let article = Article(title: "Just a test")
if !articles.contains(where: {$0.title == article.title}) {
articles.append(article)
}
}
func clearArticles(){
articles.removeAll()
}
}
struct View1: View{
#StateObject var dataManager = DataManager()
var body: some View {
NavigationView{
List(){
NavigationLink(destination: View2(), label: { Text("View 2") })
}
.navigationBarTitle("View 1")
.listStyle(.insetGrouped)
}
.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(dataManager)
}
}
struct View2: View{
let categories = [
Category(name: "Category 1"),
Category(name: "Category 2"),
Category(name: "Category 3")
]
var body: some View {
List(categories){ category in
NavigationLink(destination: View3(category: category), label: { Text(category.name) })
}
.navigationBarTitle("View 2")
.navigationBarTitleDisplayMode(.inline)
}
}
struct View3: View{
#EnvironmentObject var dataManager: DataManager
let category: Category
var body: some View {
List(dataManager.articles) { article in
HStack {
Text(article.title)
}
}.task{
dataManager.getArticles()
}.onDisappear(){
dataManager.clearArticles()
}
.navigationTitle("View 3")
.navigationBarTitleDisplayMode(.inline)
}
}
PlaygroundPage.current.setLiveView(View1())
This is the resulting behaviour:
The Category id is not stable so every time the View2 is init, a new array with different ids is created so the ForEach is creating new NavigationLinks which are not active so the previously active one is gone and it reacts by popping it off the nav stack, try this:
struct Category: Identifiable {
var id: String {
name
}
var name : String
}
But it would be better if you put them in the data store object so you aren't creating them every time View2 is init.

My published variables in my view Model get reset to their default values when i run the code

I have been having problems with updating a published variable in my model, so I tried to replicate the problem with a very basic and simple set of files/codes. So basically in NavLink view, there is a navigation link, which when clicked, it updates the published variable in ListRepository model by giving it a string value of "yes", prints it to the console then navigates to its destination which is called ContentView view. The problem is in ContentView, I tried to print the data contained in the published variable called selectedFolderId hoping it will print "yes", but i noticed that instead of printing the value that was set in NavLink view, it instead printed the default value of "", which was not what was set in NavLink view. Please can anyone explain the reason for this behaviour and explain to me how it can fix this as i am very new in swift ui. That will mean alot.
Please find the supporting files below:
import SwiftUI
struct NavLink: View {
#StateObject var listRepository = ListRepository()
var body: some View {
NavigationView{
ScrollView {
NavigationLink("Hello world", destination: ContentView(listRepository: listRepository))
Text("Player 1")
Text("Player 2")
Text("Player 3")
}
.simultaneousGesture(TapGesture().onEnded{
listRepository.selectedFolderId = "yes"
listRepository.md()
})
.navigationTitle("Players")
}
}
}
struct NavLink_Previews: PreviewProvider {
static var previews: some View {
NavLink()
}
}
import Foundation
class ListRepository: ObservableObject {
#Published var selectedFolderId = ""
func md(){
print("=====")
print(self.selectedFolderId)
print("======")
}
}
import SwiftUI
struct ContentView: View {
#ObservedObject var taskListVM = ShoppingListItemsViewModel()
#ObservedObject var listRepository:ListRepository
var body: some View {
VStack{
Text("content 1")
Text("content 2")
Text("content 3")
}
.onAppear{
taskListVM.addTask()
print("========")
print(listRepository.selectedFolderId)
print("========")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class ShoppingListItemsViewModel: ObservableObject {
#Published var listRepository = ListRepository()
#Published var taskCellViewModels = [ShoppingListItemCellViewModel]()
private var cancellables = Set<AnyCancellable>()
init() {
listRepository.$tasks
.map { lists in
lists.map { list in
ShoppingListItemCellViewModel(task: list)
}
}
.assign(to: \.taskCellViewModels, on: self)
.store(in: &cancellables)
}
func addTask() {
listRepository.addTask(task)
}
}
This is a common issue when you first deal with data flow in an app. The problem is straightforward. In your 'NavLink' view you are creating one version of ListRepository, and in ContentView you create a separate and different version of ListRepository. What you need to do is pass the ListRepository created in NavLink into ContentView when you call it. Here is one example as to how:
struct NavLink: View {
#StateObject var listRepository = ListRepository() // Create as StateObject, not ObservedObject
var body: some View {
NavigationView{
ScrollView {
NavigationLink("Hello world", destination: ContentView(listRepository: listRepository)) // Pass it here
Text("Player 1")
Text("Player 2")
Text("Player 3")
}
.simultaneousGesture(TapGesture().onEnded{
listRepository.selectedFolderId = "yes"
listRepository.md()
})
.navigationTitle("Players")
}
}
}
struct ContentView: View {
#ObservedObject var listRepository: ListRepository // Do not create it here, just receive it
var body: some View {
VStack{
Text("content 1")
Text("content 2")
Text("content 3")
}
.onAppear{
print("========")
print(listRepository.selectedFolderId)
print("========")
}
}
}
You should also notice that I created ListRepository as a StateObject. The view that originally creates an ObservableObject must create it as a StateObject or you can get undesirable side effects.

"Variable is used before initialized" when #Environment(\.dismiss) is used

I'm trying to have a very simple SwiftUI view hierarchy:
parent and a child.
The child should be able to dismiss itself with a tap of a button.
The following code works fine
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
ChildView()
} label: {
Text("Go")
}
}
}
}
struct ChildView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
Text("Hello, world!")
.padding()
Button(action: {
dismiss()
}, label: {
Text("Dismiss")
})
}
}
However, when I try to add a simple state and init() to the ChildView, I'm hitting a compilation error
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
ChildView(foo: 42)
} label: {
Text("Go")
}
}
}
}
struct ChildView: View {
#Environment(\.dismiss) var dismiss
#State private var myInt: Int
init(foo: Int) {
self.myInt = foo // <--- ERROR IS HERE, "Variable 'self.myInt' used before being initialized"
}
var body: some View {
Text("Hello, world!")
.padding()
Button(action: {
dismiss()
}, label: {
Text("Dismiss")
})
}
}
This error is very puzzling. I tried different things and as far as I can tell, the problem is in the combination of init() and #Environment(\.dismiss) presents.
But I really need to have them both for the real project.
Anybody knows what's the reason for the error or how to fix it?
Xcode version: 13.4.1
A #State var has to be initialised differently:
self._myInt = State(initialValue: foo)

Edit details of a dynamic list of core data object

I used the Xcode default CoreData template to build my app.
I have tried to use CoreData and create an entity like this:
I then created a AddItemView which allows me to add item to the view.
struct AddItemView: View {
#Environment(\.managedObjectContext) var viewContext
#Environment(\.presentationMode) var presentationMode
#State private var notes = ""
#State private var selectedDate = Date()
var body: some View {
NavigationView {
Form {
Section {
TextField("notes", text: $notes)
}
Section {
DatePicker("", selection: $selectedDate, displayedComponents: .date)
Text("Your selected date: \(selectedDate)")
}
Section {
Button("Save") {
let newItem = Item(context: self.viewContext)
newItem.notes = self.notes
newItem.recordDate = self.selectedDate
newItem.timestamp = Date()
try? self.viewContext.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Item")
}
}
}
It works well and can add items.
Then I want to click on each of the item to go to a Detail View. In the DetailView, there should be an edit button to allow me to modify the object.
I therefore created three files for the purpose: ItemHost, DetailView, EditorView
The Navigation Destination of the item will go to the ItemHost.
struct ItemListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
#State private var showingAddScreen = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: ItemHost(item: item)) {
VStack {
Text("Item at \(item.timestamp!, formatter: FormatterUtility.dateTimeFormatter)")
Text("notes: \(item.notes ?? "")")
Text("Item Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
#if os(iOS)
EditButton()
#endif
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {self.showingAddScreen.toggle()}) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $showingAddScreen) {
AddItemView().environment(\.managedObjectContext, self.viewContext)
}
}
}
The ItemHost as follows:
struct ItemHost: View {
#Environment(\.editMode) var editMode
#Environment(\.managedObjectContext) var contextView
#State var item: Item
var body: some View {
NavigationView {
if editMode?.wrappedValue == .active {
Button("Cancel") {
editMode?.animation().wrappedValue = .inactive
}
}
if editMode?.wrappedValue == .inactive {
ItemDetailView(item: item)
} else {
ItemEditor(item: item)
}
}.navigationBarTitle("EditMode Problem")
.navigationBarItems(trailing: EditButton())
}
}
The DetailView is just a view to display the details, without any special.
struct ItemDetailView: View {
#Environment(\.managedObjectContext) var contextView
#Environment(\.presentationMode) var presentationMode
#State private var showingDeleteAlert = false
let item: Item
var body: some View {
VStack {
Text("notes: \(item.notes ?? "")")
Text("Record Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
.navigationBarTitle(Text("Item Detail"), displayMode: .inline)
.alert(isPresented: $showingDeleteAlert) {
Alert(title: Text("Delete Item"), message: Text("Are you sure?"),
primaryButton: .destructive(Text("Delete")) {
self.deleteItem()
}, secondaryButton: .cancel()
)
}
.navigationBarItems(trailing: Button(action: {
self.showingDeleteAlert = true
}) {
Image(systemName: "trash")
})
}
// Problem here
// Can delete the item and go back to list page. But the actual item in the CoreData has not been removed. If I call contextView.save() it will crash.
func deleteItem() {
contextView.delete(item)
presentationMode.wrappedValue.dismiss()
}
}
The EditorView like this:
struct ItemEditor: View {
#Environment(\.presentationMode) var presentation
#State var item: Item
var body: some View {
List {
HStack {
Text("Notes").bold()
TextField("Notes", text: $item.notes) // Error
}
// Error
DatePicker(selection: $item.recordDate, displayedComponents: .date) {
Text("Record Date").bold()
}
}
}
}
A few problem here:
ItemEditor: Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding'. I have no way to pick the original item object values and display it to let the user know what was the old value inside the object.
Nothing to be displayed once I click on the individual navigation item. I expect that it will originally (not edit mode) and then show the detail view. If it is edit mode, then show the editor.
I get confused with the #binding and how to pass the item into the DetailView and also the Editor. How the editor save the data back to the item object in the contextView?
For the deleteItem() in the ItemDetailView. It can remove the item and go back to the ItemListView apparently. However, when I quit the app, and then run again. I found that the item re-appeared again, not really deleted.
Click on the item now, it shows this:
Don't use #State to var Item in Core Data. You should use #ObservedObject instead. It will refresh a parent view after updating data.
Please read this article:
https://purple.telstra.com/blog/swiftui---state-vs--stateobject-vs--observedobject-vs--environme

SwiftUI - Is there a popViewController equivalent in SwiftUI?

I was playing around with SwiftUI and want to be able to come back to the previous view when tapping a button, the same we use popViewController inside a UINavigationController.
Is there a provided way to do it so far ?
I've also tried to use NavigationDestinationLink to do so without success.
struct AView: View {
var body: some View {
NavigationView {
NavigationButton(destination: BView()) {
Text("Go to B")
}
}
}
}
struct BView: View {
var body: some View {
Button(action: {
// Trying to go back to the previous view
// previously: navigationController.popViewController(animated: true)
}) {
Text("Come back to A")
}
}
}
Modify your BView struct as follows. The button will perform just as popViewController did in UIKit.
struct BView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
Button(action: { self.mode.wrappedValue.dismiss() })
{ Text("Come back to A") }
}
}
Use #Environment(\.presentationMode) var presentationMode to go back previous view. Check below code for more understanding.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.gray.opacity(0.2)
NavigationLink(destination: NextView(), label: {Text("Go to Next View").font(.largeTitle)})
}.navigationBarTitle(Text("This is Navigation"), displayMode: .large)
.edgesIgnoringSafeArea(.bottom)
}
}
}
struct NextView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Color.gray.opacity(0.2)
}.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "arrow.left") }))
.navigationBarTitle("", displayMode: .inline)
}
}
struct NameRow: View {
var name: String
var body: some View {
HStack {
Image(systemName: "circle.fill").foregroundColor(Color.green)
Text(name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
With State Variables. Try that.
struct ContentViewRoot: View {
#State var pushed: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination:ContentViewFirst(pushed: self.$pushed), isActive: self.$pushed) { EmptyView() }
.navigationBarTitle("Root")
Button("push"){
self.pushed = true
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentViewFirst: View {
#Binding var pushed: Bool
#State var secondPushed: Bool = false
var body: some View {
VStack{
NavigationLink(destination: ContentViewSecond(pushed: self.$pushed, secondPushed: self.$secondPushed), isActive: self.$secondPushed) { EmptyView() }
.navigationBarTitle("1st")
Button("push"){
self.secondPushed = true;
}
}
}
}
struct ContentViewSecond: View {
#Binding var pushed: Bool
#Binding var secondPushed: Bool
var body: some View {
VStack{
Spacer()
Button("PopToRoot"){
self.pushed = false
} .navigationBarTitle("2st")
Spacer()
Button("Pop"){
self.secondPushed = false
} .navigationBarTitle("1st")
Spacer()
}
}
}
This seems to work for me on watchOS (haven't tried on iOS):
#Environment(\.presentationMode) var presentationMode
And then when you need to pop
self.presentationMode.wrappedValue.dismiss()
There is now a way to programmatically pop in a NavigationView, if you would like. This is in beta 5.
Notice that you don't need the back button. You could programmatically trigger the showSelf property in the DetailView any way you like. And you don't have to display the "Push" text in the master. That could be an EmptyView(), thereby creating an invisible segue.
(The new NavigationLink functionality takes over the deprecated NavigationDestinationLink)
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State var showDetail = false
var body: some View {
VStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Text("Push")
}
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
It seems that a ton of basic navigation functionality is super buggy, which is disappointing and may be worth walking away from for now to save hours of frustration. For me, PresentationButton is the only one that works. TabbedView tabs don't work properly, and NavigationButton doesn't work for me at all. Sounds like YMMV if NavigationButton works for you.
I'm hoping that they fix it at the same time they fix autocomplete, which would give us much better insight as to what is available to us. In the meantime, I'm reluctantly coding around it and keeping notes for when fixes come out. It sucks to have to figure out if we're doing something wrong or if it just doesn't work, but that's beta for you!
Update: the NavigationDestinationLink API in this solution has been deprecated as of iOS 13 Beta 5. It is now recommended to use NavigationLink with an isActive binding.
I figured out a solution for programmatic pushing/popping of views in a NavigationView using NavigationDestinationLink.
Here's a simple example:
import Combine
import SwiftUI
struct DetailView: View {
var onDismiss: () -> Void
var body: some View {
Button(
"Here are details. Tap to go back.",
action: self.onDismiss
)
}
}
struct MainView: View {
var link: NavigationDestinationLink<DetailView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
DetailView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
VStack {
Button("I am root. Tap for more details.", action: {
self.link.presented?.value = true
})
}
.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
struct RootView: View {
var body: some View {
NavigationView {
MainView()
}
}
}
I wrote about this in a blog post here.
You can also do it with .sheet
.navigationBarItems(trailing: Button(action: {
self.presentingEditView.toggle()
}) {
Image(systemName: "square.and.pencil")
}.sheet(isPresented: $presentingEditView) {
EditItemView()
})
In my case I use it from a right navigation bar item, then you have to create the view (EditItemView() in my case) that you are going to display in that modal view.
https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)
EDIT: This answer over here is better than mine, but both work: SwiftUI dismiss modal
What you really want (or should want) is a modal presentation, which several people have mentioned here. If you go that path, you definitely will need to be able to programmatically dismiss the modal, and Erica Sadun has a great example of how to do that here: https://ericasadun.com/2019/06/16/swiftui-modal-presentation/
Given the difference between declarative coding and imperative coding, the solution there may be non-obvious (toggling a bool to false to dismiss the modal, for example), but it makes sense if your model state is the source of truth, rather than the state of the UI itself.
Here's my quick take on Erica's example, using a binding passed into the TestModal so that it can dismiss itself without having to be a member of the ContentView itself (as Erica's is, for simplicity).
struct TestModal: View {
#State var isPresented: Binding<Bool>
var body: some View {
Button(action: { self.isPresented.value = false }, label: { Text("Done") })
}
}
struct ContentView : View {
#State var modalPresented = false
var body: some View {
NavigationView {
Text("Hello World")
.navigationBarTitle(Text("View"))
.navigationBarItems(trailing:
Button(action: { self.modalPresented = true }) { Text("Show Modal") })
}
.presentation(self.modalPresented ? Modal(TestModal(isPresented: $modalPresented)) {
self.modalPresented.toggle()
} : nil)
}
}
Below works for me in XCode11 GM
self.myPresentationMode.wrappedValue.dismiss()
instead of NavigationButton use Navigation DestinationLink
but You should import Combine
struct AView: View {
var link: NavigationDestinationLink<BView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
BView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
NavigationView {
Button(action:{
self.link.presented?.value = true
}) {
Text("Go to B")
}.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
}
struct BView: View {
var onDismiss: () -> Void
var body: some View {
Button(action: self.onDismiss) {
Text("Come back to A")
}
}
}
In the destination pass the view you want to redirect, and inside block pass data you to pass in another view.
NavigationLink(destination: "Pass the particuter View") {
Text("Push")
}