feat: redesign all share cards, remove unused achievement types, fix sport selector
Redesign trip, progress, and achievement share cards with premium sports-media aesthetic. Remove unused milestone/context achievement card types (only used in debug exporter). Fix gold text unreadable in light mode. Fix sport selector to only show stroke on selected sport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// ShareCardComponents.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Reusable components for share cards: header, footer, stats row, map snapshot.
|
||||
// Shared building blocks for the new shareable card system.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
@@ -16,15 +16,7 @@ struct ShareCardBackground: View {
|
||||
var sports: Set<Sport>? = nil
|
||||
|
||||
var body: some View {
|
||||
if let sports = sports, !sports.isEmpty {
|
||||
ShareCardSportBackground(sports: sports, theme: theme)
|
||||
} else {
|
||||
LinearGradient(
|
||||
colors: theme.gradientColors,
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
ShareCardSportBackground(sports: sports ?? [], theme: theme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,24 +28,62 @@ struct ShareCardHeader: View {
|
||||
let theme: ShareTheme
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
if let sport = sport {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(theme.accentColor.opacity(0.2))
|
||||
.frame(width: 80, height: 80)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack(alignment: .center, spacing: 18) {
|
||||
if let sport = sport {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(theme.accentColor.opacity(0.22))
|
||||
.frame(width: 88, height: 88)
|
||||
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.system(size: 40))
|
||||
.foregroundStyle(theme.accentColor)
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(theme.accentColor.opacity(0.65), lineWidth: 1.5)
|
||||
.frame(width: 88, height: 88)
|
||||
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.system(size: 40, weight: .bold))
|
||||
.foregroundStyle(theme.accentColor)
|
||||
}
|
||||
.shadow(color: .black.opacity(0.22), radius: 10, y: 6)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
Text("SPORTSTIME")
|
||||
.font(.system(size: 15, weight: .bold))
|
||||
.tracking(5)
|
||||
.foregroundStyle(theme.secondaryTextColor)
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: 47, weight: .black, design: .default))
|
||||
.foregroundStyle(theme.textColor)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.65)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: 48, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(theme.textColor)
|
||||
.multilineTextAlignment(.center)
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: theme.highlightGradient,
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.frame(height: 4)
|
||||
}
|
||||
.padding(.horizontal, 26)
|
||||
.padding(.vertical, 22)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 30)
|
||||
.fill(theme.surfaceColor)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 30)
|
||||
.stroke(theme.borderColor, lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,19 +93,32 @@ struct ShareCardFooter: View {
|
||||
let theme: ShareTheme
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.font(.system(size: 20))
|
||||
Text("SportsTime")
|
||||
.font(.system(size: 24, weight: .semibold))
|
||||
}
|
||||
.foregroundStyle(theme.accentColor)
|
||||
HStack(spacing: 14) {
|
||||
Text("SPORTSTIME")
|
||||
.font(.system(size: 16, weight: .black))
|
||||
.tracking(3.5)
|
||||
.foregroundStyle(theme.accentColor)
|
||||
|
||||
Text("Plan your stadium adventure")
|
||||
.font(.system(size: 18))
|
||||
Capsule()
|
||||
.fill(theme.borderColor)
|
||||
.frame(width: 26, height: 2)
|
||||
|
||||
Text("build your next game-day route")
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundStyle(theme.secondaryTextColor)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(theme.surfaceColor)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(theme.borderColor, lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,25 +129,33 @@ struct ShareStatsRow: View {
|
||||
let theme: ShareTheme
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 60) {
|
||||
HStack(spacing: 14) {
|
||||
ForEach(Array(stats.enumerated()), id: \.offset) { _, stat in
|
||||
VStack(spacing: 8) {
|
||||
VStack(spacing: 7) {
|
||||
Text(stat.value)
|
||||
.font(.system(size: 36, weight: .bold, design: .rounded))
|
||||
.font(.system(size: 44, weight: .black, design: .rounded))
|
||||
.foregroundStyle(theme.accentColor)
|
||||
.minimumScaleFactor(0.7)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(stat.label)
|
||||
.font(.system(size: 20))
|
||||
Text(stat.label.uppercased())
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.tracking(2.2)
|
||||
.foregroundStyle(theme.secondaryTextColor)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 18)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.fill(theme.surfaceColor)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(theme.borderColor, lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 30)
|
||||
.padding(.horizontal, 40)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(theme.textColor.opacity(0.05))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,42 +165,54 @@ struct ShareProgressRing: View {
|
||||
let current: Int
|
||||
let total: Int
|
||||
let theme: ShareTheme
|
||||
var size: CGFloat = 320
|
||||
var lineWidth: CGFloat = 24
|
||||
var size: CGFloat = 340
|
||||
var lineWidth: CGFloat = 30
|
||||
|
||||
private let segmentCount = 72
|
||||
|
||||
private var progress: Double {
|
||||
guard total > 0 else { return 0 }
|
||||
return Double(current) / Double(total)
|
||||
return min(max(Double(current) / Double(total), 0), 1)
|
||||
}
|
||||
|
||||
private var filledSegments: Int {
|
||||
Int(round(progress * Double(segmentCount)))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background ring
|
||||
Circle()
|
||||
.stroke(theme.accentColor.opacity(0.2), lineWidth: lineWidth)
|
||||
.frame(width: size, height: size)
|
||||
ForEach(0..<segmentCount, id: \.self) { index in
|
||||
Capsule()
|
||||
.fill(index < filledSegments ? theme.accentColor : theme.surfaceColor.opacity(0.90))
|
||||
.frame(width: lineWidth * 0.62, height: lineWidth * 1.18)
|
||||
.offset(y: -(size / 2 - lineWidth * 0.78))
|
||||
.rotationEffect(.degrees(Double(index) * 360 / Double(segmentCount)))
|
||||
}
|
||||
|
||||
// Progress ring
|
||||
Circle()
|
||||
.trim(from: 0, to: progress)
|
||||
.stroke(
|
||||
theme.accentColor,
|
||||
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
|
||||
)
|
||||
.frame(width: size, height: size)
|
||||
.rotationEffect(.degrees(-90))
|
||||
ForEach([0.25, 0.50, 0.75], id: \.self) { mark in
|
||||
Circle()
|
||||
.fill(theme.textColor.opacity(0.32))
|
||||
.frame(width: 8, height: 8)
|
||||
.offset(y: -(size / 2 - lineWidth * 0.20))
|
||||
.rotationEffect(.degrees(mark * 360))
|
||||
}
|
||||
|
||||
// Center content
|
||||
VStack(spacing: 8) {
|
||||
Circle()
|
||||
.stroke(theme.glowColor.opacity(0.45), lineWidth: 14)
|
||||
.blur(radius: 12)
|
||||
.frame(width: size - lineWidth * 1.4, height: size - lineWidth * 1.4)
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text("\(current)")
|
||||
.font(.system(size: 96, weight: .bold, design: .rounded))
|
||||
.font(.system(size: 108, weight: .black, design: .rounded))
|
||||
.foregroundStyle(theme.textColor)
|
||||
|
||||
Text("of \(total)")
|
||||
.font(.system(size: 32, weight: .medium))
|
||||
.font(.system(size: 27, weight: .light))
|
||||
.foregroundStyle(theme.secondaryTextColor)
|
||||
}
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +221,6 @@ struct ShareProgressRing: View {
|
||||
@MainActor
|
||||
final class ShareMapSnapshotGenerator {
|
||||
|
||||
/// Generate a progress map showing visited/remaining stadiums
|
||||
func generateProgressMap(
|
||||
visited: [Stadium],
|
||||
remaining: [Stadium],
|
||||
@@ -167,9 +229,8 @@ final class ShareMapSnapshotGenerator {
|
||||
let allStadiums = visited + remaining
|
||||
guard !allStadiums.isEmpty else { return nil }
|
||||
|
||||
let region = calculateRegion(for: allStadiums)
|
||||
let options = MKMapSnapshotter.Options()
|
||||
options.region = region
|
||||
options.region = calculateRegion(for: allStadiums)
|
||||
options.size = ShareCardDimensions.mapSnapshotSize
|
||||
options.mapType = theme.useDarkMap ? .mutedStandard : .standard
|
||||
|
||||
@@ -188,19 +249,15 @@ final class ShareMapSnapshotGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a route map for trip cards
|
||||
func generateRouteMap(
|
||||
stops: [TripStop],
|
||||
theme: ShareTheme
|
||||
) async -> UIImage? {
|
||||
let stopsWithCoordinates = stops.filter { $0.coordinate != nil }
|
||||
guard stopsWithCoordinates.count >= 2 else { return nil }
|
||||
let validStops = stops.filter { $0.coordinate != nil }
|
||||
guard validStops.count >= 2 else { return nil }
|
||||
|
||||
let coordinates = stopsWithCoordinates.compactMap { $0.coordinate }
|
||||
|
||||
let region = calculateRegion(for: coordinates)
|
||||
let options = MKMapSnapshotter.Options()
|
||||
options.region = region
|
||||
options.region = calculateRegion(for: validStops.compactMap { $0.coordinate })
|
||||
options.size = ShareCardDimensions.routeMapSize
|
||||
options.mapType = theme.useDarkMap ? .mutedStandard : .standard
|
||||
|
||||
@@ -208,18 +265,12 @@ final class ShareMapSnapshotGenerator {
|
||||
|
||||
do {
|
||||
let snapshot = try await snapshotter.start()
|
||||
return drawRoute(
|
||||
on: snapshot,
|
||||
stops: stopsWithCoordinates,
|
||||
accentColor: UIColor(theme.accentColor)
|
||||
)
|
||||
return drawRoute(on: snapshot, stops: validStops, accentColor: UIColor(theme.accentColor))
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private func calculateRegion(for stadiums: [Stadium]) -> MKCoordinateRegion {
|
||||
let coordinates = stadiums.map {
|
||||
CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude)
|
||||
@@ -233,17 +284,16 @@ final class ShareMapSnapshotGenerator {
|
||||
let minLon = coordinates.map(\.longitude).min() ?? 0
|
||||
let maxLon = coordinates.map(\.longitude).max() ?? 0
|
||||
|
||||
let center = CLLocationCoordinate2D(
|
||||
latitude: (minLat + maxLat) / 2,
|
||||
longitude: (minLon + maxLon) / 2
|
||||
return MKCoordinateRegion(
|
||||
center: CLLocationCoordinate2D(
|
||||
latitude: (minLat + maxLat) / 2,
|
||||
longitude: (minLon + maxLon) / 2
|
||||
),
|
||||
span: MKCoordinateSpan(
|
||||
latitudeDelta: max((maxLat - minLat) * 1.35, 1),
|
||||
longitudeDelta: max((maxLon - minLon) * 1.35, 1)
|
||||
)
|
||||
)
|
||||
|
||||
let span = MKCoordinateSpan(
|
||||
latitudeDelta: max((maxLat - minLat) * 1.4, 1),
|
||||
longitudeDelta: max((maxLon - minLon) * 1.4, 1)
|
||||
)
|
||||
|
||||
return MKCoordinateRegion(center: center, span: span)
|
||||
}
|
||||
|
||||
private func drawStadiumMarkers(
|
||||
@@ -252,26 +302,25 @@ final class ShareMapSnapshotGenerator {
|
||||
remaining: [Stadium],
|
||||
accentColor: UIColor
|
||||
) -> UIImage {
|
||||
let size = ShareCardDimensions.mapSnapshotSize
|
||||
return UIGraphicsImageRenderer(size: size).image { context in
|
||||
UIGraphicsImageRenderer(size: ShareCardDimensions.mapSnapshotSize).image { context in
|
||||
snapshot.image.draw(at: .zero)
|
||||
|
||||
// Draw remaining (gray) first
|
||||
for stadium in remaining {
|
||||
let point = snapshot.point(for: CLLocationCoordinate2D(
|
||||
latitude: stadium.latitude,
|
||||
longitude: stadium.longitude
|
||||
))
|
||||
drawMarker(at: point, color: .gray, context: context.cgContext)
|
||||
drawStadiumDot(
|
||||
at: snapshot.point(for: CLLocationCoordinate2D(latitude: stadium.latitude, longitude: stadium.longitude)),
|
||||
color: UIColor.systemGray3,
|
||||
visited: false,
|
||||
context: context.cgContext
|
||||
)
|
||||
}
|
||||
|
||||
// Draw visited (accent) on top
|
||||
for stadium in visited {
|
||||
let point = snapshot.point(for: CLLocationCoordinate2D(
|
||||
latitude: stadium.latitude,
|
||||
longitude: stadium.longitude
|
||||
))
|
||||
drawMarker(at: point, color: accentColor, context: context.cgContext)
|
||||
drawStadiumDot(
|
||||
at: snapshot.point(for: CLLocationCoordinate2D(latitude: stadium.latitude, longitude: stadium.longitude)),
|
||||
color: accentColor,
|
||||
visited: true,
|
||||
context: context.cgContext
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,126 +330,125 @@ final class ShareMapSnapshotGenerator {
|
||||
stops: [TripStop],
|
||||
accentColor: UIColor
|
||||
) -> UIImage {
|
||||
let size = ShareCardDimensions.routeMapSize
|
||||
return UIGraphicsImageRenderer(size: size).image { context in
|
||||
UIGraphicsImageRenderer(size: ShareCardDimensions.routeMapSize).image { context in
|
||||
snapshot.image.draw(at: .zero)
|
||||
|
||||
let cgContext = context.cgContext
|
||||
|
||||
// Draw route line
|
||||
cgContext.setStrokeColor(accentColor.cgColor)
|
||||
cgContext.setLineWidth(4)
|
||||
cgContext.setLineCap(.round)
|
||||
cgContext.setLineJoin(.round)
|
||||
|
||||
let cg = context.cgContext
|
||||
let points = stops.compactMap { stop -> CGPoint? in
|
||||
guard let coord = stop.coordinate else { return nil }
|
||||
return snapshot.point(for: coord)
|
||||
}
|
||||
|
||||
if let first = points.first {
|
||||
cgContext.move(to: first)
|
||||
cg.setLineCap(.round)
|
||||
cg.setLineJoin(.round)
|
||||
|
||||
cg.setStrokeColor(UIColor.black.withAlphaComponent(0.28).cgColor)
|
||||
cg.setLineWidth(11)
|
||||
cg.move(to: first)
|
||||
for point in points.dropFirst() {
|
||||
cgContext.addLine(to: point)
|
||||
cg.addLine(to: point)
|
||||
}
|
||||
cgContext.strokePath()
|
||||
cg.strokePath()
|
||||
|
||||
cg.setStrokeColor(accentColor.cgColor)
|
||||
cg.setLineWidth(6)
|
||||
cg.move(to: first)
|
||||
for point in points.dropFirst() {
|
||||
cg.addLine(to: point)
|
||||
}
|
||||
cg.strokePath()
|
||||
}
|
||||
|
||||
// Draw city markers
|
||||
for (index, stop) in stops.enumerated() {
|
||||
guard let coord = stop.coordinate else { continue }
|
||||
let point = snapshot.point(for: coord)
|
||||
drawCityMarker(
|
||||
let isFirst = index == 0
|
||||
let isLast = index == stops.count - 1
|
||||
|
||||
drawCityLabel(
|
||||
at: point,
|
||||
label: String(stop.city.prefix(3)).uppercased(),
|
||||
isFirst: index == 0,
|
||||
isLast: index == stops.count - 1,
|
||||
label: isFirst ? "START" : isLast ? "FINISH" : String(stop.city.prefix(3)).uppercased(),
|
||||
endpoint: isFirst || isLast,
|
||||
color: accentColor,
|
||||
context: cgContext
|
||||
context: cg
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func drawMarker(at point: CGPoint, color: UIColor, context: CGContext) {
|
||||
let markerSize: CGFloat = 16
|
||||
private func drawStadiumDot(
|
||||
at point: CGPoint,
|
||||
color: UIColor,
|
||||
visited: Bool,
|
||||
context: CGContext
|
||||
) {
|
||||
let size: CGFloat = 22
|
||||
|
||||
context.setFillColor(UIColor.black.withAlphaComponent(0.28).cgColor)
|
||||
context.fillEllipse(in: CGRect(x: point.x - size / 2 - 3, y: point.y - size / 2 + 2, width: size + 6, height: size + 6))
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(x: point.x - size / 2 - 2, y: point.y - size / 2 - 2, width: size + 4, height: size + 4))
|
||||
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fillEllipse(in: CGRect(
|
||||
x: point.x - markerSize / 2,
|
||||
y: point.y - markerSize / 2,
|
||||
width: markerSize,
|
||||
height: markerSize
|
||||
))
|
||||
context.fillEllipse(in: CGRect(x: point.x - size / 2, y: point.y - size / 2, width: size, height: size))
|
||||
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
context.setLineWidth(2)
|
||||
context.strokeEllipse(in: CGRect(
|
||||
x: point.x - markerSize / 2,
|
||||
y: point.y - markerSize / 2,
|
||||
width: markerSize,
|
||||
height: markerSize
|
||||
))
|
||||
if visited {
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
context.setLineWidth(2.6)
|
||||
context.setLineCap(.round)
|
||||
context.setLineJoin(.round)
|
||||
|
||||
context.move(to: CGPoint(x: point.x - 4.5, y: point.y + 0.5))
|
||||
context.addLine(to: CGPoint(x: point.x - 0.8, y: point.y + 4.6))
|
||||
context.addLine(to: CGPoint(x: point.x + 6.2, y: point.y - 3.2))
|
||||
context.strokePath()
|
||||
}
|
||||
}
|
||||
|
||||
private func drawCityMarker(
|
||||
private func drawCityLabel(
|
||||
at point: CGPoint,
|
||||
label: String,
|
||||
isFirst: Bool,
|
||||
isLast: Bool,
|
||||
endpoint: Bool,
|
||||
color: UIColor,
|
||||
context: CGContext
|
||||
) {
|
||||
let markerSize: CGFloat = isFirst || isLast ? 24 : 18
|
||||
let dotSize: CGFloat = endpoint ? 22 : 17
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(x: point.x - dotSize / 2 - 2, y: point.y - dotSize / 2 - 2, width: dotSize + 4, height: dotSize + 4))
|
||||
|
||||
// Outer circle
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fillEllipse(in: CGRect(
|
||||
x: point.x - markerSize / 2,
|
||||
y: point.y - markerSize / 2,
|
||||
width: markerSize,
|
||||
height: markerSize
|
||||
))
|
||||
context.fillEllipse(in: CGRect(x: point.x - dotSize / 2, y: point.y - dotSize / 2, width: dotSize, height: dotSize))
|
||||
|
||||
// White border
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
context.setLineWidth(3)
|
||||
context.strokeEllipse(in: CGRect(
|
||||
x: point.x - markerSize / 2,
|
||||
y: point.y - markerSize / 2,
|
||||
width: markerSize,
|
||||
height: markerSize
|
||||
))
|
||||
|
||||
// Label above marker
|
||||
let labelRect = CGRect(
|
||||
x: point.x - 30,
|
||||
y: point.y - markerSize / 2 - 22,
|
||||
width: 60,
|
||||
height: 20
|
||||
)
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = .center
|
||||
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFont.systemFont(ofSize: 12, weight: .bold),
|
||||
.foregroundColor: UIColor.white,
|
||||
.paragraphStyle: paragraphStyle
|
||||
let font = UIFont.systemFont(ofSize: endpoint ? 12.5 : 11, weight: .heavy)
|
||||
let attrs: [NSAttributedString.Key: Any] = [
|
||||
.font: font,
|
||||
.foregroundColor: UIColor.white
|
||||
]
|
||||
|
||||
// Draw label background
|
||||
let labelBgRect = CGRect(
|
||||
x: point.x - 22,
|
||||
y: point.y - markerSize / 2 - 24,
|
||||
width: 44,
|
||||
height: 18
|
||||
let textSize = (label as NSString).size(withAttributes: attrs)
|
||||
let bgRect = CGRect(
|
||||
x: point.x - textSize.width / 2 - 11,
|
||||
y: point.y - dotSize / 2 - textSize.height - 12,
|
||||
width: textSize.width + 22,
|
||||
height: textSize.height + 8
|
||||
)
|
||||
context.setFillColor(color.withAlphaComponent(0.9).cgColor)
|
||||
let path = UIBezierPath(roundedRect: labelBgRect, cornerRadius: 4)
|
||||
|
||||
let path = UIBezierPath(roundedRect: bgRect, cornerRadius: 9)
|
||||
context.setFillColor(color.withAlphaComponent(0.94).cgColor)
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
|
||||
label.draw(in: labelRect, withAttributes: attributes)
|
||||
(label as NSString).draw(
|
||||
in: CGRect(
|
||||
x: bgRect.origin.x + 11,
|
||||
y: bgRect.origin.y + 4,
|
||||
width: textSize.width,
|
||||
height: textSize.height
|
||||
),
|
||||
withAttributes: attrs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user