I have a list of entries each with an attached date. I would like to display the date only if there is a change in date. I first developed this software in iOS 14.4 that resulted in a view immutable error. This was because I was storing and changing a copy of the entry date.
Now in version iOS 14.5 I don't see the immutable error. But my software still doesn't work. If you run the code and look in the console you will note that Xcode is going through my six entries twice: the first time is always true (show the date) and the second time always false (don't show the date). Why?
In my actual code I am using dates of type Date instead of Strings in this example code. In my actual code, operation hangs as it loops endlessly through my function checkDate (Many times more than the number of entries). Does date of type Date include the time causing the compare to fail?
Is there a better way to prevent display of the date if it is the same as the previous entry?
struct KitchenItem: Codable, Identifiable {
var id = UUID()
var item: String
var itemDate: String
var itemCost: Double
}
class Pantry: ObservableObject {
#Published var oldDate: String = ""
#Published var kitchenItem: [KitchenItem]
init() {
self.kitchenItem = []
let item0 = KitchenItem(item: "String Beans", itemDate: "1/13/2021", itemCost: 4.85)
self.kitchenItem.append(item0)
let item1 = KitchenItem(item: "Tomatoes", itemDate: "1/22/2021", itemCost: 5.39)
self.kitchenItem.append(item1)
let item2 = KitchenItem(item: "Bread", itemDate: "1/22/2021", itemCost: 4.35)
self.kitchenItem.append(item2)
let item3 = KitchenItem(item: "Corn", itemDate: "3/18/2021", itemCost: 2.75)
self.kitchenItem.append(item3)
let item4 = KitchenItem(item: "Peas", itemDate: "3/18/2021", itemCost: 7.65)
self.kitchenItem.append(item4)
let item5 = KitchenItem(item: "Ice Cream", itemDate: "4/12/2021", itemCost: 7.95)
self.kitchenItem.append(item5)
}
}
struct ContentView: View {
#ObservedObject var pantry: Pantry = Pantry()
var body: some View {
LazyVStack (alignment: .leading) {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
ForEach(0..<pantry.kitchenItem.count, id: \.self) { item in
VStack (alignment: .leading) {
showRow(item: item)
}
}
}
}
}
struct showRow: View {
#ObservedObject var pantry: Pantry = Pantry()
var item: Int
var body: some View {
// don't show the date if is the same as the previous entry
let newDate = pantry.kitchenItem[item].itemDate
if checkDate(newDate: newDate) == true {
Text("\n\(newDate)")
.font(.title2)
.padding(.leading, 10)
}
HStack {
Text("\(pantry.kitchenItem[item].item)")
.padding(.leading, 50)
.frame(width: 150, alignment: .leading)
Text("\(pantry.kitchenItem[item].itemCost, specifier: "$%.2f")")
}
}
func checkDate(newDate: String) -> (Bool) {
print(" ")
print("new date = \(newDate)")
if newDate == pantry.oldDate {
print("false: don't show the date")
return false
} else {
pantry.oldDate = newDate
print("old date = \(pantry.oldDate)")
print("true: show the date")
return true
}
}
}
Actual code:
struct ListView: View {
#EnvironmentObject var categories: Categories
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var viewContext
var money: String = ""
var xchRate: Double = 0.0
var cat: Int = 0
var mny: String = ""
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
GeometryReader { g in
ScrollView {
LazyVStack (alignment: .leading) {
TitleView()
ForEach(currTrans, id: \.self) { item in
showRow(item: item, priorDate: priorDate(forItemIndex: item), g: g)
}
.onDelete(perform: deleteItem)
}
.font(.body)
}
}
}
private func priorDate(forItemIndex item: Int) -> Date? {
guard item > 0 else { return nil }
return currTrans[item - 1].entryDT
}
}
struct showRow: View {
#EnvironmentObject var userData: UserData
var item: CurrTrans
var priorDate: Date?
var g: GeometryProxy
var payType = ["Cash/Debit", "Credit"]
var body: some View {
// don't show the date if is the same as the previous entry
let gotDate = item.entryDT ?? Date()
let newDate = gotDate.getFormattedDate()
Text("\(newDate)")
.opacity(gotDate == priorDate ? 0 : 1)
.font(.title2)
.padding(.leading, 10)
displays entry parameters in HStack...
Thou shalt not mutate thy data inside thy body method, for it is an abomination in the eyes of SwiftUI.
Modifying oldDate inside the body method is wrong. SwiftUI will get confused if you modify the data it is observing while it is rendering your views. Furthermore, SwiftUI doesn't make any guarantees about the order in which it renders the children of a LazyVStack (or any other container).
Is there a better way to prevent display of the date if it is the same as the previous entry?
Yes. Pass the current entry, and the prior entry's date, to the entry view.
Here's your data model and store, without the cruft:
struct KitchenItem: Codable, Identifiable {
var id = UUID()
var item: String
var itemDate: String
var itemCost: Double
}
class Pantry: ObservableObject {
#Published var kitchenItems: [KitchenItem] = [
.init(item: "String Beans", itemDate: "1/13/2021", itemCost: 4.85),
.init(item: "Tomatoes", itemDate: "1/22/2021", itemCost: 5.39),
.init(item: "Bread", itemDate: "1/22/2021", itemCost: 4.35),
.init(item: "Corn", itemDate: "3/18/2021", itemCost: 2.75),
.init(item: "Peas", itemDate: "3/18/2021", itemCost: 7.65),
.init(item: "Ice Cream", itemDate: "4/12/2021", itemCost: 7.95),
]
}
For each KitchenItem, you need to also extract the prior item's date, if there is a prior item. We'll use a helper method, priorDate(forItemIndex:), to do that. Also, you need to use StateObject, not ObservedObject, if you're going to create your store inside the view. Thus:
struct ContentView: View {
#StateObject var pantry: Pantry = Pantry()
var body: some View {
ScrollView(.vertical) {
LazyVStack (alignment: .leading) {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
ForEach(0 ..< pantry.kitchenItems.count) { i in
if i > 0 {
Divider()
}
KitchenItemRow(item: pantry.kitchenItems[i], priorDate: priorDate(forItemIndex: i))
}
}
}
}
private func priorDate(forItemIndex i: Int) -> String? {
guard i > 0 else { return nil }
return pantry.kitchenItems[i - 1].itemDate
}
}
Here is KitchenItemRow. You can see that it makes the date Text transparent if the date is the same as the prior item's date. I keep it in place but make it transparent so the row lays out the same:
struct KitchenItemRow: View {
var item: KitchenItem
var priorDate: String?
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(item.item)
Spacer()
Text("\(item.itemCost, specifier: "$%.2f")")
}
Text(item.itemDate)
.opacity(item.itemDate == priorDate ? 0 : 1)
}
.padding([.leading, .trailing], 10)
}
}
And here is TitleView, extracted from ContentView for hygiene:
struct TitleView: View {
var body: some View {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
}
}
Result:
UPDATE
Since your “real code” uses onDelete, it's important to give ForEach an id for each item instead of using the indexes.
Note that onDelete only works inside List, not inside LazyVStack.
So we need to map each item to its index, so we can find the prior item. Here's a revised version of my ContentView that uses onDelete:
struct ContentView: View {
#StateObject var pantry: Pantry = Pantry()
var body: some View {
let indexForItem: [UUID: Int] = .init(
uniqueKeysWithValues: pantry.kitchenItems.indices.map {
(pantry.kitchenItems[$0].id, $0) })
List {
TitleView()
ForEach(pantry.kitchenItems, id: \.id) { item in
let i = indexForItem[item.id]!
KitchenItemRow(item: item, priorDate: priorDate(forItemIndex: i))
}
.onDelete(perform: deleteItems(at:))
}
}
private func priorDate(forItemIndex i: Int) -> String? {
guard i > 0 else { return nil }
return pantry.kitchenItems[i - 1].itemDate
}
private func deleteItems(at offsets: IndexSet) {
pantry.kitchenItems.remove(atOffsets: offsets)
}
}
In my testing, this works and allows swipe-to-delete. I trust you can adapt it to your “real” code.
Related
I'm seeing very strange behavior within a view. Here's my layout:
struct EventDetailViewContainer: View {
let eventID: EventRecord.ID
#State var event: EventRecord = EventRecord(keyResults: [], text: "", achievesKR: false)
#State var editing: Bool = true
var body: some View {
if #available(iOS 15.0, *) {
VStack {
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
EventDetailView(event: $event, editing: $editing)
}
} else {
// Fallback on earlier versions
}
}
}
#available(iOS 15.0, *)
struct EventDetailView: View {
#Binding var event: EventRecord
#Binding var editing: Bool
#FocusState var textIsFocused: Bool
var body: some View {
VStack {
TextField(
"Event text",
text: $event.text
)
.focused($textIsFocused)
.disabled(!editing)
.padding()
DatePicker("Event Date:", selection: $event.date)
.disabled(!editing)
.padding()
Toggle("Goal is Reached?", isOn: $event.achievesKR)
.disabled(!editing)
.padding()
HStack {
Text("Notes:")
Spacer()
}
.padding()
TextEditor(text: $event.notes)
.disabled(!editing)
.padding()
Spacer()
}
}
}
struct EventRecord: Identifiable, Equatable {
typealias ID = Identifier
struct Identifier: Identifiable, Equatable, Hashable {
typealias ID = UUID
let id: UUID = UUID()
}
let id: ID
var keyResults: [KeyResult.ID]
var date: Date
var text: String
var notes: String
var achievesKR: Bool
init(
id: ID = ID(),
keyResults: [KeyResult.ID],
date: Date = Date(),
text: String,
notes: String = "",
achievesKR: Bool
) {
self.id = id
self.keyResults = keyResults
self.date = date
self.text = text
self.notes = notes
self.achievesKR = achievesKR
}
}
So this works perfectly when I run it as an iPad app, but when I run it on the simulator, the the top toggle doesn't respond to text input.
The strange thing is, when I simply duplicate the toggle, the top one doesn't work and the bottom one works perfectly:
struct EventDetailViewContainer: View {
let eventID: EventRecord.ID
#State var event: EventRecord = EventRecord(keyResults: [], text: "", achievesKR: false)
#State var editing: Bool = true
var body: some View {
if #available(iOS 15.0, *) {
VStack {
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
EventDetailView(event: $event, editing: $editing)
}
} else {
// Fallback on earlier versions
}
}
}
It seems like this should be totally unrelated to the touch behavior of the other views.
Btw this is being displayed in the context of a navigation view.
Is there anything that can explain this? And how can I get it working without adding this extra view on top?
edit: Here's a gif of this behavior being demonstrated. The two controls are exactly the same, but the lower one responds to touch and the upper one does not.
I am attempting the configure the text field and button in my openweathermap app to be in its own view other than the main content view. In TextFieldView, the action of the button is set up to call an API response. Then, the weather data from the response is populated on a sheet-based DetailView, which is triggered by the button in TextFieldView. I configured the ForEach method in the sheet to return the last city added to the WeatherModel array (which would technically be the most recent city entered into the text field), then populate the sheet-based DetailView with weather data for that city. Previously, When I had the HStack containing the text field, button, and sheet control set up in the ContentView, the Sheet would properly display weather for the city that had just entered into the text field. After moving those items to a separate TextFieldView, the ForEach method appears to have stopped working. Instead, the weather info returned after entering a city name into the text field is displayed on the wrong count. For instance, if I were to enter "London" in the text field, the DetailView in the sheet is completely blank. If I then enter "Rome" as the next entry, the DetailView in the sheet shows weather info for the previous "London" entry. Entering "Paris" in the textfield displays weather info for "Rome", and so on...
To summarize, the ForEach method in the sheet stopped working properly after I moved the whole textfield and button feature to a separate view. Any idea why the issue I described is happening?
Here is my code:
ContentView
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
List {
ForEach(viewModel.cityNameList.reversed()) { city in
NavigationLink(destination: DetailView(detail: city), label: {
Text(city.name).font(.system(size: 18))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°")
.font(.system(size: 18))
})
}
.onDelete { indexSet in
let reversed = Array(viewModel.cityNameList.reversed())
let items = Set(indexSet.map { reversed[$0].id })
viewModel.cityNameList.removeAll { items.contains($0.id) }
}
}
.refreshable {
viewModel.updatedAll()
}
TextFieldView(viewModel: viewModel)
}.navigationBarTitle("Weather", displayMode: .inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
TextFieldView
struct TextFieldView: View {
#State private var cityName = ""
#State private var showingDetail = false
#FocusState var isInputActive: Bool
var viewModel: WeatherViewModel
var body: some View {
HStack {
TextField("Enter City Name", text: $cityName)
.focused($isInputActive)
Spacer()
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Done") {
isInputActive = false
}
}
}
if isInputActive == false {
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
Image(systemName: "plus")
.font(.largeTitle)
.frame(width: 75, height: 75)
.foregroundColor(Color.white)
.background(Color(.systemBlue))
.clipShape(Circle())
}
.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
.frame(minWidth: 100, idealWidth: 150, maxWidth: 500, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
.padding(.leading, 16)
.padding(.trailing, 16)
}
}
struct TextFieldView_Previews: PreviewProvider {
static var previews: some View {
TextFieldView(viewModel: WeatherViewModel())
}
}
DetailView
struct DetailView: View {
#State private var cityName = ""
#State var selection: Int? = nil
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName.escaped())&units=imperial&appid=<YourAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.addToList(model)
}
}
catch {
print(error)
}
}
task.resume()
}
func updatedAll() {
// keep a copy of all the cities names
let listOfNames = cityNameList.map{$0.name}
// fetch the up-to-date weather info
for city in listOfNames {
fetchWeather(for: city)
}
}
func addToList( _ city: WeatherModel) {
// if already have this city, just update
if let ndx = cityNameList.firstIndex(where: {$0.name == city.name}) {
cityNameList[ndx].main = city.main
cityNameList[ndx].weather = city.weather
} else {
// add a new city
cityNameList.append(city)
}
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
var humidity = 0
}
struct WeatherInfo: Codable {
var description: String = ""
}
You need to use an ObservedObject in your TextFieldView to use your
original (single source of truth) #StateObject var viewModel that you create in ContentView and observe any change to it.
So use this:
struct TextFieldView: View {
#ObservedObject var viewModel: WeatherViewModel
...
}
As the title might not very clear, below is an example:
I have a View which is just DatePicker, name is "MIMIRxDatePicker"
struct MIMIRxDatePicker: View {
#Binding var dobStr: String
#Binding var screenShouldGrayOut: Bool
var body: some View {
VStack {
Text("Select Date of Birth: \(dateFormatter.string(from: self.dobStr))")
DatePicker(selection: self.dobStr , in: ...Date(), displayedComponents: .date) {
Text("")
}
Button(action: {
withAnimation {
self.screenShouldGrayOut.toggle()
}
}) {
Text("Choose").foregroundColor(.white).padding()
}.background(Color(Constants.ThemeColor)).cornerRadius(20)
}.cornerRadius(10).frame(width: 270).padding(20).background(Color(.white))
}
}
Heres is MIMIRxDatePicker s parent view: SignupContentView
struct SignupContentView: View {
#State var email: String = ""
#State var firstName: String = ""
#State var lastName: String = ""
#State var dobStr: String = ""
#State var screenShouldGrayOut: Bool = false
var body: some View {
ZStack {
Color(Constants.ThemeColor)
ScrollView(.vertical) {
Spacer().frame(height: 50)
VStack (alignment: .center, spacing: 2) {
Spacer()
Group {
Group {
HStack {
Text("Email:").modifier(AuthTextLabelModifier())
Spacer()
}.padding(2)
HStack {
Image(systemName: "envelope").foregroundColor(.white)
TextField("Email", text: $email).foregroundColor(.white)
}.modifier(AuthTextFieldContainerModifier())
}
Group {
HStack {
Text("First Name:").modifier(AuthTextLabelModifier())
Spacer()
}.padding(2)
HStack {
Image(systemName: "person").foregroundColor(.white)
TextField("First Name", text: $firstName).foregroundColor(.white)
}.modifier(AuthTextFieldContainerModifier())
}
Group {
HStack {
Text("Last Name:").modifier(AuthTextLabelModifier())
Spacer()
}.padding(2)
HStack {
Image(systemName: "person").foregroundColor(.white)
TextField("Last Name", text: $lastName).foregroundColor(.white)
}.modifier(AuthTextFieldContainerModifier())
Spacer()
HStack { GenderSelector() }
}
Group {
HStack {
Text("Date of Birth:").modifier(AuthTextLabelModifier())
Spacer()
}.padding(2)
HStack {
Image(systemName: "calendar").foregroundColor(.white)
TextField("", text: $dobStr).foregroundColor(.white).onTapGesture {
withAnimation {
self.screenShouldGrayOut.toggle()
}
}
}.modifier(AuthTextFieldContainerModifier())
}
}
}.padding()
}.background(screenShouldGrayOut ? Color(.black) : Color(.clear)).navigationBarTitle("Sign Up", displayMode: .inline)
if screenShouldGrayOut {
MIMIRxDatePicker(dobStr: $dobStr, screenShouldGrayOut: $screenShouldGrayOut).animation(.spring())
}
}.edgesIgnoringSafeArea(.all)
}
}
As you can see the #State var dobStr in "SignupContentView" is a String which is Date of birth's TextField needed, but in the MIMIRxDatePicker the DatePicker's selection param needs a type of Date, not a string, even if I declared a #Binding var dobStr: String in MIMIRxDatePicker.
I also tried:
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
and do:
DatePicker(selection: dateFormatter.date(from: self.dobStr) , in: ...Date()....
But it didn't work.
I know that assume if the DatePicker is a TextField then everything will work and good to go because TextField accept String only also, but that's not the case, I need the DatePicker.
So in SwiftUI how can I use #State and #Binding to deal with different types?
What you can do is to use Date for date manipulation and String for displaying the date.
Which means you can use Date variable in your Picker and String in the Text views.
struct MIMIRxDatePicker: View {
#State var dob: Date
...
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
var dobStr: String {
dateFormatter.string(from: self.dob)
}
var body: some View {
VStack {
// `String` for displaying...
Text("Select Date of Birth: \(self.dobStr)")
// and `Date` for date manipulation...
DatePicker(selection: self.$dob, in: ...Date(), displayedComponents: .date) {
Text("")
}
...
}
...
}
}
Then follow the same pattern in your SignupContentView. Generally try to use Date objects for date manipulation as they are less prone to errors and malformed data.
You need to first identify the types involved.
You are passing a Binding<String> to selection: which expects a Binding<Date>.
#Binding var dobStr: String is still Binding<String>
dateFormatter.date(from: self.dobStr) is Date, not Binding<Date>
What you need is to create a custom Binding<Date>, with its getter/setter interacting with Binding<String>
E.g.;
Binding(
get: { // return a Date from dobStr binding },
set: { // set dobStr binding from a Date}
)
Then use this Binding as argument to selection:
I used a DatePicker inside a Form, and It looks like the following image ,
Now,I hope it display "2020-4-19 " instead of "4/19/20 ".
Someone knows how to do it?
I could not find a way with the default DatePicker, so I've taken the code from
Change selected date format from DatePicker SwiftUI
made some changes to make it work. This CustomDatePicker should do you what you asked for even within a Form.
struct CustomDatePicker: View {
#State var text: String = "Date"
#Binding var date: Date
#State var formatString: String = "yyyy-MM-dd"
#State private var disble: Bool = false
#State private var showPicker: Bool = false
#State private var selectedDateText: String = "Date"
let formatter = DateFormatter()
private func setDateString() {
formatter.dateFormat = formatString
self.selectedDateText = formatter.string(from: self.date)
}
var body: some View {
VStack {
HStack {
Text(text).frame(alignment: .leading)
Spacer()
Text(self.selectedDateText)
.onAppear() {
self.setDateString()
}
.foregroundColor(.blue)
.onTapGesture {
self.showPicker.toggle()
}.multilineTextAlignment(.trailing)
}
if showPicker {
DatePicker("", selection: Binding<Date>(
get: { self.date},
set : {
self.date = $0
self.setDateString()
}), displayedComponents: .date)
.datePickerStyle(WheelDatePickerStyle())
.labelsHidden()
}
}
}
}
struct ContentView: View {
#State var date = Date()
var body: some View {
Form {
Section {
CustomDatePicker(text: "my date", date: $date, formatString: "yyyy-MM-dd")
Text("test")
}
}
}
Here's the situation, I have a Master / Detail view set up. When navigating from the "Events" view to the Events Details view. If a user taps the "Back" button, which I have designed using "Button(action: {self.presentationMode.wrappedValue.dismiss()})..", the view will temporarily change back to the Events list, but then jumps automatically back to the details view that a user was navigating from.
Here's the code on the Events List page
import SwiftUI
import Firebase
struct EventsView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var data: [EventObject] = []
let db = Firestore.firestore()
var body: some View {
ZStack {
VStack {
List {
ForEach((self.data), id: \.self.eventID) { item in
NavigationLink(destination: EventDetail()) {
VStack {
HStack{
Text("\(item.eventDate)")
.font(.footnote)
.foregroundColor(Color("bodyText"))
Spacer()
}
HStack {
Text("\(item.eventTitle)")
.fontWeight(.bold)
.foregroundColor(Color("Charcoal"))
Spacer()
}.padding(.top, 8)
}.padding(.bottom, 16)
} // nav
}
Spacer()
}
.padding(.top, 60)
}
//Floating Navbar
ZStack {
VStack {
GeometryReader { gr in
HStack {
Button(action: {self.presentationMode.wrappedValue.dismiss()}) {
Image(systemName: "chevron.left")
.foregroundColor(Color("Charcoal"))
.padding(.leading, 16)
HStack {
Text("Explore · Disney Events")
.font(.system(size: 15))
.fontWeight(.bold)
.foregroundColor(Color("Charcoal"))
.padding()
Spacer()
}
}.frame(width: gr.size.width * 0.92, height: 48)
.background(Color("navBackground"))
.cornerRadius(8)
.shadow(color: Color("Shadow"), radius: 10, x: 2, y: 7)
}.padding(.leading, 16)
Spacer()
}
}
.padding(.top, 50)
.edgesIgnoringSafeArea(.top)
// Floating Nav Ends
}
}.onAppear(perform: self.queryEvents)
}
func queryEvents() {
self.data.removeAll()
self.db.collectionGroup("events").getDocuments() {(querySnapshot, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
for document in querySnapshot!.documents {
let id = document.documentID
let title = document.get("eventTitle") as! String
let shortDesc = document.get("eventShort") as! String
let description = document.get("eventDescription") as! String
let date = document.get("eventDate") as! Timestamp
let aDate = date.dateValue()
let formatter = DateFormatter()
formatter.dateFormat = "E, MMM d · h:mm a"
let formattedTimeZoneStr = formatter.string(from: aDate)
let address = document.get("eventAddress") as! String
let cost = document.get("eventCost") as! Double
let location = document.get("eventLocation") as! String
let webURL = document.get("eventURL") as! String
self.data.append(EventObject(id: id, title: title, shortDesc: shortDesc, description: description, date: formattedTimeZoneStr, address: address, cost: cost, location: location, webURL: webURL))
}
}
}
}
}
class EventObject: ObservableObject {
#Published var eventID: String
#Published var eventTitle: String
#Published var eventShort: String
#Published var eventDescription: String
#Published var eventDate: String
#Published var eventAddress: String
#Published var eventCost: Double
#Published var eventLocation: String
#Published var eventURL: String
init(id: String, title: String, shortDesc: String, description: String, date: String, address: String, cost: Double, location: String, webURL: String) {
eventID = id
eventTitle = title
eventShort = shortDesc
eventDescription = description
eventDate = date
eventAddress = address
eventCost = cost
eventLocation = location
eventURL = webURL
}
}
Event Details stripped down code below. I tried to take things away to search for the cause. It seems to be isolated to the Firebase call.
import SwiftUI
import Firebase
import MapKit
struct EventDetail: View {
#Environment(\.presentationMode) var presentationMode:
Binding<PresentationMode>
// var eventID: String
// var eventTitle: String
// var eventShort: String
// var eventDescription: String
// var eventDate: String
// var eventAddress: String
// var eventCost: Double
// var eventLocation: String
// var eventURL: String
var body: some View {
ZStack {
VStack {
GeometryReader { gr in
HStack {
Button(action: {self.presentationMode.wrappedValue.dismiss()}) {
Image(systemName: "chevron.left")
.foregroundColor(Color("Charcoal"))
.padding(.leading, 16)
HStack {
Text("Events · Event Details")
.font(.system(size: 15))
.fontWeight(.bold)
.foregroundColor(Color("Charcoal"))
.padding()
Spacer()
}
}.frame(width: gr.size.width * 0.92, height: 48)
.background(Color("navBackground"))
.cornerRadius(8)
.shadow(color: Color("Shadow"), radius: 10, x: 2, y: 7)
}.padding(.leading, 16)
Spacer()
}
}
.padding(.top, 50)
.edgesIgnoringSafeArea(.top)
}
}
}
Here's a video to illustrate what I'm talking about.
Dropbox Video Link
Here is a demo of possible approach based on simplified variant of your views. The idea is to use tag/selection based NavigationLink constructor and pass binding to selection to EventDetail to deactivate selection via binding and thus activate back navigation.
Note: I think that presentationMode was not designed for navigation scenario.
struct EventsView: View {
#State private var selectedItem: Int? = nil
var body: some View {
NavigationView {
List {
ForEach(0..<10, id: \.self) { item in
NavigationLink("Item \(item)", destination: EventDetail(selected: self.$selectedItem), tag: item, selection: self.$selectedItem)
}
}
}
}
}
struct EventDetail: View {
#Binding var selected: Int?
var body: some View {
VStack {
HStack {
Button(action: { self.selected = nil }) {
Image(systemName: "chevron.left")
HStack {
Text("Events · Event Details")
.padding()
Spacer()
}
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
Spacer()
}
}
}