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")
}
}
}
Related
I have a problem with Navigation View hierarchy.
All screens in my app use the same ViewModel.
When a screen inside navigation link updates the ViewModel (here it is called DataManager), the navigation view automatically goes back to the first screen, as if the "Back" button was pressed.
Here's what it looks like
I tried to shrink my code as much as I could
struct DataModel: Identifiable, Codable {
var name: String
var isPinned: Bool = false
var id: String = UUID().uuidString
}
class DataManager: ObservableObject {
#Published private(set) var allModules: [DataModel]
var pinnedModules: [DataModel] {
allModules.filter { $0.isPinned }
}
var otherModules: [DataModel] {
allModules.filter { !$0.isPinned }
}
func pinModule(id: String) {
if let moduleIndex = allModules.firstIndex(where: { $0.id == id }) {
allModules[moduleIndex].isPinned = true
}
}
func unpinModule(id: String) {
if let moduleIndex = allModules.firstIndex(where: { $0.id == id }) {
allModules[moduleIndex].isPinned = false
}
}
static let instance = DataManager()
fileprivate init() {
allModules =
[DataModel(name: "One"),
DataModel(name: "Two"),
DataModel(name: "Three"),
DataModel(name: "Four"),
DataModel(name: "Five")]
}
}
struct ModulesList: View {
#StateObject private var dataStorage = DataManager.instance
var body: some View {
NavigationView {
List {
Section("Pinned") {
ForEach(dataStorage.pinnedModules) { module in
ModulesListCell(module: module)
}
}
Section("Other") {
ForEach(dataStorage.otherModules) { module in
ModulesListCell(module: module)
}
}
}
}
}
fileprivate struct ModulesListCell: View {
let module: DataModel
var body: some View {
NavigationLink {
SingleModuleScreen(module: module)
} label: {
Text(module.name)
}
}
}
}
struct SingleModuleScreen: View {
#State var module: DataModel
#StateObject var dataStorage = DataManager.instance
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(module.name)
.font(.title)
Button {
dataStorage.pinModule(id: module.id)
} label: {
Text("Pin")
}
}
}
}
}
I can guess because when your dataStorage changed, the ModulesList will be redrawn, that cause all current ModulesListCell removed from memory.
Your cells are NavigationLink and when it destroyed, the navigation stack doesn't keep the screen that's being linked.
I would recommend to watch this wwdc https://developer.apple.com/videos/play/wwdc2021/10022/ and you will know how to manage your view identity properly when your data's changed.
When you tap Pin, your otherModules array is recreated and you do not have the view in Navigation Stack from where you navigated. Thus you are going back automatically, which is desired behaviour. So the solution is Don't destroy your array from where your NavigationLink is created. Make a temporary published array, load Other modules from that array and change the array onAppear like below:
As a workaround which is working in my end:
Add this line in DataManger:
#Published var tempOtherModules:[DataModel] = []
Change your ModulesList like below
struct ModulesList: View {
#StateObject private var dataStorage = DataManager.instance
var body: some View {
NavigationView {
List {
Section("Pinned") {
ForEach(dataStorage.pinnedModules) { module in
ModulesListCell(module: module)
}
}
Section("Other") {
ForEach(dataStorage.tempOtherModules) { module in
ModulesListCell(module: module)
}
}
}.onAppear {
dataStorage.tempOtherModules = dataStorage.otherModules
}
}
}
}
I have a simple SwiftUI view with a Picker containing a list of objects from a data array. The Picker lists the objects just fine, but the selected value is not being saved to the binding variable $selectedCar. It returns empty string. This is the view in question:
struct GarageSpace: View {
var currentUserID: String
#Environment(\.presentationMode) var presentationMode
#Binding var selectedPlaceID: String
#Binding var selectedPlaceName: String
#Binding var selectedPlaceDate: Date
#Binding var selectedSpaceID: String
#State var selectedCar: String
#Binding var cars: CarArrayObject
var body: some View {
VStack{
Group{
Picker("Car", selection: $selectedCar) {
if let cars = cars{
ForEach(cars.dataArray, id: \.self) {car in
let year = car.year! as String
let make = car.make as String
let model = car.model! as String
let string = year + " " + make + " " + model
Text(string) //displays correctly in Picker
}
}
}
Spacer()
if let cars = cars {
Button {
print("yes")
print(selectedCar) //returns empty string
} label: {
Text("Confirm")
}
}
}
}
}
}
The above view is displayed via a NavigationLink on the previous screen:
NavigationLink(destination: GarageSpace(currentUserID: currentUserID, selectedPlaceID: $selectedPlaceID, selectedPlaceName: $selectedPlaceName, selectedPlaceDate: $selectedPlaceDate, selectedSpaceID: $selectedSpaceID, selectedCar: "", cars: $cars)) {
}
This NavigationLink might be the culprit because I'm sending an empty string for selectedCar. However, it forces me to initialize a value with the NavigationLink.
Any ideas? Thanks!
EDIT:
Added a tag of type String, still same outcome:
Text(string).tag(car.carID)
EDIT: FOUND THE ISSUE! However, I'm still stumped. The selection variable is empty because I wasn't pressing on the Picker since I only had one item in the array. How can I get the Picker to "select" an item if it's the only one in the array by default?
With tag, all works well in my simple tests. Here is my test code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
GarageSpace()
}
}
struct GarageSpace: View {
#State var selectedCar: String = ""
#State var cars: CarArrayObject? = CarArrayObject(car: CarModel(make: "Ford"))
var body: some View {
VStack {
Group {
Picker("Car", selection: $selectedCar) {
if let cars = cars {
ForEach(cars.dataArray, id: \.self) { car in
Text(car.make).tag(car.carID)
}
}
}
Spacer()
if let cars = cars {
Button {
print("----> selectedCar carID: \(selectedCar)")
} label: {
Text("Show selected carID")
}
}
}
}
// optional, to select the first car
.onAppear {
if let cars = cars {
selectedCar = (cars.dataArray.first != nil) ? cars.dataArray.first!.carID : ""
}
}
}
}
struct CarModel: Hashable {
var make = ""
var carID = UUID().uuidString
}
class CarArrayObject: ObservableObject{
// for testing
#Published var dataArray = [CarModel(make: "Toyota"), CarModel(make: "Suzuki"), CarModel(make: "VW")]
/// USED FOR SINGLE CAR SELECTION
init(car: CarModel) {
self.dataArray.append(car)
}
/// USED FOR GETTING CARS FOR USER PROFILE
init(userID: String) {
// print("GET CARS FOR USER ID \(userID)")
// DataService.instance.downloadCarForUser(userID: userID) { (returnedCars) in
//
// let sortedCars = returnedCars.sorted { (car1, car2) -> Bool in
// return car1.dateCreated > car2.dateCreated
// }
// self.dataArray.append(contentsOf: sortedCars)
// }
}
}
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"
}
}
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...
[ Ed: Once I had worked this out, I edited the title of this question to better reflect what I actually needed. - it wasn't until I answered my own question that I clarified what I needed :-) ]
I am developing an App using SwiftUI on IOS in which I have 6 situations where I will have a List of items which I can select and in all cases the action will be to move to a screen showing that Item.
I am a keen "DRY" advocate so rather than write the List Code 6 times I want to abstract away the list and select code and for each of the 6 scenarios I want to just provide what is unique to that instance.
I want to use a protocol but want to keep boilerplate to a minimum.
My protocol and associated support is this:
import SwiftUI
/// -----------------------------------------------------------------
/// ListAndSelect
/// -----------------------------------------------------------------
protocol ListAndSelectItem: Identifiable {
var name: String { get set }
var value: Int { get set }
// For listView:
static var listTitle: String { get }
associatedtype ItemListView: View
func itemListView() -> ItemListView
// For detailView:
var detailTitle: String { get }
associatedtype DetailView: View
func detailView() -> DetailView
}
extension Array where Element: ListAndSelectItem {
func listAndSelect() -> some View {
return ListView(items: self, itemName: Element.listTitle)
}
}
struct ListView<Item: ListAndSelectItem>: View {
var items: [Item]
var itemName: String
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(
destination: DetailView(item: item, index: String(item.value))
) {
VStack(alignment: .leading){
item.itemListView()
.font(.system(size: 15)) // Feasible that we should remove this
}
}
}
.navigationBarTitle(Text(itemName).foregroundColor(Color.black))
}
}
}
struct DetailView<Item: ListAndSelectItem>: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var item: Item
var index: String
var body: some View {
NavigationView(){
item.detailView()
}
.navigationBarTitle(Text(item.name).foregroundColor(Color.black))
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Text("<").foregroundColor(Color.black)}))
}
}
which means I can then just write:
struct Person: ListAndSelectItem {
var id = UUID()
var name: String
var value: Int
typealias ItemListView = PersonListView
static var listTitle = "People"
func itemListView() -> PersonListView {
PersonListView(person: self)
}
typealias DetailView = PersonDetailView
let detailTitle = "Detail Title"
func detailView() -> DetailView {
PersonDetailView(person: self)
}
}
struct PersonListView: View {
var person: Person
var body: some View {
Text("List View for \(person.name)")
}
}
struct PersonDetailView: View {
var person: Person
var body: some View {
Text("Detail View for \(person.name)")
}
}
struct ContentView: View {
let persons: [Person] = [
Person(name: "Jane", value: 1),
Person(name: "John", value: 2),
Person(name: "Jemima", value: 3),
]
var body: some View {
persons.listAndSelect()
}
}
which isn't bad but I feel I ought to be able to go further.
Having to write:
typealias ItemListView = PersonListView
static var listTitle = "People"
func itemListView() -> PersonListView {
PersonListView(person: self)
}
with
struct PersonListView: View {
var person: Person
var body: some View {
Text("List View for \(person.name)")
}
}
still seems cumbersome to me.
In each of my 6 cases I'd be writing very similar code.
I feel like I ought to be able to just write:
static var listTitle = "People"
func itemListView() = {
Text("List View for \(name)")
}
}
because that's the unique bit.
But that certainly won't compile.
And then the same for the Detail.
I can't get my head around how to simplify further.
Any ideas welcome?
The key to this is, if you want to use a view in a protocol then:
1) In the protocol:
associatedtype SpecialView: View
var specialView: SpecialView { get }
2) In the struct using the protocol:
var specialView: some View { Text("Special View") }
So in the situation of the question:
By changing my protocol to:
protocol ListAndSelectItem: Identifiable {
var name: String { get set }
var value: Int { get set }
// For listView:
static var listTitle: String { get }
associatedtype ListView: View
var listView: ListView { get }
// For detailView:
var detailTitle: String { get }
associatedtype DetailView: View
var detailView: DetailView { get }
}
I can now define Person as:
struct Person: ListAndSelectItem {
var id = UUID()
var name: String
var value: Int
static var listTitle = "People"
var listView: some View { Text("List View for \(name)") }
var detailTitle = "Person"
var detailView: some View { Text("Detail View for \(name)") }
}
which is suitable DRY and free of boilerplate!