[Need Compiler Expert] Is there a way to workaround 'Symbol not found' crash on iOS 15 (sim) for APIS available since iOS 13?

I've run into a dead end right now. For quick context, I've been using some hidden SwiftUI APIs which are public but prefixed with an underscore. I'm fully aware that those APIs are hidden for good reasons, that's not my issue with them though. Those APIs work as expected, but there's a really strange catch to it.

SwiftUI has hidden APIs like _ScrollView, _PagingView and some underlying structures such as _ScrollableLayout protocol etc.
The _PagingView, constructs _ScrollView<_ScrollableLayoutView<some Collection, PagingLayout>>. The PagingLayout type is private to the framework but it's known to conform to the _ScrollableLayout protocol. The _PagingView can be constructed and used just fine with an iOS 15 simulator.

However this issue starts when we implement and call a custom type which also conforms to the _ScrollableLayout protocol.

This minimal example can reproduce the issue:

import SwiftUI

@main
struct YOUR_APPS_NAME: App {
  var body: some Scene {
    WindowGroup {
      let views: [Color] = [] // empty, not relevant
      // the subscript comes protocol and constructs
      // _ScrollView<_ScrollableLayoutView<[Color], CustomLayout>>
      CustomLayout()[views] 
    }
  }
}

struct CustomLayout: _ScrollableLayout {
  func update(state: inout (), proxy: inout _ScrollableLayoutProxy) {
    // can be empty for the test, not relevant
  }

  // no effect if uncommented
  // func decelerationTarget(contentOffset: CGPoint, originalContentOffset: CGPoint, velocity: _Velocity<CGSize>, size: CGSize) -> CGPoint? {
  //   nil
  // }
}

The iOS 15.X simulators all crash (tested only with 15.0 and 15.5). iOS 16.0 runs the application just fine.

The crash:

dyld[22806]: Symbol not found: _$s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVAOSQ12CoreGraphicsyHCg_GAOtFTq
  Referenced from: /Users/admin/Library/Developer/CoreSimulator/Devices/098D24BA-E32C-49FB-BD04-2DBD1CB0584C/data/Containers/Bundle/Application/A249D7E6-0CFF-45A4-AD7B-521E80405ED8/ScrollTester.app/ScrollTester
  Expected in: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.0.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/SwiftUI.framework/SwiftUI

When demangling the above we get:

method descriptor for SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?

This particular protocol function exists since iOS 13, I double checked iOS 13 and 15.5 .swiftinterface files from the system modules.

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol _ScrollableLayout : SwiftUI.Animatable {
  ...
  func decelerationTarget(contentOffset: CoreGraphics.CGPoint, originalContentOffset: CoreGraphics.CGPoint, velocity: SwiftUI._Velocity<CoreGraphics.CGSize>, size: CoreGraphics.CGSize) -> CoreGraphics.CGPoint?
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension SwiftUI._ScrollableLayout {
  public func decelerationTarget(contentOffset: CoreGraphics.CGPoint, originalContentOffset: CoreGraphics.CGPoint, velocity: SwiftUI._Velocity<CoreGraphics.CGSize>, size: CoreGraphics.CGSize) -> CoreGraphics.CGPoint?
}

I sent the example to someone with an iPhone running iOS 15.x to test if the issue is present on a real device, and it also crashes there. I'm running out of ideas. Is there anything I can do about it? I really have to use that custom layout type which also implements the decelerationTarget method.

More context:

  • Version 15.2 (15C500b)
  • swift-driver version: 1.87.3 Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)

Will test an older Xcode version to see if it makes any difference.


This must be a compiler bug, right?

I can reproduce the issue with iOS 15.5 Simulator here.

And the binary do contains such symbol here.

It have a default implementation to return nil here.

_$s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF:        // SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?
000000000038bf04         mov        x0, #0x0
000000000038bf08         mov        x1, #0x0
000000000038bf0c         mov        w2, #0x1
000000000038bf10         ret

If you do a grep for nm result of the SwiftUI binary, you'll get the following relevant results.

nm ./SwiftUI | grep xx
000000000038bf04 t _$s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF
0000000000392944 T _$s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtFTj
0000000000c0e720 S _$s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtFTq
000000000038bf14 T _$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF

Then you can run xcrun swift-demangle on them respectively.

xcrun swift-demangle s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF
$s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF ---> SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?
xcrun swift-demangle s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtFTj
$s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtFTj ---> dispatch thunk of SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?
xcrun swift-demangle s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtFTq
$s7SwiftUI17_ScrollableLayoutP18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtFTq ---> method descriptor for SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?
xcrun swift-demangle s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF
$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF ---> (extension in SwiftUI):SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?

Looks good so far. However let's check the error message again.

dyld[70796]: Symbol not found: _$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVAOSQ12CoreGraphicsyHCg_GAOtF
  Referenced from: <private>/Demo.app/Demo
  Expected in: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.5.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/SwiftUI.framework/SwiftUI

The missing symbol is _$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVAOSQ12CoreGraphicsyHCg_GAOtF. And it is not here in the binary of course.

Demangle it and we'll get a familiar result.

xcrun swift-demangle s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVAOSQ12CoreGraphicsyHCg_GAOtF
$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVAOSQ12CoreGraphicsyHCg_GAOtF ---> (extension in SwiftUI):SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?

So the key is here.

mangled symbol demangled name
15.5 SwiftUI binary _$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF (extension in SwiftUI):SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?
What the binary build by Xcode 15 is calling _$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVAOSQ12CoreGraphicsyHCg_GAOtF (extension in SwiftUI):SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?

I'm not a Swift compiler expert. So I can't tell why the second symbol gives the same result of the first one and where is "CoreGraphics" going?

2 Likes

I do not think it is a Swift compiler issue. And I suspect the old Xcode/iOS SDK will build fine because it will make you app call the old symbol without "CoreGraphicsyHCg_GAOtF" suffix.

Good find, but we cannot rely on the old sdk or build tools. We're already using new language features in our project. Not sure what to do about it as this is a critical issue as we have to support iOS 15 and a certain scrolling behavior, which isn't possible in SwiftUI otherwise without significant design compramises. :pensive:

Usually such binary symbol migration should keep binary compatibility. And if not, you should file a issue to Apple so that they will fix it. But considering this is a private API, they may just deny your request.

Considering SwiftUI is a black box system framework. When you rely on such private API, you must be prepare in advance that it may not be available in future OS updates/Xcode updates.

1 Like

UI/UX compromises when supporting of old OS are reasonable. At some point you'd want to get rid of iOS 15 support altogether and remove the associated workarounds. As to using private API's – I won't recommend it, but that's your choice of course.

That part is fine as on iOS 16 I can build a UIKit sandwich to achieve what we need, but native SwiftUI has no answer to our needs, not even with the newest iOS 17 scroll view extensions.

So after all this is an Xcode issue, right?

I believe this is an iOS SDK issue. You need a Swift compiler expert here to help confirm the trailing "CoreGraphics"''s meaning. And maybe there is some workaround that you can modifying the SDK to make it call the old symbol on iOS 15 here.

1 Like

Not in our particular case. Migrating from UIKit to SwiftUI comes with a significant downgrade both in terms UX and UI in the context of scroll views. SwiftUI did not provide any support for custom pickers and still doesn't. To make things worse the paging ability which it provides through the TabView with a paging style is broken on all OS versions (haven't re-tested iOS 17.x). If SwiftUI would have provided a UIHostingView from the beginning the there wouldn't be any issues, you could create UIKit sandwiches where necessary, but on iOS it's still only _UIHostingView. That capability was only made available for macOS through NSHostingView. Abusing UIHostingController for lazy and reusable things like UICollectionView cells is just another can of worms.

That said, the public _ScrollView provides all we need from a scroll view to implement custom pickers, laziness and even pagination. It just happened that the symbols mismatch on iOS 15, which is our minimum deployment target.

That would be great. I'm willing to do some shenanigans, even if it means to create some kind of binary dependency that I can include into our SPM module. The _ScrollableLayout conforming type is completely independent, I could extract it into a standalone library if necessary.

It's private API so I'd rather consider it non existing.

I used it in exactly the same context (for cells) and didn't find any issues.

Public? I can see only ScrollView, not the underscored version.

