Makefile, linting, and formatting

This commit is contained in:
2025-12-15 16:32:59 -07:00
parent 0cc576bb12
commit c0d9702e54
91 changed files with 6342 additions and 5079 deletions

22
ios/.editorconfig Normal file
View File

@@ -0,0 +1,22 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.swift]
indent_size = 4
max_line_length = 140
[*.{plist,storyboard,xib,xcscheme,xcworkspacedata}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

68
ios/.swiftformat Normal file
View File

@@ -0,0 +1,68 @@
# SwiftFormat Configuration for Ascently iOS
# Maintains consistent formatting across the project
# File options
--exclude build,Pods,DerivedData,.build
# Format options
--swiftversion 6.0
--indent 4
--tabwidth 4
--maxwidth 140
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--wrapconditions after-first
--wrapreturntype preserve
--wrapeffects preserve
--closingparen balanced
--funcattributes prev-line
--typeattributes prev-line
--varattributes preserve
--storedvarattrs same-line
--computedvarattrs preserve
--complexattributes prev-line
--typeblanklines preserve
--structthreshold 20
--enumthreshold 20
--extensionacl on-declarations
--organizetypes class,struct,enum,extension,protocol
--modifierorder acl,setteracl,override,static,final,required,convenience,lazy,dynamic
--self remove
--importgrouping alpha
--semicolons inline
--operatorfunc spaced
--nospaceoperators ..<,...
--ranges no-space
--emptybraces no-space
--trimwhitespace always
--stripunusedargs closure-only
--header ignore
--guardelse auto
--elseposition same-line
--shortoptionals always
--linebreaks lf
--xcodeindentation disabled
--fragment false
--conflictmarkers reject
--ifdef no-indent
--extensionlength 0
# Enable rules
--enable isEmpty,sortedSwitchCases,redundantInit,redundantGet,redundantObjc
--enable blankLineAfterSwitchCase,consecutiveSpaces,duplicateImports
--enable elseOnSameLine,emptyBraces,hoistAwait,hoistPatternLet,hoistTry
--enable leadingDelimiters,redundantBackticks,redundantBreak,redundantClosure
--enable redundantExtensionACL,redundantFileprivate,redundantLetError
--enable redundantNilInit,redundantParens,redundantPattern,redundantRawValues
--enable redundantReturn,redundantSelf,redundantType,redundantVoidReturnType
--enable semicolons,sortImports,spaceAroundBraces,spaceAroundBrackets
--enable spaceAroundComments,spaceAroundGenerics,spaceAroundOperators
--enable spaceAroundParens,spaceInsideBraces,spaceInsideBrackets
--enable spaceInsideComments,spaceInsideGenerics,spaceInsideParens
--enable strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas
--enable typeSugar,unusedArguments,void,wrapArguments,wrapAttributes
--enable yodaConditions,blankLinesBetweenScopes,blankLinesAtEndOfScope
# Disable rules
--disable wrapMultilineStatementBraces

139
ios/.swiftlint.yml Normal file
View File

@@ -0,0 +1,139 @@
excluded:
- build
- Pods
- DerivedData
- .build
- AscentlyTests
disabled_rules:
- trailing_whitespace
- todo
- nesting
- opening_brace
- multiple_closures_with_trailing_closure
- trailing_comma
- attributes
- function_parameter_count
- large_tuple
opt_in_rules:
- empty_count
- explicit_init
- closure_spacing
- overridden_super_call
- redundant_nil_coalescing
- first_where
- sorted_first_last
- contains_over_filter_count
- contains_over_filter_is_empty
- empty_string
- prefer_zero_over_explicit_init
- flatmap_over_map_reduce
- last_where
- sorted_imports
- toggle_bool
- unavailable_function
- unneeded_parentheses_in_closure_argument
- vertical_whitespace_closing_braces
- yoda_condition
- collection_alignment
- literal_expression_end_indentation
line_length:
warning: 140
error: 250
ignores_comments: true
ignores_urls: true
ignores_function_declarations: true
ignores_interpolated_strings: true
type_body_length:
warning: 600
error: 1000
file_length:
warning: 1000
error: 1500
ignore_comment_only_lines: true
function_body_length:
warning: 100
error: 200
cyclomatic_complexity:
warning: 20
error: 30
ignores_case_statements: true
identifier_name:
min_length:
warning: 2
error: 1
max_length:
warning: 50
error: 60
validates_start_with_lowercase: error
allowed_symbols:
- _
excluded:
- id
- i
- j
- x
- y
- z
- to
- at
- or
- is
- no
- go
- db
- DATA_JSON_FILENAME
- IMAGES_DIR_NAME
- METADATA_FILENAME
# ViewBuilder section functions (SwiftUI convention)
- StatusSection
- IconDisplaySection
- DebugSection
- TestingSection
- ResultsSection
- GymSelectionSection
- SessionDetailsSection
- ProblemSelectionSection
- CreateProblemSection
- AttemptDetailsSection
- BasicInfoSection
- ClimbTypesSection
- DifficultySystemsSection
- NotesSection
- ClimbTypeSection
- DifficultySection
- LocationSection
- TagsSection
- PhotosSection
- AdditionalInfoSection
type_name:
min_length:
warning: 3
error: 2
max_length:
warning: 50
error: 60
force_cast: warning
force_try: warning
large_tuple:
warning: 4
error: 6
nesting:
type_level:
warning: 3
function_level:
warning: 5
reporter: "xcode"

View File

@@ -465,7 +465,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -487,7 +487,7 @@
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.5.1;
MARKETING_VERSION = 2.5.2;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -513,7 +513,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -535,7 +535,7 @@
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.5.1;
MARKETING_VERSION = 2.5.2;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -602,7 +602,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -613,7 +613,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.1;
MARKETING_VERSION = 2.5.2;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -632,7 +632,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -643,7 +643,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.1;
MARKETING_VERSION = 2.5.2;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;

View File

@@ -45,7 +45,7 @@ struct ContentView: View {
}
.environmentObject(dataManager)
.environmentObject(MusicService.shared)
.onChange(of: scenePhase) { oldPhase, newPhase in
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .active {
// Add slight delay to ensure app is fully loaded
Task {

View File

@@ -1,7 +1,7 @@
import MusicKit
import AVFoundation
import SwiftUI
import Combine
import MusicKit
import SwiftUI
@MainActor
class MusicService: ObservableObject {

View File

@@ -1,5 +1,5 @@
import Foundation
import Combine
import Foundation
class ServerSyncProvider: SyncProvider {
var type: SyncProviderType { .server }
@@ -235,7 +235,7 @@ class ServerSyncProvider: SyncProvider {
}.map { problem -> BackupProblem in
let backupProblem = BackupProblem(from: problem)
if !problem.imagePaths.isEmpty {
let normalizedPaths = problem.imagePaths.enumerated().map { index, _ in
let normalizedPaths = problem.imagePaths.indices.map { index in
ImageNamingUtils.generateImageFilename(
problemId: problem.id.uuidString, imageIndex: index)
}
@@ -859,7 +859,6 @@ class ServerSyncProvider: SyncProvider {
attempts: filteredAttempts,
deletedItems: backup.deletedItems
)
} else {
// Filter out deleted items even when no image path mapping
let deletedGymIds = Set(
@@ -930,7 +929,6 @@ class ServerSyncProvider: SyncProvider {
// Update local data state to match imported data timestamp
DataStateManager.shared.setLastModified(backup.exportedAt)
AppLogger.info("Data state synchronized to imported timestamp: \(backup.exportedAt)", tag: logTag)
} catch {
throw SyncError.importFailed(error)
}

View File

@@ -153,9 +153,7 @@ struct SyncMerger {
let activeSessionIds = Set(
local.compactMap { attempt in
return attempt.sessionId
}.filter { sessionId in
// Check if this session ID belongs to an active session
// For now, we'll be conservative and not delete attempts during merge
}.filter { _ in
return true
})

View File

@@ -133,7 +133,6 @@ class SyncService: ObservableObject {
if let lastSync = userDefaults.object(forKey: Keys.lastSyncTime) as? Date {
self.lastSyncTime = lastSync
}
} catch {
syncError = error.localizedDescription
throw error

View File

@@ -238,7 +238,6 @@ class ImageManager {
saveMigrationState(initialState)
performMigrationWithCheckpoints(files: allLegacyFiles, currentState: initialState)
} catch {
logError("ERROR: Failed to start migration: \(error)")
}
@@ -255,7 +254,6 @@ class ImageManager {
logInfo("Resuming with \(remainingFiles.count) remaining files")
performMigrationWithCheckpoints(files: remainingFiles, currentState: state)
} catch {
logError("ERROR: Failed to resume migration: \(error)")
// Fallback: start fresh
@@ -325,7 +323,6 @@ class ImageManager {
migratedCount += 1
logInfo("Migrated: \(fileName) (\(migratedCount)/\(currentState.totalFiles))")
} catch {
failedCount += 1
logError("ERROR: Failed to migrate \(fileName): \(error)")
@@ -676,7 +673,7 @@ class ImageManager {
for fileName in files {
let filePath = imagesDirectory.appendingPathComponent(fileName)
if let data = try? Data(contentsOf: filePath), data.count > 0 {
if let data = try? Data(contentsOf: filePath), !data.isEmpty {
// Basic validation - check if file has content and is reasonable size
if data.count > 100 { // Minimum viable image size
validFiles += 1
@@ -825,7 +822,7 @@ class ImageManager {
let primaryEmpty =
(try? fileManager.contentsOfDirectory(atPath: imagesDirectory.path).isEmpty) ?? true
let backupHasFiles =
((try? fileManager.contentsOfDirectory(atPath: backupDirectory.path)) ?? []).count > 0
!((try? fileManager.contentsOfDirectory(atPath: backupDirectory.path)) ?? []).isEmpty
if primaryEmpty && backupHasFiles {
logDebug("DEBUG SAFE: Primary empty but backup exists - restoring")
@@ -944,7 +941,6 @@ class ImageManager {
}
logInfo("Completed migration from previous Application Support directory")
} catch {
logError("ERROR: Failed to migrate from previous Application Support: \(error)")
}

View File

@@ -1,5 +1,5 @@
import SwiftUI
import Combine
import SwiftUI
class ThemeManager: ObservableObject {
@Published var accentColor: Color = .blue {

View File

@@ -73,7 +73,7 @@ struct ZipUtils {
do {
let imageData = try Data(contentsOf: imageURL)
if imageData.count > 0 {
if !imageData.isEmpty {
let imageEntryName = "\(IMAGES_DIR_NAME)/\(imageName)"
try addFileToZip(
filename: imageEntryName,

View File

@@ -526,7 +526,6 @@ struct AddAttemptView: View {
dismiss()
}
}
struct ProblemSelectionRow: View {
@@ -1302,7 +1301,6 @@ struct EditAttemptView: View {
dismiss()
}
}
#Preview {

View File

@@ -168,7 +168,6 @@ struct AddEditProblemView: View {
await loadSelectedPhotos()
}
}
}
@ViewBuilder
@@ -224,7 +223,6 @@ struct AddEditProblemView: View {
.fill(.quaternary)
)
}
}
}

View File

@@ -366,7 +366,7 @@ struct BarChartView: View {
)
} else {
VStack(alignment: .leading) {
// Chart area
// Chart area
HStack(alignment: .bottom, spacing: spacing / CGFloat(sortedData.count)) {
ForEach(Array(sortedData.enumerated()), id: \.offset) { index, gradeCount in
VStack(spacing: 4) {

View File

@@ -231,7 +231,6 @@ struct SessionDetailView: View {
uniqueProblemsCompleted: completedProblems.count
)
}
}
struct SessionHeaderCard: View {
@@ -305,7 +304,6 @@ struct SessionHeaderCard: View {
formatter.dateStyle = .full
return formatter.string(from: date)
}
}
struct SessionStatsCard: View {

View File

@@ -182,7 +182,8 @@ struct ProblemsView: View {
Button(action: {
showingFilters = true
}) {
Image(systemName: (selectedClimbType != nil || selectedGym != nil) ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
let hasFilters = selectedClimbType != nil || selectedGym != nil
Image(systemName: hasFilters ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
.font(.system(size: 16, weight: .medium))
.foregroundColor(themeManager.accentColor)
}
@@ -365,7 +366,6 @@ struct FilterSection: View {
}
}
}
}
struct FilterChip: View {
@@ -392,8 +392,6 @@ struct FilterChip: View {
}
}
struct ProblemRow: View {
let problem: Problem
@EnvironmentObject var dataManager: ClimbingDataManager

View File

@@ -256,7 +256,6 @@ struct ActiveSessionBanner: View {
SessionDetailView(sessionId: session.id)
}
}
}
struct SessionRow: View {

View File

@@ -251,9 +251,16 @@ struct DataManagementSection: View {
dataManager.resetAllData()
}
} message: {
Text(
"Are you sure you want to reset all data? This will permanently delete:\n\n• All gyms and their information\n• All problems and their images\n• All climbing sessions\n• All attempts and progress data\n\nThis action cannot be undone. Consider exporting your data first."
)
Text("""
Are you sure you want to reset all data? This will permanently delete:
• All gyms and their information
• All problems and their images
• All climbing sessions
• All attempts and progress data
This action cannot be undone. Consider exporting your data first.
""")
}
.alert("Delete All Images", isPresented: $showingDeleteImagesAlert) {
@@ -262,9 +269,14 @@ struct DataManagementSection: View {
deleteAllImages()
}
} message: {
Text(
"This will permanently delete ALL image files from your device.\n\nProblems will keep their references but the actual image files will be removed. This cannot be undone.\n\nConsider exporting your data first if you want to keep your images."
)
Text("""
This will permanently delete ALL image files from your device.
Problems will keep their references but the actual image files will be removed. \
This cannot be undone.
Consider exporting your data first if you want to keep your images.
""")
}
}
@@ -651,7 +663,6 @@ struct SyncSection: View {
.foregroundColor(.red)
.padding(.leading, 24)
}
}
}
.sheet(isPresented: $showingSyncSettings) {

View File

@@ -22,7 +22,6 @@ struct SessionStatusLiveLiveActivity: Widget {
LiveActivityView(context: context)
.activityBackgroundTint(Color.blue.opacity(0.2))
.activitySystemActionForegroundColor(Color.primary)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {