Add sharing style picker for design variation selection
Users can now swipe between design variations (e.g. Gradient vs Color Block) when sharing from month/year views and the sharing templates list. Removes #if DEBUG wrappers from variation files and disables auto-start demo animation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,54 +37,102 @@ struct SharingListView: View {
|
||||
|
||||
@MainActor
|
||||
init() {
|
||||
let earliestDate = DataController.shared.earliestEntry?.forDate ?? Date()
|
||||
let now = Date()
|
||||
let tenDaysAgo = Calendar.current.date(byAdding: .day, value: -10, to: now)!
|
||||
let monthStart = now.startOfMonth
|
||||
let monthEnd = now.endOfMonth
|
||||
|
||||
// — All Moods —
|
||||
let allMoodsEntries = DataController.shared.getData(startDate: earliestDate, endDate: now, includedDays: [1,2,3,4,5,6,7])
|
||||
let allMoodsMetrics = Random.createTotalPerc(fromEntries: allMoodsEntries)
|
||||
let allMoodsCount = allMoodsEntries.count
|
||||
|
||||
// — Current Streak (Last 10 Days) —
|
||||
let streakEntries = DataController.shared.getData(startDate: tenDaysAgo, endDate: now, includedDays: [1,2,3,4,5,6,7])
|
||||
|
||||
// — Month Total —
|
||||
let monthEntries = DataController.shared.getData(startDate: monthStart, endDate: monthEnd, includedDays: [1,2,3,4,5,6,7])
|
||||
let monthMetrics = Random.createTotalPerc(fromEntries: monthEntries)
|
||||
let month = Calendar.current.component(.month, from: now)
|
||||
|
||||
self.sharebleItems = [
|
||||
WrappedSharable(preview: AnyView(
|
||||
AllMoodsTotalTemplate(isPreview: true,
|
||||
startDate: DataController.shared.earliestEntry?.forDate ?? Date(),
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
),destination: AnyView(
|
||||
AllMoodsTotalTemplate(isPreview: false,
|
||||
startDate: DataController.shared.earliestEntry?.forDate ?? Date(),
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
),description: AllMoodsTotalTemplate.description),
|
||||
// All Time Moods — style picker with 2 variations
|
||||
WrappedSharable(
|
||||
preview: AnyView(
|
||||
AllMoodsTotalTemplate(isPreview: true, startDate: earliestDate, endDate: now, fakeData: false)
|
||||
),
|
||||
destination: AnyView(
|
||||
SharingStylePickerView(title: "All Time Moods", designs: [
|
||||
SharingDesign(
|
||||
name: "Gradient",
|
||||
shareView: AnyView(AllMoodsV2(metrics: allMoodsMetrics, totalCount: allMoodsCount)),
|
||||
image: { AllMoodsV2(metrics: allMoodsMetrics, totalCount: allMoodsCount).image }
|
||||
),
|
||||
SharingDesign(
|
||||
name: "Color Block",
|
||||
shareView: AnyView(AllMoodsV5(metrics: allMoodsMetrics, totalCount: allMoodsCount)),
|
||||
image: { AllMoodsV5(metrics: allMoodsMetrics, totalCount: allMoodsCount).image }
|
||||
),
|
||||
])
|
||||
),
|
||||
description: AllMoodsTotalTemplate.description
|
||||
),
|
||||
//////////////////////////////////////////////////////////
|
||||
WrappedSharable(preview: AnyView(
|
||||
CurrentStreakTemplate(isPreview: true,
|
||||
startDate: Calendar.current.date(byAdding: .day, value: -10, to: Date())!,
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
), destination: AnyView(
|
||||
CurrentStreakTemplate(isPreview: false,
|
||||
startDate: Calendar.current.date(byAdding: .day, value: -10, to: Date())!,
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
), description: CurrentStreakTemplate.description),
|
||||
// Last 10 Days — style picker with 2 variations
|
||||
WrappedSharable(
|
||||
preview: AnyView(
|
||||
CurrentStreakTemplate(isPreview: true, startDate: tenDaysAgo, endDate: now, fakeData: false)
|
||||
),
|
||||
destination: AnyView(
|
||||
SharingStylePickerView(title: "Last 10 Days", designs: [
|
||||
SharingDesign(
|
||||
name: "Gradient Cards",
|
||||
shareView: AnyView(CurrentStreakV2(moodEntries: streakEntries)),
|
||||
image: { CurrentStreakV2(moodEntries: streakEntries).image }
|
||||
),
|
||||
SharingDesign(
|
||||
name: "Color Strips",
|
||||
shareView: AnyView(CurrentStreakV5(moodEntries: streakEntries)),
|
||||
image: { CurrentStreakV5(moodEntries: streakEntries).image }
|
||||
),
|
||||
])
|
||||
),
|
||||
description: CurrentStreakTemplate.description
|
||||
),
|
||||
//////////////////////////////////////////////////////////
|
||||
WrappedSharable(preview: AnyView(
|
||||
LongestStreakTemplate(isPreview: true,
|
||||
startDate: DataController.shared.earliestEntry?.forDate ?? Date(),
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
), destination: AnyView(
|
||||
LongestStreakTemplate(isPreview: false,
|
||||
startDate: DataController.shared.earliestEntry?.forDate ?? Date(),
|
||||
endDate: Date(),
|
||||
fakeData: false)
|
||||
), description: LongestStreakTemplate.description),
|
||||
// Longest Streak — custom picker with mood selection
|
||||
WrappedSharable(
|
||||
preview: AnyView(
|
||||
LongestStreakTemplate(isPreview: true, startDate: earliestDate, endDate: now, fakeData: false)
|
||||
),
|
||||
destination: AnyView(
|
||||
LongestStreakPickerView(startDate: earliestDate, endDate: now)
|
||||
),
|
||||
description: LongestStreakTemplate.description
|
||||
),
|
||||
//////////////////////////////////////////////////////////
|
||||
WrappedSharable(preview: AnyView(
|
||||
MonthTotalTemplate(isPreview: true,
|
||||
startDate: Date().startOfMonth,
|
||||
endDate: Date().endOfMonth,
|
||||
fakeData: false)
|
||||
), destination: AnyView(
|
||||
MonthTotalTemplate(isPreview: false,
|
||||
startDate: Date().startOfMonth,
|
||||
endDate: Date().endOfMonth,
|
||||
fakeData: false)
|
||||
), description: MonthTotalTemplate.description)
|
||||
// This Month — style picker with 2 variations
|
||||
WrappedSharable(
|
||||
preview: AnyView(
|
||||
MonthTotalTemplate(isPreview: true, startDate: monthStart, endDate: monthEnd, fakeData: false)
|
||||
),
|
||||
destination: AnyView(
|
||||
SharingStylePickerView(title: "This Month", designs: [
|
||||
SharingDesign(
|
||||
name: "Clean Calendar",
|
||||
shareView: AnyView(MonthTotalV1(moodMetrics: monthMetrics, moodEntries: monthEntries, month: month)),
|
||||
image: { MonthTotalV1(moodMetrics: monthMetrics, moodEntries: monthEntries, month: month).image }
|
||||
),
|
||||
SharingDesign(
|
||||
name: "Stacked Bars",
|
||||
shareView: AnyView(MonthTotalV5(moodMetrics: monthMetrics, moodEntries: monthEntries, month: month)),
|
||||
image: { MonthTotalV5(moodMetrics: monthMetrics, moodEntries: monthEntries, month: month).image }
|
||||
),
|
||||
])
|
||||
),
|
||||
description: MonthTotalTemplate.description
|
||||
),
|
||||
//////////////////////////////////////////////////////////
|
||||
]
|
||||
}
|
||||
|
||||
267
Shared/Views/Sharing/SharingStylePickerView.swift
Normal file
267
Shared/Views/Sharing/SharingStylePickerView.swift
Normal file
@@ -0,0 +1,267 @@
|
||||
//
|
||||
// SharingStylePickerView.swift
|
||||
// Feels
|
||||
//
|
||||
// A horizontal pager that lets users swipe between design variations
|
||||
// for a sharing template, then export the selected design.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SharingDesign: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
let shareView: AnyView
|
||||
let image: () -> UIImage
|
||||
}
|
||||
|
||||
struct SharePickerData: Identifiable {
|
||||
let id = UUID()
|
||||
let title: String
|
||||
let designs: [SharingDesign]
|
||||
}
|
||||
|
||||
struct SharingStylePickerView: View {
|
||||
let title: String
|
||||
let designs: [SharingDesign]
|
||||
|
||||
@State private var selectedIndex = 0
|
||||
@StateObject private var shareImage = ShareImageStateViewModel()
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
private var safeIndex: Int {
|
||||
guard !designs.isEmpty else { return 0 }
|
||||
return min(selectedIndex, designs.count - 1)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if designs.isEmpty {
|
||||
Text("No designs available")
|
||||
.foregroundColor(textColor)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
// Title bar
|
||||
HStack {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}) {
|
||||
Text("Exit")
|
||||
.font(.headline)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Invisible placeholder for symmetry
|
||||
Text("Exit")
|
||||
.font(.headline)
|
||||
.hidden()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
// Pager showing scaled-down shareViews
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(Array(designs.enumerated()), id: \.offset) { index, design in
|
||||
design.shareView
|
||||
.scaleEffect(0.45, anchor: .center)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.tag(index)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .always))
|
||||
|
||||
// Design name label
|
||||
Text(designs[safeIndex].name)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(textColor)
|
||||
.padding(.top, 8)
|
||||
.animation(.none, value: selectedIndex)
|
||||
|
||||
// Share button
|
||||
Button(action: {
|
||||
let img = designs[safeIndex].image()
|
||||
shareImage.selectedShareImage = img
|
||||
shareImage.showSheet = true
|
||||
}) {
|
||||
Text("Share")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(Color.green)
|
||||
.cornerRadius(14)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.background(theme.currentTheme.bg.edgesIgnoringSafeArea(.all))
|
||||
.sheet(isPresented: $shareImage.showSheet) {
|
||||
if let uiImage = shareImage.selectedShareImage {
|
||||
ShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Longest Streak Picker (mood selection + style picker)
|
||||
|
||||
struct LongestStreakPickerView: View {
|
||||
let startDate: Date
|
||||
let endDate: Date
|
||||
|
||||
@State private var selectedMood: Mood = .great
|
||||
@State private var streakEntries: [MoodEntryModel] = []
|
||||
@State private var selectedIndex = 0
|
||||
@StateObject private var shareImage = ShareImageStateViewModel()
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
private var designs: [SharingDesign] {
|
||||
[
|
||||
SharingDesign(
|
||||
name: "Gradient Bar",
|
||||
shareView: AnyView(LongestStreakV2(streakEntries: streakEntries, selectedMood: selectedMood)),
|
||||
image: { LongestStreakV2(streakEntries: streakEntries, selectedMood: selectedMood).image }
|
||||
),
|
||||
SharingDesign(
|
||||
name: "Dark Badge",
|
||||
shareView: AnyView(LongestStreakV3(streakEntries: streakEntries, selectedMood: selectedMood)),
|
||||
image: { LongestStreakV3(streakEntries: streakEntries, selectedMood: selectedMood).image }
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Title bar with mood picker
|
||||
HStack {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}) {
|
||||
Text("Exit")
|
||||
.font(.headline)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Menu {
|
||||
ForEach(Mood.allValues) { mood in
|
||||
Button(mood.strValue) {
|
||||
selectedMood = mood
|
||||
recomputeStreak()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text(selectedMood.strValue)
|
||||
.font(.headline)
|
||||
.foregroundColor(textColor)
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Exit")
|
||||
.font(.headline)
|
||||
.hidden()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
// Pager
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(Array(designs.enumerated()), id: \.offset) { index, design in
|
||||
design.shareView
|
||||
.scaleEffect(0.45, anchor: .center)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.tag(index)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .always))
|
||||
|
||||
// Design name
|
||||
Text(designs[selectedIndex].name)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(textColor)
|
||||
.padding(.top, 8)
|
||||
.animation(.none, value: selectedIndex)
|
||||
|
||||
// Share button
|
||||
Button(action: {
|
||||
let img = designs[selectedIndex].image()
|
||||
shareImage.selectedShareImage = img
|
||||
shareImage.showSheet = true
|
||||
}) {
|
||||
Text("Share")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(Color.green)
|
||||
.cornerRadius(14)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.background(theme.currentTheme.bg.edgesIgnoringSafeArea(.all))
|
||||
.sheet(isPresented: $shareImage.showSheet) {
|
||||
if let uiImage = shareImage.selectedShareImage {
|
||||
ShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
.onAppear { recomputeStreak() }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func recomputeStreak() {
|
||||
let allEntries = DataController.shared.getData(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
includedDays: [1, 2, 3, 4, 5, 6, 7]
|
||||
)
|
||||
var splitArrays = createSubArrays(fromMoodEntries: allEntries, splitOn: selectedMood)
|
||||
splitArrays.sort { $0.count > $1.count }
|
||||
streakEntries = splitArrays.first ?? []
|
||||
}
|
||||
|
||||
private func createSubArrays(fromMoodEntries: [MoodEntryModel], splitOn: Mood) -> [[MoodEntryModel]] {
|
||||
var splitArrays = [[MoodEntryModel]]()
|
||||
var currentSplit = [MoodEntryModel]()
|
||||
for entry in fromMoodEntries {
|
||||
if entry.mood == splitOn {
|
||||
currentSplit.append(entry)
|
||||
} else {
|
||||
splitArrays.append(currentSplit)
|
||||
currentSplit.removeAll()
|
||||
}
|
||||
}
|
||||
splitArrays.append(currentSplit)
|
||||
return splitArrays
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user