If SwiftUI doesn't work well for you under iOS 15 and UIKit works better – you could use UIKit component on iOS 15. You mentioned "migrating", so I assume you already had the UIKit app, doesn't look impossible to grab a particular component from the old app and use it in the otherwise SwiftUI app on iOS 15. It's more work, sure, but not impossible. And you don't have to maintain it for long, say in a year's time we'd have iOS 18 and you'd probably be dropping support for iOS 15.

It's not private, it's public but hidden, just because it has the underscore prefix. (Check the .swiftinterface file for SwiftUI.)

Let's please not derail further conversation whether this APIs should be used or not and what organization decisions should be made. :) We're aware of the risks we have to take when using them. The only problem is that the symbols mismatch is a more or less a bug, not that the APIs in questions cause a crash due to some undefined behavior.

The public but hidden _PagingView uses a private PageLayout which conforms to public but hidden _ScrollableLayout protocol. I can create a type that conforms to that protocol myself, but on iOS 15 the symbols get out of hand. :firecracker: It works perfectly fine on iOS 16 and newer.

1 Like

You can use the -expand option of swift-demangle to show the full demangle tree, which should help spot what the extra “CoreGraphics” is doing. I hesitate to guess exactly what’s broken down here, but Apple could very well have changed something about this symbol between releases expecting nobody to reference it directly, in which case Swift-the-language gives you zero tools to adjust.

2 Likes

Thanks for pointing out the -expand option.

I run swift-demangle with -expand to both of them.

And the second one with "CoreGraphics" has an extra item of RetroactiveConformance here.

@@ -1,4 +1,4 @@
-Demangling for $s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CG
PointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF
+Demangling for $s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CG
PointVSgAJ_AjA9_VelocityVySo6CGSizeVAOSQ12CoreGraphicsyHCg_GAOtF
 kind=Global
   kind=Function
     kind=Extension
@@ -39,6 +39,21 @@ kind=Global
                         kind=Structure
                           kind=Module, text="__C"
                           kind=Identifier, text="CGSize"
+                    kind=TypeList
+                      kind=RetroactiveConformance
+                        kind=Number, index=0
+                        kind=ConcreteProtocolConformance
+                          kind=Type
+                            kind=Structure
+                              kind=Module, text="__C"
+                              kind=Identifier, text="CGSize"
+                          kind=ProtocolConformanceRefInOtherModule
+                            kind=Type
+                              kind=Protocol
+                                kind=Module, text="Swift"
+                                kind=Identifier, text="Equatable"
+                            kind=Module, text="CoreGraphics"
+                          kind=AnyProtocolConformanceList
               kind=TupleElement
                 kind=Type
                   kind=Structure
@@ -56,4 +71,4 @@ kind=Global
                   kind=Structure
                     kind=Module, text="__C"
                     kind=Identifier, text="CGPoint"
-$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVGAOtF ---> (extension in SwiftUI):SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) -> __C.CGPoint?
\ No newline at end of file
+$s7SwiftUI17_ScrollableLayoutPAAE18decelerationTarget13contentOffset015originalContentH08velocity4sizeSo7CGPointVSgAJ_AjA9_VelocityVySo6CGSizeVAOSQ12CoreGraphicsyHCg_GAOtF ---> (extension in SwiftUI):SwiftUI._ScrollableLayout.decelerationTarget(contentOffset: __C.CGPoint, originalContentOffset: __C.CGPoint, velocity: SwiftUI._Velocity<__C.CGSize>, size: __C.CGSize) 
-> __C.CGPoint?
\ No newline at end of file
1 Like

+1. Such issue is out of scope of the Swift forum. And such conversation and suggestion is better made on Apple's developer forum. (We don't care whether OP's app will be rejected for using private API or otherwise violate the Apple Store review. We are concerned about the symbol evolution compatibility of Swift binary here)

We may better focusing on Swift language itself here to see if there is some workaround to fix the crash OP is facing.

1 Like

Checking the implementation of SwiftUI on iOS 15.5 :arrow_down:

struct SwiftUI._Velocity<A> where A: Equatable{
    var valuePerSecond: A
}

It already has a "Equtable" constraint on _Velocity's generic type. So the conformance seems reasonable for me.

I guess with iOS 16/17 SDK, Apple has moved extension CGSize: Swift.Equatable or CGSize def to somewhere else. And that's the problem here.

