[camera] Remove `@throw` from iOS implementation (#5034)

Using `@throw` in iOS code violates the style guide, so it shouldn't be done in the plugin as mechanism for communicating errors. More importantly, `NSError` is not intended to be used with `@throw`/`@catch`, and is causing issues when compiled with the iOS 17 SDK.

This removes all use of `@throw`, and all `@catch (NSError* e)`, in favor of other methods of communicating errors. It explicitly does not try to fix all the other strange things about this code (having an `NSError` out-param in an init method, using Cocoa and NSURL error domains and codes for some reason), and instead preserves existing behavior as much as possible. In practice, none of these codepaths should ever actually happen (they indicate programming errors within the plugin, not unexpected runtime behavior), and all of this code will go away when converting to Pigeon anyway, so there's not much value in trying to unwind this structure further.

Fixes https://github.com/flutter/flutter/issues/135195
diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md
index 011d9c5..8f22184 100644
--- a/packages/camera/camera_avfoundation/CHANGELOG.md
+++ b/packages/camera/camera_avfoundation/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.13+6
+
+* Fixes incorrect use of `NSError` that could cause crashes on launch.
+
 ## 0.9.13+5
 
 * Ignores audio samples until the first video sample arrives.
diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj
index a20638d..6006b9f 100644
--- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj
@@ -280,7 +280,7 @@
 		97C146E61CF9000F007C117D /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 1300;
+				LastUpgradeCheck = 1430;
 				ORGANIZATIONNAME = "The Flutter Authors";
 				TargetAttributes = {
 					03BB76672665316900CE5A93 = {
diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index f4b3c10..1ff4b57 100644
--- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1300"
+   LastUpgradeVersion = "1430"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m
index 18c01e5..95203bf 100644
--- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m
+++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m
@@ -19,7 +19,7 @@
   XCTAssertEqual(FLTFlashModeAuto, FLTGetFLTFlashModeForString(@"auto"));
   XCTAssertEqual(FLTFlashModeAlways, FLTGetFLTFlashModeForString(@"always"));
   XCTAssertEqual(FLTFlashModeTorch, FLTGetFLTFlashModeForString(@"torch"));
-  XCTAssertThrows(FLTGetFLTFlashModeForString(@"unkwown"));
+  XCTAssertEqual(FLTFlashModeInvalid, FLTGetFLTFlashModeForString(@"unknown"));
 }
 
 - (void)testFLTGetAVCaptureFlashModeForFLTFlashMode {
@@ -34,13 +34,13 @@
 - (void)testFLTGetStringForFLTExposureMode {
   XCTAssertEqualObjects(@"auto", FLTGetStringForFLTExposureMode(FLTExposureModeAuto));
   XCTAssertEqualObjects(@"locked", FLTGetStringForFLTExposureMode(FLTExposureModeLocked));
-  XCTAssertThrows(FLTGetStringForFLTExposureMode(-1));
+  XCTAssertNil(FLTGetStringForFLTExposureMode(-1));
 }
 
 - (void)testFLTGetFLTExposureModeForString {
   XCTAssertEqual(FLTExposureModeAuto, FLTGetFLTExposureModeForString(@"auto"));
   XCTAssertEqual(FLTExposureModeLocked, FLTGetFLTExposureModeForString(@"locked"));
-  XCTAssertThrows(FLTGetFLTExposureModeForString(@"unknown"));
+  XCTAssertEqual(FLTExposureModeInvalid, FLTGetFLTExposureModeForString(@"unknown"));
 }
 
 #pragma mark - focus mode tests
@@ -48,13 +48,13 @@
 - (void)testFLTGetStringForFLTFocusMode {
   XCTAssertEqualObjects(@"auto", FLTGetStringForFLTFocusMode(FLTFocusModeAuto));
   XCTAssertEqualObjects(@"locked", FLTGetStringForFLTFocusMode(FLTFocusModeLocked));
-  XCTAssertThrows(FLTGetStringForFLTFocusMode(-1));
+  XCTAssertNil(FLTGetStringForFLTFocusMode(-1));
 }
 
 - (void)testFLTGetFLTFocusModeForString {
   XCTAssertEqual(FLTFocusModeAuto, FLTGetFLTFocusModeForString(@"auto"));
   XCTAssertEqual(FLTFocusModeLocked, FLTGetFLTFocusModeForString(@"locked"));
-  XCTAssertThrows(FLTGetFLTFocusModeForString(@"unknown"));
+  XCTAssertEqual(FLTFocusModeInvalid, FLTGetFLTFocusModeForString(@"unknown"));
 }
 
 #pragma mark - resolution preset tests
@@ -67,7 +67,7 @@
   XCTAssertEqual(FLTResolutionPresetVeryHigh, FLTGetFLTResolutionPresetForString(@"veryHigh"));
   XCTAssertEqual(FLTResolutionPresetUltraHigh, FLTGetFLTResolutionPresetForString(@"ultraHigh"));
   XCTAssertEqual(FLTResolutionPresetMax, FLTGetFLTResolutionPresetForString(@"max"));
-  XCTAssertThrows(FLTGetFLTFlashModeForString(@"unknown"));
+  XCTAssertEqual(FLTResolutionPresetInvalid, FLTGetFLTResolutionPresetForString(@"unknown"));
 }
 
 #pragma mark - video format tests
@@ -89,7 +89,7 @@
   XCTAssertEqual(UIDeviceOrientationLandscapeLeft,
                  FLTGetUIDeviceOrientationForString(@"landscapeRight"));
   XCTAssertEqual(UIDeviceOrientationPortrait, FLTGetUIDeviceOrientationForString(@"portraitUp"));
-  XCTAssertThrows(FLTGetUIDeviceOrientationForString(@"unknown"));
+  XCTAssertEqual(UIDeviceOrientationUnknown, FLTGetUIDeviceOrientationForString(@"unknown"));
 }
 
 - (void)testFLTGetStringForUIDeviceOrientation {
diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h
index aee4d64..4d0818d 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h
+++ b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h
@@ -17,6 +17,9 @@
   FLTFlashModeAuto,
   FLTFlashModeAlways,
   FLTFlashModeTorch,
+  // This should never occur; it indicates an unknown value was received over
+  // the platform channel.
+  FLTFlashModeInvalid,
 };
 
 /**
@@ -39,6 +42,9 @@
 typedef NS_ENUM(NSInteger, FLTExposureMode) {
   FLTExposureModeAuto,
   FLTExposureModeLocked,
+  // This should never occur; it indicates an unknown value was received over
+  // the platform channel.
+  FLTExposureModeInvalid,
 };
 
 /**
@@ -61,6 +67,9 @@
 typedef NS_ENUM(NSInteger, FLTFocusMode) {
   FLTFocusModeAuto,
   FLTFocusModeLocked,
+  // This should never occur; it indicates an unknown value was received over
+  // the platform channel.
+  FLTFocusModeInvalid,
 };
 
 /**
@@ -100,6 +109,9 @@
   FLTResolutionPresetVeryHigh,
   FLTResolutionPresetUltraHigh,
   FLTResolutionPresetMax,
+  // This should never occur; it indicates an unknown value was received over
+  // the platform channel.
+  FLTResolutionPresetInvalid,
 };
 
 /**
diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m
index e36f98a..147d3e3 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m
@@ -16,13 +16,7 @@
   } else if ([mode isEqualToString:@"torch"]) {
     return FLTFlashModeTorch;
   } else {
-    NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
-                                         code:NSURLErrorUnknown
-                                     userInfo:@{
-                                       NSLocalizedDescriptionKey : [NSString
-                                           stringWithFormat:@"Unknown flash mode %@", mode]
-                                     }];
-    @throw error;
+    return FLTFlashModeInvalid;
   }
 }
 
@@ -48,14 +42,11 @@
       return @"auto";
     case FLTExposureModeLocked:
       return @"locked";
+    case FLTExposureModeInvalid:
+      // This value should never actually be used.
+      return nil;
   }
-  NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
-                                       code:NSURLErrorUnknown
-                                   userInfo:@{
-                                     NSLocalizedDescriptionKey : [NSString
-                                         stringWithFormat:@"Unknown string for exposure mode"]
-                                   }];
-  @throw error;
+  return nil;
 }
 
 FLTExposureMode FLTGetFLTExposureModeForString(NSString *mode) {
@@ -64,13 +55,7 @@
   } else if ([mode isEqualToString:@"locked"]) {
     return FLTExposureModeLocked;
   } else {
-    NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
-                                         code:NSURLErrorUnknown
-                                     userInfo:@{
-                                       NSLocalizedDescriptionKey : [NSString
-                                           stringWithFormat:@"Unknown exposure mode %@", mode]
-                                     }];
-    @throw error;
+    return FLTExposureModeInvalid;
   }
 }
 
@@ -82,14 +67,11 @@
       return @"auto";
     case FLTFocusModeLocked:
       return @"locked";
+    case FLTFocusModeInvalid:
+      // This value should never actually be used.
+      return nil;
   }
-  NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
-                                       code:NSURLErrorUnknown
-                                   userInfo:@{
-                                     NSLocalizedDescriptionKey : [NSString
-                                         stringWithFormat:@"Unknown string for focus mode"]
-                                   }];
-  @throw error;
+  return nil;
 }
 
 FLTFocusMode FLTGetFLTFocusModeForString(NSString *mode) {
@@ -98,13 +80,7 @@
   } else if ([mode isEqualToString:@"locked"]) {
     return FLTFocusModeLocked;
   } else {
-    NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
-                                         code:NSURLErrorUnknown
-                                     userInfo:@{
-                                       NSLocalizedDescriptionKey : [NSString
-                                           stringWithFormat:@"Unknown focus mode %@", mode]
-                                     }];
-    @throw error;
+    return FLTFocusModeInvalid;
   }
 }
 
@@ -120,14 +96,7 @@
   } else if ([orientation isEqualToString:@"portraitUp"]) {
     return UIDeviceOrientationPortrait;
   } else {
-    NSError *error = [NSError
-        errorWithDomain:NSCocoaErrorDomain
-                   code:NSURLErrorUnknown
-               userInfo:@{
-                 NSLocalizedDescriptionKey :
-                     [NSString stringWithFormat:@"Unknown device orientation %@", orientation]
-               }];
-    @throw error;
+    return UIDeviceOrientationUnknown;
   }
 }
 
@@ -163,13 +132,7 @@
   } else if ([preset isEqualToString:@"max"]) {
     return FLTResolutionPresetMax;
   } else {
-    NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
-                                         code:NSURLErrorUnknown
-                                     userInfo:@{
-                                       NSLocalizedDescriptionKey : [NSString
-                                           stringWithFormat:@"Unknown resolution preset %@", preset]
-                                     }];
-    @throw error;
+    return FLTResolutionPresetInvalid;
   }
 }
 
diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m
index e0f0300..1f94040 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m
@@ -118,10 +118,16 @@
                              error:(NSError **)error {
   self = [super init];
   NSAssert(self, @"super init cannot be nil");
-  @try {
-    _resolutionPreset = FLTGetFLTResolutionPresetForString(resolutionPreset);
-  } @catch (NSError *e) {
-    *error = e;
+  _resolutionPreset = FLTGetFLTResolutionPresetForString(resolutionPreset);
+  if (_resolutionPreset == FLTResolutionPresetInvalid) {
+    *error = [NSError
+        errorWithDomain:NSCocoaErrorDomain
+                   code:NSURLErrorUnknown
+               userInfo:@{
+                 NSLocalizedDescriptionKey :
+                     [NSString stringWithFormat:@"Unknown resolution preset %@", resolutionPreset]
+               }];
+    return nil;
   }
   _enableAudio = enableAudio;
   _captureSessionQueue = captureSessionQueue;
@@ -162,7 +168,9 @@
   _motionManager = [[CMMotionManager alloc] init];
   [_motionManager startAccelerometerUpdates];
 
-  [self setCaptureSessionPreset:_resolutionPreset];
+  if (![self setCaptureSessionPreset:_resolutionPreset withError:error]) {
+    return nil;
+  }
   [self updateOrientation];
 
   return self;
@@ -337,7 +345,7 @@
   return file;
 }
 
-- (void)setCaptureSessionPreset:(FLTResolutionPreset)resolutionPreset {
+- (BOOL)setCaptureSessionPreset:(FLTResolutionPreset)resolutionPreset withError:(NSError **)error {
   switch (resolutionPreset) {
     case FLTResolutionPresetMax:
     case FLTResolutionPresetUltraHigh:
@@ -382,17 +390,17 @@
         _videoCaptureSession.sessionPreset = AVCaptureSessionPresetLow;
         _previewSize = CGSizeMake(352, 288);
       } else {
-        NSError *error =
-            [NSError errorWithDomain:NSCocoaErrorDomain
-                                code:NSURLErrorUnknown
-                            userInfo:@{
-                              NSLocalizedDescriptionKey :
-                                  @"No capture session available for current capture session."
-                            }];
-        @throw error;
+        *error = [NSError errorWithDomain:NSCocoaErrorDomain
+                                     code:NSURLErrorUnknown
+                                 userInfo:@{
+                                   NSLocalizedDescriptionKey :
+                                       @"No capture session available for current capture session."
+                                 }];
+        return NO;
       }
   }
   _audioCaptureSession.sessionPreset = _videoCaptureSession.sessionPreset;
+  return YES;
 }
 
 - (void)captureOutput:(AVCaptureOutput *)output
@@ -726,11 +734,17 @@
 
 - (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result
                              orientation:(NSString *)orientationStr {
-  UIDeviceOrientation orientation;
-  @try {
-    orientation = FLTGetUIDeviceOrientationForString(orientationStr);
-  } @catch (NSError *e) {
-    [result sendError:e];
+  UIDeviceOrientation orientation = FLTGetUIDeviceOrientationForString(orientationStr);
+  // "Unknown" should never be sent, so is used to represent an unexpected
+  // value.
+  if (orientation == UIDeviceOrientationUnknown) {
+    [result sendError:[NSError errorWithDomain:NSCocoaErrorDomain
+                                          code:NSURLErrorUnknown
+                                      userInfo:@{
+                                        NSLocalizedDescriptionKey : [NSString
+                                            stringWithFormat:@"Unknown device orientation %@",
+                                                             orientationStr]
+                                      }]];
     return;
   }
 
@@ -749,11 +763,14 @@
 }
 
 - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
-  FLTFlashMode mode;
-  @try {
-    mode = FLTGetFLTFlashModeForString(modeStr);
-  } @catch (NSError *e) {
-    [result sendError:e];
+  FLTFlashMode mode = FLTGetFLTFlashModeForString(modeStr);
+  if (mode == FLTFlashModeInvalid) {
+    [result sendError:[NSError errorWithDomain:NSCocoaErrorDomain
+                                          code:NSURLErrorUnknown
+                                      userInfo:@{
+                                        NSLocalizedDescriptionKey : [NSString
+                                            stringWithFormat:@"Unknown flash mode %@", modeStr]
+                                      }]];
     return;
   }
   if (mode == FLTFlashModeTorch) {
@@ -800,11 +817,14 @@
 }
 
 - (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
-  FLTExposureMode mode;
-  @try {
-    mode = FLTGetFLTExposureModeForString(modeStr);
-  } @catch (NSError *e) {
-    [result sendError:e];
+  FLTExposureMode mode = FLTGetFLTExposureModeForString(modeStr);
+  if (mode == FLTExposureModeInvalid) {
+    [result sendError:[NSError errorWithDomain:NSCocoaErrorDomain
+                                          code:NSURLErrorUnknown
+                                      userInfo:@{
+                                        NSLocalizedDescriptionKey : [NSString
+                                            stringWithFormat:@"Unknown exposure mode %@", modeStr]
+                                      }]];
     return;
   }
   _exposureMode = mode;
@@ -825,16 +845,24 @@
         [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
       }
       break;
+    case FLTExposureModeInvalid:
+      // This state is not intended to be reachable; it exists only for error handling during
+      // message deserialization.
+      NSAssert(false, @"");
+      break;
   }
   [_captureDevice unlockForConfiguration];
 }
 
 - (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
-  FLTFocusMode mode;
-  @try {
-    mode = FLTGetFLTFocusModeForString(modeStr);
-  } @catch (NSError *e) {
-    [result sendError:e];
+  FLTFocusMode mode = FLTGetFLTFocusModeForString(modeStr);
+  if (mode == FLTFocusModeInvalid) {
+    [result sendError:[NSError errorWithDomain:NSCocoaErrorDomain
+                                          code:NSURLErrorUnknown
+                                      userInfo:@{
+                                        NSLocalizedDescriptionKey : [NSString
+                                            stringWithFormat:@"Unknown focus mode %@", modeStr]
+                                      }]];
     return;
   }
   _focusMode = mode;
@@ -861,6 +889,11 @@
         [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
       }
       break;
+    case FLTFocusModeInvalid:
+      // This state is not intended to be reachable; it exists only for error handling during
+      // message deserialization.
+      NSAssert(false, @"");
+      break;
   }
   [captureDevice unlockForConfiguration];
 }
diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml
index e13e957..b4a4396 100644
--- a/packages/camera/camera_avfoundation/pubspec.yaml
+++ b/packages/camera/camera_avfoundation/pubspec.yaml
@@ -2,7 +2,7 @@
 description: iOS implementation of the camera plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.13+5
+version: 0.9.13+6
 
 environment:
   sdk: ">=2.19.0 <4.0.0"