DEV Community

Including Figma images in your SwiftUI/Compose Previews

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:

Image description

How to use FigmaPreview

To use FigmaPreview you just need to follow the following simple steps:

  1. 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

Image description

  1. Get a link from Figma by clicking on the view that you need to work on

Image description

  1. 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
                    ...
                )
            }
        }
    }
    
  2. 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

Image description

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.

Image description

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.

Image description

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:

  1. Extracting the fileId and nodeId: First, we parse the URL of the Figma file to obtain two key parameters, the fileId and the nodeId. These identifiers specify the file and specific frame or component from which we want to obtain an image.
  2. Requesting the image from Figma’s API: Now, with the fileId and nodeId we can request the image with a simple GET request to the following endpoint: https://api.figma.com/v1/images/{fileId}?ids={nodeId} In the request header, we include a custom header named X-Figma-Token with our figma access token. This returns a URL of the image, which we then use as the base of this component.

Obtaining the image URL

Here is an example of how to retrieve the Figma 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]?
    }
Enter fullscreen mode Exit fullscreen mode

Android implementation:

        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
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

Displaying the Figma image

Once we have the URL, we can create the view to display the image as a background:

    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)
                }
            }
        }
    }

Enter fullscreen mode Exit fullscreen mode

Android implementation:

        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)
                    )
                }
            }
        }
        ```


Enter fullscreen mode Exit fullscreen mode

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 Image description

Top comments (0)