
If you design in Figma on Windows and your iOS developers want finished .xcassets bundles, you can do most of the work on your own machine: configure 1x/2x/3x export in Figma, convert the folder to the Xcode layout with a short Python script, then open the bundle in Xcode on a rented Mac to preview it in the iOS Simulator. Only the Xcode and Simulator step actually needs macOS.
The folder structure for an Xcode asset catalog is nothing but directories and Contents.json files — Windows or Linux can write them with ordinary file APIs. What needs macOS is Xcode itself: compiling the catalog, validating image sets, and launching the iOS Simulator so you can see an asset at a given device scale. That is the step a remote Mac is for.
Figment's $29 plan is a virtual Intel macOS environment (2 vCPU, 8 GiB RAM, 80 GiB disk, no GPU acceleration, no audio) reached through a browser console. There is no inbound SSH to the VM, and Xcode is not preinstalled — after you sign in you install Xcode yourself from Apple's Developer site or the App Store. Apple's porting guide is explicit that to run Xcode on an Intel processor you must use Xcode 26 or earlier, so this VM is fine for asset-catalog validation and any iOS Simulator runtime still supported by Xcode 26. If your team is on Xcode 27, needs visionOS, needs faster Simulator rendering on a real GPU, or just wants more RAM, choose the M-series plan from $95 — the exact chip, RAM and disk are confirmed in the quote. The $49 i3 Mac and the physical M-series tiers currently collect requests until supply is confirmed, so treat those as quote-first rather than instant stock.
Figma's own export documentation, Export formats and settings, is the source of truth. Two rules matter for this workflow:
1x, 2x, 3x multipliers Apple expects. The suffix field is a plain string appended to the filename after export — Figma does not add the @ for you, you type it.For each layer, frame, or component you want in the catalog:
1x, suffix empty, format PNG — produces Name.png (the 1x file).2x, suffix @2x, format PNG — produces Name@2x.png.3x, suffix @3x, format PNG — produces Name@3x.png.If a Figma layer name literally contains @2x or @3x (for example Icon@2x), rename the layer before exporting — otherwise the converter below will read the suffix as a scale.
Xcode expects each image set inside .xcassets to look like this:
MyAssets.xcassets/
Contents.json (top-level)
ButtonPrimary.imageset/
Contents.json
ButtonPrimary.png (1x)
ButtonPrimary@2x.png (2x)
ButtonPrimary@3x.png (3x)
The script below is an untested reference implementation — run it against a small test folder first and open the result in Xcode before you point it at a real design library. It needs Python 3.8+, standard library only, and leaves your original export folder untouched because it copies files rather than moving them. On macOS, Python 3 arrives with the Command Line Tools; on Windows it is a separate install from python.org.
#!/usr/bin/env python3
"""figma_to_xcassets.py - Convert a Figma export folder to a .xcassets bundle.
Usage: python3 figma_to_xcassets.py <figma_export_dir> <output.xcassets>
"""
import json, re, shutil, sys
from collections import defaultdict
from pathlib import Path
PATTERNS = [
(re.compile(r"^(.+)@3x\.(png|jpg)$", re.IGNORECASE), "3x"),
(re.compile(r"^(.+)@2x\.(png|jpg)$", re.IGNORECASE), "2x"),
(re.compile(r"^(.+)@1x\.(png|jpg)$", re.IGNORECASE), "1x"),
(re.compile(r"^(.+)\.(png|jpg)$", re.IGNORECASE), "1x"), # bare == 1x
]
def classify(name):
for pattern, scale in PATTERNS:
m = pattern.match(name)
if m:
return m.group(1), scale, m.group(2).lower()
return None
def contents_for(entries):
images = []
for scale in ("1x", "2x", "3x"):
slot = {"idiom": "universal", "scale": scale}
if scale in entries:
slot["filename"] = entries[scale]
images.append(slot)
return {"images": images, "info": {"version": 1, "author": "xcode"}}
def main():
if len(sys.argv) != 3:
sys.exit("Usage: figma_to_xcassets.py <export_dir> <output.xcassets>")
src, out = Path(sys.argv[1]), Path(sys.argv[2])
if not src.is_dir():
sys.exit(f"Not a directory: {src}")
groups = defaultdict(dict)
for f in sorted(src.iterdir()):
if not f.is_file():
continue
r = classify(f.name)
if not r:
continue
base, scale, ext = r
groups[base].setdefault(scale, (f, ext))
if not groups:
sys.exit("No PNG/JPG files found in the export directory.")
out.mkdir(parents=True, exist_ok=True)
(out / "Contents.json").write_text(
json.dumps({"info": {"version": 1, "author": "xcode"}}, indent=2),
encoding="utf-8",
)
for base, scale_map in sorted(groups.items()):
safe = re.sub(r"[^\w\s-]", "", base).strip().replace(" ", "_") or "Asset"
imageset = out / f"{safe}.imageset"
imageset.mkdir(exist_ok=True)
entries = {}
for scale, (src_file, ext) in scale_map.items():
suffix = "" if scale == "1x" else f"@{scale}"
dest_name = f"{safe}{suffix}.{ext}"
shutil.copy2(src_file, imageset / dest_name)
entries[scale] = dest_name
(imageset / "Contents.json").write_text(
json.dumps(contents_for(entries), indent=2), encoding="utf-8",
)
missing = [s for s in ("1x", "2x", "3x") if s not in entries]
tail = f", missing {missing}" if missing else ""
print(f" {imageset.name}: have {sorted(entries)}{tail}")
if __name__ == "__main__":
main()
Edge cases worth knowing before you run it:
Contents.json and only fills in filename for scales that exist. Xcode flags any truly missing scale in the catalog editor so you can add it or switch the image set to Single Scale.Icon.png) plus Icon@2x.png and Icon@3x.png, the script groups them correctly under one Icon.imageset.Because the $29 VM uses a browser console (no inbound SSH, no verified native VNC, no guaranteed bidirectional clipboard or drag-and-drop file upload), plan the transfer deliberately instead of expecting a desktop file dialog that reaches your Windows machine:
MyAssets.xcassets to the same git repository as the Xcode project and git pull inside the VM's desktop terminal. This is usually the easiest option because the same version lands in source control for the developers.Once Xcode is installed on the Mac, open your project, right-click the existing Assets.xcassets in the Project navigator, choose Show in Finder, drop the new imagesets next to the existing ones, then build to an iOS Simulator with Cmd+R. If the asset catalog editor flags a missing scale, either add it in Figma and re-export, or toggle the image set to Single Scale in the Attributes Inspector. Note that remote previews over a browser are not colour-calibrated and are not a substitute for checking the final build on real hardware before release.
The Figma configuration and the Python conversion run on Windows, Linux or macOS. Only the preview step needs a Mac, so size the plan by how often you preview and how heavy the Simulator work is.
| What the Mac is doing | Suggested Figment plan | Monthly list price |
|---|---|---|
Catalog validation, Contents.json checks, occasional Simulator spot-checks on an Xcode 26-or-earlier toolchain you install yourself | Virtual Intel macOS VM | $29 |
| Daily interactive Simulator, GPU-backed previews, Xcode 27, or SDKs that require Apple silicon | M-series Mac (chip/RAM/disk confirmed in quote) | from $95 |
| A shared design-handoff Mac with CI runners and managed configuration | Managed Build Mac (one M-series Mac, CI setup, backups, monitoring, 30 minutes monthly support) | $249 |
A short cost worksheet: a solo designer who previews two or three afternoons a month stays on the $29 VM for $29 x 12 = $348 a year. A designer pairing daily with iOS engineers on a current Xcode will typically want the M-series tier at $95 a month or more, priced per the quote, which is $1,140 a year at the entry list price. A team that wants the Mac to also run CI and keep backups should compare the $249 Managed Build Mac against the time an in-house admin would otherwise spend on setup and monitoring.
To request a configuration — including the exact Xcode version your project targets and whether you need Apple silicon — use /mac-hosting/start. The full lineup lives at /mac-hosting.
Walk through this list before calling the asset bundle done:
.xcassets and every imageset is visible in the Project navigator..xcassets in git, and the Xcode build all agree on the base filename.@2x collisions are reflected back in the Figma layer names so the next export round-trip stays clean.That is the whole pipeline: Figma for the slices, a short Python script for the Apple layout, and a Mac — virtual Intel for lightweight validation or M-series when the Simulator work gets real — for the one step that actually needs macOS.