DEV Community

Cover image for Skip: Build Native iOS and Android Apps with a Single SwiftUI Codebase
Tech Tales
Tech Tales

Posted on

Skip: Build Native iOS and Android Apps with a Single SwiftUI Codebase

Skip is a platform designed to create fully native iOS and Android applications using a shared Swift and SwiftUI codebase.
It achieves this by converting your Swift code into Kotlin, the primary language for Android development, and transforming your SwiftUI components to work seamlessly with Jetpack Compose, Android’s native UI framework.

RecipeCraft is Apple’s flagship tutorial app for SwiftUI. This article’ll demonstrate how to use Skip to transform RecipeCraft into a native Android application.

Overview

Apple’s RecipeCraft tutorial is a comprehensive, hands-on guide for building a fully-featured SwiftUI application. Over the years, it has been refined and updated to incorporate modern features and best practices. The tutorial showcases a variety of UI components, custom drawing capabilities, and Swift features such as Codable for data persistence.

This blog begins where Apple’s tutorial concludes. You’ll explore the steps needed to bring an existing iOS app to Android, understand the challenges involved, and learn how to address them effectively. Let’s dive in!

About Skip

Skip empowers developers to create fully native applications for iOS and Android using a single Swift and SwiftUI codebase. The tool converts Swift code into Kotlin and adapts SwiftUI to Jetpack Compose, ensuring compatibility with Android’s native UI.
The Android version of RecipeCraft generated by Skip will not look exactly the same as the iOS version—and that’s intentional. By leveraging each platform’s native UI elements and controls, Skip ensures a high-quality user experience, avoiding the inconsistencies often seen in non-native solutions.
To start, follow the Skip installation guide and set up your Android development environment, including Android Studio. Once ready, open Android Studio and access the Virtual Device Manager from the ellipsis menu on the Welcome screen. Create a new device (such as “Medium Phone”) and start the Emulator. A connected Android device or Emulator is required to run your app on Android using Skip.

Image description

Now we’re ready to turn RecipeCraft into a dual-platform Skip app.

Recipeskipper

Updating an existing Swift Package Manager package to support Skip is relatively straightforward. However, adapting a complete app is more complex. Android development with Skip requires a specific folder structure and Xcode project configuration. To simplify the process, we recommend starting with a new Skip Xcode project and then migrating your existing app’s code and assets into it.
To begin, open your Terminal and run the following command to initialize a dual-platform version of the RecipeCraft app, named Recipeskipper:

skip init --open-xcode --appid=com.xyz.Recipeskipper recipe-skipper Recipeskipper

This command will generate a template SwiftUI application and automatically open it in Xcode. Before proceeding further, verify that the template project is functioning correctly. Select an iOS Simulator of your choice in Xcode and click the Run button.
If you’ve recently installed or updated Skip, you might need to trust the Skip plugin before running the project.

Image description

If everything goes smoothly, you should encounter something similar to the following:

Image description

Great! Next, copy RecipeCraft’s source code to Recipeskipper:

Drag the RecipeCraft/Views/AllRecipesView and RecipeCraft/Views/AddRecipeView files from RecipeCraft’s Xcode window into the Receipeskipper/Sources/Receipeskipper/ folder in Receipeskipper’s window.

Image description

Migration Process

The moment of truth has arrived—time to hit that Run button in Xcode!

Almost immediately, you’ll get an API unavailable error like this one:

Image description

Migrating an existing iOS codebase to Android using Skip is no small task. While starting a new app with Skip can be exciting and manageable—allowing you to design with cross-platform compatibility in mind—adapting an existing project presents its own set of challenges. When tackling an established codebase, all potential compatibility issues often surface simultaneously. Even if Skip accurately translates over 95% of your Swift code and API calls, the remaining 5%—likely written without cross-platform considerations—can result in dozens or even hundreds of errors.
That said, it’s worth remembering that addressing this 5% is still far less effort than a full Android rewrite, potentially reducing your workload by 20 times or more. Once you’ve resolved these issues, you’ll have a unified Swift and SwiftUI codebase that is easy to maintain across both platforms.
For example, consider the pictured error message indicating that the showAddSheet.toggle() method isn’t supported in Skip. Each of Skip’s major frameworks includes documentation listing the APIs currently supported on Android. These lists are regularly updated as new functionality is ported. For instance, you can refer to the table of supported SwiftUI components to confirm compatibility.
When an API isn’t supported on Android, it doesn’t mean you need to compromise your iOS app. Skip allows you to handle such cases by creating alternative Android-specific code paths. You can either contribute a missing API implementation or, more commonly, choose a different approach for the Android version. To maintain your iOS code while providing an Android alternative, use compiler directives like #if SKIP or #if !SKIP to create platform-specific paths in your code.
showAddSheet.toggle() is not supported

Update from this:

func addRecipe() {
        recipeDelegate?.addRecipe(.init(foodName: foodNameText, cookingInstruction: instructionsText, cookingTime: timeText))
        showAddSheet.toggle()
}

Enter fullscreen mode Exit fullscreen mode

To this:

func addRecipe() {
        recipeDelegate?.addRecipe(.init(foodName: foodNameText, cookingInstruction: instructionsText, cookingTime: timeText))
        #if !SKIP
        showAddSheet.toggle()
        #else
        showAddSheet = !showAddSheet
        #endif
}

Enter fullscreen mode Exit fullscreen mode

.border(.gray) is not supported
Update from this:

       HStack(spacing: 15) {
                    Text("Food Name: ")
                        .frame(width: 115, height: 40)
                        .font(.caption)
                        .foregroundColor(.secondary)
                        .multilineTextAlignment(.leading)


                    ZStack() {
                        TextField("", text: $foodNameText)
                            .frame(height: 40)
                            .font(.caption)
                            .foregroundColor(.black)
                            .padding(.all, 5)
                    }
                    .frame(height: 40)
                    .border(.gray)


                }
                .padding(.top, 40)

                HStack(spacing: 15) {
                    Text("Cooking Instruction: ")
                        .frame(width: 115, height: 40)
                        .font(.caption)
                        .foregroundColor(.secondary)

                    ZStack() {
                        TextEditor(text: $instructionsText)
                            .frame(height: 70)
                            .font(.caption)
                            .foregroundColor(.black)
                            .padding(.all, 5)
                    }
                    .frame(height: 70)
                    .border(.gray)
                }
                .padding(.top, 20)

                HStack(spacing: 15) {
                    Text("Cooking Time: ")
                        .frame(width: 115, height: 40)
                        .font(.caption)
                        .foregroundColor(.secondary)

                    ZStack() {
                        TextField("", text: $timeText)
                            .frame(height: 40)
                            .font(.caption)
                            .foregroundColor(.black)
                            .padding(.all, 5)
                    }
                    .frame(height: 40)
                    .border(.gray)
                }
                .padding(.top, 20)

Enter fullscreen mode Exit fullscreen mode

To this:

               HStack(spacing: 15) {
                    Text("Food Name: ")
                        .frame(width: 115, height: 40)
                        .font(.caption)
                        .foregroundColor(.secondary)
                        .multilineTextAlignment(.leading)

                    #if !SKIP
                    ZStack() {
                        TextField("", text: $foodNameText)
                            .frame(height: 40)
                            .font(.caption)
                            .foregroundColor(.black)
                            .padding(.all, 5)
                    }
                    .frame(height: 40)
                    .border(.gray)
                    #else
                    TextField("", text: $foodNameText)
                        .frame(height: 50)
                        .font(.caption)
                        .foregroundColor(.black)
                   #endif

                }
                .padding(.top, 40)

                HStack(spacing: 15) {
                    Text("Cooking Instruction: ")
                        .frame(width: 115, height: 40)
                        .font(.caption)
                        .foregroundColor(.secondary)

                    ZStack() {
                        TextEditor(text: $instructionsText)
                             #if !SKIP
                            .padding(.all, 5)
                             #endif
                            .frame(height: 70)
                            .font(.caption)
                            .foregroundColor(.black)
                    }
                    .frame(height: 70)
                    #if !SKIP
                    .border(.gray)
                    #else
                    .background(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(.gray, lineWidth: 2)
                    )
                    .cornerRadius(5)
                    #endif
                }
                .padding(.top, 20)

                HStack(spacing: 15) {
                    Text("Cooking Time: ")
                        .frame(width: 115, height: 40)
                        .font(.caption)
                        .foregroundColor(.secondary)
                    #if !SKIP
                    ZStack() {
                        TextField("", text: $timeText)
                            .frame(height: 40)
                            .font(.caption)
                            .foregroundColor(.black)
                            .padding(.all, 5)
                    }
                    .frame(height: 40)
                    .border(.gray)
                    #else
                    TextField("", text: $timeText)
                        .frame(height: 50)
                        .font(.caption)
                        .foregroundColor(.black)
                    #endif
                }
                .padding(.top, 20)


Enter fullscreen mode Exit fullscreen mode

.renderingMode(.template) is not supported
Update from this:

HStack() {
                    Spacer()
                    Image(systemName: "plus")
                        .resizable()
                        .renderingMode(.template)
                        .frame(width: 21, height: 21)
                        .foregroundColor(.blue)
                        .padding(.trailing, 5)
                        .onTapGesture {
                            showAddSheet.toggle()
                        }
                }
                .padding(.top, 10)
Enter fullscreen mode Exit fullscreen mode

To this:

HStack() {
                    Spacer()
                    Image(systemName: "plus")
                        .resizable()
                        #if !SKIP
                        .renderingMode(.template)
                        #endif
                        .frame(width: 21, height: 21)
                        .foregroundColor(.blue)
                        .padding(.trailing, 5)
                        .onTapGesture {
                            #if !SKIP
                            showAddSheet.toggle()
                            #else
                            showAddSheet = !showAddSheet
                            #endif
                        }
                }
                .padding(.top, 10)

Enter fullscreen mode Exit fullscreen mode

Top comments (0)