Files
Reflect/Shared/Random.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

429 lines
14 KiB
Swift

//
// Random.swift
// Reflect
//
// Created by Trey Tartt on 1/9/22.
//
import Foundation
import SwiftUI
import SwiftData
struct Constants {
static let groupShareId = "group.com.88oakapps.reflect"
static let groupShareIdDebug = "group.com.88oakapps.reflect.debug"
static var currentGroupShareId: String {
#if DEBUG
return groupShareIdDebug
#else
return groupShareId
#endif
}
static let viewsCornerRaidus: CGFloat = 10
}
struct GroupUserDefaults {
static var groupDefaults: UserDefaults {
#if DEBUG
return UserDefaults(suiteName: Constants.groupShareIdDebug)!
#else
return UserDefaults(suiteName: Constants.groupShareId)!
#endif
}
}
class Random {
static var tomorrowMidnightThirty: Date {
let components = DateComponents(hour: 0, minute: 30, second: 0)
var updateTime = Date()
if let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()),
let tomorrowMorning = Calendar.current.date(byAdding: components, to: tomorrow) {
updateTime = tomorrowMorning
}
return updateTime
}
static var existingWeekdayName = [Int: String]()
static func weekdayName(fromDate date: Date) -> String {
let weekday = Calendar.current.component(.weekday, from: date)
let calendar = Calendar.current
let dayIndex = ((weekday - 1) + (calendar.firstWeekday - 1)) % 7
if let value = Random.existingWeekdayName[dayIndex] {
return value
}
let newValue = calendar.weekdaySymbols[dayIndex]
Random.existingWeekdayName[dayIndex] = newValue
return newValue
}
/// Cached month symbols to avoid creating DateFormatter repeatedly
private static let monthSymbols: [String] = DateFormatter().monthSymbols
static func monthName(fromMonthInt: Int) -> String {
return monthSymbols[fromMonthInt-1]
}
static var existingDayFormat = [NSNumber: String]()
static func dayFormat(fromDate date: Date) -> String {
let components = Calendar.current.dateComponents([.day], from: date)
let day = components.day!
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
let num = NSNumber(integerLiteral: day)
if let value = existingDayFormat[num] {
return value
}
let newValue = formatter.string(from: num) ?? ""
existingDayFormat[num] = newValue
return newValue
}
#if !os(watchOS)
static func createTotalPerc(fromEntries entries: [MoodEntryModel]) -> [MoodMetrics] {
let filteredEntries = entries.filter({
return ![.missing, .placeholder].contains($0.mood)
})
var returnData = [MoodMetrics]()
for (_, mood) in Mood.allValues.enumerated() {
let moodEntries = filteredEntries.filter({
$0.moodValue == mood.rawValue
})
let total = moodEntries.count
let perc = (Float(total) / Float(filteredEntries.count)) * 100
returnData.append(MoodMetrics(mood: mood, total: total, percent: perc))
}
returnData = returnData.sorted(by: {
$0.mood.rawValue > $1.mood.rawValue
})
return returnData
}
#endif
}
#if !os(watchOS)
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
func asImage(size: CGSize) -> UIImage {
let wrapped = self.ignoresSafeArea().frame(width: size.width, height: size.height)
let controller = UIHostingController(rootView: wrapped)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.backgroundColor = .clear
controller.view.layoutIfNeeded()
let image = controller.view.asImage()
return image
}
}
extension UIView {
func asImage() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
return UIGraphicsImageRenderer(size: self.layer.frame.size, format: format).image { context in
self.drawHierarchy(in: self.layer.bounds, afterScreenUpdates: true)
}
}
}
extension Color {
static func random() -> Self {
Self(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
public func lighter(by amount: CGFloat = 0.2) -> Self { Self(UIColor(self).lighter(by: amount)) }
public func darker(by amount: CGFloat = 0.2) -> Self { Self(UIColor(self).darker(by: amount)) }
}
extension String {
/// Cache for rendered emoji images to avoid expensive re-rendering
private static var textToImageCache = [String: UIImage]()
func textToImage() -> UIImage? {
// Return cached image if available
if let cached = Self.textToImageCache[self] {
return cached
}
let nsString = (self as NSString)
let font = UIFont.systemFont(ofSize: 100) // you can change your font size here
let stringAttributes = [NSAttributedString.Key.font: font]
let imageSize = nsString.size(withAttributes: stringAttributes)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0) // begin image context
UIColor.clear.set() // clear background
UIRectFill(CGRect(origin: CGPoint(), size: imageSize)) // set rect size
nsString.draw(at: CGPoint.zero, withAttributes: stringAttributes) // draw text within rect
let image = UIGraphicsGetImageFromCurrentImageContext() // create image from context
UIGraphicsEndImageContext() // end image context
let result = image ?? UIImage()
// Cache the rendered image
Self.textToImageCache[self] = result
return result
}
}
extension UIColor {
func lighter(by percentage: CGFloat = 10.0) -> UIColor {
return self.adjust(by: abs(percentage))
}
func darker(by percentage: CGFloat = 10.0) -> UIColor {
return self.adjust(by: -abs(percentage))
}
func adjust(by percentage: CGFloat) -> UIColor {
var alpha, hue, saturation, brightness, red, green, blue, white : CGFloat
(alpha, hue, saturation, brightness, red, green, blue, white) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
let multiplier = percentage / 100.0
if self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
let newBrightness: CGFloat = max(min(brightness + multiplier*brightness, 1.0), 0.0)
return UIColor(hue: hue, saturation: saturation, brightness: newBrightness, alpha: alpha)
}
else if self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
let newRed: CGFloat = min(max(red + multiplier*red, 0.0), 1.0)
let newGreen: CGFloat = min(max(green + multiplier*green, 0.0), 1.0)
let newBlue: CGFloat = min(max(blue + multiplier*blue, 0.0), 1.0)
return UIColor(red: newRed, green: newGreen, blue: newBlue, alpha: alpha)
}
else if self.getWhite(&white, alpha: &alpha) {
let newWhite: CGFloat = (white + multiplier*white)
return UIColor(white: newWhite, alpha: alpha)
}
return self
}
}
#endif
// MARK: - Date Formatting Cache
/// High-performance date formatting cache to eliminate repeated ICU Calendar operations.
/// Caches formatted date strings keyed by (date's day start, format type).
final class DateFormattingCache {
static let shared = DateFormattingCache()
enum Format: Int {
case day // "15"
case weekdayWide // "Monday"
case weekdayAbbreviated // "Mon"
case weekdayWideDay // "Monday 15"
case monthWide // "January"
case monthAbbreviated // "Jan"
case monthAbbreviatedDay // "Jan 15"
case monthAbbreviatedYear // "Jan 2025"
case monthWideYear // "January 2025"
case weekdayAbbrevMonthAbbrev // "Mon Jan"
case weekdayWideMonthAbbrev // "Monday Jan"
case yearMonthDayDigits // "2025/01/15"
case dateMedium // "Jan 15, 2025" (dateStyle = .medium)
case dateFull // "Monday, January 15, 2025" (dateStyle = .full)
}
private var cache = [Int: [Format: String]]()
private let calendar = Calendar.current
// Reusable formatters (creating DateFormatter is expensive)
private lazy var dayFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "d"
return f
}()
private lazy var weekdayWideFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "EEEE"
return f
}()
private lazy var weekdayAbbrevFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "EEE"
return f
}()
private lazy var weekdayWideDayFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "EEEE d"
return f
}()
private lazy var monthWideFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMMM"
return f
}()
private lazy var monthAbbrevFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMM"
return f
}()
private lazy var monthAbbrevDayFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMM d"
return f
}()
private lazy var monthAbbrevYearFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMM yyyy"
return f
}()
private lazy var monthWideYearFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMMM yyyy"
return f
}()
private lazy var weekdayAbbrevMonthAbbrevFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "EEE MMM"
return f
}()
private lazy var weekdayWideMonthAbbrevFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "EEEE MMM"
return f
}()
private lazy var yearMonthDayDigitsFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy/MM/dd"
return f
}()
private lazy var dateMediumFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
return f
}()
private lazy var dateFullFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .full
return f
}()
private init() {}
/// Get cached formatted string for a date
func string(for date: Date, format: Format) -> String {
let dayKey = dayIdentifier(for: date)
// Check cache
if let formatCache = cache[dayKey], let cached = formatCache[format] {
return cached
}
// Format and cache
let formatted = formatDate(date, format: format)
if cache[dayKey] == nil {
cache[dayKey] = [:]
}
cache[dayKey]?[format] = formatted
return formatted
}
private func dayIdentifier(for date: Date) -> Int {
// Use days since reference date as unique key
Int(calendar.startOfDay(for: date).timeIntervalSinceReferenceDate / 86400)
}
private func formatDate(_ date: Date, format: Format) -> String {
switch format {
case .day:
return dayFormatter.string(from: date)
case .weekdayWide:
return weekdayWideFormatter.string(from: date)
case .weekdayAbbreviated:
return weekdayAbbrevFormatter.string(from: date)
case .weekdayWideDay:
return weekdayWideDayFormatter.string(from: date)
case .monthWide:
return monthWideFormatter.string(from: date)
case .monthAbbreviated:
return monthAbbrevFormatter.string(from: date)
case .monthAbbreviatedDay:
return monthAbbrevDayFormatter.string(from: date)
case .monthAbbreviatedYear:
return monthAbbrevYearFormatter.string(from: date)
case .monthWideYear:
return monthWideYearFormatter.string(from: date)
case .weekdayAbbrevMonthAbbrev:
return weekdayAbbrevMonthAbbrevFormatter.string(from: date)
case .weekdayWideMonthAbbrev:
return weekdayWideMonthAbbrevFormatter.string(from: date)
case .yearMonthDayDigits:
return yearMonthDayDigitsFormatter.string(from: date)
case .dateMedium:
return dateMediumFormatter.string(from: date)
case .dateFull:
return dateFullFormatter.string(from: date)
}
}
}
extension Bundle {
var appName: String {
return infoDictionary?["CFBundleName"] as! String
}
var bundleId: String {
return bundleIdentifier!
}
var versionNumber: String {
return infoDictionary?["CFBundleShortVersionString"] as! String
}
var buildNumber: String {
return infoDictionary?["CFBundleVersion"] as! String
}
}