Why NavigationLink() stays open in SwiftUI 2 - swift

I have the following code. When app started MasterView is opening and then I click a row and going to DetailView. After I'm changing the tab in RootTabView to OtherView. And then when I turned back to the MasterView its automatically opens the DetailView.
Also both of vm.getList() and vm.getDetail() methods works. Why is that happening in SwiftUI 2? Because in SwiftUI 1 it wasn't work like that.
struct RootTabView: View {
#State var tabSelection = 0
#State private var vm = ViewModel()
var body: some View {
TabView(selection: $tabSelection) {
MasterView(vm: vm).tabItem({
Text("Master")
}).tag(0)
OtherView().tabItem({
Text("Other")
}).tag(1)
}
}
}
struct MasterView: View {
#ObservedObject var vm: ViewModel
var body: some View {
NavigationView {
List(vm.toDoList, id: \.self) { toDo in
NavigationLink(destination: DetailView(vm: vm)) {
Text(toDo)
}
}
}
.onAppear {
vm.getList()
}
}
}
struct DetailView: View {
#ObservedObject var vm: ViewModel
var body: some View {
Text(vm.toDoItem)
.onAppear {
vm.getDetail()
}
}
}
class ViewModel: ObservableObject {
#Published var toDoList: [String] = []
#Published var toDoItem: String = ""
func getList() {
toDoList = ["a", "b", "c"]
}
func getDetail() {
// do some stuffs
toDoItem = "A"
}
}

Related

View dismisses unintentionally in SwiftUI

My goal is to have an array of structs that when tapped on an individual item, a change is made and passed up to the parent view but doesn't automatically dismiss my child view. I'm really not sure why the child view is automatically dismissing and how to prevent it. Here is my code for the ContentView. Note that I have now updated the struct to Identifiable.
//
// ContentView.swift
// test
//
// Created by Kevin McQuown on 3/7/22.
//
import SwiftUI
struct Person: Identifiable {
var id = UUID()
var name: String = ""
}
struct ContentView: View {
#State private var patients: [Person]
init() {
var temp: [Person] = []
for index in 0 ..< 3 {
temp.append(Person(name: "\(index)"))
}
patients = temp
}
var body: some View {
NavigationView {
VStack(spacing: 40) {
ForEach($patients) { $patient in
NavigationLink("\(patient.name)") {
View2(patient: $patient)
}
}
}
}
}
}
struct View2: View {
#Binding var patient: Person
var body: some View {
NavigationLink("\(patient.name)") {
ChangeNameView(patient: $patient)
}
}
}
struct ChangeNameView: View {
#Binding var patient: Person
var body: some View {
Button {
patient.name = "New Name"
} label: {
Text("Set New Name")
}
}
}

SwiftUI Executing a method from a view after confirmation from the subview

Is it technically possible to call a method from a view after getting the confirmation from the subview ? I could call it from the SubStruct if I pass the viewModel and item to it, but I am just curious about the code below which results in
Segmentation Fault: 11
import SwiftUI
struct ContentView: View {
var body: some View {
MainStruct(viewModel: MyViewModel())
}
}
struct MainStruct: View {
#StateObject var viewModel: MyViewModel
#State var functionToPass: ()
let items = ["test1", "test2", "test2"]
var body: some View {
ForEach(items, id: \.self) { (item) in
Text(item).onTapGesture {
functionToPass = viewModel.deleteItem(name: item)
}
}
SubStruct(passedFunction: {functionToPass})
}
struct SubStruct: View {
var passedFunction: () -> Void
var body: some View {
Button(action: {passedFunction()}, label: {
Text("confirm deletion")
})
}
}
}
class MyViewModel: ObservableObject {
func deleteItem(name: String) {
print(name)
///deletion logic
}
}
Try this :
struct MainStruct: View {
#StateObject var viewModel: MyViewModel
#State private var functionToPass: () -> Void = {}
let items = ["test1", "test2", "test2"]
var body: some View {
VStack {
ForEach(items, id: \.self) { item in
Text(item)
.onTapGesture {
functionToPass = {
viewModel.deleteItem(name: item)
}
}
}
SubStruct(passedFunction: functionToPass)
}
}
struct SubStruct: View {
var passedFunction: () -> Void
var body: some View {
Button(action: {
passedFunction()
}, label: {
Text("confirm deletion")
})
}
}
}

UserDefault value does not update in a list SwiftUI

