Sharing Data between Views in Swift/better approach for this? - ios

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 😄

Related

Swiftui form re-using at multiple place not working

I have created model and using that model Im modify variable data at multiple places I can modify and enter data succesfully in FirstView. I could able to modify data in the SecondView. In SecondView, Whatever content I type in the textfield it goes away instanly (in short not allowing to enter data and ofc no error shown)
I want to know am i using proper object variable to call model every time
class MainViewModel: ObservableObject {
#Published var name = ""
#Published var age = ""
}
// Using at one place
struct FirstView : View {
#StateObject var mainViewModel = MainViewModel()
var body: some View {
Form {
TextField("", text: self.$MainViewModel.name)
TextField("", text: self.$MainViewModel.age)
}
}
}
// ReUsing same at another place
struct SecondView : View {
#EnvironmentObject var mainViewModel = MainViewModel()
var body: some View {
Form {
TextField("", text: self.$MainViewModel.name)
TextField("", text: self.$MainViewModel.age)
}
}
}
I have tried using #EnvironmentObject using at both view but doesnt work either here
Change
#EnvironmentObject var mainViewModel = MainViewModel()
To
#EnvironmentObject var mainViewModel : MainViewModel
Make sure you are injecting in the parent view
.environmentObject(mainViewModel)
#lorem ipsum explain the question perfectly. I am just converting his comments into working code. Please have look. This will make you more clear about your issue about injecting from parent.
import SwiftUI
#main
struct StackOverflowApp: App {
#State private var searchText = ""
var body: some Scene {
WindowGroup {
NavigationView {
FirstView()
.environmentObject(MainViewModel())
}
}
}
}
import SwiftUI
class MainViewModel: ObservableObject {
#Published var name = ""
#Published var age = ""
}
// Using at one place
struct FirstView : View {
#EnvironmentObject var mainViewModel : MainViewModel
var body: some View {
VStack {
Form {
TextField("", text: $mainViewModel.name)
TextField("", text: $mainViewModel.age)
}
NavigationLink {
SecondView()
.environmentObject(mainViewModel)
// Either you can inject new or same object from child to parent. #lorem ipsum
// .environmentObject(MainViewModel())
} label: {
Text("Second View")
}
}
}
}
// ReUsing same at another place
struct SecondView : View {
#EnvironmentObject var mainViewModel : MainViewModel
var body: some View {
Form {
TextField("", text: $mainViewModel.name)
TextField("", text: $mainViewModel.age)
}
}
}

MVVM Passing data from view to another view's viewModel

