I am working on supporting some formatted text editing in my app, and I've been experimenting with copy and paste support for formatted text. I discovered that NSAttributedString implements NSItemProviderWriting, which means I can give it to UIPasteboard via setObjects and all the built-in attributes transfer perfectly if I then paste it into another text view, or even another app that behaves itself.
But if I have custom attributes in my attributed string, having their values implement Codable doesn't let them transfer across the clipboard. In my implementation of textPasteConfigurationSupporting(_: transform:), I try to get an attributed string like this:
let attr = item.itemProvider.loadObject(ofClass: NSAttributedString.self) { val, err in
//...handle here
}
I get an error like this:
Error Domain=NSItemProviderErrorDomain Code=-1000 "Cannot load representation of type com.apple.uikit.attributedstring" UserInfo={NSLocalizedDescription=Cannot load representation of type com.apple.uikit.attributedstring, NSUnderlyingError=0x600003e7bea0 {Error Domain=NSCocoaErrorDomain Code=260 "The file “b036c42113e34c2f9d9af14d6fefcbd534f627d6” couldn’t be opened because there is no such file." UserInfo={NSURL=file:///Users/username/Library/Developer/CoreSimulator/Devices/86E8BDD4-B6AA-4170-B0EB-57C74EC7DDF0/data/Library/Caches/com.apple.Pasteboard/eb77e5f8f043896faf63b5041f0fbd121db984dd/b036c42113e34c2f9d9af14d6fefcbd534f627d6, NSFilePath=/Users/username/Library/Developer/CoreSimulator/Devices/86E8BDD4-B6AA-4170-B0EB-57C74EC7DDF0/data/Library/Caches/com.apple.Pasteboard/eb77e5f8f043896faf63b5041f0fbd121db984dd/b036c42113e34c2f9d9af14d6fefcbd534f627d6, NSUnderlyingError=0x600003e7ac70 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}}}
But I tried making my custom attribute values implement NSSecureCoding, and then it worked.
Why is Codable conformance not enough here? Is it because the code that serializes and deserializes is still in Objective-C and isn't aware of Codable? Will this change as the open-source Foundation in Swift work continues?
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I'm working to make my iOS app available via Catalyst, and I'm adding a leading-edge sidebar via UISplitViewController and putting a toggle button in the window toolbar to control it. I'd like that sidebar toggle button to go on the leading side of the toolbar, before the window title.
In the Adding a Toolbar Catalyst tutorial, there are screenshots of this behavior:
But when I download the finished project and run it on my machine (macOS 14.4), the toolbar buttons are clustered on the trailing edge:
How do I achieve the behavior shown in the tutorial's screenshot, where the toggle sidebar button (or, ideally any custom toolbar item I choose) shows up on the leading edge? Even more ideally, is there a way I can make the sidebar toggle button show up in the header section for the sidebar, like it does in Xcode - right next to the stoplight buttons?
I have a Catalyst app that I'm adding a sidebar to via UISplitViewController. I have a toolbar on the window with buttons that I want to be enabled or disabled based on the state of the view controller in the split view's secondary column. But it seems to want to check the primary view controller instead.
In the Catalyst tutorial Adding a Toolbar, this exact approach is demonstrated. The RecipeDetailViewController has methods for toggleFavorite and editRecipe, and the toolbar items are set up to reference those selectors in the ToolbarDelegate class. In the screenshots in the tutorial, the buttons are shown as enabled based on RecipeDetailViewController.canPerformAction. But when I download and run the complete project on my computer (macOS 14.4.1), the toolbar items are disabled. And if I add the methods to the RecipeListViewController (which is in the primary column of the UISplitViewController, the toolbar items get enabled.
Is there a way to make the system ask the correct split view column for canPerformAction? Or is this a bug?
I have a Catalyst app that plays audio via AVQueuePlayer, and I'd like to use the system play/pause key (F8 on my MacBook Pro keyboard) to play and pause it. It doesn't seem to work automatically, and if I hook up a UIKeyCommand using UIKeyInputF8, it works with Fn-F8, not F8 on its own.
It does seem to work in Overcast's Mac app, but I think that's an iPad app for Mac, not Catalyst, so it's probably going through whatever system pathway that the Lock Screen controls would be using on iOS.
How do I make this work on Catalyst?
I'm working on replacing an AppKit-based Mac app with one built on Catalyst, and the Catalyst app doesn't seem to be able to read the keychain item that was saved by the old app.
Both apps are using the same bundle ID. The old app uses the old SecKeychain APIs - SecKeychainFindGenericPassword and friends - and the Catalyst app uses the newer SecItemCopyMatching and such. When I try using the new API in the old app to search for the entry, it works, but the exact same code in Catalyst fails.
Here's how I save an item in the old app:
NSString *strItemId = @"my_item_id;
NSString *username = @"user";
const char *userPointer = [username UTF8String];
NSString *password = @"password";
const char *pwPointer = [password UTF8String];
SecKeychainItemRef ref = NULL;
OSStatus status = SecKeychainFindGenericPassword(0, (UInt32)strlen(strItemId.UTF8String), strItemId.UTF8String, 0, NULL, NULL, NULL, &ref);
if (status == errSecSuccess && ref != NULL)
{
//update existing item
SecKeychainAttribute attr;
attr.length = (UInt32)strlen(userPointer);
attr.data = (void *)userPointer;
attr.tag = kSecAccountItemAttr;
SecKeychainAttributeList list;
list.count = 1;
list.attr = &attr;
OSStatus writeStatus = SecKeychainItemModifyAttributesAndData(ref, &list, (UInt32)strlen(pwPointer), pwPointer);
}
else
{
status = SecKeychainAddGenericPassword(NULL, (UInt32)strlen(strItemId.UTF8String), strItemId.UTF8String, (UInt32)strlen(userPointer), userPointer, (UInt32)strlen(pwPointer), pwPointer, NULL);
}
And here's the query code that works in the old app but returns errSecItemNotFound in Catalyst:
NSMutableDictionary *queryDict = [[[NSMutableDictionary alloc]init]autorelease];
[queryDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[queryDict setObject:(@"my_item_id") forKey:(__bridge id)kSecAttrService];
[queryDict setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[queryDict setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];
CFMutableDictionaryRef outDictionary = nil;
OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef)queryDict, (CFTypeRef *)&outDictionary);
I tried creating a new blank AppKit-based Mac app project in Xcode and gave it the old Mac app's bundle ID, and the SecItemCopyMatching query code above works there. Then I created a new iOS target with Catalyst enabled, also with the same bundle ID, and the query code running there under Catalyst returned errSecItemNotFound. So maybe the issue is something specific to Catalyst?
Is there something I need to do with the Catalyst app to give it access to the old app's keychain entry, besides setting its bundle ID to match the old app?
I've got a UIKit app and I want to add some buttons in a top-edge window ornament. I'm looking at the WWDC23 talk Meet UIKit for Spatial Computing, and it does exactly what I think I want to do:
extension EditorViewController {
func showEditingControlsOrnament() {
let ornament = UIHostingOrnament(sceneAlignment: .bottom, contentAlignment: .center) {
EditingControlsView(model: controlsViewModel)
.glassBackgroundEffect()
}
self.ornaments = [ornament]
editorView.style = .edgeToEdge
}
}
But the thing I really want to know is what is in EditingControlsView. Is it a toolbar? How do you make a toolbar in SwiftUI without something to attach the .toolbar modifier to?
I've got a UIKit app with a collapsible trailing-edge child view controller, implemented sort of like UISplitViewController but it's got a bunch of custom behavior - moving to the bottom edge in portrait orientation, etc. It exposes a couple of different app functions via a UITabBar on the bottom edge on iOS.
When I run the app on visionOS, that tab bar transforms to a leading-edge ornament. This would be great, but that means it tries to overlap the trailing-edge content of its parent view controller, which isn't ideal.
Is there a way to get the tab bar to lay out on the trailling edge of the child view controller? Or can I create a custom ornament that has the same auto-expand behavior as the tab bar, where it shows a vertical column of icons that expands to show titles when you're gazing at it?
I have a Catalyst app that supports multiple scenes / windows. It has one "main" window type and lots of other secondary windows that can be opened. I'm using the system window restoration functionality via NSQuitAlwaysKeepsWindows to restore windows with their user activity data after the app is quit and restarted.
Normally, this works great. If I open my app, change the size and position of my window, quit, and reopen it, the window size and position comes back as expected. But if I close the window using the red stoplight button and then click the app icon to bring it back, it comes back at the default position and size.
This isn't how other system apps work - if I close the system Calendar app with the stoplight button, it comes back at the same size and position. How do I get this behavior with my Catalyst app? Is there some identifier property I need to set somewhere? I don't see such a property on UISceneConfiguration.
If it matters, I'm using the configurationForConnectingSceneSession method to configure my windows when they open, instead of setting it up in the Info.plist.
If I have a Catalyst app with a WKWebView and I select text, I can drag forward to extend the selection, but I can't reduce the length of the selected range by dragging backwards. I've reproduced this in a trivial sample app.
Is there some property I need to set somewhere?
I've filed this as FB15645411.
In iOS 26 beta 3, it was possible to have a UIScrollEdgeElementContainerInteraction work with a WKWebView's scroll view. But it stopped working in beta 4, and is still broken in beta 5 today. Is this expected? Or is this a bug that will be fixed before public release?
Filed as FB19386650 with a trivial sample app.
There seems to be a regression in the behavior of UIScrollEdgeElementContainerInteraction on iOS 26.1 when it's over a WKWebView. If the web view's scroll view's topEdgeEffect.style is changed to .hard and then back to .soft, it will stop tracking the color of the content underneath it and use the wrong-mix in color in its blur.
I've filed this as FB20655398.
Here's some sample code to illustrate the issue. The test.html file being loaded is just a bunch of div elements with lorem ipsum.
private var webView: WKWebView? = nil
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = self
self.view.addSubview(webView)
webView.autoPinEdgesToSuperviewEdges()
webView.isInspectable = true
self.webView = webView
let url = Bundle.main.url(forResource: "test", withExtension: "html")!
webView.loadFileURL(url, allowingReadAccessTo: Bundle.main.bundleURL)
let blurView = UIView()
self.view.addSubview(blurView)
blurView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
let label = UILabel()
label.text = "This is a title bar"
blurView.addSubview(label)
label.autoAlignAxis(toSuperviewAxis: .vertical)
label.autoPinEdge(toSuperviewEdge: .bottom, withInset: 8)
label.autoPinEdge(toSuperviewSafeArea: .top, withInset: 8)
let interaction = UIScrollEdgeElementContainerInteraction()
interaction.scrollView = webView.scrollView
interaction.edge = .top
blurView.addInteraction(interaction)
self.webView?.scrollView.topEdgeEffect.style = .hard
DispatchQueue.main.asyncAfterUnsafe(deadline: .now() + .seconds(2)) {
self.webView?.scrollView.topEdgeEffect.style = .soft
}
registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (self: Self, previousTraitCollection: UITraitCollection) in
self._updateWebViewColors()
}
}
private func _updateWebViewColors() {
let dark = self.traitCollection.userInterfaceStyle == .dark
let text = dark ? "#FFFFFF" : "#000000"
let bg = dark ? "#000000" : "#FFFFFF"
let js = "document.body.style.color = '\(text)';\ndocument.body.style.backgroundColor = '\(bg)';"
self.webView?.evaluateJavaScript(js) { res, err in
if let err {
print("JS error: \(err)")
}
}
}
If you run that, then change the system them to dark mode, you get this. Has anyone else seen this before? Know how to work around it?
I've got a UIBarButtonItem in my app that currently presents an action sheet of items. I want to switch this to a UIMenu with iOS 14's new APIs for buttons and bar button items. But somme of the contents of the action sheet change based on the state of the view controller when the action sheet is triggered.
With the action sheet, I generate a different action sheet every time the bar button item's target action is called. But with the new APIs, I have to generate the menu ahead of time and assign it to the button.
What's the best way to get the menu to update every time it's presented? I tried UIDeferredMenuElement, but it caches the result of its provider.
Feedback FB7824467 suggests adding a property to UIDeferredMenuElement to disable the caching of the provider result.
In the session on StoreKit 2 (which looks amazing!), the presenter says:
In fact, if your app is running when a purchase is made on another device, you'll be notified about the new transaction.
This seems to mean that when an app uses the listener API to be notified of transactions, it will get transactions that happened on other devices.
My app offers purchases across other platforms in addition to iOS, and when a purchase happens we register it with our own account system. If a user has the app running on both their iPad and iPhone and makes a purchase on the phone, if the iPad gets notified of it the same way it would of a purchase made on the iPad, both devices will try to report it to our system. This seems undesirable.
What's the recommended approach here? Should we just make sure our system will disregard duplicate transaction reports? Or is there a way to know whether a transaction originated on this device? I don't see a property on the transaction type that looks like it could accomplish this. Maybe the deviceVerification properties? But that's seems more like the new edition of transaction receipt verification - failing that check would presumably mean that the purchase is invalid, not that it didn't happen on this device...?
I'm working on enabling Catalyst for my existing iOS app. When I try to archive and export Catalyst as a Developer ID-signed Mac app, I get the following error:
Cannot create a Mac Catalyst Developer ID provisioning profile for "[my bundle ID]".
The Siri capability is not available for Mac Catalyst Developer ID provisioning profiles. Disable this feature and try again.
My iOS app uses SiriKit to donate a Siri intent, so Siri is among the capabilities listed in the Signing and Capabilities tab in the project inspector in Xcode. I don't see a way to turn that capability off only for Catalyst (like you can link some frameworks only for Catalyst or iOS), and I don't want to disable Siri entirely in my iOS app.
What's going on here? What do I need to do to "disable this feature" for Catalyst?
I have a UICollectionView tied to a UICollectionViewDiffableDataSource, and I'm generating and applying snapshots to the datasource on a background serial queue, and generating cells using UICollectionViewCellRegistration. I'm working on supporting reordering of the contents of the collection view via drag and drop, and I'm having trouble with what to do in collectionView:performDropWithCoordinator: so the reorder animation looks right.
Normally, I would do something like this:
-(void)collectionView:(UICollectionView *)collectionView performDropWithCoordinator:(id<UICollectionViewDropCoordinator>)coordinator
{
NSIndexPath *sourcePath = (NSIndexPath *)coordinator.items.firstObject.dragItem.localObject;
NSInteger fromIndex = sourcePath.item;
NSInteger toIndex = coordinator.destinationIndexPath.item;
NSNumber *fromItem = [self.datasource itemIdentifierForIndexPath:sourcePath];
NSNumber *toItem = [self.datasource itemIdentifierForIndexPath:coordinator.destinationIndexPath];
//Do the move in the data model
[MyModel moveItemFrom:fromIndex to:toIndex];
//Do the move in the datasource. This is the data source equivalent of:
//[collectionView moveItemAtIndexPath:sourcePath toIndexPath:coordinator.destinationIndexPath];
NSDiffableDataSourceSnapshot *snap = self.datasource.snapshot;
if (toIndex < fromIndex)
[snap moveItemWithIdentifier:fromItem beforeItemWithIdentifier:toItem];
else
[snap moveItemWithIdentifier:fromItem afterItemWithIdentifier:toItem];
[self.dataSource applySnapshot:snap animated:YES];
//Drop the item
[coordinator dropItem:coordinator.items.firstObject.dragItem toItemAtIndexPath:coordinator.destinationIndexPath];
}
But because my datasource updates happen on a background queue, I have to do at least the snapshot generation and application asynchronously, and I'd like to do the actual data model modification there too to avoid hangs. And I need to call dropItem on the coordinator on the main queue in this method. This results in an odd animation where the dropped item momentarily disappears (when drop is called) and then reappears (when the data source is updated on the background queue).
The best idea I have so far is to use UICollectionViewDropPlaceholder to hold the place in the collection view until the data source is updated. But to create a placeholder I need a cell reuse identifier (docs on init method), and I don't have one of those because I'm creating my cells using cell registrations.
So my question: what do I do in the performDrop method to make this work correctly? If the placeholder is the right idea, how do I use it in this situation?