Realm-Swift Type Projections
Tags: realm, mobile, swift, mongodb
Published: 2022-01-26 17:06:37

# Introduction

[Realm natively provides a broad set of data types](https://docs.mongodb.com/realm/sdk/swift/data-types/supported-property-types/), including `Bool`, `Int`, `Float`, `Double`, `String`, `Date`, `ObjectID`, `List`, `Mutable Set`, `enum`, `Map`, …

But, there are other data types that many of your iOS apps are likely to use. As an example, if you're using Core Graphics, then it's hard to get away without using types such as `CGFloat`, `CGPoint`, etc. When working with SwiftUI, you use the [`Color`](https://developer.apple.com/documentation/swiftui/color) struct when working with colors.

A typical design pattern is to persist data using types natively supported by Realm, and then use a richer set of types in your app. When reading data, you add extra boilerplate code to convert them to your app's types. When persisting data, you add more boilerplate code to convert your data back into types supported by Realm.

That works fine and gives you complete control over the type conversions. The downside is that you can end up with dozens of places in your code where you need to make the conversion.

Type projections still give you total control over how to map a `CGPoint` into something that can be persisted in Realm. But, you write the conversion code just once and then forget about it. The Realm-Swift SDK will then ensure that types are converted back and forth as required in the rest of your app.

The Realm-Swift SDK enables this by adding two new protocols that you can use to extend any Swift type. You opt whether to implement `CustomPersistable` or the version that's allowed to fail (`FailableCustomPersistable`):

```swift
protocol CustomPersistable {
   associatedtype PersistedType
   init(persisted: PersistedType)
   var persistableValue: PersistedType { get }
}
protocol FailableCustomPersistable {
   associatedtype PersistedType
   init?(persisted: PersistedType)
   var persistableValue: PersistedType { get }
}
```

In this post, I'll show how the [Realm-Drawing](https://github.com/realm/Realm-Drawing) app uses type projections to interface between Realm and Core Graphics.

# Prerequisites

- iOS 15+
- Xcode 13.2+
- [Realm-Swift 10.21.0+](https://github.com/realm/realm-swift/releases)

# The Realm-Drawing App

[Realm-Drawing](https://github.com/realm/Realm-Drawing) is a simple, collaborative drawing app. If two people log into the app using the same username, they can work on a drawing together. All strokes making up the drawing are persisted to Realm and then synced to all other instances of the app where the same username is logged in.

![Animated screen captures showing two iOS devices working on the same drawing](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/Realm_Draw_5f4559a2b9.gif)

It's currently iOS-only, but it would also sync with any Android drawing app that is connected to the same Realm back end.

# Using Type Projections in the App

The Realm-Drawing iOS app uses three types that aren't natively supported by Realm:

- `CGFloat`
- `CGPoint`
- `Color` (SwiftUI)

In this section, you'll see how simple it is to use type projections to convert them into types that can be persisted to Realm and synced.

## Realm Schema (The Model)

An individual drawing is represented by a single [`Drawing`](https://github.com/realm/Realm-Drawing/blob/main/iOS/DrawingApp/Model/Drawing.swift) object:

```swift
class Drawing: Object, ObjectKeyIdentifiable {
   @Persisted(primaryKey: true) var _id: ObjectId
   @Persisted var name = UUID().uuidString
   @Persisted var lines = RealmSwift.List<Line>()
}
```

A Drawing contains a `List` of [`Line`](https://github.com/realm/Realm-Drawing/blob/main/iOS/DrawingApp/Model/Line.swift) objects:

```swift
class Line: EmbeddedObject, ObjectKeyIdentifiable {
   @Persisted var lineColor: Color
   @Persisted var lineWidth: CGFloat = 5.0
   @Persisted var linePoints = RealmSwift.List<CGPoint>()
}
```

It's the `Line` class that uses the non-Realm-native types.

Let's see how each type is handled.

### CGFloat

I extend `CGFloat` to conform to Realm-Swift's `CustomPersistable` protocol. All I needed to provide was:

- An initializer to convert what's persisted in Realm (a `Double`) into the `CGFloat` used by the model
- A method to convert a `CGFloat` into a `Double`:

```swift
extension CGFloat: CustomPersistable {
   public typealias PersistedType = Double
   public init(persistedValue: Double) { self.init(persistedValue) }
   public var persistableValue: Double { Double(self) }
}
```

The [`view`](https://github.com/realm/Realm-Drawing/blob/main/iOS/DrawingApp/Views/DrawingView.swift) can then use `lineWidth` from the model object without worrying about how it's converted by the Realm SDK:

```swift
context.stroke(path, with: .color(line.lineColor),
   style: StrokeStyle(
       lineWidth: line.lineWidth,
       lineCap: .round, l
       ineJoin: .round
   )
)
```

### CGPoint

`CGPoint` is a little trickier, as it can't just be cast into a Realm-native type. `CGPoint` contains the x and y coordinates for a point, and so, I create a Realm-friendly class ([`PersistablePoint`](https://github.com/realm/Realm-Drawing/blob/main/iOS/DrawingApp/Model/PersistablePoint.swift)) that stores just that—`x` and `y` values as `Doubles`:

```swift
public class PersistablePoint: EmbeddedObject, ObjectKeyIdentifiable {
   @Persisted var x = 0.0
   @Persisted var y = 0.0

   convenience init(_ point: CGPoint) {
       self.init()
       self.x = point.x
       self.y = point.y
   }
}
```

I implement the `CustomPersistable` protocol for `CGPoint` by mapping between a `CGPoint` and the `x` and `y` coordinates within a `PersistablePoint`:

```swift
extension CGPoint: CustomPersistable {
   public typealias PersistedType = PersistablePoint   
   public init(persistedValue: PersistablePoint) { self.init(x: persistedValue.x, y: persistedValue.y) }
   public var persistableValue: PersistablePoint { PersistablePoint(self) }
}
```

### SwiftUI.Color

`Color` is made up of the three RGB components plus the opacity. I use the [`PersistableColor`](https://github.com/realm/Realm-Drawing/blob/main/iOS/DrawingApp/Model/PersistableColor.swift) class to persist a representation of `Color`:

```swift
public class PersistableColor: EmbeddedObject {
   @Persisted var red: Double = 0
   @Persisted var green: Double = 0
   @Persisted var blue: Double = 0
   @Persisted var opacity: Double = 0

   convenience init(color: Color) {
       self.init()
       if let components = color.cgColor?.components {
           if components.count >= 3 {
               red = components[0]
               green = components[1]
               blue = components[2]
           }
           if components.count >= 4 {
               opacity = components[3]
           }
       }
   }
}
```

The extension to implement `CustomPersistable` for `Color` provides methods to initialize `Color` from a `PersistableColor`, and to generate a `PersistableColor` from itself:

```swift
extension Color: CustomPersistable {
   public typealias PersistedType = PersistableColor

   public init(persistedValue: PersistableColor) { self.init(
       .sRGB,
       red: persistedValue.red,
       green: persistedValue.green,
       blue: persistedValue.blue,
       opacity: persistedValue.opacity) }

   public var persistableValue: PersistableColor {
       PersistableColor(color: self)
   }
}
```

The [view](https://github.com/realm/Realm-Drawing/blob/main/iOS/DrawingApp/Views/DrawingView.swift) can then use `selectedColor` from the model object without worrying about how it's converted by the Realm SDK:

```swift
context.stroke(
   path,
   with: .color(line.lineColor),
   style: StrokeStyle(lineWidth:
   line.lineWidth,
   lineCap: .round,
   lineJoin: .round)
)
```

# Conclusion

Type projections provide a simple, elegant way to convert any type to types that can be persisted and synced by Realm.

It's your responsibility to define how the mapping is implemented. After that, the Realm SDK takes care of everything else.

Please provide feedback and ask any questions in the [Realm Community Forum](https://www.mongodb.com/community/forums/c/realm-sdks/58).