SwiftUI pass overlay content to parent views - swift

MainView and AnotherMainView contain names arrays. They iterate over these arrays and both use the DetailView to show the name with some additional content. MyViewModel provides some functionalities such as adding a name etc. These should be triggered from a DetailOverlayView which uses the selected name for the actions (please see code comments.
My problem is, starting an overlay within the DetailView results in a very small overlay within the DetailView "cell" which is not what I want. This is why I would like to show it on the parent view. Unfortunately, I don't know how to pass the DetailOverlayView to the MainView and AnotherMainView. I tried with bindings or environment but this did not work. I would like to avoid redundancy such as defining overlays in both parent views etc.
Alternatively, maybe there is a solution to call it from the DetailView, but bypassing the detail view frame without giving some hardcoded height and width values.
struct MainView: View {
#StateObject var viewModel: MyViewModel
var names = ["aaa", "ccc", "ddd", "gght"]
var body: some View {
VStack {
ForEach(names, id: \.self, content: { name in
DetailView(name: name).environmentObject(viewModel)
})
}///.overlay(... -> need DetailOverlayView here
}
}
struct AnotherMainView: View {
#StateObject var viewModel: MyViewModel
var names = ["bbb", "hhh"]
var body: some View {
VStack {
ForEach(names, id: \.self, content: { name in
DetailView(name: name).environmentObject(viewModel)
})
}///.overlay(... -> need DetailOverlayView here
}
}
struct DetailView: View {
#EnvironmentObject var viewModel: MyViewModel
#State var name: String
var body: some View {
VStack {
Text("Name of the user: ")
Text(name)
}///.overlay(... -> this would show an DetailOverlayView as a part of this small DetailView and in its bounds which is not what I want
}
struct DetailOverlayView: View {
#Binding var name: String ///this is the same name that the DetailView has, not sure if binding makes sense here
#EnvironmentObject var viewModel: MyViewModel
var body: some View {
VStack {
Button(action: {
viewModel.addName(name: name)
}, label: {
Text("Add name")
})
Button(action: {
viewModel.doSomething1(name: name)
}, label: {
Text("doSomething1")
})
Button(action: {
viewModel.doSomething2(name: name)
}, label: {
Text("doSomething2")
})
}
}
}
}
class MyViewModel: ObservableObject {
func addName(name: String) {
///
}
func doSomething1(name: String) {
///
}
func doSomething2(name: String) {
///
}
}

Related

Share binding between two ViewModels in Swift

I just got started with SwiftUI and I would like to use ViewModels to encapsulate my logic, and separate it from my Views.
Now I just hit my first roadblock and I am not sure how to get passed this.
So my app so far is fairly simple. I have two Views, each with their own ViewModels: Parent and Child.
The Parent ViewModel holds a list of Items, which are fetched from a backend API. I want to pass this to Child and its ViewModel, since it is responsible for adding Items to the list.
Here's the simplified code for this:
struct ParentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
ChildView()
Text("Items: \(viewModel.items.count)")
}
}
}
extension ParentView {
#MainActor class ViewModel: ObservableObject {
#Published var items: [Item] = []
}
}
struct ChildView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.items) { item in
Text(item.name)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
viewModel.AddItem()
} label: {
Label("Add item", systemImage: "plus")
}
}
}
}
}
extension ChildView {
#MainActor class ViewModel: ObservableObject {
#Published var items: [Item] = []
func AddItem() {
items.append(Item(name: "Test"))
}
}
}
How can I make it so that the list of items from the parent view model is passed down to the view model of the child, ensuring that there is only a single list, while also making sure that both views get refreshed when this list changes?
Thanks!
You must only have one source of truth, in your ParentView that you then pass to the child views. Currently you have multiple ViewModel that have no relations to each other.
In ChildView replace #StateObject private var viewModel = ViewModel() with
#ObservedObject var viewModel: ViewModel, and in ParentView,
use ChildView(viewModel: viewModel) to pass the ViewModel to it.
Remove also extension ChildView ... and take #MainActor class ViewModel: ObservableObject out of the extension ParentView.
Have a look at this link, it gives you some good examples of how to use ObservableObject and manage data in your app https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
EDIT-1:
Here is my full test example code to show how the parent view model
is passed down to the child view ...ensuring that there is only a single list, while also making sure that both views get refreshed when this list changes
struct ContentView: View {
var body: some View {
NavigationStack { // <-- here
ParentView()
}
}
}
struct Item: Identifiable {
let id = UUID()
var name:String
}
struct ParentView: View {
#StateObject var viewModel = ViewModel() // <-- here
var body: some View {
VStack {
ChildView(viewModel: viewModel) // <-- here
Text("Items: \(viewModel.items.count)")
}
}
}
// -- here
#MainActor class ViewModel: ObservableObject {
#Published var items: [Item] = [Item(name: "item-1"), Item(name: "item-2")]
}
struct ChildView: View {
#ObservedObject var viewModel: ViewModel // <-- here
var body: some View {
List {
ForEach(viewModel.items) { item in
Text(item.name)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
viewModel.items.append(Item(name: "Test")) // <-- here
} label: {
Label("Add item", systemImage: "plus")
}
}
}
}
}

