How do I pass data from one SwiftUI view file to another? - ios

The basic gist is I'm building a meal planner app. I'm new to Swift and SwiftUI and this is my first project so I'm probably making a noob mistake lol Basically I want to pass the current index, which is a data object for a day and it's meals, to the DailyMealPlan view when I tap on the corresponding card. I don't get an error when I pass the currentDay variable to the view, but inside the second view file I'm not sure how I handle that data. I keep getting an error that "CurrentMealPlan is not in scope". I understand how scope works and I suspect I'm just missing something when it comes to passing the data into the second view.
ForEach code
ForEach(CurrentMealPlan.indices) { index in
let currentDay = CurrentMealPlan[index]
NavigationLink(
destination: DailyMealPlan(DailyMealPlan: currentDay))
{
MealPlanCard(
Day: "\(CurrentMealPlan[index].DayTitle)",
Breakfast: "\(CurrentMealPlan[index].Breakfast)",
Lunch: "\(CurrentMealPlan[index].Lunch)",
Dinner: "\(CurrentMealPlan[index].Dinner)"
)
}
}
DailyMealPlan view
struct DailyMealPlan: View {
var DailyMealPlan: Day = CurrentMealPlan[index]
var body: some View {
ZStack {
ScrollView {
VStack {
SingleMealCard(Meal: "Breakfast", CalCount: 500, MealInfo: "Meal info here")
SingleMealCard(Meal: "Lunch", CalCount: 500, MealInfo: "Meal info here")
SingleMealCard(Meal: "Dinner", CalCount: 500, MealInfo: "Meal info here")
}
}
}
}
}
CurrentMealPlan model
struct Day: Hashable {
var id: Int
var Date: String
var DayTitle: String
var Breakfast: String
var Lunch: String
var Dinner: String
init(id:Int=0,Date:String="",DayTitle:String="",Breakfast:String="",Lunch:String="",Dinner:String="") {
self.id = id
self.Date = Date
self.DayTitle = DayTitle
self.Breakfast = Breakfast
self.Lunch = Lunch
self.Dinner = Dinner
}
}
let CurrentMealPlan: [Day] = [
Day(
id: 0,
DayTitle: "Sunday",
Breakfast:"Oatmeal",
Lunch: "Sandwich",
Dinner: "Cheeseburger with Fries"
)
]

Let's walk through your code. First, you declared the CurrentMealPlan array inside the parent view. It probably looks something like this:
#State var CurrentMealPlan = [Day]()
Note: As jnpdx commented, you should lowercase property names like var currentMealPlan and var dailyMealPlan. Also, you should not have the same name for the DailyMealPlan view and the DailyMealPlan property... it's extremely confusing.
Your code is correct. Then, you loop over CurrentMealPlan, and want to pass each element over to the DailyMealPlan view:
DailyMealPlan(DailyMealPlan: currentDay))
That's also completely fine. So where does the CurrentMealPlan is not in scope error come from? It's this line:
var DailyMealPlan: Day = CurrentMealPlan[index] /// NO!
Remember, you declared CurrentMealPlan inside the parent view, not the DailyMealPlan view. That means the DailyMealPlan view can't access CurrentMealPlan.
However, the DailyMealPlan view does not need to access CurrentMealPlan. Back in the parent view, you're already looping through CurrentMealPlan and passing in each currentDay over to the DailyMealPlan view.
So, all you need to do is define the type of the DailyMealPlan property:
struct DailyMealPlan: View {
var DailyMealPlan: Day /// here!
...
}
This lets the compiler know that var DailyMealPlan takes in a Day.

