diff --git a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj index 33a277bdd6..ba2bbc12cf 100644 --- a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj +++ b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj @@ -70,6 +70,9 @@ 62E0736325535E8700BAAADB /* flat.json in CopyFiles */ = {isa = PBXBuildFile; fileRef = 62E0735425535E6500BAAADB /* flat.json */; }; 62E0736425535E8700BAAADB /* hierarchy.json in CopyFiles */ = {isa = PBXBuildFile; fileRef = 62E0735525535E6500BAAADB /* hierarchy.json */; }; 62E207AE2588234500A78983 /* WebViewDelegationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E207AD2588234500A78983 /* WebViewDelegationHandler.swift */; }; + 62FABD1A25AE5C01007B3814 /* Array+Capacitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD1925AE5C01007B3814 /* Array+Capacitor.swift */; }; + 62FABD2325AE60BA007B3814 /* BridgedTypesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD2225AE60BA007B3814 /* BridgedTypesTests.m */; }; + 62FABD2B25AE6182007B3814 /* BridgedTypesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD2A25AE6182007B3814 /* BridgedTypesHelper.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -151,7 +154,6 @@ 625AF1EC258963C700869675 /* WebViewAssetHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewAssetHandler.swift; sourceTree = ""; }; 62959AE22524DA7700A3D7F1 /* CAPPluginCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAPPluginCall.h; sourceTree = ""; }; 62959AE32524DA7700A3D7F1 /* JSExport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSExport.swift; sourceTree = ""; }; - 62959AE42524DA7700A3D7F1 /* CAPBridgedJSTypes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CAPBridgedJSTypes.m; sourceTree = ""; }; 62959AE52524DA7700A3D7F1 /* CAPBridgedPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAPBridgedPlugin.h; sourceTree = ""; }; 62959AE62524DA7700A3D7F1 /* CAPPluginCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAPPluginCall.swift; sourceTree = ""; }; 62959AE72524DA7700A3D7F1 /* CAPFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAPFile.swift; sourceTree = ""; }; @@ -197,6 +199,9 @@ 62E0735425535E6500BAAADB /* flat.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = flat.json; sourceTree = ""; }; 62E0735525535E6500BAAADB /* hierarchy.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = hierarchy.json; sourceTree = ""; }; 62E207AD2588234500A78983 /* WebViewDelegationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDelegationHandler.swift; sourceTree = ""; }; + 62FABD1925AE5C01007B3814 /* Array+Capacitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Capacitor.swift"; sourceTree = ""; }; + 62FABD2225AE60BA007B3814 /* BridgedTypesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BridgedTypesTests.m; sourceTree = ""; }; + 62FABD2A25AE6182007B3814 /* BridgedTypesHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgedTypesHelper.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -245,7 +250,6 @@ 6296A77C253A2E49005A202A /* TestsHostApp */, 50503EE01FC08594003606DC /* Products */, 501CBAA51FC0A723009B0D4D /* Frameworks */, - 621ECCD12542059800D3D615 /* Recovered References */, ); sourceTree = ""; }; @@ -264,7 +268,9 @@ children = ( 50503EED1FC08595003606DC /* CapacitorTests.swift */, 621ECCC2254204B700D3D615 /* BridgedTypesTests.swift */, + 62FABD2225AE60BA007B3814 /* BridgedTypesTests.m */, 62A91C3325535F5700861508 /* ConfigurationTests.swift */, + 62FABD2A25AE6182007B3814 /* BridgedTypesHelper.swift */, 621ECCC7254204BE00D3D615 /* JSONSerializationWrapper.h */, 621ECCC6254204BE00D3D615 /* JSONSerializationWrapper.m */, 621ECCCD254204C400D3D615 /* CapacitorTests-Bridging-Header.h */, @@ -273,14 +279,6 @@ path = CapacitorTests; sourceTree = ""; }; - 621ECCD12542059800D3D615 /* Recovered References */ = { - isa = PBXGroup; - children = ( - 62959AE42524DA7700A3D7F1 /* CAPBridgedJSTypes.m */, - ); - name = "Recovered References"; - sourceTree = ""; - }; 62959AE12524DA7700A3D7F1 /* Capacitor */ = { isa = PBXGroup; children = ( @@ -321,6 +319,7 @@ 62959B152524DA7700A3D7F1 /* CAPNotifications.swift */, 62959B072524DA7700A3D7F1 /* CapacitorExtension.swift */, 62959B112524DA7700A3D7F1 /* Data+Capacitor.swift */, + 62FABD1925AE5C01007B3814 /* Array+Capacitor.swift */, 62D43AEF2581817500673C24 /* WKWebView+Capacitor.swift */, 62D43B642582A13D00673C24 /* WKWebView+Capacitor.m */, 62959AE92524DA7700A3D7F1 /* UIColor.swift */, @@ -573,6 +572,7 @@ 62959B302524DA7800A3D7F1 /* UIStatusBarManager+CAPHandleTapAction.m in Sources */, 62959B392524DA7800A3D7F1 /* CapacitorExtension.swift in Sources */, 62959B422524DA7800A3D7F1 /* DocLinks.swift in Sources */, + 62FABD1A25AE5C01007B3814 /* Array+Capacitor.swift in Sources */, 62959B172524DA7800A3D7F1 /* JSExport.swift in Sources */, 373A69C1255C9360000A6F44 /* NotificationHandlerProtocol.swift in Sources */, 625AF1ED258963C700869675 /* WebViewAssetHandler.swift in Sources */, @@ -594,8 +594,10 @@ buildActionMask = 2147483647; files = ( 50503EEE1FC08595003606DC /* CapacitorTests.swift in Sources */, + 62FABD2B25AE6182007B3814 /* BridgedTypesHelper.swift in Sources */, 621ECCC8254204BE00D3D615 /* JSONSerializationWrapper.m in Sources */, 62A91C3425535F5700861508 /* ConfigurationTests.swift in Sources */, + 62FABD2325AE60BA007B3814 /* BridgedTypesTests.m in Sources */, 621ECCC3254204B700D3D615 /* BridgedTypesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Capacitor/Capacitor/Array+Capacitor.swift b/ios/Capacitor/Capacitor/Array+Capacitor.swift new file mode 100644 index 0000000000..dffdfd7a01 --- /dev/null +++ b/ios/Capacitor/Capacitor/Array+Capacitor.swift @@ -0,0 +1,31 @@ +// convenience wrappers to transform Arrays between NSNull and Optional values, for interoperability with Obj-C +extension Array: CapacitorExtension {} +extension CapacitorExtensionTypeWrapper where T == Array { + public func replacingNullValues() -> Array { + return baseType.map({ (value) -> JSValue? in + if value is NSNull { + return nil + } + return value + }) + } + + public func replacingOptionalValues() -> Array { + return baseType + } +} + +extension CapacitorExtensionTypeWrapper where T == Array { + public func replacingNullValues() -> Array { + return baseType + } + + public func replacingOptionalValues() -> Array { + return baseType.map({ (value) -> JSValue in + if let value = value { + return value + } + return NSNull() + }) + } +} diff --git a/ios/Capacitor/Capacitor/CAPPluginCall.swift b/ios/Capacitor/Capacitor/CAPPluginCall.swift index 3ae5dca327..1f52257db2 100644 --- a/ios/Capacitor/Capacitor/CAPPluginCall.swift +++ b/ios/Capacitor/Capacitor/CAPPluginCall.swift @@ -23,8 +23,12 @@ extension CAPPluginCall: JSValueContainer { } @objc public extension CAPPluginCall { + @available(*, deprecated, message: "Presence of a key should not be considered significant. Use typed accessors to check the value instead.") func hasOption(_ key: String) -> Bool { - return self.options.index(forKey: key) != nil + guard let value = options[key] else { + return false + } + return !(value is NSNull) } @available(*, deprecated, renamed: "resolve()") diff --git a/ios/Capacitor/Capacitor/JSTypes.swift b/ios/Capacitor/Capacitor/JSTypes.swift index f362b096ad..1e6b27f65e 100644 --- a/ios/Capacitor/Capacitor/JSTypes.swift +++ b/ios/Capacitor/Capacitor/JSTypes.swift @@ -8,6 +8,7 @@ extension Int: JSValue {} extension Float: JSValue {} extension Double: JSValue {} extension NSNumber: JSValue {} +extension NSNull: JSValue {} extension Array: JSValue {} extension Date: JSValue {} extension Dictionary: JSValue where Key == String, Value == JSValue {} @@ -183,6 +184,10 @@ extension JSTypes { public static func coerceDictionaryToJSObject(_ dictionary: [AnyHashable: Any]?) -> JSObject? { return coerceToJSValue(dictionary) as? JSObject } + + public static func coerceArrayToJSArray(_ array: [Any]?) -> JSArray? { + return array?.compactMap { coerceToJSValue($0) } + } } private func coerceToJSValue(_ value: Any?) -> JSValue? { @@ -204,6 +209,8 @@ private func coerceToJSValue(_ value: Any?) -> JSValue? { return doubleValue case let dateValue as Date: return dateValue + case let nullValue as NSNull: + return nullValue case let arrayValue as NSArray: return arrayValue.compactMap { coerceToJSValue($0) } case let dictionaryValue as NSDictionary: diff --git a/ios/Capacitor/CapacitorTests/BridgedTypesHelper.swift b/ios/Capacitor/CapacitorTests/BridgedTypesHelper.swift new file mode 100644 index 0000000000..9df3f786a8 --- /dev/null +++ b/ios/Capacitor/CapacitorTests/BridgedTypesHelper.swift @@ -0,0 +1,31 @@ +import Foundation +@testable import Capacitor + +enum BridgeTypeError: Error { + case badCast +} + +@objc class BridgedTypesHelper: NSObject { + @objc static let shared = BridgedTypesHelper() + + var untypedArray: [Any] { + return [] + } + + @objc func validTransformationOf(array: [Any]) -> [Any] { + let result = JSTypes.coerceArrayToJSArray(array)!.capacitor.replacingNullValues() + return result.capacitor.replacingOptionalValues() as [Any] + } + + @objc func invalidTransformationOf(array: [Any]) -> [Any] { + let result = JSTypes.coerceArrayToJSArray(array)!.capacitor.replacingNullValues() + return result as [Any] + } + + @objc func testCast(of array: [Any], atIndex index: Int) throws -> Any { + if let castArray = array as? [JSValue] { + return castArray[index] as Any + } + throw BridgeTypeError.badCast + } +} diff --git a/ios/Capacitor/CapacitorTests/BridgedTypesTests.m b/ios/Capacitor/CapacitorTests/BridgedTypesTests.m new file mode 100644 index 0000000000..71ac945dbd --- /dev/null +++ b/ios/Capacitor/CapacitorTests/BridgedTypesTests.m @@ -0,0 +1,54 @@ +#import +#import +// forward declaration of internal capacitor classes that are exposed in the swift header via the @testable import. +@interface CAPWebViewAssetHandler: NSObject +@end +@interface CapacitorBridge: NSObject +@end +@interface CAPWebViewDelegationHandler: NSObject +@end +// import that will fail without the declarations +#import "CapacitorTests-Swift.h" + +// interface for this class +@interface BridgedTypesTestsObjc : XCTestCase +@end + +@implementation BridgedTypesTestsObjc + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testNullHandling { + NSArray* source = @[@"test", [NSNull null], @3]; + NSArray* result = [[BridgedTypesHelper shared] validTransformationOfArray:source]; + NSError *error = nil; + // test that the replaced null value exists + id value = [result objectAtIndex:1]; + XCTAssertNotNil(value); + XCTAssertTrue([value isKindOfClass:[NSNull class]]); + // test that the null value casts to non-optional + value = [[BridgedTypesHelper shared] testCastOf:result atIndex:1 error:&error]; + XCTAssertNotNil(value); + XCTAssertNil(error); +} + +- (void)testOptionalHandling { + NSArray* source = @[@"test", [NSNull null], @3]; + NSArray* result = [[BridgedTypesHelper shared] invalidTransformationOfArray:source]; + NSError *error = nil; + // test that the removed null value, now optional, is automatically transformed back into a NSNull + id value = [result objectAtIndex:1]; + XCTAssertNotNil(value); + XCTAssertTrue([value isKindOfClass:[NSNull class]]); + // test that the optional value fails to cast to non-optional + value = [[BridgedTypesHelper shared] testCastOf:result atIndex:1 error:&error]; + XCTAssertNil(value); + XCTAssertNotNil(error); +} +@end diff --git a/ios/Capacitor/CapacitorTests/BridgedTypesTests.swift b/ios/Capacitor/CapacitorTests/BridgedTypesTests.swift index 873a52f499..ccac9209c9 100644 --- a/ios/Capacitor/CapacitorTests/BridgedTypesTests.swift +++ b/ios/Capacitor/CapacitorTests/BridgedTypesTests.swift @@ -83,4 +83,43 @@ class BridgedTypesTests: XCTestCase { let coercedFloat = coercedResult["testFloat"]! XCTAssertTrue(type(of: coercedFloat) == underlyingType.self) } + + func testNullWrapping() throws { + let dictionary: [AnyHashable: Any] = ["testInt": 1 as Int, "testNull": NSNull()] + let coercedDictionary = JSTypes.coerceDictionaryToJSObject(dictionary)! + XCTAssertNotNil(coercedDictionary) + XCTAssertEqual(coercedDictionary.count, 2) + XCTAssertTrue(coercedDictionary["testNull"]! is NSNull) + } + + func testNullTransformation() throws { + let array: [Any] = [1, NSNull(), "test string"] + let coercedArray = JSTypes.coerceArrayToJSArray(array)! + XCTAssertNotNil(coercedArray) + XCTAssertEqual(coercedArray.count, 3) + XCTAssertTrue(type(of: coercedArray[1]) == NSNull.self) + let filteredArray = coercedArray.capacitor.replacingNullValues() + XCTAssertEqual(filteredArray.count, 3) + XCTAssertNil(filteredArray[1]) + let restoredArray = filteredArray.capacitor.replacingOptionalValues() + XCTAssertEqual(restoredArray.count, 3) + XCTAssertNotNil(restoredArray[1]) + XCTAssertTrue(restoredArray[0] is NSNumber) + XCTAssertTrue(restoredArray[1] is NSNull) + XCTAssertTrue(restoredArray[2] is String) + } + + func testSparseArrayCastSuccess() throws { + let array: [Any] = ["test string 1", "test string 2", NSNull()] + let sparseArray = JSTypes.coerceArrayToJSArray(array)?.capacitor.replacingNullValues() as? [String?] + XCTAssertNotNil(sparseArray) + XCTAssertEqual(sparseArray!.count, 3) + XCTAssertNil(sparseArray![2]) + } + + func testSparseArrayCastFailure() throws { + let array: [Any] = ["test string 1", 1, NSNull()] + let sparseArray = JSTypes.coerceArrayToJSArray(array)?.capacitor.replacingNullValues() as? [String?] + XCTAssertNil(sparseArray) + } }