Post

Replies

Boosts

Views

Activity

Image Caching and Actors
I was fortunate enough to talk to two very knowledgeable Apple Engineers in a lab tonight and they helped me through my refactor for image caching using actors. In this case I want to be able to access the images directly on the main thread for use in the UI but have all image downloading and resizing happen on background threads. Briefly, it looked something like this: @MainActor class OsuImageCache { init() { ... } /// Grab an image on the main thread for use in collection view cells func image(for url: URL) -> UIImage? { } /// Cache an image on a background thread func cacheImage(for url: URL) async throws -> (image: UIImage?, thumbnail: UIImage?) { let (data, _) = try await session.data(from: url) async let processedImages = await processImageData(data, for: url) try saveInFileSystem(imageData: processedImages.imageData, thumbnailImageData: processedImages.thumbnailData, for url: URL) return (UIImage(data: processedImages.imageData), UIImage(data: processedImages.thumbnailData)) } /// Resize images and create thumbnails func processImageData(_ data: Data, url: URL) async -> (UIImage, UIImage) { // Code that resizes the image and the thumbnail goes here. It's all inline, no completion handlers nor async calls... ... // Finally, return return (finalProcessedImageData, finalProcessedThumbnailData) } } When we were talking, what I expected to have happen was the line with async let images = await processImageData(data, for: url) was supposed to kick off to a background thread and do the processing of the image for me. I happily disconnected with them thinking it was solved, but when I ran the app afterwards I noticed to my dismay that the image processing was still happening on the main thread. At this point I assume that I need to try something different, and I refactor by adding in a separate actor for image processing like so: @MainActor class OsuImageCache { /// Actor just for image processing and thumbnail generation let imageProcessing = ImageProcessing() init() { ... } func image(for url: URL) -> UIImage? { } func cacheImage(for url: URL) async throws -> (image: UIImage?, thumbnail: UIImage?) { let (data, _) = try await session.data(from: url) async let processedImages = await imageProcessing.processImageData(data, for: url) try saveInFileSystem(imageData: processedImages.imageData, thumbnailImageData: processedImages.thumbnailData, for url: URL) return (UIImage(data: processedImages.imageData), UIImage(data: processedImages.thumbnailData)) } } actor ImageProcessing { func processImageData(_ data: Data, url: URL) async -> (UIImage, UIImage) { // Code that resizes the image and the thumbnail goes here. It's all inline, no completion handlers nor async calls... ... // Finally, return return (finalProcessedImageData, finalProcessedThumbnailData) } } When I run it this time, I still see that the image processing within the ImageProcessing actor is still on the main thread. This is not what I expected. I flipped between different session videos and then changed the ImageProcessingActor like so: actor ImageProcessing { func processImageData(_ data: Data, url: URL) async -> (UIImage, UIImage) { typealias ImageContinuation = CheckedContinuation<(Data, Data), Never> return await withCheckedContinuation { (continuation: ImageContinuation) in async { // Code that resizes the image and the thumbnail goes here. ... // Finally, return continuation.resume(returning: (finalProcessedImageData, finalProcessedThumbnailData)) } } } } At this point it works! Image resizing happens on background threads and I still have the ability to use the images on the main thread! What I'm curious to know is if this is considered an OK pattern, or if I just stumbled by chance into something that worked but isn't actually the intended path. The checked continuation + async combo gets me the behavior I want, but I'm not sure if it's clean. Is there a way to simplify this further and remove any of this boilerplate? Thanks for any input you can give me!
1
0
1.5k
Jun ’21
UICollectionViewDiffableDatasource contentOffset changed on backgrounding app due to system snapshot
I've got several collection views that use diffable datasources, and we were generally very happy with how they worked until we noticed some strange behavior on iPad. We have a universal app that works on both iPhone and iPad and everything seems to be fine until you scroll to the bottom of a list on iPad. When you hit the home button there appears to be an attempt by the device to take a snapshot of the app, which is understandable, except that the frame changes when this happens and a redraw is requested by the collection view. Because the new view is narrower than the old one it makes it appear as if the list is longer than it is, and that we're now somewhere in the middle of it. When the app comes back to the foreground the correct frame is restored but the intermediary scroll offset is applied which gives a really janky feel because things don't appear to be how we left them. So to summarize: Diffable collection view loads Hit the home button FBSSceneSnapshotAction runs View frame is resized as it's minimizing for some reason, subviews are redrawn, data source configuration methods are re-run App is brought to foreground again but the content offset is now incorrect, somewhere up in the middle of the list. Is there any way to prevent the snapshot from taking the wrong frame values and messing up our content offset? We are using the new iOS 14 split view controller to host these views. It happens in both the primary and secondary containers. Below is the call stack for what is modifying the view hierarchy when I hit the home button in the simulator: Stack trace when snapshot triggers - https://developer.apple.com/forums/content/attachment/348e848d-3162-4f7a-8935-1dc5feea8efb If I put a simple print statement in my datasource I get different values based upon whether the app is in the foreground vs. just processing as it hits the background, which I think is the root problem. We're in the foreground! Size of the collection view: (703.5, 768.0) ☝️[OsuListCollectionViewController.swift]:236 26 configureDataSource() Oh no we're in the background now! Size of the collection view: (447.5, 1024.0) ☝️[OsuListCollectionViewController.swift]:236 26 configureDataSource()
0
0
1.1k
May ’21
Child items not deselecting in outline view
I have an outline with the new collection view list configuration set to sidebar. If I select an item at the root level and then disclose a node with children, I can select a child and the highlight of the outline switches to it. If I then tap the parent to hide the child and select a root item again, the highlight moves HOWEVER if I then disclose to display the child again it maintains its selected state and now it looks like my sidebar has two active values. How do I prevent this from happening?
0
0
685
Jul ’20
Sticker Pack problems with Xcode 11
I've got two problems with Xcode 11 GM:1. Whenever I upload my app that contains a sticker extension, App Store Connect yells at me: WARNING ITMS-90747: "Architecture incompatible with MinimumOSVersion. The app bundle at 'Ohio State.app/PlugIns/Ohio State Stickers.appex' specifies a MinimumOSVersion of '13.0' but contains a 32-bit architecture that is unsupported on iOS 12 and later."I don't see how this is possible from my configuration. It's set the same way as our other application extensions and they generate only for arm64. For some reason the sticker pack includes armv7, armv7s, and arm64.2. I can no longer select the messages extension app icon from the project navigator view. The value from Xcode 10 is in there still, but it's red and the drop down for App Icons Source doesn't even list the asset catalog that contains the icon. It appears to only be looking for standard iOS icons now.At this point I guess I'm going to be releasing our iOS 13 update with broken sticker apps unless anyone has a solid idea on how to fix this.
3
0
2.2k
Sep ’19
NSCoreDataCoreSpotlightDelegate crash: "Can't add store"
We are using Core Data with Core Spotlight integration and a small percentage of our users get a crash on app instantiation. It's very bizarre because 99% of the time it works fine and we have never seen this while developing. The NSInvalidArgumentException reads as "Can't add store" although we know that's not true in most cases because the spotlight database gets filled properly from our Core Data values and search generally works great.The exception backtrace looks like this:Fatal Exception: NSInvalidArgumentException 0 CoreFoundation 0x18ff3f758 __exceptionPreprocess 1 libobjc.A.dylib 0x18f147d00 objc_exception_throw 2 CoreData 0x192a95a30 -[NSCoreDataCoreSpotlightDelegate initForStoreWithDescription:model:] 3 OhioStateKit 0x1007da228 @objc CoreSpotlightDelegate.init(forStoreWith:model:) (CoreSpotlightDelegate.swift) 4 OhioStateKit 0x1007da18c CoreSpotlightDelegate.__allocating_init(forStoreWith:model:) (CoreSpotlightDelegate.swift) 5 OhioStateKit 0x1007115cc Persistence.constructAndConfigurePersistentContainer(completion:) (Persistence.swift:61) 6 OhioStateKit 0x100bec434 AppDelegate.application(_:didFinishLaunchingWithOptions:) (AppDelegate.swift:107) 7 OhioStateKit 0x100bed4c0 @objc AppDelegate.application(_:didFinishLaunchingWithOptions:) (&lt;compiler-generated&gt;) 8 UIKitCore 0x1bce170c8 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] 9 UIKitCore 0x1bce18890 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] 10 UIKitCore 0x1bce1e2b0 -[UIApplication _runWithMainScene:transitionContext:completion:] 11 UIKitCore 0x1bc6b1e20 __111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke 12 UIKitCore 0x1bc6ba7e8 +[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] 13 UIKitCore 0x1bc6b1a88 -[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] 14 UIKitCore 0x1bc6b23a4 -[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] 15 UIKitCore 0x1bc6b05ec __82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke 16 UIKitCore 0x1bc6b02b0 -[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] 17 UIKitCore 0x1bc6b4fd0 __125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke 18 UIKitCore 0x1bc6b5e1c _performActionsWithDelayForTransitionContext 19 UIKitCore 0x1bc6b4e84 -[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] 20 UIKitCore 0x1bc6b9e68 -[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] 21 UIKitCore 0x1bce1c7ec -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] 22 UIKitCore 0x1bc9ff648 -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] 23 FrontBoardServices 0x1928fd3a8 -[FBSSceneImpl _didCreateWithTransitionContext:completion:] 24 FrontBoardServices 0x1929076e4 __56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 25 FrontBoardServices 0x192906e34 __40-[FBSWorkspace _performDelegateCallOut:]_block_invoke 26 libdispatch.dylib 0x18f9518f4 _dispatch_client_callout 27 libdispatch.dylib 0x18f954ecc _dispatch_block_invoke_direct 28 FrontBoardServices 0x19293a218 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ 29 FrontBoardServices 0x192939e94 -[FBSSerialQueue _performNext] 30 FrontBoardServices 0x19293a490 -[FBSSerialQueue _performNextFromRunLoopSource] 31 CoreFoundation 0x18fecf954 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 32 CoreFoundation 0x18fecf8d0 __CFRunLoopDoSource0 33 CoreFoundation 0x18fecf18c __CFRunLoopDoSources0 34 CoreFoundation 0x18fec9e60 __CFRunLoopRun 35 CoreFoundation 0x18fec9764 CFRunLoopRunSpecific 36 GraphicsServices 0x192105dd4 GSEventRunModal 37 UIKitCore 0x1bce2000c UIApplicationMain 38 Ohio State 0x1004e78c0 main (main.swift:29) 39 libdyld.dylib 0x18f988ffc startThe instantiation code mostly looks like this:let persistentStoreUrl = Device.shared.applicationSupportDirectory().appendingPathComponent("OSU Mobile.sqlite") let storeDescription = NSPersistentStoreDescription(url: persistentStoreUrl) let container = NSPersistentContainer(name: "OSU Mobile", managedObjectModel: model) storeDescription.setOption(CoreSpotlightDelegate(forStoreWith: storeDescription, model: container.managedObjectModel): NSCoreDataCoreSpotlightExporter) container.persistentStoreDescriptions = [storeDescription] container.loadPersistentStores(completionHandler: { (storeDescription, error) in // The app is ready to go }Has anyone experienced anything like this? From what we can tell from the somewhat sparse documentation we think we are using it correctly.
1
0
1.6k
Apr ’19
Changes in Swift 5 to inheritance?
We have a strange situation where behavior has changed from Swift 4 -&gt; 5 and I can't figure out what is going wrong. We have our own open base table view controller class in a framework that conforms to UITableViewDatasource and UITableViewDelegate. In Swift 4 the subclasses could simply implement any of those delegate methods and they would work as expected. With Swift 5 and the latest Xcode this has all changed and any signature from those protocols that isn't explicitly written out in the base class are completely ignored, causing all kinds of strange bugs.Our base class implementation looks something like this:open class OsuTableViewController: OsuViewController, UITableViewDelegate, UITableViewDataSource { public let tableView = UITableView(frame: .zero, style: .grouped) public var tableSections = [OsuTableSection]() override open func viewDidLoad() { super.viewDidLoad() tableView.datasource = self tableView.delegate = self } open func numberOfSections(in tableView: UITableView) -&gt; Int { return tableSections.count } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int { switch tableSections[section].tableSectionType { default: return tableSections[section].rows.count } } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell { let cell = tableView.dequeueReusableCell(OsuTableViewCell.self) return cell } }In Swift 4 any subclass then would be able to implement didSelectRowAtIndexPath or viewForHeaderInSection even without those declarations being explicitly stated in the base class.class AboutYouCustomizeTableViewController: OsuTableViewController { // With Xcode 10.2 and Swift 5 this is never called. It worked for years prior. func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -&gt; Bool { return true } }With Swift 5 we find that the signatures in the subclasses are simply ignored unless we start writing out every single one in the base class. This is painful and sometimes leads to undesired behavior. Is this an intentional change or a bug? How do we get the old behavior back?
3
0
4.0k
Apr ’19