I suggest using a MVVM approach, each screen view should have it's own view model (which should be a class extending a base view model or protocol) which you pass when you create the view.
In your case you should have:
protocol BaseViewModel {
}
class DailyMealPlanViewModel: BaseViewModel {
#Published var day: Day
}
in the above I also added day as #Published so any changes on that will refresh the view, if the day never changes you can remove #Published.
in your view:
struct DailyMealPlan: View {
#StateObject var viewModel: DailyMealPlanViewModel
...
...
And when navigating:
NavigationLink(
destination: DailyMealPlan(viewModel: DailyMealPlanViewModel(day: currentDay))
...
...

Related

Using a Picker with CoreData - Saving and fetching data

I am trying to save a string selection from a picker into CoreData, but it is stating
"Picker: the selection "" is invalid and does not have an associated tag, this will give undefined results." I can see all the options, but it won't let me click any of them and its throwing that error down below in the console.
I have the following:
'feeling' is defined in my CoreData Entity as a String & defined in my SleepModel type as a String
PickerValues.swift
class PickerValues {
enum Feeling: String, CaseIterable, Codable {
case great = "Great"
case ok = "OK"
case bad = "Bad"
}
}
In the AddEntryView.swift:
#State var feelingSelect = String()
Picker ("Feeling", selection: $feelingSelect, content: {
ForEach(PickerValues.Feeling.allCases, id: \.self){ feeling in
Text(feeling.rawValue).tag(feeling)
}
}).foregroundColor(.black)
//inside the function called when clicking the add entry button
var sleepModel = SleepModel(feeling: feelingSelect)

SwiftUI ForEach with array or sub-array

I want to iterate an array using ForEach and, depending on the use-case, its sub-version with fewer elements.
ForEach(isExpanded ? $items : $items[..<4])
The problem is if I am using a subscript, I am getting an error Result values in '? :' expression have mismatching types 'Binding<[RowItem]>' and 'Binding<[RowItem]>.SubSequence' (aka 'Slice<Binding<Array<RowItem>>>'). I can not use it like this, because it is not an array but a Slice. Just casting to Array does not work either.
This is a sample code which responds to the poster's second question.
Use binding to store/pass/receive data from another struct:
struct Test: View {
#State var bindWithChild: String = ""
var body: some View {
Text("\(bindWithChild)")
//when you edit data in the child view, it will updates back to the parent's variable
TestChild(receiveAndUpdate: $bindWithChild)
}
}
struct TestChild: View {
#Binding var receiveAndUpdate: String
var body: some View {
TextField("Enter here", text: $receiveAndUpdate)
}
}
However, If your data only works within one struct, and you don't need to pass the data back and forth within another struct, use #State:
struct Test: View {
#State var binding: String = ""
var body: some View {
Text("\(binding)")
//when you edit data in the TextField below, it will update this Text too.
TextField("Enter data: ", text: $binding)
}
}
You don’t have to use $ sign in front of your arrays.
ForEach(isExpanded ? items[..<items.count] : items[..<4], id: \.self) { sub in
}

swiftUI using picker to show different values in the Text field than in the selector

I've got an array of strings that I want to present using swiftUI Picker widget. Each string is composed of multiple words delimited by spaces.
I'd like to get the Picker showing the full string of each item in the list, while the selection variable should only get the first word (the selected item is stored in arg)
This was my attempt to do so. notice that the object that hold the items called myHandler, and it's shared to the swiftUI view, and can be modified by external swift closure:
class myHandler: ObservableObject {
#Published var items = [String]()
}
struct ContentView: View {
#State var arg: String = ""
#ObservedObject var handler : myHandler
...
VStack {
Picker("items", selection: $arg) {
Text("AAA").tag("xxx")
Text("BBB").tag("yyy")
Text("CCC").tag("zzz")
ForEach(handler.items , id: \.self, content: {
Text($0).tag($0.components(separatedBy: " ")[0])
})
}
}
.frame()
TextField("firstword", text: $arg).frame()
For the options outside the ForEach statement, I can see that arg get the value written in the tag. However, for all options that derived from the ForEach, I see that arg equals to the iterable item ($0) which is the multi work string, and not to the first word as expected.
Any idea how to fix those items that are generated from the ForEach, so that selection of such item, will set the arg to the string value of the first word in the iterator ?
Picker("items", selection: $arg) {
ForEach(handler.items, id: \.self) {
Text($0)
.tag($0.components(separatedBy: " ").first ?? "")
}
}

List is not showing data from API in SwiftUI?

I am trying to get data from API and want to show that in List.
List {
ForEach(0 ..< (declarationViewModel.items?.count)!) { index in
HStack {
......
but as its taking some time to fetch data from API, initially data is 0 hence can not able to show data in list as it's not reloading after getting data.
public class DeclarationViewModel: ObservableObject {
#Published var items: [Item]?
#Published var title: String?
init() {
self.items = [Item]()
self.title = ""
}
init(items: [Item]?, title: String) {
self.items = items
self.title = title
}
}
Error log
forEach<Range<Int>, Int, HStack<TupleView<(HStack<TupleView<(ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>, Text, Spacer)>>, Spacer, _ConditionalContent<ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>, ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>>, _ConditionalContent<ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>, ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>>)>>> count (14) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
you could try something simple like this:
EDIT: and if your items are Identifiable, then remove the "id:.\self",
nothing to it.
List {
if let items = declarationViewModel.items {
ForEach(items, id: \.self) { item in
....
}
}
}
Check out your console, you'll see following warning:
ForEach<Range, Int, ModifiedContent<Text, AddGestureModifier<_EndedGesture>>> count (2) != its initial count (1). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Newer use 0..<shoppingList.count inside ForEach or List. At least use declarationViewModel.items.indices instead.
Make items non optional by specifying a default value of an empty array instead of initializing it in the init. You can do same with title:
#Published var items: [Item] = []
#Published var title = ""
But perfectly instead of using indices you need to confirm your Item to Identifiable and make sure each item has a unique id, so your animations will work good and SwiftUI reuse cells to increase performance. You can use UUID:
struct Item: Identifiable {
let id = UUID()
}
Then in ForEach you get item instead of index:
ForEach(declarationViewModel.items) { item in
If you also need index in ForEach, check out my ForEachIndexed in this answer

SwiftUI List not recognizing object type

So I'm trying to make a List in SwiftUI like this:
struct DetailsView: View {
var piis = [IDPiece]()
var body: some View {
List(piis, id: \.identifier) { pii in
Text( pii.label )
}
}
}
where IDPiece looks like:
struct IDPiece: Equatable {
init() {}
init(claim: Claim) {
self.document = claim.document
self.identifier = claim.identifier
self.claimUID = claim.claimUID
self.label = claim.label
}
var document: DocumentType = .na
var identifier: String = ""
var claimUID: String = ""
var label: String?
}
But I keep receiving the following error on the line where I initialize the list:
Type '_' has no member 'identifier'
It doesn't seem to be parsing the type of object contains in my piis list. Anyone know why that may be?
SwiftUI compiler errors are generally useless (this will improve over time, but today they're useless). Your problem has nothing to do with \.identifier. The problem is you have an optional .label, but you don't handle the case where it's nil. Almost certainly you should just make label non-optional. But if it needs to be optional (if you treat nil differently than empty in some place), then you need to do something about that, such as:
Text(pii.label ?? "N/A")

Resources