I'm new to MVVM and i am trying to pass a location data from my ContenView to DetailsView's viewModel which is DetailsViewViewModel.
My Opening View -> ContentView (My data is here)
Second View -> DetailsView
Data must be reach -> DetailsViewViewModel
Here is my sheet in ContentView
.sheet(item: $viewModel.selectedPlace) { place in
DetailsView(location: place) { newLocation in
viewModel.updateLocation(location: newLocation)
}
I know i'm trying to send my data to details view and it's wrong. It was like that before i convert the architecture to the MVVM and this is the only place that i couldn't convert.
Also here is my DetailsViewViewModel
extension DetailsView {
#MainActor class DetailsViewViewModel: ObservableObject {
enum LoadingState {
case loading, loaded, failed
}
var location: Location
#Published var name: String
#Published var description: String
#Published var loadingState = LoadingState.loading
#Published var pages = [Page]()
init() {
self.location = // ??? how should i initialize?
self.name = location.name
self.description = location.description
}
What is the proper way to this. Using another views data in another views viewmodel.
Let me try to put in an example that uses the convenience of #EnvironmentObject:
Your view model is a class that conforms to ObservableObject, so you can have those nice variables #Published that change the state of the views.
Your main view - or also your App - must "own" the view model, meaning it needs to create the one and only instance of your view model that will be used by all views.
You pass the view model from one view to another using #StateObject and #ObservableObject, but in this example I prefer to use another approach. Make your main view inject the instance of your view model in the environment, so all other views will read from that. The main view uses .environmentObject(viewModel) to do that.
The other views read the view model from the environment by calling #EnvironmentObject. They create a variable specifying only the type - there can only be one instance per type in the environment.
This is the way with which all view will read from the same model. See below a functioning example:
Step 1:
class MyViewModel: ObservableObject {
#Published private(set) var color: Color = .blue
#Published var showSheet = false
func changeColorTo(_ color: Color) {
self.color = color
}
}
Steps 2 and 3:
struct Example: View {
#StateObject private var viewModel = MyViewModel() // Here is the step (2)
var body: some View {
OneView()
.environmentObject(viewModel) // Here is the step (3)
}
}
Step 4 in two different views:
struct OneView: View {
#EnvironmentObject var viewModel: MyViewModel // Here is step (4)
var body: some View {
VStack {
Text("Current color")
.padding()
.background(viewModel.color)
Button {
if viewModel.color == .blue {
viewModel.changeColorTo(.yellow)
} else {
viewModel.changeColorTo(.blue)
}
} label: {
Text("Change color")
}
Button {
viewModel.showSheet.toggle()
} label: {
Text("Now, show a sheet")
}
.padding()
}
.sheet(isPresented: $viewModel.showSheet) {
DetailView()
}
}
}
struct DetailView: View {
#EnvironmentObject var viewModel: MyViewModel // Here is step (4)
var body: some View {
VStack {
Text("The sheet is showing")
.padding()
Button {
viewModel.showSheet.toggle()
} label: {
Text("Now, stop showing the sheet")
}
}
}
}
since location data is your business layer data, you need a use-case to provide it to both view models, and to optimize it caching the response is the way to go.
-ViewModel is responsible to hold the latest view states and data
-The domain layer is responsible to handle business logic
-The data layer (networking, cache, persistence, or in-memory) is responsible for providing the most efficient data storage/retrieval solutions
So, if you are okay with these defenitions and think of writing test for these view models you know that it is not right to inject data from another ViewModel because you would not test that view model on making sure it passes the data to the next viewModel and it is not its responsibility, but you write many tests for you data layer to make sure service calls and caching systems are working properly.
#StateObject var viewModel = ViewModel()
struct ParentView: View {
var body: some View {
Button(action: {
}, label: {
Text("btn")
})
.sheet(item: $viewModel.selectedPlace) { place in
DetailView(name: place.name,
location: place.location,
description: place.description)
}
}
}
struct DetailView: View {
var name: String
var location: String
var description: String
var body: some View {
VStack {
Text(name)
Text(location)
Text(description)
}
}
}
You need to initialise DetailsViewModel from ContentView sheet when you are adding the DetailsView like below:
ContentView
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
Text("Hello, world!")
.sheet(item: $vm.selectedPlace,
onDismiss: didDismiss) {newLocation in
//Here Initialise the DetailViewModel with a location
DetailsView(detailsVM: DetailsViewModel(location: newLocation))
}
}
func didDismiss(){
}
}
DetailsView:
struct DetailsView: View {
#StateObject var detailsVM : DetailsViewModel
var body: some View {
Text("This is the DetailesView")
}
}
DetailsViewModel:
class DetailsViewModel:ObservableObject{
#Published var location:Location
init(location:Location){
self.location = location
}
}

Updating a binding value pops back to the parent view in the navigation stack

I am passing a Person binding from the first view to the second view to the third view, when I update the binding value in the third view it pops back to the second view, I understand that SwiftUI updates the views that depend on the state value, but is poping the current view is the expected behavior or I am doing something wrong?
struct Person: Identifiable {
let id = UUID()
var name: String
var numbers = [1, 2]
}
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct SecondView: View {
#Binding var person: Person
var body: some View {
Form {
NavigationLink(destination: ThirdView(person: $person)) {
Text("Update Info")
}
}
}
}
struct ThirdView: View {
#Binding var person: Person
var body: some View {
Form {
Button(action: {
person.numbers.append(3)
}) {
Text("Append a new number")
}
}
}
}
When navigating twice you need to either use isDetailLink(false) or StackNavigationViewStyle, e.g.
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
.isDetailLink(false) // option 1
}
}
.navigationViewStyle(.stack) // option 2
}
}
SwiftUI works by updating the rendered views to match what you have in your state.
In this case, you first have a list that contains an element called Current Name. Using a NavigationLink you select this item.
You update the name and now that previous element no longer exists, it's been replaced by a new element called New Name.
Since Current Name no longer exists, it also cannot be selected any longer, and the view pops back to the list.
To be able to edit the name without popping back, you'll need to make sure that the item on the list is the same, even if the name has changed. You can do this by using an Identifiable struct instead of a String.
struct Person: Identifiable {
let id = UUID().uuidString
var name = "Current Name"
}
struct ParentView: View {
#State private var people = [Person()]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: ChildView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct ChildView: View {
#Binding var person: Person
var body: some View {
Button(action: {
person.name = "New Name"
}) {
Text("Update Name")
}
}
}

