diff --git a/src/Core/AppleNative/ApiDefinition.cs b/src/Core/AppleNative/ApiDefinition.cs new file mode 100644 index 000000000000..c563d5b48e2a --- /dev/null +++ b/src/Core/AppleNative/ApiDefinition.cs @@ -0,0 +1,30 @@ +using System; +using CoreAnimation; +using Foundation; +using ObjCRuntime; + +namespace Microsoft.Maui.Platform +{ + /// + /// Behavior that automatically resizes a CALayer to match its superlayer's bounds + /// + [BaseType(typeof(NSObject), Name = "MauiCALayerAutosizeToSuperLayerBehavior")] + [Internal] + interface MauiCALayerAutosizeToSuperLayerBehavior + { + /// + /// Attaches this behavior to the given layer. + /// The layer must have a superlayer when this method is called. + /// The layer's frame will be kept in sync with the superlayer's bounds. + /// + /// The layer that needs to be resized to match the superlayer's bounds. + [Export("attachWithLayer:")] + MauiCALayerAutosizeToSuperLayerResult Attach(CALayer layer); + + /// + /// Detaches this behavior from the current layer and stops observing + /// + [Export("detach")] + void Detach(); + } +} \ No newline at end of file diff --git a/src/Core/AppleNative/PlatformInterop/.gitignore b/src/Core/AppleNative/PlatformInterop/.gitignore new file mode 100644 index 000000000000..ffaa87ce641f --- /dev/null +++ b/src/Core/AppleNative/PlatformInterop/.gitignore @@ -0,0 +1,5 @@ +Framework/catalyst.xcarchive +Framework/ios_devices.xcarchive +Framework/ios_simulator.xcarchive +MauiPlatformInterop.xcodeproj/xcuserdata +MauiPlatformInterop.xcodeproj/project.xcworkspace/xcuserdata diff --git a/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcframework.zip b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcframework.zip new file mode 100644 index 000000000000..6e472ab6d25f Binary files /dev/null and b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcframework.zip differ diff --git a/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.pbxproj b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..184265e35449 --- /dev/null +++ b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.pbxproj @@ -0,0 +1,358 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + AB0B2BC82E3529AF00B2B77C /* MauiPlatformInterop.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MauiPlatformInterop.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + AB0B2BCA2E3529AF00B2B77C /* MauiPlatformInterop */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = MauiPlatformInterop; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + AB0B2BC52E3529AE00B2B77C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + AB0B2BBE2E3529AE00B2B77C = { + isa = PBXGroup; + children = ( + AB0B2BCA2E3529AF00B2B77C /* MauiPlatformInterop */, + AB0B2BC92E3529AF00B2B77C /* Products */, + ); + sourceTree = ""; + }; + AB0B2BC92E3529AF00B2B77C /* Products */ = { + isa = PBXGroup; + children = ( + AB0B2BC82E3529AF00B2B77C /* MauiPlatformInterop.framework */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + AB0B2BC32E3529AE00B2B77C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + AB0B2BC72E3529AE00B2B77C /* MauiPlatformInterop */ = { + isa = PBXNativeTarget; + buildConfigurationList = AB0B2BCE2E3529AF00B2B77C /* Build configuration list for PBXNativeTarget "MauiPlatformInterop" */; + buildPhases = ( + AB0B2BC32E3529AE00B2B77C /* Headers */, + AB0B2BC42E3529AE00B2B77C /* Sources */, + AB0B2BC52E3529AE00B2B77C /* Frameworks */, + AB0B2BC62E3529AE00B2B77C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + AB0B2BCA2E3529AF00B2B77C /* MauiPlatformInterop */, + ); + name = MauiPlatformInterop; + packageProductDependencies = ( + ); + productName = MauiPlatformInterop; + productReference = AB0B2BC82E3529AF00B2B77C /* MauiPlatformInterop.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AB0B2BBF2E3529AE00B2B77C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 1640; + TargetAttributes = { + AB0B2BC72E3529AE00B2B77C = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = AB0B2BC22E3529AE00B2B77C /* Build configuration list for PBXProject "MauiPlatformInterop" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AB0B2BBE2E3529AE00B2B77C; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = AB0B2BC92E3529AF00B2B77C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AB0B2BC72E3529AE00B2B77C /* MauiPlatformInterop */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AB0B2BC62E3529AE00B2B77C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AB0B2BC42E3529AE00B2B77C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + AB0B2BCF2E3529AF00B2B77C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos*]" = 12.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.maui.MauiPlatformInterop; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AB0B2BD02E3529AF00B2B77C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos*]" = 12.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.maui.MauiPlatformInterop; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AB0B2BD12E3529AF00B2B77C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + AB0B2BD22E3529AF00B2B77C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AB0B2BC22E3529AE00B2B77C /* Build configuration list for PBXProject "MauiPlatformInterop" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AB0B2BD12E3529AF00B2B77C /* Debug */, + AB0B2BD22E3529AF00B2B77C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AB0B2BCE2E3529AF00B2B77C /* Build configuration list for PBXNativeTarget "MauiPlatformInterop" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AB0B2BCF2E3529AF00B2B77C /* Debug */, + AB0B2BD02E3529AF00B2B77C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AB0B2BBF2E3529AE00B2B77C /* Project object */; +} diff --git a/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..0c67376ebacb --- /dev/null +++ b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/xcshareddata/xcschemes/MauiPlatformInterop.xcscheme b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/xcshareddata/xcschemes/MauiPlatformInterop.xcscheme new file mode 100644 index 000000000000..ea4aa4a88c73 --- /dev/null +++ b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop.xcodeproj/xcshareddata/xcschemes/MauiPlatformInterop.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop/CALayerAutosizeToSuperLayerBehavior.swift b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop/CALayerAutosizeToSuperLayerBehavior.swift new file mode 100644 index 000000000000..f10374f9c77c --- /dev/null +++ b/src/Core/AppleNative/PlatformInterop/MauiPlatformInterop/CALayerAutosizeToSuperLayerBehavior.swift @@ -0,0 +1,58 @@ +import Foundation +import QuartzCore + +/// Result codes for MauiCALayerAutosizeToSuperLayerBehavior +@objc +public enum MauiCALayerAutosizeToSuperLayerResult: Int { + case success = 0 + case missingSuperlayer = 1 +} + +/// Behavior that automatically resizes a CALayer to match its superlayer's bounds +@objc(MauiCALayerAutosizeToSuperLayerBehavior) +public class MauiCALayerAutosizeToSuperLayerBehavior: NSObject { + + private var observation: NSKeyValueObservation? + + /// Attaches this behavior to the given layer + /// The layer must have a superlayer when this method is called + /// The layer's frame will be kept in sync with the superlayer's bounds. + /// - Parameter layer: The layer that needs to be resized to match the superlayer's bounds. + @objc + @discardableResult + public func attach(layer: CALayer) -> MauiCALayerAutosizeToSuperLayerResult { + // Detach from any previous layer + detach() + + guard let superLayer = layer.superlayer else { + return .missingSuperlayer + } + + // Set initial frame to match superlayer bounds + layer.frame = superLayer.bounds + + // Observe superlayer's bounds using modern block-based KVO + // Note: CALayer is not Sendable in Swift 6, which triggers a compiler warning. + // This is safe because: + // 1. KVO change handlers for UI objects are delivered on the main thread + // 2. The weak capture prevents retain cycles + // 3. CALayer operations are inherently main-thread bound + observation = superLayer.observe(\.bounds, options: .new) { [weak layer] observedLayer, _ in + guard let layer = layer else { return } + layer.frame = observedLayer.bounds + } + + return .success + } + + /// Detaches this behavior from the current layer, stopping the automatic resizing. + @objc + public func detach() { + observation?.invalidate() + observation = nil + } + + deinit { + detach() + } +} diff --git a/src/Core/AppleNative/PlatformInterop/build-xcframework.sh b/src/Core/AppleNative/PlatformInterop/build-xcframework.sh new file mode 100755 index 000000000000..bf50cfa75801 --- /dev/null +++ b/src/Core/AppleNative/PlatformInterop/build-xcframework.sh @@ -0,0 +1,63 @@ +#!/bin/bash -e + +PROJECT="MauiPlatformInterop" +CONFIGURATION="Release" +SCHEME=$PROJECT +FRAMEWORK_NAME=$PROJECT +OUTPUT_DIR="Framework" +XCFRAMEWORK_DIR="${OUTPUT_DIR}/${FRAMEWORK_NAME}.xcframework" + +rm -rf "$OUTPUT_DIR" +rm -rf "${FRAMEWORK_NAME}.xcframework.zip" +mkdir -p "$OUTPUT_DIR" + +# iOS Device +xcodebuild archive \ + -project "${PROJECT}.xcodeproj" \ + -scheme "$SCHEME" \ + -configuration "$CONFIGURATION" \ + -destination "generic/platform=iOS" \ + -archivePath "$OUTPUT_DIR/ios_devices.xcarchive" \ + -sdk iphoneos \ + SKIP_INSTALL=NO \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES + +# iOS Simulator +xcodebuild archive \ + -project "${PROJECT}.xcodeproj" \ + -scheme "$SCHEME" \ + -configuration "$CONFIGURATION" \ + -destination "generic/platform=iOS Simulator" \ + -archivePath "$OUTPUT_DIR/ios_simulator.xcarchive" \ + -sdk iphonesimulator \ + SKIP_INSTALL=NO \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES + +# Catalyst +xcodebuild archive \ + -project "${PROJECT}.xcodeproj" \ + -scheme "$SCHEME" \ + -configuration "$CONFIGURATION" \ + -destination "platform=macOS,variant=Mac Catalyst" \ + -archivePath "$OUTPUT_DIR/catalyst.xcarchive" \ + -sdk macosx \ + SKIP_INSTALL=NO \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES + +# Combine into an XCFramework +xcodebuild -create-xcframework \ + -framework "$OUTPUT_DIR/ios_devices.xcarchive/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \ + -framework "$OUTPUT_DIR/ios_simulator.xcarchive/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \ + -framework "$OUTPUT_DIR/catalyst.xcarchive/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \ + -output "$XCFRAMEWORK_DIR" + +# Zip the $XCFRAMEWORK_DIR directory +# Create zip using ditto (preserves metadata better than zip for frameworks) +( + cd "$OUTPUT_DIR" + ditto -c -k --sequesterRsrc --keepParent "${FRAMEWORK_NAME}.xcframework" "../${FRAMEWORK_NAME}.xcframework.zip" +) + +rm -rf "$OUTPUT_DIR" + + diff --git a/src/Core/AppleNative/StructsAndEnums.cs b/src/Core/AppleNative/StructsAndEnums.cs new file mode 100644 index 000000000000..b09a8bf15315 --- /dev/null +++ b/src/Core/AppleNative/StructsAndEnums.cs @@ -0,0 +1,14 @@ +using ObjCRuntime; + +namespace Microsoft.Maui.Platform +{ + /// + /// Result codes for MauiCALayerAutosizeToSuperLayerBehavior + /// + [Native] + enum MauiCALayerAutosizeToSuperLayerResult : long + { + Success = 0, + MissingSuperlayer = 1 + } +} \ No newline at end of file diff --git a/src/Core/src/Core.csproj b/src/Core/src/Core.csproj index 9c3b44e4fdb1..43c5de929746 100644 --- a/src/Core/src/Core.csproj +++ b/src/Core/src/Core.csproj @@ -23,6 +23,10 @@ true + + true + + true @@ -61,6 +65,13 @@ + + + Static + + + + @@ -87,6 +98,9 @@ + + + diff --git a/src/Core/src/Platform/iOS/CALayerAutosizeObserver.cs b/src/Core/src/Platform/iOS/CALayerAutosizeObserver.cs deleted file mode 100644 index 905946867b66..000000000000 --- a/src/Core/src/Platform/iOS/CALayerAutosizeObserver.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using CoreAnimation; -using CoreGraphics; -using Foundation; - -namespace Microsoft.Maui.Platform; - -[Register("MauiCALayerAutosizeObserver")] -class CALayerAutosizeObserver : NSObject -{ - static readonly NSString _boundsKey = new("bounds"); - - readonly WeakReference _layerReference; - bool _disposed; - - public static CALayerAutosizeObserver Attach(CALayer layer) - { - _ = layer ?? throw new ArgumentNullException(nameof(layer)); - - var superLayer = layer.SuperLayer ?? throw new InvalidOperationException("SuperLayer should be set before creating CALayerAutosizeObserver"); - var observer = new CALayerAutosizeObserver(layer); - superLayer.AddObserver(observer, _boundsKey, NSKeyValueObservingOptions.New, observer.Handle); - layer.Frame = superLayer.Bounds; - return observer; - } - - private CALayerAutosizeObserver(CALayer layer) - { - _layerReference = new WeakReference(layer); - IsDirectBinding = false; - } - - [Preserve(Conditional = true)] - public override void ObserveValue(NSString keyPath, NSObject ofObject, NSDictionary change, IntPtr context) - { - if (!_disposed && keyPath == _boundsKey && context == Handle && _layerReference.TryGetTarget(out var layer)) - { - layer.Frame = layer.SuperLayer?.Bounds ?? CGRect.Empty; - } - } - - protected override void Dispose(bool disposing) - { - if (!_disposed) - { - _disposed = true; - - if (_layerReference.TryGetTarget(out var layer)) - { - layer?.SuperLayer?.RemoveObserver(this, _boundsKey); - } - } - - base.Dispose(disposing); - } -} diff --git a/src/Core/src/Platform/iOS/MauiCALayer.cs b/src/Core/src/Platform/iOS/MauiCALayer.cs index 75472b966efa..f2d6ead0e4a0 100644 --- a/src/Core/src/Platform/iOS/MauiCALayer.cs +++ b/src/Core/src/Platform/iOS/MauiCALayer.cs @@ -11,8 +11,12 @@ namespace Microsoft.Maui.Platform { public class MauiCALayer : CALayer, IAutoSizableCALayer { + [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in MauiCALayerAutosizeToSuperLayerBehavior_DoesNotLeak test.")] + readonly MauiCALayerAutosizeToSuperLayerBehavior _autosizeToSuperLayerBehavior = new(); + CGRect _bounds; WeakReference _shape; + UIColor? _backgroundColor; Paint? _background; @@ -29,9 +33,6 @@ public class MauiCALayer : CALayer, IAutoSizableCALayer nfloat _strokeMiterLimit; - [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in CALayerAutosizeObserver_DoesNotLeak test.")] - CALayerAutosizeObserver? _boundsObserver; - public MauiCALayer() { _bounds = new CGRect(); @@ -41,22 +42,19 @@ public MauiCALayer() protected override void Dispose(bool disposing) { - _boundsObserver?.Dispose(); - _boundsObserver = null; + _autosizeToSuperLayerBehavior.Detach(); base.Dispose(disposing); } public override void RemoveFromSuperLayer() { - _boundsObserver?.Dispose(); - _boundsObserver = null; + _autosizeToSuperLayerBehavior.Detach(); base.RemoveFromSuperLayer(); } void IAutoSizableCALayer.AutoSizeToSuperLayer() { - _boundsObserver?.Dispose(); - _boundsObserver = CALayerAutosizeObserver.Attach(this); + _autosizeToSuperLayerBehavior.AttachOrThrow(this); } public override void AddAnimation(CAAnimation animation, string? key) diff --git a/src/Core/src/Platform/iOS/MauiCALayerAutosizeToSuperLayerBehaviorExtensions.cs b/src/Core/src/Platform/iOS/MauiCALayerAutosizeToSuperLayerBehaviorExtensions.cs new file mode 100644 index 000000000000..75706ed24b84 --- /dev/null +++ b/src/Core/src/Platform/iOS/MauiCALayerAutosizeToSuperLayerBehaviorExtensions.cs @@ -0,0 +1,17 @@ +using System; +using CoreAnimation; + +namespace Microsoft.Maui.Platform; + +internal static class MauiCALayerAutosizeToSuperLayerBehaviorExtensions +{ + public static void AttachOrThrow(this MauiCALayerAutosizeToSuperLayerBehavior behavior, CALayer layer) + { + var result = behavior.Attach(layer); + + if (result != MauiCALayerAutosizeToSuperLayerResult.Success) + { + throw new InvalidOperationException($"Failed to attach MauiCALayerAutosizeToSuperLayerBehavior: {result}"); + } + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/iOS/StaticCAGradientLayer.cs b/src/Core/src/Platform/iOS/StaticCAGradientLayer.cs index 0d8775e1c236..65e7311e87a0 100644 --- a/src/Core/src/Platform/iOS/StaticCAGradientLayer.cs +++ b/src/Core/src/Platform/iOS/StaticCAGradientLayer.cs @@ -5,27 +5,24 @@ namespace Microsoft.Maui.Platform; class StaticCAGradientLayer : CAGradientLayer, IAutoSizableCALayer { - [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in CALayerAutosizeObserver_DoesNotLeak test.")] - CALayerAutosizeObserver? _boundsObserver; + [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in MauiCALayerAutosizeToSuperLayerBehavior_DoesNotLeak test.")] + readonly MauiCALayerAutosizeToSuperLayerBehavior _autosizeToSuperLayerBehavior = new(); protected override void Dispose(bool disposing) { - _boundsObserver?.Dispose(); - _boundsObserver = null; + _autosizeToSuperLayerBehavior.Detach(); base.Dispose(disposing); } public override void RemoveFromSuperLayer() { - _boundsObserver?.Dispose(); - _boundsObserver = null; + _autosizeToSuperLayerBehavior.Detach(); base.RemoveFromSuperLayer(); } void IAutoSizableCALayer.AutoSizeToSuperLayer() { - _boundsObserver?.Dispose(); - _boundsObserver = CALayerAutosizeObserver.Attach(this); + _autosizeToSuperLayerBehavior.AttachOrThrow(this); } public override void AddAnimation(CAAnimation animation, string? key) diff --git a/src/Core/src/Platform/iOS/StaticCALayer.cs b/src/Core/src/Platform/iOS/StaticCALayer.cs index f5eeeeddc287..f08a59ce877b 100644 --- a/src/Core/src/Platform/iOS/StaticCALayer.cs +++ b/src/Core/src/Platform/iOS/StaticCALayer.cs @@ -5,27 +5,24 @@ namespace Microsoft.Maui.Platform; class StaticCALayer : CALayer, IAutoSizableCALayer { - [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in CALayerAutosizeObserver_DoesNotLeak test.")] - CALayerAutosizeObserver? _boundsObserver; + [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in MauiCALayerAutosizeToSuperLayerBehavior_DoesNotLeak test.")] + readonly MauiCALayerAutosizeToSuperLayerBehavior _autosizeToSuperLayerBehavior = new(); protected override void Dispose(bool disposing) { - _boundsObserver?.Dispose(); - _boundsObserver = null; + _autosizeToSuperLayerBehavior.Detach(); base.Dispose(disposing); } public override void RemoveFromSuperLayer() { - _boundsObserver?.Dispose(); - _boundsObserver = null; + _autosizeToSuperLayerBehavior.Detach(); base.RemoveFromSuperLayer(); } void IAutoSizableCALayer.AutoSizeToSuperLayer() { - _boundsObserver?.Dispose(); - _boundsObserver = CALayerAutosizeObserver.Attach(this); + _autosizeToSuperLayerBehavior.AttachOrThrow(this); } public override void AddAnimation(CAAnimation animation, string? key) diff --git a/src/Core/src/Platform/iOS/StaticCAShapeLayer.cs b/src/Core/src/Platform/iOS/StaticCAShapeLayer.cs index 6d753e641b2f..5c767e8b3e72 100644 --- a/src/Core/src/Platform/iOS/StaticCAShapeLayer.cs +++ b/src/Core/src/Platform/iOS/StaticCAShapeLayer.cs @@ -5,27 +5,24 @@ namespace Microsoft.Maui.Platform; class StaticCAShapeLayer : CAShapeLayer, IAutoSizableCALayer { - [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in CALayerAutosizeObserver_DoesNotLeak test.")] - CALayerAutosizeObserver? _boundsObserver; + [UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in MauiCALayerAutosizeToSuperLayerBehavior_DoesNotLeak test.")] + readonly MauiCALayerAutosizeToSuperLayerBehavior _autosizeToSuperLayerBehavior = new(); protected override void Dispose(bool disposing) { - _boundsObserver?.Dispose(); - _boundsObserver = null; + _autosizeToSuperLayerBehavior.Detach(); base.Dispose(disposing); } public override void RemoveFromSuperLayer() { - _boundsObserver?.Dispose(); - _boundsObserver = null; + _autosizeToSuperLayerBehavior.Detach(); base.RemoveFromSuperLayer(); } void IAutoSizableCALayer.AutoSizeToSuperLayer() { - _boundsObserver?.Dispose(); - _boundsObserver = CALayerAutosizeObserver.Attach(this); + _autosizeToSuperLayerBehavior.AttachOrThrow(this); } public override void AddAnimation(CAAnimation animation, string? key) diff --git a/src/Core/tests/DeviceTests/Memory/CALayerAutosizeObserverTests.cs b/src/Core/tests/DeviceTests/Memory/CALayerAutosizeToSuperLayerBehaviorTests.cs similarity index 77% rename from src/Core/tests/DeviceTests/Memory/CALayerAutosizeObserverTests.cs rename to src/Core/tests/DeviceTests/Memory/CALayerAutosizeToSuperLayerBehaviorTests.cs index c1e7a67b1f7b..66113a7ed6bf 100644 --- a/src/Core/tests/DeviceTests/Memory/CALayerAutosizeObserverTests.cs +++ b/src/Core/tests/DeviceTests/Memory/CALayerAutosizeToSuperLayerBehaviorTests.cs @@ -12,14 +12,14 @@ namespace Microsoft.Maui.DeviceTests.Memory { // Set of tests to verify auto-sizing layers do not leak [Category(TestCategory.Memory)] - public class CALayerAutosizeObserverTests : TestBase + public class MauiCALayerAutosizeToSuperLayerBehaviorTests : TestBase { [Theory] [InlineData(typeof(MauiCALayer))] [InlineData(typeof(StaticCALayer))] [InlineData(typeof(StaticCAGradientLayer))] [InlineData(typeof(StaticCAShapeLayer))] - public async Task CALayerAutosizeObserver_DoesNotLeak([DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type sublayerType) + public async Task MauiCALayerAutosizeToSuperLayerBehavior_DoesNotLeak([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type sublayerType) { WeakReference viewReference = null; WeakReference layerReference = null; @@ -39,6 +39,8 @@ await InvokeOnMainThreadAsync(() => sublayer.AutoSizeToSuperLayer(); view.Frame = new CoreGraphics.CGRect(0, 0, 100, 100); + + view.AttachAndRun(() => view.Frame = new CoreGraphics.CGRect(0, 0, 200, 200)); }); await AssertionExtensions.WaitForGC(viewReference, layerReference, sublayerReference);