Skip to content

Conversation

@RobertOdrowaz
Copy link
Contributor

Migrates camera package to the Swift-based pigeon interface as the last part of flutter/flutter#119109

This PR replaces the ObjC-based pigeon interfaces with Swift-based ones, thus removing the last remaining pieces of ObjC code in the package 🎉

I know there are a lot of changes in this PR, but there is no way to split it into smaller parts.

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2 3

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request completes the migration of the camera_avfoundation package to a Swift-based Pigeon interface, removing the last Objective-C-based Pigeon interfaces. The changes are extensive and primarily involve:

  • Replacing Objective-C generated Pigeon files with new Swift-generated ones (Messages.swift).
  • Updating the CameraApi implementation in CameraPlugin.swift and DefaultCamera.swift to use the new Swift types and Result-based completion handlers.
  • Removing the Objective-C Pigeon target and related source files.
  • Updating all unit and integration tests to align with the new Swift API, including changes to mock objects and assertions to handle Result and PigeonError types.
  • Adjusting the podspec and Package.swift files to reflect the removal of the Objective-C target.

My feedback focuses on improving the maintainability of the updated test files by reducing code duplication.

Comment on lines 65 to 72
permissionManager.requestCameraPermission { error in
XCTAssertEqual(error, expectedError)
XCTAssertEqual(error?.code, "CameraAccessDeniedWithoutPrompt")
XCTAssertEqual(
error?.message,
"User has previously denied the camera access request. Go to Settings to enable camera access."
)
expectation.fulfill()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a lot of repeated code for asserting error properties across this test file. To improve maintainability and reduce duplication, consider adding a helper function to assert the properties of a PigeonError.

For example:

private func assertPermissionError(
  _ error: PigeonError?,
  hasCode expectedCode: String,
  hasMessage expectedMessage: String,
  file: StaticString = #file,
  line: UInt = #line
) {
  XCTAssertNotNil(error, "Expected a permission error, but got nil", file: file, line: line)
  XCTAssertEqual(error?.code, expectedCode, file: file, line: line)
  XCTAssertEqual(error?.message, expectedMessage, file: file, line: line)
}

You could then simplify this test case and others like it:

permissionManager.requestCameraPermission { error in
  assertPermissionError(
    error,
    hasCode: "CameraAccessDeniedWithoutPrompt",
    hasMessage: "User has previously denied the camera access request. Go to Settings to enable camera access."
  )
  expectation.fulfill()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls let me know if you're in favor or agains a helper in those cases. I remember that you were against in an earlier PR but it would be a much simpler helper here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with either approach. Both seem readable to me.

Comment on lines +47 to 55
cameraPlugin.lockCaptureOrientation(orientation: targetOrientation) { result in
switch result {
case .success:
break
case .failure:
XCTFail("Unexpected error")
}
expectation.fulfill()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Throughout this test file, there are many switch statements to check for a successful Result and fail the test on failure. This pattern is repeated frequently. To make the tests more concise and reduce boilerplate, you could introduce a helper function.

For example, a helper could unwrap the success value or fail the test:

func assertSuccess<T>(_ result: Result<T, any Error>, file: StaticString = #file, line: UInt = #line) -> T? {
  switch result {
  case .success(let value):
    return value
  case .failure(let error):
    XCTFail("Expected success, but got failure: \(error)", file: file, line: line)
    return nil
  }
}

This would simplify tests that expect success. For this test, it would look like:

cameraPlugin.lockCaptureOrientation(orientation: targetOrientation) { result in
  _ = assertSuccess(result)
  expectation.fulfill()
}

For tests that also check the returned value, it would be even cleaner:

// Example from another test in this file
cameraPlugin.stopVideoRecording { result in
  if let path = assertSuccess(result) {
      XCTAssertEqual(path, targetPath)
  }
  expectation.fulfill()
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems nice cleanup that saves lots of lines

@stuartmorgan-g stuartmorgan-g added the triage-ios Should be looked at in iOS triage label Feb 10, 2026
@RobertOdrowaz
Copy link
Contributor Author

@hellohuanlin pls take a look at this PR when you have some time

@hellohuanlin
Copy link
Contributor

@RobertOdrowaz Hello, this PR is 2000 LOC and all in a single commit. Could you give a tour of the change and how i can start reviewing it, if possible?

@RobertOdrowaz
Copy link
Contributor Author

I know it's over 2000 LOC, but the vast majority are trivial changes:

  1. ~1300 LOC is in Messages.swift which is generated by pigeon
  2. ~600 LOC in tests which is mostly very repetitive change from something like
camera.setZoomLevel(CGFloat(1.0)) { error in
      XCTAssertNotNil(error)
      XCTAssertEqual(error?.code, "ZOOM_ERROR")
      expectation.fulfill()
    }

to

camera.setZoomLevel(CGFloat(1.0)) { result in
      switch result {
      case .failure(let error as PigeonError):
        XCTAssertEqual(error.code, "ZOOM_ERROR")
      default:
        XCTFail("Expected failure")
      }
      expectation.fulfill()
    }

because methods now pass a Result union type instead of nullable error to the completion.

  1. ~100 LOC in CameraPlugin.swift mostly changes from nullable error to a Result in completions
  2. ~140 LOC in DefaultCamera.swift mostly changes from nullable error to a Result and PigeonError instead of FlutterError.

I can split 1, 2 and 3+4 into separate commits if it will help you but IMO going file by file is effectively the same.

@hellohuanlin
Copy link
Contributor

@RobertOdrowaz Thanks for the tour. Don't worry about splitting it. Taking a look now.

Copy link
Contributor

@hellohuanlin hellohuanlin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with just a few questions

settings: FCPPlatformMediaSettings.make(
with: FCPPlatformResolutionPreset.medium,
settings: PlatformMediaSettings(
resolutionPreset: PlatformResolutionPreset.medium,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can infer type

Comment on lines 65 to 72
permissionManager.requestCameraPermission { error in
XCTAssertEqual(error, expectedError)
XCTAssertEqual(error?.code, "CameraAccessDeniedWithoutPrompt")
XCTAssertEqual(
error?.message,
"User has previously denied the camera access request. Go to Settings to enable camera access."
)
expectation.fulfill()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with either approach. Both seem readable to me.

handler(false)
}
permissionManager.requestCameraPermission { error in
XCTAssertEqual(error, expectedError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious why are we changing this? I don't see any problem with this approach. Is it related to equatable?

Comment on lines +47 to 55
cameraPlugin.lockCaptureOrientation(orientation: targetOrientation) { result in
switch result {
case .success:
break
case .failure:
XCTFail("Unexpected error")
}
expectation.fulfill()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems nice cleanup that saves lots of lines

with: testResolutionPreset,
framesPerSecond: NSNumber(value: testFramesPerSecond),
videoBitrate: NSNumber(value: testVideoBitrate),
audioBitrate: NSNumber(value: testAudioBitrate),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to see these gone

/// @param messenger Nullable messenger for capturing each frame.
func startVideoRecording(
completion: @escaping (_ error: FlutterError?) -> Void,
completion: @escaping (Result<Void, any Error>) -> Void,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it changing from FlutterError to any Error?

# Combine camera_avfoundation and camera_avfoundation_objc sources into a single pod, unlike
# SwiftPM, where separate Swift and Objective-C targets are required.
s.source_files = 'camera_avfoundation/Sources/camera_avfoundation*/**/*.{h,m,swift}'
s.public_header_files = 'camera_avfoundation/Sources/camera_avfoundation_objc/include/**/*.h'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants