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.
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.
If everything goes smoothly, you should encounter something similar to the following:
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.
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:
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()
}
To this:
func addRecipe() {
recipeDelegate?.addRecipe(.init(foodName: foodNameText, cookingInstruction: instructionsText, cookingTime: timeText))
#if !SKIP
showAddSheet.toggle()
#else
showAddSheet = !showAddSheet
#endif
}
.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)
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)
.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)
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)
Top comments (0)