Is there a way to create objects in swiftUI view based on a value gathered from a previous view?

I have recently started my journey into iOS development learning swift and swift UI. I keep running into issues when it comes to app architecture. The problem i am trying to solve is this: Let's say I have an app where the user first selects a number and then presses next. The user selected number is supposed to represent the number of text fields that appear on the next view. For example, if the user selects 3 then 3 text fields will appear on the next view but if the user selects 5 then 5 texts fields will appear. Is the solution to just have a view for each case? Or is there some way to dynamically add objects to a view based on the user input. Can anyone explain how they would handle a case like this?
Views can get passed parameters (including in NavigationLink) that can determine what they look like. Here's a simple example with what you described:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(numberOfFields: numberOfFields)) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailView : View {
var numberOfFields : Int
var body: some View {
VStack {
ForEach(0..<numberOfFields) { index in
TextField("", text: .constant("Field \(index + 1)"))
}
}
}
}
Notice how numberOfFields is stored as #State in the parent view and then passed to the child view dynamically.
In general, it would probably be a good idea to visit some SwiftUI tutorials as this type of thing will be covered by most of them. Apple's official tutorials are here: https://developer.apple.com/tutorials/swiftui
Another very popular resource is Hacking With Swift: https://www.hackingwithswift.com/100/swiftui
Update, based on comments:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(textInputs: Array(repeating: "test", count: numberOfFields))) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct Model : Identifiable {
var id = UUID()
var text : String
}
class ViewModel : ObservableObject {
#Published var strings : [Model] = []
}
struct DetailView : View {
var textInputs : [String]
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(Array(viewModel.strings.enumerated()), id: \.1.id) { (index,text) in
TextField("", text: $viewModel.strings[index].text)
}
}.onAppear {
viewModel.strings = textInputs.map { Model(text: $0) }
}
}
}

Setting a shared title within a common Header View amongst Views; per Active View

