diff --git a/Gaze.xcodeproj/project.pbxproj b/Gaze.xcodeproj/project.pbxproj index 1a6740a..8b5e2a9 100644 --- a/Gaze.xcodeproj/project.pbxproj +++ b/Gaze.xcodeproj/project.pbxproj @@ -424,7 +424,7 @@ CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 6GK4F9L62V; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -438,7 +438,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.3.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -460,7 +460,7 @@ CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 6GK4F9L62V; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -474,7 +474,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.3.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -492,11 +492,11 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 6GK4F9L62V; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -513,11 +513,11 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 6GK4F9L62V; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -533,10 +533,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 6GK4F9L62V; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -552,10 +552,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 6GK4F9L62V; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; diff --git a/Gaze/Views/Containers/SettingsWindowView.swift b/Gaze/Views/Containers/SettingsWindowView.swift index fca49c7..2e96f95 100644 --- a/Gaze/Views/Containers/SettingsWindowView.swift +++ b/Gaze/Views/Containers/SettingsWindowView.swift @@ -37,14 +37,16 @@ struct SettingsWindowView: View { Label("Posture", systemImage: "figure.stand") } - UserTimersView(userTimers: Binding( - get: { settingsManager.settings.userTimers }, - set: { settingsManager.settings.userTimers = $0 } - )) - .tag(3) - .tabItem { - Label("User Timers", systemImage: "plus.circle") - } + UserTimersView( + userTimers: Binding( + get: { settingsManager.settings.userTimers }, + set: { settingsManager.settings.userTimers = $0 } + ) + ) + .tag(3) + .tabItem { + Label("User Timers", systemImage: "plus.circle") + } GeneralSetupView( settingsManager: settingsManager, @@ -60,12 +62,12 @@ struct SettingsWindowView: View { HStack { #if DEBUG - Button("Retrigger Onboarding") { - retriggerOnboarding() - } - .buttonStyle(.bordered) + Button("Retrigger Onboarding") { + retriggerOnboarding() + } + .buttonStyle(.bordered) #endif - + Spacer() Button("Close") { @@ -94,21 +96,21 @@ struct SettingsWindowView: View { window.close() } } - + #if DEBUG - private func retriggerOnboarding() { - // Close settings window first - closeWindow() - - // Get AppDelegate and open onboarding - if let appDelegate = NSApplication.shared.delegate as? AppDelegate { - // Reset onboarding state so it shows as fresh - settingsManager.settings.hasCompletedOnboarding = false - - // Open onboarding window - appDelegate.openOnboarding() + private func retriggerOnboarding() { + // Close settings window first + closeWindow() + + // Get AppDelegate and open onboarding + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + // Reset onboarding state so it shows as fresh + settingsManager.settings.hasCompletedOnboarding = false + + // Open onboarding window + appDelegate.openOnboarding() + } } - } #endif } diff --git a/GazeTests/TimerEngineTests.swift b/GazeTests/TimerEngineTests.swift index fb13eab..914e3ce 100644 --- a/GazeTests/TimerEngineTests.swift +++ b/GazeTests/TimerEngineTests.swift @@ -32,9 +32,9 @@ final class TimerEngineTests: XCTestCase { timerEngine.start() XCTAssertEqual(timerEngine.timerStates.count, 3) - XCTAssertNotNil(timerEngine.timerStates[.lookAway]) - XCTAssertNotNil(timerEngine.timerStates[.blink]) - XCTAssertNotNil(timerEngine.timerStates[.posture]) + XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)]) + XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)]) + XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)]) } func testDisabledTimersNotInitialized() { @@ -43,16 +43,16 @@ final class TimerEngineTests: XCTestCase { timerEngine.start() XCTAssertEqual(timerEngine.timerStates.count, 2) - XCTAssertNotNil(timerEngine.timerStates[.lookAway]) - XCTAssertNil(timerEngine.timerStates[.blink]) - XCTAssertNotNil(timerEngine.timerStates[.posture]) + XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)]) + XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)]) + XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)]) } func testTimerStateInitialValues() { timerEngine.start() - let lookAwayState = timerEngine.timerStates[.lookAway]! - XCTAssertEqual(lookAwayState.type, .lookAway) + let lookAwayState = timerEngine.timerStates[.builtIn(.lookAway)]! + XCTAssertEqual(lookAwayState.identifier, .builtIn(.lookAway)) XCTAssertEqual(lookAwayState.remainingSeconds, 20 * 60) XCTAssertFalse(lookAwayState.isPaused) XCTAssertTrue(lookAwayState.isActive) @@ -81,33 +81,33 @@ final class TimerEngineTests: XCTestCase { settingsManager.settings.lookAwayTimer.intervalSeconds = 60 timerEngine.start() - timerEngine.timerStates[.lookAway]?.remainingSeconds = 10 + timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 10 - timerEngine.skipNext(type: .lookAway) + timerEngine.skipNext(identifier: .builtIn(.lookAway)) - XCTAssertEqual(timerEngine.timerStates[.lookAway]?.remainingSeconds, 60) + XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 60) } func testGetTimeRemaining() { timerEngine.start() - let timeRemaining = timerEngine.getTimeRemaining(for: .lookAway) + let timeRemaining = timerEngine.getTimeRemaining(for: .builtIn(.lookAway)) XCTAssertEqual(timeRemaining, TimeInterval(20 * 60)) } func testGetFormattedTimeRemaining() { timerEngine.start() - timerEngine.timerStates[.lookAway]?.remainingSeconds = 125 + timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 125 - let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) + let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) XCTAssertEqual(formatted, "2:05") } func testGetFormattedTimeRemainingWithHours() { timerEngine.start() - timerEngine.timerStates[.lookAway]?.remainingSeconds = 3665 + timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 3665 - let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) + let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) XCTAssertEqual(formatted, "1:01:05") } @@ -121,13 +121,13 @@ final class TimerEngineTests: XCTestCase { func testDismissReminderResetsTimer() { timerEngine.start() - timerEngine.timerStates[.blink]?.remainingSeconds = 0 + timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds = 0 timerEngine.activeReminder = .blinkTriggered timerEngine.dismissReminder() XCTAssertNil(timerEngine.activeReminder) - XCTAssertEqual(timerEngine.timerStates[.blink]?.remainingSeconds, 5 * 60) + XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 5 * 60) } func testDismissLookAwayResumesTimers() { @@ -145,7 +145,7 @@ final class TimerEngineTests: XCTestCase { func testTriggerReminderForLookAway() { timerEngine.start() - timerEngine.triggerReminder(for: .lookAway) + timerEngine.triggerReminder(for: .builtIn(.lookAway)) XCTAssertNotNil(timerEngine.activeReminder) if case .lookAwayTriggered(let countdown) = timerEngine.activeReminder { @@ -162,7 +162,7 @@ final class TimerEngineTests: XCTestCase { func testTriggerReminderForBlink() { timerEngine.start() - timerEngine.triggerReminder(for: .blink) + timerEngine.triggerReminder(for: .builtIn(.blink)) XCTAssertNotNil(timerEngine.activeReminder) if case .blinkTriggered = timerEngine.activeReminder { @@ -175,7 +175,7 @@ final class TimerEngineTests: XCTestCase { func testTriggerReminderForPosture() { timerEngine.start() - timerEngine.triggerReminder(for: .posture) + timerEngine.triggerReminder(for: .builtIn(.posture)) XCTAssertNotNil(timerEngine.activeReminder) if case .postureTriggered = timerEngine.activeReminder { @@ -186,58 +186,58 @@ final class TimerEngineTests: XCTestCase { } func testGetTimeRemainingForNonExistentTimer() { - let timeRemaining = timerEngine.getTimeRemaining(for: .lookAway) + let timeRemaining = timerEngine.getTimeRemaining(for: .builtIn(.lookAway)) XCTAssertEqual(timeRemaining, 0) } func testGetFormattedTimeRemainingZeroSeconds() { timerEngine.start() - timerEngine.timerStates[.lookAway]?.remainingSeconds = 0 + timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 0 - let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) + let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) XCTAssertEqual(formatted, "0:00") } func testGetFormattedTimeRemainingLessThanMinute() { timerEngine.start() - timerEngine.timerStates[.lookAway]?.remainingSeconds = 45 + timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 45 - let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) + let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) XCTAssertEqual(formatted, "0:45") } func testGetFormattedTimeRemainingExactHour() { timerEngine.start() - timerEngine.timerStates[.lookAway]?.remainingSeconds = 3600 + timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 3600 - let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) + let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) XCTAssertEqual(formatted, "1:00:00") } func testMultipleStartCallsResetTimers() { timerEngine.start() - timerEngine.timerStates[.lookAway]?.remainingSeconds = 100 + timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 100 timerEngine.start() - XCTAssertEqual(timerEngine.timerStates[.lookAway]?.remainingSeconds, 20 * 60) + XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 20 * 60) } func testSkipNextPreservesPausedState() { timerEngine.start() timerEngine.pause() - timerEngine.skipNext(type: .lookAway) + timerEngine.skipNext(identifier: .builtIn(.lookAway)) - XCTAssertTrue(timerEngine.timerStates[.lookAway]?.isPaused ?? false) + XCTAssertTrue(timerEngine.timerStates[.builtIn(.lookAway)]?.isPaused ?? false) } func testSkipNextPreservesActiveState() { timerEngine.start() - timerEngine.skipNext(type: .lookAway) + timerEngine.skipNext(identifier: .builtIn(.lookAway)) - XCTAssertTrue(timerEngine.timerStates[.lookAway]?.isActive ?? false) + XCTAssertTrue(timerEngine.timerStates[.builtIn(.lookAway)]?.isActive ?? false) } func testDismissReminderWithNoActiveReminder() { @@ -279,8 +279,8 @@ final class TimerEngineTests: XCTestCase { timerEngine.start() XCTAssertEqual(timerEngine.timerStates.count, 3) - for timerType in TimerType.allCases { - XCTAssertNotNil(timerEngine.timerStates[timerType]) + for builtInTimer in TimerType.allCases { + XCTAssertNotNil(timerEngine.timerStates[.builtIn(builtInTimer)]) } } @@ -302,8 +302,8 @@ final class TimerEngineTests: XCTestCase { timerEngine.start() XCTAssertEqual(timerEngine.timerStates.count, 2) - XCTAssertNotNil(timerEngine.timerStates[.lookAway]) - XCTAssertNil(timerEngine.timerStates[.blink]) - XCTAssertNotNil(timerEngine.timerStates[.posture]) + XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)]) + XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)]) + XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)]) } } diff --git a/GazeUITests/EnhancedOnboardingUITests.swift b/GazeUITests/EnhancedOnboardingUITests.swift new file mode 100644 index 0000000..e756cc0 --- /dev/null +++ b/GazeUITests/EnhancedOnboardingUITests.swift @@ -0,0 +1,175 @@ +// +// EnhancedOnboardingUITests.swift +// GazeUITests +// +// Created by Gaze Team on 1/13/26. +// + +import XCTest + +@MainActor +final class EnhancedOnboardingUITests: XCTestCase { + + var app: XCUIApplication! + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("--reset-onboarding") + app.launch() + } + + override func tearDownWithError() throws { + app = nil + } + + func testOnboardingCompleteFlowWithUserTimers() throws { + // Navigate through the complete onboarding flow + let continueButtons = app.buttons.matching(identifier: "Continue") + let nextButtons = app.buttons.matching(identifier: "Next") + + var currentStep = 0 + let maxSteps = 15 + + while currentStep < maxSteps { + if continueButtons.firstMatch.exists && continueButtons.firstMatch.isHittable { + continueButtons.firstMatch.tap() + currentStep += 1 + sleep(1) + } else if nextButtons.firstMatch.exists && nextButtons.firstMatch.isHittable { + nextButtons.firstMatch.tap() + currentStep += 1 + sleep(1) + } else if app.buttons["Get Started"].exists { + app.buttons["Get Started"].tap() + break + } else if app.buttons["Done"].exists { + app.buttons["Done"].tap() + break + } else { + break + } + } + + // Verify onboarding completed successfully + XCTAssertLessThan(currentStep, maxSteps, "Onboarding flow should complete") + + // Verify main application UI is visible (menubar should be active) + XCTAssertTrue(app.menuBarItems.firstMatch.exists, "Menubar should be available after onboarding") + } + + func testUserTimerCreationInOnboarding() throws { + // Reset to fresh onboarding state + app.terminate() + app = XCUIApplication() + app.launchArguments.append("--reset-onboarding") + app.launch() + + // Navigate to user timer setup section (assumes it's at the end) + let continueButtons = app.buttons.matching(identifier: "Continue") + let nextButtons = app.buttons.matching(identifier: "Next") + + // Skip through initial screens + var currentStep = 0 + while currentStep < 8 && (continueButtons.firstMatch.exists || nextButtons.firstMatch.exists) { + if continueButtons.firstMatch.exists && continueButtons.firstMatch.isHittable { + continueButtons.firstMatch.tap() + currentStep += 1 + sleep(1) + } else if nextButtons.firstMatch.exists && nextButtons.firstMatch.isHittable { + nextButtons.firstMatch.tap() + currentStep += 1 + sleep(1) + } + } + + // Look for timer creation UI or related elements + let timerSetupElement = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Timer' OR label CONTAINS 'Custom'")).firstMatch + XCTAssertTrue(timerSetupElement.exists, "User timer setup section should be available during onboarding") + + // If we can create a timer in onboarding, test that flow + if app.buttons["Add Timer"].exists { + app.buttons["Add Timer"].tap() + + // Fill out timer details - this would be specific to the actual UI structure + let titleField = app.textFields["Timer Title"] + if titleField.exists { + titleField.typeText("Test Timer") + } + + let intervalField = app.textFields["Interval (minutes)"] + if intervalField.exists { + intervalField.typeText("10") + } + + // Submit the timer + app.buttons["Save"].tap() + } + } + + func testSettingsPersistenceAfterOnboarding() throws { + // Reset to fresh onboarding state + app.terminate() + app = XCUIApplication() + app.launchArguments.append("--reset-onboarding") + app.launch() + + // Complete onboarding flow + let continueButtons = app.buttons.matching(identifier: "Continue") + let nextButtons = app.buttons.matching(identifier: "Next") + + while continueButtons.firstMatch.exists || nextButtons.firstMatch.exists { + if continueButtons.firstMatch.exists && continueButtons.firstMatch.isHittable { + continueButtons.firstMatch.tap() + sleep(1) + } else if nextButtons.firstMatch.exists && nextButtons.firstMatch.isHittable { + nextButtons.firstMatch.tap() + sleep(1) + } + } + + // Get to the end and complete onboarding + app.buttons["Get Started"].tap() + + // Verify that settings are properly initialized + let menuBar = app.menuBarItems.firstMatch + XCTAssertTrue(menuBar.exists, "Menubar should exist after onboarding") + + // Re-launch the app to verify settings persistence + app.terminate() + let newApp = XCUIApplication() + newApp.launchArguments.append("--skip-onboarding") + newApp.launch() + + XCTAssertTrue(newApp.menuBarItems.firstMatch.exists, "Application should maintain state after restart") + newApp.terminate() + } + + func testOnboardingNavigationEdgeCases() throws { + // Test that navigation buttons work properly at each step + let continueButton = app.buttons["Continue"] + if continueButton.waitForExistence(timeout: 2) { + continueButton.tap() + + // Verify we moved to the next screen + let nextScreen = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Setup' OR label CONTAINS 'Configure'")).firstMatch + XCTAssertTrue(nextScreen.exists, "Should navigate to next screen on Continue") + } + + // Test back navigation + let backButton = app.buttons["Back"] + if backButton.waitForExistence(timeout: 1) { + backButton.tap() + + // Should return to previous screen + XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Welcome'")).firstMatch.exists) + } + + // Test that we can go forward again + let continueButton2 = app.buttons["Continue"] + if continueButton2.waitForExistence(timeout: 1) { + continueButton2.tap() + XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Setup'")).firstMatch.exists) + } + } +} \ No newline at end of file diff --git a/build_dmg b/build_dmg index a0b61e3..7e5c3d4 100755 --- a/build_dmg +++ b/build_dmg @@ -111,11 +111,18 @@ ask_version_bump() { echo "New version: v${NEW_VERSION}" + # Get current build number for display + DISPLAY_CURRENT_BUILD=$(grep -A 1 "CURRENT_PROJECT_VERSION" Gaze.xcodeproj/project.pbxproj | grep -o '[0-9]\+' | head -1) + if [ -z "$DISPLAY_CURRENT_BUILD" ]; then + DISPLAY_CURRENT_BUILD=0 + fi + DISPLAY_NEW_BUILD=$((DISPLAY_CURRENT_BUILD + 1)) + # Ask for confirmation to proceed with version bumping echo "" echo "This will:" echo " 1. Update project.pbxproj → MARKETING_VERSION = ${NEW_VERSION}" - echo " 2. Update project.pbxproj → CURRENT_PROJECT_VERSION = $(($(date +%Y%m%d)))" + echo " 2. Update project.pbxproj → CURRENT_PROJECT_VERSION = ${DISPLAY_NEW_BUILD} (currently ${DISPLAY_CURRENT_BUILD})" echo " 3. Create git tag v${NEW_VERSION}" echo "" read -p "Proceed with version bump? (y/n) " -n 1 -r @@ -132,8 +139,12 @@ ask_version_bump() { sed -i.bak "s/MARKETING_VERSION = [0-9.]*;/MARKETING_VERSION = ${NEW_VERSION};/" Gaze.xcodeproj/project.pbxproj rm -f Gaze.xcodeproj/project.pbxproj.bak - # Update CURRENT_PROJECT_VERSION (build number) - BUILD_NUMBER=$(date +%Y%m%d) + # Update CURRENT_PROJECT_VERSION (build number) - increment by 1 + CURRENT_BUILD=$(grep -A 1 "CURRENT_PROJECT_VERSION" Gaze.xcodeproj/project.pbxproj | grep -o '[0-9]\+' | head -1) + if [ -z "$CURRENT_BUILD" ]; then + CURRENT_BUILD=0 + fi + BUILD_NUMBER=$((CURRENT_BUILD + 1)) sed -i.bak "s/CURRENT_PROJECT_VERSION = [0-9]*;/CURRENT_PROJECT_VERSION = ${BUILD_NUMBER};/" Gaze.xcodeproj/project.pbxproj rm -f Gaze.xcodeproj/project.pbxproj.bak