[ios] add icons to the live activity widget

Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
Kiryl Kaveryn 2025-03-04 17:12:41 +04:00
parent dba6d30337
commit 0435b6eb2a
26 changed files with 212 additions and 53 deletions

View file

@ -55,12 +55,20 @@ private extension TrackRecordingLiveActivityAttributes.ContentState {
let ascent = AltitudeFormatter.altitudeString(fromMeters: Double(trackInfo.ascent))
let descent = AltitudeFormatter.altitudeString(fromMeters: Double(trackInfo.descent))
self.distance = StatisticsViewModel(key: "", value: distance)
self.duration = StatisticsViewModel(key: "", value: duration)
self.maxElevation = StatisticsViewModel(key: L("elevation_profile_max_elevation"), value: maxElevation)
self.minElevation = StatisticsViewModel(key: L("elevation_profile_min_elevation"), value: minElevation)
self.ascent = StatisticsViewModel(key: L("elevation_profile_ascent"), value: ascent)
self.descent = StatisticsViewModel(key: L("elevation_profile_descent"), value: descent)
self.distance = ValueViewModel(value: distance)
self.duration = ValueViewModel(value: duration)
self.ascent = DetailViewModel(.ascent, value: ascent)
self.descent = DetailViewModel(.descent, value: descent)
self.minElevation = DetailViewModel(.minElevation, value: minElevation)
self.maxElevation = DetailViewModel(.maxElevation, value: maxElevation)
}
}
private extension TrackRecordingLiveActivityAttributes.ContentState.DetailViewModel {
init(_ elevationDescription: ElevationDescription, value: String) {
self.value = value
self.key = elevationDescription.title
self.imageName = elevationDescription.imageName
}
}

View file