I have two views, embedded in TabView.
I am using userdefaults in a class called usersettings.
class UserSettings: ObservableObject {
#Published var favList: [String] {
willSet {
print("willset")
}
didSet {
UserDefaults.standard.set(favList, forKey: "isAccountPrivate")
print("didset")
}
}
init() {
self.favList = UserDefaults.standard.object(forKey: "isAccountPrivate") as? [String] ?? ["Sepetiniz Boş"]
}
}
In Button View, which acts like add/remove favorite. It successfully adds and remove from the UserDefaults. But when I add something it does not show on the other view (please see the next code after FavButton)
struct FavButton: View {
#Binding var passedFood: String
#ObservedObject var userSettings = UserSettings()
var body: some View {
Button(action: {
if userSettings.favList.contains(passedFood) {
userSettings.favList.remove(at: userSettings.favList.firstIndex(of: passedFood )!)
} else {
userSettings.favList.append(passedFood)
}
})
}
}
But it does not update my list in this other view unless I close and open my app. If I remove something from the list, it actually removes from the userdefault.
If I add a new word within this view, it works too.
My only problem is when I add something from another view (FavButton) it does not show in this view (FavView).
struct FavView: View {
#ObservedObject var userSettings = UserSettings()
#State private var newWord = ""
var body: some View {
NavigationView {
List {
TextField("Ürün Ekleyin...", text: $newWord, onCommit: addNewWord)
ForEach( self.userSettings.favList, id: \.self) { list in
Text(list)
.font(.headline)
.padding()
}
.onDelete(perform: self.deleteRow)
}
.navigationTitle("Sepetim")
}
}
private func deleteRow(at indexSet: IndexSet) {
self.userSettings.favList.remove(atOffsets: indexSet)
}
private func addNewWord() {
let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
self.userSettings.favList.append(answer)
guard answer.count > 0 else {
return
}
newWord = ""
}
}
A better approach to follow the SwiftUI idiom is to use the .environmentObject() modifier.
When you declare your app:
struct AppScene: App {
#StateObject private var userSettings = UserSettings() // Use state object to persist the object
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userSettings) // Inject userSettings into the environment
}
}
}
and then in you ContentView you can reach into your environment and get the object:
struct ContentView: View {
#EnvironmentObject private var userSettings: UserSettings
var body: some View {
Text("Number of items in favList: \(userSettings.favList.count)")
}
}
You need to use same instance of UserSettings in all views where you want to have observed user settings, like
class UserSettings: ObservableObject {
static let global = UserSettings()
//... other code
}
and now
struct FavButton: View {
#ObservedObject var userSettings = UserSettings.global // << here !!
// ... other code
}
and
struct FavView: View {
#ObservedObject var userSettings = UserSettings.global // << here !!
// ... other code
}

change a value inside a swiftUI view based in a change made in his child

I have this code:
Main view
import SwiftUI
struct ContentView: View {
#EnvironmentObject var data:Pessoa
var body: some View {
NavigationView{
VStack{
NavigationLink(destination:view2(data: data)){
Text(data.data.firstObject as! String)
}
}
}.environmentObject(data)
}
}
2nd view
import SwiftUI
struct view2: View {
var data:Pessoa
var body: some View {
Button(action: {
self.data.data[0] = "New btn Text"
}){
Text("Edit Btn Texr")
}.environmentObject(data)
}
}
Class Pessoa
class Pessoa:ObservableObject {
var data:NSMutableArray
init() {
self.data = NSMutableArray()
self.data.add("Btn")
}
}
How I can update the main view when I return form the 2nd view.
Yes I need to pass the object or at least, the array.
The main idea is in a structure like:
V1 -> v2 -> V3
if I make a change in some parameter of a class, in the V3, how I can propagate (in the layout) this change to the v2 and v1
Just to get you up and running, you could use the #Published property wrapper and for your example you actually don't need #EnvironmentObject. You can use #ObservedObject instead...
class Pessoa: ObservableObject {
#Published var data: Array<String>
init() {
self.data = Array()
self.data.append("Btn")
}
}
struct view2: View {
var data: Pessoa
var body: some View {
Button(action: {
self.data.data[0] = "New btn Text"
}){
Text("Edit Btn Texr")
}
}
}
struct ContentView: View {
#ObservedObject var data = Pessoa()
var body: some View {
NavigationView{
VStack{
NavigationLink(destination:view2(data: data)){
Text(data.data.first ?? "N/A")
}
}
}
}
}
But you should check the link of Joakim...

Updating Picker on update of ObservedObject

I'm trying up dynamically add new rows to a Picker as follows:
class ViewModel: ObservableObject {
#Published private (set) var drinks = ["Tea", "Coffee", "Wine"]
func addDrink(_ drink: String) {
drinks.append(drink)
}
}
struct PickerTest: View {
#State private var selectedDrink = "Tea"
#State private var customDrink = ""
#ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack {
HStack {
TextField("Enter a drink", text: $customDrink)
Spacer()
Button("Add") {
self.viewModel.addDrink(self.customDrink)
}
}
Picker("Drinks", selection: $selectedDrink) { // Removing the wrapping Picker works
ForEach(viewModel.drinks, id: \.self) { drink in
Text(drink)
}
}
}.padding().labelsHidden()
}
}
This doesn't work. If I remove the Picker wrapping the ForEach, the ForEach updates as expected.
Is there a way to update the Picker dynamically?
It looks like Pickers bug - I hope, that Apple fixes it in future releases of SwiftUI.
I found ugly (I really don't like it) workaround for this problem:
class ViewModel: ObservableObject {
#Published var selectedDrink = "Tea"
#Published var drinks = ["Tea", "Coffee", "Wine"]
#Published var drinksChanged = true
func addDrink(_ drink: String) {
drinks.append(drink)
drinksChanged.toggle()
}
}
struct DrinksPicker: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Picker("Drinks", selection: $viewModel.selectedDrink) {
ForEach(viewModel.drinks, id: \.self) { drink in
Text(drink)
}
}
}
}
struct PickerTest: View {
#State private var customDrink = ""
#ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack {
HStack {
TextField("Enter a drink", text: $customDrink)
Spacer()
Button("Add") {
self.viewModel.addDrink(self.customDrink)
self.customDrink = ""
}
}
if viewModel.drinksChanged {
DrinksPicker(viewModel: viewModel)
} else {
DrinksPicker(viewModel: viewModel)
}
}.padding().labelsHidden()
}
}
You can also hide this if-else in some another container:
struct DrinksPickerContainer: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Group {
if viewModel.drinksChanged {
DrinksPicker(viewModel: viewModel)
} else {
DrinksPicker(viewModel: viewModel)
}
}
}
}
and then use only DrinksPickerContainer(viewModel: viewModel) in PickerTest