KMPizza
Kotlin Multiplatform + Pizza = ❤️

Step 15: Bind iOS UI to the shared KMM ViewModel

Now let’s move to the iOS side for a while and implement the UI there.
First, open the workspace from iosApp folder inside your project folder in XCode.

open iosApp workspace

Open ContentView

open ContentView

There you’ll see

struct ContentView: View {
	let greet = Greeting().greeting()
 
	var body: some View {
		Text(greet)
	}
}

As we’ve already removed the Greeting file from our shared module, we won’t be able to access it here, so let’s replace

 
let greet = Greeting().greeting()

with

let greet = "Give me pizza!"

You should be able to run the code now and see the app asking for pizza.

Let’s bind this iOS app to the recipe backend just like we did for Android.
The networking logic is already in the shared module. All we need to do is to find a way to use that logic in our ios UI.

First, create an observable state for our recipe list screen.
Organise the iosApp structure bit: Create a New Group ui and move ContenView there.
Also add recipes group to the ui package.
In recipes Create a New File RecipesState.

create RecipesState

The simplest version of RecipesState looks like this:

 
import Foundation
import shared
 
class RecipesState: ObservableObject { [1]     
    let viewModel: RecipeViewModel
    
    @Published private var recipes: [RecipeResponse]  = [] [2]
    
    init() { 
        viewModel = RecipeViewModel() [3]
         
        viewModel.observeRecipes { recipes in
            self.recipes = recipes [4]
        }
    }
    
    deinit {
        viewModel.dispose()
    }
}

[1] @Published wrapper combined with the ObservableObject protocol will help us to observe changes to the published properties within this observable RecipesState and redraw our UI accordingly.
[2] In this @Published recipes list property we’ll store the recipes received from backend through the shared module.
[3] Look how easily we can initiate the shared RecipeViewModel instance in iOS!
[4] Thanks to observeRecipes function from the shared module we can get the recipes from the backend and use them for the iOS UI

Now, add RecipesView file to the recipes group.
Use the observable RecipesState object in the RecipesView like this:

import SwiftUI
import shared
 
struct RecipesView: View {
     
    @ObservedObject var state: RecipesState [1]
    
    init() { 
        state = RecipesState()
    }
    
    var body: some View {
            List(state.recipes, id: \.id) { recipe in [2]
                    Text(recipe.title)
            }
            .listStyle(PlainListStyle())
    }
}

[1] Here we’re using the RecipesState as an ObservedObject.
[2] Whenever the @Published list of recipes in RecipesState changes, we’ll refresh the List (analog for lazyColumn in Compose) with recipe titles.

Now change the main ContentView to display the RecipeView:

 
struct ContentView: View {
    
    var body: some View {
            VStack {
                RecipesView()
            }
    }
}

But if you try running the app, you’ll get an error: Uncaught Kotlin exception: kotlin.Throwable: The process was terminated due to the unhandled exception thrown in the coroutine [StandaloneCoroutine{Cancelling}@258c458, MainDispatcher]: KoinApplication has not been started

AGAIN?! 😱
No panic.
We’ll just need to setup Koin for the iosApp.
First, go back to your Android Studio and in CommonModule add

 
fun initKoin() = initKoin {}

Then return to XCode and change your iosApp.swift to the following:

 
import SwiftUI
import shared
 
@main
struct KMPizzaApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
 
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
 
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization
        CommonModuleKt.doInitKoin()
        return true
    }
}

This piece of code allows us to customize the entry point for the iosApp and lets us define when to start Koin.
Here we create an AppDelegate and while doing it, initialize Koin with this line: CommonModuleKt.doInitKoin()

Now run the app again.
It may take a while, but you’ll finally see the “Pizza dough” item in our one-item-list.
Yay! We’ve moved one step forward.

happy flying pizza

In the next step we’ll come up with a better design and learn to use KMP image libraries.