How to update one view from within another in SwiftUI

I am working on a SwiftUI view where I have it populates other subviews within the main view. My question is how can I call something in the main view from within a sub view?
Here is what the code looks like for my main view:
struct MovieList: View {
#ObservedObject var viewModel = MovieViewModel()
var body: some View {
VStack {
ScrollView(.vertical) {
VStack {
ForEach(self.viewModel.movie) { movie in
MovieView(movie: movie)
}
}
}
}
.navigationBarTitle("Movies")
.onAppear {
self.viewModel.fetchMovies() // Fetch all movies and cause entire view to refresh and populate movies
}
}
}
This main view populates a list of movies by adding multiple MovieView instances.
Here is some example code for the MovieView:
struct MovieView: View {
let movie: Movie
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(“Movie Title: \(movie.title)”)
}.padding([.top, .leading, .bottom])
Button("Do not show this movie") {
// Update user prefers to hide the movie.
// But also somehow from within here call viewModel.fetchMovies() in the other view to refresh the movies list
}
}
}
}
So for example, from within one of the MovieView views, how can I have fetchMovies() from within the main view called so that everything gets updated?
Essentially a list of items is being populated and I would like for any one of these to have the ability to refresh/ perform some action on the entire main view
In terms of updating the Movie itself, as was pointed out in the comments, you probably want to pass a Binding to it. See the changes to the ForEach.
In terms of calling fetchMovies again, you can either pass the entire ObservableObject to the child view or just pass a reference to the function you need (which I've shown below):
struct Movie : Identifiable {
var id = UUID()
var title : String
var isHidden: Bool
}
class MovieViewModel : ObservableObject {
#Published var movies = [Movie]()
func fetchMovies() {
//fetch
}
}
struct MovieList: View {
#ObservedObject var viewModel = MovieViewModel()
var body: some View {
VStack {
ScrollView(.vertical) {
VStack {
ForEach($viewModel.movies) { $movie in
MovieView(movie: $movie, fetchMovies: viewModel.fetchMovies)
}
}
}
}
.navigationBarTitle("Movies")
.onAppear {
self.viewModel.fetchMovies()
}
}
}
struct MovieView: View {
#Binding var movie: Movie
var fetchMovies : () -> Void
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Movie Title: \(movie.title)")
}.padding([.top, .leading, .bottom])
Button("Do not show this movie") {
movie.isHidden = true
fetchMovies()
}
}
}
}

How to update an element of an array in an Observable Object

