Repository: beeware/toga

Issue:
iOS Camera take_photo
### Describe the bug

When using the documented:

```python3
if await app.camera.request_permission():
    photo = await app.camera.take_photo()
```

### Steps to reproduce

1. request permission to use camera
2. await the take_photo function.


### Expected behavior

Return a toga.Image or None as documented here https://toga.readthedocs.io/en/stable/reference/api/hardware/camera.html#toga.hardware.camera.Camera.take_photo

### Screenshots

Successfully get permission to use camera: <img width="378" alt="Screenshot 2024-02-08 at 22 14 03" src="https://github.com/beeware/toga/assets/25908768/5867a18e-b5c5-4141-bd72-a4b737c0fbd1">

No camera screen comes up.


### Environment

- Operating System: iOS 16.4 & 17.2 (built and development from Macbook Air M1 macOS 14.3.1)
- Python version: 3.10.2
- Software versions:
  - Briefcase: 0.3.17
  - Toga: 4.0.2


### Logs

Simulator Log
```
Error in async handler: 'NoneType' object has no attribute 'result'
Traceback (most recent call last):
  File "/Users/j/Library/Developer/CoreSimulator/Devices/6EAED83E-F2F7-4497-8F74-52F3DC368DA2/data/Containers/Bundle/Application/E864BC86-AFF3-410A-A4CF-D594ADB4AFCA/Health App.app/app_packages/toga/handlers.py", line 38, in handler_with_cleanup
    result = await handler(interface, *args, **kwargs)
  File "/Users/j/Library/Developer/CoreSimulator/Devices/6EAED83E-F2F7-4497-8F74-52F3DC368DA2/data/Containers/Bundle/Application/E864BC86-AFF3-410A-A4CF-D594ADB4AFCA/Health App.app/app/healthapp/app.py", line 71, in analyse_gait_handler
    photo = await self.app.camera.take_photo()
  File "/Users/j/Library/Developer/CoreSimulator/Devices/6EAED83E-F2F7-4497-8F74-52F3DC368DA2/data/Containers/Bundle/Application/E864BC86-AFF3-410A-A4CF-D594ADB4AFCA/Health App.app/app_packages/toga/hardware/camera.py", line 122, in take_photo
    self._impl.take_photo(photo, device=device, flash=flash)
  File "/Users/j/Library/Developer/CoreSimulator/Devices/6EAED83E-F2F7-4497-8F74-52F3DC368DA2/data/Containers/Bundle/Application/E864BC86-AFF3-410A-A4CF-D594ADB4AFCA/Health App.app/app_packages/toga_iOS/hardware/camera.py", line 159, in take_photo
    self.native.delegate.result = result
AttributeError: 'NoneType' object has no attribute 'result'
```


Physical iOS device testing log:
```
2024-02-08 22:27:03.097595+0000 Health App[2216:513956] [TraitCollection] Class CKBrowserSwitcherViewController overrides the -traitCollection getter, which is not supported. If you're trying to override traits, you must use the appropriate API.
2024-02-08 22:27:03.179852+0000 Health App[2216:513956] Error in async handler: 'NoneType' object has no attribute 'result'

2024-02-08 22:27:03.183351+0000 Health App[2216:513956] Traceback (most recent call last):

2024-02-08 22:27:03.183705+0000 Health App[2216:513956]   File "/private/var/containers/Bundle/Application/22F0E7F3-012A-46ED-93DE-C8EC8125EE19/Health App.app/app_packages/toga/handlers.py", line 38, in handler_with_cleanup

2024-02-08 22:27:03.183927+0000 Health App[2216:513956]     result = await handler(interface, *args, **kwargs)

2024-02-08 22:27:03.184161+0000 Health App[2216:513956]   File "/private/var/containers/Bundle/Application/22F0E7F3-012A-46ED-93DE-C8EC8125EE19/Health App.app/app/healthapp/app.py", line 71, in analyse_gait_handler

2024-02-08 22:27:03.184361+0000 Health App[2216:513956]     photo = await self.app.camera.take_photo()

2024-02-08 22:27:03.184604+0000 Health App[2216:513956]   File "/private/var/containers/Bundle/Application/22F0E7F3-012A-46ED-93DE-C8EC8125EE19/Health App.app/app_packages/toga/hardware/camera.py", line 122, in take_photo

2024-02-08 22:27:03.184827+0000 Health App[2216:513956]     self._impl.take_photo(photo, device=device, flash=flash)

2024-02-08 22:27:03.185124+0000 Health App[2216:513956]   File "/private/var/containers/Bundle/Application/22F0E7F3-012A-46ED-93DE-C8EC8125EE19/Health App.app/app_packages/toga_iOS/hardware/camera.py", line 159, in take_photo

2024-02-08 22:27:03.185537+0000 Health App[2216:513956]     self.native.delegate.result = result

2024-02-08 22:27:03.185785+0000 Health App[2216:513956] AttributeError: 'NoneType' object has no attribute 'result'
```

