├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sample ├── Sample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Sample │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── TestImage.imageset │ │ │ ├── Contents.json │ │ │ └── IMG_3996.jpg │ │ └── TestImage2.imageset │ │ │ ├── Contents.json │ │ │ └── IMG_5635.jpeg │ ├── CGSizeExtensions.swift │ ├── ContentView.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Sample.entitlements │ └── SampleApp.swift ├── SampleTests │ └── SampleTests.swift └── SampleUITests │ ├── SampleUITests.swift │ └── SampleUITestsLaunchTests.swift ├── Sources └── MorphingView │ ├── CGSizeExtensions.swift │ ├── MorphView.swift │ └── MorphingView.swift └── Tests └── MorphingViewTests └── MorphingViewTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Harlan Haskins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "MorphingView", 6 | platforms: [.iOS(.v13)], 7 | products: [ 8 | .library( 9 | name: "MorphingView", 10 | targets: ["MorphingView"]), 11 | ], 12 | targets: [ 13 | .target( 14 | name: "MorphingView"), 15 | .testTarget( 16 | name: "MorphingViewTests", 17 | dependencies: ["MorphingView"] 18 | ), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MorphingView 2 | 3 | UIKit and SwiftUI views for morphing between views, similar to the morph in iOS's 4 | drag-and-drop, context menu, and zoom transition. 5 | 6 | ## Installation 7 | 8 | MorphingView can be added to your project using Swift Package Manager. For more 9 | information on using SwiftPM in Xcode, see [Apple's guide](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) 10 | 11 | If you're using package dependencies directly, you can add this as one of your dependencies: 12 | 13 | ```swift 14 | dependencies: [ 15 | .package(url: "https://github.com/harlanhaskins/MorphingView.git", from: "0.0.1") 16 | ] 17 | ``` 18 | 19 | ## Usage 20 | 21 | See the sample app for a working example. 22 | 23 | https://github.com/user-attachments/assets/4fa4ca59-52ae-4271-b968-efd1bcd7b806 24 | 25 | ### SwiftUI 26 | 27 | Create a MorphView with a list of child views that are each tagged with an appropriate tag. To choose 28 | the currently displayed view, pass the ID corresponding to one of the children to the `id:` parameter. 29 | 30 | ```swift 31 | enum MorphContent { 32 | case image, text, complexView 33 | } 34 | 35 | struct MyMorphingView: View { 36 | @State var selectedViewID: MorphContent = .image 37 | 38 | var body: some View { 39 | MorphView(id: selectedViewID) { 40 | Image(...) 41 | .tag(MorphContent.image) 42 | Text("...") 43 | .tag(MorphContent.text) 44 | ComplexView(...) 45 | .tag(MorphContent.complexView) 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | You can animate these changes either by wrapping the change in a `withAnimation` block or by adding 52 | ``` 53 | .animation(yourAnimation, value: selectedViewID) 54 | ``` 55 | 56 | to the view hierarchy. 57 | 58 | ### UIKit 59 | 60 | First, add the set of views you intend to morph between as subviews of this view. 61 | 62 | ```swift 63 | let morphingView = MorphingView() 64 | morphingView.addSubview(imageView) 65 | morphingView.addSubview(textView) 66 | morphingView.addSubview(shapeView) 67 | ``` 68 | 69 | From here, you can either morph in-order from one view to another using the `morph(to:timingParameters:)` 70 | method, or set the `.index` property directly. 71 | 72 | ```swift 73 | // Morph without animation 74 | morphingView.morph() 75 | 76 | // Morph with an internal interruptible animation with the given timing curve 77 | let springTiming = UISpringTimingParameters(duration: 0.4, bounce: 0.1) 78 | morphingView.morph(timingParameters: springTiming) 79 | 80 | // Morph to a specific index with an implicit UIView animation 81 | UIView.animate(withDuration: 2.0) { 82 | morphingView.morph(to: 2) // equivalent to `morphingView.index = 2` 83 | } 84 | 85 | // Morph to a specific index without animating. 86 | morphingView.index = 2 87 | ``` 88 | 89 | ## Author 90 | 91 | Harlan Haskins ([harlan@harlanhaskins.com](mailto:harlan@harlanhaskins.com)) 92 | -------------------------------------------------------------------------------- /Sample/Sample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DCC29A4B2D2F923500EC6184 /* MorphingView in Frameworks */ = {isa = PBXBuildFile; productRef = DCC29A4A2D2F923500EC6184 /* MorphingView */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXContainerItemProxy section */ 14 | DCC29A2D2D2F921300EC6184 /* PBXContainerItemProxy */ = { 15 | isa = PBXContainerItemProxy; 16 | containerPortal = DCC29A132D2F921200EC6184 /* Project object */; 17 | proxyType = 1; 18 | remoteGlobalIDString = DCC29A1A2D2F921200EC6184; 19 | remoteInfo = Sample; 20 | }; 21 | DCC29A372D2F921300EC6184 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = DCC29A132D2F921200EC6184 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = DCC29A1A2D2F921200EC6184; 26 | remoteInfo = Sample; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | DCC29A1B2D2F921200EC6184 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | DCC29A2C2D2F921300EC6184 /* SampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | DCC29A362D2F921300EC6184 /* SampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | /* End PBXFileReference section */ 35 | 36 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 37 | DCC29A1D2D2F921200EC6184 /* Sample */ = { 38 | isa = PBXFileSystemSynchronizedRootGroup; 39 | path = Sample; 40 | sourceTree = ""; 41 | }; 42 | DCC29A2F2D2F921300EC6184 /* SampleTests */ = { 43 | isa = PBXFileSystemSynchronizedRootGroup; 44 | path = SampleTests; 45 | sourceTree = ""; 46 | }; 47 | DCC29A392D2F921300EC6184 /* SampleUITests */ = { 48 | isa = PBXFileSystemSynchronizedRootGroup; 49 | path = SampleUITests; 50 | sourceTree = ""; 51 | }; 52 | /* End PBXFileSystemSynchronizedRootGroup section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | DCC29A182D2F921200EC6184 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | DCC29A4B2D2F923500EC6184 /* MorphingView in Frameworks */, 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | DCC29A292D2F921300EC6184 /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | DCC29A332D2F921300EC6184 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | DCC29A122D2F921200EC6184 = { 81 | isa = PBXGroup; 82 | children = ( 83 | DCC29A1D2D2F921200EC6184 /* Sample */, 84 | DCC29A2F2D2F921300EC6184 /* SampleTests */, 85 | DCC29A392D2F921300EC6184 /* SampleUITests */, 86 | DCC29A1C2D2F921200EC6184 /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | DCC29A1C2D2F921200EC6184 /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | DCC29A1B2D2F921200EC6184 /* Sample.app */, 94 | DCC29A2C2D2F921300EC6184 /* SampleTests.xctest */, 95 | DCC29A362D2F921300EC6184 /* SampleUITests.xctest */, 96 | ); 97 | name = Products; 98 | sourceTree = ""; 99 | }; 100 | /* End PBXGroup section */ 101 | 102 | /* Begin PBXNativeTarget section */ 103 | DCC29A1A2D2F921200EC6184 /* Sample */ = { 104 | isa = PBXNativeTarget; 105 | buildConfigurationList = DCC29A402D2F921300EC6184 /* Build configuration list for PBXNativeTarget "Sample" */; 106 | buildPhases = ( 107 | DCC29A172D2F921200EC6184 /* Sources */, 108 | DCC29A182D2F921200EC6184 /* Frameworks */, 109 | DCC29A192D2F921200EC6184 /* Resources */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | fileSystemSynchronizedGroups = ( 116 | DCC29A1D2D2F921200EC6184 /* Sample */, 117 | ); 118 | name = Sample; 119 | packageProductDependencies = ( 120 | DCC29A4A2D2F923500EC6184 /* MorphingView */, 121 | ); 122 | productName = Sample; 123 | productReference = DCC29A1B2D2F921200EC6184 /* Sample.app */; 124 | productType = "com.apple.product-type.application"; 125 | }; 126 | DCC29A2B2D2F921300EC6184 /* SampleTests */ = { 127 | isa = PBXNativeTarget; 128 | buildConfigurationList = DCC29A432D2F921300EC6184 /* Build configuration list for PBXNativeTarget "SampleTests" */; 129 | buildPhases = ( 130 | DCC29A282D2F921300EC6184 /* Sources */, 131 | DCC29A292D2F921300EC6184 /* Frameworks */, 132 | DCC29A2A2D2F921300EC6184 /* Resources */, 133 | ); 134 | buildRules = ( 135 | ); 136 | dependencies = ( 137 | DCC29A2E2D2F921300EC6184 /* PBXTargetDependency */, 138 | ); 139 | fileSystemSynchronizedGroups = ( 140 | DCC29A2F2D2F921300EC6184 /* SampleTests */, 141 | ); 142 | name = SampleTests; 143 | packageProductDependencies = ( 144 | ); 145 | productName = SampleTests; 146 | productReference = DCC29A2C2D2F921300EC6184 /* SampleTests.xctest */; 147 | productType = "com.apple.product-type.bundle.unit-test"; 148 | }; 149 | DCC29A352D2F921300EC6184 /* SampleUITests */ = { 150 | isa = PBXNativeTarget; 151 | buildConfigurationList = DCC29A462D2F921300EC6184 /* Build configuration list for PBXNativeTarget "SampleUITests" */; 152 | buildPhases = ( 153 | DCC29A322D2F921300EC6184 /* Sources */, 154 | DCC29A332D2F921300EC6184 /* Frameworks */, 155 | DCC29A342D2F921300EC6184 /* Resources */, 156 | ); 157 | buildRules = ( 158 | ); 159 | dependencies = ( 160 | DCC29A382D2F921300EC6184 /* PBXTargetDependency */, 161 | ); 162 | fileSystemSynchronizedGroups = ( 163 | DCC29A392D2F921300EC6184 /* SampleUITests */, 164 | ); 165 | name = SampleUITests; 166 | packageProductDependencies = ( 167 | ); 168 | productName = SampleUITests; 169 | productReference = DCC29A362D2F921300EC6184 /* SampleUITests.xctest */; 170 | productType = "com.apple.product-type.bundle.ui-testing"; 171 | }; 172 | /* End PBXNativeTarget section */ 173 | 174 | /* Begin PBXProject section */ 175 | DCC29A132D2F921200EC6184 /* Project object */ = { 176 | isa = PBXProject; 177 | attributes = { 178 | BuildIndependentTargetsInParallel = 1; 179 | LastSwiftUpdateCheck = 1620; 180 | LastUpgradeCheck = 1620; 181 | TargetAttributes = { 182 | DCC29A1A2D2F921200EC6184 = { 183 | CreatedOnToolsVersion = 16.2; 184 | }; 185 | DCC29A2B2D2F921300EC6184 = { 186 | CreatedOnToolsVersion = 16.2; 187 | TestTargetID = DCC29A1A2D2F921200EC6184; 188 | }; 189 | DCC29A352D2F921300EC6184 = { 190 | CreatedOnToolsVersion = 16.2; 191 | TestTargetID = DCC29A1A2D2F921200EC6184; 192 | }; 193 | }; 194 | }; 195 | buildConfigurationList = DCC29A162D2F921200EC6184 /* Build configuration list for PBXProject "Sample" */; 196 | developmentRegion = en; 197 | hasScannedForEncodings = 0; 198 | knownRegions = ( 199 | en, 200 | Base, 201 | ); 202 | mainGroup = DCC29A122D2F921200EC6184; 203 | minimizedProjectReferenceProxies = 1; 204 | packageReferences = ( 205 | DCC29A492D2F923500EC6184 /* XCLocalSwiftPackageReference "../../MorphingView" */, 206 | ); 207 | preferredProjectObjectVersion = 77; 208 | productRefGroup = DCC29A1C2D2F921200EC6184 /* Products */; 209 | projectDirPath = ""; 210 | projectRoot = ""; 211 | targets = ( 212 | DCC29A1A2D2F921200EC6184 /* Sample */, 213 | DCC29A2B2D2F921300EC6184 /* SampleTests */, 214 | DCC29A352D2F921300EC6184 /* SampleUITests */, 215 | ); 216 | }; 217 | /* End PBXProject section */ 218 | 219 | /* Begin PBXResourcesBuildPhase section */ 220 | DCC29A192D2F921200EC6184 /* Resources */ = { 221 | isa = PBXResourcesBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | }; 227 | DCC29A2A2D2F921300EC6184 /* Resources */ = { 228 | isa = PBXResourcesBuildPhase; 229 | buildActionMask = 2147483647; 230 | files = ( 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | }; 234 | DCC29A342D2F921300EC6184 /* Resources */ = { 235 | isa = PBXResourcesBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | }; 241 | /* End PBXResourcesBuildPhase section */ 242 | 243 | /* Begin PBXSourcesBuildPhase section */ 244 | DCC29A172D2F921200EC6184 /* Sources */ = { 245 | isa = PBXSourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | DCC29A282D2F921300EC6184 /* Sources */ = { 252 | isa = PBXSourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | DCC29A322D2F921300EC6184 /* Sources */ = { 259 | isa = PBXSourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | /* End PBXSourcesBuildPhase section */ 266 | 267 | /* Begin PBXTargetDependency section */ 268 | DCC29A2E2D2F921300EC6184 /* PBXTargetDependency */ = { 269 | isa = PBXTargetDependency; 270 | target = DCC29A1A2D2F921200EC6184 /* Sample */; 271 | targetProxy = DCC29A2D2D2F921300EC6184 /* PBXContainerItemProxy */; 272 | }; 273 | DCC29A382D2F921300EC6184 /* PBXTargetDependency */ = { 274 | isa = PBXTargetDependency; 275 | target = DCC29A1A2D2F921200EC6184 /* Sample */; 276 | targetProxy = DCC29A372D2F921300EC6184 /* PBXContainerItemProxy */; 277 | }; 278 | /* End PBXTargetDependency section */ 279 | 280 | /* Begin XCBuildConfiguration section */ 281 | DCC29A3E2D2F921300EC6184 /* Debug */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 286 | CLANG_ANALYZER_NONNULL = YES; 287 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_ENABLE_OBJC_WEAK = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 307 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 308 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 309 | CLANG_WARN_STRICT_PROTOTYPES = YES; 310 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 311 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = dwarf; 316 | ENABLE_STRICT_OBJC_MSGSEND = YES; 317 | ENABLE_TESTABILITY = YES; 318 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 319 | GCC_C_LANGUAGE_STANDARD = gnu17; 320 | GCC_DYNAMIC_NO_PIC = NO; 321 | GCC_NO_COMMON_BLOCKS = YES; 322 | GCC_OPTIMIZATION_LEVEL = 0; 323 | GCC_PREPROCESSOR_DEFINITIONS = ( 324 | "DEBUG=1", 325 | "$(inherited)", 326 | ); 327 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 328 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 329 | GCC_WARN_UNDECLARED_SELECTOR = YES; 330 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 331 | GCC_WARN_UNUSED_FUNCTION = YES; 332 | GCC_WARN_UNUSED_VARIABLE = YES; 333 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 334 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 335 | MTL_FAST_MATH = YES; 336 | ONLY_ACTIVE_ARCH = YES; 337 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 338 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 339 | }; 340 | name = Debug; 341 | }; 342 | DCC29A3F2D2F921300EC6184 /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ALWAYS_SEARCH_USER_PATHS = NO; 346 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 347 | CLANG_ANALYZER_NONNULL = YES; 348 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 349 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 350 | CLANG_ENABLE_MODULES = YES; 351 | CLANG_ENABLE_OBJC_ARC = YES; 352 | CLANG_ENABLE_OBJC_WEAK = YES; 353 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 354 | CLANG_WARN_BOOL_CONVERSION = YES; 355 | CLANG_WARN_COMMA = YES; 356 | CLANG_WARN_CONSTANT_CONVERSION = YES; 357 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 358 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 359 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 360 | CLANG_WARN_EMPTY_BODY = YES; 361 | CLANG_WARN_ENUM_CONVERSION = YES; 362 | CLANG_WARN_INFINITE_RECURSION = YES; 363 | CLANG_WARN_INT_CONVERSION = YES; 364 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 366 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 367 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 368 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 369 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 370 | CLANG_WARN_STRICT_PROTOTYPES = YES; 371 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 372 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 373 | CLANG_WARN_UNREACHABLE_CODE = YES; 374 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 375 | COPY_PHASE_STRIP = NO; 376 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 377 | ENABLE_NS_ASSERTIONS = NO; 378 | ENABLE_STRICT_OBJC_MSGSEND = YES; 379 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 380 | GCC_C_LANGUAGE_STANDARD = gnu17; 381 | GCC_NO_COMMON_BLOCKS = YES; 382 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 383 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 384 | GCC_WARN_UNDECLARED_SELECTOR = YES; 385 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 386 | GCC_WARN_UNUSED_FUNCTION = YES; 387 | GCC_WARN_UNUSED_VARIABLE = YES; 388 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 389 | MTL_ENABLE_DEBUG_INFO = NO; 390 | MTL_FAST_MATH = YES; 391 | SWIFT_COMPILATION_MODE = wholemodule; 392 | }; 393 | name = Release; 394 | }; 395 | DCC29A412D2F921300EC6184 /* Debug */ = { 396 | isa = XCBuildConfiguration; 397 | buildSettings = { 398 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 399 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 400 | CODE_SIGN_ENTITLEMENTS = Sample/Sample.entitlements; 401 | CODE_SIGN_STYLE = Automatic; 402 | CURRENT_PROJECT_VERSION = 1; 403 | DEVELOPMENT_ASSET_PATHS = "\"Sample/Preview Content\""; 404 | DEVELOPMENT_TEAM = W7GJMYD5A4; 405 | ENABLE_HARDENED_RUNTIME = YES; 406 | ENABLE_PREVIEWS = YES; 407 | GENERATE_INFOPLIST_FILE = YES; 408 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 409 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 410 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 411 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 412 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 413 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 414 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 415 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 416 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 417 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 418 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 419 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 420 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 421 | MACOSX_DEPLOYMENT_TARGET = 15.1; 422 | MARKETING_VERSION = 1.0; 423 | PRODUCT_BUNDLE_IDENTIFIER = com.harlanhaskins.Sample; 424 | PRODUCT_NAME = "$(TARGET_NAME)"; 425 | SDKROOT = auto; 426 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; 427 | SUPPORTS_MACCATALYST = NO; 428 | SWIFT_EMIT_LOC_STRINGS = YES; 429 | SWIFT_VERSION = 5.0; 430 | TARGETED_DEVICE_FAMILY = "1,2,7"; 431 | XROS_DEPLOYMENT_TARGET = 2.2; 432 | }; 433 | name = Debug; 434 | }; 435 | DCC29A422D2F921300EC6184 /* Release */ = { 436 | isa = XCBuildConfiguration; 437 | buildSettings = { 438 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 439 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 440 | CODE_SIGN_ENTITLEMENTS = Sample/Sample.entitlements; 441 | CODE_SIGN_STYLE = Automatic; 442 | CURRENT_PROJECT_VERSION = 1; 443 | DEVELOPMENT_ASSET_PATHS = "\"Sample/Preview Content\""; 444 | DEVELOPMENT_TEAM = W7GJMYD5A4; 445 | ENABLE_HARDENED_RUNTIME = YES; 446 | ENABLE_PREVIEWS = YES; 447 | GENERATE_INFOPLIST_FILE = YES; 448 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 449 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 450 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 451 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 452 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 453 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 454 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 455 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 456 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 457 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 458 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 459 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 460 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 461 | MACOSX_DEPLOYMENT_TARGET = 15.1; 462 | MARKETING_VERSION = 1.0; 463 | PRODUCT_BUNDLE_IDENTIFIER = com.harlanhaskins.Sample; 464 | PRODUCT_NAME = "$(TARGET_NAME)"; 465 | SDKROOT = auto; 466 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; 467 | SUPPORTS_MACCATALYST = NO; 468 | SWIFT_EMIT_LOC_STRINGS = YES; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2,7"; 471 | XROS_DEPLOYMENT_TARGET = 2.2; 472 | }; 473 | name = Release; 474 | }; 475 | DCC29A442D2F921300EC6184 /* Debug */ = { 476 | isa = XCBuildConfiguration; 477 | buildSettings = { 478 | BUNDLE_LOADER = "$(TEST_HOST)"; 479 | CODE_SIGN_STYLE = Automatic; 480 | CURRENT_PROJECT_VERSION = 1; 481 | DEVELOPMENT_TEAM = W7GJMYD5A4; 482 | GENERATE_INFOPLIST_FILE = YES; 483 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 484 | MACOSX_DEPLOYMENT_TARGET = 15.1; 485 | MARKETING_VERSION = 1.0; 486 | PRODUCT_BUNDLE_IDENTIFIER = com.harlanhaskins.SampleTests; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | SDKROOT = auto; 489 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 490 | SWIFT_EMIT_LOC_STRINGS = NO; 491 | SWIFT_VERSION = 5.0; 492 | TARGETED_DEVICE_FAMILY = "1,2,7"; 493 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Sample"; 494 | XROS_DEPLOYMENT_TARGET = 2.2; 495 | }; 496 | name = Debug; 497 | }; 498 | DCC29A452D2F921300EC6184 /* Release */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | BUNDLE_LOADER = "$(TEST_HOST)"; 502 | CODE_SIGN_STYLE = Automatic; 503 | CURRENT_PROJECT_VERSION = 1; 504 | DEVELOPMENT_TEAM = W7GJMYD5A4; 505 | GENERATE_INFOPLIST_FILE = YES; 506 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 507 | MACOSX_DEPLOYMENT_TARGET = 15.1; 508 | MARKETING_VERSION = 1.0; 509 | PRODUCT_BUNDLE_IDENTIFIER = com.harlanhaskins.SampleTests; 510 | PRODUCT_NAME = "$(TARGET_NAME)"; 511 | SDKROOT = auto; 512 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 513 | SWIFT_EMIT_LOC_STRINGS = NO; 514 | SWIFT_VERSION = 5.0; 515 | TARGETED_DEVICE_FAMILY = "1,2,7"; 516 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Sample"; 517 | XROS_DEPLOYMENT_TARGET = 2.2; 518 | }; 519 | name = Release; 520 | }; 521 | DCC29A472D2F921300EC6184 /* Debug */ = { 522 | isa = XCBuildConfiguration; 523 | buildSettings = { 524 | CODE_SIGN_STYLE = Automatic; 525 | CURRENT_PROJECT_VERSION = 1; 526 | DEVELOPMENT_TEAM = W7GJMYD5A4; 527 | GENERATE_INFOPLIST_FILE = YES; 528 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 529 | MACOSX_DEPLOYMENT_TARGET = 15.1; 530 | MARKETING_VERSION = 1.0; 531 | PRODUCT_BUNDLE_IDENTIFIER = com.harlanhaskins.SampleUITests; 532 | PRODUCT_NAME = "$(TARGET_NAME)"; 533 | SDKROOT = auto; 534 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 535 | SWIFT_EMIT_LOC_STRINGS = NO; 536 | SWIFT_VERSION = 5.0; 537 | TARGETED_DEVICE_FAMILY = "1,2,7"; 538 | TEST_TARGET_NAME = Sample; 539 | XROS_DEPLOYMENT_TARGET = 2.2; 540 | }; 541 | name = Debug; 542 | }; 543 | DCC29A482D2F921300EC6184 /* Release */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | CODE_SIGN_STYLE = Automatic; 547 | CURRENT_PROJECT_VERSION = 1; 548 | DEVELOPMENT_TEAM = W7GJMYD5A4; 549 | GENERATE_INFOPLIST_FILE = YES; 550 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 551 | MACOSX_DEPLOYMENT_TARGET = 15.1; 552 | MARKETING_VERSION = 1.0; 553 | PRODUCT_BUNDLE_IDENTIFIER = com.harlanhaskins.SampleUITests; 554 | PRODUCT_NAME = "$(TARGET_NAME)"; 555 | SDKROOT = auto; 556 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 557 | SWIFT_EMIT_LOC_STRINGS = NO; 558 | SWIFT_VERSION = 5.0; 559 | TARGETED_DEVICE_FAMILY = "1,2,7"; 560 | TEST_TARGET_NAME = Sample; 561 | XROS_DEPLOYMENT_TARGET = 2.2; 562 | }; 563 | name = Release; 564 | }; 565 | /* End XCBuildConfiguration section */ 566 | 567 | /* Begin XCConfigurationList section */ 568 | DCC29A162D2F921200EC6184 /* Build configuration list for PBXProject "Sample" */ = { 569 | isa = XCConfigurationList; 570 | buildConfigurations = ( 571 | DCC29A3E2D2F921300EC6184 /* Debug */, 572 | DCC29A3F2D2F921300EC6184 /* Release */, 573 | ); 574 | defaultConfigurationIsVisible = 0; 575 | defaultConfigurationName = Release; 576 | }; 577 | DCC29A402D2F921300EC6184 /* Build configuration list for PBXNativeTarget "Sample" */ = { 578 | isa = XCConfigurationList; 579 | buildConfigurations = ( 580 | DCC29A412D2F921300EC6184 /* Debug */, 581 | DCC29A422D2F921300EC6184 /* Release */, 582 | ); 583 | defaultConfigurationIsVisible = 0; 584 | defaultConfigurationName = Release; 585 | }; 586 | DCC29A432D2F921300EC6184 /* Build configuration list for PBXNativeTarget "SampleTests" */ = { 587 | isa = XCConfigurationList; 588 | buildConfigurations = ( 589 | DCC29A442D2F921300EC6184 /* Debug */, 590 | DCC29A452D2F921300EC6184 /* Release */, 591 | ); 592 | defaultConfigurationIsVisible = 0; 593 | defaultConfigurationName = Release; 594 | }; 595 | DCC29A462D2F921300EC6184 /* Build configuration list for PBXNativeTarget "SampleUITests" */ = { 596 | isa = XCConfigurationList; 597 | buildConfigurations = ( 598 | DCC29A472D2F921300EC6184 /* Debug */, 599 | DCC29A482D2F921300EC6184 /* Release */, 600 | ); 601 | defaultConfigurationIsVisible = 0; 602 | defaultConfigurationName = Release; 603 | }; 604 | /* End XCConfigurationList section */ 605 | 606 | /* Begin XCLocalSwiftPackageReference section */ 607 | DCC29A492D2F923500EC6184 /* XCLocalSwiftPackageReference "../../MorphingView" */ = { 608 | isa = XCLocalSwiftPackageReference; 609 | relativePath = ../../MorphingView; 610 | }; 611 | /* End XCLocalSwiftPackageReference section */ 612 | 613 | /* Begin XCSwiftPackageProductDependency section */ 614 | DCC29A4A2D2F923500EC6184 /* MorphingView */ = { 615 | isa = XCSwiftPackageProductDependency; 616 | productName = MorphingView; 617 | }; 618 | /* End XCSwiftPackageProductDependency section */ 619 | }; 620 | rootObject = DCC29A132D2F921200EC6184 /* Project object */; 621 | } 622 | -------------------------------------------------------------------------------- /Sample/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sample/Sample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sample/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "scale" : "1x", 33 | "size" : "16x16" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "scale" : "2x", 38 | "size" : "16x16" 39 | }, 40 | { 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "32x32" 44 | }, 45 | { 46 | "idiom" : "mac", 47 | "scale" : "2x", 48 | "size" : "32x32" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "128x128" 54 | }, 55 | { 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "128x128" 59 | }, 60 | { 61 | "idiom" : "mac", 62 | "scale" : "1x", 63 | "size" : "256x256" 64 | }, 65 | { 66 | "idiom" : "mac", 67 | "scale" : "2x", 68 | "size" : "256x256" 69 | }, 70 | { 71 | "idiom" : "mac", 72 | "scale" : "1x", 73 | "size" : "512x512" 74 | }, 75 | { 76 | "idiom" : "mac", 77 | "scale" : "2x", 78 | "size" : "512x512" 79 | } 80 | ], 81 | "info" : { 82 | "author" : "xcode", 83 | "version" : 1 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sample/Sample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sample/Sample/Assets.xcassets/TestImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_3996.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sample/Sample/Assets.xcassets/TestImage.imageset/IMG_3996.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harlanhaskins/MorphingView/109f94e41488bc997bbff981bc61630b1288d3fe/Sample/Sample/Assets.xcassets/TestImage.imageset/IMG_3996.jpg -------------------------------------------------------------------------------- /Sample/Sample/Assets.xcassets/TestImage2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_5635.jpeg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sample/Sample/Assets.xcassets/TestImage2.imageset/IMG_5635.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harlanhaskins/MorphingView/109f94e41488bc997bbff981bc61630b1288d3fe/Sample/Sample/Assets.xcassets/TestImage2.imageset/IMG_5635.jpeg -------------------------------------------------------------------------------- /Sample/Sample/CGSizeExtensions.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGSize { 4 | /// Determines the appropriate scale factor to apply to the receiver such that the receiver 5 | /// fits itself into the target size without extending outside. 6 | func scaleFactorToFit(_ target: CGSize) -> CGFloat { 7 | min(target.width / width, target.height / height) 8 | } 9 | 10 | /// Determines the appropriate scale factor to apply to the receiver such that the receiver 11 | /// fills the target size, possibly extending outside the target on its larger dimension. 12 | func scaleFactorToFill(_ target: CGSize) -> CGFloat { 13 | max(target.width / width, target.height / height) 14 | } 15 | 16 | /// Determines the scaled size of the receiver such that it fits itself into the target 17 | /// size without extending outside. 18 | func scaledToFit(_ target: CGSize) -> CGSize { 19 | let scale = scaleFactorToFit(target) 20 | return CGSize(width: width * scale, height: height * scale) 21 | } 22 | 23 | /// Determines the scaled size of the receiver such that it fills the target size, 24 | /// possibly extending outside the target on its larger dimension. 25 | func scaledToFill(_ target: CGSize) -> CGSize { 26 | let scale = scaleFactorToFill(target) 27 | return CGSize(width: width * scale, height: height * scale) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sample/Sample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // MorphingView 4 | // 5 | // Created by Harlan Haskins on 1/8/25. 6 | // 7 | 8 | import MorphingView 9 | import SwiftUI 10 | import UIKit 11 | 12 | struct UIViewAdaptor: UIViewRepresentable { 13 | var view: UIView 14 | 15 | func makeUIView(context: Context) -> some UIView { 16 | view 17 | } 18 | func updateUIView(_ uiView: UIViewType, context: Context) { 19 | } 20 | } 21 | 22 | func makeTestView() -> UIView { 23 | let width = CGFloat.random(in: 100..<200) 24 | let height = CGFloat.random(in: 100..<200) 25 | let v = UIView(frame: CGRect(x: 0, y: 0, width: width, height: height)) 26 | v.backgroundColor = UIColor(hue: .random(in: 0..<1), saturation: 0.7, brightness: 0.9, alpha: 1) 27 | return v 28 | } 29 | 30 | func makeTestImage(two: Bool = false) -> UIImageView { 31 | let image = UIImage(named: "TestImage\(two ? "2" : "")")! 32 | let imageView = UIImageView(image: image) 33 | imageView.contentMode = .scaleAspectFit 34 | imageView.bounds.size = image.size.scaledToFit(CGSize(width: 300, height: 300)) 35 | return imageView 36 | } 37 | 38 | struct ContentView: View { 39 | @State var view = MorphingView(views: [ makeTestImage(), makeTestImage(two: true), makeTestView(), makeTestView(), makeTestView()]) 40 | @State var morphID: Int = 0 41 | var body: some View { 42 | VStack(spacing: 20) { 43 | VStack { 44 | UIViewAdaptor(view: view) 45 | .frame(width: 300, height: 300) 46 | MorphView(id: morphID) { 47 | Image("TestImage") 48 | .resizable() 49 | .scaledToFit() 50 | .tag(0) 51 | Image("TestImage2") 52 | .resizable() 53 | .scaledToFit() 54 | .tag(1) 55 | Text("This is a long message, I am testing morphing to text") 56 | .frame(width: 100) 57 | .tag(2) 58 | } 59 | .frame(width: 300, height: 300) 60 | } 61 | 62 | HStack { 63 | Button("Previous", systemImage: "chevron.backward") { 64 | changeImage(offset: -1) 65 | } 66 | Button("Next", systemImage: "chevron.forward") { 67 | changeImage(offset: 1) 68 | } 69 | } 70 | .buttonBorderShape(.circle) 71 | .buttonStyle(.borderedProminent) 72 | .labelStyle(.iconOnly) 73 | } 74 | .padding() 75 | } 76 | 77 | func changeImage(offset: Int) { 78 | let newIndex = view.index + offset 79 | view.morph(to: newIndex, timingParameters: UISpringTimingParameters(duration: 0.4, bounce: 0.1)) 80 | withAnimation(.spring(duration: 0.4, bounce: 0.1)) { 81 | morphID += offset 82 | if morphID > 2 { 83 | morphID = 0 84 | } 85 | if morphID < 0 { 86 | morphID = 2 87 | } 88 | } 89 | } 90 | } 91 | 92 | #Preview { 93 | ContentView() 94 | } 95 | -------------------------------------------------------------------------------- /Sample/Sample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sample/Sample/Sample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sample/Sample/SampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleApp.swift 3 | // Sample 4 | // 5 | // Created by Harlan Haskins on 1/9/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sample/SampleTests/SampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleTests.swift 3 | // SampleTests 4 | // 5 | // Created by Harlan Haskins on 1/9/25. 6 | // 7 | 8 | import Testing 9 | 10 | struct SampleTests { 11 | 12 | @Test func example() async throws { 13 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sample/SampleUITests/SampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleUITests.swift 3 | // SampleUITests 4 | // 5 | // Created by Harlan Haskins on 1/9/25. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SampleUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 37 | // This measures how long it takes to launch your application. 38 | measure(metrics: [XCTApplicationLaunchMetric()]) { 39 | XCUIApplication().launch() 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sample/SampleUITests/SampleUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleUITestsLaunchTests.swift 3 | // SampleUITests 4 | // 5 | // Created by Harlan Haskins on 1/9/25. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SampleUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | @MainActor 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/MorphingView/CGSizeExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSizeExtensions.swift 3 | // MorphingView 4 | // 5 | // Created by Harlan Haskins on 1/8/25. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension CGSize { 11 | /// Determines the counter-scale necessary to apply to the receiver such that it becomes 12 | /// the target size. 13 | func relativeScale( 14 | to target: CGSize 15 | ) -> CGSize { 16 | CGSize( 17 | width: target.width / width, 18 | height: target.height / height) 19 | } 20 | 21 | /// Creates a CGAffineTransform that counter-scales the recieving layer to make it visually 22 | /// the same as the target size. 23 | func scaleTransform(to target: CGSize) -> CGAffineTransform { 24 | let scale = relativeScale(to: target) 25 | return CGAffineTransform(scaleX: scale.width, y: scale.height) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/MorphingView/MorphView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MorphView.swift 3 | // MorphingView 4 | // 5 | // Created by Harlan Haskins on 1/8/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A SwiftUI container view that morphs its contents between one of a set of child Views. 11 | /// 12 | /// To use it, add a list of child views that are each tagged with an appropriate tag. To choose 13 | /// the currently displayed view, pass the ID corresponding to one of the children to the `id:` parameter. 14 | /// 15 | /// ``` 16 | /// enum MorphContent { 17 | /// case image, text, complexView 18 | /// } 19 | /// 20 | /// MorphView(id: selectedViewID) { 21 | /// Image(...) 22 | /// .tag(MorphContent.image) 23 | /// Text("...") 24 | /// .tag(MorphContent.text) 25 | /// ComplexView(...) 26 | /// .tag(MorphContent.complexView) 27 | /// } 28 | /// ``` 29 | /// 30 | /// You can animate these changes either by wrapping the change in a `withAnimation` block or by adding 31 | /// ``` 32 | /// .animation(yourAnimation, value: selectedViewID) 33 | /// ``` 34 | /// to the view hierarchy. 35 | @available(iOS 18.0, macCatalyst 18.0, *) 36 | public struct MorphView: View { 37 | var id: ID 38 | var content: Content 39 | @State var childSizes = [ID: CGSize]() 40 | 41 | public init( 42 | id: ID, 43 | @ViewBuilder content: () -> Content 44 | ) { 45 | self.id = id 46 | self.content = content() 47 | } 48 | 49 | public var body: some View { 50 | ZStack { 51 | let selectedSize = childSizes[id] ?? .zero 52 | ForEach(subviews: content) { subview in 53 | let tag = subview.containerValues.tag(for: ID.self) 54 | .unwrap("MorphView child views must have tags of type \(_typeName(ID.self, qualified: false))") 55 | let size = childSizes[tag, default: .zero] 56 | subview 57 | .zIndex(tag == id ? 1 : 0) 58 | .opacity(tag == id ? 1 : 0) 59 | .onGeometryChange(for: CGSize.self) { proxy in 60 | proxy.size 61 | } action: { size in 62 | childSizes[tag] = size 63 | } 64 | .scaleEffect( 65 | size.relativeScale(to: selectedSize) 66 | ) 67 | } 68 | } 69 | } 70 | } 71 | 72 | extension Optional { 73 | @_transparent 74 | func unwrap(_ message: String) -> Wrapped { 75 | guard let value = self else { 76 | fatalError(message) 77 | } 78 | return value 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/MorphingView/MorphingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MorphingView.swift 3 | // MorphingView 4 | // 5 | // Created by Harlan Haskins on 1/8/25. 6 | // 7 | 8 | import UIKit 9 | 10 | /// A container UIView that "morphs" its content between its subviews. 11 | /// 12 | /// To use it, first add the set of views you intend to morph between as subviews of this view. 13 | /// 14 | /// ``` 15 | /// let morphingView = MorphingView() 16 | /// morphingView.addSubview(imageView) 17 | /// morphingView.addSubview(textView) 18 | /// morphingView.addSubview(shapeView) 19 | /// ``` 20 | /// 21 | /// From here, you can either morph in-order from one view to another using the `morph(to:timingParameters:)` 22 | /// method: 23 | /// 24 | /// ``` 25 | /// // Morph without animation 26 | /// morphingView.morph() 27 | /// 28 | /// // Morph with an internal interruptible animation with the given timing curve 29 | /// let springTiming = UISpringTimingParameters(duration: 0.4, bounce: 0.1) 30 | /// morphingView.morph(timingParameters: springTiming) 31 | /// 32 | /// // Morph to a specific index with an implicit UIView animation 33 | /// UIView.animate(withDuration: 2.0) { 34 | /// morphingView.morph(to: 2) 35 | /// } 36 | /// 37 | /// // Morph to a specific index without animating. 38 | /// morphingView.index = 2 39 | /// ``` 40 | public final class MorphingView: UIView { 41 | private var viewIndex = 0 { 42 | didSet { 43 | clampIndex() 44 | } 45 | } 46 | 47 | func clampIndex() { 48 | if viewIndex < 0 { 49 | viewIndex = subviews.count - 1 50 | } 51 | 52 | if viewIndex >= subviews.count { 53 | viewIndex = 0 54 | } 55 | } 56 | 57 | /// Sets the currently visible view, morphing other views out. 58 | /// Non-animated; you are responsible for animating the morph index if this is used. 59 | public var index: Int { 60 | get { 61 | viewIndex 62 | } 63 | set { 64 | viewIndex = newValue 65 | update() 66 | } 67 | } 68 | private var animator: UIViewPropertyAnimator? 69 | private var hasInitialized = false 70 | 71 | /// Creates a `MorphingView` with an initial set of subviews. 72 | public init(views: [UIView] = []) { 73 | super.init(frame: .zero) 74 | 75 | for view in views { 76 | addSubview(view) 77 | } 78 | update() 79 | hasInitialized = true 80 | } 81 | 82 | /// Morphs the contents of this view to the view at the specified index, or to the next index 83 | /// if no index is provided. 84 | /// - Parameters: 85 | /// - newIndex: The index to morph to. If this index is out of bounds, the nearest index will be chosen. 86 | /// - timingParameters: Optional animation timing parameters to animate this change with. 87 | /// This uses an interruptible property animator to morph between views. If not provided, the change 88 | /// is not animated; you will be responsible for animating these changes. 89 | public func morph( 90 | to newIndex: Int? = nil, 91 | timingParameters: (any UITimingCurveProvider)? = nil 92 | ) { 93 | viewIndex = newIndex ?? viewIndex + 1 94 | animator?.stopAnimation(/* withoutFinishing: */true) 95 | 96 | if let timingParameters { 97 | let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters) 98 | animator.addAnimations { 99 | self.update() 100 | } 101 | animator.addCompletion { [weak self] _ in 102 | self?.animator = nil 103 | } 104 | animator.startAnimation() 105 | self.animator = animator 106 | } else { 107 | self.update() 108 | } 109 | } 110 | 111 | public override func didAddSubview(_ subview: UIView) { 112 | super.didAddSubview(subview) 113 | if hasInitialized { 114 | update() 115 | } 116 | } 117 | 118 | public override func willRemoveSubview(_ subview: UIView) { 119 | super.willRemoveSubview(subview) 120 | if hasInitialized { 121 | clampIndex() 122 | } 123 | } 124 | 125 | private func update() { 126 | if subviews.isEmpty { return } 127 | 128 | let current = subviews[index] 129 | for (idx, view) in subviews.enumerated() { 130 | if idx == index { 131 | view.alpha = 1 132 | view.transform = .identity 133 | } else { 134 | view.alpha = 0 135 | view.transform = view.bounds.size.scaleTransform(to: current.bounds.size) 136 | } 137 | } 138 | } 139 | 140 | public override func sizeToFit() { 141 | var size = CGSize.zero 142 | for view in subviews { 143 | let childSize = view.bounds.size 144 | size.width = max(size.width, childSize.width) 145 | size.height = max(size.height, childSize.height) 146 | } 147 | bounds.size = size 148 | } 149 | 150 | public override func layoutSubviews() { 151 | super.layoutSubviews() 152 | 153 | let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY) 154 | for view in subviews { 155 | view.center = boundsCenter 156 | } 157 | } 158 | 159 | @available(*, unavailable) 160 | required init?(coder: NSCoder) { 161 | fatalError() 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Tests/MorphingViewTests/MorphingViewTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import MorphingView 3 | 4 | @Test func example() async throws { 5 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 6 | } 7 | --------------------------------------------------------------------------------