class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDelegate, MessageReminderListControllerDelegate {
let locationProvider = LocationProvider.shared
let currentUserController: CurrentChatUserController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the current user controller and load the initial active live location messages
currentUserController.delegate = self
currentUserController.loadActiveLiveLocationMessages()
// Set up the location updates
locationProvider.didUpdateLocation = { [weak self] location in
let newLocation = LocationInfo(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude
)
self?.currentUserController.updateLiveLocation(newLocation)
}
}
func currentUserControllerDidStartSharingLiveLocation(
_ controller: CurrentChatUserController
) {
locationProvider.startMonitoringLocation()
}
func currentUserControllerDidStopSharingLiveLocation(_ controller: CurrentChatUserController) {
locationProvider.stopMonitoringLocation()
}
func currentUserController(
_ controller: CurrentChatUserController,
didChangeActiveLiveLocationMessages messages: [ChatMessage]
) {
// Whenever the active live location messages change
// this delegate method will be called.
// You can use this to update some custom UI or for fine-grained control over the location tracking.
}
}
Location Sharing
Introduction
Location sharing is a powerful feature that allows users to share their current position or real-time location with other participants in a channel. Stream Chat supports both static and live location sharing through location attachments.
There are two types of location sharing:
- Static Location: A one-time location share that does not update over time.
- Live Location: A real-time location share that updates over time for a specified duration.
The SDK handles location message creation and updates, but location tracking must be implemented by the application using device location services.
Adding Location Sharing into a UIKit Chat App
In this guide, we will be adding location sharing functionality to a UIKit chat app, similar to WhatsApp, iMessage, or Telegram. Other location sharing use cases, like delivery tracking, can be implemented in a similar way. The Stream Chat SDK provides the underlying functionality for location sharing, but, at the moment, does not include default UI components. However, we provide a complete example implementation in our Demo App that you can reference and adapt for your needs.
You can find the reference implementation in the Custom Attachments folder, which includes the following features:
- Location picker view
- Location sharing permissions handling
- Location snapshot preview for the message list
- Full map view with live updates
After completing the guide, you should have a working location sharing functionality in your chat app, like in the pictures below:
Location Picker | Message List | Map View |
---|---|---|
![]() | ![]() | ![]() |
Setting up location services
For location sharing functionality, it’s important that you ask for permissions to use the device location services. Therefore, you should add the following entries in your Info.plist
file:
Privacy - Location When In Use Usage Description
- “YOUR_APP_NAME requires location access to share your location with other users”Privacy - Location Always and When In Use Usage Description
- “YOUR_APP_NAME requires location access to share your location with other users”
Once you have added the entries, you will need to create a LocationProvider
class to handle the location monitoring as well as the permission requests. If you already have a similar class in your app, you can use it, otherwise, you can take inspiration from our LocationProvider in the Demo App.
Monitoring location updates
In order to update the location of the user, you need to set up the CurrentChatUserController
and implement location tracking. The CurrentChatUserController
makes it easy to start and stop monitoring location updates, as well as observing the active live location messages. The controller will only call the didStartSharingLiveLocation()
if the user was not already sharing a live location, and when initializing the controller if the user was already sharing a live location.
The following steps are required to set up the location tracking:
- Set up the
CurrentChatUserController
and set the delegate - Load the initial active live location messages (only once)
- Send location updates by calling
currentUserController.updateLiveLocation()
- Observe when to start and stop the location updates through the delegate methods
We recommend setting up the location tracking as soon as you connect the user. In the example below, taken from our Demo App, we set up the location tracking in the tab bar view controller of the app, but you can do it wherever is more convenient for you.
By default, the CurrentChatUserController
does not handle if the location was created from the current device or not. However, every location object has a createdByDeviceId
property that you can use to check if the location was created from the current device. If you want to make sure that location sharing is device-specific, you can use the didChangeActiveLiveLocationMessages
delegate method to control when to track location updates, and ignore the didStartSharingLiveLocation()
and didStopSharingLiveLocation()
methods. Here is a simple example on how you can do this:
func currentUserController(
_ controller: CurrentChatUserController,
didChangeActiveLiveLocationMessages messages: [ChatMessage]
) {
let currentDeviceId = controller.currentUser?.currentDevice?.id
let currentDeviceLocationMessages = messages.filter {
$0.sharedLocation?.createdByDeviceId == currentDeviceId
}
if currentDeviceLocationMessages.isEmpty {
locationProvider.stopMonitoringLocation()
} else {
locationProvider.startMonitoringLocation()
}
}
Adding location picker
For sending location messages, we first need to customize the composer.
For this, we’ll need to:
- Create a custom composer view controller that inherits from
ComposerVC
- Add a location button to the composer’s attachment button collection
- Present a location picker view controller to select the location type (static or live)
In order to add a new location action button to the composer, we need to override the attachmentsPickerActions
property of the ComposerVC
class. When the button is tapped, we will present a location picker view controller to select the location type (static or live).
class DemoComposerVC: ComposerVC {
override var attachmentsPickerActions: [UIAlertAction] {
var actions = super.attachmentsPickerActions
let isLocationEnabled = channelController?.channel?.config.sharedLocationsEnabled == true
if isLocationEnabled && content.isInsideThread == false {
let locationAction = UIAlertAction(
title: "Location",
style: .default,
handler: { [weak self] _ in
self?.presentLocationSelection()
}
)
actions.append(locationAction)
}
return actions
}
func presentLocationSelection() {
guard let channelController = channelController else { return }
let locationSelectionVC = LocationSelectionViewController(channelController: channelController)
let navigationController = UINavigationController(rootViewController: locationSelectionVC)
present(navigationController, animated: true)
}
}
Components.default.messageComposerVC = DemoComposerVC.self
As you can see, we are checking if the location sharing is enabled for the channel and if the message is not inside a thread. Since location sharing is not supported inside threads.
Then, we are creating a new LocationSelectionViewController
instance and presenting it. This view controller will be responsible for requesting the location permission and selecting whether the user wants to send a static or live location message.
class LocationSelectionViewController: UIViewController, ThemeProvider {
private let channelController: ChatChannelController
private let locationProvider = LocationProvider.shared
private var currentLocation: CLLocation?
init(channelController: ChatChannelController) {
self.channelController = channelController
super.init(nibName: nil, bundle: nil)
}
private lazy var mapView: MKMapView = {
let view = MKMapView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isZoomEnabled = true
view.showsCompass = false
view.showsUserLocation = true
return view
}()
private lazy var staticLocationButton: LocationOptionButton = {
let button = LocationOptionButton()
button.configure(
icon: UIImage(systemName: "mappin.circle"),
title: "Send Current Location",
subtitle: "Share your current location once"
)
button.addTarget(self, action: #selector(sendStaticLocationTapped), for: .touchUpInside)
button.isEnabled = false
return button
}()
private lazy var liveLocationButton: LocationOptionButton = {
let button = LocationOptionButton()
button.configure(
icon: UIImage(systemName: "location.circle"),
title: "Share Live Location",
subtitle: "Share your location in real-time"
)
button.addTarget(self, action: #selector(shareLiveLocationTapped), for: .touchUpInside)
button.isEnabled = false
button.layer.borderColor = appearance.colorPalette.accentPrimary.cgColor
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupConstraints()
getCurrentLocation()
}
private func getCurrentLocation() {
locationProvider.getCurrentLocation { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let location):
self?.handleLocationReceived(location)
case .failure:
self?.showLocationPermissionAlert()
}
}
}
}
private func handleLocationReceived(_ location: CLLocation) {
currentLocation = location
let coordinate = location.coordinate
let region = MKCoordinateRegion(
center: coordinate,
latitudinalMeters: 1000,
longitudinalMeters: 1000
)
mapView.setRegion(region, animated: true)
mapView.userTrackingMode = .follow
staticLocationButton.isEnabled = true
liveLocationButton.isEnabled = true
}
private func showLocationPermissionAlert() {
let alert = UIAlertController(
title: "Location Access Required",
message: "Please enable location access in Settings to share your location.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
@objc private func sendStaticLocationTapped() {
guard let location = currentLocation else { return }
let locationInfo = LocationInfo(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude
)
channelController.sendStaticLocation(locationInfo)
dismiss(animated: true)
}
@objc private func shareLiveLocationTapped() {
guard let location = currentLocation else { return }
let alertController = UIAlertController(
title: "Share Live Location",
message: "Select the duration for sharing your live location.",
preferredStyle: .actionSheet
)
let durations: [(String, TimeInterval)] = [
("1 minute", 61),
("10 minutes", 600),
("1 hour", 3600)
]
for (title, duration) in durations {
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
guard let self = self, let location = self.currentLocation else { return }
let locationInfo = LocationInfo(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude
)
let endDate = Date().addingTimeInterval(duration)
self.channelController.startLiveLocationSharing(locationInfo, endDate: endDate) { [weak self] result in
self?.dismiss(animated: true)
}
}
alertController.addAction(action)
}
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alertController, animated: true)
}
}
We use the LocationProvider
class to get the current location of the user, which will first ask for permissions if the user has not granted them yet. Then, we are setting up the map view so that the user can see their current location and the two buttons to select the location type. If the user taps the static location button, we will send a static location message to the channel by calling channelController.sendStaticLocation(locationInfo)
. If the user taps the live location button, we will present an alert controller to select the duration for the live location sharing, and call channelController.startLiveLocationSharing(locationInfo, endDate: endDate)
.
For the full implementation, you can check the LocationSelectionViewController in the Demo App.
Adding message location view
Now that we have a way to send location messages, we need to render them in the message list. The Stream Chat SDK uses an attachment system to customize how different types of content are displayed. For location messages, we need to:
- Create a custom attachment view catalog
- Create a location attachment view injector
- Create a location snapshot view
- Handle location interactions
Setting up the attachment view catalog
First, create a custom attachment view catalog that detects location messages and uses the custom view injector:
class CustomAttachmentViewCatalog: AttachmentViewCatalog {
override class func attachmentViewInjectorClassFor(message: ChatMessage, components: Components) -> AttachmentViewInjector.Type? {
let hasLocationAttachment = message.sharedLocation != nil
if hasLocationAttachment {
return LocationAttachmentViewInjector.self
}
return super.attachmentViewInjectorClassFor(message: message, components: components)
}
}
// Register the custom catalog
Components.default.attachmentViewCatalog = CustomAttachmentViewCatalog.self
Creating the location attachment view injector
The view injector is responsible for the following:
- Adding the location view to the message view
- Forwarding the location tap to the message list delegate
- Forwarding the stop sharing location tap to the message list delegate
Here’s the full implementation:
class LocationAttachmentViewInjector: AttachmentViewInjector {
lazy var locationAttachmentView = LocationAttachmentSnapshotView()
let mapWidth: CGFloat = 300
override func contentViewDidLayout(options: ChatMessageLayoutOptions) {
super.contentViewDidLayout(options: options)
contentView.bubbleContentContainer.insertArrangedSubview(locationAttachmentView, at: 0)
contentView.bubbleThreadFootnoteContainer.width(mapWidth)
locationAttachmentView.didTapOnLocation = { [weak self] in
self?.handleTapOnLocationAttachment()
}
locationAttachmentView.didTapOnStopSharingLocation = { [weak self] in
self?.handleTapOnStopSharingLocation()
}
let isSentByCurrentUser = contentView.content?.isSentByCurrentUser == true
let maskedCorners: CACornerMask = isSentByCurrentUser
? [.layerMinXMaxYCorner, .layerMinXMinYCorner]
: [.layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMaxXMinYCorner]
locationAttachmentView.layer.maskedCorners = maskedCorners
locationAttachmentView.layer.cornerRadius = 16
locationAttachmentView.layer.masksToBounds = true
}
override func contentViewDidUpdateContent() {
super.contentViewDidUpdateContent()
if let message = contentView.content, let location = message.sharedLocation {
locationAttachmentView.content = .init(
message: message,
location: location
)
}
}
func handleTapOnLocationAttachment() {
guard let locationAttachmentDelegate = contentView.delegate as? LocationAttachmentViewDelegate else {
return
}
guard let location = contentView.content?.sharedLocation else {
return
}
locationAttachmentDelegate.didTapOnLocation(location)
}
func handleTapOnStopSharingLocation() {
guard let locationAttachmentDelegate = contentView.delegate as? LocationAttachmentViewDelegate else {
return
}
guard let location = contentView.content?.sharedLocation else {
return
}
locationAttachmentDelegate.didTapOnStopSharingLocation(location)
}
}
Creating the location snapshot view
The location snapshot view displays a map snapshot of the location. Here’s the essential structure:
class LocationAttachmentSnapshotView: _View, ThemeProvider {
struct Content {
var coordinate: CLLocationCoordinate2D
var isLive: Bool
var isSharingLiveLocation: Bool
var message: ChatMessage?
var author: ChatUser?
}
var content: Content? { didSet { updateContent() } }
var didTapOnLocation: (() -> Void)?
var didTapOnStopSharingLocation: (() -> Void)?
// UI Components
lazy var imageView: UIImageView = { /* Map snapshot display */ }()
lazy var stopButton: UIButton = { /* Stop sharing button for live locations */ }()
lazy var avatarView: ChatUserAvatarView = { /* User avatar for live locations */ }()
lazy var sharingStatusView: LocationSharingStatusView = { /* Live status indicator */ }()
override func updateContent() {
super.updateContent()
guard let content = self.content else { return }
// Show/hide UI elements based on location type
if content.isSharingLiveLocation && content.isFromCurrentUser {
stopButton.isHidden = false
sharingStatusView.isHidden = true
} else if content.isLive {
stopButton.isHidden = true
sharingStatusView.isHidden = false
} else {
stopButton.isHidden = true
sharingStatusView.isHidden = true
}
configureMapPosition()
loadMapSnapshotImage()
}
}
This view will generate a snapshot image of the location and display it in the message cell. The snapshot is generated using MKMapSnapshotter
and cached for performance.
For the full implementation, you can check the LocationAttachmentSnapshotView in the Demo App.
Note: When quoting a message with a location attachment, by default it won’t display anything. You will need to customize the QuotedChatMessageView
, for example:
class DemoQuotedChatMessageView: QuotedChatMessageView {
override func updateContent() {
super.updateContent()
if let sharedLocation = content?.message.sharedLocation {
if sharedLocation.isLive {
attachmentPreviewView.contentMode = .scaleAspectFit
attachmentPreviewView.image = UIImage(systemName: "location.fill")
attachmentPreviewView.tintColor = .systemBlue
textView.text = "Live Location"
} else {
attachmentPreviewView.contentMode = .scaleAspectFit
attachmentPreviewView.image = UIImage(systemName: "mappin.circle.fill")
attachmentPreviewView.tintColor = .systemRed
textView.text = "Static Location"
}
}
}
}
Components.default.quotedMessageView = DemoQuotedChatMessageView.self
Handling location interactions
We need to handle the location interactions that were forwarded by the view injector. We do this by extending the ChatMessageContentViewDelegate
with two new methods, didTapOnLocation
and didTapOnStopSharingLocation
.
protocol LocationAttachmentViewDelegate: ChatMessageContentViewDelegate {
func didTapOnLocation(_ location: SharedLocation)
func didTapOnStopSharingLocation(_ location: SharedLocation)
}
class DemoChatMessageListVC: LocationAttachmentViewDelegate {
func didTapOnLocation(_ location: SharedLocation) {
let messageController = client.messageController(
cid: location.channelId,
messageId: location.messageId
)
showDetailViewController(messageController: messageController)
}
func didTapOnStopSharingLocation(_ location: SharedLocation) {
client
.messageController(cid: location.channelId, messageId: location.messageId)
.stopLiveLocationSharing()
}
private func showDetailViewController(messageController: ChatMessageController) {
let mapViewController = LocationDetailViewController(
messageController: messageController
)
navigationController?.pushViewController(mapViewController, animated: true)
}
}
// Register the custom message list
Components.default.messageListVC = DemoChatMessageListVC.self
Full map view with live updates
Now that we have everything mostly set up, the only thing left to do is to create a view controller that will display the map and the live location updates.
We will use the ChatMessageController
to notify us whenever the location message changes. Below you will find the essential code to display the map and the live location updates. For the full implementation, you can check the LocationDetailViewController in the Demo App.
class LocationDetailViewController: UIViewController, ThemeProvider {
let messageController: ChatMessageController
init(
messageController: ChatMessageController
) {
self.messageController = messageController
super.init(nibName: nil, bundle: nil)
}
private var userAnnotation: UserAnnotation?
let mapView: MKMapView = {
let view = MKMapView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isZoomEnabled = true
view.showsCompass = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Location"
messageController.synchronize()
messageController.delegate = self
mapView.register(
UserAnnotationView.self,
forAnnotationViewWithReuseIdentifier: UserAnnotationView.reuseIdentifier
)
mapView.showsUserLocation = !isFromCurrentUser
mapView.delegate = self
view.backgroundColor = appearance.colorPalette.background
view.addSubview(mapView)
setUpConstraints()
var locationCoordinate: CLLocationCoordinate2D?
if let location = messageController.message?.sharedLocation {
locationCoordinate = CLLocationCoordinate2D(
latitude: location.latitude,
longitude: location.longitude
)
}
if let locationCoordinate {
mapView.region = .init(
center: locationCoordinate,
span: coordinateSpan
)
updateUserLocation(
locationCoordinate
)
}
}
}
extension LocationDetailViewController: ChatMessageControllerDelegate {
func messageController(
_ controller: ChatMessageController,
didChangeMessage change: EntityChange<ChatMessage>
) {
guard let location = messageController.message?.sharedLocation, location.isLive else {
return
}
let locationCoordinate = CLLocationCoordinate2D(
latitude: location.latitude,
longitude: location.longitude
)
updateUserLocation(
locationCoordinate
)
let isLiveLocationSharingStopped = location.isLiveSharingActive == false
if isLiveLocationSharingStopped, let userAnnotation = self.userAnnotation {
let userAnnotationView = mapView.view(for: userAnnotation) as? UserAnnotationView
userAnnotationView?.stopPulsingAnimation()
}
updateBannerState()
}
}
extension LocationDetailViewController: MKMapViewDelegate {
func mapView(
_ mapView: MKMapView,
viewFor annotation: MKAnnotation
) -> MKAnnotationView? {
guard let userAnnotation = annotation as? UserAnnotation else {
return nil
}
let annotationView = mapView.dequeueReusableAnnotationView(
withIdentifier: UserAnnotationView.reuseIdentifier,
for: userAnnotation
) as? UserAnnotationView
annotationView?.setUser(userAnnotation.user)
let location = messageController.message?.sharedLocation
if location?.isLiveSharingActive == true {
annotationView?.startPulsingAnimation()
} else {
annotationView?.stopPulsingAnimation()
}
return annotationView
}
}
For the most part, this implementation will suit most use cases. However, you can customize it to your needs, for example, you can also show all active live location messages in the same map. For this you will need to use the channel.activeLiveLocations
property and use the EventsController
to listen for message events and update the location messages accordingly.
Conclusion
In this guide, we’ve covered how to integrate location sharing functionality into a UIKit-based chat application using the Stream Chat SDK. The SDK provides the core functionality for sending, receiving, and managing location messages, while giving you the flexibility to create custom UI components that match your app’s design.
Key points to remember:
- The SDK handles location message lifecycle management
- Your app is responsible for location tracking and UI components
- Both static and live location sharing are supported
- Events allow real-time updates of location messages