### Additional context

Fully works on android, same problem on macOS I believe.


Discussion:
when running on MacOS, the app just closes/crashes and log just stops...

```
        print("Analyse Gait button pressed!")

        # Note this doesnt return on iOS/macOS yet?, fully working on android.
        if await self.app.camera.request_permission():
            photo = await self.app.camera.take_photo()
```

```
Running app module: healthapp
---------------------------------------------------------------------------
Analyse Gait button pressed!
(beeware-venv) j@MacBook-Air-2 Mobile_Health_App % 
```
Thanks for the report. 

The behaviour on the simulator is known (and [documented](https://toga.readthedocs.io/en/stable/reference/api/hardware/camera.html#notes)) behavior. For reasons that make no sense to me, the iOS simulator doesn't have a camera, and using the camera APIs has some unpredictable behaviour (ranging from crashes to "GUI that can't actually take a photo").

However, the error on physical devices is new to me. I'll need to dig into the cause - but based on the stack trace you've provided, I have some theories on what could be happening.

Also - to clarify: are you saying this *does* or *doesn't* work on macOS as well? I haven't tested on Sonoma yet, so that *might* be the cause if you're seeing problems. If it is failing there, could you share a stack trace?
Correct, MacOS does not even ask for permission so not reaching the take_photo I assume, and it silently closes so i assume crashes

I'm not sure how to get a stack trace for you.
Ok - I wasn't sure if you were seeing a Python error - it sounds like you're getting a hard crash.

Out of interest - on the Mac, do you have any additional cameras attached, or just the main built-in camera? This includes potentially using your phone in handoff mode.
> Ok - I wasn't sure if you were seeing a Python error - it sounds like you're getting a hard crash.
> 
> Out of interest - on the Mac, do you have any additional cameras attached, or just the main built-in camera? This includes potentially using your phone in handoff mode.

~~Nothing but the built in camera, never had anything else so default camera profiles/config on macbook air.~~

Oooo, now you say that there may be a virtual camera from OBS.
Dont think its default but let me turn that off or do something with that
Nope not OBS, so all I have is the inbuilt camera on device.

<img width="498" alt="Screenshot 2024-02-08 at 23 00 56" src="https://github.com/beeware/toga/assets/25908768/798e9870-8257-43cb-85c9-779e38282881">
MacOS trace:

Looks like the MacOS Crash is due to the camera perms/entitlements I'll try and fix that in the toml file
I must have got confused here https://briefcase.readthedocs.io/en/latest/reference/platforms/macOS/app.html#permissions where it just mentions the entitlement and not the Info `NSCameraUsageDescription` like iOS

```
 handler...
2024-02-08 23:07:20.570136+0000 Health App[10840:138060] Running app module: healthapp
2024-02-08 23:07:20.598308+0000 Health App[10840:138060] ---------------------------------------------------------------------------
2024-02-08 23:07:26.300812+0000 Health App[10840:138060] WARNING: Secure coding is not enabled for restorable state! Enable secure coding by implementing NSApplicationDelegate.applicationSupportsSecureRestorableState: and returning YES.
2024-02-08 23:07:30.501669+0000 Health App[10840:138060] [1 2 3]
2024-02-08 23:07:30.501728+0000 Health App[10840:138060] Analyse Gait button pressed!
2024-02-08 23:07:30.515680+0000 Health App[10840:138178] [access] This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.
Process 10840 stopped
* thread #5, queue = 'com.apple.root.default-qos', stop reason = signal SIGABRT
    frame #0: 0x0000000186c8272c libsystem_kernel.dylib`__abort_with_payload + 8
libsystem_kernel.dylib`:
->  0x186c8272c <+8>:  b.lo   0x186c8274c               ; <+40>
    0x186c82730 <+12>: pacibsp 
    0x186c82734 <+16>: stp    x29, x30, [sp, #-0x10]!
    0x186c82738 <+20>: mov    x29, sp
Target 0: (Health App) stopped.
(lldb) bt
* thread #5, queue = 'com.apple.root.default-qos', stop reason = signal SIGABRT
  * frame #0: 0x0000000186c8272c libsystem_kernel.dylib`__abort_with_payload + 8
    frame #1: 0x0000000186ca7f88 libsystem_kernel.dylib`abort_with_payload_wrapper_internal + 104
    frame #2: 0x0000000186ca7fbc libsystem_kernel.dylib`abort_with_payload + 16
    frame #3: 0x000000018c3b6c6c TCC`__TCC_CRASHING_DUE_TO_PRIVACY_VIOLATION__ + 172
    frame #4: 0x000000018c3b756c TCC`__TCCAccessRequest_block_invoke.218 + 600
    frame #5: 0x000000018c3b47c0 TCC`__tccd_send_message_block_invoke + 632
    frame #6: 0x00000001869d9024 libxpc.dylib`_xpc_connection_reply_callout + 116
    frame #7: 0x00000001869d8f1c libxpc.dylib`_xpc_connection_call_reply_async + 80
    frame #8: 0x0000000186b04990 libdispatch.dylib`_dispatch_client_callout3 + 20
    frame #9: 0x0000000186b22388 libdispatch.dylib`_dispatch_mach_msg_async_reply_invoke + 344
    frame #10: 0x0000000186b17358 libdispatch.dylib`_dispatch_root_queue_drain_deferred_item + 336
    frame #11: 0x0000000186b16c40 libdispatch.dylib`_dispatch_kevent_worker_thread + 500
    frame #12: 0x0000000186cb214c libsystem_pthread.dylib`_pthread_wqthread + 344
(lldb) 
```
Fixed MacOS hard crash by adding the `info."NSCameraUsageDescription" = "Blah blah blah"`
Missed it only had the entitlement set to true docs didnt mention the info for macOS only iOS.

But issue persists on iOS
> Fixed MacOS hard crash by adding the `info."NSCameraUsageDescription" = "Blah blah blah"` Missed it only had the entitlement set to true docs didnt mention the info for macOS only iOS.

Interesting... I didn't think that was needed; if it is, that's an easy fix in Briefcase (and the docs in Toga).
 
> But issue persists on iOS

Ok - if you're up for some spelunking; my theory is that the delegate object is being garbage collected. If you modify the iOS camera class so that the object assigned to `self.native.delegate` is stored *explicitly* on the Camera class, in addition to being assigned as `self.native.delegate`, that might resolve the issue. This is the sort of thing that will be highly situation dependent, which explains why it hasn't been a problem in my own testing.

The easiest way to test this will be to use `briefcase open iOS` to open the project in Xcode, and modify the copy of the code that has been installed. If that works, whatever change you make can be turned into a PR in a code checkout.

Resolving patch:
diff --git a/iOS/src/toga_iOS/hardware/camera.py b/iOS/src/toga_iOS/hardware/camera.py
--- a/iOS/src/toga_iOS/hardware/camera.py
+++ b/iOS/src/toga_iOS/hardware/camera.py
@@ -74,7 +74,8 @@ def __init__(self, interface):
             ):
                 self.native = iOS.UIImagePickerController.new()
                 self.native.sourceType = UIImagePickerControllerSourceTypeCamera
-                self.native.delegate = TogaImagePickerDelegate.new()
+                self.delegate_link = TogaImagePickerDelegate.new()
+                self.native.delegate = self.delegate_link
             else:
                 self.native = None
         else:  # pragma: no cover


Test patch: