Post

Replies

Boosts

Views

Activity

Reply to Animate layout change
Adapting sample code for a "Flow Layout", I built this Layout, to be able to animate changes: (see code block below) struct MyLayout: Layout { func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { guard !subviews.isEmpty else { return .zero } // ask subviews for their ideal size let sizes = subviews.map { $0.sizeThatFits(.unspecified) } var totalHeight: CGFloat = 0 var totalWidth: CGFloat = 0 var lineWidth: CGFloat = 0 var lineHeight: CGFloat = 0 for size in sizes { if lineWidth > 0 { if let propWidth = proposal.width { print("sizeThatFits: proposal.width: \(propWidth)") if lineWidth + horzSpacing + size.width > propWidth { totalHeight += lineHeight + vertSpacing // next line + spacing lineWidth = size.width lineHeight = size.height } else { lineWidth += size.width + horzSpacing // next item + spacing lineHeight = max(lineHeight, size.height) } } else { lineWidth += size.width + horzSpacing // next item + spacing lineHeight = max(lineHeight, size.height) } } else { // first subview, just place it lineWidth = size.width lineHeight = size.height } totalWidth = max(totalWidth, lineWidth) } totalHeight += lineHeight // first line, zero if there is no first return .init(width: totalWidth, height: totalHeight) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let sizes = subviews.map { $0.sizeThatFits(.unspecified) } var lineX = bounds.minX var lineY = bounds.minY var lineHeight: CGFloat = 0 for index in subviews.indices { let size = sizes[index] if lineX > bounds.minX { if let propWidth = proposal.width { print("placeSubviews: proposal.width: \(propWidth)") if lineX + size.width > propWidth { lineY += lineHeight + vertSpacing lineHeight = 0 lineX = bounds.minX } } } subviews[index].place(at: .init(x: lineX, y: lineY), anchor: .zero, proposal: ProposedViewSize(size) ) lineHeight = max(lineHeight, size.height) lineX += size.width + horzSpacing } } } But this is never called with the space available in proposal, thus it cannot calculate whether the circles would fit adjacent to the squares, or whether they should be moved to the next row. I wonder how the "Flow Layout" should work at all... Then I found this explanation about sizeThatFits: /// the parent view can call this method more than once with different proposals: /// .zero for the layout’s minimum size /// .infinity for the layout’s maximum size /// .unspecified for the layout’s ideal size which absolutely makes no sense IMHO. I need the exact (horizontal) space available as input to be able to calculate my layout, and place the subviews... Of course, I could return oneLine both as ideal and maximum size, and twoLines as minimum size, but it really depends on how much space there is which layout should be chosen, and whether I need to put twoLines into a (horizontal) scrollView or not. I remember having the same problem 15 years ago when implementing heightForTableViewCell: You tell me how much space I have horizontally, then I can return how much space I need vertically to place my content. Was tricky then, seems still not easy now :-( The sample code Apple provides for layout: "Composing custom layouts with SwiftUI" is completely useless. sizeThatFits should measure the box around the circle layout where the Avatars are placed, but it always returns the same size since that doesn't change at all, only the subview placement changes based on the user input. So, any tips how I could solve my animation problem?
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Mar ’25
Reply to UserDefaults.didChangeNotification not firing
OK, got it running: extension UserDefaults { private static let myBoolKey = "myBool" @objc dynamic var myBool: Bool { get { bool(forKey: UserDefaults.myBoolKey) } set { set(newValue, forKey: UserDefaults.myBoolKey) } } } (note that the value of the key "myBool" MUST BE identical to the name of the dynamic myBool, otherwise it doesn't work) and then call it in the init function of the (singleton) class: self.myBoolObservation = UserDefaults.standard.observe(\.myBool, options: [.new, .old,.prior,.initial]) { [weak self](_, _) in self?.myBool = UserDefaults.standard.myBool } However, this works only once on iOS 18, or rather only while the app keeps running. When you kill the app and launch it again, the closure is never called again. On iOS 17 I can quit and relaunch my app, and this code continues to work fine... Thus, still the problem of the thread starter (aolguin) -and mine too- is unsolved: since iOS 18 we don't get notifications in the app when the user changes app settings in Settings->Apps->myApp.
Topic: App & System Services SubTopic: General Tags:
Mar ’25
Reply to UserDefaults.didChangeNotification not firing
Hi Quinn, sadly this doesn't work for my SwiftUI iOS app. I added the startObserving() routine to my controller class, and called it from init, but it neither triggers the print(change) closure when I switch to iOS Settings->Apps->myApp and toggle the Toggle, nor when I return back to my app... Is this because the UserDefaults property is a string and my Settings.bundle Root.plist defines a PSToggleSwitchSpecifier? How can I observe a Toggle (instead of a string)?
Topic: App & System Services SubTopic: General Tags:
Mar ’25
Reply to Share arbitrary struct from HostApp to Extension and get arbitrary result back
Third, I played around with the NSExtensionActivationRule. While I can easily hide my extension (and thus the ContainingApp) from being shown in the ShareSheet, I cannot prevent Freeform and Threema in the application row, and “New Quick Note”, "Save to Files”, and “Airdrop” being shown when the HostApp opens the ShareSheet: How can the HostApp limit the possible share targets to just my ActionExtension and nothing else?
Topic: App & System Services SubTopic: General Tags:
Aug ’24
Reply to Share arbitrary struct from HostApp to Extension and get arbitrary result back
Second, SwiftUIs ShareLink is able to use the transferRepresentation of my ShareInput and ShareResult structs. I can call itemProvider.loadTransferable to extract my data. However, when using an ItemProvider for NSItemProviderWriting, NSItemProviderReading for the UIActivityItemsConfiguration, the transferred item is not a ShareInput or ShareResult struct, but the ItemProvider as wrapper around my struct. Probably because the transferred item must inherit from NSObject. Thus I cannot use UTType.shareInput.identifier, but must export and import the wrapper UTType.inputItemSource.identifier instead. While this works, it is ugly! Is there a possibility to transfer a struct with a UIActivityItemsConfiguration, or do I need to convert my ShareInput and ShareResult structs into classes, and then add NSItemProviderWriting, NSItemProviderReading with an extension? How does ShareLink do this? It transfers the struct (which is no descendant of NSObject) and not a wrapper...
Topic: App & System Services SubTopic: General Tags:
Aug ’24
Reply to Share arbitrary struct from HostApp to Extension and get arbitrary result back
Hi dts, using your sample code, I was able to transfer my ShareInput struct from the Host-App to my extension and the ShareResult struct back to the Host-App. See the sample project at github. If I defined a SharingExtension, then returnedItems was nil. I only got something back in returnedItems when I defined an ActionExtension - even though it had the exact same code... But there are still some problems: Your sample used NSItemProviderWriting, NSItemProviderReading for the ItemSource. How can I add a preview image and text (so users know what they’re sharing)? With ShareLink, I can specify a SharePreview: let previewTitle = "Choose the ContainingApp" let image = UIImage(systemName: "arrowshape.right.fill") let preview = SharePreview(previewTitle, image: Image(uiImage: image!)) ShareLink("ShareLink", item: shareInput, preview: preview) and get this: but since ShareLink cannot receive data sent back from the extension, I need to use a UIActivityViewController with completionWithItemsHandler: let itemSource = InputItemSource(shareInput: shareInput, previewTitle: previewTitle, previewImage: image!) let configuration = UIActivityItemsConfiguration(objects: [itemSource]) ActivityViewController(configuration: configuration) .presentationDetents([.medium]) As you can see, there’s no preview image and text: I tried to extend the ItemSource class to the UIActivityItemSource protocol: extension InputItemSource: UIActivityItemSource { func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize ) -> UIImage? { previewImage.image } func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { previewTitle } and even added func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { let metaData = LPLinkMetadata() metaData.title = previewTitle metaData.imageProvider = NSItemProvider(object: previewImage.image) return metaData } but this didn’t work - the share view still doesn’t show a preview. How can I add a preview image and text (so users know what they’re sharing)?
Topic: App & System Services SubTopic: General Tags:
Aug ’24
Reply to Share arbitrary struct from HostApp to Extension and get arbitrary result back
And finally, how do I transfer the ShareResult back to the HostApp? ShareLink has no return path, or does it? And this again will need an encoding conversion in the ShareExtension and a decoding conversion in the HostApp. How do I do that? As far as I'm aware, you can't. You could use alternative data sharing methods such as a shared App groups container. No, I can't. As I wrote before, the host app(s) will be build by a 3rd party. I definitely need to transfer the data back with the sharing call. If ShareLink cannot do that, then I need to fall back to UIActivityViewController with completionHandler - or do you have another suggestion?
Topic: App & System Services SubTopic: General Tags:
Jul ’24
Reply to Share arbitrary struct from HostApp to Extension and get arbitrary result back
Hi Emmanuel, thanks for your answers. In the host target, define "com.fesh.shareInput" as an exported type identifier But this is NOT defined by the host (there can and will be dozens of different host apps using my ShareExtension in the future). The user may have multiple of those 3rd party apps, and they could have a different idea what the data looks like. The real source of truth is only my ShareExtension. So why should I (or rather the 3rd party developers that want to use my extension - I am just building the sample for them) define that struct as exported type? Is this really necessary? In my experience it makes no difference for the ShareSheet whether the host defines it as export or import...
Topic: App & System Services SubTopic: General Tags:
Jul ’24
Reply to Share arbitrary struct from HostApp to Extension and get arbitrary result back
OK dts, you gave some hints about the declaration. I doubt changing the declaration in my ContainingApp from import to export really makes sense, because now there are TWO parties claiming to own the data (== exporting): The ShareExtension and the ContainingApp. Anyway, it doesn't change the behaviour of my sample project at all. I still have the same problems: • When I share via ShareLink, in my Extension I see the data arriving, can load it and get no error (see ShareViewController, line 50ff): if itemProvider.hasItemConformingToTypeIdentifier(shareInputType) { itemProvider.loadItem(forTypeIdentifier: shareInputType , options: nil) { [weak self] (input, error) in guard error == nil else { self?.cancel(error: error!); return } however, I cannot convert that data into my ShareInput struct: if let shareInput = input as? ShareInput { let text = shareInput.inputStr A breakpoint on that last line shows I never arrive there (but I do arrive on the line above starting with "if"). What am I doing wrong here? • When I try to share via UIActivityViewController, then Transferable doesn't work. In the Share-Sheet, my ContainingApp is not shown as target to share to, thus the shared data was not converted correctly. Text works: ShareButton(title: "Text", shareItems: [ItemSource(dataToShare: "sharing some text")]) but my struct doesn't: ShareButton(title: "JSON", shareItems: [ItemSource(dataToShare: shareInput) // ,ItemSource(dataToShare: "sharing some text") ]) When I uncomment the text item, then my ContainingApp again is shown in the ShareSheet as share target, but it receives only the text, not the shareInfo struct. The main problems are still unsolved: How can I encode/convert the ShareInput struct for UIActivityViewController? When using ShareLink, the encoding/conversion works. But how can I decode/convert the data to a ShareInput struct in my ShareExtension? And finally, how do I transfer the ShareResult back to the HostApp? ShareLink has no return path, or does it? And this again will need an encoding conversion in the ShareExtension and a decoding conversion in the HostApp. How do I do that? Do you have any sample code showing this? If not, please help me with this demo project and I am happy to pass it to you to make an official demo from it. Thanks, Marc
Topic: App & System Services SubTopic: General Tags:
Jul ’24
Reply to Share arbitrary struct from HostApp to Extension and get arbitrary result back
Hi dts, thanks for chiming in. In the debugger of the HostApp I see this error: Type "com.myapp.shareInput" was expected to be exported in the Info.plist of Host.app, but it was imported instead. Library: UniformTypeIdentifiers | Subsystem: com.apple.runtime-issues | Category: Type Declaration Issues You're seeing the error because "com.fesh.shareInput" wasn't added in the app's info.plist But it was added in the Host.app Info.plist - as imported, not exported. I wrote in my first posting: I definitely want to define and export both ShareInput and ShareResult as UTExportedTypeDeclarations in my extension, and all 3rd-party apps (like this demo HostApp) using my extension need to import them. We expect more 3rd party apps to use our extension, which IS the source of truth for these types. So it doesn't make sense for my sample demo Host.app to export them. However, I will add them just for debugging, to check whether that's the problem. Side Question 3: In the HostApp, can I use ShareLink() to present the Share-sheet and receive the result struct You could use ShareLink to present the share interface. For the imported content type, Transferable. handles the CodableRepresentation handles the conversion of the model type to and from binary data. Both my types are really simple structs, input contains 3 strings and output contains 4. For the simplicity of this demo project I only used 1 string each, just to demonstrate the data passing as Transferable. I could easily pack this data myself as JSON in a single TEXT blob since this seems to work best in my experience up to now, but I really want to use Transferable with CodableRepresentation and understand what is needed to make this work. However in some cases you would need to implement the closure that instantiates the item inother to convert the binary data to a type that your app is aware of My data only exists in memory and not as file, thus a file representation doesn't make sense for me. When you define a uniform type identifier, remember you need to declare it to be either an imported or exported type: • An exported type declaration is declared and owned by your application. For example, the Codable schema of ShareInput is an exported content type com.fesh.shareInput. Your application is the source of truth about given type, and it is also the primary handler of files with the corresponding file extension. Actually, my extension is the source of truth about both structs. I didn't define file extensions because the data is only in memory. It doesn't make sense to share that data via Mail, Messenger or AirDrop with another device. The only usecase is that both the 3rd party HostApp and my ContainingApp are installed on the same iPhone, and the HostApp uses my SharingExtension to communicate with my ContainingApp without switching context. An imported type declaration is owned by some other app, that may not be present on the device when your app is installed. We want to create a demo to show 3rd party developers how to use our SharingExtension. I understand that the 3rd party HostApp thus needs to import the types to be able to make Transferable work. In my demo project: I imported the types both in the Host.App as well as in the Containing.App, and exported them in the ShareExtension. I tried to export them from the Containing.App instead (and removed the declaration from the Extension's Info.plist) - but that lead to this error: Type "com.fesh.shareInput" was expected to be declared and exported in the Info.plist of ShareExtension.appex, but it was not found. Thus I added the export declaration back to the ShareExtension's Info.plist, and that error vanished. Now I am exporting both types, both from the ContainingApp as well as from it's ShareExtension. Is that really OK?
Topic: App & System Services SubTopic: General Tags:
Jul ’24
Reply to Share arbitrary struct from HostApp to Extension and get arbitrary result back
When I share both JSON and plain text: ShareButton(title: "JSON", shareItems: [ItemSource(dataToShare: shareInput) ,ItemSource(dataToShare: "sharing some text") ]) I get (iOS 16 vs. iOS 17): where iOS 16 ShareExtension[9236:652213] [ShareSheet] ❗️itemProvider=<NSItemProvider: 0x280afa1b0> {types = ( "public.plain-text" )} and iOS 17 transfer: ❗️itemProvider=<NSItemProvider: 0x300f99e30> {types = ( "public.plain-text" )} Library: ShareExtension | Subsystem: com.myapp.containingdemo.ShareExtensionDemo | Category: ShareSheet So again it's clear that the ShareInput is not transferred, but only the text. For the result, I didn't manage to pass a ShareResult struct to NSItemProvider: I just found NSItemProvider(item: contactData as NSSecureCoding, typeIdentifier: UTType.vCard.identifier) which probably works because contactData is known to iOS... See this thread: https://forums.developer.apple.com/forums/thread/24368 Again, transferring plain text was possible... Main Question: How can I transfer my ShareInput struct from the HostApp to the extension (iOS 16 and later), and then my ShareResult struct from the extension back to the HostApp (iOS 16 and later)? And how can the HostApp limit the possible sharing targets? I want neither persons nor actions to appear in the sharing dialog, just apps - preferably only my ContainingApp. This should be possible with a strict NSExtensionActivationRule, right? (Bummer, it is not possible to upload the zipped demo project. Thus I'll need to upload to github...)
Topic: App & System Services SubTopic: General Tags:
Jul ’24