KMPizza
Kotlin Multiplatform + Pizza = ❤️

Step 22: Bind iOS Swift UI to shared RecipeDetail ViewModel and add Navigation

Now let’s add a new Swift UI Recipe Details screen and Navigation to it.
Create recipeDetail folder and add RecipeDetailState.swift and RecipeDetailView.swift files:

create recipe detail state and view files

Add Navigation to ContentView:

 
        NavigationView {
            VStack {
                RecipesView()
            }
        }

Then, add a navigation link to the recipes list in RecipesView.swift like this:

 
List(state.recipes, id: \.id) { recipe in
            NavigationLink(destination: RecipeDetailView (id: KotlinLong.init(value: recipe.id))) { [1]
                    RecipeView(item: recipe)
               
            }
        }

[1] Here we’re transforming Swift Int64? To KotlinLong?, which is a Kotlin Native type of recipeId

To make it neater, let’s write an extension. Create a separate Extensions.swift in a utils group:

add toKotlinLong() extension
 
import shared
 
extension Int64 {
    func toKotlinLong() -> KotlinLong {
        return KotlinLong(value: self)
    }
}

Now you can use this extension instead of KotlinLong.init(value: recipe.id)):

 
NavigationLink(destination: RecipeDetailView (id: recipe.id.toKotlinLong())) {
                    RecipeView(item: recipe)
               
            }

Populate the RecipeDetailState.swift:

 
import SwiftUI
import Foundation
import shared
 
class RecipeDetailState: ObservableObject{
    
    let recipeId: KotlinLong? [1]
    let viewModel: RecipeDetailsViewModel [2]
    
    @Published private(set) var recipe: RecipeUiModel? = nil [3]
    @Published private(set) var upload: Bool? = nil
    
    init(recipeId: KotlinLong?) {
        self.recipeId = recipeId
        viewModel = RecipeDetailsViewModel(id: recipeId)
        
        viewModel.observeRecipe { recipe in
            self.recipe = recipe
        }
    }
    
    deinit {
        viewModel.dispose()
    }
}

[1] We will use KotlinLong for our recipe ids
[2] The shared RecipeDetailsViewModel will hold the data and deliver it to iOS through Kotlin flows, just like it did before in the recipes list view model
[3] In this @Published recipe property we’ll store the recipe details received through the shared view model.

Finally, add this to the RecipeDetailView.swift:

 
import SwiftUI
import shared
import Kingfisher
 
struct RecipeDetailView: View {
    
    let recipeId: KotlinLong?
    @ObservedObject var state: RecipeDetailState
    
    init(id: KotlinLong?) {
        self.recipeId = id
        state = RecipeDetailState(recipeId: id)
    }
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(state.recipe?.ingredients ?? [], id: \.id, content: { ingredient in
                    Text(ingredient.name)
                        .font(.body)
                })
            }
        }
        
    }
    
}

[1] The observable RecipeDetailState will hold the data for the RecipeDetailView
[2] To begin with, we are just displaying a list of ingredients to make sure everything works

Run the App now and try navigation to the pizza recipe. You’ll see the list of ingredients.

add toKotlinLong() extension

Now add other views to the recipe detail screen:

 
var body: some View {
        ScrollView { [1]
            Text(state.recipe?.title ?? "")
                .font(.headline)
           KFImage(URL(string: "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"))
                .resizable()
                .frame(width: 200, height: 150)
                .padding(.trailing)
            
            Text("Ingredients") [3]
                .font(.subheadline)
            LazyVStack {
                ForEach(state.recipe?.ingredients ?? [], id: \.id, content: { ingredient in
                    HStack {
                        Text(ingredient.name)
                            .font(.body)
                            .frame(maxWidth: .infinity, alignment: .leading)
                        
                        HStack {
                            Text("\(ingredient.amount, specifier: "%.1f")")
                            Text(ingredient.metric)
                                .font(.body)
                        }
                    }
                    
                })
            }
            .padding()
            
            Text("Instructions") [4]
                .font(.subheadline)
            LazyVStack {
                ForEach(state.recipe?.instructions ?? [], id: \.id, content: { instruction in
                    HStack {
                        Text("\(instruction.order). ")
                            .font(.body)
                        Text(instruction.description_)
                            .font(.body)
                            .frame(maxWidth: .infinity, alignment: .leading)
                    }
                    .padding()
                    
                })
            }
        }
        
    }

[1] Use ScrollView to make RecipeDetailsView scrollable in case it has to many ingredients or instructions
[2] Add a recipe title image with Kingfisher image library
[3] Add a section with ingredients’ names and measurements
[4] Add a section with instructions

Run the app again, go to recipe detail view and you’ll see, it looks much better now.

add toKotlinLong() extension

Our next step will be: adding a floating button and edit recipe functionality.