Sorry if my question is silly, I am a beginner to programming. I have a Navigation Link to a detail view from a List produced from my view model's array. In the detail view, I want to be able to mutate one of the tapped-on element's properties, but I can't seem to figure out how to do this. I don't think I explained that very well, so here is the code.
// model
struct Activity: Identifiable {
var id = UUID()
var name: String
var completeDescription: String
var completions: Int = 0
}
// view model
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
}
// view
struct ActivityView: View {
#StateObject var viewModel = ActivityViewModel()
#State private var showingAddEditActivityView = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.activities, id: \.id) {
activity in
NavigationLink(destination: ActivityDetailView(activity: activity, viewModel: self.viewModel)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
}
}
.navigationBarItems(trailing: Button("Add new"){
self.showingAddEditActivityView.toggle()
})
.navigationTitle(Text("Activity List"))
}
.sheet(isPresented: $showingAddEditActivityView) {
AddEditActivityView(copyViewModel: self.viewModel)
}
}
}
// detail view
struct ActivityDetailView: View {
#State var activity: Activity
#ObservedObject var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
var tempActivity = viewModel.activities.first{ activity in activity.id == self.activity.id
}!
tempActivity.completions += 1
}
}
// Add new activity view (doesn't have anything to do with question)
struct AddEditActivityView: View {
#ObservedObject var copyViewModel : ActivityViewModel
#State private var activityName: String = ""
#State private var description: String = ""
var body: some View {
VStack {
TextField("Enter an activity", text: $activityName)
TextField("Enter an activity description", text: $description)
Button("Save"){
// I want this to be outside of my view
saveActivity()
}
}
}
func saveActivity() {
copyViewModel.activities.append(Activity(name: self.activityName, completeDescription: self.description))
print(copyViewModel.activities)
}
}
In the detail view, I am trying to update the completion count of that specific activity, and have it update my view model. The method I tried above probably doesn't make sense and obviously doesn't work. I've just left it to show what I tried.
Thanks for any assistance or insight.
The problem is here:
struct ActivityDetailView: View {
#State var activity: Activity
...
This needs to be a #Binding in order for changes to be reflected back in the parent view. There's also no need to pass in the entire viewModel in - once you have the #Binding, you can get rid of it.
// detail view
struct ActivityDetailView: View {
#Binding var activity: Activity /// here!
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
}
Text("\(activity.completeDescription)")
}
}
}
But how do you get the Binding? If you're using iOS 15, you can directly loop over $viewModel.activities:
/// here!
ForEach($viewModel.activities, id: \.id) { $activity in
NavigationLink(destination: ActivityDetailView(activity: $activity)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
And for iOS 14 or below, you'll need to loop over indices instead. But it works.
/// from https://stackoverflow.com/a/66944424/14351818
ForEach(Array(zip(viewModel.activities.indices, viewModel.activities)), id: \.1.id) { (index, activity) in
NavigationLink(destination: ActivityDetailView(activity: $viewModel.activities[index])) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
You are changing and increment the value of tempActivity so it will not affect the main array or data source.
You can add one update function inside the view model and call from view.
The view model is responsible for this updation.
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
func updateCompletionCount(for id: UUID) {
if let index = activities.firstIndex(where: {$0.id == id}) {
self.activities[index].completions += 1
}
}
}
struct ActivityDetailView: View {
var activity: Activity
var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
self.viewModel.updateCompletionCount(for: activity.id)
}
}
Not needed #State or #ObservedObject for details view if don't have further action.

How do I instantiate a view with multiple EnvironmentObjects?

I was learning about how EnvironmentObject works for a school project, and I was confused about how to instantiate a view with multiple EnvironmentObjects. For example, the following code:
import SwiftUI
class names: ObservableObject {
#Published var myName = ""
}
struct FirstView: View {
#StateObject var FirstName = names()
var body: some View {
NavigationView {
VStack {
TextField("Type", text: $FirstName.myName)
NavigationLink(destination: SecondView()) {
Text("Second View")
}
}
}.environmentObject(FirstName)
}
}
struct SecondView: View {
#StateObject var LastName = names()
var body: some View {
VStack {
TextField("Type", text: $LastName.myName)
NavigationLink(destination: ThirdView().environmentObject(FirstName).environmentObject(LastName)) {
Text("Third View")
}
}.environmentObject(LastName)
}
}
struct ThirdView: View {
#EnvironmentObject var FirstName: names
#EnvironmentObject var LastName: names
var body: some View {
Text("Full name: \(FirstName.myName) \(LastName.myName)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
I need ThirdView to receive FirstName from FirstView and LastName from SecondView, but I can't instantiate ThirdView from SecondView with the required Environment Objects; this code above crashes with the error "Cannot find FirstName in scope".
Alternatively, If I try to instantiate ThirdView with only LastName as an environment object, the code will present something like "Smith Smith" if I entered "John" in the text field on FirstView and "Smith" in the text field on SecondView.
Can someone tell me what I'm doing wrong? Thank you! :)
Since they are of the same type you can’t have two. SwiftUI can’t tell the difference
//Names for classes and structs should start with an uppercase letter
class PersonModel: ObservableObject {
#Published var firstName = ""
#Published var lastName = ""
}
struct FirstNameView: View {
//variables start with lowercase
#StateObject var person: PersonModel = PersonModel()
var body: some View {
NavigationView {
VStack {
TextField("Type", text: $person.firstName)
NavigationLink(destination: LastNameView()) {
Text("Second View")
}
}
}.environmentObject(person)
}
}
struct LastNameView: View {
#EnvironmentObject var person: PersonModel
var body: some View {
VStack {
TextField("Type", text: $person.lastName)
NavigationLink(destination: FullNameView()) {
Text("Third View")
}
}
}
}
struct FullNameView: View {
#EnvironmentObject var person: PersonModel
var body: some View {
Text("Full name: \(person.firstName) \(person.lastName)")
}
}
environmentObject(_:) modifier method takes an ObservableObject and passes it down the view tree. It works without specifying an environment key because the type of the object is automatically used as the key.
So to resume your last name instance is somehow invalidating your first name instance.
I'd then suggest either to create a model that contains both first and last name or simply use #Environment with a key (as it's suggested by Damiaan Dufaux) if it’s possible to get away with passing a value type, because it’s the safer mechanism.
You are probably looking for EnvironmentKeys.
Use them like this:
private struct FirstNameKey: EnvironmentKey {
static let defaultValue = "No first name"
}
private struct LastNameKey: EnvironmentKey {
static let defaultValue = "No last name"
}
And add them to your EnvironmentValues:
extension EnvironmentValues {
var firstName: String {
get { self[FirstNameKey.self] }
set { self[FirstNameKey.self] = newValue }
}
var lastName: String {
get { self[LastNameKey.self] }
set { self[LastNameKey.self] = newValue }
}
}
They values can then be bound to the environment like this:
var body: some View {
MyCustomView()
.environment(\.firstName, "John")
.environment(\.lastName, "Doe")
}
And retrieved like this:
struct ThirdView: View {
#Environment(\.firstName) var firstName
#Environment(\.lastName) var lastName
var body: some View {
Text("Full name: \(firstName) \(lastName)")
}
}
Side note on conventions
To understand code more easily the Swift.org community asks to
Give types UpperCamelCase names (such as SomeStructure and SomeClass here) to match the capitalization of standard Swift types (such as String, Int, and Bool). Give properties and methods lowerCamelCase names (such as frameRate and incrementCount) to differentiate them from type names.
So it would be better to write your class names as class Names as it greatly improves readability for Swift users.

Why doesn't calling method of child view from parent view update the child view?

I'm trying to call a method of a child view which includes clearing some of its fields. When the method is called from a parent view, nothing happens. However, calling the method from the child view will clear its field. Here is some example code:
struct ChildView: View {
#State var response = ""
var body: some View {
TextField("", text: $response)
}
func clear() {
self.response = ""
}
}
struct ParentView: View {
private var child = ChildView()
var body: some View {
HStack {
self.child
Button(action: {
self.child.clear()
}) {
Text("Clear")
}
}
}
}
Can someone tell me why this happens and how to fix it/work around it? I can't directly access the child view's response because there are too many fields in my actual code and that would clutter it up too much.
SwiftUI view is not a reference-type, you cannot create it once, store in var, and then access it - SwiftUI view is a struct, value type, so storing it like did you work with copies it values, ie
struct ParentView: View {
private var child = ChildView() // << original value
var body: some View {
HStack {
self.child // created copy 1
Button(action: {
self.child.clear() // created copy 2
}) {
Here is a correct SwiftUI approach to construct parent/child view - everything about child view should be inside child view or injected in it via init arguments:
struct ChildView: View {
#State private var response = ""
var body: some View {
HStack {
TextField("", text: $response)
Button(action: {
self.clear()
}) {
Text("Clear")
}
}
}
func clear() {
self.response = ""
}
}
struct ParentView: View {
var body: some View {
ChildView()
}
}
Try using #Binding instead of #State. Bindings are a way of communicating state changes down to children.
Think of it this way: #State variables are used for View specific state. They are usually made private for this reason. If you need to communicate anything down, then #Binding is the way to do it.
struct ChildView: View {
#Binding var response: String
var body: some View {
TextField("", text: $response)
}
}
struct ParentView: View {
#State private var response = ""
var body: some View {
HStack {
ChildView(response: $response)
Button(action: {
self.clear()
}) {
Text("Clear")
}
}
}
private func clear() {
self.response = ""
}
}