Jon Bailey
05/28/2024, 1:02 PMFatal error: Unknown subtype. This error should not happen under normal circumstances since SirClass: shared.NetworkStateUIModel is sealed.
It breaks when upgrading from 0.6.4 to 0.7.0. If I turn on swift library evolution, then it works fine again. I'm not sure why that would be an issue though, as I'm not building an xcframework, just building a dynamic linked framework alongside the iOS app everytime with the embedAndSignAppleFrameworkForXcode
task. Additionally:
• If I print the value of the sealed class before calling onEnum(of:)
it prints a valid subclass name (None
, bad name I know but it's legacy code)
• the generated swift code is identical in 0.6.4 and 0.7.2.
• This is the only sealed class I have with an out
type parameter. The one other generic sealed class works and doesn't have out
for the type parameters.
I'll put the code in a threadJon Bailey
05/28/2024, 1:03 PM@SealedInterop.Enabled
public sealed class NetworkStateUIModel<out UIModel> {
public data object None : NetworkStateUIModel<Nothing>()
public data class Error(val error: UIErrorData) : NetworkStateUIModel<Nothing>()
public data object Loading : NetworkStateUIModel<Nothing>()
public data class Loaded<UIModel : Any>(val model: UIModel) :
NetworkStateUIModel<UIModel>()
}
Jon Bailey
05/28/2024, 1:04 PM// Generated by Touchlab SKIE 0.7.0
import Foundation
public extension shared.Skie.BusinessLogic.NetworkStateUIModel {
@frozen
enum __Sealed<UIModel : Swift.AnyObject> : Swift.Hashable {
case error(shared.NetworkStateUIModelError)
case loaded(shared.NetworkStateUIModelLoaded<UIModel>)
case loading(shared.NetworkStateUIModelLoading)
case none(shared.NetworkStateUIModelNone)
}
}
public func onEnum<UIModel : Swift.AnyObject, __Sealed : shared.NetworkStateUIModel<UIModel>>(of sealed: __Sealed) -> shared.Skie.BusinessLogic.NetworkStateUIModel.__Sealed<UIModel> {
if let sealed = sealed as? shared.NetworkStateUIModelError {
return shared.Skie.BusinessLogic.NetworkStateUIModel.__Sealed<UIModel>.error(sealed)
} else if let sealed = sealed as? shared.NetworkStateUIModelLoaded<UIModel> {
return shared.Skie.BusinessLogic.NetworkStateUIModel.__Sealed<UIModel>.loaded(sealed)
} else if let sealed = sealed as? shared.NetworkStateUIModelLoading {
return shared.Skie.BusinessLogic.NetworkStateUIModel.__Sealed<UIModel>.loading(sealed)
} else if let sealed = sealed as? shared.NetworkStateUIModelNone {
return shared.Skie.BusinessLogic.NetworkStateUIModel.__Sealed<UIModel>.none(sealed)
} else {
fatalError("Unknown subtype. This error should not happen under normal circumstances since SirClass: shared.NetworkStateUIModel is sealed.")
}
}
@_disfavoredOverload
public func onEnum<UIModel : Swift.AnyObject, __Sealed : shared.NetworkStateUIModel<UIModel>>(of sealed: __Sealed?) -> shared.Skie.BusinessLogic.NetworkStateUIModel.__Sealed<UIModel>? {
if let sealed {
return onEnum(of: sealed) as shared.Skie.BusinessLogic.NetworkStateUIModel.__Sealed<UIModel>
} else {
return nil
}
}
Filip Dolník
05/28/2024, 1:05 PMJon Bailey
05/28/2024, 1:06 PMFilip Dolník
05/28/2024, 1:13 PMJon Bailey
05/28/2024, 1:13 PMFilip Dolník
05/28/2024, 1:38 PMJon Bailey
05/28/2024, 1:48 PMpublic fun <NetworkError, NetworkData, Model, UIModel : Any> NetworkStateModelHolder<NetworkError, NetworkData, Model>.toUIModel(
create: (NetworkData, Model) -> UIModel,
errorToUIErrorData: (NetworkError) -> UIErrorData,
): NetworkStateUIModel<UIModel> =
when (networkState) {
is NetworkStateModel.Loading -> NetworkStateUIModel.Loading
is NetworkStateModel.None -> NetworkStateUIModel.None // <------ Here
is NetworkStateModel.Error -> NetworkStateUIModel.Error(
errorToUIErrorData(networkState.networkError)
)
is NetworkStateModel.Loaded -> NetworkStateUIModel.Loaded(
create(networkState.networkData, model)
)
}
Jon Bailey
05/28/2024, 1:48 PMpublic data class NetworkStateModelHolder<out NetworkError, out NetworkData, Model>(
public val networkState: NetworkStateModel<NetworkError, NetworkData>,
public val model: Model
) {
public fun copyModel(createNew: Model.() -> Model): NetworkStateModelHolder<NetworkError, NetworkData, Model> =
copy(model = model.createNew())
}
Jon Bailey
05/28/2024, 1:50 PM//
// ManageInactiveDDView.swift
// Elfin
//
// Created by Jonathan on 08/11/2023.
//
import SwiftUI
import UIComponents
import shared
class ManageInactiveDDViewModel: OrbitViewModel.BorrowerLoggedin_ManageDirectDebitContainerHost, ObservableObject {
@Published
var goToNextScreen: BorrowerReinstateDirectDebitResultKs?
init() {
super.init { KotlinModule.shared.borrowerLoggedInModule.ManageDirectDebitContainerHost(scope: $0) }
}
override func handleSideEffect(_ effect: ManageDirectDebitContainerHostEffect) {
switch onEnum(of: effect) {
case .showErrorToast(let toast):
showToast(error: toast.error)
case .reinstateResult(let model):
goToNextScreen = ks(model.result)
}
}
}
struct ManageInactiveDDView: View {
@StateObject
private var viewModel = ManageInactiveDDViewModel()
var body: some View {
VStack {
switch onEnum(of: viewModel.state) { // <-------- here
case .loading, .none:
FullScreenActivityIndicator()
.elfinNavBarClose()
case .error(let error):
ErrorMessageView(error: error.error, retryAction: { viewModel.events.refresh() })
.elfinNavBarClose()
case .loaded(let model):
ManageInactiveDDLoadedView(model: model.model, events: viewModel.events, toast: $viewModel.toast)
.navigation(item: $viewModel.goToNextScreen) { screen in
switch screen {
case .reinstateFailed:
ReinstateDirectDebitFailureView()
case .reinstateSucceeded(let model):
ReinstateDirectDebitSuccessView(model: model)
}
}
.elfinNavBarClose()
}
}
.track(.reinstateDirectDebit)
}
}
Filip Dolník
05/28/2024, 1:51 PMonEnum(of: NetworkStateUIModelNone.shared)
?Jon Bailey
05/28/2024, 1:51 PMxcode-select -p
gives the path to Xcode 15.1Jon Bailey
05/28/2024, 2:04 PMFilip Dolník
05/28/2024, 2:05 PMTadeas Kriz
05/28/2024, 2:28 PMstate
property is?Jon Bailey
05/28/2024, 2:42 PMTadeas Kriz
05/28/2024, 2:42 PMOrbitViewModel.BorrowerLoggedin_ManageDirectDebitContainerHost
and it's superclassesJon Bailey
05/28/2024, 2:43 PMTadeas Kriz
05/28/2024, 2:43 PMJon Bailey
05/28/2024, 2:44 PMTadeas Kriz
05/28/2024, 2:44 PMTadeas Kriz
05/28/2024, 2:45 PMOrbitViewModel.BorrowerLoggedin_ManageDirectDebitContainerHost
should be enoughJon Bailey
05/28/2024, 2:45 PMextension OrbitViewModel {
typealias BorrowerLoggedin_ManageDirectDebitContainerHost = OrbitViewModel<NetworkStateUIModel<ManageDirectDebitContainerHost.UIState>, ManageDirectDebitContainerHost, ManageDirectDebitContainerHostEffect>
}
Jon Bailey
05/28/2024, 2:45 PMimport Foundation
import Combine
import shared
import UIComponents
import SwiftUI
class OrbitViewModel<UIState, EventSender: AnyObject, UIEffect> {
private let container: OrbitContainerHostSwiftAbstraction<UIState, EventSender, UIEffect>
private var cancellables = Set<AnyCancellable>()
@Published
var state: UIState
var events: EventSender {
container.eventSender
}
@Published
var toast: Toast?
func handleSideEffect(_ effect: UIEffect) {
//
}
func onStateDidChange() {
}
func showToast(error: UIErrorData) {
self.toast = Toast(.failure, title: error.title, message: error.message)
}
init(_ containerProvider: (CoroutineScopeWrapper) -> OrbitContainerHostAbstraction<UIState.KotlinType, EventSender, UIEffect.KotlinType>) where UIState: GeneratedKotlinWrapper, UIEffect: GeneratedKotlinWrapper {
self.container = .init(uistateMap: UIState.init, uieffectMap: UIEffect.init, containerProvider)
self.state = container.initialValue
setup()
}
init(_ containerProvider: (CoroutineScopeWrapper) -> OrbitContainerHostAbstraction<UIState, EventSender, UIEffect.KotlinType>) where UIState: AnyObject, UIEffect: GeneratedKotlinWrapper {
self.container = .init(uistateMap: { $0 }, uieffectMap: UIEffect.init, containerProvider)
self.state = container.initialValue
setup()
}
init(_ containerProvider: (CoroutineScopeWrapper) -> OrbitContainerHostAbstraction<UIState.KotlinType, EventSender, UIEffect>) where UIState: GeneratedKotlinWrapper, UIEffect: AnyObject {
self.container = .init(uistateMap: UIState.init, uieffectMap: { $0 }, containerProvider)
self.state = container.initialValue
setup()
}
init(_ containerProvider: (CoroutineScopeWrapper) -> OrbitContainerHostAbstraction<UIState, EventSender, UIEffect>) where UIState: AnyObject, UIEffect: AnyObject {
self.container = .init(uistateMap: { $0 }, uieffectMap: { $0 }, containerProvider)
self.state = container.initialValue
setup()
}
// New Container host
init(_ containerProvider: (CoroutineScopeWrapper) -> EventSender) where UIState: GeneratedKotlinWrapper, UIEffect: GeneratedKotlinWrapper, EventSender: NewOrbitContainerHostAbstraction<UIState.KotlinType, UIEffect.KotlinType> {
self.container = .init(uistateMap: UIState.init, uieffectMap: UIEffect.init, containerProvider)
self.state = container.initialValue
setup()
}
init(_ containerProvider: (CoroutineScopeWrapper) -> EventSender) where UIState: AnyObject, UIEffect: GeneratedKotlinWrapper, EventSender: NewOrbitContainerHostAbstraction<UIState, UIEffect.KotlinType> {
self.container = .init(uistateMap: { $0 }, uieffectMap: UIEffect.init, containerProvider)
self.state = container.initialValue
setup()
}
init(_ containerProvider: (CoroutineScopeWrapper) -> EventSender) where UIState: GeneratedKotlinWrapper, UIEffect: AnyObject, EventSender: NewOrbitContainerHostAbstraction<UIState.KotlinType, UIEffect> {
self.container = .init(uistateMap: UIState.init, uieffectMap: { $0 }, containerProvider)
self.state = container.initialValue
setup()
}
init(_ containerProvider: (CoroutineScopeWrapper) -> EventSender) where UIState: AnyObject, UIEffect: AnyObject, EventSender: NewOrbitContainerHostAbstraction<UIState, UIEffect> {
self.container = .init(uistateMap: { $0 }, uieffectMap: { $0 }, containerProvider)
self.state = container.initialValue
setup()
}
private func setup() {
container.stateFlow.sink { [weak self] state in
guard let self else { return }
DispatchQueue.main.async {
self.state = state
self.onStateDidChange()
}
}.store(in: &cancellables)
container.sideEffectFlow.sink { [weak self] effect in
DispatchQueue.main.async {
self?.handleSideEffect(effect)
}
}.store(in: &cancellables)
}
}
Jon Bailey
05/28/2024, 2:47 PMJon Bailey
05/28/2024, 2:49 PMtypealias KSwiftSealedClassWrapper = GeneratedKotlinWrapper
protocol GeneratedKotlinWrapper {
associatedtype KotlinType: AnyObject
init(_ obj: KotlinType)
}
class OrbitContainerHostSwiftAbstraction<UIState, EventSender: AnyObject, UIEffect> {
internal init<UIStateKt: AnyObject, UIEffectKt: AnyObject>(uistateMap: @escaping (UIStateKt) -> UIState, uieffectMap: @escaping (UIEffectKt) -> UIEffect, _ containerProvider: (CoroutineScopeWrapper) -> OrbitContainerHostAbstraction<UIStateKt, EventSender, UIEffectKt>) {
self.scope = KotlinMultiPlatformUtils.createImmediateCoroutineScope()
let container = containerProvider(scope)
let flow = createPublisher(flow: container.stateFlow, scope: scope)
self.container = container
self.initialValue = uistateMap(flow.value)
self.stateFlow = flow.map(uistateMap).eraseToAnyPublisher()
self.sideEffectFlow = createPublisher(flow: container.sideEffectFlow, scope: scope).map(uieffectMap).eraseToAnyPublisher()
self.eventSender = container.eventSender
}
internal init<UIStateKt: AnyObject, UIEffectKt: AnyObject>(uistateMap: @escaping (UIStateKt) -> UIState, uieffectMap: @escaping (UIEffectKt) -> UIEffect, _ containerProvider: (CoroutineScopeWrapper) -> EventSender) where EventSender: NewOrbitContainerHostAbstraction<UIStateKt, UIEffectKt> {
self.scope = KotlinMultiPlatformUtils.createImmediateCoroutineScope()
let container = containerProvider(scope)
let flow = createPublisher(flow: container.stateFlow, scope: scope)
self.container = container
self.initialValue = uistateMap(flow.value)
self.stateFlow = flow.map(uistateMap).eraseToAnyPublisher()
self.sideEffectFlow = createPublisher(flow: container.sideEffectFlow, scope: scope).map(uieffectMap).eraseToAnyPublisher()
self.eventSender = container
}
private let scope: CoroutineScopeWrapper
private let container: Any
let initialValue: UIState
let stateFlow: AnyPublisher<UIState, Never>
let sideEffectFlow: AnyPublisher<UIEffect, Never>
let eventSender: EventSender
}
private func createPublisher<T>(flow: StateFlowCollectorHelper<T>, scope: Kotlinx_coroutines_coreCoroutineScope) -> CurrentValueSubject<T, Never> {
let publisher = CurrentValueSubject<T, Never>(flow.value)
flow.subscribe(scope: scope) {
publisher.send($0)
}
return publisher
}
private func createPublisher<T>(flow: FlowCollectorHelper<T>, scope: Kotlinx_coroutines_coreCoroutineScope) -> AnyPublisher<T, Never> {
let publisher = PassthroughSubject<T, Never>()
flow.subscribe(scope: scope) {
publisher.send($0)
}
return publisher.eraseToAnyPublisher()
}
Jon Bailey
05/28/2024, 2:52 PMTadeas Kriz
05/28/2024, 2:53 PMTadeas Kriz
05/28/2024, 2:54 PMJon Bailey
05/28/2024, 2:54 PMJon Bailey
05/28/2024, 2:54 PMJon Bailey
05/28/2024, 4:53 PMJon Bailey
05/28/2024, 4:54 PMworks
which removes the out
and it doesn't give the error.
(and a branch mre-without-nothing
where it has the error but without using Nothing)Jon Bailey
05/28/2024, 4:55 PMJon Bailey
05/28/2024, 4:55 PMTadeas Kriz
05/28/2024, 4:55 PMTadeas Kriz
05/28/2024, 4:55 PMJon Bailey
05/28/2024, 4:56 PMJon Bailey
05/28/2024, 5:01 PMTadeas Kriz
05/28/2024, 10:55 PMonEnum
function directly into iOSApp.swift
file, it's behaving the same way. I also found that while sealed is Shared.NetworkStateUIModelNone
returns false, type(of: sealed) == Shared.NetworkStateUIModelNone.self
is true, sealed.isKind(of: NetworkStateUIModelNone.self)
is also true.
So tomorrow I'll discuss with Filip whether we'll change to use isKind(of:)
for all usages, or if we'll keep looking into why this is happening. So far my main guess is that since Nothing
is jus a regular NSObject
subclass, Swift's release optimization shoots itself in the foot and thinks that when the generic param T is NSString
vs Nothing
that it'll never happen. But I'll probably look into assembly if I'm curious what the real answer is.Jon Bailey
05/28/2024, 11:10 PMJon Bailey
05/28/2024, 11:11 PMNothing
so in the project I sent, in the mre-without-nothing
branch I tried with a Parent, Child subclass and it still produced the problem.Tadeas Kriz
05/28/2024, 11:14 PMJon Bailey
05/29/2024, 12:39 AMisKind(of:)
everywhere is that it crashes when force casting it afterwards to store in the enum associated value. unsafeDowncast(to:)
works but that seems scary to use so much. Generic variance has previously caused me enough issues going from Kotlin to Swift, that I avoid it, and map to custom classes before exposing it to Swift. So maybe SKIE should just skip/error on any sealed classes that have out
(and in
?) type parameters.Jon Bailey
05/29/2024, 12:49 AMlet foo: SharedNetworkStateUIModel<Parent> = MRE.test()
print(foo as? SharedNetworkStateUIModelNone)
print((foo as! SharedNetworkStateUIModel<Child>) as! SharedNetworkStateUIModelNone)
Here is the only Swift and ObjC project:Tadeas Kriz
05/29/2024, 12:58 AMTadeas Kriz
05/29/2024, 12:58 AMTadeas Kriz
05/29/2024, 12:59 AMTadeas Kriz
05/29/2024, 1:56 AMif else
altogether and just keep the fatalError
. So that means it's confident it can do itTadeas Kriz
05/29/2024, 1:31 PMisKind(of:)
and unsafeDowncast(:to:)
as these seem to work correctly in all cases. As far as I understand it should be safe to do so, since these are ObjC classes where the generic type doesn't matter (it's erased in runtime)Jon Bailey
05/29/2024, 1:57 PMis
check returns true, which is the opposite of the case here. But I guess as the app would crash from the fatalError anyway it's ok, would there be any case where a usafeDowncast succeeds where it shouldn't instead of crashing?
It doesn't seem like anyone has used an out type parameter before and come across this, so wouldn't it be safer to disallow sealed interop to be used with such classes?Tadeas Kriz
05/29/2024, 1:59 PMTadeas Kriz
05/29/2024, 1:59 PMisKind(of:)
and unsafeDowncast(:to:)
Tadeas Kriz
05/29/2024, 2:00 PMis
does return true in debug builds as you expectTadeas Kriz
05/29/2024, 2:00 PMunsafeDowncast
and then it crashes way later when you access something that doesn't work. It may not even crash and lead to data corruption.Tadeas Kriz
05/29/2024, 2:01 PMisKind(of:)
which asks Objc runtime if one is subclass of the other. Since this is a dynamic call, Swift can't reason about it the same way it does with is
and can't optimize it away.Tadeas Kriz
05/29/2024, 2:07 PMJon Bailey
05/29/2024, 2:08 PMTadeas Kriz
05/29/2024, 2:09 PMJon Bailey
05/29/2024, 2:10 PMJon Bailey
05/29/2024, 2:11 PMis
returning true in the debug builds, I did get it in that pure swift+Objc project where it returns false in debug builds but it shouldn'tTadeas Kriz
05/29/2024, 2:13 PMTadeas Kriz
05/29/2024, 2:14 PMChild*
argument in your None
, but it should be completely different class:Tadeas Kriz
05/29/2024, 2:14 PMTadeas Kriz
05/29/2024, 2:14 PMJon Bailey
05/29/2024, 2:16 PMTadeas Kriz
05/29/2024, 2:18 PMTadeas Kriz
05/29/2024, 2:19 PMTadeas Kriz
05/29/2024, 2:19 PMis
the way you did is expected to produce false
in SwiftTadeas Kriz
05/29/2024, 2:20 PMTadeas Kriz
05/29/2024, 2:20 PMonEnum
that's generic for this to get into "works in debug, doesn't in release"Tadeas Kriz
05/29/2024, 2:20 PMTadeas Kriz
05/29/2024, 2:22 PM__covariant
, but it doesn't, still the same behaviorJon Bailey
05/29/2024, 2:23 PMTadeas Kriz
05/29/2024, 2:23 PMTadeas Kriz
05/29/2024, 2:24 PMTadeas Kriz
05/29/2024, 2:25 PMonEnum
replaces the generic param with a new bound UIModel
, which lets you put anything in and then inside it uses the Objc checkingTadeas Kriz
05/29/2024, 2:26 PMJon Bailey
05/29/2024, 2:34 PMunsafeDowncast(to:)
is needed so that it can't be optimized out?Tadeas Kriz
05/29/2024, 2:35 PMisKind(of:)
it's a runtime-call that Swift's optimizer can't know what it'll do (Objective-C has a weird type system, you can do real weird things) and so that makes the if
stay there. But then we need a way to cast it that is also a runtime thing because as!
is static. Therefore unsafeDowncast
Tadeas Kriz
05/29/2024, 2:38 PMTadeas Kriz
05/29/2024, 2:39 PMTadeas Kriz
05/29/2024, 2:42 PMTadeas Kriz
05/29/2024, 2:42 PMTadeas Kriz
05/29/2024, 2:43 PMJon Bailey
05/29/2024, 2:47 PMJon Bailey
05/29/2024, 2:47 PMTadeas Kriz
05/29/2024, 2:47 PMTadeas Kriz
05/29/2024, 2:48 PMJon Bailey
05/29/2024, 2:51 PMTadeas Kriz
05/29/2024, 2:51 PMTadeas Kriz
05/29/2024, 2:51 PMTadeas Kriz
05/29/2024, 2:51 PMTadeas Kriz
05/29/2024, 2:52 PMJon Bailey
05/29/2024, 2:58 PMTadeas Kriz
05/29/2024, 3:00 PMTadeas Kriz
05/29/2024, 3:00 PM@_optimize(none)
on the onEnum(of:)
declaration also makes it keep workingJon Bailey
05/29/2024, 3:01 PMerased
variable would be idealTadeas Kriz
05/29/2024, 3:03 PMwithExtendedLifetime
Tadeas Kriz
05/29/2024, 3:04 PMTadeas Kriz
05/29/2024, 3:04 PMlet erased: Any = sealed
and then erased as? X
is equal to sealed as? X
Jon Bailey
05/29/2024, 3:13 PMX
and the type of sealed
Jon Bailey
05/29/2024, 3:36 PMTadeas Kriz
05/29/2024, 3:37 PMTadeas Kriz
05/29/2024, 3:45 PM@inlinable
.Tadeas Kriz
05/29/2024, 3:48 PM@_semantics("optimize.sil.specialize.generic.never")
, then it works without the erased
workaroundFilip Dolník
05/31/2024, 7:13 AM