Improve goal sharing experience

This commit is contained in:
alexandrev-tibco
2026-02-01 11:14:48 +01:00
parent e328767c4a
commit f97f8026bc
7 changed files with 886 additions and 78 deletions
@@ -5,22 +5,39 @@ struct GoalShareCardView: View {
let progress: Double
let currentValue: Decimal
let targetValue: Decimal
var targetDate: Date? = nil
var estimatedCompletionDate: Date? = nil
var privacyMode: Bool = false
var qrCodeImage: UIImage? = nil
private var hasExtraContent: Bool {
targetDate != nil || estimatedCompletionDate != nil
}
private var cardHeight: CGFloat {
let base: CGFloat = hasExtraContent ? 340 : 290
return privacyMode ? base + 10 : base
}
private var displayCurrentValue: String {
privacyMode ? "***" : currentValue.currencyString
}
private var progressText: String {
let percent = Int((progress * 100).rounded())
return "\(percent)%"
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Goal Progress")
.font(.caption.weight(.semibold))
.foregroundColor(.white.opacity(0.8))
Text(name)
.font(.title2.weight(.bold))
.foregroundColor(.white)
}
Spacer()
Image(systemName: "sparkles")
.font(.title2)
.foregroundColor(.white.opacity(0.9))
VStack(alignment: .leading, spacing: 14) {
// Header with goal name
VStack(alignment: .leading, spacing: 4) {
Text("Goal Progress")
.font(.caption.weight(.semibold))
.foregroundColor(.white.opacity(0.8))
Text(name)
.font(.title2.weight(.bold))
.foregroundColor(.white)
}
GoalProgressBar(
@@ -32,25 +49,118 @@ struct GoalShareCardView: View {
)
.frame(height: 10)
HStack {
Text(currentValue.currencyString)
.font(.headline)
.foregroundColor(.white)
Spacer()
Text("of \(targetValue.currencyString)")
.font(.subheadline.weight(.medium))
.foregroundColor(.white.opacity(0.8))
Text("\(progressText) complete")
.font(.caption.weight(.semibold))
.foregroundColor(.white.opacity(0.85))
if privacyMode {
HStack {
Text("Progress")
.font(.subheadline.weight(.semibold))
.foregroundColor(.white.opacity(0.9))
Spacer()
Text(progressText)
.font(.headline.weight(.bold))
.foregroundColor(.white)
}
} else {
HStack {
Text(displayCurrentValue)
.font(.headline)
.foregroundColor(.white)
Spacer()
Text("of \(targetValue.currencyString)")
.font(.subheadline.weight(.medium))
.foregroundColor(.white.opacity(0.8))
}
}
HStack {
Image(systemName: "arrow.up.right")
Text("Track yours in Portfolio Journal")
if let targetDate {
HStack(spacing: 6) {
Image(systemName: "calendar")
.font(.caption)
Text("Target: \(targetDate.mediumDateString)")
.font(.caption.weight(.medium))
}
.foregroundColor(.white.opacity(0.85))
}
if let estimatedCompletionDate {
HStack(spacing: 6) {
Image(systemName: "chart.line.uptrend.xyaxis")
.font(.caption)
Text("Est. completion: \(estimatedCompletionDate.mediumDateString)")
.font(.caption.weight(.medium))
}
.foregroundColor(.white.opacity(0.85))
}
if privacyMode {
HStack(spacing: 6) {
Image(systemName: "eye.slash")
Text("Privacy mode enabled")
}
.font(.caption.weight(.semibold))
.foregroundColor(.white.opacity(0.7))
}
Spacer(minLength: 0)
// Branding footer with QR code
VStack(spacing: 10) {
// Divider line
Rectangle()
.fill(Color.white.opacity(0.2))
.frame(height: 1)
HStack(spacing: 12) {
// App icon and branding
Image("BrandMark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.clipShape(RoundedRectangle(cornerRadius: 9, style: .continuous))
VStack(alignment: .leading, spacing: 2) {
Text("Powered by")
.font(.caption2.weight(.medium))
.foregroundColor(.white.opacity(0.7))
Text("Portfolio Journal")
.font(.subheadline.weight(.bold))
.foregroundColor(.white)
}
Spacer()
// QR Code
if let qrCodeImage {
VStack(spacing: 4) {
Image(uiImage: qrCodeImage)
.interpolation(.none)
.resizable()
.frame(width: 50, height: 50)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
Text("Scan to download")
.font(.system(size: 7, weight: .medium))
.foregroundColor(.white.opacity(0.8))
}
} else {
// Fallback if QR code generation fails
VStack(spacing: 2) {
Image(systemName: "qrcode")
.font(.system(size: 28))
Text("App Store")
.font(.caption2.weight(.semibold))
}
.foregroundColor(.white.opacity(0.9))
}
}
}
.font(.caption.weight(.semibold))
.foregroundColor(.white.opacity(0.85))
}
.padding(24)
.frame(width: 320, height: 220)
.padding(20)
.frame(width: 320, height: cardHeight)
.background(
LinearGradient(
colors: [Color.appPrimary, Color.appSecondary],
@@ -74,3 +184,24 @@ struct GoalShareCardView: View {
targetValue: 1_000_000
)
}
#Preview("With Dates") {
GoalShareCardView(
name: "1M Goal",
progress: 0.42,
currentValue: 420_000,
targetValue: 1_000_000,
targetDate: Date().addingTimeInterval(365 * 24 * 60 * 60),
estimatedCompletionDate: Date().addingTimeInterval(300 * 24 * 60 * 60)
)
}
#Preview("Privacy Mode") {
GoalShareCardView(
name: "1M Goal",
progress: 0.42,
currentValue: 420_000,
targetValue: 1_000_000,
privacyMode: true
)
}
+67 -41
View File
@@ -21,12 +21,10 @@ struct GoalsView: View {
goal: goal,
progress: viewModel.progress(for: goal),
totalValue: viewModel.totalValue(for: goal),
paceStatus: viewModel.paceStatus(for: goal)
paceStatus: viewModel.paceStatus(for: goal),
estimatedCompletionDate: viewModel.estimateCompletionDate(for: goal),
onEdit: { editingGoal = goal }
)
.contentShape(Rectangle())
.onTapGesture {
editingGoal = goal
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
viewModel.deleteGoal(goal)
@@ -102,50 +100,78 @@ struct GoalRowView: View {
let progress: Double
let totalValue: Decimal
let paceStatus: GoalPaceStatus?
let estimatedCompletionDate: Date?
let onEdit: () -> Void
@State private var showingShareOptions = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(goal.name)
.font(.headline)
Spacer()
Button {
GoalShareService.shared.shareGoal(
name: goal.name,
progress: progress,
currentValue: totalValue,
targetValue: goal.targetDecimal
)
} label: {
Image(systemName: "square.and.arrow.up")
.foregroundColor(.appPrimary)
ZStack(alignment: .topTrailing) {
Button(action: onEdit) {
VStack(alignment: .leading, spacing: 12) {
Text(goal.name)
.font(.headline)
GoalProgressBar(progress: progress, tint: .appSecondary, iconColor: .appSecondary)
HStack {
Text(totalValue.currencyString)
.font(.subheadline.weight(.semibold))
Spacer()
Text("of \(goal.targetDecimal.currencyString)")
.font(.subheadline)
.foregroundColor(.secondary)
}
if let targetDate = goal.targetDate {
Text("Target date: \(targetDate.mediumDateString)")
.font(.caption)
.foregroundColor(.secondary)
}
if let paceStatus {
Text(paceStatus.statusText)
.font(.caption.weight(.semibold))
.foregroundColor(paceStatus.isBehind ? .negativeRed : .positiveGreen)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.buttonStyle(.plain)
GoalProgressBar(progress: progress, tint: .appSecondary, iconColor: .appSecondary)
HStack {
Text(totalValue.currencyString)
.font(.subheadline.weight(.semibold))
Spacer()
Text("of \(goal.targetDecimal.currencyString)")
.font(.subheadline)
.foregroundColor(.secondary)
}
if let targetDate = goal.targetDate {
Text("Target date: \(targetDate.mediumDateString)")
.font(.caption)
.foregroundColor(.secondary)
}
if let paceStatus {
Text(paceStatus.statusText)
.font(.caption.weight(.semibold))
.foregroundColor(paceStatus.isBehind ? .negativeRed : .positiveGreen)
Button {
showingShareOptions = true
} label: {
Image(systemName: "square.and.arrow.up")
.foregroundColor(.appPrimary)
.padding(.top, 2)
}
.buttonStyle(.borderless)
}
.padding(.vertical, 8)
.confirmationDialog("Share Goal", isPresented: $showingShareOptions, titleVisibility: .visible) {
Button("Share with amounts") {
shareGoal(privacyMode: false)
}
Button("Share (privacy mode)") {
shareGoal(privacyMode: true)
}
Button("Cancel", role: .cancel) {}
} message: {
Text("Choose how to share your goal progress")
}
}
private func shareGoal(privacyMode: Bool) {
GoalShareService.shared.shareGoal(
name: goal.name,
progress: progress,
currentValue: totalValue,
targetValue: goal.targetDecimal,
targetDate: goal.targetDate,
estimatedCompletionDate: estimatedCompletionDate,
privacyMode: privacyMode
)
}
}