Activities
The Activities module provides calendar-based access to a member's activity history. Query by month and year to retrieve aggregate statistics and individual activity records -- gym visits, step tracking, self-reports, and at-home workouts.
UI Reference
Build a monthly calendar that color-codes each day by activity type and status. Use get(month, year) to populate the grid and summary statistics.
Key data mappings:
| UI Element | AFCore Source |
|---|---|
| Month header & progress bar | monthlySummary.acceptedActivities / monthlySummary.acceptedVisitGoalDays |
| Day activity dots | activities grouped by date, colored by ActivityType |
| Summary counters | monthlySummary.acceptedActivities, .pendingActivities, .rejectedActivities |
| Legend colors | GYM_VISIT_ACTIVITY (green), SMART_WALKING_ACTIVITY (blue), HOME_ACTIVITY (orange) |
Data Model
AFActivitiesSummary
Returned by get(), this contains both the monthly aggregate and the list of individual activities.
| Field | Type | Description |
|---|---|---|
monthlySummary | MonthlySummary? | Aggregate statistics for the requested month. |
activities | List<Activity> | Individual activity records for the month. |
MonthlySummary
| Field | Type | Description |
|---|---|---|
month | Int | Calendar month (1--12). |
year | Int | Calendar year. |
acceptedVisitGoalDays | Int | Number of days the visit goal was met. |
acceptedVisitDays | Int | Number of days with at least one accepted visit. |
acceptedActivities | Int | Total accepted activities in the month. |
pendingActivities | Int | Activities awaiting verification. |
rejectedActivities | Int | Activities that were denied. |
Activity
| Field | Type | Description |
|---|---|---|
type | ActivityType? | Category of the activity. |
name | String? | Display name (e.g., facility name or "SmartWalking"). |
iconRes | String? | Optional icon resource identifier. |
statusType | ActivityStatus | Current verification status. |
date | String? | Date of the activity (ISO-8601). |
metaData | ActivityMetaData? | Additional details specific to the activity type. |
message | String? | Server message (e.g., rejection reason). |
ActivityType
| Value | Key | Description |
|---|---|---|
GYM_VISIT_ACTIVITY | "gym" | Facility visit detected by geofencing. |
SMART_WALKING_ACTIVITY | "walking" | Step-based activity from SmartWalking. |
SELF_REPORTED_ACTIVITY | "mobileSelfReport" | Manually submitted visit report. |
HOME_ACTIVITY | "fitAtHome" | At-home workout verified by selfie. |
ActivityStatus
| Value | Description |
|---|---|
APPROVED / ACCEPTED / COMPLETED | Activity was verified and counts toward goals. Treat these as equivalent. |
PENDING | Activity is awaiting verification or still in progress. |
DENIED / REJECTED | Activity did not pass validation. Check Activity.message for the reason. |
PARTIAL | Activity was recorded but did not meet the required threshold (e.g., insufficient steps). |
IGNORE | Recorded for tracking purposes only; does not count toward goals. |
DEFAULT | Fallback when the server returns an unmapped status. |
ActivityMetaData
| Field | Type | Description |
|---|---|---|
facilityId | Int? | Facility identifier. |
facilityName | String? | Display name of the facility. |
address | String? | Street address. |
city | String? | City. |
state | String? | Two-letter state code. |
details | String? | Brief description (used for MOVR, SpotIt, and MyCarePath activity types). |
timeSpent | Int? | Duration in minutes. |
steps | Int? | Step count (SmartWalking activities). |
distance | Double? | Distance traveled. |
distanceUnit | String? | Unit of distance ("Miles" or "Kilometers"). |
calories | Double? | Calories burned (SmartWalking activities). |
activeMinutes | Int? | Active minutes (SmartWalking activities). |
lastSync | String? | Timestamp of the last sync for this activity. |
latitude | Double? | Latitude of the recorded activity location. |
longitude | Double? | Longitude of the recorded activity location. |
deviceType | DeviceType? | Source device/data provider for the activity. |
facility | Facility? | Full facility record this activity was attributed to, hydrated client-side (best-effort; null when unavailable). |
Get Activities
Retrieve the monthly summary and activity list for a given month and year.
- Android (Kotlin)
- iOS (Swift)
lifecycleScope.launch {
try {
val result = AFCore.activities().get(month = 2, year = 2026)
// Display monthly progress
result.monthlySummary?.let { summary ->
progressBar.max = summary.acceptedVisitGoalDays
progressBar.progress = summary.acceptedVisitDays
summaryLabel.text = "${summary.acceptedActivities} accepted, " +
"${summary.pendingActivities} pending, " +
"${summary.rejectedActivities} rejected"
}
// Group activities by date for a calendar view
val activitiesByDate = result.activities
.groupBy { it.date?.substringBefore("T") }
calendarAdapter.submitGrouped(activitiesByDate)
} catch (e: Exception) {
showError("Could not load activities: ${e.message}")
}
}
do {
let result = try await AFCore.shared.activities().get(month: 2, year: 2026)
// Display monthly progress
if let summary = result.monthlySummary {
progressView.total = summary.acceptedVisitGoalDays
progressView.current = summary.acceptedVisitDays
}
// Group activities by date for a calendar view
let grouped = Dictionary(grouping: result.activities) { activity in
activity.date?.prefix(10) ?? "Unknown"
}
self.activitiesByDate = grouped
self.calendarView.reloadData()
} catch {
showError("Could not load activities: \(error)")
}
Rendering Activities by Type
Each activity type carries different metadata. Use the type field to determine which details to display.
// Android -- render an activity card based on type
fun renderActivity(activity: Activity) {
titleLabel.text = activity.name
dateLabel.text = activity.date
statusIcon.setImageResource(statusIconFor(activity.statusType))
when (activity.type) {
ActivityType.GYM_VISIT_ACTIVITY -> {
subtitleLabel.text = activity.metaData?.facilityName
detailLabel.text = "${activity.metaData?.timeSpent ?: 0} min"
}
ActivityType.SMART_WALKING_ACTIVITY -> {
subtitleLabel.text = "${activity.metaData?.steps ?: 0} steps"
detailLabel.text = "${activity.metaData?.calories ?: 0} cal"
}
ActivityType.SELF_REPORTED_ACTIVITY -> {
subtitleLabel.text = activity.metaData?.facilityName
detailLabel.text = "Self-reported"
}
ActivityType.HOME_ACTIVITY -> {
subtitleLabel.text = "At-home workout"
detailLabel.text = activity.metaData?.details
}
else -> {
subtitleLabel.text = activity.message
}
}
}
Submitting an Automatic Visit
Record a visit that your app detected automatically (e.g., from a geofence dwell). submitAutomaticVisit returns an AFResult — check result.status. If the device is offline, the submission is accepted into the offline outbox and result.queued is true; it will deliver later with no manual retry needed.
- Android (Kotlin)
- iOS (Swift)
lifecycleScope.launch {
try {
val result = AFCore.activities().submitAutomaticVisit(
visitType = VisitType.GEOFENCING,
timestampInSeconds = entryTimeSeconds,
latitude = facility.latitude,
longitude = facility.longitude,
facilityId = facility.id,
timeSpentInSeconds = dwellSeconds,
)
if (result.status) {
// result.queued == true means it was deferred to the offline outbox.
refreshVisibleMonth()
}
} catch (e: Throwable) {
showError("Could not submit visit: ${e.message}")
}
}
Task {
do {
let result = try await AFCore.shared.activities().submitAutomaticVisit(
visitType: .geofencing,
timestampInSeconds: entryTimeSeconds,
latitude: facility.latitude,
longitude: facility.longitude,
facilityId: facility.id,
timeSpentInSeconds: dwellSeconds
)
if result.status {
// result.queued == true means it was deferred to the offline outbox.
refreshVisibleMonth()
}
} catch {
showError("Could not submit visit: \(error)")
}
}
Deleting an Automatic Visit
Remove a previously submitted automatic visit by its date (e.g., the member indicates it was a mistake). Returns an AFResult — check result.status.
- Android (Kotlin)
- iOS (Swift)
lifecycleScope.launch {
try {
val result = AFCore.activities().deleteAutomaticVisit(date = "2025-08-01")
if (result.status) refreshVisibleMonth() // remove the item from the list
} catch (e: Throwable) {
showError("Could not delete visit: ${e.message}")
}
}
Task {
do {
let result = try await AFCore.shared.activities().deleteAutomaticVisit(date: "2025-08-01")
if result.status { refreshVisibleMonth() } // remove the item from the list
} catch {
showError("Could not delete visit: \(error)")
}
}
Observing Activity Changes
The activityEvents stream emits a MemberActivityEvent whenever the backend confirms a calendar change — a SmartWalking sync, a member visit, a self-report, or a Fit@Home selfie being added or removed. Use it as a "refresh now" signal: re-fetch the visible month on each emission.
activityEvents is a SharedFlow(replay = 0). A subscriber that attaches after an event fired does not receive that past event on attach, so always pair subscription with a fresh get() to capture the current state. Offline submissions (AFResult.queued == true) do not fire an event immediately — the event fires once the outbox flush delivers the submission and the server acknowledges it.
- Android (Kotlin)
- iOS (Swift)
lifecycleScope.launch {
AFCore.activities().activityEvents.collect { event ->
when (event) {
is MemberActivityEvent.StepsAdded,
is MemberActivityEvent.MemberVisitAdded,
is MemberActivityEvent.MemberVisitRemoved,
is MemberActivityEvent.SelfReportAdded,
is MemberActivityEvent.SelfReportRemoved,
is MemberActivityEvent.SelfieAdded,
is MemberActivityEvent.SelfieRemoved -> refreshVisibleMonth()
}
}
}
Use the callback-based subscribeToActivityEvents bridge so you don't need Kotlin Flow interop. Call close() on the returned subscription to stop receiving events.
let sub = AFCore.shared.activities().subscribeToActivityEvents(
onEvent: { event in self.refreshVisibleMonth() },
onError: { error in print("Error: \(error)") }
)
// Later, in deinit / viewDidDisappear:
sub.close()
Best Practices
- Query one month at a time. The API is calendar-based. To build a scrollable history, load each month as the user navigates backward.
- Color-code by status. Use distinct colors for accepted (green), pending (yellow), and rejected (red) activities in calendar and list views.
- Treat APPROVED, ACCEPTED, and COMPLETED as equivalent. The server may return any of these for verified activities. Normalize them in your UI logic.
- Show rejection reasons. When
statusTypeisDENIEDorREJECTED, displayactivity.messageso the member understands why. - Members may delete only pending activities. Accepted, rejected, and SmartWalking entries are non-deletable — the backend enforces this. Hide or disable delete affordances for those entries in your UI.
- Pull-to-refresh the current month, since activities can change as visits are verified or step data is processed.
Quick Reference
- Android (Kotlin)
- iOS (Swift)
// Android
AFCore.activities().get(month = 2, year = 2026) // Returns AFActivitiesSummary
// iOS
try await AFCore.shared.activities().get(month: 2, year: 2026)