Environment Object and Views

To see and run the project scroll down and download the folder or Xcode project to run it.

I created a Xcode project to deepen my understanding of some Swift elements:

  • „Environment Object“
  • „TabView“
  • „NavigationView“ and „NavigationLink“
  • „ForEach“
  • Passing Objects to Views

The App is one view with three tabs. Each tab uses a different technic to display data. This main View with three tabs is called „MainTabView“ and has one „Environment Object“. All three tabs can access the same „Environment Object“

 
				
					MainTabView().environmentObject(ViewModel())
				
			

The model is „Recipe“. It has the attributes „name“ and „preparation“. Both attributes are of the type String. Recipe conforms to the protocol „Identifiable“ so that it can be used in a ForEach Loop. To conform to the protocol, it also has an attribute of id of the type UUID.

 
				
					struct Recipe: Identifiable{
    
    var id = UUID()
    
    var name: String
    var preparation: String
    
}
				
			

The „ViewModel“ is an array of Recipes. When being initialized four Recipes are set for the array. These Recipes are, for simplicity purposes, hard-coded in the init Method. The class conforms to the protocol „ObservableObject“ to be used as Environment Object. The attribute property uses the property wrapper „@Published“ so that the view is being updated when the array changes.

				
					class ViewModel: ObservableObject{
    
    @Published var recipes: [Recipe]
    
    init() {
        
        recipes = [
            Recipe(name: "Pasta", preparation: "Pasta preparation ..."),
            Recipe(name: "Pizza", preparation: "Pizza preparation ..."),
            Recipe(name: "Rice", preparation: "Rice preparation ..."),
            Recipe(name: "Salad", preparation: "Salad preparation ...")
        ]
          
    }
				
			

The main view contains one Tab View with three Navigation Views. Each Navigation View is a tab item.

				
					var body: some View {

        TabView {
            NavigationView{...}.tabItem {...}
            
            NavigationView{...}.tabItem {...}
            
            NavigationView{...}.tabItem {...}
                
        }
    }
				
			

Every Tab contains a List which contains a ForEach loop. The ForEach loop contains a navigation link. Because the array contains four elements, the list has four rows. These four rows which show the name of the recipe are tappable.

				
					List{
	ForEach(...){ ... in
		NavigationLink(...)
	}
}
				
			

The first tab uses the following code to create the view:

				
					ForEach(viewModel.recipes){ recipe in
	NavigationLink(
		destination: ListViewRowObject(recipe: recipe),
		label: {
			Text(recipe.name)
	})
}
				
			

The ForEach loop uses the array itself to loop through the recipes. However, the ForEach loop only creates a copy of the element in the array. Therefore, I cannot pass that copy called recipe as a binding into the view called „ListViewRowObject“. I am only able to pass a copy of this recipe object. This instance that I pass into the view is saved as a normal variable. The View then shows the recipe name and preparation.

 
				
					var recipe: Recipe
    
    var body: some View {
        
        VStack(spacing: 30){
            
            Text(recipe.name)
            
            Text(recipe.preparation)
            
        }
        
    }
				
			

The second tab uses the following code:

				
					ForEach(viewModel.recipes.indices){ recipeIndex in
	let bindingRecipe = $viewModel.recipes[recipeIndex]
                        
	NavigationLink(
		destination: ListViewRowBinding(recipe: bindingRecipe, selection: $selectionForTabBinding),
		tag: recipeIndex + 1,
		selection: $selectionForTabReference,
		label: {
			Text(viewModel.recipes[recipeIndex].name)
		})
}
				
			

This view uses the array as well but calls the .indices method on the array. This method returns „Range<Int>“. Therefore, the variable recipeIndex is the index and not an instance of the recipe object. Then I am creating a constant called „bindingRecipe“. This constant stores a binding of the recipe that is in the array. Then I am able to pass the binding into the view called „ListViewRowBinding“. This view saves this variable as binding and uses it to display the recipe name and preparation. The navigation link initializer is different because I wanted to add the option to go back with a button. For that purpose, I am also passing a binding called „$selectinForTabBinding“. This keeps track of the current selected navigation link and when set to nil returns to the navigation view.

By passing a binding rather than a copy I am able to manipulate the object. To demonstrate this I have added a button that adds an „A“ to the name of the selected recipe. The view looks like this:

				
					@Binding var recipe: Recipe
    
@Binding var selection: Int?
    
	var body: some View {
        
		VStack(spacing: 30){
            
			Text(recipe.name)
            
			Text(recipe.preparation)
            
			Button("Name + A"){
				recipe.name.append("A")
        }
            
      Button("back"){
         selection = nil
         }
            
      }
        
	}
				
			

For the third tab the code looks like this:

				
					ForEach(0..<viewModel.recipes.count){ recipeIndex in
                        
	NavigationLink(
		destination: ListViewRowIndex(recipeIndex: recipeIndex, selection: $selectionForTabIndex),
		tag: recipeIndex + 1,
		selection: $selectionForTabIndex,
		label: {
			Text(viewModel.recipes[recipeIndex].name)
		})
	}
				
			

The ForEach loop uses another method of getting the index. However, the result is still the same. This time however I am passing in only the index of the selected recipe. I am passing the index into the view called „ListViewRowIndex“. This view is a subview of the main view and therefore has access to the environment object. The advantage of this is, that I now also have access to the entire array. Before this way I only had access to the object but not the array itself. By doing this I can implement another functionality into this view. I can add a button which says, „Next recipe“ or „Last recipe“, depending on the array. This was not possible before. By accessing the environment object and checking if the received index is the last one I can display the button. If the index is not the last one of the array I can increase the recipeIndex. By doing this the view is updated and because the tag is set to recipeIndex + 1 it will show the next view.

				
					@EnvironmentObject var viewModel: ViewModel
@State var recipeIndex: Int
    
@Binding var selection: Int?
    
	var body: some View {
        
		VStack(spacing: 30){
            
			Text(viewModel.recipes[recipeIndex].name)
            
			Text(viewModel.recipes[recipeIndex].preparation)
            
			Button("Name + A"){
				viewModel.recipes[recipeIndex].name.append("A")
			}
            
			if recipeIndexIsNotLastRecipe{
				Button("Next recipe"){
					recipeIndex += 1
				}
			}
			else{
			Text("last recipe")
			}
            
			Button("back"){
			selection = nil
			}
		}
	}
				
			

To reduce the logic written in the view I extracted the if statement into a calculated variable that the if statement is using to evaluate if it should show the next button or if this is the last element in the array. This variable is defined as the following:

				
					var recipeIndexIsNotLastRecipe: Bool {
        if recipeIndex == viewModel.recipes.count - 1 {
            return false
        }
        else {
            return true
        }
    }
				
			

This experiment showes the different use cases of when to pass which data. I tried to pass a copy of an object, a binding object and an index. Each possibility has different advantages and disadvantages. One advantage of passing a copy or a binding is, that the use in the subview is a lot easier because the object can be used directly. By passing the index we have to write more code to get to the same property. This however could be changed by using an attribute that is referring to the object with the passed index.

Download project as folder or as Xcode project

Download Xcode Project

 

Mich benachrichtigen, wenn es einen neuen Beitrag gibt.

 
 
Share on whatsapp
WhatsApp
Share on email
Email
Share on linkedin
LinkedIn
Share on facebook
Facebook