├── .github └── workflows │ └── license-update.yml ├── .gitignore ├── .swiftlint.yml ├── DynamicIslandDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── DynamicIslandDemo ├── Application │ └── Application.swift ├── Constants │ └── Const.swift ├── Extensions │ └── CGFloat+Extenions.swift ├── Managers │ └── DynamicIslandManager.swift ├── Models │ └── User.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ └── icon.jpeg │ │ ├── Contents.json │ │ └── avatar-image.imageset │ │ │ ├── Contents.json │ │ │ └── avatar-image.jpeg │ ├── Design Reference │ │ ├── IMG_7450.PNG │ │ └── IMG_7451.PNG │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Scenes │ └── Profile │ │ ├── ProfileView.swift │ │ └── ProfileViewModel.swift └── Views │ ├── Cells │ └── ToggleCellView.swift │ ├── Modifiers │ ├── OpacityTransitionModifier.swift │ └── ScrollStatusModifier.swift │ └── Views │ ├── OffsetObservingScrollView.swift │ └── PositionObservingView.swift ├── LICENSE ├── README.md ├── demo.gif ├── icon.png └── scripts └── license-update.sh /.github/workflows/license-update.yml: -------------------------------------------------------------------------------- 1 | name: license-update 2 | 3 | on: 4 | schedule: 5 | - cron: '0 12 * * 0' # Every Sunday at 12 PM 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-license: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Git user 16 | run: | 17 | git config --global user.email "${{ secrets.EMAIL }}" 18 | git config --global user.name "${{ secrets.USER_NAME }}" 19 | 20 | - name: Update LICENSE and README.md 21 | run: | 22 | chmod +x ./scripts/license-update.sh 23 | ./scripts/license-update.sh 24 | 25 | - name: Commit and push changes 26 | run: | 27 | git add LICENSE README.md 28 | git commit -m "Updated License and ReadMe files" 29 | git push origin $(git rev-parse --abbrev-ref HEAD) 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/xcuserdata 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - DynamicIslandDemo 3 | 4 | analyzer_rules: 5 | - unused_declaration 6 | - unused_import 7 | 8 | opt_in_rules: 9 | - array_init 10 | - closure_end_indentation 11 | - closure_spacing 12 | - collection_alignment 13 | - contains_over_filter_count 14 | - contains_over_filter_is_empty 15 | - contains_over_first_not_nil 16 | - contains_over_range_nil_comparison 17 | - empty_collection_literal 18 | - empty_count 19 | - empty_string 20 | - empty_xctest_method 21 | - enum_case_associated_values_count 22 | - explicit_init 23 | - extension_access_modifier 24 | - fallthrough 25 | - fatal_error_message 26 | - flatmap_over_map_reduce 27 | - identical_operands 28 | - joined_default_parameter 29 | - legacy_random 30 | - let_var_whitespace 31 | - last_where 32 | - literal_expression_end_indentation 33 | - lower_acl_than_parent 34 | - modifier_order 35 | - nimble_operator 36 | - number_separator 37 | - operator_usage_whitespace 38 | - overridden_super_call 39 | - override_in_extension 40 | - private_action 41 | - private_outlet 42 | - prohibited_super_call 43 | - quick_discouraged_call 44 | - quick_discouraged_focused_test 45 | - quick_discouraged_pending_test 46 | - redundant_nil_coalescing 47 | - single_test_class 48 | - sorted_first_last 49 | - sorted_imports 50 | - static_operator 51 | - strong_iboutlet 52 | - toggle_bool 53 | - unneeded_parentheses_in_closure_argument 54 | - unowned_variable_capture 55 | - vertical_parameter_alignment_on_call 56 | - vertical_whitespace_closing_braces 57 | - xct_specific_matcher 58 | - yoda_condition 59 | 60 | custom_rules: 61 | 62 | custom_zero: 63 | excluded: ".*.Const.swift" 64 | name: "Zero" 65 | regex: '( 0\.0| 0)(\n| |, |\))' 66 | message: "Zero values must be replaced with .zero" 67 | 68 | disabled_rules: 69 | - identifier_name 70 | - force_cast 71 | 72 | type_body_length: 73 | warning: 400 74 | error: 500 75 | 76 | file_length: 77 | warning: 500 78 | error: 600 79 | 80 | line_length: 81 | warning: 130 82 | error: 140 83 | 84 | function_parameter_count: 85 | warning: 6 86 | error: 8 87 | 88 | function_body_length: 89 | warning: 50 90 | error: 80 91 | -------------------------------------------------------------------------------- /DynamicIslandDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C46492582AB83ED80083CA3D /* IMG_7451.PNG in Resources */ = {isa = PBXBuildFile; fileRef = C46492562AB83ED80083CA3D /* IMG_7451.PNG */; }; 11 | C46492592AB83ED80083CA3D /* IMG_7450.PNG in Resources */ = {isa = PBXBuildFile; fileRef = C46492572AB83ED80083CA3D /* IMG_7450.PNG */; }; 12 | C464925B2AB86A1B0083CA3D /* OpacityTransitionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464925A2AB86A1B0083CA3D /* OpacityTransitionModifier.swift */; }; 13 | C47B7CED2AFCE71B00FA4781 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47B7CEC2AFCE71B00FA4781 /* ProfileView.swift */; }; 14 | C47B7CEF2AFCE72700FA4781 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47B7CEE2AFCE72700FA4781 /* ProfileViewModel.swift */; }; 15 | C4A00A3B2AA60FF600D5244A /* Const.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A252AA60FF600D5244A /* Const.swift */; }; 16 | C4A00A3C2AA60FF600D5244A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C4A00A272AA60FF600D5244A /* Assets.xcassets */; }; 17 | C4A00A3D2AA60FF600D5244A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C4A00A282AA60FF600D5244A /* Preview Assets.xcassets */; }; 18 | C4A00A3E2AA60FF600D5244A /* DynamicIslandManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A2A2AA60FF600D5244A /* DynamicIslandManager.swift */; }; 19 | C4A00A3F2AA60FF600D5244A /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A2C2AA60FF600D5244A /* User.swift */; }; 20 | C4A00A402AA60FF600D5244A /* CGFloat+Extenions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A2E2AA60FF600D5244A /* CGFloat+Extenions.swift */; }; 21 | C4A00A422AA60FF600D5244A /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A312AA60FF600D5244A /* Application.swift */; }; 22 | C4A00A432AA60FF600D5244A /* ToggleCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A342AA60FF600D5244A /* ToggleCellView.swift */; }; 23 | C4A00A442AA60FF600D5244A /* ScrollStatusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A362AA60FF600D5244A /* ScrollStatusModifier.swift */; }; 24 | C4A00A452AA60FF600D5244A /* OffsetObservingScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A382AA60FF600D5244A /* OffsetObservingScrollView.swift */; }; 25 | C4A00A462AA60FF600D5244A /* PositionObservingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A00A392AA60FF600D5244A /* PositionObservingView.swift */; }; 26 | C4E312AC2A84F93E00A28192 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = C4E312AB2A84F93E00A28192 /* SwiftUIIntrospect */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | C41E45502A83A826009470CB /* DynamicIslandDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamicIslandDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | C46492562AB83ED80083CA3D /* IMG_7451.PNG */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = IMG_7451.PNG; sourceTree = ""; }; 32 | C46492572AB83ED80083CA3D /* IMG_7450.PNG */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = IMG_7450.PNG; sourceTree = ""; }; 33 | C464925A2AB86A1B0083CA3D /* OpacityTransitionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpacityTransitionModifier.swift; sourceTree = ""; }; 34 | C47B7CEC2AFCE71B00FA4781 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 35 | C47B7CEE2AFCE72700FA4781 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; 36 | C4A00A252AA60FF600D5244A /* Const.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Const.swift; sourceTree = ""; }; 37 | C4A00A272AA60FF600D5244A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | C4A00A282AA60FF600D5244A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 39 | C4A00A2A2AA60FF600D5244A /* DynamicIslandManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicIslandManager.swift; sourceTree = ""; }; 40 | C4A00A2C2AA60FF600D5244A /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 41 | C4A00A2E2AA60FF600D5244A /* CGFloat+Extenions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat+Extenions.swift"; sourceTree = ""; }; 42 | C4A00A312AA60FF600D5244A /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 43 | C4A00A342AA60FF600D5244A /* ToggleCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleCellView.swift; sourceTree = ""; }; 44 | C4A00A362AA60FF600D5244A /* ScrollStatusModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollStatusModifier.swift; sourceTree = ""; }; 45 | C4A00A382AA60FF600D5244A /* OffsetObservingScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OffsetObservingScrollView.swift; sourceTree = ""; }; 46 | C4A00A392AA60FF600D5244A /* PositionObservingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PositionObservingView.swift; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | C41E454D2A83A826009470CB /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | C4E312AC2A84F93E00A28192 /* SwiftUIIntrospect in Frameworks */, 55 | ); 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXFrameworksBuildPhase section */ 59 | 60 | /* Begin PBXGroup section */ 61 | C41E45472A83A826009470CB = { 62 | isa = PBXGroup; 63 | children = ( 64 | C4A00A212AA60FF600D5244A /* DynamicIslandDemo */, 65 | C41E45512A83A826009470CB /* Products */, 66 | ); 67 | sourceTree = ""; 68 | }; 69 | C41E45512A83A826009470CB /* Products */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | C41E45502A83A826009470CB /* DynamicIslandDemo.app */, 73 | ); 74 | name = Products; 75 | sourceTree = ""; 76 | }; 77 | C46492552AB83EC60083CA3D /* Design Reference */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | C46492572AB83ED80083CA3D /* IMG_7450.PNG */, 81 | C46492562AB83ED80083CA3D /* IMG_7451.PNG */, 82 | ); 83 | path = "Design Reference"; 84 | sourceTree = ""; 85 | }; 86 | C47B7CEB2AFCE70500FA4781 /* Profile */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | C47B7CEC2AFCE71B00FA4781 /* ProfileView.swift */, 90 | C47B7CEE2AFCE72700FA4781 /* ProfileViewModel.swift */, 91 | ); 92 | path = Profile; 93 | sourceTree = ""; 94 | }; 95 | C4A00A212AA60FF600D5244A /* DynamicIslandDemo */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | C4A00A242AA60FF600D5244A /* Constants */, 99 | C4A00A292AA60FF600D5244A /* Managers */, 100 | C4A00A2B2AA60FF600D5244A /* Models */, 101 | C4A00A322AA60FF600D5244A /* Views */, 102 | C4A00A222AA60FF600D5244A /* Scenes */, 103 | C4A00A2D2AA60FF600D5244A /* Extensions */, 104 | C4A00A262AA60FF600D5244A /* Resources */, 105 | C4A00A302AA60FF600D5244A /* Application */, 106 | ); 107 | path = DynamicIslandDemo; 108 | sourceTree = ""; 109 | }; 110 | C4A00A222AA60FF600D5244A /* Scenes */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | C47B7CEB2AFCE70500FA4781 /* Profile */, 114 | ); 115 | path = Scenes; 116 | sourceTree = ""; 117 | }; 118 | C4A00A242AA60FF600D5244A /* Constants */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | C4A00A252AA60FF600D5244A /* Const.swift */, 122 | ); 123 | path = Constants; 124 | sourceTree = ""; 125 | }; 126 | C4A00A262AA60FF600D5244A /* Resources */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | C46492552AB83EC60083CA3D /* Design Reference */, 130 | C4A00A272AA60FF600D5244A /* Assets.xcassets */, 131 | C4A00A282AA60FF600D5244A /* Preview Assets.xcassets */, 132 | ); 133 | path = Resources; 134 | sourceTree = ""; 135 | }; 136 | C4A00A292AA60FF600D5244A /* Managers */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | C4A00A2A2AA60FF600D5244A /* DynamicIslandManager.swift */, 140 | ); 141 | path = Managers; 142 | sourceTree = ""; 143 | }; 144 | C4A00A2B2AA60FF600D5244A /* Models */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | C4A00A2C2AA60FF600D5244A /* User.swift */, 148 | ); 149 | path = Models; 150 | sourceTree = ""; 151 | }; 152 | C4A00A2D2AA60FF600D5244A /* Extensions */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | C4A00A2E2AA60FF600D5244A /* CGFloat+Extenions.swift */, 156 | ); 157 | path = Extensions; 158 | sourceTree = ""; 159 | }; 160 | C4A00A302AA60FF600D5244A /* Application */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | C4A00A312AA60FF600D5244A /* Application.swift */, 164 | ); 165 | path = Application; 166 | sourceTree = ""; 167 | }; 168 | C4A00A322AA60FF600D5244A /* Views */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | C4A00A332AA60FF600D5244A /* Cells */, 172 | C4A00A352AA60FF600D5244A /* Modifiers */, 173 | C4A00A372AA60FF600D5244A /* Views */, 174 | ); 175 | path = Views; 176 | sourceTree = ""; 177 | }; 178 | C4A00A332AA60FF600D5244A /* Cells */ = { 179 | isa = PBXGroup; 180 | children = ( 181 | C4A00A342AA60FF600D5244A /* ToggleCellView.swift */, 182 | ); 183 | path = Cells; 184 | sourceTree = ""; 185 | }; 186 | C4A00A352AA60FF600D5244A /* Modifiers */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | C4A00A362AA60FF600D5244A /* ScrollStatusModifier.swift */, 190 | C464925A2AB86A1B0083CA3D /* OpacityTransitionModifier.swift */, 191 | ); 192 | path = Modifiers; 193 | sourceTree = ""; 194 | }; 195 | C4A00A372AA60FF600D5244A /* Views */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | C4A00A382AA60FF600D5244A /* OffsetObservingScrollView.swift */, 199 | C4A00A392AA60FF600D5244A /* PositionObservingView.swift */, 200 | ); 201 | path = Views; 202 | sourceTree = ""; 203 | }; 204 | /* End PBXGroup section */ 205 | 206 | /* Begin PBXNativeTarget section */ 207 | C41E454F2A83A826009470CB /* DynamicIslandDemo */ = { 208 | isa = PBXNativeTarget; 209 | buildConfigurationList = C41E455E2A83A826009470CB /* Build configuration list for PBXNativeTarget "DynamicIslandDemo" */; 210 | buildPhases = ( 211 | C41E454C2A83A826009470CB /* Sources */, 212 | C41E454D2A83A826009470CB /* Frameworks */, 213 | C41E454E2A83A826009470CB /* Resources */, 214 | C42934002AA618F6005ED087 /* SwiftLint */, 215 | ); 216 | buildRules = ( 217 | ); 218 | dependencies = ( 219 | ); 220 | name = DynamicIslandDemo; 221 | packageProductDependencies = ( 222 | C4E312AB2A84F93E00A28192 /* SwiftUIIntrospect */, 223 | ); 224 | productName = DynamicIslandSample; 225 | productReference = C41E45502A83A826009470CB /* DynamicIslandDemo.app */; 226 | productType = "com.apple.product-type.application"; 227 | }; 228 | /* End PBXNativeTarget section */ 229 | 230 | /* Begin PBXProject section */ 231 | C41E45482A83A826009470CB /* Project object */ = { 232 | isa = PBXProject; 233 | attributes = { 234 | BuildIndependentTargetsInParallel = 1; 235 | LastSwiftUpdateCheck = 1430; 236 | LastUpgradeCheck = 1500; 237 | TargetAttributes = { 238 | C41E454F2A83A826009470CB = { 239 | CreatedOnToolsVersion = 14.3.1; 240 | }; 241 | }; 242 | }; 243 | buildConfigurationList = C41E454B2A83A826009470CB /* Build configuration list for PBXProject "DynamicIslandDemo" */; 244 | compatibilityVersion = "Xcode 14.0"; 245 | developmentRegion = en; 246 | hasScannedForEncodings = 0; 247 | knownRegions = ( 248 | en, 249 | Base, 250 | ); 251 | mainGroup = C41E45472A83A826009470CB; 252 | packageReferences = ( 253 | C4E312AA2A84F93E00A28192 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, 254 | ); 255 | productRefGroup = C41E45512A83A826009470CB /* Products */; 256 | projectDirPath = ""; 257 | projectRoot = ""; 258 | targets = ( 259 | C41E454F2A83A826009470CB /* DynamicIslandDemo */, 260 | ); 261 | }; 262 | /* End PBXProject section */ 263 | 264 | /* Begin PBXResourcesBuildPhase section */ 265 | C41E454E2A83A826009470CB /* Resources */ = { 266 | isa = PBXResourcesBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | C4A00A3D2AA60FF600D5244A /* Preview Assets.xcassets in Resources */, 270 | C4A00A3C2AA60FF600D5244A /* Assets.xcassets in Resources */, 271 | C46492582AB83ED80083CA3D /* IMG_7451.PNG in Resources */, 272 | C46492592AB83ED80083CA3D /* IMG_7450.PNG in Resources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXResourcesBuildPhase section */ 277 | 278 | /* Begin PBXShellScriptBuildPhase section */ 279 | C42934002AA618F6005ED087 /* SwiftLint */ = { 280 | isa = PBXShellScriptBuildPhase; 281 | alwaysOutOfDate = 1; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | ); 285 | inputFileListPaths = ( 286 | ); 287 | inputPaths = ( 288 | ); 289 | name = SwiftLint; 290 | outputFileListPaths = ( 291 | ); 292 | outputPaths = ( 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | shellPath = /bin/sh; 296 | shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint; then\n swiftlint —-fix && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 297 | }; 298 | /* End PBXShellScriptBuildPhase section */ 299 | 300 | /* Begin PBXSourcesBuildPhase section */ 301 | C41E454C2A83A826009470CB /* Sources */ = { 302 | isa = PBXSourcesBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | C4A00A3F2AA60FF600D5244A /* User.swift in Sources */, 306 | C4A00A422AA60FF600D5244A /* Application.swift in Sources */, 307 | C4A00A3B2AA60FF600D5244A /* Const.swift in Sources */, 308 | C47B7CED2AFCE71B00FA4781 /* ProfileView.swift in Sources */, 309 | C4A00A432AA60FF600D5244A /* ToggleCellView.swift in Sources */, 310 | C4A00A3E2AA60FF600D5244A /* DynamicIslandManager.swift in Sources */, 311 | C4A00A452AA60FF600D5244A /* OffsetObservingScrollView.swift in Sources */, 312 | C4A00A402AA60FF600D5244A /* CGFloat+Extenions.swift in Sources */, 313 | C464925B2AB86A1B0083CA3D /* OpacityTransitionModifier.swift in Sources */, 314 | C47B7CEF2AFCE72700FA4781 /* ProfileViewModel.swift in Sources */, 315 | C4A00A442AA60FF600D5244A /* ScrollStatusModifier.swift in Sources */, 316 | C4A00A462AA60FF600D5244A /* PositionObservingView.swift in Sources */, 317 | ); 318 | runOnlyForDeploymentPostprocessing = 0; 319 | }; 320 | /* End PBXSourcesBuildPhase section */ 321 | 322 | /* Begin XCBuildConfiguration section */ 323 | C41E455C2A83A826009470CB /* Debug */ = { 324 | isa = XCBuildConfiguration; 325 | buildSettings = { 326 | ALWAYS_SEARCH_USER_PATHS = NO; 327 | CLANG_ANALYZER_NONNULL = YES; 328 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 329 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 330 | CLANG_ENABLE_MODULES = YES; 331 | CLANG_ENABLE_OBJC_ARC = YES; 332 | CLANG_ENABLE_OBJC_WEAK = YES; 333 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 334 | CLANG_WARN_BOOL_CONVERSION = YES; 335 | CLANG_WARN_COMMA = YES; 336 | CLANG_WARN_CONSTANT_CONVERSION = YES; 337 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 338 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 339 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 340 | CLANG_WARN_EMPTY_BODY = YES; 341 | CLANG_WARN_ENUM_CONVERSION = YES; 342 | CLANG_WARN_INFINITE_RECURSION = YES; 343 | CLANG_WARN_INT_CONVERSION = YES; 344 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 345 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 346 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 347 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 348 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 349 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 350 | CLANG_WARN_STRICT_PROTOTYPES = YES; 351 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 352 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 353 | CLANG_WARN_UNREACHABLE_CODE = YES; 354 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 355 | COPY_PHASE_STRIP = NO; 356 | DEBUG_INFORMATION_FORMAT = dwarf; 357 | ENABLE_STRICT_OBJC_MSGSEND = YES; 358 | ENABLE_TESTABILITY = YES; 359 | GCC_C_LANGUAGE_STANDARD = gnu11; 360 | GCC_DYNAMIC_NO_PIC = NO; 361 | GCC_NO_COMMON_BLOCKS = YES; 362 | GCC_OPTIMIZATION_LEVEL = 0; 363 | GCC_PREPROCESSOR_DEFINITIONS = ( 364 | "DEBUG=1", 365 | "$(inherited)", 366 | ); 367 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 368 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 369 | GCC_WARN_UNDECLARED_SELECTOR = YES; 370 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 371 | GCC_WARN_UNUSED_FUNCTION = YES; 372 | GCC_WARN_UNUSED_VARIABLE = YES; 373 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 374 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 375 | MTL_FAST_MATH = YES; 376 | ONLY_ACTIVE_ARCH = YES; 377 | SDKROOT = iphoneos; 378 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 379 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 380 | }; 381 | name = Debug; 382 | }; 383 | C41E455D2A83A826009470CB /* Release */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ALWAYS_SEARCH_USER_PATHS = NO; 387 | CLANG_ANALYZER_NONNULL = YES; 388 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 389 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 390 | CLANG_ENABLE_MODULES = YES; 391 | CLANG_ENABLE_OBJC_ARC = YES; 392 | CLANG_ENABLE_OBJC_WEAK = YES; 393 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 394 | CLANG_WARN_BOOL_CONVERSION = YES; 395 | CLANG_WARN_COMMA = YES; 396 | CLANG_WARN_CONSTANT_CONVERSION = YES; 397 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 398 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 399 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 400 | CLANG_WARN_EMPTY_BODY = YES; 401 | CLANG_WARN_ENUM_CONVERSION = YES; 402 | CLANG_WARN_INFINITE_RECURSION = YES; 403 | CLANG_WARN_INT_CONVERSION = YES; 404 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 405 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 406 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 407 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 408 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 409 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 410 | CLANG_WARN_STRICT_PROTOTYPES = YES; 411 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 412 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 413 | CLANG_WARN_UNREACHABLE_CODE = YES; 414 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 415 | COPY_PHASE_STRIP = NO; 416 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 417 | ENABLE_NS_ASSERTIONS = NO; 418 | ENABLE_STRICT_OBJC_MSGSEND = YES; 419 | GCC_C_LANGUAGE_STANDARD = gnu11; 420 | GCC_NO_COMMON_BLOCKS = YES; 421 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 422 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 423 | GCC_WARN_UNDECLARED_SELECTOR = YES; 424 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 425 | GCC_WARN_UNUSED_FUNCTION = YES; 426 | GCC_WARN_UNUSED_VARIABLE = YES; 427 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 428 | MTL_ENABLE_DEBUG_INFO = NO; 429 | MTL_FAST_MATH = YES; 430 | SDKROOT = iphoneos; 431 | SWIFT_COMPILATION_MODE = wholemodule; 432 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 433 | VALIDATE_PRODUCT = YES; 434 | }; 435 | name = Release; 436 | }; 437 | C41E455F2A83A826009470CB /* Debug */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 441 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 442 | CODE_SIGN_STYLE = Automatic; 443 | CURRENT_PROJECT_VERSION = 1; 444 | DEVELOPMENT_ASSET_PATHS = "\"DynamicIslandDemo/Resources/\""; 445 | DEVELOPMENT_TEAM = XPJG98X97P; 446 | ENABLE_PREVIEWS = YES; 447 | GENERATE_INFOPLIST_FILE = YES; 448 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 449 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 450 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 451 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 452 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 453 | LD_RUNPATH_SEARCH_PATHS = ( 454 | "$(inherited)", 455 | "@executable_path/Frameworks", 456 | ); 457 | MARKETING_VERSION = 1.0; 458 | PRODUCT_BUNDLE_IDENTIFIER = com.codeIT.DynamicIslandSample; 459 | PRODUCT_NAME = "$(TARGET_NAME)"; 460 | SWIFT_EMIT_LOC_STRINGS = YES; 461 | SWIFT_VERSION = 5.0; 462 | TARGETED_DEVICE_FAMILY = "1,2"; 463 | }; 464 | name = Debug; 465 | }; 466 | C41E45602A83A826009470CB /* Release */ = { 467 | isa = XCBuildConfiguration; 468 | buildSettings = { 469 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 470 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 471 | CODE_SIGN_STYLE = Automatic; 472 | CURRENT_PROJECT_VERSION = 1; 473 | DEVELOPMENT_ASSET_PATHS = "\"DynamicIslandDemo/Resources/\""; 474 | DEVELOPMENT_TEAM = XPJG98X97P; 475 | ENABLE_PREVIEWS = YES; 476 | GENERATE_INFOPLIST_FILE = YES; 477 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 478 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 479 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 480 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 481 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 482 | LD_RUNPATH_SEARCH_PATHS = ( 483 | "$(inherited)", 484 | "@executable_path/Frameworks", 485 | ); 486 | MARKETING_VERSION = 1.0; 487 | PRODUCT_BUNDLE_IDENTIFIER = com.codeIT.DynamicIslandSample; 488 | PRODUCT_NAME = "$(TARGET_NAME)"; 489 | SWIFT_EMIT_LOC_STRINGS = YES; 490 | SWIFT_VERSION = 5.0; 491 | TARGETED_DEVICE_FAMILY = "1,2"; 492 | }; 493 | name = Release; 494 | }; 495 | /* End XCBuildConfiguration section */ 496 | 497 | /* Begin XCConfigurationList section */ 498 | C41E454B2A83A826009470CB /* Build configuration list for PBXProject "DynamicIslandDemo" */ = { 499 | isa = XCConfigurationList; 500 | buildConfigurations = ( 501 | C41E455C2A83A826009470CB /* Debug */, 502 | C41E455D2A83A826009470CB /* Release */, 503 | ); 504 | defaultConfigurationIsVisible = 0; 505 | defaultConfigurationName = Release; 506 | }; 507 | C41E455E2A83A826009470CB /* Build configuration list for PBXNativeTarget "DynamicIslandDemo" */ = { 508 | isa = XCConfigurationList; 509 | buildConfigurations = ( 510 | C41E455F2A83A826009470CB /* Debug */, 511 | C41E45602A83A826009470CB /* Release */, 512 | ); 513 | defaultConfigurationIsVisible = 0; 514 | defaultConfigurationName = Release; 515 | }; 516 | /* End XCConfigurationList section */ 517 | 518 | /* Begin XCRemoteSwiftPackageReference section */ 519 | C4E312AA2A84F93E00A28192 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = { 520 | isa = XCRemoteSwiftPackageReference; 521 | repositoryURL = "https://github.com/siteline/swiftui-introspect"; 522 | requirement = { 523 | kind = upToNextMajorVersion; 524 | minimumVersion = 0.10.0; 525 | }; 526 | }; 527 | /* End XCRemoteSwiftPackageReference section */ 528 | 529 | /* Begin XCSwiftPackageProductDependency section */ 530 | C4E312AB2A84F93E00A28192 /* SwiftUIIntrospect */ = { 531 | isa = XCSwiftPackageProductDependency; 532 | package = C4E312AA2A84F93E00A28192 /* XCRemoteSwiftPackageReference "swiftui-introspect" */; 533 | productName = SwiftUIIntrospect; 534 | }; 535 | /* End XCSwiftPackageProductDependency section */ 536 | }; 537 | rootObject = C41E45482A83A826009470CB /* Project object */; 538 | } 539 | -------------------------------------------------------------------------------- /DynamicIslandDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DynamicIslandDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DynamicIslandDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swiftui-introspect", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/siteline/swiftui-introspect", 7 | "state" : { 8 | "revision" : "ccb973cfff703cba53fb88197413485c060eb26b", 9 | "version" : "0.10.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Application/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - Application 12 | 13 | @main 14 | struct Application: App { 15 | 16 | // MARK: - Body 17 | 18 | var body: some Scene { 19 | WindowGroup { 20 | ProfileView(viewModel: .init()) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Constants/Const.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Const.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Const 12 | 13 | enum Const { 14 | 15 | // MARK: - General 16 | 17 | enum General { 18 | static let bulletPointSymbol = "•" 19 | } 20 | 21 | // MARK: - Managers 22 | 23 | enum DynamicIslandManager { 24 | static let topPadding: CGFloat = 11.0 25 | } 26 | 27 | // MARK: - Views 28 | 29 | enum MainView { 30 | static let imageViewId = "Image" 31 | static let islandViewId = "Island" 32 | static let headerViewId = "Header" 33 | 34 | static let imageTopPadding: CGFloat = 8.0 35 | static let imageSize: CGFloat = 100.0 36 | } 37 | 38 | enum OffsetObservingScrollView { 39 | static let openOffsetPosition: CGFloat = 80.0 40 | static let coordinateSpaceName: UUID = UUID() 41 | } 42 | 43 | enum ToggleCellView { 44 | static let enabledTitle = "Enabled" 45 | static let disabledTitle = "Disabled" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Extensions/CGFloat+Extenions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloat+Extenions.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - CGFloat Extension 12 | 13 | extension CGFloat { 14 | 15 | func percentage(_ perc: CGFloat) -> CGFloat { 16 | self * perc / 100 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Managers/DynamicIslandManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicIslandManager.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - DynamicIslandManager 12 | 13 | class DynamicIslandManager { 14 | 15 | // MARK: - Singleton 16 | 17 | static let shared = DynamicIslandManager() 18 | 19 | private init() {} 20 | 21 | // MARK: - Internal Properties 22 | 23 | /// Returns whether this device supports the Dynamic Island. 24 | /// This returns `true` for iPhone 14 Pro and iPhone Pro Max, otherwise returns `false`. 25 | let isIslandAvailable: Bool = { 26 | if #unavailable(iOS 16) { 27 | return false 28 | } 29 | 30 | #if targetEnvironment(simulator) 31 | let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]! 32 | #else 33 | var systemInfo = utsname() 34 | uname(&systemInfo) 35 | let machineMirror = Mirror(reflecting: systemInfo.machine) 36 | let identifier = machineMirror.children.reduce("") { identifier, element in 37 | guard let value = element.value as? Int8, value != .zero else { return identifier } 38 | return identifier + String(UnicodeScalar(UInt8(value))) 39 | } 40 | #endif 41 | 42 | let prefix = "iPhone" 43 | let iPhone14MinNumber: Double = 15.0 44 | let model = String(identifier.suffix(identifier.count - prefix.count)) 45 | let modelNumber = Double(model.replacingOccurrences(of: ",", with: ".")) 46 | guard let modelNumber = modelNumber else { return false } 47 | return modelNumber > iPhone14MinNumber 48 | }() 49 | 50 | /// The top padding of the Dynamic Island cutout. 51 | var islandTopPadding: CGFloat { 52 | isIslandAvailable ? Const.DynamicIslandManager.topPadding : .zero 53 | } 54 | 55 | /// The size of the Dynamic Island cutout. 56 | var islandSize: CGSize { 57 | isIslandAvailable ? islandActualSize : notchActualSize 58 | } 59 | 60 | // MARK: - Private Properties 61 | 62 | private var islandActualSize: CGSize { 63 | CGSize(width: 126.0, height: 37.33) 64 | } 65 | 66 | private var notchActualSize: CGSize { 67 | CGSize(width: 94, height: 32) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - User 12 | 13 | struct User { 14 | let name: String 15 | let avatarImageName: String 16 | let phoneNumber: String 17 | let nickname: String 18 | } 19 | 20 | // MARK: - User Mock Extension 21 | 22 | extension User { 23 | 24 | static func mock( 25 | name: String = "Konstantin", 26 | avatarImageName: String = "avatar-image", 27 | phoneNumber: String = "+380 66 666 6666", 28 | nickname: String = "@stolyarenkoks" 29 | ) -> Self { 30 | .init( 31 | name: name, 32 | avatarImageName: avatarImageName, 33 | phoneNumber: phoneNumber, 34 | nickname: nickname 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/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 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon.jpeg", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stolyarenkoks/Dynamic-Island-SwiftUI-Demo/f7970494943cb5a6303b527acdde63be5cac7295/DynamicIslandDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon.jpeg -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/Assets.xcassets/avatar-image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "avatar-image.jpeg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/Assets.xcassets/avatar-image.imageset/avatar-image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stolyarenkoks/Dynamic-Island-SwiftUI-Demo/f7970494943cb5a6303b527acdde63be5cac7295/DynamicIslandDemo/Resources/Assets.xcassets/avatar-image.imageset/avatar-image.jpeg -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/Design Reference/IMG_7450.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stolyarenkoks/Dynamic-Island-SwiftUI-Demo/f7970494943cb5a6303b527acdde63be5cac7295/DynamicIslandDemo/Resources/Design Reference/IMG_7450.PNG -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/Design Reference/IMG_7451.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stolyarenkoks/Dynamic-Island-SwiftUI-Demo/f7970494943cb5a6303b527acdde63be5cac7295/DynamicIslandDemo/Resources/Design Reference/IMG_7451.PNG -------------------------------------------------------------------------------- /DynamicIslandDemo/Resources/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Scenes/Profile/ProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.11.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - ProfileView 12 | 13 | struct ProfileView: View { 14 | 15 | // MARK: - Private Properties 16 | 17 | @ObservedObject private var viewModel: ViewModel 18 | @Environment(\.scenePhase) private var scenePhase 19 | 20 | // MARK: - Init 21 | 22 | init(viewModel: ViewModel) { 23 | self.viewModel = viewModel 24 | } 25 | 26 | // MARK: - Body 27 | 28 | var body: some View { 29 | GeometryReader { bounds in 30 | ZStack(alignment: .top) { 31 | if viewModel.isIslandShapeVisible { 32 | Canvas { context, size in 33 | context.addFilter(.alphaThreshold(min: 0.5, color: .black)) 34 | context.addFilter(.blur(radius: 6)) 35 | context.drawLayer { ctx in 36 | if let island = ctx.resolveSymbol(id: Const.MainView.islandViewId) { 37 | ctx.draw(island, at: CGPoint(x: (size.width / 2), 38 | y: viewModel.islandTopPadding + (viewModel.islandSize.height / 2))) 39 | } 40 | if let image = ctx.resolveSymbol(id: Const.MainView.imageViewId) { 41 | let yImageOffset = (Const.MainView.imageSize / 2) + Const.MainView.imageTopPadding 42 | let yImagePosition = bounds.safeAreaInsets.top + yImageOffset 43 | ctx.draw(image, at: CGPoint(x: size.width / 2, y: yImagePosition)) 44 | } 45 | } 46 | } symbols: { 47 | islandShapeView() 48 | avatarShapeView() 49 | } 50 | .edgesIgnoringSafeArea(.top) 51 | } 52 | 53 | avatarView() 54 | scrollView() 55 | navigationButtons() 56 | } 57 | } 58 | .background(Color(uiColor: .systemGray6)) 59 | .onChange(of: scenePhase) { newPhase in 60 | let isActive = newPhase == .active 61 | let duration = isActive ? 0.3 : .zero 62 | withAnimation(Animation.linear(duration: duration).delay(duration)) { 63 | viewModel.isIslandShapeVisible = isActive 64 | } 65 | } 66 | } 67 | 68 | // MARK: - Private Methods 69 | 70 | private func islandShapeView() -> some View { 71 | Capsule(style: .continuous) 72 | .frame(width: viewModel.islandSize.width, 73 | height: viewModel.islandSize.height, 74 | alignment: .center) 75 | .scaleEffect(viewModel.islandScale) 76 | .tag(Const.MainView.islandViewId) 77 | } 78 | 79 | private func avatarShapeView() -> some View { 80 | Circle() 81 | .fill(.black) 82 | .frame(width: Const.MainView.imageSize, height: Const.MainView.imageSize, alignment: .center) 83 | .scaleEffect(viewModel.scale) 84 | .offset(y: max(-viewModel.offset.y, -Const.MainView.imageSize + Const.MainView.imageSize.percentage(20))) 85 | .tag(Const.MainView.imageViewId) 86 | } 87 | 88 | private func avatarView() -> some View { 89 | Image(viewModel.userAvatarImageName) 90 | .resizable() 91 | .aspectRatio(1, contentMode: .fit) 92 | .frame(width: Const.MainView.imageSize, height: Const.MainView.imageSize, alignment: .center) 93 | .clipShape(Circle()) 94 | .scaleEffect(viewModel.scale) 95 | .blur(radius: viewModel.blur) 96 | .opacity(viewModel.avatarOpacity) 97 | .offset(y: max(-viewModel.offset.y, -Const.MainView.imageSize + Const.MainView.imageSize.percentage(10))) 98 | .padding(.top, Const.MainView.imageTopPadding) 99 | } 100 | 101 | private func scrollView() -> some View { 102 | OffsetObservingScrollView( 103 | offset: $viewModel.offset, 104 | showsIndicators: $viewModel.showsIndicators, 105 | isHeaderPagingEnabled: $viewModel.isHeaderPagingEnabled 106 | ) { 107 | LazyVStack( 108 | alignment: .center, 109 | pinnedViews: viewModel.isHeaderPinningEnabled ? [.sectionHeaders] : [] 110 | ) { 111 | Section(header: headerView()) { 112 | scrollViewCells() 113 | } 114 | } 115 | .padding(.top, Const.MainView.imageSize + Const.MainView.imageTopPadding) 116 | .padding(.horizontal) 117 | } 118 | .padding(.top, Const.MainView.imageTopPadding) 119 | .scrollDismissesKeyboard(.interactively) 120 | } 121 | 122 | private func headerView() -> some View { 123 | VStack(spacing: 4.0) { 124 | Text(viewModel.userName) 125 | .font(.system(size: viewModel.titleFontSize, weight: .medium)) 126 | 127 | HStack(spacing: 4.0) { 128 | Text(viewModel.userPhoneNumber) 129 | Text(Const.General.bulletPointSymbol) 130 | Text(viewModel.userNickname) 131 | } 132 | .foregroundColor(Color(uiColor: .systemGray)) 133 | .font(.system(size: viewModel.descriptionFontSize, weight: .regular)) 134 | .opacity(viewModel.headerOpacity) 135 | .padding(.bottom, viewModel.headerPadding) 136 | } 137 | .frame(maxWidth: .infinity) 138 | .background(Color(uiColor: .systemGray6)) 139 | .id(Const.MainView.headerViewId) 140 | } 141 | 142 | private func scrollViewCells() -> some View { 143 | VStack(spacing: 24.0) { 144 | generalSettingsCells() 145 | headerSettingsCells() 146 | emptyCells() 147 | } 148 | } 149 | 150 | private func generalSettingsCells() -> some View { 151 | VStack(spacing: 24.0) { 152 | ToggleCellView(parameterName: "Indicators", isToggleOn: $viewModel.showsIndicators) 153 | ToggleCellView(parameterName: "Zoom Effect", isToggleOn: $viewModel.isZoomEffectEnabled) 154 | } 155 | } 156 | 157 | private func headerSettingsCells() -> some View { 158 | VStack { 159 | ToggleCellView(parameterName: "Header Paging", isToggleOn: $viewModel.isHeaderPagingEnabled) 160 | ToggleCellView(parameterName: "Header Pinning", isToggleOn: $viewModel.isHeaderPinningEnabled) 161 | } 162 | } 163 | 164 | private func emptyCells() -> some View { 165 | VStack { 166 | ForEach(0..<15) { _ in 167 | ToggleCellView(isToggleOn: .constant(false), showToggle: false) 168 | } 169 | } 170 | } 171 | 172 | private func navigationButtons() -> some View { 173 | HStack { 174 | if viewModel.isAvatarHidden { 175 | Button { 176 | print("QR button tapped") 177 | } label: { 178 | Image(systemName: "qrcode").imageScale(.large) 179 | } 180 | .opacityTransition(move: .top) 181 | } 182 | 183 | Spacer() 184 | 185 | Button { 186 | print("\(viewModel.isAvatarHidden ? "Edit" : "Search") button tapped") 187 | } label: { 188 | if viewModel.isAvatarHidden { 189 | AnyView(Text("Edit")) 190 | .opacityTransition(move: .top) 191 | } else { 192 | if viewModel.isHeaderPinningEnabled { 193 | AnyView(Image(systemName: "magnifyingglass").imageScale(.large)) 194 | .opacityTransition(move: .bottom) 195 | } 196 | } 197 | } 198 | } 199 | .padding(.horizontal, 16.0) 200 | .padding(.top, 4.0) 201 | .onChange( 202 | of: viewModel.percentage, 203 | perform: { value in 204 | withAnimation(.linear(duration: 0.2)) { 205 | viewModel.isAvatarHidden = !(value == 100) 206 | } 207 | } 208 | ) 209 | } 210 | } 211 | 212 | // MARK: - PreviewProvider 213 | 214 | struct ProfileView_Previews: PreviewProvider { 215 | 216 | static var previews: some View { 217 | ProfileView(viewModel: .init(user: .mock())) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Scenes/Profile/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModel.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.11.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension ProfileView { 12 | 13 | // MARK: - ViewModel 14 | 15 | @MainActor class ViewModel: ObservableObject { 16 | 17 | // MARK: - Internal Properties 18 | 19 | @Published var showsIndicators = false 20 | @Published var isZoomEffectEnabled = true 21 | @Published var isHeaderPagingEnabled = true 22 | @Published var isHeaderPinningEnabled = true 23 | @Published var isIslandShapeVisible = true 24 | @Published var isAvatarHidden: Bool = true 25 | 26 | @Published var offset: CGPoint = .zero 27 | 28 | var islandSize: CGSize { 29 | DynamicIslandManager.shared.islandSize 30 | } 31 | 32 | var islandTopPadding: CGFloat { 33 | DynamicIslandManager.shared.islandTopPadding 34 | } 35 | 36 | var percentage: CGFloat { 37 | return min(offset.y, Const.MainView.imageSize) 38 | } 39 | 40 | var scale: CGFloat { 41 | let coefficient = 1 / 1.2 42 | let percentage = percentage * coefficient 43 | let scale = (percentage * (0 - 1) / 100) + 1 44 | return min(scale, 1) 45 | } 46 | 47 | var islandScale: CGFloat { 48 | let coefficient: CGFloat = isZoomEffectEnabled ? 1.1 : 1.0 49 | var scaleFactor: CGFloat = 1 50 | scaleFactor = abs((offset.y / 1.5) - islandSize.height) / islandSize.height 51 | let percentage = min(max(scaleFactor, .zero), 1) 52 | return (percentage * (1 - coefficient)) + coefficient 53 | } 54 | 55 | var avatarOpacity: CGFloat { 56 | let coefficient = 2.2 57 | let percentage = percentage * coefficient 58 | let opacity = (percentage * (0 - 1) / 100) + 1 59 | return min(opacity, 1) 60 | } 61 | 62 | var headerOpacity: CGFloat { 63 | let coefficient = 1.0 64 | let percentage = percentage * coefficient 65 | let opacity = (percentage * (0 - 1) / 100) + 1 66 | return min(opacity, 1) 67 | } 68 | 69 | var blur: CGFloat { 70 | let coefficient = 3.5 71 | let percentage = percentage * coefficient 72 | let opacity = (percentage * (0 - 1) / 100) + 1 73 | return 1 - min(opacity, 1) 74 | } 75 | 76 | var userAvatarImageName: String { 77 | user.avatarImageName 78 | } 79 | 80 | var userName: String { 81 | user.name 82 | } 83 | 84 | var userPhoneNumber: String { 85 | user.phoneNumber 86 | } 87 | 88 | var userNickname: String { 89 | user.nickname 90 | } 91 | 92 | var titleFontSize: CGFloat { 93 | interpolateValue(minValue: 18.0, maxValue: 32.0, percent: 100 - percentage) 94 | } 95 | 96 | var descriptionFontSize: CGFloat { 97 | interpolateValue(minValue: 14.0, maxValue: 17.0, percent: 100 - percentage) 98 | } 99 | 100 | var headerPadding: CGFloat { 101 | interpolateValue(maxValue: 18.0, percent: 100 - percentage) 102 | } 103 | 104 | // MARK: - Private Properties 105 | 106 | private let user: User 107 | 108 | // MARK: - Init 109 | 110 | init(user: User = .mock()) { 111 | self.user = user 112 | 113 | setup() 114 | } 115 | 116 | // MARK: - Private Methods 117 | 118 | private func setup() { 119 | isZoomEffectEnabled = DynamicIslandManager.shared.isIslandAvailable 120 | } 121 | 122 | private func interpolateValue(minValue: Double = .zero, maxValue: Double, percent: Double) -> Double { 123 | let value = minValue + (maxValue - minValue) * (percent / 100) 124 | let balancedValue = min(max(value, minValue), maxValue) 125 | return balancedValue 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Views/Cells/ToggleCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleCellView.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - ToggleCellView 12 | 13 | struct ToggleCellView: View { 14 | 15 | // MARK: - Internal Properties 16 | 17 | var parameterName: String = "" 18 | @Binding var isToggleOn: Bool 19 | var showToggle = true 20 | 21 | // MARK: - Body 22 | 23 | var body: some View { 24 | HStack { 25 | Text(makeToggleTitle(parameterName: parameterName, isEnabled: isToggleOn)) 26 | .font(.callout) 27 | .lineLimit(1) 28 | 29 | Spacer() 30 | 31 | if showToggle { 32 | Toggle("", isOn: $isToggleOn) 33 | .frame(maxWidth: 50) 34 | } 35 | } 36 | .padding([.all], 16) 37 | .frame(maxWidth: .infinity, maxHeight: 44) 38 | .background(.background) 39 | .cornerRadius(12) 40 | } 41 | 42 | // MARK: - Private Methods 43 | 44 | private func makeToggleTitle(parameterName: String, isEnabled: Bool) -> String { 45 | guard showToggle else { return "" } 46 | return parameterName + " " + (isEnabled ? Const.ToggleCellView.enabledTitle : Const.ToggleCellView.disabledTitle) 47 | } 48 | } 49 | 50 | // MARK: - PreviewProvider 51 | 52 | struct ToggleCellView_Previews: PreviewProvider { 53 | 54 | static var previews: some View { 55 | 56 | VStack { 57 | ToggleCellView(parameterName: "Not Long Title", isToggleOn: .constant(true)) 58 | ToggleCellView(parameterName: "Long Title Very Long Title Title Title Title", isToggleOn: .constant(false)) 59 | ToggleCellView(parameterName: "Empty", isToggleOn: .constant(true), showToggle: false) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Views/Modifiers/OpacityTransitionModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpacityTransitionModifier.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 18.09.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - OpacityTransitionModifier 12 | 13 | struct OpacityTransitionModifier: ViewModifier { 14 | 15 | let move: Edge 16 | 17 | func body(content: Content) -> some View { 18 | content 19 | .transition( 20 | .asymmetric(insertion: .move(edge: move), removal: .move(edge: move)) 21 | .combined(with: .asymmetric(insertion: .opacity, removal: .opacity)) 22 | ) 23 | } 24 | } 25 | 26 | // MARK: - OpacityTransition View Extension 27 | 28 | extension View { 29 | 30 | func opacityTransition(move: Edge) -> some View { 31 | modifier(OpacityTransitionModifier(move: move)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Views/Modifiers/ScrollStatusModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollStatusModifier.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import SwiftUIIntrospect 11 | 12 | // MARK: - ScrollStatusModifier 13 | 14 | struct ScrollStatusModifier: ViewModifier { 15 | 16 | // MARK: - Internal Properties 17 | 18 | @State var delegate = ScrollDelegate() 19 | @Binding var isScrolling: Bool 20 | 21 | // MARK: - Body 22 | 23 | func body(content: Content) -> some View { 24 | content 25 | .onAppear { 26 | self.delegate.isScrolling = $isScrolling 27 | } 28 | .introspect(.scrollView, on: .iOS(.v17, .v16, .v15, .v14, .v13), customize: { scrollView in 29 | scrollView.delegate = delegate 30 | }) 31 | } 32 | } 33 | 34 | // MARK: - ScrollDelegate 35 | 36 | final class ScrollDelegate: NSObject, UITableViewDelegate, UIScrollViewDelegate { 37 | 38 | // MARK: - Internal Properties 39 | 40 | var isScrolling: Binding? 41 | 42 | // MARK: - Internal Methods 43 | 44 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 45 | guard let isScrolling = isScrolling?.wrappedValue, !isScrolling else { return } 46 | Task { 47 | self.isScrolling?.wrappedValue = true 48 | } 49 | } 50 | 51 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 52 | guard let isScrolling = isScrolling?.wrappedValue, isScrolling else { return } 53 | Task { 54 | self.isScrolling?.wrappedValue = false 55 | } 56 | } 57 | 58 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 59 | guard !decelerate, let isScrolling = isScrolling?.wrappedValue, isScrolling else { return } 60 | Task { 61 | self.isScrolling?.wrappedValue = false 62 | } 63 | } 64 | } 65 | 66 | // MARK: - ScrollStatusModifier View Extension 67 | 68 | extension View { 69 | 70 | func scrollStatus(isScrolling: Binding) -> some View { 71 | modifier(ScrollStatusModifier(isScrolling: isScrolling)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Views/Views/OffsetObservingScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OffsetObservingScrollView.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - OffsetObservingScrollView 12 | 13 | struct OffsetObservingScrollView: View { 14 | 15 | // MARK: - Internal Properties 16 | 17 | @Binding var offset: CGPoint 18 | @Binding var showsIndicators: Bool 19 | @Binding var isHeaderPagingEnabled: Bool 20 | @ViewBuilder var content: () -> Content 21 | 22 | var axes: Axis.Set = [.vertical] 23 | @State var delegate = ScrollDelegate() 24 | @State var isScrolling = false 25 | 26 | // MARK: - Body 27 | 28 | var body: some View { 29 | ScrollViewReader { proxy in 30 | ScrollView(axes, showsIndicators: showsIndicators) { 31 | PositionObservingView( 32 | coordinateSpace: .named(Const.OffsetObservingScrollView.coordinateSpaceName), 33 | position: Binding( 34 | get: { offset }, 35 | set: { newOffset in 36 | offset = CGPoint( 37 | x: -newOffset.x, 38 | y: -newOffset.y 39 | ) 40 | } 41 | ), 42 | content: content 43 | ) 44 | } 45 | .scrollStatus(isScrolling: $isScrolling) 46 | .coordinateSpace(name: Const.OffsetObservingScrollView.coordinateSpaceName) 47 | .onChange( 48 | of: isScrolling, 49 | perform: { _ in 50 | guard isHeaderPagingEnabled, 51 | offset.y < Const.OffsetObservingScrollView.openOffsetPosition && offset.y > 0, 52 | !isScrolling else { return } 53 | let shouldShowBottom = offset.y > Const.OffsetObservingScrollView.openOffsetPosition / 2 54 | let anchor: UnitPoint = shouldShowBottom ? .top : .bottom 55 | withAnimation { 56 | proxy.scrollTo(Const.MainView.headerViewId, anchor: anchor) 57 | } 58 | } 59 | ) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /DynamicIslandDemo/Views/Views/PositionObservingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PositionObservingView.swift 3 | // DynamicIslandDemo 4 | // 5 | // Created by Konstantin Stolyarenko on 09.07.2023. 6 | // Copyright © 2023 SKS. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - PositionObservingView 12 | 13 | struct PositionObservingView: View { 14 | 15 | // MARK: - CurrentPreferenceKey 16 | 17 | private struct CurrentPreferenceKey: PreferenceKey { 18 | static var defaultValue: CGPoint { .zero } 19 | 20 | static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { } 21 | } 22 | 23 | // MARK: - Internal Properties 24 | 25 | var coordinateSpace: CoordinateSpace 26 | @Binding var position: CGPoint 27 | @ViewBuilder var content: () -> Content 28 | 29 | // MARK: - Body 30 | 31 | var body: some View { 32 | content() 33 | .background( 34 | GeometryReader { geometry in 35 | Color.clear.preference( 36 | key: CurrentPreferenceKey.self, 37 | value: geometry.frame(in: coordinateSpace).origin 38 | ) 39 | } 40 | ) 41 | .onPreferenceChange(CurrentPreferenceKey.self) { position in 42 | self.position = position 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © June 01, 2025 Konstantin Stolyarenko 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, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | Additional Condition: 15 | If you use this software in your project, you must provide clear attribution 16 | to the original author (Konstantin Stolyarenko) and include a link to the 17 | original repository: https://github.com/stolyarenkoks/Dynamic-Island-SwiftUI-Demo 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Island SwiftUI Demo like Telegram 2 | An example of a custom animation of the user's avatar smoothly flowing and dissolving into the Dynamic Island, following the example of how it is done in Telegram. 3 | Developed entirely natively using Swift and SwiftUI. 4 | 5 | ![Demo](demo.gif) 6 | 7 | ## Installation 8 | All dependencies are managed by SPM automatically. 9 | 10 | ## Build 11 | No additional setup is needed. Build project using Xcode. 12 | 13 | ## Technologies 14 | * Swift 15 | * SwiftUI 16 | 17 | ## Versions 18 | * Xcode 14.3.1 (latest) 19 | * Swift 5.8.1 (latest) 20 | 21 | ## Branches 22 | GitFlow is strictly enforced on this repository. [GitFlow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) 23 | 24 | ### Branch overview 25 | * master 26 | * develop 27 | * feature/name 28 | * hotfix/name 29 | 30 | ### Git Flow: 31 | feature -> develop -> master 32 | 33 | ## License 34 | Copyright © June 01, 2025 Konstantin Stolyarenko. All rights reserved. 35 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stolyarenkoks/Dynamic-Island-SwiftUI-Demo/f7970494943cb5a6303b527acdde63be5cac7295/demo.gif -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stolyarenkoks/Dynamic-Island-SwiftUI-Demo/f7970494943cb5a6303b527acdde63be5cac7295/icon.png -------------------------------------------------------------------------------- /scripts/license-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get full date: "January 1, 2025" 4 | FULL_DATE=$(date +"%B %d, %Y") 5 | 6 | # Update LICENSE file, replacing only the date 7 | sed -i "s/Copyright © [A-Za-z]* [0-9]*, [0-9]*\(.*\)$/Copyright © $FULL_DATE\1/" LICENSE 8 | 9 | # Update README.md file, replacing only the date 10 | sed -i "s/Copyright © [A-Za-z]* [0-9]*, [0-9]*\(.*\)$/Copyright © $FULL_DATE\1/" README.md 11 | --------------------------------------------------------------------------------