A seamless way to integrate your figma designs into your workflow
Introduction
During our development process here at Playtomic, we often encounter challenges that lead to frustrating back-and-forth exchanges between developers and designers. One of the main issues is ensuring that the implementation aligns perfectly with the designs, often due to small misalignments like missing or extra spaces.
To address this, we looked for ways to enhance our workflow and improve collaboration. Figma is the tool our product design team uses to create stunning interfaces, so integrating those designs into our mobile projects can be cumbersome.
That’s why we created FigmaPreview, a component available for both SwiftUI and Jetpack Compose, allowing us to set an image from Figma as a background directly in the Xcode or Android Studio preview while we code. This simple yet powerful addition to our workflow helps ensure pixel-perfect precision across platforms, significantly reducing the back-and-forth between design and development and allowing us to move faster while maintaining high-quality UI.
💡To keep this post clear and avoid repetition, I'll focus on the SwiftUI implementation. However, both the SwiftUI and Jetpack Compose versions share the same API and behave almost identically, with just a few platform-specific differences that I'll cover at the end.
What is FigmaPreview?
FigmaPreview is a custom SwiftUI component that lets you overlay your figma designs directly onto the SwiftUI views, and this background image can serve as a reference, allowing you to make a pixel perfect (or very closely) implementation.
Before diving into the details, here’s a quick example of how FigmaPreview helps catch subtle misalignments that might go unnoticed at first glance:
How to use FigmaPreview
To use FigmaPreview you just need to follow the following simple steps:
- Obtain a token from your account to allow to the component to reach up to your private design files. Obtaining a token is pretty simple, you just need to go to your account settings and click on
Generate new token
button
- Get a link from Figma by clicking on the view that you need to work on
-
Wrap your preview view inside of the FigmaPreview component. You only need to invoke the class init from your preview view
struct ExamplePlaytomicView_Previews: PreviewProvider { static var previews: some View { FigmaPreview(url: "Your figma's url here") { ExamplePlaytomicView( firstParam: "something", secondParam: 47 ... ) } } }
Now you can adjust the alpha of the figma image that you have in the background of your preview to compare the figma design with your implementation
How we use FigmaPreview
Essentially, we are doing 2 main usages of FigmaPreview
across our development process.
Coding complete screens
When building entire screens, we use FigmaPreview
as a background, so we can align our implementation with precision. This ensures that our screens closely matches the intended design from the very beginning, reducing misalignments.
Catalog components
In addition to full screens, we also use FigmaPreview
for individual UI components. To support this, we maintain a separate app within our main project as a component catalog. Our designers can use this app to review and validate UI components, ensuring they meet pixel perfect standards with FigmaPreview
enabled.
This approach not only ensures design accuracy at a granular level but also streamlines our workflow, as designers can review, test and approve components directly in the catalog app. By the time the components are added to the main screens, they are already pixel perfect, reducing the need for further adjustments.
How it is implemented
The implementation of this component is not very complex, essentially, we just need to transform the url that we obtain from figma into a one that provides you only the image view.
💡To obtain the original figma url you can review the second step of the section
How to use FigmaPreview
To implement this, we use the Figma API, which provides the necessary functionality to access images from Figma files. Here’s a quick breakdown of the process:
- Extracting the
fileId
andnodeId
: First, we parse the URL of the Figma file to obtain two key parameters, thefileId
and thenodeId
. These identifiers specify the file and specific frame or component from which we want to obtain an image. - Requesting the image from Figma’s API: Now, with the
fileId
andnodeId
we can request the image with a simpleGET
request to the following endpoint:https://api.figma.com/v1/images/{fileId}?ids={nodeId}
In the request header, we include a custom header namedX-Figma-Token
with our figma access token. This returns a URL of the image, which we then use as the base of this component.
Here is an example of how to retrieve the Figma image URL: Android implementation:Obtaining the image URL
class FigmaPreviewModel: ObservableObject {
@State private var originalUrl = ""
@Published var imageUrl: String = ""
/**
* Original URL: https://www.figma.com/file/ILeBSptVwqyqpr3gR2KZLh/Improvements-for-switching-teams?type=design&node-id=2012-6765&mode=design&t=60wrWFvQkrJyMHIg-1
* API URL that needs to be tranformed: https://api.figma.com/v1/images/ILeBApGukqyqprK932KZLh?ids=2099-6965&format=png
*/
func updateUrl(figmaUrl: String, scale: CGFloat) {
guard originalUrl != figmaUrl else { return }
guard
let url = URL(string: figmaUrl),
let components = NSURLComponents(url: url, resolvingAgainstBaseURL: false),
let fileId = components.path?.split(usingRegex: "/").getOrNull(index: 2),
let nodeId = components.queryItems?.first(where: { $0.name == "node-id" })?.value
else {
return
}
guard
let requesrUrl =
URL(string: "https://api.figma.com/v1/images/\(fileId)?ids=\(nodeId)&format=png&use_absolute_bounds=true&scale=\(scale)")
else { return }
var request = URLRequest(url: requesrUrl)
request.addValue("{your_token_here}", forHTTPHeaderField: "X-Figma-Token")
let dataTask = URLSession.shared.dataTask(with: request) { data, response, _ in
guard
(response as? HTTPURLResponse)?.statusCode == 200,
let data
else { return }
DispatchQueue.main.async {
guard
let decodedPreview = try? JSONDecoder().decode(FigmaPreviewItem.self, from: data),
let imageURL = decodedPreview.images?[nodeId.replacingOccurrences(of: "-", with: ":")]
else { return }
self.imageUrl = imageURL
self.originalUrl = figmaUrl
}
}
dataTask.resume()
}
}
struct FigmaPreviewItem: Decodable {
let err: String?
let images: [String: String]?
}
class FigmaPreviewModel {
private var originalUrl = ""
var imageUrl by mutableStateOf("")
/**
* Original URL: https://www.figma.com/file/ILeBSptVwqyqpr3gR2KZLh/Improvements-for-switching-teams?type=design&node-id=2012-6765&mode=design&t=60wrWFvQkrJyMHIg-1
* API URL that needs to be tranformed: https://api.figma.com/v1/images/ILeBApGukqyqprK932KZLh?ids=2099-6965&format=png
*/
@Throws(IOException::class)
fun updateUrl(figmaUrl: String, density: Float) {
if (originalUrl == figmaUrl) {
return
}
val url = URL(figmaUrl)
val components = url.path.split("/")
val fileId = components.getOrNull(2) ?: return
val query = url.query ?: return
val nodeId = query.split("&").firstOrNull { it.startsWith("node-id=") }?.split("=")?.getOrNull(1) ?: return
val requestUrl = URL("https://api.figma.com/v1/images/$fileId?ids=$nodeId&format=png&use_absolute_bounds=true&scale=$density")
val httpURLConnection = requestUrl.openConnection() as HttpURLConnection
httpURLConnection.apply {
requestMethod = "GET"
addRequestProperty("X-Figma-Token", "{your_token_here}")
}
httpURLConnection.requestMethod = "GET"
val responseCode = httpURLConnection.responseCode
if (responseCode != HttpURLConnection.HTTP_OK) {
return
}
val inputReader = BufferedReader(InputStreamReader(httpURLConnection.inputStream))
var inputLine: String?
val response = StringBuffer()
while (inputReader.readLine().also { inputLine = it } != null) {
response.append(inputLine)
}
inputReader.close()
Handler(Looper.getMainLooper()).post {
JSONObject(response.toString()).optJSONObject("images")?.optString(nodeId.replace("-", ":"))?.let { imageUrl = it }
originalUrl = figmaUrl
}
}
}
Once we have the URL, we can create the view to display the image as a background: Android implementation:Displaying the Figma image
public struct FigmaPreview<Content: View>: View {
@StateObject var model = FigmaPreviewModel()
@State private var url: String
@State private var opacity: Double
private let content: Content
public init(
url: String,
opacity: Double = 0.4,
@ViewBuilder content: () -> Content
) {
_url = State(initialValue: url)
_opacity = State(initialValue: opacity)
self.content = content()
}
public var body: some View {
@Environment(\.colorScheme) var colorScheme
ScrollView {
VStack {
HStack {
Text("Code")
Slider(value: $opacity, in: 0 ... 1)
.padding(.horizontal)
Text("Figma")
}.padding(.horizontal, medium)
ZStack(alignment: .top) {
content.frame(maxWidth: figmaPreviewDefaultWidth)
HStack {
let scale = UIScreen.main.scale
AsyncImage(url: URL(string: model.imageUrl), scale: scale)
.opacity(opacity)
.onAppear { model.updateUrl(figmaUrl: url, scale: scale) }
.dashedBorder(color: R.color.playtomicSwiftUI.emerald.opacity(0.5))
}
.ignoresSafeArea(.all)
.frame(width: figmaPreviewDefaultWidth)
.dashedBorder(color: R.color.playtomicSwiftUI.sky_blue.opacity(0.5))
}.frame(minWidth: figmaPreviewDefaultWidth)
}
}
}
}
val figmaPreviewDefaultWidth = 375.dp
@Composable
fun FigmaPreview(url: String, opacity: Float = 0.4f, contentScale: ContentScale = ContentScale.FillBounds, content: @Composable () -> Unit) {
var opacityState by remember { mutableStateOf(opacity) }
val model = remember { FigmaPreviewModel() }
val density = LocalDensity.current.density
val localContext = LocalContext.current
LaunchedEffect(url) { Executors.newFixedThreadPool(1).execute { model.updateUrl(url, density = density) } }
LaunchedEffect(Unit) {
// mgonzalez: Put here the context that is needed to start any preview
val contextProvider = object : IContextProvider {
override val applicationContext: android.content.Context
get() = localContext.applicationContext
override val currentActivity: Activity?
get() = localContext as? Activity
override val activityStack: List<Activity>
get() = listOf()
}
Context.contextProvider = contextProvider
PlaytomicUI.initialize(contextProvider = contextProvider)
}
Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = Medium)) {
Text("Code")
Slider(
value = opacityState, onValueChange = { opacityState = it }, valueRange = 0f..1f, modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f)
)
Text("Figma")
}
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.requiredWidth(figmaPreviewDefaultWidth)
.dashedBorder(Color.Blue)
) {
Box(modifier = Modifier.dashedBorder(Color.Green)) { content() }
val modifier = if (contentScale == ContentScale.FillBounds) {
Modifier
} else {
Modifier.matchParentSize()
}
PlaytomicImage(
urlString = model.imageUrl,
placeholder = com.playtomicui.R.drawable.ic_asset_add_picture_disable,
options = ImageOptions.ORIGINAL,
contentScale = contentScale,
modifier = modifier.alpha(opacityState)
)
}
}
}
```
This approach simplifies our development process by displaying the figma design right within Xcode. You can see our implementation here:
Some limitations that the component have
- Each time you make a change in your code, the component needs to load again the image, what some times might be a bit annoying
- On android, you need to run the preview to see the figmapreview working
Top comments (0)