Building a Recommendation App With Create ML in SwiftUI

Learn how to train a model and how to give it prediction capability using Core ML and Create ML in SwiftUI. By Saeed Taheri.

Leave a rating/review
Download materials
Save for later
Share

Believe it or not, research into artificial intelligence, or AI, goes way back to the 1950s, but it wasn’t until the late 1990s that it started to show its value by finding specific solutions to specific problems.

Machine learning, or ML, is one of the important fields of AI and primarily focuses on understanding and building methods that learn. It tries to build a model based on training data so it can make decisions or predictions without someone having programmed it to do so.

ML has two main objectives: classification and prediction.

  • Classification classifies currently available data and makes decisions based on the developed models.
  • Prediction makes forecasts of future outcomes.

In Apple platforms, Core ML and Create ML are the main frameworks for machine learning.

  • Core ML lets you train a model based on the training data, and you can use the produced model in your apps on most Apple platforms.
  • Create ML, introduced in iOS 15, provides you with a means to create a Core ML model inside your app on iOS, macOS, iPadOS, and Mac Catalyst.

In this tutorial, you’ll develop an app called Tshirtinder — an app designed to match you to the perfect t-shirt. As its name suggests, it shows you a t-shirt, then you express your interest — or lack thereof — with Tinder-style gestures of swiping right or left.

After each swipe, the app shows a selection of t-shirts it thinks would interest you. As the app learns your t-shirt preferences, the recommendations become more relevant.

Before you get to the fun part of judging t-shirts, you’ll satisfy these learning objectives:

  • How to use Create ML to integrate AI within an app.
  • Create and train a model.
  • Build out predictive capabilities.

Getting Started

Download the starter project by clicking on the Download Materials button at the top or bottom of the tutorial.

Open TShirtinder.xcodeproj, then build and run it on your device.

Take a moment to play with the app. All the code to support core features, such as Tinder-style swipe animation, are already there for you to enjoy.

Swipe to right to like

Swipe to left to dislike

Note: You’ll need a real device to see all the functionalities working, because Create ML and Core ML aren’t available on the simulator. You could use the Mac (Designed for iPad) run destination if you’re on a Mac with an Apple M1 or better processor.

Regression vs. Classification

Regression predictive modeling problems are different from those of classification predictive modeling — in essence:

  • Regression predicts a continuous quantity.
  • Classification predicts a discrete class label.

Some overlaps exist between regression and classification:

  • A regression algorithm may predict a discrete value if it’s in the form of an integer quantity.
  • A classification algorithm may be in the form of a probability for a class label. If so, it may predict a continuous value.

With these in mind, you can use any of these modelings for your Tshirtinder. Yet, looking at the algorithms available in Create ML, a linear regression seems like a good fit.

What is Linear Regression?

Linear regression is a well-known algorithm in statistics and machine learning.

It’s a model that assumes a linear relationship between the input variables x and the single output variable y. It will calculate y from a linear combination of the input variables x.

In ML terms, people sometimes call input variables features. A feature is an individual measurable property or characteristic of a phenomenon.

Open shirts.json. As you see, all the t-shirts the app can show are in this file. For each t-shirt, there are features such as sleeve type, color, and neck type.

{
  "title": "Non-Plain Polo Short-Sleeve White",
  "image_name": "white-short-graphic-polo",
  "color": "white",
  "sleeve": "short",   
  "design": "non-plain",
  "neck": "polo"
} 

You can’t consider all the properties in each instance as features. For instance, the title or image_name isn’t suitable for showing the characteristics of a t-shirt — you can’t use them to predict the output.

Imagine you want to predict a value for a set of data with a single feature. You could visualize the data as such:

Two dimensional linear regression

Linear regression tries to fit a line through the data.

Then you use it to predict an estimated output for an unseen input. Assuming you have a model with two features, a two-dimensional plane will fit through the data.

To generalize this idea, imagine that you have a model with n features, so an (n-1) dimensional plane will be the regressor.

Consider the equation below:

Y = a + b * X

Where X is the explanatory variable and Y is the dependent variable. The slope of the line is b, and a is the intercept — the value of Y when X equals 0.

That’s enough theory for now.

How about you get your hands dirty and let technology help you get some new threads?

Preparing Data for Training

First, have a look at the methods you’ll work with and get to know how they work.

Open MainViewModel.swift and look at loadAllShirts().

This method asynchronously fetches all the shirts from shirts.json then stores them as a property of type FavoriteWrapper in MainViewModel. This wrapper adds a property to store the favorite status of each item, but the value is nil when there’s no information about the user’s preferences.

Now examine the other method — where most of the “magic” happens: didRemove(_:isLiked:). You call this method each time a user swipes an item.

The isLiked parameter tracks if the user liked a specific item or not.

This method first removes the item from shirts then updates the isFavorite field of the item in allShirts.

The shirts property holds all the items the user hasn’t yet acted on. Here’s when the ML part of the app comes in: You’ll compute recommended shirts anytime the user swipes left or right on a given t-shirt.

RecommendationStore handles the process of computing recommendations — it’ll train the model based on updated user inputs then suggest items the user might like.

Computing Recommendations

First, add an instance property to MainViewModel to hold and track the task of computing t-shirt recommendations to the user:

private var recommendationsTask: Task<Void, Never>?

If this were a real app, you’d probably want the output of the task and you’d also need some error handling. But this is a tutorial, so the generic types of Void and Never will do.

Next, add these lines at the end of didRemove(_:isLiked:):

// 1
recommendationsTask?.cancel()

// 2
recommendationsTask = Task {
  do {
    // 3
    let result = try await recommendationStore.computeRecommendations(basedOn: allShirts)

    // 4
    if !Task.isCancelled {
      recommendations = result
    }
  } catch {
    // 5
    print(error.localizedDescription)
  }
}

When the user swipes, didRemove(_:isLiked:) is called and the following happens:

  1. Cancel any ongoing computation task since the user may swipe quickly.
  2. Store the task inside the property you just created — step 1 exemplifies why you need this.
  3. Ask recommendationStore to compute recommendations based on all the shirts. As you saw before, allShirts is of the type FavoriteWrapper and holds the isFavorite status of shirts. Disregard the compiler error — you’ll address its complaint soon.
  4. Check for the canceled task, because by the time the result is ready, you might have canceled it. You check for that incident here so you don’t show stale data. If the task is still active, set the result to recommendations published property. The view is watching this property and updates it accordingly.
  5. Computing recommendations throws an async function. If it fails, print an error log to the console.

Now open RecommendationStore.swift. Inside RecommendationStore, create this method:

func computeRecommendations(basedOn items: [FavoriteWrapper<Shirt>]) async throws -> [Shirt] {
  return []
}

This is the signature you used earlier in MainViewModel. For now, you return an empty array to silence the compiler.