Repository: KrishKrosh/TrackWeight
Branch: main
Commit: e322cae241d2
Files: 28
Total size: 109.8 KB
Directory structure:
gitextract_foro58fa/
├── .github/
│ └── workflows/
│ ├── README.md
│ └── build-and-sign-dmg.yml
├── .gitignore
├── ExportOptions.plist
├── LICENSE
├── README.md
├── TrackWeight/
│ ├── Assets.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ContentView.swift
│ ├── ContentViewModel.swift
│ ├── DebugView.swift
│ ├── HomeView.swift
│ ├── Preview Content/
│ │ └── Preview Assets.xcassets/
│ │ └── Contents.json
│ ├── ScaleView.swift
│ ├── ScaleViewModel.swift
│ ├── SettingsView.swift
│ ├── TrackWeight.entitlements
│ ├── TrackWeightApp.swift
│ ├── TrackWeightView.swift
│ ├── WeighingState.swift
│ └── WeighingViewModel.swift
├── TrackWeight.xcodeproj/
│ ├── project.pbxproj
│ └── project.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm/
│ └── Package.resolved
└── scripts/
├── README.md
├── setup-signing.sh
└── test-build-locally.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/README.md
================================================
# GitHub Actions Workflows
This directory contains GitHub Actions workflows for the TrackWeight project.
## build-and-sign-dmg.yml
This workflow builds, signs (if certificates are provided), and packages the TrackWeight macOS application into a DMG file.
### Features
- **Automated Building**: Builds the Xcode project using the latest stable Xcode
- **Code Signing**: Supports optional code signing with certificates
- **DMG Creation**: Creates a professional DMG with proper layout and attribution
- **Attribution**: Includes proper credits to the original repository (https://github.com/KrishKrosh/TrackWeight)
- **Release Integration**: Can create GitHub releases with the built DMG
- **Artifact Upload**: Uploads DMG as a GitHub Actions artifact
### Triggers
The workflow runs on:
- Git tags starting with 'v' (e.g., v1.0.0)
- Published releases
- Manual workflow dispatch
### Setup Instructions
#### Required Secrets (for signed builds)
To enable code signing, add these secrets to your GitHub repository:
1. **BUILD_CERTIFICATE_BASE64**: Base64-encoded Developer ID Application certificate (.p12 file)
```bash
base64 -i YourCertificate.p12 | pbcopy
```
2. **P12_PASSWORD**: Password for the .p12 certificate file
3. **BUILD_PROVISION_PROFILE_BASE64**: Base64-encoded provisioning profile (optional for Developer ID)
```bash
base64 -i YourProvisioningProfile.mobileprovision | pbcopy
```
#### Setting up Certificates
1. Export your Developer ID Application certificate from Keychain Access as a .p12 file
2. Convert to base64 and add as `BUILD_CERTIFICATE_BASE64` secret
3. Add the certificate password as `P12_PASSWORD` secret
#### Unsigned Builds
If you don't have code signing certificates, the workflow will automatically create an unsigned development build. These builds can still be used but may require users to manually allow them in System Preferences.
## build-unsigned-dmg.yml
This workflow specifically builds unsigned development versions of the TrackWeight app without requiring any certificates.
### Features
- **No Certificate Requirements**: Builds completely without code signing
- **Development Build**: Creates unsigned development builds that work on any Mac
- **Same DMG Features**: Includes all the same DMG features as the signed version
- **Clear User Instructions**: Includes instructions for running unsigned apps
- **Attribution**: Maintains proper credits to the original repository
### Triggers
The workflow runs on:
- Pushes to main branches and the current working branch
- Manual workflow dispatch
### Benefits
- **Easy Testing**: Perfect for testing builds without setting up certificates
- **No Configuration**: Works immediately without any secrets or setup
- **User-Friendly**: Includes clear instructions for users on how to run unsigned apps
## Choosing the Right Workflow
### Use `build-and-sign-dmg.yml` when:
- You have Apple Developer certificates
- You want to distribute signed, trusted builds
- You're creating official releases
### Use `build-unsigned-dmg.yml` when:
- You don't have certificates
- You want to test builds quickly
- You're developing or experimenting
- You need a simple build process
If no signing certificates are provided, the workflow will create an unsigned development build that can still be distributed and run locally (users may need to allow it in System Preferences > Security & Privacy).
### Usage
#### Manual Trigger
1. Go to Actions tab in your GitHub repository
2. Select "Build and Sign DMG" workflow
3. Click "Run workflow"
4. Optionally check "Create a GitHub release" to create a release
#### Automatic Trigger
1. Create a git tag: `git tag v1.0.0`
2. Push the tag: `git push origin v1.0.0`
3. The workflow will automatically build and create a release
### Output
The workflow produces:
- **DMG file**: TrackWeight-{version}.dmg containing the app and attribution
- **GitHub Release**: (if triggered by tag or manual release creation)
- **Artifacts**: DMG file uploaded as GitHub Actions artifact
### Attribution
This workflow ensures proper attribution to the original TrackWeight repository:
- README.txt file included in DMG with credits
- Release notes include attribution
- Links to original repository: https://github.com/KrishKrosh/TrackWeight
## update-homebrew.yml
This workflow automatically updates the Homebrew cask when a new release is published.
### Features
- Updates version in Homebrew tap repository
- Automatically triggered on release publication
- Can be manually triggered with version input
For more information about the original TrackWeight project, visit: https://github.com/KrishKrosh/TrackWeight
================================================
FILE: .github/workflows/build-and-sign-dmg.yml
================================================
name: Build and Sign DMG
on:
push:
tags:
- 'v*'
branches:
- 'copilot/fix-1' # Enable testing on current working branch
release:
types: [published]
workflow_dispatch:
inputs:
create_release:
description: 'Create a GitHub release'
required: false
default: false
type: boolean
env:
APP_NAME: TrackWeight
SCHEME: TrackWeight
CONFIGURATION: Release
jobs:
build-and-sign:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Import Code-Signing Certificates
if: ${{ env.BUILD_CERTIFICATE_BASE64 != '' }}
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
uses: Apple-Actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
p12-password: ${{ secrets.P12_PASSWORD }}
- name: Install provisioning profile
if: ${{ env.BUILD_PROVISION_PROFILE_BASE64 != '' }}
env:
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
run: |
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- name: Build and Archive App
run: |
xcodebuild \
-project TrackWeight.xcodeproj \
-scheme ${{ env.SCHEME }} \
-configuration ${{ env.CONFIGURATION }} \
-archivePath "$RUNNER_TEMP/${{ env.APP_NAME }}.xcarchive" \
-destination 'generic/platform=macOS' \
ARCHS="arm64 x86_64" \
ONLY_ACTIVE_ARCH=NO \
archive
- name: Export App
run: |
# Use development export method if no signing certificates are available
if [ -z "${{ secrets.BUILD_CERTIFICATE_BASE64 }}" ]; then
# Create export options for unsigned build
cat > "$RUNNER_TEMP/ExportOptions.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>debugging</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>destination</key>
<string>export</string>
<key>signingCertificate</key>
<string>-</string>
<key>teamID</key>
<string>-</string>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<false/>
</dict>
</plist>
EOF
else
# Use the existing ExportOptions.plist for signed builds
cp ExportOptions.plist "$RUNNER_TEMP/ExportOptions.plist"
fi
xcodebuild \
-archivePath "$RUNNER_TEMP/${{ env.APP_NAME }}.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
-exportArchive
- name: Re-sign App Components
if: ${{ env.BUILD_CERTIFICATE_BASE64 != '' }}
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
echo "🔏 Re-signing framework with Developer ID certificate..."
FRAMEWORK_PATH="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app/Contents/Frameworks/OpenMultitouchSupportXCF.framework"
if [[ -d "$FRAMEWORK_PATH" ]]; then
codesign --force --sign "Developer ID Application" \
--options runtime \
--timestamp \
"$FRAMEWORK_PATH/Versions/A/OpenMultitouchSupportXCF"
codesign --force --sign "Developer ID Application" \
--options runtime \
--timestamp \
"$FRAMEWORK_PATH"
echo "✅ Framework re-signed successfully"
fi
# Re-sign the main app to ensure everything is consistent
echo "🔏 Re-signing main application..."
codesign --force --sign "Developer ID Application" \
--options runtime \
--entitlements "TrackWeight/TrackWeight.entitlements" \
--timestamp \
--deep \
--strict \
"$RUNNER_TEMP/export/${{ env.APP_NAME }}.app"
echo "✅ Application re-signed successfully"
- name: Verify Universal Binary and Code Signatures
run: |
echo "🏗️ Verifying Universal Binary Architecture..."
APP_BINARY="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app/Contents/MacOS/${{ env.APP_NAME }}"
if [[ -f "$APP_BINARY" ]]; then
echo "📊 Binary architectures:"
lipo -archs "$APP_BINARY"
if lipo -archs "$APP_BINARY" | grep -q "arm64" && lipo -archs "$APP_BINARY" | grep -q "x86_64"; then
echo "✅ Universal binary confirmed: Contains both ARM64 and x86_64"
else
echo "❌ Warning: Binary may not be universal"
lipo -detailed_info "$APP_BINARY"
fi
fi
# Check framework architecture if it exists
FRAMEWORK_PATH="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app/Contents/Frameworks/OpenMultitouchSupportXCF.framework"
if [[ -d "$FRAMEWORK_PATH" ]]; then
FRAMEWORK_BINARY="$FRAMEWORK_PATH/Versions/A/OpenMultitouchSupportXCF"
if [[ -f "$FRAMEWORK_BINARY" ]]; then
echo "📊 Framework architectures:"
lipo -archs "$FRAMEWORK_BINARY"
fi
fi
# Only verify signatures if certificates are available
if [[ -n "${{ secrets.BUILD_CERTIFICATE_BASE64 }}" ]]; then
echo "🔍 Verifying main application signature..."
codesign --verify --verbose "$RUNNER_TEMP/export/${{ env.APP_NAME }}.app"
echo "🔍 Verifying framework signature..."
if [[ -d "$FRAMEWORK_PATH" ]]; then
codesign --verify --verbose "$FRAMEWORK_PATH"
fi
echo "🔍 Deep verification with online validation..."
codesign --verify --deep --strict --verbose=2 "$RUNNER_TEMP/export/${{ env.APP_NAME }}.app"
echo "✅ All signature verifications passed"
else
echo "ℹ️ Skipping signature verification (no certificates provided)"
fi
- name: Notarize App
if: ${{ env.BUILD_CERTIFICATE_BASE64 != '' && env.APPLE_ID != '' }}
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
echo "🍎 Starting notarization process..."
# Create a zip file for notarization (notarytool prefers zip over raw .app)
cd "$RUNNER_TEMP/export"
# Use ditto for creating zip compatible with Apple's notarization service
ditto -c -k --keepParent "${{ env.APP_NAME }}.app" "${{ env.APP_NAME }}.zip"
# Submit for notarization
echo "📤 Submitting app for notarization..."
xcrun notarytool submit "${{ env.APP_NAME }}.zip" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait \
--timeout 20m
# Staple the notarization ticket to the app
echo "📎 Stapling notarization ticket..."
xcrun stapler staple "${{ env.APP_NAME }}.app"
# Verify the stapling worked
echo "✅ Verifying notarization..."
xcrun stapler validate "${{ env.APP_NAME }}.app"
echo "🎉 Notarization complete!"
- name: Install create-dmg
run: |
brew install create-dmg
- name: Create DMG
run: |
# Create a clean directory with only the app for DMG creation
echo "📁 Preparing clean DMG contents..."
DMG_STAGING="$RUNNER_TEMP/dmg_staging"
rm -rf "$DMG_STAGING"
mkdir -p "$DMG_STAGING"
# Copy only the app to staging directory
cp -R "$RUNNER_TEMP/export/${{ env.APP_NAME }}.app" "$DMG_STAGING/"
# Create clean professional DMG
create-dmg \
--volname "${{ env.APP_NAME }}" \
--volicon "$DMG_STAGING/${{ env.APP_NAME }}.app/Contents/Resources/AppIcon.icns" \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "${{ env.APP_NAME }}.app" 175 120 \
--hide-extension "${{ env.APP_NAME }}.app" \
--app-drop-link 425 120 \
--hdiutil-quiet \
"$RUNNER_TEMP/${{ env.APP_NAME }}.dmg" \
"$DMG_STAGING/"
- name: Get version info
id: version_info
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
else
VERSION=$(date +%Y%m%d-%H%M%S)
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "dmg_name=${{ env.APP_NAME }}.dmg" >> $GITHUB_OUTPUT
- name: Rename DMG with version
run: |
mv "$RUNNER_TEMP/${{ env.APP_NAME }}.dmg" "$RUNNER_TEMP/${{ steps.version_info.outputs.dmg_name }}"
- name: Upload DMG as artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.version_info.outputs.dmg_name }}
path: ${{ runner.temp }}/${{ steps.version_info.outputs.dmg_name }}
retention-days: 30
- name: Create Release
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.create_release == 'true')
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.version_info.outputs.version }}
name: ${{ env.APP_NAME }} ${{ steps.version_info.outputs.version }}
body: |
# TrackWeight ${{ steps.version_info.outputs.version }}
Transform your MacBook's trackpad into a precise digital weighing scale!
## Installation
1. Download the DMG file below
2. Open the DMG and drag TrackWeight.app to your Applications folder
3. Run the app and follow the setup instructions
## Requirements
- macOS 13.0 or later (Ventura or newer)
- MacBook with Force Touch trackpad (2015 or newer MacBook Pro, 2016 or newer MacBook)
## Usage
1. Open the scale
2. Rest your finger on the trackpad
3. While maintaining finger contact, put your object on the trackpad
4. Apply minimal pressure while maintaining contact to get the weight
files: |
${{ runner.temp }}/${{ steps.version_info.outputs.dmg_name }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
echo "✅ Successfully built and packaged TrackWeight DMG" >> $GITHUB_STEP_SUMMARY
echo "📦 DMG file: ${{ steps.version_info.outputs.dmg_name }}" >> $GITHUB_STEP_SUMMARY
echo "🔗 Original repository: https://github.com/KrishKrosh/TrackWeight" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The DMG includes:" >> $GITHUB_STEP_SUMMARY
echo "- TrackWeight.app (signed and notarized if certificates provided)" >> $GITHUB_STEP_SUMMARY
echo "- Clean professional DMG with just the app and Applications link" >> $GITHUB_STEP_SUMMARY
echo "- Proper code signing with hardened runtime enabled" >> $GITHUB_STEP_SUMMARY
================================================
FILE: .gitignore
================================================
# Mac
.DS_Store
# Xcode
xcuserdata/
*.xcuserstate
# Swift Package Manager
Packages.resolved
.swiftpm/
.build/
# Framework
*.framework/
*.xcframework/
*.zip
build/
# AI
.claude/
.env
.secrets
local_build/
================================================
FILE: ExportOptions.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>9ZRLG6277G</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Developer ID Application: Krish Shah (9ZRLG6277G)</string>
<key>stripSwiftSymbols</key>
<true/>
<key>destination</key>
<string>export</string>
<key>manageAppVersionAndBuildNumber</key>
<true/>
<key>compileBitcode</key>
<false/>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<false/>
</dict>
</plist>
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Krish Shah
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# TrackWeight
**Turn your MacBook's trackpad into a precise digital weighing scale**
[TrackWeight](
https://x.com/KrishRShah/status/1947186835811193330) is a macOS application that transforms your MacBook's trackpad into an accurate weighing scale by leveraging the Force Touch pressure sensors built into modern MacBook trackpads.
https://github.com/user-attachments/assets/7eaf9e0b-3dec-4829-b868-f54a8fd53a84
To use it yourself:
1. Open the scale
2. Rest your finger on the trackpad
3. While maintaining finger contact, put your object on the trackpad
4. Try to put as little pressure on the trackpad while still maintaining contact. This is the weight of your object
## How It Works
TrackWeight utilizes a custom fork of the [Open Multi-Touch Support library](https://github.com/krishkrosh/OpenMultitouchSupport) by [Takuto Nakamura](https://github.com/Kyome22) to gain private access to all mouse and trackpad events on macOS. This library provides detailed touch data including pressure readings that are normally inaccessible to standard applications.
The key insight is that trackpad pressure events are only generated when there's capacitance detected on the trackpad surface - meaning your finger (or another conductive object) must be in contact with the trackpad. When this condition is met, the trackpad's Force Touch sensors provide precise pressure readings that can be calibrated and converted into weight measurements.
## Requirements
- **macOS 13.0+** (Ventura or later)
- **MacBook with Force Touch trackpad** (2015 or newer MacBook Pro, 2016 or newer MacBook)
- **App Sandbox disabled** (required for low-level trackpad access)
- **Xcode 16.0+** and **Swift 6.0+** (for development)
## Installation
### Option 1: Download DMG (Recommended)
1. Go to the [Releases](https://github.com/krishkrosh/TrackWeight/releases) page
2. Download the latest TrackWeight DMG file
3. Open the DMG and drag TrackWeight.app to your Applications folder
4. Run the application (you may need to allow it in System Preferences > Security & Privacy for unsigned builds)
### Option 2: Homebrew
```bash
brew install --cask krishkrosh/apps/trackweight --force
```
### Option 3: Build from Source
1. Clone this repository
2. Open `TrackWeight.xcodeproj` in Xcode
3. Disable App Sandbox in the project settings (required for trackpad access)
4. Build and run the application
For more information about setting up the build pipeline, see [.github/workflows/README.md](.github/workflows/README.md).
### Calibration Process
The weight calculations have been validated by:
1. Placing the MacBook trackpad directly on top of a conventional digital scale
2. Applying various known weights while maintaining finger contact with the trackpad
3. Comparing and calibrating the pressure readings against the reference scale measurements
4. Ensuring consistent accuracy across different weight ranges
It turns out that the data we get from MultitouchSupport is already in grams!
## Limitations
- **Finger contact required**: The trackpad only provides pressure readings when it detects capacitance (finger touch), so you cannot weigh objects directly without maintaining contact
- **Surface contact**: Objects being weighed must be placed in a way that doesn't interfere with the required finger contact
- **Metal objects**: Metal objects may be detected as a finger touch, so you may need to place a piece of paper or a cloth between the object and the trackpad to get an accurate reading
## Technical Details
The application is built using:
- **SwiftUI** for the user interface
- **Combine** for reactive data flow
- **Open Multi-Touch Support library** for low-level trackpad access
### Open Multi-Touch Support Library
This project relies heavily on the excellent work by **Takuto Nakamura** ([@Kyome22](https://github.com/Kyome22)) and the [Open Multi-Touch Support library](https://github.com/krishkrosh/OpenMultitouchSupport). The library provides:
- Access to global multitouch events on macOS trackpads
- Detailed touch data including position, pressure, angle, and density
- Thread-safe async/await support for touch event streams
- Touch state tracking and comprehensive sensor data
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Disclaimer
This application is for experimental and educational purposes. While efforts have been made to ensure accuracy, TrackWeight should not be used for critical measurements or commercial applications where precision is essential. Always verify measurements with a calibrated scale for important use cases.
================================================
FILE: TrackWeight/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: TrackWeight/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: TrackWeight/ContentView.swift
================================================
//
// ContentView.swift
// TrackWeight
//
import SwiftUI
struct ContentView: View {
@State private var showHomePage = true
@State private var selectedTab = 1 // Start with Scale tab (index 1)
var body: some View {
if showHomePage {
HomeView {
showHomePage = false
}
.frame(minWidth: 700, minHeight: 500)
} else {
TabView(selection: $selectedTab) {
TrackWeightView()
.tabItem {
Image(systemName: "arrow.3.trianglepath")
Text("Guided (Experimental)")
}
.tag(0)
ScaleView()
.tabItem {
Image(systemName: "scalemass")
Text("Scale")
}
.tag(1)
SettingsView()
.tabItem {
Image(systemName: "gearshape")
Text("Settings")
}
.tag(2)
}
.frame(minWidth: 700, minHeight: 500)
}
}
}
#Preview {
ContentView()
}
================================================
FILE: TrackWeight/ContentViewModel.swift
================================================
//
// ContentViewModel.swift
// OMSDemo
//
// Created by Takuto Nakamura on 2024/03/02.
//
import OpenMultitouchSupport
import SwiftUI
@MainActor
final class ContentViewModel: ObservableObject {
@Published var touchData = [OMSTouchData]()
@Published var isListening: Bool = false
@Published var availableDevices = [OMSDeviceInfo]()
@Published var selectedDevice: OMSDeviceInfo?
private let manager = OMSManager.shared
private var task: Task<Void, Never>?
init() {
loadDevices()
}
func onAppear() {
task = Task { [weak self, manager] in
for await touchData in manager.touchDataStream {
await MainActor.run {
self?.touchData = touchData
}
}
}
}
func onDisappear() {
task?.cancel()
stop()
}
func start() {
if manager.startListening() {
isListening = true
}
}
func stop() {
if manager.stopListening() {
isListening = false
}
}
func loadDevices() {
availableDevices = manager.availableDevices
selectedDevice = manager.currentDevice
}
func selectDevice(_ device: OMSDeviceInfo) {
if manager.selectDevice(device) {
selectedDevice = device
}
}
}
================================================
FILE: TrackWeight/DebugView.swift
================================================
//
// DebugView.swift
// TrackWeight
//
// Created by Takuto Nakamura on 2024/03/02.
//
import OpenMultitouchSupport
import SwiftUI
struct DebugView: View {
@StateObject var viewModel = ContentViewModel()
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack {
// Header with close button
HStack {
Text("Debug Console")
.font(.title2)
.fontWeight(.semibold)
Spacer()
Button(action: {
dismiss()
}) {
Image(systemName: "xmark.circle.fill")
.font(.title2)
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())
.help("Close Debug Console")
}
.padding(.bottom)
// Device Selector
if !viewModel.availableDevices.isEmpty {
VStack(alignment: .leading) {
Text("Trackpad Device:")
.font(.headline)
Picker("Select Device", selection: Binding(
get: { viewModel.selectedDevice },
set: { device in
if let device = device {
viewModel.selectDevice(device)
}
}
)) {
ForEach(viewModel.availableDevices, id: \.self) { device in
Text("\(device.deviceName) (ID: \(device.deviceID))")
.tag(device as OMSDeviceInfo?)
}
}
.pickerStyle(MenuPickerStyle())
}
.padding(.bottom)
}
if viewModel.isListening {
Button {
viewModel.stop()
} label: {
Text("Stop")
}
} else {
Button {
viewModel.start()
} label: {
Text("Start")
}
}
Canvas { context, size in
viewModel.touchData.forEach { touch in
let path = makeEllipse(touch: touch, size: size)
context.fill(path, with: .color(.primary.opacity(Double(touch.total))))
}
}
.frame(width: 600, height: 400)
.border(Color.primary)
}
.fixedSize()
.padding()
.onAppear {
viewModel.onAppear()
}
.onDisappear {
viewModel.onDisappear()
}
}
private func makeEllipse(touch: OMSTouchData, size: CGSize) -> Path {
let x = Double(touch.position.x) * size.width
let y = Double(1.0 - touch.position.y) * size.height
let u = size.width / 100.0
let w = Double(touch.axis.major) * u
let h = Double(touch.axis.minor) * u
return Path(ellipseIn: CGRect(x: -0.5 * w, y: -0.5 * h, width: w, height: h))
.rotation(.radians(Double(-touch.angle)), anchor: .topLeading)
.offset(x: x, y: y)
.path(in: CGRect(origin: .zero, size: size))
}
}
#Preview {
DebugView()
}
================================================
FILE: TrackWeight/HomeView.swift
================================================
//
// HomeView.swift
// TrackWeight
//
import SwiftUI
struct HomeView: View {
let onBegin: () -> Void
var body: some View {
VStack(spacing: 40) {
Spacer()
// Title section
VStack(spacing: 15) {
Image(systemName: "scalemass")
.font(.system(size: 80, weight: .ultraLight))
.foregroundStyle(Color.blue)
Text("TrackWeight")
.font(.system(size: 48, weight: .bold, design: .rounded))
.foregroundStyle(
LinearGradient(
colors: [.blue, .teal, .cyan],
startPoint: .leading,
endPoint: .trailing
)
)
}
// Description section
VStack(spacing: 20) {
Text("Transform your MacBook trackpad into a precision scale using Apple's private MultitouchSupport framework to read pressure values with gram-level accuracy.")
.font(.system(size: 18, weight: .medium))
.foregroundStyle(Color.primary)
.multilineTextAlignment(.center)
.frame(maxWidth: 550)
// Limitations section
VStack(spacing: 12) {
Text("Important Limitations")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Color.orange)
VStack(spacing: 8) {
LimitationRow(
icon: "hand.point.up.left",
text: "Requires finger contact for capacitive detection"
)
LimitationRow(
icon: "chart.line.downtrend.xyaxis",
text: "May experience pressure drift when placing objects"
)
LimitationRow(
icon: "cube.fill",
text: "Metal/magnetic objects may not work"
)
}
}
.padding(.horizontal, 30)
.padding(.vertical, 20)
.background(
RoundedRectangle(cornerRadius: 15)
.foregroundColor(Color.orange.opacity(0.05))
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(Color.orange.opacity(0.2), lineWidth: 1)
)
)
.frame(maxWidth: 500)
}
Spacer()
// Begin button
Button(action: onBegin) {
HStack(spacing: 10) {
Text("Begin")
.font(.system(size: 18, weight: .semibold))
Image(systemName: "arrow.right")
.font(.system(size: 16, weight: .semibold))
}
.foregroundStyle(Color.white)
.frame(width: 140, height: 50)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(
LinearGradient(
colors: [.blue, .teal],
startPoint: .leading,
endPoint: .trailing
)
)
.shadow(color: .blue.opacity(0.3), radius: 10, x: 0, y: 5)
)
}
.buttonStyle(.plain)
.scaleEffect(1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: true)
.padding(.vertical, 10)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal, 40)
}
}
struct LimitationRow: View {
let icon: String
let text: String
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(Color.orange)
.frame(width: 20)
Text(text)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(Color.secondary)
.multilineTextAlignment(.leading)
Spacer()
}
}
}
#Preview {
HomeView(onBegin: {})
}
================================================
FILE: TrackWeight/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: TrackWeight/ScaleView.swift
================================================
//
// ScaleView.swift
// TrackWeight
//
import SwiftUI
struct ScaleView: View {
@StateObject private var viewModel = ScaleViewModel()
@State private var scaleCompression: CGFloat = 0
@State private var displayShake = false
@State private var particleOffset: CGFloat = 0
@State private var keyMonitor: Any?
var body: some View {
GeometryReader { geometry in
ZStack {
// Animated gradient background
// LinearGradient(
// colors: [
// Color(red: 0.95, green: 0.97, blue: 1.0),
// Color(red: 0.85, green: 0.92, blue: 0.98)
// ],
// startPoint: .topLeading,
// endPoint: .bottomTrailing
// )
// .ignoresSafeArea()
VStack(spacing: geometry.size.height * 0.06) {
// Title with subtitle directly underneath
VStack(spacing: 8) {
Text("Track Weight")
.font(.system(size: min(max(geometry.size.width * 0.05, 24), 42), weight: .bold, design: .rounded))
.foregroundStyle(
LinearGradient(
colors: [.blue, .teal, .cyan],
startPoint: .leading,
endPoint: .trailing
)
)
.minimumScaleFactor(0.7)
.lineLimit(1)
Text("Place your finger on the trackpad to begin")
.font(.system(size: min(max(geometry.size.width * 0.022, 14), 18), weight: .medium))
.foregroundStyle(.gray)
.multilineTextAlignment(.center)
.frame(maxWidth: geometry.size.width * 0.8)
.opacity(viewModel.hasTouch ? 0 : 1)
.animation(.easeInOut(duration: 0.5), value: viewModel.hasTouch)
}
.frame(height: max(geometry.size.height * 0.15, 80)) // Fixed height for title + subtitle
.frame(maxWidth: .infinity) // Ensure full width for centering
Spacer()
// Cartoon Digital Scale - responsive size
HStack {
Spacer()
CartoonScaleView(
weight: viewModel.currentWeight,
hasTouch: viewModel.hasTouch,
compression: $scaleCompression,
displayShake: $displayShake,
scaleFactor: min(geometry.size.width / 700, geometry.size.height / 500)
)
Spacer()
}
Spacer()
// Fixed container for button to prevent jumping
VStack(spacing: 10) {
if viewModel.hasTouch {
Text("Press spacebar or click to zero")
.font(.system(size: min(max(geometry.size.width * 0.018, 12), 16), weight: .medium))
.foregroundStyle(.gray)
}
Button(action: {
viewModel.zeroScale()
}) {
HStack(spacing: 8) {
Image(systemName: "arrow.clockwise")
.font(.system(size: min(max(geometry.size.width * 0.02, 14), 18), weight: .semibold))
Text("Zero Scale")
.font(.system(size: min(max(geometry.size.width * 0.02, 14), 18), weight: .semibold))
}
.foregroundStyle(.white)
.frame(width: min(max(geometry.size.width * 0.2, 140), 180),
height: min(max(geometry.size.height * 0.08, 40), 55))
.background(
RoundedRectangle(cornerRadius: 25)
.fill(
LinearGradient(
colors: [.blue, .teal],
startPoint: .leading,
endPoint: .trailing
)
)
)
}
.buttonStyle(.plain)
.opacity(viewModel.hasTouch ? 1 : 0)
.scaleEffect(viewModel.hasTouch ? 1 : 0.8)
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: viewModel.hasTouch)
}
.frame(height: min(max(geometry.size.height * 0.15, 80), 100)) // Fixed space for button + instruction
.frame(maxWidth: .infinity) // Ensure full width for centering
}
.padding(.horizontal, max(geometry.size.width * 0.05, 20))
.padding(.vertical, max(geometry.size.height * 0.03, 20))
.frame(maxWidth: .infinity, maxHeight: .infinity) // Ensure the VStack takes full available space
}
}
.focusable()
.modifier(FocusEffectModifier())
.onChange(of: viewModel.currentWeight) { newWeight in
withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
scaleCompression = CGFloat(min(newWeight / 100.0, 0.2))
}
}
.onAppear {
viewModel.startListening()
setupKeyMonitoring()
}
.onDisappear {
viewModel.stopListening()
removeKeyMonitoring()
}
}
private func setupKeyMonitoring() {
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
// Space key code is 49
if event.keyCode == 49 && viewModel.hasTouch {
viewModel.zeroScale()
}
return event
}
}
private func removeKeyMonitoring() {
if let monitor = keyMonitor {
NSEvent.removeMonitor(monitor)
keyMonitor = nil
}
}
}
struct CartoonScaleView: View {
let weight: Float
let hasTouch: Bool
@Binding var compression: CGFloat
@Binding var displayShake: Bool
let scaleFactor: CGFloat
var body: some View {
VStack(spacing: 0) {
// Scale platform (top) - responsive to weight
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
colors: [.gray.opacity(0.3), .gray.opacity(0.6)],
startPoint: .top,
endPoint: .bottom
)
)
.frame(width: 200 * scaleFactor, height: 12 * scaleFactor)
.offset(y: compression * 15)
// Scale body
ZStack {
// Main body
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
colors: [
Color(red: 0.95, green: 0.95, blue: 0.97),
Color(red: 0.85, green: 0.85, blue: 0.90)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 250 * scaleFactor, height: 150 * scaleFactor)
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 8)
// Display screen
RoundedRectangle(cornerRadius: 12)
.fill(.black)
.frame(width: 180 * scaleFactor, height: 60 * scaleFactor)
.offset(y: -10)
.overlay(
RoundedRectangle(cornerRadius: 12)
.fill(
LinearGradient(
colors: [.teal.opacity(0.8), .blue.opacity(0.6)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 176 * scaleFactor, height: 56 * scaleFactor)
.offset(y: -10)
)
// Weight display
VStack(spacing: 2) {
Text(String(format: "%.1f", weight))
.font(.system(size: 32 * scaleFactor, weight: .bold, design: .monospaced))
.foregroundStyle(.white)
.shadow(color: .teal, radius: hasTouch ? 2 : 0)
.animation(.easeInOut(duration: 0.2), value: weight)
Text("grams")
.font(.system(size: 12 * scaleFactor, weight: .medium))
.foregroundStyle(.white.opacity(0.8))
}
.offset(y: -10)
// Status indicator - simple and clean
if hasTouch {
Circle()
.fill(.teal)
.frame(width: 8 * scaleFactor, height: 8 * scaleFactor)
.offset(x: 90 * scaleFactor, y: -50 * scaleFactor)
}
// Fun face on the scale - positioned below the display screen
VStack(spacing: 8 * scaleFactor) {
// Eyes
HStack(spacing: 15 * scaleFactor) {
Circle()
.fill(.black)
.frame(width: 8 * scaleFactor, height: 8 * scaleFactor)
Circle()
.fill(.black)
.frame(width: 8 * scaleFactor, height: 8 * scaleFactor)
}
// Responsive mouth expression
Group {
if hasTouch && weight > 5 {
// Happy mouth when weighing something substantial
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: 20, y: 0), control: CGPoint(x: 0, y: 15))
}
.stroke(.black, lineWidth: 2 * scaleFactor)
.frame(width: 20 * scaleFactor, height: 10 * scaleFactor)
} else {
// Neutral mouth
Rectangle()
.fill(.black)
.frame(width: 12 * scaleFactor, height: 2 * scaleFactor)
}
}
.animation(.easeInOut(duration: 0.3), value: weight > 5)
}
.offset(y: 60 * scaleFactor) // Position well below the display screen
}
// Scale legs
HStack(spacing: 140 * scaleFactor) {
ForEach(0..<2, id: \.self) { _ in
RoundedRectangle(cornerRadius: 4)
.fill(.gray.opacity(0.7))
.frame(width: 12 * scaleFactor, height: 25 * scaleFactor)
.offset(y: compression * 3)
}
}
.offset(y: -5)
}
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: compression)
}
}
struct FocusEffectModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 14.0, *) {
content.focusEffectDisabled()
} else {
content
}
}
}
#Preview {
ScaleView()
}
================================================
FILE: TrackWeight/ScaleViewModel.swift
================================================
//
// ScaleViewModel.swift
// TrackWeight
//
import OpenMultitouchSupport
import SwiftUI
import Combine
@MainActor
final class ScaleViewModel: ObservableObject {
@Published var currentWeight: Float = 0.0
@Published var zeroOffset: Float = 0.0
@Published var isListening = false
@Published var hasTouch = false
private let manager = OMSManager.shared
private var task: Task<Void, Never>?
private var rawWeight: Float = 0.0
func startListening() {
if manager.startListening() {
isListening = true
}
task = Task { [weak self, manager] in
for await touchData in manager.touchDataStream {
await MainActor.run {
self?.processTouchData(touchData)
}
}
}
}
func stopListening() {
task?.cancel()
if manager.stopListening() {
isListening = false
hasTouch = false
currentWeight = 0.0
}
}
func zeroScale() {
if hasTouch {
zeroOffset = rawWeight
}
}
private func processTouchData(_ touchData: [OMSTouchData]) {
if touchData.isEmpty {
hasTouch = false
currentWeight = 0.0
zeroOffset = 0.0 // Reset zero when finger is lifted
} else {
hasTouch = true
rawWeight = touchData.first?.pressure ?? 0.0
currentWeight = max(0, rawWeight - zeroOffset)
}
}
deinit {
task?.cancel()
manager.stopListening()
}
}
================================================
FILE: TrackWeight/SettingsView.swift
================================================
//
// SettingsView.swift
// TrackWeight
//
import OpenMultitouchSupport
import SwiftUI
struct SettingsView: View {
@StateObject private var viewModel = ContentViewModel()
@State private var showDebugView = false
var body: some View {
VStack(spacing: 0) {
// Minimal Header
Text("Settings")
.font(.title)
.fontWeight(.medium)
.padding(.top, 32)
.padding(.bottom, 32)
// Settings Cards
VStack(spacing: 20) {
// Device Card
SettingsCard {
VStack(spacing: 20) {
// Status Row
HStack {
HStack(spacing: 12) {
Text("Trackpad")
.font(.headline)
.fontWeight(.medium)
}
Spacer()
if !viewModel.availableDevices.isEmpty {
Text("\(viewModel.availableDevices.count) device\(viewModel.availableDevices.count == 1 ? "" : "s")")
.font(.caption)
.foregroundColor(.secondary)
}
}
// Device Selector
if !viewModel.availableDevices.isEmpty {
HStack {
Picker("", selection: Binding(
get: { viewModel.selectedDevice },
set: { device in
if let device = device {
viewModel.selectDevice(device)
}
}
)) {
ForEach(viewModel.availableDevices, id: \.self) { device in
Text(device.deviceName)
.tag(device as OMSDeviceInfo?)
}
}
.pickerStyle(MenuPickerStyle())
Spacer()
}
} else {
HStack {
Text("No devices available")
.foregroundColor(.secondary)
Spacer()
}
}
}
}
// Debug Card
SettingsCard {
Button(action: { showDebugView = true }) {
HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
Text("Debug Console")
.font(.headline)
.fontWeight(.medium)
.foregroundColor(.primary)
Text("Raw touch data & diagnostics")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.fontWeight(.medium)
.foregroundColor(.secondary.opacity(0.6))
}
.contentShape(Rectangle())
}
.buttonStyle(CardButtonStyle())
}
}
.frame(maxWidth: 480)
.padding(.horizontal, 40)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(NSColor.windowBackgroundColor))
.sheet(isPresented: $showDebugView) {
DebugView()
.frame(minWidth: 700, minHeight: 500)
}
.onAppear {
viewModel.loadDevices()
}
}
}
struct SettingsCard<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack {
content
}
.padding(24)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.03), radius: 1, x: 0, y: 1)
.shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 4)
}
}
struct CardButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
#Preview {
SettingsView()
}
================================================
FILE: TrackWeight/TrackWeight.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Hardened Runtime (required for notarization) -->
<key>com.apple.security.cs.allow-jit</key>
<false/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<false/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<false/>
<key>com.apple.security.cs.disable-library-validation</key>
<false/>
<!-- App Sandbox disabled (needed for trackpad access) -->
<key>com.apple.security.app-sandbox</key>
<false/>
<!-- Device access for trackpad -->
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.security.device.serial</key>
<true/>
<!-- Network access (if needed) -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
================================================
FILE: TrackWeight/TrackWeightApp.swift
================================================
//
// TrackWeightApp.swift
// TrackWeight
//
// Created by Takuto Nakamura on 2024/03/02.
//
import SwiftUI
@main
struct TrackWeightApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
}
================================================
FILE: TrackWeight/TrackWeightView.swift
================================================
//
// TrackWeightView.swift
// TrackWeight
//
import SwiftUI
struct TrackWeightView: View {
@StateObject private var viewModel = WeighingViewModel()
var body: some View {
VStack(spacing: 30) {
switch viewModel.state {
case .welcome:
WelcomeView {
viewModel.startWeighing()
}
case .waitingForFinger:
FingerTimerView(
progress: viewModel.fingerTimer,
hasDetectedFinger: viewModel.fingerTimer > 0
)
case .waitingForItem:
InstructionView(
title: "Place your item",
subtitle: "While maintaining contact with the trackpad, gently place your item. Use as little pressure as possible with your reference finger.",
disclaimer: "Keep your finger still and apply minimal pressure",
icon: "cube.box"
)
case .weighing:
WeighingView(
currentPressure: viewModel.currentPressure,
isStabilizing: viewModel.isStabilizing,
stabilityProgress: viewModel.stabilityProgress
)
case .result(let weight):
ResultView(weight: weight) {
viewModel.restart()
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.windowBackgroundColor))
.animation(.easeInOut(duration: 0.6), value: viewModel.state)
}
}
struct WelcomeView: View {
let onStart: () -> Void
var body: some View {
VStack(spacing: 25) {
Image(systemName: "scalemass")
.font(.system(size: 80, weight: .ultraLight))
.foregroundStyle(.primary)
Text("TrackWeight")
.font(.system(size: 36, weight: .bold, design: .rounded))
.foregroundStyle(.primary)
Text("Turn your trackpad into a precision scale. Place objects and get their weight in grams.")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 400)
VStack(spacing: 15) {
Button(action: onStart) {
Text("Begin")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 120, height: 40)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
)
}
.buttonStyle(.plain)
}
}
}
}
struct FingerTimerView: View {
let progress: Float
let hasDetectedFinger: Bool
var body: some View {
VStack(spacing: 30) {
Image(systemName: "hand.point.up.left")
.font(.system(size: 60, weight: .light))
.foregroundStyle(.blue)
Text("Hold your finger steady")
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundStyle(.primary)
VStack(spacing: 8) {
Text("Keep your finger on the trackpad for 3 seconds")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 300)
Text("This establishes your baseline pressure")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.tertiary)
.multilineTextAlignment(.center)
.frame(maxWidth: 300)
}
// Bubble filling animation
ZStack {
Circle()
.stroke(.blue.opacity(0.3), lineWidth: 4)
.frame(width: 100, height: 100)
Circle()
.fill(.blue.opacity(0.2))
.frame(width: 100, height: 100)
if hasDetectedFinger {
Circle()
.trim(from: 0, to: CGFloat(progress))
.stroke(.blue, style: StrokeStyle(lineWidth: 4, lineCap: .round))
.frame(width: 100, height: 100)
.rotationEffect(.degrees(-90))
.animation(.linear(duration: 0.1), value: progress)
// Gentle bubble effect
Circle()
.fill(
RadialGradient(
colors: [.blue.opacity(0.3), .blue.opacity(0.1)],
center: .center,
startRadius: 0,
endRadius: 50
)
)
.frame(width: CGFloat(progress) * 80 + 20, height: CGFloat(progress) * 80 + 20)
.animation(.easeInOut(duration: 0.2), value: progress)
Text("\(Int((1 - progress) * 3) + 1)")
.font(.system(size: 24, weight: .bold, design: .monospaced))
.foregroundStyle(.blue)
}
}
.scaleEffect(hasDetectedFinger ? 1.0 : 0.8)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: hasDetectedFinger)
}
}
}
struct InstructionView: View {
let title: String
let subtitle: String
let disclaimer: String?
let icon: String
init(title: String, subtitle: String, disclaimer: String? = nil, icon: String) {
self.title = title
self.subtitle = subtitle
self.disclaimer = disclaimer
self.icon = icon
}
var body: some View {
VStack(spacing: 20) {
Image(systemName: icon)
.font(.system(size: 60, weight: .light))
.foregroundStyle(.blue)
Text(title)
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundStyle(.primary)
VStack(spacing: 10) {
Text(subtitle)
.font(.system(size: 16, weight: .medium))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 350)
if let disclaimer = disclaimer {
Text(disclaimer)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.orange)
.multilineTextAlignment(.center)
.frame(maxWidth: 300)
}
}
}
}
}
struct WeighingView: View {
let currentPressure: Float
let isStabilizing: Bool
let stabilityProgress: Float
var body: some View {
VStack(spacing: 30) {
Text("Weighing...")
.font(.system(size: 24, weight: .semibold, design: .rounded))
.foregroundStyle(.primary)
VStack(spacing: 10) {
Text(String(format: "%.1f", currentPressure))
.font(.system(size: 64, weight: .bold, design: .monospaced))
.foregroundStyle(.blue)
.animation(.easeInOut(duration: 0.2), value: currentPressure)
Text("grams")
.font(.system(size: 20, weight: .medium))
.foregroundStyle(.secondary)
}
VStack(spacing: 12) {
Text("Release pressure while maintaining contact")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(.orange)
.multilineTextAlignment(.center)
Text("Keep your finger on the trackpad but apply as little pressure as possible")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 350)
if isStabilizing {
VStack(spacing: 8) {
Text("Stabilizing...")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.orange)
// Progress bar
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(.orange.opacity(0.2))
.frame(width: 200, height: 8)
HStack {
RoundedRectangle(cornerRadius: 4)
.fill(.orange)
.frame(width: 200 * CGFloat(stabilityProgress), height: 8)
.animation(.linear(duration: 0.1), value: stabilityProgress)
Spacer()
}
}
.frame(width: 200)
Text("\(Int((1 - stabilityProgress) * 2) + 1)s remaining")
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(.orange.opacity(0.8))
}
}
}
.frame(maxWidth: 350)
}
}
}
struct ResultView: View {
let weight: Float
let onRestart: () -> Void
var body: some View {
VStack(spacing: 30) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 60))
.foregroundStyle(.green)
.scaleEffect(1.0)
.onAppear {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
// Animation handled by parent view
}
}
Text("Your object weighs")
.font(.system(size: 20, weight: .medium))
.foregroundStyle(.secondary)
VStack(spacing: 5) {
Text(String(format: "%.1f", weight))
.font(.system(size: 56, weight: .bold, design: .monospaced))
.foregroundStyle(.primary)
Text("grams")
.font(.system(size: 18, weight: .medium))
.foregroundStyle(.secondary)
}
Button(action: onRestart) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 20, weight: .medium))
.foregroundStyle(.blue)
.frame(width: 44, height: 44)
.background(
Circle()
.fill(.blue.opacity(0.1))
)
}
.buttonStyle(.plain)
}
}
}
#Preview {
TrackWeightView()
}
================================================
FILE: TrackWeight/WeighingState.swift
================================================
//
// WeighingState.swift
// TrackWeight
//
import Foundation
enum WeighingState: Equatable {
case welcome
case waitingForFinger
case waitingForItem
case weighing
case result(weight: Float)
}
================================================
FILE: TrackWeight/WeighingViewModel.swift
================================================
//
// WeighingViewModel.swift
// TrackWeight
//
import OpenMultitouchSupport
import SwiftUI
import Combine
@MainActor
final class WeighingViewModel: ObservableObject {
@Published var state: WeighingState = .welcome
@Published var currentPressure: Float = 0.0
@Published var maxPressure: Float = 0.0
@Published var isListening = false
@Published var fingerTimer: Float = 0.0 // 0.0 to 1.0 for animation
private let manager = OMSManager.shared
private var task: Task<Void, Never>?
private var timerTask: Task<Void, Never>?
private var baselinePressure: Float = 0.0
private var hasDetectedFinger = false
private var hasDetectedItem = false
private var finalWeight: Float = 0.0
private let fingerHoldDuration: TimeInterval = 3.0
private var pressureHistory: [Float] = []
private let historySize = 10
private let rateOfChangeThreshold: Float = 5
// Weighing stability properties
private let stabilityDuration: TimeInterval = 3.0
private let stabilityAnimationDelay: TimeInterval = 1.0 // Show animation after 1s of stability
private var stabilityStartTime: Date?
private var stableWeight: Float = 0.0
private let stabilityThreshold: Float = 2.0 // Max allowed weight variation
@Published var stabilityProgress: Float = 0.0
@Published var isStabilizing: Bool = false
func startWeighing() {
state = .waitingForFinger
hasDetectedFinger = false
hasDetectedItem = false
baselinePressure = 0.0
currentPressure = 0.0
maxPressure = 0.0
finalWeight = 0.0
fingerTimer = 0.0
stabilityProgress = 0.0
stabilityStartTime = nil
stableWeight = 0.0
isStabilizing = false
pressureHistory.removeAll()
if manager.startListening() {
isListening = true
}
task = Task { [weak self, manager] in
for await touchData in manager.touchDataStream {
await MainActor.run {
self?.processTouchData(touchData)
}
}
}
}
func restart() {
stopListening()
state = .welcome
fingerTimer = 0.0
stabilityProgress = 0.0
stabilityStartTime = nil
isStabilizing = false
}
private func stopListening() {
task?.cancel()
timerTask?.cancel()
if manager.stopListening() {
isListening = false
}
}
private func startFingerTimer() {
timerTask?.cancel()
fingerTimer = 0.0
timerTask = Task { [weak self] in
let startTime = Date()
while !Task.isCancelled {
let elapsed = Date().timeIntervalSince(startTime)
let progress = min(elapsed / (self?.fingerHoldDuration ?? 3.0), 1.0)
await MainActor.run {
self?.fingerTimer = Float(progress)
}
if progress >= 1.0 {
await MainActor.run {
self?.completeFingerTimer()
}
break
}
try? await Task.sleep(nanoseconds: 16_666_667) // ~60fps
}
}
}
private func resetFingerTimer() {
timerTask?.cancel()
fingerTimer = 0.0
}
private func completeFingerTimer() {
hasDetectedFinger = true
baselinePressure = currentPressure
state = .waitingForItem
timerTask?.cancel()
}
private func startStabilityTimer(with weight: Float) {
// stabilityStartTime and stableWeight are already set in the calling code
stabilityProgress = 0.0
isStabilizing = true // Start showing animation since we're already past the 1s delay
timerTask = Task { [weak self] in
// We start from the point where animation should begin (after 1s delay)
let animationStartTime = Date()
let remainingDuration = (self?.stabilityDuration ?? 3.0) - (self?.stabilityAnimationDelay ?? 1.0)
while !Task.isCancelled {
let elapsed = Date().timeIntervalSince(animationStartTime)
let progress = min(elapsed / remainingDuration, 1.0)
await MainActor.run {
self?.stabilityProgress = Float(progress)
}
if progress >= 1.0 {
await MainActor.run {
self?.completeWeighing()
}
break
}
try? await Task.sleep(nanoseconds: 16_666_667) // ~60fps
}
}
}
private func completeWeighing() {
state = .result(weight: currentPressure)
stopListening()
}
private func resetStabilityTimer() {
stabilityStartTime = nil
stabilityProgress = 0.0
isStabilizing = false
timerTask?.cancel()
}
private func processTouchData(_ touchData: [OMSTouchData]) {
guard !touchData.isEmpty else {
// Reset timer if finger is lifted during waiting
if state == .waitingForFinger && !hasDetectedFinger {
resetFingerTimer()
}
if state == .weighing {
if hasDetectedItem && finalWeight > 0 {
state = .result(weight: finalWeight)
stopListening()
}
}
return
}
let mainTouch = touchData.first!
currentPressure = mainTouch.pressure
// Add current pressure to history
pressureHistory.append(currentPressure)
if pressureHistory.count > historySize {
pressureHistory.removeFirst()
}
// log the average pressure (moving avg)
let avgPressure = pressureHistory.reduce(0, +) / Float(pressureHistory.count)
print("average pressure: \(avgPressure)")
currentPressure = avgPressure
switch state {
case .waitingForFinger:
if !hasDetectedFinger {
currentPressure = mainTouch.pressure
if fingerTimer == 0.0 {
startFingerTimer()
}
}
case .waitingForItem:
if hasDetectedFinger {
// Calculate rate of change if we have enough history
if pressureHistory.count == historySize && !hasDetectedItem {
let rateOfChange = pressureHistory.last! - pressureHistory.first!
if rateOfChange > rateOfChangeThreshold {
print("pressure before item: \(pressureHistory)")
print("Old baseline: \(baselinePressure)")
baselinePressure = pressureHistory.first!
print("New baseline: \(baselinePressure)")
hasDetectedItem = true
state = .weighing
resetStabilityTimer()
}
}
} else {
state = .waitingForFinger
pressureHistory.removeAll()
}
case .weighing:
// Check if weight is stable (hasn't changed by more than 2g)
let weightDifference = stabilityStartTime != nil ? abs(currentPressure - stableWeight) : 0
if stabilityStartTime == nil {
// First reading in weighing state - start tracking stability
stabilityStartTime = Date()
stableWeight = currentPressure
} else if weightDifference > stabilityThreshold {
// Weight became unstable, reset stability tracking
resetStabilityTimer()
stabilityStartTime = Date()
stableWeight = currentPressure
} else {
// Weight is stable, check if we should start the stabilization process
let timeSinceStable = Date().timeIntervalSince(stabilityStartTime!)
if timeSinceStable >= stabilityAnimationDelay && !isStabilizing {
// Weight has been stable for at least 1 second, start stabilization timer
startStabilityTimer(with: stableWeight)
}
}
default:
break
}
}
deinit {
task?.cancel()
timerTask?.cancel()
manager.stopListening()
}
}
================================================
FILE: TrackWeight.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
77292A882B931953001CA3F6 /* TrackWeightApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A872B931953001CA3F6 /* TrackWeightApp.swift */; };
77292A8A2B931953001CA3F6 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A892B931953001CA3F6 /* ContentView.swift */; };
77292A8C2B931954001CA3F6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77292A8B2B931954001CA3F6 /* Assets.xcassets */; };
77292A8F2B931954001CA3F6 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77292A8E2B931954001CA3F6 /* Preview Assets.xcassets */; };
77292A982B931D60001CA3F6 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A972B931D60001CA3F6 /* ContentViewModel.swift */; };
77292A9C2B931E01001CA3F6 /* WeighingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A9D2B931E01001CA3F6 /* WeighingState.swift */; };
77292A9E2B931E02001CA3F6 /* WeighingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292A9F2B931E02001CA3F6 /* WeighingViewModel.swift */; };
77292AA02B931E03001CA3F6 /* TrackWeightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292AA12B931E03001CA3F6 /* TrackWeightView.swift */; };
77292AA22B931E04001CA3F6 /* DebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292AA32B931E04001CA3F6 /* DebugView.swift */; };
77292AA42B931E05001CA3F6 /* ScaleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292AA52B931E05001CA3F6 /* ScaleView.swift */; };
77292AA62B931E06001CA3F6 /* ScaleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77292AA72B931E06001CA3F6 /* ScaleViewModel.swift */; };
93A095122E33359600E1E1D1 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A095112E33359600E1E1D1 /* SettingsView.swift */; };
93A095162E33624200E1E1D1 /* OpenMultitouchSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 93A095152E33624200E1E1D1 /* OpenMultitouchSupport */; };
93ABD0212E2E01E200668D4F /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ABD0202E2E01E200668D4F /* HomeView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
77292A842B931953001CA3F6 /* TrackWeight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TrackWeight.app; sourceTree = BUILT_PRODUCTS_DIR; };
77292A872B931953001CA3F6 /* TrackWeightApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackWeightApp.swift; sourceTree = "<group>"; };
77292A892B931953001CA3F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
77292A8B2B931954001CA3F6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
77292A8E2B931954001CA3F6 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
77292A902B931954001CA3F6 /* TrackWeight.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TrackWeight.entitlements; sourceTree = "<group>"; };
77292A972B931D60001CA3F6 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = "<group>"; };
77292A9D2B931E01001CA3F6 /* WeighingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeighingState.swift; sourceTree = "<group>"; };
77292A9F2B931E02001CA3F6 /* WeighingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeighingViewModel.swift; sourceTree = "<group>"; };
77292AA12B931E03001CA3F6 /* TrackWeightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackWeightView.swift; sourceTree = "<group>"; };
77292AA32B931E04001CA3F6 /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = "<group>"; };
77292AA52B931E05001CA3F6 /* ScaleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleView.swift; sourceTree = "<group>"; };
77292AA72B931E06001CA3F6 /* ScaleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleViewModel.swift; sourceTree = "<group>"; };
93A095112E33359600E1E1D1 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
93ABD0202E2E01E200668D4F /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
77292A812B931953001CA3F6 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
93A095162E33624200E1E1D1 /* OpenMultitouchSupport in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
77292A7B2B931953001CA3F6 = {
isa = PBXGroup;
children = (
77292A862B931953001CA3F6 /* TrackWeight */,
77292A852B931953001CA3F6 /* Products */,
77292A992B931D7A001CA3F6 /* Frameworks */,
);
sourceTree = "<group>";
};
77292A852B931953001CA3F6 /* Products */ = {
isa = PBXGroup;
children = (
77292A842B931953001CA3F6 /* TrackWeight.app */,
);
name = Products;
sourceTree = "<group>";
};
77292A862B931953001CA3F6 /* TrackWeight */ = {
isa = PBXGroup;
children = (
77292A902B931954001CA3F6 /* TrackWeight.entitlements */,
77292A8B2B931954001CA3F6 /* Assets.xcassets */,
77292A872B931953001CA3F6 /* TrackWeightApp.swift */,
77292A892B931953001CA3F6 /* ContentView.swift */,
77292A972B931D60001CA3F6 /* ContentViewModel.swift */,
77292A9D2B931E01001CA3F6 /* WeighingState.swift */,
93ABD0202E2E01E200668D4F /* HomeView.swift */,
77292A9F2B931E02001CA3F6 /* WeighingViewModel.swift */,
93A095112E33359600E1E1D1 /* SettingsView.swift */,
77292AA12B931E03001CA3F6 /* TrackWeightView.swift */,
77292AA32B931E04001CA3F6 /* DebugView.swift */,
77292AA52B931E05001CA3F6 /* ScaleView.swift */,
77292AA72B931E06001CA3F6 /* ScaleViewModel.swift */,
77292A8D2B931954001CA3F6 /* Preview Content */,
);
path = TrackWeight;
sourceTree = "<group>";
};
77292A8D2B931954001CA3F6 /* Preview Content */ = {
isa = PBXGroup;
children = (
77292A8E2B931954001CA3F6 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
77292A992B931D7A001CA3F6 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
77292A832B931953001CA3F6 /* TrackWeight */ = {
isa = PBXNativeTarget;
buildConfigurationList = 77292A932B931954001CA3F6 /* Build configuration list for PBXNativeTarget "TrackWeight" */;
buildPhases = (
77292A802B931953001CA3F6 /* Sources */,
77292A812B931953001CA3F6 /* Frameworks */,
77292A822B931953001CA3F6 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = TrackWeight;
packageProductDependencies = (
93A095152E33624200E1E1D1 /* OpenMultitouchSupport */,
);
productName = TrackWeight;
productReference = 77292A842B931953001CA3F6 /* TrackWeight.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
77292A7C2B931953001CA3F6 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1520;
LastUpgradeCheck = 1620;
TargetAttributes = {
77292A832B931953001CA3F6 = {
CreatedOnToolsVersion = 15.2;
};
};
};
buildConfigurationList = 77292A7F2B931953001CA3F6 /* Build configuration list for PBXProject "TrackWeight" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 77292A7B2B931953001CA3F6;
packageReferences = (
93A095142E33624200E1E1D1 /* XCRemoteSwiftPackageReference "OpenMultitouchSupport" */,
);
productRefGroup = 77292A852B931953001CA3F6 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
77292A832B931953001CA3F6 /* TrackWeight */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
77292A822B931953001CA3F6 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
77292A8F2B931954001CA3F6 /* Preview Assets.xcassets in Resources */,
77292A8C2B931954001CA3F6 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
77292A802B931953001CA3F6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
77292A8A2B931953001CA3F6 /* ContentView.swift in Sources */,
77292A982B931D60001CA3F6 /* ContentViewModel.swift in Sources */,
77292A882B931953001CA3F6 /* TrackWeightApp.swift in Sources */,
93ABD0212E2E01E200668D4F /* HomeView.swift in Sources */,
77292A9C2B931E01001CA3F6 /* WeighingState.swift in Sources */,
77292A9E2B931E02001CA3F6 /* WeighingViewModel.swift in Sources */,
77292AA02B931E03001CA3F6 /* TrackWeightView.swift in Sources */,
77292AA22B931E04001CA3F6 /* DebugView.swift in Sources */,
77292AA42B931E05001CA3F6 /* ScaleView.swift in Sources */,
77292AA62B931E06001CA3F6 /* ScaleViewModel.swift in Sources */,
93A095122E33359600E1E1D1 /* SettingsView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
77292A912B931954001CA3F6 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
77292A922B931954001CA3F6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
77292A942B931954001CA3F6 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = TrackWeight/TrackWeight.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 9ZRLG6277G;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.krishkrosh.trackweight;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
};
name = Debug;
};
77292A952B931954001CA3F6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = TrackWeight/TrackWeight.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 9ZRLG6277G;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.krishkrosh.trackweight;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
77292A7F2B931953001CA3F6 /* Build configuration list for PBXProject "TrackWeight" */ = {
isa = XCConfigurationList;
buildConfigurations = (
77292A912B931954001CA3F6 /* Debug */,
77292A922B931954001CA3F6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
77292A932B931954001CA3F6 /* Build configuration list for PBXNativeTarget "TrackWeight" */ = {
isa = XCConfigurationList;
buildConfigurations = (
77292A942B931954001CA3F6 /* Debug */,
77292A952B931954001CA3F6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
93A095142E33624200E1E1D1 /* XCRemoteSwiftPackageReference "OpenMultitouchSupport" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/KrishKrosh/OpenMultitouchSupport.git";
requirement = {
branch = main;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
93A095152E33624200E1E1D1 /* OpenMultitouchSupport */ = {
isa = XCSwiftPackageProductDependency;
package = 93A095142E33624200E1E1D1 /* XCRemoteSwiftPackageReference "OpenMultitouchSupport" */;
productName = OpenMultitouchSupport;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 77292A7C2B931953001CA3F6 /* Project object */;
}
================================================
FILE: TrackWeight.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
================================================
FILE: TrackWeight.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
================================================
FILE: TrackWeight.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
================================================
{
"originHash" : "231443e4712351510ab418164c13b6d5894774039d8c118ed8208a5fc9fe884e",
"pins" : [
{
"identity" : "openmultitouchsupport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/KrishKrosh/OpenMultitouchSupport.git",
"state" : {
"branch" : "main",
"revision" : "fb6991fa1ece8c5faa8eef49190beb2baf071694"
}
}
],
"version" : 3
}
================================================
FILE: scripts/README.md
================================================
# Scripts
This directory contains helper scripts for the TrackWeight project.
## setup-signing.sh
A helper script to set up code signing certificates for automated DMG builds.
### Usage
```bash
./scripts/setup-signing.sh
```
### What it does
1. **Guides you through certificate export**: Provides step-by-step instructions to export your Developer ID Application certificate from Keychain Access
2. **Encodes certificates**: Converts your .p12 certificate file to base64 format required for GitHub Secrets
3. **Generates secret values**: Provides the exact values you need to add as GitHub repository secrets
4. **Optional provisioning profile**: Handles provisioning profile encoding if needed
### Prerequisites
- macOS (required for Keychain Access and signing tools)
- Valid Apple Developer ID Application certificate
- Access to GitHub repository settings to add secrets
### Output
The script will generate the values for these GitHub repository secrets:
- `BUILD_CERTIFICATE_BASE64`: Base64-encoded .p12 certificate
- `P12_PASSWORD`: Certificate password
- `BUILD_PROVISION_PROFILE_BASE64`: Base64-encoded provisioning profile (optional)
### Adding Secrets to GitHub
1. Go to your GitHub repository
2. Navigate to Settings > Secrets and variables > Actions
3. Click "New repository secret"
4. Add each secret with the name and value provided by the script
### Attribution
This script is part of the enhanced TrackWeight fork that adds automated build pipelines.
**Original TrackWeight project**: https://github.com/KrishKrosh/TrackWeight
**Created by**: Krish Shah (@KrishKrosh)
================================================
FILE: scripts/setup-signing.sh
================================================
#!/bin/bash
# setup-signing.sh
# Helper script to set up code signing certificates for TrackWeight DMG builds
#
# This script helps you prepare the necessary secrets for GitHub Actions
# to build signed DMG files.
set -e
echo "🔐 TrackWeight Code Signing Setup"
echo "=================================="
echo ""
echo "This script helps you set up code signing for automated DMG builds."
echo "You'll need a valid Apple Developer ID Application certificate."
echo ""
# Check if we're on macOS
if [[ "$OSTYPE" != "darwin"* ]]; then
echo "❌ This script must be run on macOS to access Keychain and signing tools."
exit 1
fi
# Function to encode file to base64
encode_file() {
local file_path="$1"
if [[ -f "$file_path" ]]; then
base64 -i "$file_path"
else
echo "❌ File not found: $file_path"
return 1
fi
}
echo "Step 1: Export your Developer ID Application certificate"
echo "--------------------------------------------------------"
echo "1. Open Keychain Access"
echo "2. Find your 'Developer ID Application' certificate"
echo "3. Right-click and select 'Export'"
echo "4. Save as .p12 format with a password"
echo ""
read -p "Enter the path to your exported .p12 certificate: " cert_path
if [[ ! -f "$cert_path" ]]; then
echo "❌ Certificate file not found: $cert_path"
exit 1
fi
echo ""
read -s -p "Enter the password for your .p12 certificate: " cert_password
echo ""
echo ""
echo "Step 2: Encoding certificate for GitHub Secrets"
echo "----------------------------------------------"
# Encode the certificate
echo "Encoding certificate..."
cert_base64=$(encode_file "$cert_path")
if [[ -z "$cert_base64" ]]; then
echo "❌ Failed to encode certificate"
exit 1
fi
echo "✅ Certificate encoded successfully"
echo ""
echo "Step 3: GitHub Repository Secrets"
echo "--------------------------------"
echo "Add these secrets to your GitHub repository:"
echo "(Go to Settings > Secrets and variables > Actions)"
echo ""
echo "1. Secret name: BUILD_CERTIFICATE_BASE64"
echo " Value: (copy the text below)"
echo ""
echo "$cert_base64"
echo ""
echo "2. Secret name: P12_PASSWORD"
echo " Value: $cert_password"
echo ""
echo "3. Secret name: APPLE_ID"
echo " Value: your-apple-id@example.com"
echo ""
echo "4. Secret name: APPLE_ID_PASSWORD"
echo " Value: (App-specific password - see instructions below)"
echo ""
echo "5. Secret name: APPLE_TEAM_ID"
echo " Value: (Your 10-character Team ID - see instructions below)"
echo ""
# Check for provisioning profile (optional for Developer ID)
echo "Step 4: Provisioning Profile (Optional)"
echo "--------------------------------------"
read -p "Do you have a provisioning profile to include? (y/n): " include_profile
if [[ "$include_profile" =~ ^[Yy]$ ]]; then
read -p "Enter the path to your .mobileprovision file: " profile_path
if [[ -f "$profile_path" ]]; then
profile_base64=$(encode_file "$profile_path")
echo ""
echo "3. Secret name: BUILD_PROVISION_PROFILE_BASE64"
echo " Value: (copy the text below)"
echo ""
echo "$profile_base64"
else
echo "❌ Provisioning profile not found: $profile_path"
fi
else
echo "📝 Skipping provisioning profile (Developer ID usually doesn't need one)"
fi
echo ""
echo "Step 5: Additional Secrets for Notarization"
echo "-------------------------------------------"
echo "For full notarization (eliminates security warnings), you'll also need:"
echo ""
echo "📧 Apple ID (APPLE_ID):"
echo " - Use your Apple Developer account email"
echo ""
echo "🔑 App-Specific Password (APPLE_ID_PASSWORD):"
echo " 1. Go to appleid.apple.com"
echo " 2. Sign in with your Apple ID"
echo " 3. In the 'App-Specific Passwords' section, click 'Generate Password'"
echo " 4. Label it something like 'GitHub Actions Notarization'"
echo " 5. Copy the generated password (xxxx-xxxx-xxxx-xxxx)"
echo ""
echo "🏢 Team ID (APPLE_TEAM_ID):"
echo " 1. Go to developer.apple.com"
echo " 2. Sign in and go to 'Membership'"
echo " 3. Your Team ID is the 10-character string (e.g., ABC1234567)"
echo ""
echo "ℹ️ Without these notarization secrets, the app will still be signed but users"
echo " will see security warnings when trying to run it."
echo ""
echo "🎉 Setup Complete!"
echo "================="
echo ""
echo "Next steps:"
echo "1. Add ALL the secrets to your GitHub repository"
echo "2. Create a git tag to trigger the build: git tag v1.0.0 && git push origin v1.0.0"
echo "3. Or manually trigger the workflow from the Actions tab"
echo ""
echo "The workflow will:"
echo "- Build and sign your app with the provided certificate"
echo "- Notarize the app with Apple (eliminates security warnings)"
echo "- Create a professional DMG with attribution to the original repo"
echo "- Upload the DMG as a release artifact"
echo ""
echo "Original TrackWeight project: https://github.com/KrishKrosh/TrackWeight"
================================================
FILE: scripts/test-build-locally.sh
================================================
#!/bin/bash
# test-build-locally.sh
# Test the GitHub Actions workflow steps locally on macOS
set -e
echo "🧪 Testing TrackWeight Build Workflow Locally"
echo "=============================================="
# Load environment variables from .env file
if [[ -f ".env" ]]; then
echo "📄 Loading environment variables from .env file..."
# Export all variables from .env file
set -a # automatically export all variables
source .env
set +a # turn off automatic export
echo "✅ Environment variables loaded"
else
echo "⚠️ No .env file found - some features may not work"
fi
# Configuration
export APP_NAME="TrackWeight"
export SCHEME="TrackWeight"
export CONFIGURATION="Release"
export BUILD_DIR="$(pwd)/local_build"
# Clean up previous builds
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
echo ""
echo "Step 1: Building and Archiving App (Universal Binary)"
echo "====================================================="
xcodebuild \
-project TrackWeight.xcodeproj \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$BUILD_DIR/$APP_NAME.xcarchive" \
-destination 'generic/platform=macOS' \
ARCHS="arm64 x86_64" \
ONLY_ACTIVE_ARCH=NO \
archive
echo ""
echo "Step 1.5: Setting up Code Signing (if available)"
echo "================================================"
if [[ -n "$BUILD_CERTIFICATE_BASE64" && -n "$P12_PASSWORD" ]]; then
echo "🔐 Setting up code signing certificate from .env..."
# Decode and import certificate
echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > "$BUILD_DIR/certificate.p12"
# Import into keychain (temporary)
security import "$BUILD_DIR/certificate.p12" -k ~/Library/Keychains/login.keychain-db -P "$P12_PASSWORD" -T /usr/bin/codesign
echo "✅ Certificate imported successfully"
echo "📝 Note: Certificate will remain in keychain after script completion"
else
echo "⚠️ No certificate in .env - will use existing keychain certificates"
fi
echo ""
echo "Step 2: Exporting App"
echo "===================="
# Use existing ExportOptions.plist or create a basic one
if [[ ! -f "ExportOptions.plist" ]]; then
echo "Creating basic ExportOptions.plist..."
cat > "$BUILD_DIR/ExportOptions.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>destination</key>
<string>export</string>
</dict>
</plist>
EOF
else
cp ExportOptions.plist "$BUILD_DIR/ExportOptions.plist"
fi
xcodebuild \
-archivePath "$BUILD_DIR/$APP_NAME.xcarchive" \
-exportPath "$BUILD_DIR/export" \
-exportOptionsPlist "$BUILD_DIR/ExportOptions.plist" \
-exportArchive
# Re-sign the framework explicitly to ensure proper signature
if [[ -n "$BUILD_CERTIFICATE_BASE64" && -n "$P12_PASSWORD" ]]; then
echo "🔏 Re-signing framework with Developer ID certificate..."
FRAMEWORK_PATH="$BUILD_DIR/export/$APP_NAME.app/Contents/Frameworks/OpenMultitouchSupportXCF.framework"
if [[ -d "$FRAMEWORK_PATH" ]]; then
codesign --force --sign "Developer ID Application: Krish Shah (9ZRLG6277G)" \
--options runtime \
--timestamp \
"$FRAMEWORK_PATH/Versions/A/OpenMultitouchSupportXCF"
codesign --force --sign "Developer ID Application: Krish Shah (9ZRLG6277G)" \
--options runtime \
--timestamp \
"$FRAMEWORK_PATH"
echo "✅ Framework re-signed successfully"
fi
# Re-sign the main app to ensure everything is consistent
echo "🔏 Re-signing main application..."
codesign --force --sign "Developer ID Application: Krish Shah (9ZRLG6277G)" \
--options runtime \
--entitlements "TrackWeight/TrackWeight.entitlements" \
--timestamp \
--deep \
--strict \
"$BUILD_DIR/export/$APP_NAME.app"
echo "✅ Application re-signed successfully"
fi
echo ""
echo "Step 2.5: Verifying Universal Binary and Code Signatures"
echo "========================================================"
echo "🏗️ Verifying Universal Binary Architecture..."
APP_BINARY="$BUILD_DIR/export/$APP_NAME.app/Contents/MacOS/$APP_NAME"
if [[ -f "$APP_BINARY" ]]; then
echo "📊 Binary architectures:"
lipo -archs "$APP_BINARY"
if lipo -archs "$APP_BINARY" | grep -q "arm64" && lipo -archs "$APP_BINARY" | grep -q "x86_64"; then
echo "✅ Universal binary confirmed: Contains both ARM64 and x86_64"
else
echo "❌ Warning: Binary may not be universal"
lipo -detailed_info "$APP_BINARY"
fi
fi
# Check framework architecture if it exists
FRAMEWORK_PATH="$BUILD_DIR/export/$APP_NAME.app/Contents/Frameworks/OpenMultitouchSupportXCF.framework"
if [[ -d "$FRAMEWORK_PATH" ]]; then
FRAMEWORK_BINARY="$FRAMEWORK_PATH/Versions/A/OpenMultitouchSupportXCF"
if [[ -f "$FRAMEWORK_BINARY" ]]; then
echo "📊 Framework architectures:"
lipo -archs "$FRAMEWORK_BINARY"
fi
fi
echo "🔍 Verifying main application signature..."
codesign --verify --verbose "$BUILD_DIR/export/$APP_NAME.app" || echo "⚠️ Main app signature verification failed"
echo "🔍 Verifying framework signature..."
if [[ -d "$FRAMEWORK_PATH" ]]; then
codesign --verify --verbose "$FRAMEWORK_PATH" || echo "⚠️ Framework signature verification failed"
fi
echo "🔍 Checking for hardened runtime..."
RUNTIME_FLAGS=$(codesign --display --verbose "$BUILD_DIR/export/$APP_NAME.app" 2>&1 | grep "flags=")
if [[ "$RUNTIME_FLAGS" == *"runtime"* ]]; then
echo "✅ Hardened runtime enabled: $RUNTIME_FLAGS"
else
echo "⚠️ No hardened runtime detected: $RUNTIME_FLAGS"
fi
echo "🔍 Checking certificate validity..."
codesign --display --verbose=4 "$BUILD_DIR/export/$APP_NAME.app" | grep -E "(Authority|Timestamp|TeamIdentifier)" || echo "Certificate details extracted"
echo "🔍 Deep verification with online validation..."
if codesign --verify --deep --strict --verbose=2 "$BUILD_DIR/export/$APP_NAME.app"; then
echo "✅ Deep verification passed"
else
echo "⚠️ Deep verification failed but continuing..."
fi
echo ""
echo "Step 3: Testing Notarization (Optional)"
echo "======================================="
if [[ -n "$APPLE_ID" && -n "$APPLE_ID_PASSWORD" && -n "$APPLE_TEAM_ID" ]]; then
echo "🍎 Starting notarization with provided credentials..."
cd "$BUILD_DIR/export"
echo "📦 Creating zip file for notarization..."
# Use ditto for creating zip compatible with Apple's notarization service
ditto -c -k --keepParent "$APP_NAME.app" "$APP_NAME.zip"
echo "📤 Submitting for notarization..."
if xcrun notarytool submit "$APP_NAME.zip" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait \
--timeout 20m; then
echo "✅ Notarization successful!"
echo "📎 Stapling notarization ticket..."
xcrun stapler staple "$APP_NAME.app"
echo "✅ Verifying notarization..."
xcrun stapler validate "$APP_NAME.app"
echo "✅ Notarization complete!"
else
echo "❌ Notarization failed!"
echo "🔍 This could be due to:"
echo " - Invalid Apple credentials"
echo " - App signing issues"
echo " - Missing hardened runtime"
echo " - Sandbox/entitlement issues"
echo " - Deprecated APIs"
echo ""
echo "⚠️ Continuing without notarization..."
fi
# Return to original directory
cd "$BUILD_DIR"
else
echo "⚠️ Skipping notarization (set APPLE_ID, APPLE_ID_PASSWORD, APPLE_TEAM_ID to test)"
fi
echo ""
echo "Step 4: Creating DMG"
echo "==================="
# Install create-dmg if not present
if ! command -v create-dmg &> /dev/null; then
echo "Installing create-dmg..."
brew install create-dmg
fi
# Create a clean directory with only the app for DMG creation
echo "📁 Preparing clean DMG contents..."
DMG_STAGING="$BUILD_DIR/dmg_staging"
rm -rf "$DMG_STAGING"
mkdir -p "$DMG_STAGING"
# Copy only the app to staging directory
cp -R "$BUILD_DIR/export/$APP_NAME.app" "$DMG_STAGING/"
# Create DMG with appropriate naming
if [[ -n "$APPLE_ID" && -n "$APPLE_ID_PASSWORD" && -n "$APPLE_TEAM_ID" ]]; then
DMG_NAME="$APP_NAME-local-NOTARIZED.dmg"
else
DMG_NAME="$APP_NAME-local-SIGNED.dmg"
fi
echo "📀 Creating professional DMG..."
create-dmg \
--volname "$APP_NAME" \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "$APP_NAME.app" 175 120 \
--hide-extension "$APP_NAME.app" \
--app-drop-link 425 120 \
--hdiutil-quiet \
"$BUILD_DIR/$DMG_NAME" \
"$DMG_STAGING/"
echo ""
echo "🎉 Local Build Complete!"
echo "======================="
echo "📦 DMG created: $BUILD_DIR/$DMG_NAME"
echo "📁 Build directory: $BUILD_DIR"
echo ""
if [[ -n "$APPLE_ID" && -n "$APPLE_ID_PASSWORD" && -n "$APPLE_TEAM_ID" ]]; then
echo "✅ Notarization was attempted using credentials from .env"
else
echo "ℹ️ To enable notarization, add these to your .env file:"
echo " APPLE_ID=your@email.com"
echo " APPLE_ID_PASSWORD=xxxx-xxxx-xxxx-xxxx"
echo " APPLE_TEAM_ID=ABC1234567"
fi
echo ""
echo "🔧 Cleaning up temporary files..."
rm -f "$BUILD_DIR/certificate.p12"
rm -rf "$DMG_STAGING"
echo "✅ Cleanup complete"
gitextract_foro58fa/
├── .github/
│ └── workflows/
│ ├── README.md
│ └── build-and-sign-dmg.yml
├── .gitignore
├── ExportOptions.plist
├── LICENSE
├── README.md
├── TrackWeight/
│ ├── Assets.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ContentView.swift
│ ├── ContentViewModel.swift
│ ├── DebugView.swift
│ ├── HomeView.swift
│ ├── Preview Content/
│ │ └── Preview Assets.xcassets/
│ │ └── Contents.json
│ ├── ScaleView.swift
│ ├── ScaleViewModel.swift
│ ├── SettingsView.swift
│ ├── TrackWeight.entitlements
│ ├── TrackWeightApp.swift
│ ├── TrackWeightView.swift
│ ├── WeighingState.swift
│ └── WeighingViewModel.swift
├── TrackWeight.xcodeproj/
│ ├── project.pbxproj
│ └── project.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm/
│ └── Package.resolved
└── scripts/
├── README.md
├── setup-signing.sh
└── test-build-locally.sh
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (120K chars).
[
{
"path": ".github/workflows/README.md",
"chars": 4672,
"preview": "# GitHub Actions Workflows\n\nThis directory contains GitHub Actions workflows for the TrackWeight project.\n\n## build-and-"
},
{
"path": ".github/workflows/build-and-sign-dmg.yml",
"chars": 12125,
"preview": "name: Build and Sign DMG\n\non:\n push:\n tags:\n - 'v*'\n branches:\n - 'copilot/fix-1' # Enable testing on "
},
{
"path": ".gitignore",
"chars": 209,
"preview": "# Mac\n.DS_Store\n\n# Xcode\nxcuserdata/\n*.xcuserstate\n\n# Swift Package Manager\nPackages.resolved\n.swiftpm/\n.build/\n\n# Frame"
},
{
"path": "ExportOptions.plist",
"chars": 744,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2025 Krish Shah\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 4627,
"preview": "# TrackWeight\n\n**Turn your MacBook's trackpad into a precise digital weighing scale**\n\n[TrackWeight](\nhttps://x.com/Kris"
},
{
"path": "TrackWeight/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1301,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"icon_16x16.png\",\n \"idiom\" : \"mac\",\n \"scale\" : \"1x\",\n \"size\" : "
},
{
"path": "TrackWeight/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "TrackWeight/ContentView.swift",
"chars": 1243,
"preview": "//\n// ContentView.swift\n// TrackWeight\n//\n\nimport SwiftUI\n\nstruct ContentView: View {\n @State private var showHomeP"
},
{
"path": "TrackWeight/ContentViewModel.swift",
"chars": 1355,
"preview": "//\n// ContentViewModel.swift\n// OMSDemo\n//\n// Created by Takuto Nakamura on 2024/03/02.\n//\n\nimport OpenMultitouchSupp"
},
{
"path": "TrackWeight/DebugView.swift",
"chars": 3430,
"preview": "//\n// DebugView.swift\n// TrackWeight\n//\n// Created by Takuto Nakamura on 2024/03/02.\n//\n\nimport OpenMultitouchSupport"
},
{
"path": "TrackWeight/HomeView.swift",
"chars": 4697,
"preview": "//\n// HomeView.swift\n// TrackWeight\n//\n\nimport SwiftUI\n\nstruct HomeView: View {\n let onBegin: () -> Void\n \n v"
},
{
"path": "TrackWeight/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "TrackWeight/ScaleView.swift",
"chars": 12613,
"preview": "//\n// ScaleView.swift\n// TrackWeight\n//\n\nimport SwiftUI\n\nstruct ScaleView: View {\n @StateObject private var viewMod"
},
{
"path": "TrackWeight/ScaleViewModel.swift",
"chars": 1614,
"preview": "//\n// ScaleViewModel.swift\n// TrackWeight\n//\n\nimport OpenMultitouchSupport\nimport SwiftUI\nimport Combine\n\n@MainActor\nf"
},
{
"path": "TrackWeight/SettingsView.swift",
"chars": 5371,
"preview": "//\n// SettingsView.swift\n// TrackWeight\n//\n\nimport OpenMultitouchSupport\nimport SwiftUI\n\nstruct SettingsView: View {\n "
},
{
"path": "TrackWeight/TrackWeight.entitlements",
"chars": 944,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "TrackWeight/TrackWeightApp.swift",
"chars": 471,
"preview": "//\n// TrackWeightApp.swift\n// TrackWeight\n//\n// Created by Takuto Nakamura on 2024/03/02.\n//\n\nimport SwiftUI\n\n@main\ns"
},
{
"path": "TrackWeight/TrackWeightView.swift",
"chars": 11714,
"preview": "//\n// TrackWeightView.swift\n// TrackWeight\n//\n\nimport SwiftUI\n\nstruct TrackWeightView: View {\n @StateObject private"
},
{
"path": "TrackWeight/WeighingState.swift",
"chars": 215,
"preview": "//\n// WeighingState.swift\n// TrackWeight\n//\n\nimport Foundation\n\nenum WeighingState: Equatable {\n case welcome\n c"
},
{
"path": "TrackWeight/WeighingViewModel.swift",
"chars": 8832,
"preview": "//\n// WeighingViewModel.swift\n// TrackWeight\n//\n\nimport OpenMultitouchSupport\nimport SwiftUI\nimport Combine\n\n@MainActo"
},
{
"path": "TrackWeight.xcodeproj/project.pbxproj",
"chars": 18566,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 56;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "TrackWeight.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 135,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:\">\n </FileRef"
},
{
"path": "TrackWeight.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "TrackWeight.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
"chars": 412,
"preview": "{\n \"originHash\" : \"231443e4712351510ab418164c13b6d5894774039d8c118ed8208a5fc9fe884e\",\n \"pins\" : [\n {\n \"identit"
},
{
"path": "scripts/README.md",
"chars": 1601,
"preview": "# Scripts\n\nThis directory contains helper scripts for the TrackWeight project.\n\n## setup-signing.sh\n\nA helper script to "
},
{
"path": "scripts/setup-signing.sh",
"chars": 4936,
"preview": "#!/bin/bash\n\n# setup-signing.sh\n# Helper script to set up code signing certificates for TrackWeight DMG builds\n# \n# This"
},
{
"path": "scripts/test-build-locally.sh",
"chars": 9186,
"preview": "#!/bin/bash\n\n# test-build-locally.sh\n# Test the GitHub Actions workflow steps locally on macOS\n\nset -e\n\necho \"🧪 Testing "
}
]
About this extraction
This page contains the full source code of the KrishKrosh/TrackWeight GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (109.8 KB), approximately 27.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.