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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Using Spots Framework for Cross-Platform Development
25 mins
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 UIViewController
s in a UINavigationController
facilitates navigation between the UIViewController
s and makes it easy for you to set the UIViewController
s’ titles. You will work with both these features within this tutorial.
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!). Item
s 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 Item
s. 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 Item
s 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.