Creating a MacOS command line tool to dynamically generate PreviewDevice devices for SwiftUI
Tags: swift, ios, development, swiftui
Published: 2021-05-17 08:37:46

When you want to preview your SwiftUI view on different devices, you need to use the `previewDevice` view modifier like so:

```swift
MyView()
 .previewDevice(PreviewDevice(rawValue: "iPhone 12 Pro Max"))
 .previewDisplayName("iPhone 12 Pro Max")
```

If you set the preview device to nil, Xcode will automatically choose the appropriate device based on your current target.

When creating a `PreviewDevice` manually, you will need to set the `rawValue` string to the correct device name. Failing to do so will generate an error in the preview window.

A way to avoid having to manually enter the `rawValue` yourself is to use the Preview UI.

![Showing how to add PreviewDevice for a particular simulator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/crg55jj9d1z9nifhxcjw.png)

I particular don't like hardcoded values/nor using the ui, so I thought I would create a utility to generate strongly typed values to make my life easier.

Our goal is to be able to do this:
```swift
Content().preview(on: .iPhoneSE2ndGeneration)
```

# Creating a Command Line Tool.

**Step 1**

`New > Project > MacOS > Command Line Tool`
![Showing how to create a command line tool](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ehpojyq8tuvkr2n1zu6z.png)

**Step 2: Retrieve a list of the available simulators.**

We can achieve this using [simctl](https://nshipster.com/simctl/)

```swift
xcrun simctl list --json devices available
```

The nice thing about this command is that you can pass in `json` so it would make it easier to parse it in swift.

**Step 3: Generating our models.**

For me the go to place is [QuickType](https://quicktype.io). It makes it easier to convert JSON into gorgeous, typesafe code in any language. (As they put it)

![SimCtl swift model being generated using QuickType](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sco65bq1ekbjife7qi4z.png)

**Step 4: Executing shell commands from swift.**

I'm using [ShellOut](https://github.com/JohnSundell/ShellOut) made by John Sundel. It wraps shell commands into a lovely wrapper that makes it easier and delightful to use it.

```swift
let output = try shellOut(to: "xcrun simctl list --json devices available")
```

**Step 5: Parsing**

Once we retrieve the json above, we will need to decode into our model.

```swift
let deviceTypes = try JSONDecoder().decode(SimCTL.self, from: data)
```

The current json structure is held in a variable `var devices: [String: [Device]]`, where the key is the runtime value (com.apple.CoreSimulator.SimRuntime.iOS-14-4). We are interested in the flat list of devices here so we can generate the final swift file. So we will parse that our selves during decoding.

```swift
public init(from decoder: Decoder) throws {
	let container = try decoder.container(keyedBy: CodingKeys.self)
	devices = try container.decode([String: [Device]].self, forKey: CodingKeys.devices)
	supportedDevices = devices
		.filter({ !$0.value.isEmpty })
		.map({ $0.value })
		.flatMap({ $0 })
}
```

**Step 6: Generating our swift file**

Once we retrieve the available devices, parsed them and have our models in place, it's time to generate our final output file. First we need to create a template string and inject the list of devices dynamically.

This is what the final file looks like:

```swift
let template =
"""extension PreviewDevice {
	{{DYNAMIC CONTENT HERE}}
}

struct PreviewDeviceModifier: ViewModifier {
	let previewDevice: PreviewDevice

	func body(content: Content) -> some View {
		content
			.previewDevice(previewDevice)
			.previewDisplayName(previewDevice.rawValue)
	}
}

public extension View {
	func preview(on previewDevice: PreviewDevice) -> some View {
		self.modifier(
			PreviewDeviceModifier(
				previewDevice: previewDevice
			)
		)
	}
}
"""
```

**Step 7: Injecting the string**

```swift
let deviceTypes = try JSONDecoder().decode(SimCTL.self, from: data)
let finalOutput = String(format: template, "\(deviceTypes)")
```

If you are wondering how we've managed to pass the variable `deviceTypes` and somehow generated `static let appleWatchSeries540mm = Self(rawValue: "Apple Watch Series 5 - 40mm")`, let me show you.

You can extend `String` to allow custom interpolation of values. So we can do this:

```swift
extension String.StringInterpolation {
	mutating func appendInterpolation(_ value: SimCTL) {
		let devicesString = value
			.supportedDevices
			.map({ "static let \($0.name.camelized) = Self(rawValue: \"\($0.name)\")"  })
			.joined(separator: "\n\t")

		appendLiteral(devicesString)
	}
}
```

Notice how we've extended the `appendInterpolation` and added `SimCTL` for the value parameter. Doing so allow us to control what will be appended to the final string result.


**Final Step: Generating the final file**

The last piece of the puzzle is to generate that file. I'm using [Files](https://github.com/JohnSundell/Files), another open source project by John Sundel.

```swift
let folder = Folder.current
		let file = try folder.createFile(named: "PreviewDeviceUtils.swift")
		try file.write(finalOutput)
```

#Conclusion

I think I've achieve our initial goal, albeit pointless as you can do all this using the UI. But hopefully we've managed to learn a few things along the way and now we can generate many more utility apps. This could also be done using a MacOS app, which I will probably attempt to develop and see what we can learn from it.

[Source Code](https://gist.github.com/vibrazy/ced9d3b9959fbfbd7ee55a7fb6c2a42a)