Build an iMessage app in iOS by adding an iMessage Application target to your Xcode project and implementing MSMessagesAppViewController with custom UI and message handling.
Understanding iMessage Apps vs Sticker Packs
Sticker packs display images only and require no code. iMessage apps provide custom UI, interactive controls, and rich message layouts.
An iMessage app uses MSMessagesAppViewController as its principal view controller. It supports a compact view above the keyboard and an expanded view that fills the screen. Developers choose which presentation style fits each interaction.
Adding an iMessage Extension Target
Open your Xcode project. Select the project file in the Project Navigator. Go to Editor and choose Add Target. Select iMessage Application under the iOS tab. Name your extension and click Finish. Activate the new scheme when prompted.
Xcode creates a folder with MessagesViewController.swift, MainInterface.storyboard, and an asset catalog for the extension's app icon.
Sharing Data with an Embedded Framework
Your main app contains data that the extension needs. Do not duplicate that data. Create an embedded framework both targets share.
Go to Editor, choose Add Target, and select Framework. Name the framework and move shared data structures into it.
import Foundation
public struct ProductItem: Hashable {
public var name: String = ""
public var imageName = ""
public var description = ""
public var price: Double = 0.0
public init(name: String, imageName: String, description: String, price: Double) {
self.name = name
self.imageName = imageName
self.description = description
self.price = price
}
}
public class ProductCatalog {
public static var items: [ProductItem] = [
ProductItem(name: "Mountain Scene", imageName: "mountain", description: "Scenic mountain view", price: 2.99),
ProductItem(name: "Ocean View", imageName: "ocean", description: "Coastal landscape", price: 1.99)
]
}
Make all structs and their properties public so both targets can access them.
Import the framework in both the main app and the extension:
import ProductDataKit
Designing the Messages UI
Open MainInterface.storyboard. The default view controller is a subclass of MSMessagesAppViewController. Delete the default label and add a UITableView.
Configure the table view with Auto Layout constraints pinned to all edges. Set Prototype Cells to 1. Set the cell identifier to ProductCellIdentifier. Add an image view and labels to the prototype cell.
Create a custom UITableViewCell subclass:
class ProductCell: UITableViewCell {
@IBOutlet var productImageView: UIImageView!
@IBOutlet var nameLabel: UILabel!
@IBOutlet var descriptionLabel: UILabel!
@IBOutlet var priceLabel: UILabel!
}
Set the prototype cell's class to ProductCell in the Identity inspector and connect the outlets.
Populating the Table View
In MessagesViewController, import the shared framework and set up a data source:
import ProductDataKit
class MessagesViewController: MSMessagesAppViewController {
private var products = ProductCatalog.items
@IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
}
}
extension MessagesViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCellIdentifier", for: indexPath) as! ProductCell
let product = products[indexPath.row]
cell.nameLabel.text = product.name
cell.descriptionLabel.text = product.description
cell.priceLabel.text = "$(product.price)"
cell.productImageView.image = UIImage(named: product.imageName)
return cell
}
}
Enabling Asset Access
Select your main app's asset catalog in the Project Navigator. Under Target Membership, check your extension target so it can load the images.
Sending Messages with MSMessage
When the user selects a product, send it to the active conversation using MSMessage and MSMessageTemplateLayout:
extension MessagesViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
requestPresentationStyle(.compact)
tableView.deselectRow(at: indexPath, animated: true)
let product = products[indexPath.row]
guard let conversation = activeConversation else { return }
let layout = MSMessageTemplateLayout()
layout.caption = product.name
layout.subcaption = "$(product.price)"
layout.image = UIImage(named: product.imageName)
let message = MSMessage()
message.layout = layout
var components = URLComponents(string: "https://example.com/product")!
components.queryItems = [
URLQueryItem(name: "name", value: product.name),
URLQueryItem(name: "imageName", value: product.imageName),
URLQueryItem(name: "price", value: String(product.price))
]
message.url = components.url
conversation.insert(message) { error in
if let error = error { print("Insert error: (error.localizedDescription)") }
}
}
}
The requestPresentationStyle call ensures the message field is visible. MSMessageTemplateLayout defines the bubble appearance with caption, subcaption, and image properties. The url property encodes product data as query parameters that the recipient's extension can decode.
Handling Received Messages
Override MSMessagesAppViewController lifecycle methods to handle message selection.
Presenting a Detail View
Create a UIViewController subclass for displaying product details:
class DetailViewController: UIViewController {
@IBOutlet var nameLabel: UILabel!
@IBOutlet var descriptionLabel: UILabel!
@IBOutlet var priceLabel: UILabel!
@IBOutlet var imageView: UIImageView!
var selectedProduct: ProductItem?
}
Managing Extension States
Override the lifecycle methods to handle message selection, activation, and presentation transitions:
override func didSelect(_ message: MSMessage, conversation: MSConversation) {
guard let selected = conversation.selectedMessage else { return }
presentProductDetail(from: selected)
}
override func willBecomeActive(with conversation: MSConversation) {
guard presentationStyle == .expanded else { return }
if let selected = conversation.selectedMessage {
presentProductDetail(from: selected)
}
}
override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
if presentationStyle == .compact {
dismiss(animated: false)
}
}
private func presentProductDetail(from message: MSMessage) {
guard let url = message.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else { return }
var name = "", imageName = "", price = ""
for item in queryItems {
switch item.name {
case "name": name = item.value ?? ""
case "imageName": imageName = item.value ?? ""
case "price": price = item.value ?? ""
default: break
}
}
let product = ProductItem(name: name, imageName: imageName, description: "", price: Double(price) ?? 0)
// present product detail via segue or navigation
}
The didSelect method fires when the user taps a message while the extension is active. The willBecomeActive method handles extension activation from a message tap. The willTransition method dismisses detail views when returning to compact mode.
Best Practices for iMessage Apps
Use embedded frameworks to share code between the main app and the extension. Never duplicate data or logic.
Handle both compact and expanded presentation modes. Design the UI to work at both sizes.
Encode structured data in the message URL. The image alone does not carry the product information. Use URLComponents for proper query string encoding.
Test both sending and receiving flows. Send a message from one simulated user and verify the detail view appears for the recipient.
Summary
This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.
Frequently Asked Questions
What Xcode version do I need for this tutorial? This tutorial uses Xcode 26 with Swift 6.2. The code examples work with any recent version of Xcode that supports the frameworks used here.
What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.