On iOS 15.5 SDK, CGSize is defined on CoreGraphics. So if the Equatable conformance is also defined on CoreGraphics, the conformance is not a RetroactiveConformance.

// CoreGraphics - CGGeometry.h
/* Sizes. */

struct CGSize {
    CGFloat width;
    CGFloat height;
};
typedef struct CG_BOXABLE CGSize CGSize;

However on iOS 17.0 SDK, CGSize is defined on CoreFoundation instead. Then the conformance would be a RetroactiveConformance.

// CoreGraphics - CGGeometry.h

#ifndef CF_DEFINES_CG_TYPES
/* Sizes. */

struct CGSize {
    CGFloat width;
    CGFloat height;
};
typedef struct CG_BOXABLE CGSize CGSize;
...
#endif /* CF_DEFINES_CG_TYPES */

// CoreFoundation - CFCGTypes.h
...
#define CF_DEFINES_CG_TYPES

struct CGSize {
    CGFloat width;
    CGFloat height;
};
typedef struct CF_BOXABLE CGSize CGSize;

I was wondering how Apple keeps such compatibility for other public binary. You need to find a public symbol in Apple's system framework which use CGSize as generic type conforming to Equatable type just like this one. Then we can continue the investigation. cc @DevAndArtist

If you can find another public one, two results:

  1. It works fine. Then we can check the magic Apple is using. And try to apply it to our case.
  2. It does not work and crash too. Since this a public symbol, then you can file a FB and ask for a fix for it. Hopefully we'll get a new Xcode/iOS SDK later to fix the
    compatibility issue.
Here are some of my irresponsible personal guesses.

I found that many types of CG on Linux are defined in the Foundation module of swift-corelibs-foundation defined using Swift. But on previous Darwin platform, they were defined on CoreGraphics module.

So I often need to use the following code to use CG type on my cross-platform package.

OpenSwiftUI/Sources/OpenSwiftUI/Layout/LayoutAdjustments/Edge/EdgeInsets.swift at 56d29ff98a989ae5604eb184b01c0256fe4b3660 · OpenSwiftUIProject/OpenSwiftUI · GitHub

With the launch of the new swift-foundation and Apple want to make it universal in all platforms Swift support, I guess Apple then decided to move these CG types to CoreFoundation which exist on Linux as an implementation detail.

2 Likes

I was unaware of the "public but hidden" notation, is that official? Is it alright to use "public but hidden" symbols in my app without a fear that Apple changes / removes them? I was under impression that Apple could do it along with coordinately changing their binaries accordingly at the same time but everyone else apps would stop working, is this not the case?


We were discussing ReferenceConvertible in the other thread and even if that is a pubic API the concencus of the discussion was that we should not use it, to the extent that the following warning was added to the documentation a few days ago as a result of that discussion:

The difference between that and "public but hidden" things like "_ScrollView", "_PagingView" is that they don't even have a documentation page where such warning could appear...


If it is somehow possible to patch dlsym and call the original, then this would work (pseudocode):

func dlsym(_ handle: UnsafeRawPointer, _ name: UnsafePointer<UInt8>) -> UnsafeRawPointer {
    if name == "s7Swift...CGSizeVAOSQ12CoreGraphicsyHCg_GAOtFTq" {
        name = "s7Swift...CGSizeVGAOtF"
    }
    return originalDlSym(handle, name)
}

No idea however how to achieve that. Google suggests it's possible via "Code Injection with Dyld Interposing" but I never tried it myself.

Yes. Apple has the right to make changes to "public but hidden" API.

(Actually we can the same thing to call the private API by patching the interface file thought those private one are more likely to be updated in a future release)

See my demo here to use SliderStyle on iOS 17. SwiftUIPostDemo/Slider at main · Kyle-Ye/SwiftUIPostDemo · GitHub
I believe Apple will make it public on 18 or 19 :)

Both @DevAndArtist and I are aware of the risk and we are not blaming Apple. It's the platform owner's right to make such change since its not part of the public API.

Maybe we can provide one with @_silgen_name for the older Runtime just like what we did for UnsafeMutablePointer in the Standard library.

Hope this helps.

2 Likes

This sounds interesting, it would save me an enormous amount of time implementing a totally different unstable solution. I'll try to experiment with it in a bit.