Goal: To use a common header View containing a shared title Text().
Scenario: I have multiple Views that share a common tab space within the one container tab View that contains a struct Header that is to be shared.
👉 This is a (many : 1) scenario.
Note: I don't want to use a NavigationView because it screws up landscape mode. A simple small header View is fine. I just need to populate the shared Title space amongst the member Views.
I don't want to merely add duplicate headers (having exactly the same layout) for each member View.
Several ideas: I need the header to respond to the 'change of title' event so I can see the new title.
So I believe I could use 1) #Binder(each member View) --> #State (shared Header View) or 2) #Environment.
I don't know how I could fit #1 into this particular scenario.
So I'm playing with #2: Environment Object.
DesignPattern: Main Header View's title set by multiple Views so the Header View is not aware of the multiple Views:
I'm not getting the EnvironmentObject paradigm to work.
Here's the codes...
MainView:
import SwiftUI
// Need to finish this.
class NYTEnvironment {
var title = "Title"
var msg = "Mother had a feeling..."
}
class NYTSettings: ObservableObject {
#Published var environment: NYTEnvironment
init() {
self.environment = NYTEnvironment()
}
}
struct NYTView: View {
var nytSettings = NYTSettings()
#State var selectionDataSegmentIndex = 0
var bindingDataSourceSegment: Binding<Int> {
.init(get: {
selectionDataSegmentIndex
}, set: {
selectionDataSegmentIndex = $0
})
}
var body: some View {
let county = 0; let state = 1; let states = 2
VStack {
NYTHeaderView()
SegmentAndDataPickerVStack(spacing: 10) {
if let segments = Source.NYT.dataSegments {
Picker("NYT Picker", selection: bindingDataSourceSegment) {
ForEach(segments.indices, id: \.self) { (index: Int) in
Text(segments[index])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
if selectionDataSegmentIndex == county {
NYTCountyView()
} else if selectionDataSegmentIndex == state {
NYTStateView()
} else if selectionDataSegmentIndex == states {
NYTStatesView()
}
Spacer()
}.environmentObject(nytSettings)
}
struct TrailingItem: View {
var body: some View {
Button(action: {
print("Info")
}, label: {
Image(systemName: "info.circle")
})
}
}
}
// ====================================================================================
struct NYTHeaderView: View {
#EnvironmentObject var nytSettings: NYTSettings
var body: some View {
ZStack {
Color.yellow
Text(nytSettings.environment.title)
}.frame(height: Header.navigationBarHeight)
}
}
Revision: I've added EnvironmentObject modifiers to the memberViews():
if selectionDataSegmentIndex == county {
NYTCountyView().environmentObject(NYTSettings())
} else if selectionDataSegmentIndex == state {
NYTStateView().environmentObject(NYTSettings())
} else if selectionDataSegmentIndex == states {
NYTStatesView().environmentObject(NYTSettings())
}
...
One of the member Views that's within the Main Container/Tab View (per above):
struct NYTCountyView: View {
#ObservedObject var dataSource = NYTCountyModel()
#EnvironmentObject var nytSettings: NYTSettings
...
...
}.onAppear {
nytSettings.environment.title = "Selected Counties"
if dataSource.revisedCountyElementListAndDuration == nil {
dataSource.getData()
}
}
Spacer()
...
}
Here's the compile-time error:
Modus Operandi: Set the title w/in header per member View upon .onAppear().
Problem: I'm not getting any title; just the default "Title" value.
Question: Am I on the right track? If so, what am I missing?
or... is there an alternative?
The whole problem boils down to a 'Many : 1' paradigm.
I got this revelation via taking a break and going for a walk.
So this is the proverbial 'round peg in a square hole' scenario.
What I needed was a lightly coupled relationship where the origin of the title value isn't required. Hence the use of the Notification paradigm.
The header view's title is the receiver and hence I used the .onReceive modifier:
struct NYTHeaderView: View {
#State private var title: String = ""
var body: some View {
ZStack {
Color.yellow
Text(title).onReceive(NotificationCenter.default.publisher(for: .headerTitle)) {note in
title = note.object as? String ?? "New York Times"
}
}.frame(height: Header.navigationBarHeight)
}
}
This sounds like what SwiftUI preferences was built to solve. The preferences are values collected and reduced from children for some distant ancestor to use. One notable example of this is how NavigationView gets its title - the title is set on the child, not on the NavigationView itself:
NavigationView {
Text("I am a simple view")
.navigationTitle("Title")
}
So, in your case you have some kind of title (simplified to String for brevity) that each child view might want to set. So you'd define a TitlePreferenceKey like so:
struct TitlePreferenceKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
Here, the reduce function is simply applying the last value it sees from descendants, but since you'd only ever have one child view selected it should work.
Then, to use it, you'd have something like this:
struct NYTView: View {
#State var title = ""
#State var selection = 0
var body: some View {
VStack {
Text(title)
Picker("", selection: $selection) {
Text("SegmentA").tag(0)
Text("SegmentB").tag(1)
}
switch selection {
case 0: NYTCountyView()
case 1: NYTStateView()
.preference(key: TitlePreferenceKey.self, value: "State view")
default: EmptyView()
}
}
.onPreferenceChange(TitlePreferenceKey.self) {
self.title = $0
}
}
struct NYTCountyView: View {
#State var selectedCounty = "..."
var body: some View {
VStack {
//...
}
.preference(key: TitlePreferenceKey.self, value: selectedCounty)
}
}
So, a preference can be set by the parent of, as in the example of NYTStateView, or by the child with the value being dynamic, as in the example of NYTCountyView

Resources