ironOS native ios app
2
fork

Configure Feed

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

feat: fix crash

+114 -45
+2 -2
ios/TinkCil.xcodeproj/project.pbxproj
··· 253 253 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 254 254 CODE_SIGN_IDENTITY = "Apple Development"; 255 255 CODE_SIGN_STYLE = Automatic; 256 - CURRENT_PROJECT_VERSION = 4; 256 + CURRENT_PROJECT_VERSION = 5; 257 257 DEVELOPMENT_TEAM = M67B42LX8D; 258 258 ENABLE_PREVIEWS = YES; 259 259 GENERATE_INFOPLIST_FILE = YES; ··· 291 291 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 292 292 CODE_SIGN_IDENTITY = "Apple Development"; 293 293 CODE_SIGN_STYLE = Automatic; 294 - CURRENT_PROJECT_VERSION = 4; 294 + CURRENT_PROJECT_VERSION = 5; 295 295 DEVELOPMENT_TEAM = M67B42LX8D; 296 296 ENABLE_PREVIEWS = YES; 297 297 GENERATE_INFOPLIST_FILE = YES;
+96 -38
ios/TinkCil/BLEManager.swift
··· 3 3 // TinkCil 4 4 // 5 5 6 - import CoreBluetooth 6 + @preconcurrency import CoreBluetooth 7 7 import Foundation 8 8 9 9 @Observable ··· 122 122 centralManager.cancelPeripheralConnection(peripheral) 123 123 } 124 124 125 - // Clean up all state 126 - operationQueue.sync { 125 + // Clean up all state and call pending completions 126 + let pendingCompletions = operationQueue.sync { () -> [(CBUUID, (UInt16?) -> Void)] in 127 127 // Cancel all pending operations 128 128 for timeout in operationTimeouts.values { 129 129 timeout.cancel() 130 130 } 131 131 operationTimeouts.removeAll() 132 132 pendingWrites.removeAll() 133 + 134 + // Extract completions to call them outside the sync block 135 + let completions = Array(settingReadCompletions) 133 136 settingReadCompletions.removeAll() 137 + return completions 138 + } 139 + 140 + // Call all pending completions with nil (disconnected) 141 + DispatchQueue.main.async { 142 + for (_, completion) in pendingCompletions { 143 + completion(nil) 144 + } 134 145 } 135 146 136 147 connectionState = .disconnected ··· 155 166 156 167 let uuid = IronOSUUIDs.settingUUID(index: index) 157 168 158 - operationQueue.sync { 159 - // If we have the characteristic cached, use it 160 - if let characteristic = discoveredCharacteristics[uuid] { 169 + // Check if characteristic is cached and prepare operation 170 + let cachedCharacteristic = operationQueue.sync { () -> CBCharacteristic? in 171 + return discoveredCharacteristics[uuid] 172 + } 173 + 174 + bleQueue.async { [weak self] in 175 + guard let self = self else { return } 176 + 177 + if let characteristic = cachedCharacteristic { 178 + // Use cached characteristic 161 179 peripheral.writeValue(value.data, for: characteristic, type: .withResponse) 162 - settingsCache.set(value, for: index) 163 - scheduleOperationTimeout(for: uuid, type: "write") 180 + DispatchQueue.main.async { 181 + self.settingsCache.set(value, for: index) 182 + } 183 + self.scheduleOperationTimeout(for: uuid, type: "write") 164 184 } else { 165 - // Otherwise discover it first 185 + // Need to discover characteristic first 166 186 if let settingsService = peripheral.services?.first(where: { $0.uuid == IronOSUUIDs.settingsService }) { 187 + self.operationQueue.sync { 188 + self.pendingWrites[uuid] = value 189 + } 167 190 peripheral.discoverCharacteristics([uuid], for: settingsService) 168 - // Store for later write after discovery 169 - pendingWrites[uuid] = value 170 - scheduleOperationTimeout(for: uuid, type: "discover-write") 191 + self.scheduleOperationTimeout(for: uuid, type: "discover-write") 171 192 } else { 172 - lastError = .characteristicNotFound(uuid) 193 + DispatchQueue.main.async { 194 + self.lastError = .characteristicNotFound(uuid) 195 + } 173 196 } 174 197 } 175 198 } ··· 192 215 193 216 let uuid = IronOSUUIDs.settingUUID(index: index) 194 217 195 - operationQueue.sync { 196 - // Store completion handler 218 + // Store completion handler and check for cached characteristic 219 + let cachedCharacteristic = operationQueue.sync { () -> CBCharacteristic? in 197 220 settingReadCompletions[uuid] = completion 221 + return discoveredCharacteristics[uuid] 222 + } 198 223 199 - if let characteristic = discoveredCharacteristics[uuid] { 224 + bleQueue.async { [weak self] in 225 + guard let self = self else { return } 226 + 227 + if let characteristic = cachedCharacteristic { 228 + // Read from cached characteristic 200 229 peripheral.readValue(for: characteristic) 201 - scheduleOperationTimeout(for: uuid, type: "read") 230 + self.scheduleOperationTimeout(for: uuid, type: "read") 202 231 } else { 203 - // Discover it first 232 + // Need to discover characteristic first 204 233 if let settingsService = peripheral.services?.first(where: { $0.uuid == IronOSUUIDs.settingsService }) { 205 234 peripheral.discoverCharacteristics([uuid], for: settingsService) 206 - scheduleOperationTimeout(for: uuid, type: "discover-read") 235 + self.scheduleOperationTimeout(for: uuid, type: "discover-read") 207 236 } else { 208 - lastError = .characteristicNotFound(uuid) 209 - completion(nil) 210 - settingReadCompletions.removeValue(forKey: uuid) 237 + DispatchQueue.main.async { 238 + self.lastError = .characteristicNotFound(uuid) 239 + completion(nil) 240 + } 241 + _ = self.operationQueue.sync { 242 + self.settingReadCompletions.removeValue(forKey: uuid) 243 + } 211 244 } 212 245 } 213 246 } ··· 221 254 lastError = .notConnected 222 255 return 223 256 } 224 - 257 + 225 258 let value = UInt16(1).data 226 - peripheral.writeValue(value, for: characteristic, type: .withResponse) 259 + bleQueue.async { 260 + peripheral.writeValue(value, for: characteristic, type: .withResponse) 261 + } 227 262 } 228 263 229 264 func setSlowPolling() { ··· 313 348 guard let characteristic = discoveredCharacteristics[IronOSUUIDs.bulkLiveData], 314 349 let peripheral = connectedPeripheral else { return } 315 350 316 - peripheral.readValue(for: characteristic) 351 + bleQueue.async { 352 + peripheral.readValue(for: characteristic) 353 + } 317 354 } 318 355 319 356 private func recordTemperature() { ··· 480 517 } 481 518 482 519 // Handle pending writes for dynamically discovered settings 483 - operationQueue.sync { 484 - if let pendingValue = pendingWrites[characteristic.uuid] { 485 - peripheral.writeValue(pendingValue.data, for: characteristic, type: .withResponse) 486 - pendingWrites.removeValue(forKey: characteristic.uuid) 487 - scheduleOperationTimeout(for: characteristic.uuid, type: "write") 488 - } 520 + let pendingValue = operationQueue.sync { () -> UInt16? in 521 + return pendingWrites.removeValue(forKey: characteristic.uuid) 522 + } 523 + if let pendingValue = pendingValue { 524 + peripheral.writeValue(pendingValue.data, for: characteristic, type: .withResponse) 525 + scheduleOperationTimeout(for: characteristic.uuid, type: "write") 526 + } 489 527 490 - // Handle pending reads for dynamically discovered settings 491 - if settingReadCompletions[characteristic.uuid] != nil { 492 - peripheral.readValue(for: characteristic) 493 - scheduleOperationTimeout(for: characteristic.uuid, type: "read") 494 - } 528 + // Handle pending reads for dynamically discovered settings 529 + let hasPendingRead = operationQueue.sync { () -> Bool in 530 + return settingReadCompletions[characteristic.uuid] != nil 531 + } 532 + if hasPendingRead { 533 + peripheral.readValue(for: characteristic) 534 + scheduleOperationTimeout(for: characteristic.uuid, type: "read") 495 535 } 496 536 } 497 537 ··· 506 546 func peripheral(_ peripheral: CBPeripheral, 507 547 didUpdateValueFor characteristic: CBCharacteristic, 508 548 error: Error?) { 549 + // Check if we're still connected to this peripheral 550 + guard peripheral == connectedPeripheral else { 551 + return 552 + } 553 + 509 554 guard error == nil else { 510 555 cancelOperationTimeout(for: characteristic.uuid) 511 - DispatchQueue.main.async { [weak self] in 512 - self?.lastError = .readFailed(error?.localizedDescription ?? "Unknown error") 556 + 557 + // Call any pending completion with nil for this characteristic 558 + let completion = operationQueue.sync { () -> ((UInt16?) -> Void)? in 559 + settingReadCompletions.removeValue(forKey: characteristic.uuid) 560 + } 561 + 562 + if let completion = completion { 563 + DispatchQueue.main.async { [weak self] in 564 + self?.lastError = .readFailed(error?.localizedDescription ?? "Unknown error") 565 + completion(nil) 566 + } 567 + } else { 568 + DispatchQueue.main.async { [weak self] in 569 + self?.lastError = .readFailed(error?.localizedDescription ?? "Unknown error") 570 + } 513 571 } 514 572 return 515 573 }
+16 -5
ios/TinkCil/SettingsView.swift
··· 312 312 private func loadSettings() async { 313 313 // Load commonly used settings (will use cache if available) 314 314 let settingsToLoad: [UInt16] = [0, 1, 2, 6, 7, 11, 13, 14, 17, 22, 24, 25, 26, 27, 28, 33, 34] 315 - 315 + 316 316 isLoading = true 317 - 317 + 318 + // Check if we're still connected before loading 319 + guard bleManager.connectionState.isConnected else { 320 + isLoading = false 321 + return 322 + } 323 + 318 324 await withTaskGroup(of: (Int, UInt16?).self) { group in 319 325 for index in settingsToLoad { 320 326 group.addTask { @MainActor in 321 - await withCheckedContinuation { (continuation: CheckedContinuation<(Int, UInt16?), Never>) in 327 + // Check connection state before each read 328 + guard bleManager.connectionState.isConnected else { 329 + return (Int(index), nil) 330 + } 331 + 332 + return await withCheckedContinuation { (continuation: CheckedContinuation<(Int, UInt16?), Never>) in 322 333 bleManager.readSetting(index: index) { value in 323 334 continuation.resume(returning: (Int(index), value)) 324 335 } 325 336 } 326 337 } 327 338 } 328 - 339 + 329 340 for await (index, value) in group { 330 341 if let value = value { 331 342 settings[index] = value 332 343 } 333 344 } 334 345 } 335 - 346 + 336 347 isLoading = false 337 348 } 338 349 }