diff --git a/packages/expo-modules-core/CHANGELOG.md b/packages/expo-modules-core/CHANGELOG.md index 7e190bdcd4edc9..f3753fdfdefbe7 100644 --- a/packages/expo-modules-core/CHANGELOG.md +++ b/packages/expo-modules-core/CHANGELOG.md @@ -14,6 +14,8 @@ ### 💡 Others +- [iOS] Improved conversions of returned arrays and dictionaries with mixed element types. ([#42641](https://github.com/expo/expo/pull/42641) by [@barthap](https://github.com/barthap)) + ## 55.0.12 — 2026-02-25 _This version does not introduce any user-facing changes._ diff --git a/packages/expo-modules-core/ios/Core/DynamicTypes/DynamicArrayType.swift b/packages/expo-modules-core/ios/Core/DynamicTypes/DynamicArrayType.swift index 82849e75b67ec3..21137147525af7 100644 --- a/packages/expo-modules-core/ios/Core/DynamicTypes/DynamicArrayType.swift +++ b/packages/expo-modules-core/ios/Core/DynamicTypes/DynamicArrayType.swift @@ -37,9 +37,7 @@ internal struct DynamicArrayType: AnyDynamicType { func convertResult(_ result: ResultType, appContext: AppContext) throws -> Any { if let result = result as? [Any] { - return try result.map({ element in - return try elementType.convertResult(element, appContext: appContext) - }) + return result.map { Conversions.convertFunctionResult($0, appContext: appContext) } } return result } diff --git a/packages/expo-modules-core/ios/Core/DynamicTypes/DynamicDictionaryType.swift b/packages/expo-modules-core/ios/Core/DynamicTypes/DynamicDictionaryType.swift index 1e9bdf0378f60d..8d0e8fa4b50c77 100644 --- a/packages/expo-modules-core/ios/Core/DynamicTypes/DynamicDictionaryType.swift +++ b/packages/expo-modules-core/ios/Core/DynamicTypes/DynamicDictionaryType.swift @@ -41,6 +41,13 @@ internal struct DynamicDictionaryType: AnyDynamicType { throw Conversions.CastingException<[AnyHashable: Any]>(value) } + func convertResult(_ result: ResultType, appContext: AppContext) throws -> Any { + if let result = result as? [AnyHashable: Any] { + return result.mapValues { Conversions.convertFunctionResult($0, appContext: appContext) } + } + return result + } + var description: String { "[Hashable: \(valueType.description)]" } diff --git a/packages/expo-modules-core/ios/Tests/DynamicTypeTests.swift b/packages/expo-modules-core/ios/Tests/DynamicTypeTests.swift index a99dc3fec66f32..1738e1d5e8ef79 100644 --- a/packages/expo-modules-core/ios/Tests/DynamicTypeTests.swift +++ b/packages/expo-modules-core/ios/Tests/DynamicTypeTests.swift @@ -251,6 +251,19 @@ struct DynamicTypeTests { try (~[String].self).cast(84, appContext: appContext) } } + + @Test + func `returns mixed elements to JS`() throws { + let mixedArray: [Any] = [1, ArrayBuffer.allocate(size: 3)] + + let converted = try (~[Any].self).convertResult(mixedArray, appContext: appContext) + let jsValue = try (~[Any].self).castToJS(converted, appContext: appContext) + + #expect(jsValue.kind == .object) + let jsArray = try jsValue.asArray() + #expect(try jsArray.first??.getInt() == 1) + #expect(try jsArray.last??.isArrayBuffer() == true) + } @Test func `wraps is true`() { @@ -280,6 +293,97 @@ struct DynamicTypeTests { } } + // MARK: - DynamicDictionaryType + + @Suite("DynamicDictionaryType") + struct DynamicDictionaryTypeTests { + let appContext: AppContext + + init() { + appContext = AppContext.create() + } + + @Test + func `is created`() { + #expect(~[String: Double].self is DynamicDictionaryType) + #expect(~[String: String?].self is DynamicDictionaryType) + #expect(~[String: [Int]].self is DynamicDictionaryType) + #expect(~[String: Any].self is DynamicDictionaryType) + } + + @Test + func `casts succeeds`() throws { + #expect(try (~[String: Double].self).cast(["a": 1.2, "b": 3.4], appContext: appContext) as? [String: Double] == ["a": 1.2, "b": 3.4]) + #expect(try (~[String: [String]].self).cast(["key": ["hello", "expo"]], appContext: appContext) as? [String: [String]] == ["key": ["hello", "expo"]]) + } + + @Test + func `casts from JS value`() throws { + let appContext = AppContext.create() + let jsValue = try appContext.runtime.eval("({a: 1.2, b: 3.4})") + #expect(try (~[String: Double].self).cast(jsValue: jsValue, appContext: appContext) as? [String: Double] == ["a": 1.2, "b": 3.4]) + } + + @Test + func `casts dictionary values`() throws { + let value = 9.9 + let anyValue: [String: Any] = ["key": value] + let result = try (~[String: Double].self).cast(anyValue, appContext: appContext) as! [AnyHashable: Any] + + #expect(result is [String: Double]) + #expect(result as? [String: Double] == ["key": value]) + } + + @Test + func `returns mixed elements to JS`() throws { + let mixedDict: [String: Any] = ["num": 1, "buf": ArrayBuffer.allocate(size: 3)] + + let converted = try (~[String: Any].self).convertResult(mixedDict, appContext: appContext) + let jsValue = try (~[String: Any].self).castToJS(converted, appContext: appContext) + + #expect(jsValue.kind == .object) + let jsObject = try jsValue.asObject() + #expect(try jsObject.getProperty("num").getInt() == 1) + #expect(jsObject.getProperty("buf").isArrayBuffer() == true) + } + + @Test + func `throws CastingException`() { + #expect(throws: Conversions.CastingException<[AnyHashable: Any]>.self) { + try (~[String: String].self).cast(84, appContext: appContext) + } + } + + @Test + func `wraps is true`() { + #expect((~[String: Double].self ~> [String: Double].self) == true) + #expect((~[String: [String]].self ~> [String: [String]].self) == true) + #expect((~[String: CGPoint].self ~> [String: CGPoint].self) == true) + #expect((~[String: Any].self ~> [String: Any].self) == true) + } + + @Test + func `wraps is false`() { + #expect((~[String: String].self !~> [String: Int].self) == true) + #expect((~[String: Double].self !~> Double.self) == true) + } + + @Test + func `equals is true`() { + #expect((~[String: String].self == ~[String: String].self) == true) + #expect((~[String: CGSize].self == ~[String: CGSize].self) == true) + #expect((~[String: [[Double]]].self == ~[String: [[Double]]].self) == true) + #expect((~[String: Any].self == ~[String: Any].self) == true) + } + + @Test + func `equals is false`() { + #expect((~[String: Int].self != ~[String: Double].self) == true) + #expect((~[String: [String]].self != ~[String: String].self) == true) + #expect((~[String: URL].self != ~[String: String].self) == true) + } + } + // MARK: - DynamicConvertibleType @Suite("DynamicConvertibleType")