Using Spots Framework for Cross-Platform Development

In this Spots framework tutorial you’ll design an iOS app interface and port it over to macOS and tvOS, creating your very own cross-platform app interface. By Brody Eller.

Leave a rating/review
Save for later
Share

Spots is an open-source framework that enables you to design your UI for one platform, and use it on iOS, tvOS, and macOS. This lets you spend more time working on your app, and less time porting it to other platforms. Spots is also architected in such a way that it makes it incredibly easy to redesign your layout, by making use of the view model pattern. You can read more about what inspired the creators of Spots here.

Getting Started

In this tutorial, you’ll start off by making a simple app for iOS and use Spots to help you port the app to tvOS and macOS. Start by downloading the starter project here.

The starter project includes Spots, which has been pre-installed via Cocoapods. If you’re curious to learn more, you can look inside the Podfile to see how it’s set up, or check out our Cocoapods with Swift tutorial. You’ll use the imported Spots framework later to port your UI to JSON.

Open up Dinopedia.xcworkspace, and then open up the Dinopedia-iOS group. Then open up Main.storyboard within that group. You’ll notice that it contains an empty UINavigationController. Embedding UIViewControllers in a UINavigationController facilitates navigation between the UIViewControllers and makes it easy for you to set the UIViewControllers’ titles. You will work with both these features within this tutorial.

Note: At the time this tutorial was written, Spots did not compile cleanly with Swift 4 so you will see a warning that conversion to Swift 4 is available. When you build, you will see a number of other warnings in the Spots libraries. You’ll just have to ignore them for now.

Creating Your First View

To build a user interface in Spots, you first have to instantiate a custom view. In Spots, you make a custom view by creating a new subclass of UIView that conforms to ItemConfigurable. Then, you set up your constraints and the size of your view.

Create a new file inside the Dinopedia-iOS group named CellView.swift that inherits from UIView. At the top of the file, add the following code:

import Spots

Add the following code inside the CellView class:

lazy var titleLabel = UILabel()

You have now created a label that you will soon populate. By declaring the property as lazy, the label will be instantiated when it is first accessed. In this case, it means it will be instantiated when the label is actually going to be populated and displayed. Properties that are not declared as lazy are instantiated when the class or struct in which they are declared is instantiated.

Below where you declared the titleLabel, add the following code:

override init(frame: CGRect) {
  super.init(frame: frame)
  
  addSubview(titleLabel)
}

This overrides the view’s initializer, and will initialize the view and add the label.

Next, add the following required method below init(frame:):

required init?(coder aDecoder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}

In Swift, a subclass does not inherit its superclass’s designated initializer(s) by default. Since CellView.swift inherits from UIView, you must override all UIView‘s designated initializers.

Finally, you’ll implement three methods for configuring your view. First you will add constraints to the titleLabel you created earlier so that it displays nicely on the screen. Constraining the titleLabel is not enough; next you will need to populate the titleLabel with text.

Add the following new method at the bottom of the class:

func setupConstraints() {
   titleLabel.translatesAutoresizingMaskIntoConstraints = false
   titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
   titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16).isActive = true
   titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16).isActive = true
}

These constraints position the label in the center of your view vertically and give it a width equal to that of your view, with a bit of padding on either side.

At the bottom of init(frame:), add the following code:

setupConstraints()

This will therefore add the constraints right when CellView is initialized.

Now add the following to the bottom of the file, outside of the class definition:

extension CellView: ItemConfigurable {
  
  func configure(with item: Item) {
    titleLabel.text = item.title
  }
  
  func computeSize(for item: Item) -> CGSize {
    return CGSize(width: bounds.width, height: 80)
  }
  
}

configure(with:) sets the label’s text with the data passed as a parameter. computeSize(for:) sets the size of the view.

Now it’s time to use your view. In order for the application to use your view, you’ll have to register it. Open AppDelegate.swift and add the following code:

import Spots

Then add the following to application(didFinishLaunchingWithOptions), before the return:

