alejandro.sawers 16 Posted April 9 iOS has a convenient way to save images to the user's device without requiring full access to the photo library, and luckily the required call is included in the iOSapi.UIKit unit in Delphi. So just doing something like this: UIImageWriteToSavedPhotosAlbum(NSObjectToID(Image), nil, nil, nil); // Image is a TUIImage instance And the system will take care of asking the corresponding user authorization and saving the image. Now if something goes wrong (from the user denying authorization to some failure on the save process) the app can get error info by filling the 2nd and 3rd parameter when calling the procedure. These parameters are quite special however: a Selector and its Target. According to the documentation I understand that a Selector allows to call an arbitrary procedure of a NSObject regardless of its class. Given a FMX TForm with a procedure CompletionHandler to act as the completion receiver: - Can a FMX TForm be casted to a NSObject to be used as completonTarget? - Is the procedure signature CompletionHandler(image: UIImage; error: NSError; contextInfo: Pointer) correct and suitable? - It's enough something like NSSelectorFromString(NSObjectToID(StrToNSStr('CompletionHandler'))) to make a Selector for a Delphi method name? - Will be possible to pass a valid Selector on Delphi at all? Share this post Link to post
alejandro.sawers 16 Posted Tuesday at 08:20 PM Well, after reading dozens of Objective-C documentation pages, diving into the internals of the Macapi.* and iOSapi.* units, and looking for clues and hints from several (and quite old) iOS-and-Delphi blog entries I finally have a working implementation of an iOS Selector in Delphi, tailored for usage with UIImageWriteToSavedPhotosAlbum (in Delphi 12.2 unpatched): Note: I'm really no expert on iOS internals, so if mistakes were made (whether technical of terminological) feel free to correct me. - First, we need a Delphi class that is also an Objective-C class. I based my implementation on the TFMXWakeHandler class in FMX.Platform.iOS, as it is simple enough to understand and replicate: uses Macapi.ObjectiveC, iOSapi.Foundation, iOSapi.UIKit, System.TypInfo; type ICompletionTarget = interface(NSObject) ['your-guid-goes-here'] // Use Ctrl+Shift+G to get a random GUID [MethodName('image:didFinishSavingWithError:contextInfo:')] procedure saveImageCompletionHandler(image: UIImage; error: NSError; contextInfo: Pointer); cdecl; end; TCompletionTarget = class(TOCLocal) private function GetNativeObject: NSObject; // From TFMXWakeHandler protected function GetObjectiveCClass: PTypeInfo; override; // From TFMXWakeHandler public [MethodName('image:didFinishSavingWithError:contextInfo:')] procedure saveImageCompletionHandler(image: UIImage; error: NSError; contextInfo: Pointer); cdecl; property NativeObject: NSObject read GetNativeObject; // From TFMXWakeHandler end; With this, TCompletionTarget will also create an Objc object when the Delphi object itself is created, as Delphi takes care of registering the class and its methods on the Objc side. I added the [MethodName] attribute to saveImageCompletionHandler so the Delphi procedure and it's arguments can be arbitrarily named while keeping the selector name static and compliant with the signature required by UIImageWriteToSavedPhotosAlbum. It's important however to match the argument types to those expected by the selector as close as possible. The mapping between iOS types and Delphi types is in charge of the RTTI and it does a good job, except for the contextInfo parameter: If you examine the procedure MangleType(var Buff: string; const RttiType: TRttiType) on the unit Macapi.ObjectiveC you will see that the tkPointer type is mapped to the character '^', but the correct mapping for a 'void *' (a pointer to a void) is '^v'. So... - Patch Macapi.ObjectiveC.pas to force a custom encoding for our signature: MethodName := MangleMethodName(ClassMethod); MangledName := MangleMethodType(ClassMethod); // Mangled name for our selector is v@:@@^ , which will not be accepted by the callback and will throw an exception if MethodName='image:didFinishSavingWithError:contextInfo:' then // Given our selector is unique we can force a custom MangledName on it MangledName := 'v@:@@^v'; // void, id, SEL, UIImage, NSError, void * My solution was to edit Macapi.ObjectiveC.pas, on the class function TRegisteredDelphiClass.RegisterClass, intercept my custom selector and force a custom encoded representation. With this, the method signature and the encoded representation will be accepted by the callback invoked by UIImageWriteToSavedPhotosAlbum. This was the hardest part to figure out, as a crash on iOS closes the app without further info and the XCode logs are barely helpful at best. - Create an instance of TCompletionTarget, pass it to UIImageWriteToSavedPhotosAlbum and handle the result: // Initialize it in e.g. FormCreate Obj := TCompletionTarget.Create(SaveImageCompletionHandler); // Save the photo and pass the target and selector UIImageWriteToSavedPhotosAlbum( NSObjectToID(Image), // UIImage NSObjectToID(Obj.NativeObject), // Target sel_getUid('image:didFinishSavingWithError:contextInfo:'), // Selector from string nil); // No context With this call, once the user is prompted to give write access to their photos (if first time) and the OS saves the image (if allowed) our handler will be called with the result: procedure TCompletionTarget.saveImageCompletionHandler(image: UIImage; error: NSError; contextInfo: Pointer); begin if error <> nil then // Do on error else // Do on success end; Some things to note: - Not sure if the handler is called on the main or on a secondary thread, so take care of this. - If the user denies access on first prompt the OS will return "Unknown error" for that and all subsequent calls. The OS will not ask permission again on your behalf. - Given the above limitation a best approach would be using the PHPhotoLibrary framework, as this allows to ask permission and check its status if previously denied, but such framework it is not implemented in Delphi by default. Share this post Link to post
Dave Nottage 605 Posted Tuesday at 08:42 PM 12 minutes ago, alejandro.sawers said: Patch Macapi.ObjectiveC.pas to force a custom encoding for our signature: If you indeed needed to do this, an issue should be filed in the Quality Portal. Kudos for working that one out - it had me stumped. 13 minutes ago, alejandro.sawers said: Given the above limitation a best approach would be using the PHPhotoLibrary framework, as this allows to ask permission and check its status if previously denied, but such framework it is not implemented in Delphi by default. In which case, you may be interested in this demo which uses an implementation of the PHPhotoLibrary framework. I had been considering creating a kind of "lite" version just for saving images to a nominated collection, because the entire implementation might be considered "overkill" when that's all that is being used. Share this post Link to post
alejandro.sawers 16 Posted 6 hours ago On 4/22/2025 at 4:42 PM, Dave Nottage said: an issue should be filed in the Quality Portal I might do that, explaining the best I can what happened here. And about the demo, yes it is really good but overkill for my use case, but now I know the imports I need are in the Kastri repo, on the DW.iOSapi.Photos unit, so I can use just the bits related to permission checking and requesting. But... - PHPhotoLibrary.requestAuthorization asks for full-access, so aware users will see the inconsistency between the request and the purpose (save a single image) and will deny. I can always ask them again to give the previously denied permission explaining what's happening, but still feels hacky. Someone at Apple forgot a couple things about consistency when designed this. - On iOS 14+ we finally have ways to request and check write-only access to the photo library via authorizationStatusForAccessLevel and requestAuthorizationForAccessLevel. These are missing from Kastri but are trivial to import, except for requestAuthorizationForAccessLevel which asks for a "void (^)(PHAuthorizationStatus status)" parameter as the handler. That thing looks scary. 1 Share this post Link to post
Dave Nottage 605 Posted 5 hours ago 56 minutes ago, alejandro.sawers said: On iOS 14+ we finally have ways to request and check write-only access to the photo library via authorizationStatusForAccessLevel and requestAuthorizationForAccessLevel. These are missing from Kastri but are trivial to import, except for requestAuthorizationForAccessLevel Thanks for the heads up. As you might gather, that import has not been updated to include newer APIs in a while. I'm planning on updating it, as well as look at those methods you mentioned. Share this post Link to post