@ -519,6 +519,7 @@
ED810EC52D566E9B00ECDE2C /* SearchOnMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED810EC42D566E9B00ECDE2C /* SearchOnMapTests.swift */; };
ED8270F02C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */; };
ED83880F2D54DEB3002A0536 /* UIImage+FilledWithColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED83880E2D54DEA4002A0536 /* UIImage+FilledWithColor.swift */; };
ED8A921C2D772904009E063B /* ElevationDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8A921B2D772904009E063B /* ElevationDescription.swift */; };
ED914AB22D35063A00973C45 /* TextColorStyleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED914AB12D35063A00973C45 /* TextColorStyleSheet.swift */; };
ED914AB82D351DF000973C45 /* StyleApplicable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED914AB72D351DF000973C45 /* StyleApplicable.swift */; };
ED914ABE2D351FF800973C45 /* UILabel+SetFontStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED914ABD2D351FF800973C45 /* UILabel+SetFontStyle.swift */; };
@ -1487,6 +1488,7 @@
ED810EC42D566E9B00ECDE2C /* SearchOnMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapTests.swift; sourceTree = "<group>"; };
ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewDetailedSwitchCell.swift; sourceTree = "<group>"; };
ED83880E2D54DEA4002A0536 /* UIImage+FilledWithColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+FilledWithColor.swift"; sourceTree = "<group>"; };
ED8A921B2D772904009E063B /* ElevationDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationDescription.swift; sourceTree = "<group>"; };
ED914AB12D35063A00973C45 /* TextColorStyleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextColorStyleSheet.swift; sourceTree = "<group>"; };
ED914AB72D351DF000973C45 /* StyleApplicable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyleApplicable.swift; sourceTree = "<group>"; };
ED914ABD2D351FF800973C45 /* UILabel+SetFontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+SetFontStyle.swift"; sourceTree = "<group>"; };
@ -3056,6 +3058,7 @@
99DEF9D523E420D2006BFD21 /* ElevationProfile */ = {
isa = PBXGroup;
children = (
ED8A921B2D772904009E063B /* ElevationDescription.swift */,
99514BB223E82B450085D3A7 /* ElevationProfilePresenter.swift */,
EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */,
99514BB423E82B450085D3A7 /* ElevationProfileViewController.swift */,
@ -4502,6 +4505,7 @@
99F3EB1223F418C900C713F8 /* PlacePageInteractor.swift in Sources */,
340708651F2905A500029ECC /* NavigationInfoArea.swift in Sources */,
993DF0CC23F6BD0600AC231A /* ElevationDetailsPresenter.swift in Sources */,
ED8A921C2D772904009E063B /* ElevationDescription.swift in Sources */,
34AB666B1FC5AA330078E451 /* TransportTransitCell.swift in Sources */,
ED2E32912D10501700807A08 /* TrackRecordingButtonViewController.swift in Sources */,
47E8163323B17734008FD836 /* MWMStorage+UI.m in Sources */,

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "Ascent.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Ascent@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Ascent@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "Descent.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Descent@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Descent@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "maxAltitude.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "maxAltitude@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "maxAltitude@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "minAltitude.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "minAltitude@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "minAltitude@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

View file

@ -1,23 +1,24 @@
import SwiftUI
struct StatisticDetailView: View {
private let title: String
private let subtitle: String
init(title: String, subtitle: String) {
self.title = title
self.subtitle = subtitle
}
let viewModel: TrackRecordingLiveActivityAttributes.ContentState.DetailViewModel
var body: some View {
VStack(alignment: .leading) {
Text(title)
.contentTransition(.numericText())
.font(.system(size: 14, weight: .bold).monospacedDigit())
.minimumScaleFactor(0.5)
.lineLimit(1)
.foregroundStyle(.white)
Text(subtitle)
HStack {
Image(uiImage: UIImage(named: viewModel.imageName)!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundStyle(.white)
Text(viewModel.value)
.contentTransition(.numericText())
.font(.system(size: 14, weight: .bold).monospacedDigit())
.minimumScaleFactor(0.5)
.lineLimit(1)
.foregroundStyle(.white)
}
Text(viewModel.key)
.font(.system(size: 10))
.minimumScaleFactor(0.5)
.lineLimit(2)

View file

@ -1,14 +1,10 @@
import SwiftUI
struct StatisticValueView: View {
private let value: String
init(_ value: String) {
self.value = value
}
let viewModel: TrackRecordingLiveActivityAttributes.ContentState.ValueViewModel
var body: some View {
Text(value)
Text(viewModel.value)
.contentTransition(.numericText())
.minimumScaleFactor(0.1)
.font(.title3.bold().monospacedDigit())

View file

@ -3,15 +3,21 @@ import AppIntents
struct TrackRecordingLiveActivityAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
struct StatisticsViewModel: Codable, Hashable {
let key: String
struct ValueViewModel: Codable, Hashable {
let value: String
}
let duration: StatisticsViewModel
let distance: StatisticsViewModel
let ascent: StatisticsViewModel
let descent: StatisticsViewModel
let maxElevation: StatisticsViewModel
let minElevation: StatisticsViewModel
struct DetailViewModel: Codable, Hashable {
let key: String
let value: String
let imageName: String
}
let duration: ValueViewModel
let distance: ValueViewModel
let ascent: DetailViewModel
let descent: DetailViewModel
let maxElevation: DetailViewModel
let minElevation: DetailViewModel
}
}

View file

@ -15,7 +15,7 @@ struct TrackRecordingLiveActivityConfiguration: Widget {
} compactLeading: {
AppLogo()
} compactTrailing: {
StatisticValueView(context.state.duration.value)
StatisticValueView(viewModel: context.state.duration)
} minimal: {
// TODO: Implement the minimal view
}

View file

@ -9,18 +9,18 @@ struct TrackRecordingLiveActivityView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 24) {
StatisticValueView(state.duration.value)
StatisticValueView(state.distance.value)
StatisticValueView(viewModel: state.duration)
StatisticValueView(viewModel: state.distance)
Spacer()
AppLogo()
.frame(width: 20, height: 20)
}
.padding([.top, .leading, .trailing], 16)
HStack(alignment: .top, spacing: 20) {
StatisticDetailView(title: state.ascent.value, subtitle: state.ascent.key)
StatisticDetailView(title: state.descent.value, subtitle: state.descent.key)
StatisticDetailView(title: state.maxElevation.value, subtitle: state.maxElevation.key)
StatisticDetailView(title: state.minElevation.value, subtitle: state.minElevation.key)
StatisticDetailView(viewModel: state.ascent)
StatisticDetailView(viewModel: state.descent)
StatisticDetailView(viewModel: state.maxElevation)
StatisticDetailView(viewModel: state.minElevation)
}
.padding([.leading, .trailing, .bottom], 16)
}
@ -37,28 +37,30 @@ struct TrackRecordingLiveActivityWidget_Previews: PreviewProvider {
let activityAttributes = TrackRecordingLiveActivityAttributes()
let activityState = TrackRecordingLiveActivityAttributes.ContentState(
duration: .init(
key: "Duration",
value: "1h 12min"
value: "10d 10h 12min"
),
distance: .init(
key: "Distance",
value: "12 km"
value: "1245 km"
),
ascent: .init(
key: "Ascent",
value: "100 m"
value: "100 m",
imageName: "ic_em_ascent_24"
),
descent: .init(
key: "Descent",
value: "100 m"
value: "100 m",
imageName: "ic_em_descent_24"
),
maxElevation: .init(
key: "Max Elevation",
value: "100 m"
value: "9999 m",
imageName: "ic_em_max_attitude_24"
),
minElevation: .init(
key: "Min Elevation",
value: "10 m"
value: "1000 m",
imageName: "ic_em_min_attitude_24"
)
)

View file

@ -0,0 +1,26 @@
enum ElevationDescription {
case ascent
case descent
case maxElevation
case minElevation
}
extension ElevationDescription {
var title: String {
switch self {
case .ascent: return L("elevation_profile_ascent")
case .descent: return L("elevation_profile_descent")
case .maxElevation: return L("elevation_profile_max_elevation")
case .minElevation: return L("elevation_profile_min_elevation")
}
}
var imageName: String {
switch self {
case .ascent: return "ic_em_ascent_24"
case .descent: return "ic_em_descent_24"
case .maxElevation: return "ic_em_max_attitude_24"
case .minElevation: return "ic_em_min_attitude_24"
}
}
}

View file

@ -18,6 +18,12 @@ fileprivate struct DescriptionsViewModel {
let title: String
let value: UInt
let imageName: String
init(_ description: ElevationDescription, value: UInt) {
self.title = description.title
self.value = value
self.imageName = description.imageName
}
}
final class ElevationProfilePresenter: NSObject {
@ -50,10 +56,10 @@ final class ElevationProfilePresenter: NSObject {
private static func descriptionModels(for trackInfo: TrackInfo) -> [DescriptionsViewModel] {
[
DescriptionsViewModel(title: L("elevation_profile_ascent"), value: trackInfo.ascent, imageName: "ic_em_ascent_24"),
DescriptionsViewModel(title: L("elevation_profile_descent"), value: trackInfo.descent, imageName: "ic_em_descent_24"),
DescriptionsViewModel(title: L("elevation_profile_max_elevation"), value: trackInfo.maxElevation, imageName: "ic_em_max_attitude_24"),
DescriptionsViewModel(title: L("elevation_profile_min_elevation"), value: trackInfo.minElevation, imageName: "ic_em_min_attitude_24")
DescriptionsViewModel(.ascent, value: trackInfo.ascent),
DescriptionsViewModel(.descent, value: trackInfo.descent),
DescriptionsViewModel(.minElevation, value: trackInfo.maxElevation),
DescriptionsViewModel(.maxElevation, value: trackInfo.minElevation)
]
}