Configuration.register(view: CellView.self, identifier: "Cell")

This registers the view you just created with the identifier "Cell". This identifier lets you reference your view within the Spots framework.

Creating Your First ComponentModel

It’s time to work with the Spots framework. First, you will create a ComponentModel.

Open ViewController.swift (make sure you choose the one in Dinopedia-iOS!). Items make up your ComponentModel and contain the data for your application. This data will be what the user sees when running the app.

There are many properties associated with Items. For example:

  • title is the name of the dinosaur’s species.
  • kind is the identifier that you gave CellView.swift in the AppDelegate.swift above.
  • meta has additional attributes, like the dinosaur’s scientific name and diet. You’ll use some of these properties now.

Add the following code at the top of the file:

import Spots

Add the following inside the viewDidLoad(), below super.viewDidLoad().

let model = ComponentModel(kind: .list, items: [
  Item(title: "Tyrannosaurus Rex", kind: "Cell", meta: [
    "ScientificName": "Tyrannosaurus Rex",
    "Speed": "12mph",
    "Lived": "Late Cretaceous Period",
    "Weight": "5 tons",
    "Diet": "Carnivore",
]),
  Item(title: "Triceratops", kind: "Cell", meta: [
    "ScientificName": "Triceratops",
    "Speed": "34mph",
    "Lived": "Late Cretaceous Period",
    "Weight": "5.5 tons",
    "Diet": "Herbivore",
]),
  Item(title: "Velociraptor", kind: "Cell", meta: [
    "ScientificName": "Velociraptor",
    "Speed": "40mph",
    "Lived": "Late Cretaceous Period",
    "Weight": "15 to 33lbs",
    "Diet": "Carnivore",
]),
  Item(title: "Stegosaurus", kind: "Cell", meta: [
    "ScientificName": "Stegosaurus Armatus",
    "Speed": "7mph",
    "Lived": "Late Jurassic Period",
    "Weight": "3.4 tons",
    "Diet": "Herbivore",
]),
  Item(title: "Spinosaurus", kind: "Cell", meta: [
    "ScientificName": "Spinosaurus",
    "Speed": "11mph",
    "Lived": "Cretaceous Period",
    "Weight": "7.5 to 23 tons",
    "Diet": "Fish",
]),
  Item(title: "Archaeopteryx", kind: "Cell", meta: [
    "ScientificName": "Archaeopteryx",
    "Speed": "4.5mph Running, 13.4mph Flying",
    "Lived": "Late Jurassic Period",
    "Weight": "1.8 to 2.2lbs",
    "Diet": "Carnivore",
]),
  Item(title: "Brachiosaurus", kind: "Cell", meta: [
    "ScientificName": "Brachiosaurus",
    "Speed": "10mph",
    "Lived": "Late Jurassic Period",
    "Weight": "60 tons",
    "Diet": "Herbivore",
]),
  Item(title: "Allosaurus", kind: "Cell", meta: [
    "ScientificName": "Allosaurus",
    "Speed": "19 to 34mph",
    "Lived": "Late Jurassic Period",
    "Weight": "2.5 tons",
    "Diet": "Carnivore",
]),
  Item(title: "Apatosaurus", kind: "Cell", meta: [
    "ScientificName": "Apatosaurus",
    "Speed": "12mph",
    "Lived": "Late Jurassic Period",
    "Weight": "24.5 tons",
    "Diet": "Herbivore",
]),
  Item(title: "Dilophosaurus", kind: "Cell", meta: [
    "ScientificName": "Dilophosaurus",
    "Speed": "20mph",
    "Lived": "Early Jurassic Period",
    "Weight": "880lbs",
    "Diet": "Carnivore",
  ]),
])

The code here is fairly straightforward. At the top, you create a new ComponentModel of type list. This causes your view to render as a UITableView instance. Then, you create your array of Items with a specific title and kind. This contains your data and sets its view type to the identifier, "Cell", which you specified earlier in AppDelegate.swift.