ironOS native ios app
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: haptics

+139 -7
+84 -1
ios/Tinkcil/ContentView.swift
··· 14 14 @State private var isTopBarExpanded = false 15 15 @State private var showingSettings = false 16 16 @State private var showingError = false 17 + @State private var lastConnectionState: BLEManager.ConnectionState = .disconnected 18 + @State private var lastMode: OperatingMode? 17 19 18 20 private var isHeating: Bool { 19 21 bleManager.liveData.mode?.isActive == true ··· 43 45 if !isEditingSlider && newValue > 0 { 44 46 targetTemp = Double(newValue) 45 47 } 48 + } 49 + .onChange(of: bleManager.connectionState) { oldState, newState in 50 + handleConnectionStateChange(from: oldState, to: newState) 51 + } 52 + .onChange(of: bleManager.liveData.mode) { oldMode, newMode in 53 + handleModeChange(from: oldMode, to: newMode) 54 + } 55 + .onChange(of: bleManager.liveData.liveTemp) { oldTemp, newTemp in 56 + checkTemperatureReached(oldTemp: oldTemp, newTemp: newTemp) 46 57 } 47 58 .onChange(of: bleManager.lastError) { _, error in 48 59 if error != nil { 60 + hapticError() 49 61 showingError = true 50 62 } 51 63 } ··· 98 110 VStack(spacing: 0) { 99 111 // Main top bar (always visible) 100 112 Button { 113 + hapticLight() 101 114 withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { 102 115 isTopBarExpanded.toggle() 103 116 } ··· 170 183 171 184 // Settings button 172 185 Button { 186 + hapticLight() 173 187 showingSettings = true 174 188 } label: { 175 189 HStack { ··· 236 250 onEditingChanged: { editing in 237 251 isEditingSlider = editing 238 252 if editing { 253 + hapticSelection() 239 254 bleManager.setSlowPolling() 240 255 } else { 241 256 // Only send if value changed 242 257 if abs(targetTemp - lastSentTemp) >= 5 { 258 + hapticLight() 243 259 bleManager.setTemperature(UInt32(targetTemp)) 244 260 lastSentTemp = targetTemp 245 261 } ··· 323 339 Text(bleManager.connectionState.isConnecting ? "Connecting..." : "Scanning...") 324 340 .font(.headline) 325 341 326 - Text("Looking for your Tinkcil") 342 + Text("Looking for your iron") 327 343 .font(.subheadline) 328 344 .foregroundStyle(.secondary) 329 345 } else { ··· 336 352 .font(.headline) 337 353 338 354 Button("Scan Again") { 355 + hapticLight() 339 356 bleManager.startScanning() 340 357 } 341 358 .buttonStyle(.borderedProminent) ··· 344 361 .padding(32) 345 362 .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24)) 346 363 .shadow(color: .black.opacity(0.2), radius: 20) 364 + } 365 + } 366 + 367 + // MARK: - Haptic Feedback 368 + 369 + private func hapticLight() { 370 + let generator = UIImpactFeedbackGenerator(style: .light) 371 + generator.impactOccurred() 372 + } 373 + 374 + private func hapticSelection() { 375 + let generator = UISelectionFeedbackGenerator() 376 + generator.selectionChanged() 377 + } 378 + 379 + private func hapticSuccess() { 380 + let generator = UINotificationFeedbackGenerator() 381 + generator.notificationOccurred(.success) 382 + } 383 + 384 + private func hapticWarning() { 385 + let generator = UINotificationFeedbackGenerator() 386 + generator.notificationOccurred(.warning) 387 + } 388 + 389 + private func hapticError() { 390 + let generator = UINotificationFeedbackGenerator() 391 + generator.notificationOccurred(.error) 392 + } 393 + 394 + private func handleConnectionStateChange(from oldState: BLEManager.ConnectionState, to newState: BLEManager.ConnectionState) { 395 + switch newState { 396 + case .connected: 397 + hapticSuccess() 398 + case .disconnected: 399 + if oldState.isConnected { 400 + hapticWarning() 401 + } 402 + case .scanning: 403 + hapticLight() 404 + default: 405 + break 406 + } 407 + } 408 + 409 + private func handleModeChange(from oldMode: OperatingMode?, to newMode: OperatingMode?) { 410 + // Only trigger haptic if mode actually changed and it's a meaningful change 411 + guard let old = oldMode, let new = newMode, old != new else { return } 412 + 413 + // Haptic for entering/exiting active heating modes 414 + if old.isActive != new.isActive { 415 + hapticLight() 416 + } 417 + } 418 + 419 + private func checkTemperatureReached(oldTemp: UInt32, newTemp: UInt32) { 420 + // Check if we just reached the target temperature (within 5 degrees) 421 + let target = bleManager.liveData.setpoint 422 + guard target > 0 && isHeating else { return } 423 + 424 + let wasBelow = oldTemp < target - 5 425 + let isNear = abs(Int(newTemp) - Int(target)) <= 5 426 + 427 + // Trigger success haptic when we reach target for the first time 428 + if wasBelow && isNear { 429 + hapticSuccess() 347 430 } 348 431 } 349 432 }
+55 -6
ios/Tinkcil/SettingsView.swift
··· 9 9 @Environment(\.dismiss) var dismiss 10 10 let bleManager: BLEManager 11 11 @State private var selectedTab = 0 12 - 12 + 13 13 var body: some View { 14 14 NavigationStack { 15 15 TabView(selection: $selectedTab) { ··· 30 30 .toolbar { 31 31 ToolbarItem(placement: .topBarTrailing) { 32 32 Button("Done") { 33 + hapticLight() 33 34 dismiss() 34 35 } 35 36 } 36 37 } 37 38 } 39 + } 40 + 41 + private func hapticLight() { 42 + let generator = UIImpactFeedbackGenerator(style: .light) 43 + generator.impactOccurred() 38 44 } 39 45 } 40 46 ··· 298 304 299 305 private func saveSettings() { 300 306 saveInProgress = true 307 + hapticLight() 301 308 bleManager.saveSettings() 302 309 303 310 // Wait for a reasonable time for the write operation 304 311 Task { 305 312 try? await Task.sleep(for: .milliseconds(500)) 306 313 await MainActor.run { 314 + hapticSuccess() 307 315 saveInProgress = false 308 316 } 309 317 } 318 + } 319 + 320 + private func hapticLight() { 321 + let generator = UIImpactFeedbackGenerator(style: .light) 322 + generator.impactOccurred() 323 + } 324 + 325 + private func hapticSuccess() { 326 + let generator = UINotificationFeedbackGenerator() 327 + generator.notificationOccurred(.success) 310 328 } 311 329 312 330 private func loadSettings() async { ··· 387 405 388 406 Section { 389 407 Button(role: .destructive) { 408 + hapticWarning() 390 409 bleManager.disconnect() 391 410 } label: { 392 411 HStack { ··· 427 446 return "\(hours)h \(minutes)m ago" 428 447 } 429 448 } 449 + 450 + private func hapticWarning() { 451 + let generator = UINotificationFeedbackGenerator() 452 + generator.notificationOccurred(.warning) 453 + } 430 454 } 431 455 432 456 // MARK: - Setting Row Components ··· 438 462 let step: UInt16 439 463 let unit: String 440 464 let onChange: (UInt16) -> Void 441 - 465 + 442 466 var body: some View { 443 467 VStack(alignment: .leading, spacing: 8) { 444 468 HStack { ··· 448 472 .foregroundStyle(.secondary) 449 473 .monospacedDigit() 450 474 } 451 - 475 + 452 476 Slider( 453 477 value: Binding( 454 478 get: { Double(value) }, ··· 457 481 in: Double(range.lowerBound)...Double(range.upperBound), 458 482 step: Double(step), 459 483 onEditingChanged: { editing in 460 - if !editing { 484 + if editing { 485 + hapticSelection() 486 + } else { 487 + hapticLight() 461 488 onChange(value) 462 489 } 463 490 } 464 491 ) 465 492 } 466 493 } 494 + 495 + private func hapticLight() { 496 + let generator = UIImpactFeedbackGenerator(style: .light) 497 + generator.impactOccurred() 498 + } 499 + 500 + private func hapticSelection() { 501 + let generator = UISelectionFeedbackGenerator() 502 + generator.selectionChanged() 503 + } 467 504 } 468 505 469 506 struct ToggleSettingRow: View { 470 507 let label: String 471 508 @Binding var value: Bool 472 509 let onChange: (Bool) -> Void 473 - 510 + 474 511 var body: some View { 475 512 Toggle(label, isOn: Binding( 476 513 get: { value }, 477 514 set: { newValue in 515 + hapticLight() 478 516 value = newValue 479 517 onChange(newValue) 480 518 } 481 519 )) 482 520 } 521 + 522 + private func hapticLight() { 523 + let generator = UIImpactFeedbackGenerator(style: .light) 524 + generator.impactOccurred() 525 + } 483 526 } 484 527 485 528 struct PickerSettingRow: View { ··· 487 530 @Binding var value: UInt16 488 531 let options: [(UInt16, String)] 489 532 let onChange: (UInt16) -> Void 490 - 533 + 491 534 var body: some View { 492 535 Picker(label, selection: Binding( 493 536 get: { value }, 494 537 set: { newValue in 538 + hapticSelection() 495 539 value = newValue 496 540 onChange(newValue) 497 541 } ··· 500 544 Text(option.1).tag(option.0) 501 545 } 502 546 } 547 + } 548 + 549 + private func hapticSelection() { 550 + let generator = UISelectionFeedbackGenerator() 551 + generator.selectionChanged() 503 552 } 504 553 } 505 554