I have two views: List of trips and detail view.
There is my coreData entity
There is list if cities:
struct TripView: View {
#EnvironmentObject var viewRouter: ViewRouter
#FetchRequest(entity: Trip.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Trip.startDate, ascending: true)], predicate: nil, animation: .linear) var trips: FetchedResults<Trip>
#State var tappedTrip = 0
var body: some View {
VStack(spacing: 20) {
ForEach(trips, id: \.self) { trip in
TripRow(trip: trip)
.environmentObject(viewRouter)
}
}
}
struct TripRow: View {
#EnvironmentObject var viewRouter: ViewRouter
var trip: Trip
var body: some View {
...
.fullScreenCover(isPresented: $viewRouter.isShowingDetailTripView) {
DetailTripView(trip: trip)
.environmentObject(viewRouter)
}
.onTapGesture {
viewRouter.isShowingDetailTripView.toggle()
}
And detail view:
struct DetailTripView: View {
var trip: Trip
var body: some View {...}
But when I tap on any city in detail view every time I have the first one
For example "Берлингтон"
How I can get the corresponding value of Trip in detail view?
Tried to debug but without any results :c
Each of your rows is monitoring $viewRouter.isShowingDetailTripView for presentation of a fullScreenCover - so when that boolean value toggles, in theory they could all be responding to it. In practice your first row is doing the job.
To correctly represent your state in the view router, it doesn't only need to know whether to show a full screen trip - it also needs to know which full screen trip to display.
The usual way is to use an Optional object in place of the boolean.
class ViewRouter: ObservableObject {
// ...
#Published var detailTrip: Trip?
}
When this optional value is nil, the full screen won't display.
In your row, change your action to set this value:
// in TipRow
.onTapGesture {
viewRouter.detailTrip = trip
}
In terms of the modifier to display the full screen, I wouldn't put that in the row itself, but move it up to the list, so that you only have the one check on that value.
// in TipView
VStack {
ForEach(...) {
}
}
.fullScreenCover(item: $viewRouter.detailTrip) { trip in
DetailTripView(trip: trip)
}
This may cause an issue if you want to persist ViewRouter between sessions for state preservation, as it'll now contain a Trip object that it doesn't own. But this approach should get you much closer to how you want your UI to work.
Related
I am brand new to Swift and SwiftUi, decided to pick it up for fun over the summer to put on my resume. As a college student, my first idea to get me started was a Check calculator to find out what each person on the check owes the person who paid. Right now I have an intro screen and then a new view to a text box to add the names of the people that ordered off the check. I stored the names in an array and wanted to next do a new view that asks for-each person that was added, what was their personal total? I am struggling with sharing data between different structs and such. Any help would be greatly appreciated, maybe there is a better approach without multiple views? Anyways, here is my code (spacing a little off cause of copy and paste):
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Image("RestaurantPhoto1").ignoresSafeArea()
VStack {
Text("TabCalculator")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 150.0)
NavigationLink(
destination: Page2(),
label: {
Text("Get Started!").font(.largeTitle).foregroundColor(Color.white).padding().background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.blue/*#END_MENU_TOKEN#*/)
})
}
}
}
}
}
struct Page2: View {
#State var nameArray = [String]()
#State var name: String = ""
#State var numberOfPeople = 0
#State var personTotal = 0
var body: some View {
NavigationView {
VStack {
TextField("Enter name", text: $name, onCommit: addName).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
List(nameArray, id: \.self) {
Text($0)
}
}
.navigationBarTitle("Group")
}
}
func addName() {
let newName = name.capitalized.trimmingCharacters(in: .whitespacesAndNewlines)
guard newName.count > 0 else {
return
}
nameArray.append(newName)
name = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
You have multiple level for passing data between views in SwiftUI. Each one has its best use cases.
Static init properties
Binding properties
Environment Objects
Static init properties.
You're probably used to that, it's just passing constants through your view init function like this :
struct MyView: View {
var body: some View {
MyView2(title: "Hello, world!")
}
}
struct MyView2: View {
let title: String
var body: some View {
Text(title)
}
}
Binding properties.
These enables you to pass data between a parent view and child. Parent can pass the value to the child on initialization and updates of this value and child view can update the value itself (which receives too).
struct MyView: View {
// State properties stored locally to MyView
#State private var title: String
var body: some View {
// Points the MyView2's "title" binding property to the local title state property using "$" sign in front of the property name.
MyView2(title: $title)
}
}
struct MyView2: View {
#Binding var title: String
var body: some View {
// Textfield presents the same value as it is stored in MyView.
// It also can update the title according to what the user entered with keyboard (which updates the value stored in MyView.
TextField("My title field", text: $title)
}
}
Environment Objects.
Those works in the same idea as Binding properties but the difference is : it passes the value globally through all children views. However, the property is to be an "ObservableObject" which comes from the Apple Combine API. It works like this :
// Your observable object
class MyViewManager: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
struct MyView: View {
// Store your Observable object in the parent View
#StateObject var manager = MyViewManager(title: "")
var body: some View {
MyView2()
// Pass the manager to MyView2 and its children
.environmentObject(manager)
}
}
struct MyView2: View {
// Read and Write access to parent environment object
#EnvironmentObject var manager: MyViewManager
var body: some View {
VStack {
// Read and write to the manager title property
TextField("My title field", text: $manager.title)
MyView3()
// .environmentObject(manager)
// No need to pass the environment object again, it is passed by inheritance.
}
}
}
struct MyView3: View {
#EnvironmentObject var manager: MyViewManager
var body: some View {
TextField("My View 3 title field", text: $manager.title)
}
}
Hope it was helpful. If it is, don't forget to mark this answer as the right one 😉
For others that are reading this to get a better understanding, don't forget to upvote by clicking on the arrow up icon 😄
I have two views. One view is a list of objects (persons) fetched from a core data store using #fetchRequest. The other view is a detail view which the user can navigate to by tapping on a person item in the list view. The idea is that the user can edit the details of a person in the detail view (e.g. name, age). So far so good.
The crucial point is that the list view is designed such that not all persons are necessarily fetched from the core data store. Only persons which fulfil a certain criteria are fetched. Let us assume that the criteria is that the person must be between the age of 30 and 40 years.
My problem is that when the user changes the age of a person in the detail view to some age which does not fulfil the criteria of the fetch request (e.g. he changes the age from 35 to 20 years), the detail view will pop once the user taps the save button and the managed object context saves, which is registered by #fetchRequest in the list view.
I understand that this happens, because the fetchRequest of persons driving the list view changes, as the edited person is removed, because he does not fulfil being between 30 and 40 years anymore. But I don't want this to happen while the user is still in the detail view. Tapping the save button should not automatically pop the detail view. Is there any way to prevent this from happening while using #fetchRequest?
Here is a condensed version of the code resulting in the described issue:
struct PersonList: View {
#FetchRequest var persons: FetchedResults<Person>
init() {
let lowerAge = NSPredicate(format: "%K >= %#", #keyPath(Person.age), 30 as CVarArg)
let upperAge = NSPredicate(format: "%K <= %#", #keyPath(Person.age), 40 as CVarArg)
let agePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [lowerAge, upperAge])
_persons = FetchRequest(entity: Person.entity(), sortDescriptors: [], predicate: agePredicate)
}
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 10) {
ForEach(persons, id: \.id) { person in
NavigationLink(destination: PersonDetail(person: person)) {
Text(person.name)
}
}
}
}
}
}
struct PersonDetail: View {
#Environment(\.managedObjectContext) var viewContext
#State var ageInput: String = ""
var person: Person
var body: some View {
TextField("Enter Age", text: $ageInput)
.onAppear(perform: {
ageInput = "\(person.age)"
})
Button("Save", action: { save() } )
}
}
func save() {
if let age = Int(ageInput) {
viewContext.saveContext()
}
}
}
As you pointed out, the NavigationLink is popped because the underlying model is removed from the fetch request. Unfortunately this means that you'll have to move the navigation link outside your list view so that you can cache the model even if it gets removed from the fetch results list.
One way to do that is to embed your VStack/ScrollView inside a ZStack with a hidden "singleton" navigation link and a #State variable that keep track of the actively selected Person:
struct PersonList: View {
#FetchRequest var persons: FetchedResults<Person>
#State private var selectedPerson = Person(entity: Person.entity(), insertInto: nil)
#State private var showPersonDetail = false
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: PersonDetail(person: selectedPerson), isActive: $showPersonDetail) { EmptyView() }.hidden()
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 10) {
ForEach(persons, id: \.id) { person in
Button(action: {
selectedPerson = person
showPersonDetail = true
}, label: {
// You could hack out the click/tap listener for a navigation link instead of
// emulating its appearance, but that hack could stop working in a future iOS release.
HStack {
Text(person.name)
Spacer()
Image(systemName: "chevron.right")
}
})
}
}
}
}
}
}
Add .navigationViewStyle(.stack) to the outermost NavigationLink.
I guess it is mostlikely a stupid question, but currently I am struggling.
To avoid tons of code, I simplified the code (see below) but it should show the problem.
I’ve got multiple views that are not in a parental relationship.
On one view (ViewA) I set a target date. On another view (ViewB) I show text, depending on the fact whether the target date is in future or not.
For both views I am using a ObservableObject.
I would like to have that ViewB changes the text when it is open and the target date is reached at that time. Since I am using the tag #Published, I was expecting it works directly.
But unfortunately, nothing happens.
This is my initial approach.
I tested some more solutions, e.g.
polling by a timer in the views with a function to get the current date
I also thought about first calculating the remaining time and a timer in another thread within the ObservedObject that fires an event when the timer reaches 0 and onReceive modifiers in the views.
But I guess my approach(es) is (are) very bad I there will be a way better solution.
Your help is really appreciated.
Thanks,
Sebastian
SearchData:
class SearchData: ObservableObject {
#Published var targetDate: Date = UserDefaults.standard.object(forKey: "targetDate") as? Date ?? Date() {
didSet {
UserDefaults.standard.set(self.targetDate, forKey: "targetDate")
}
}
}
View A:
struct ViewA: View {
#ObservedObject var searchData = SearchData()
func setDate() {
searchData.targetDate = Date() + 120
}
var body: some View {
Button(action: {
self.setDate()
}) {
Text("Set Date")
}
}
}
View B:
struct ViewB: View {
#ObservedObject var searchData = SearchData()
var body: some View {
VStack() {
if searchData.targetDate > Date() {
Text("Text A")
} else {
Text("Text B")
}
Spacer()
Text("\(searchData.targetDate)")
}
.padding()
}
}
The ViewA and ViewB use different instances of SearchData. To make published work directly, as you wrote, both views have to use one instance of observable object.
struct ViewA: View {
#ObservedObject var searchData: SearchData
/// ... other code
struct ViewB: View {
#ObservedObject var searchData: SearchData
/// ... other code
and somewhere you crate them
let searchData = SearchData()
...
ViewA(searchData: searchData)
...
ViewB(searchData: searchData)
If ViewA and ViewB live in different view hierarchies then probably would be more appropriate to use #EnvironmentObject
struct ViewA: View {
#EnvironmentObject var searchData: SearchData
/// ... other code
struct ViewB: View {
#EnvironmentObject var searchData: SearchData
/// ... other code
I am using a simple coredata model and I have an Entity that is called Movements and it has 4 fields, date, category, description, amount.
This the view:
struct TransactionsView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Movements.entity(), sortDescriptors: []) var transactions:FetchedResults<Movements>
#State private var showAddModal = false;
var body: some View {
VStack {
List {
ForEach(transactions, id: \.id) { t in
Text(t.desc ?? "No Cat")
}
}
}
}
}
struct TransactionsView_Previews: PreviewProvider {
static var previews: some View {
return TransactionsView()
}
}
I used the same code for another model, categories, and it worked. For some reason here the previ keeps crashing with a generic error, I have no idea that says basically that my app has crashed.
I thought that maybe Transaction could be a reserved word and renamed to Movements but still the same error.
Is there also a way to debug the #FetchRequest to see the data returned?
I have a view in a loop using ForEeach to show a list in an array using Core Data relationships.
The View is showing all of the elements in each loop and I need to segment them out individually while being able to keep toggling the button and it not toggle all of the goals at the same time.
Edit: I figured out how to make it so they all weren't iterating and showing but now toggling any of the buttons toggles them all. :(
GameGoalDetail
import SwiftUI
import CoreData
struct GameGoalsDetail: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#State private var showingAddGoal = false
#State private var goalComplete : Bool = false
#ObservedObject var game: Game
var body: some View {
VStack {
Text(self.game.gameName ?? "No Game Name").font(.title)
Text(self.game.gameDescription ?? "No Game Description").font(.subheadline)
List {GameGoalListView(game: self.game).environment(\.managedObjectContext, self.moc)
}
Button("Add Game Goal") {
self.showingAddGoal.toggle()
}
.sheet(isPresented: $showingAddGoal) {
AddGameGoalsView(game: self.game).environment(\.managedObjectContext, self.moc)
}
}
}
}
GameGoalsListView
import SwiftUI
import CoreData
struct GameGoalListView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#ObservedObject var game: Game
#State private var goalComplete : Bool = false
var body: some View {
VStack {
ForEach(game.goalArray, id: \.self) { goal in
HStack {
Text(goal.goalName ?? "No Goal Name")
Spacer()
Text("Complete:").font(.caption)
Image(systemName: self.goalComplete ? "checkmark.square.fill" : "app").onTapGesture {
self.goalComplete.toggle()
print(self.goalComplete)
}
}
}
}
}
}
Here's a screenshot of what is happening:
After clicking:
I'm trying to have it so that when a goal is added, it shows in the List and then I can select if the goal is completed or not via the button.
So each Game would have a list of different goals that are added by user input.
Would really appreciate help & can provide further info as requested.
I had to rework so much of this but with the help from a fantastic Redditor, I got it figured out.
GameGoalsDetail
I didn't need the Fetch Request because it was being passed in from the view.
Didn't need the #State because I should've been using the CoreData info directly.
List {
ForEach(game.goalArray, id: \.self) { goal in
GameGoalListView(goal: goal)
}
}
Only needed the GameGoalListView(goal: goal) passed in because I redid the GameGoalListView.
GameGoalListView
Got rid of the #State again and the #FetchRequest
The List in GameGoalsDetail is what is showing each goal, so an additional ForEach isn't needed in this view. This view just needed to show the thing that needed to be shown and the information pulled from CoreData.
The #ObservedObject is the Goal not the Game.
The onTapGesture is accessing the goal.goalComplete from CoreData and not the State (which creates a new Source of Truth (watch the WDCC video)).
Then the onReceive and objectWillChange let the goal knows it's changing every time the button is tapped.
struct GameGoalListView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var goal: Goal
var body: some View {
VStack {
HStack {
Text(goal.goalName ?? "No Goal Name")
Spacer()
Text("Complete:").font(.caption)
Image(systemName: self.goal.goalComplete ? "checkmark.square.fill" : "app").onTapGesture {
self.goal.goalComplete.toggle()
print(self.goal.goalComplete)
}
}
}
.onReceive(self.goal.objectWillChange) {
try? self.moc.save()
}
}
}
As I said, the help came from a Redditor and I hope this helps someone figure out what I was messing up and fix their error.