├── .gitignore ├── LICENSE ├── Musical View.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Musical View ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── sample.imageset │ │ ├── Contents.json │ │ └── sample.jpg ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── FrontPlayerViewController.swift ├── Info.plist ├── SemiModalTransition │ ├── CustomTransitioningDelegate.swift │ ├── LICENSE │ └── SemiModalPresentationController.swift ├── View │ ├── ButterflyHandleView │ │ ├── ButterflyHandleView.swift │ │ └── LICENSE │ └── UIView.swift └── ViewController.swift ├── README.md └── demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | # Pods/ 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots/**/*.png 70 | fastlane/test_output 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Musical View 4 | 5 | Copyright (c) 2018 Satori Maru. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Musical View.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 51142D6720D2BEBE002D0050 /* CustomTransitioningDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51142D6620D2BEBE002D0050 /* CustomTransitioningDelegate.swift */; }; 11 | 512B024720D29EB6003B9840 /* FrontPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512B024620D29EB6003B9840 /* FrontPlayerViewController.swift */; }; 12 | 514A4A3020E286A100BF0A34 /* ButterflyHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A4A2E20E286A100BF0A34 /* ButterflyHandleView.swift */; }; 13 | 51BB170320D16AA300359606 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB170220D16AA300359606 /* AppDelegate.swift */; }; 14 | 51BB170520D16AA300359606 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB170420D16AA300359606 /* ViewController.swift */; }; 15 | 51BB170820D16AA300359606 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51BB170620D16AA300359606 /* Main.storyboard */; }; 16 | 51BB170A20D16AA400359606 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 51BB170920D16AA400359606 /* Assets.xcassets */; }; 17 | 51BB170D20D16AA400359606 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51BB170B20D16AA400359606 /* LaunchScreen.storyboard */; }; 18 | 51BB171520D16AB500359606 /* SemiModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB171420D16AB400359606 /* SemiModalPresentationController.swift */; }; 19 | 51BB171920D1720600359606 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB171820D1720600359606 /* UIView.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 51142D6620D2BEBE002D0050 /* CustomTransitioningDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTransitioningDelegate.swift; sourceTree = ""; }; 24 | 512B024620D29EB6003B9840 /* FrontPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrontPlayerViewController.swift; sourceTree = ""; }; 25 | 514A4A2D20E286A100BF0A34 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 26 | 514A4A2E20E286A100BF0A34 /* ButterflyHandleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButterflyHandleView.swift; sourceTree = ""; }; 27 | 514A4A3320E28AB500BF0A34 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 28 | 51BB16FF20D16AA300359606 /* Musical View.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Musical View.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 51BB170220D16AA300359606 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 30 | 51BB170420D16AA300359606 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 31 | 51BB170720D16AA300359606 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 32 | 51BB170920D16AA400359606 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | 51BB170C20D16AA400359606 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 34 | 51BB170E20D16AA400359606 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | 51BB171420D16AB400359606 /* SemiModalPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SemiModalPresentationController.swift; sourceTree = ""; }; 36 | 51BB171820D1720600359606 /* UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 51BB16FC20D16AA300359606 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | 5118C4DA20E15C56005D9631 /* SemiModalTransition */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 51BB171420D16AB400359606 /* SemiModalPresentationController.swift */, 54 | 51142D6620D2BEBE002D0050 /* CustomTransitioningDelegate.swift */, 55 | 514A4A3320E28AB500BF0A34 /* LICENSE */, 56 | ); 57 | path = SemiModalTransition; 58 | sourceTree = ""; 59 | }; 60 | 5118C4DB20E15C78005D9631 /* View */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 514A4A2C20E286A100BF0A34 /* ButterflyHandleView */, 64 | 51BB171820D1720600359606 /* UIView.swift */, 65 | ); 66 | path = View; 67 | sourceTree = ""; 68 | }; 69 | 514A4A2C20E286A100BF0A34 /* ButterflyHandleView */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 514A4A2E20E286A100BF0A34 /* ButterflyHandleView.swift */, 73 | 514A4A2D20E286A100BF0A34 /* LICENSE */, 74 | ); 75 | path = ButterflyHandleView; 76 | sourceTree = ""; 77 | }; 78 | 51BB16F620D16AA300359606 = { 79 | isa = PBXGroup; 80 | children = ( 81 | 51BB170120D16AA300359606 /* Musical View */, 82 | 51BB170020D16AA300359606 /* Products */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | 51BB170020D16AA300359606 /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 51BB16FF20D16AA300359606 /* Musical View.app */, 90 | ); 91 | name = Products; 92 | sourceTree = ""; 93 | }; 94 | 51BB170120D16AA300359606 /* Musical View */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 51BB170220D16AA300359606 /* AppDelegate.swift */, 98 | 51BB170620D16AA300359606 /* Main.storyboard */, 99 | 51BB170420D16AA300359606 /* ViewController.swift */, 100 | 512B024620D29EB6003B9840 /* FrontPlayerViewController.swift */, 101 | 5118C4DB20E15C78005D9631 /* View */, 102 | 5118C4DA20E15C56005D9631 /* SemiModalTransition */, 103 | 51BB170920D16AA400359606 /* Assets.xcassets */, 104 | 51BB170B20D16AA400359606 /* LaunchScreen.storyboard */, 105 | 51BB170E20D16AA400359606 /* Info.plist */, 106 | ); 107 | path = "Musical View"; 108 | sourceTree = ""; 109 | }; 110 | /* End PBXGroup section */ 111 | 112 | /* Begin PBXNativeTarget section */ 113 | 51BB16FE20D16AA300359606 /* Musical View */ = { 114 | isa = PBXNativeTarget; 115 | buildConfigurationList = 51BB171120D16AA400359606 /* Build configuration list for PBXNativeTarget "Musical View" */; 116 | buildPhases = ( 117 | 51BB16FB20D16AA300359606 /* Sources */, 118 | 51BB16FC20D16AA300359606 /* Frameworks */, 119 | 51BB16FD20D16AA300359606 /* Resources */, 120 | ); 121 | buildRules = ( 122 | ); 123 | dependencies = ( 124 | ); 125 | name = "Musical View"; 126 | productName = "Musical View"; 127 | productReference = 51BB16FF20D16AA300359606 /* Musical View.app */; 128 | productType = "com.apple.product-type.application"; 129 | }; 130 | /* End PBXNativeTarget section */ 131 | 132 | /* Begin PBXProject section */ 133 | 51BB16F720D16AA300359606 /* Project object */ = { 134 | isa = PBXProject; 135 | attributes = { 136 | LastSwiftUpdateCheck = 0930; 137 | LastUpgradeCheck = 0930; 138 | ORGANIZATIONNAME = usagimaru; 139 | TargetAttributes = { 140 | 51BB16FE20D16AA300359606 = { 141 | CreatedOnToolsVersion = 9.3; 142 | }; 143 | }; 144 | }; 145 | buildConfigurationList = 51BB16FA20D16AA300359606 /* Build configuration list for PBXProject "Musical View" */; 146 | compatibilityVersion = "Xcode 9.3"; 147 | developmentRegion = en; 148 | hasScannedForEncodings = 0; 149 | knownRegions = ( 150 | en, 151 | Base, 152 | ); 153 | mainGroup = 51BB16F620D16AA300359606; 154 | productRefGroup = 51BB170020D16AA300359606 /* Products */; 155 | projectDirPath = ""; 156 | projectRoot = ""; 157 | targets = ( 158 | 51BB16FE20D16AA300359606 /* Musical View */, 159 | ); 160 | }; 161 | /* End PBXProject section */ 162 | 163 | /* Begin PBXResourcesBuildPhase section */ 164 | 51BB16FD20D16AA300359606 /* Resources */ = { 165 | isa = PBXResourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | 51BB170D20D16AA400359606 /* LaunchScreen.storyboard in Resources */, 169 | 51BB170A20D16AA400359606 /* Assets.xcassets in Resources */, 170 | 51BB170820D16AA300359606 /* Main.storyboard in Resources */, 171 | ); 172 | runOnlyForDeploymentPostprocessing = 0; 173 | }; 174 | /* End PBXResourcesBuildPhase section */ 175 | 176 | /* Begin PBXSourcesBuildPhase section */ 177 | 51BB16FB20D16AA300359606 /* Sources */ = { 178 | isa = PBXSourcesBuildPhase; 179 | buildActionMask = 2147483647; 180 | files = ( 181 | 51BB171920D1720600359606 /* UIView.swift in Sources */, 182 | 514A4A3020E286A100BF0A34 /* ButterflyHandleView.swift in Sources */, 183 | 51142D6720D2BEBE002D0050 /* CustomTransitioningDelegate.swift in Sources */, 184 | 51BB170520D16AA300359606 /* ViewController.swift in Sources */, 185 | 51BB170320D16AA300359606 /* AppDelegate.swift in Sources */, 186 | 51BB171520D16AB500359606 /* SemiModalPresentationController.swift in Sources */, 187 | 512B024720D29EB6003B9840 /* FrontPlayerViewController.swift in Sources */, 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | /* End PBXSourcesBuildPhase section */ 192 | 193 | /* Begin PBXVariantGroup section */ 194 | 51BB170620D16AA300359606 /* Main.storyboard */ = { 195 | isa = PBXVariantGroup; 196 | children = ( 197 | 51BB170720D16AA300359606 /* Base */, 198 | ); 199 | name = Main.storyboard; 200 | sourceTree = ""; 201 | }; 202 | 51BB170B20D16AA400359606 /* LaunchScreen.storyboard */ = { 203 | isa = PBXVariantGroup; 204 | children = ( 205 | 51BB170C20D16AA400359606 /* Base */, 206 | ); 207 | name = LaunchScreen.storyboard; 208 | sourceTree = ""; 209 | }; 210 | /* End PBXVariantGroup section */ 211 | 212 | /* Begin XCBuildConfiguration section */ 213 | 51BB170F20D16AA400359606 /* Debug */ = { 214 | isa = XCBuildConfiguration; 215 | buildSettings = { 216 | ALWAYS_SEARCH_USER_PATHS = NO; 217 | CLANG_ANALYZER_NONNULL = YES; 218 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 220 | CLANG_CXX_LIBRARY = "libc++"; 221 | CLANG_ENABLE_MODULES = YES; 222 | CLANG_ENABLE_OBJC_ARC = YES; 223 | CLANG_ENABLE_OBJC_WEAK = YES; 224 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 225 | CLANG_WARN_BOOL_CONVERSION = YES; 226 | CLANG_WARN_COMMA = YES; 227 | CLANG_WARN_CONSTANT_CONVERSION = YES; 228 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 229 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 230 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 231 | CLANG_WARN_EMPTY_BODY = YES; 232 | CLANG_WARN_ENUM_CONVERSION = YES; 233 | CLANG_WARN_INFINITE_RECURSION = YES; 234 | CLANG_WARN_INT_CONVERSION = YES; 235 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 236 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 237 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 238 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 239 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 240 | CLANG_WARN_STRICT_PROTOTYPES = YES; 241 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 242 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 243 | CLANG_WARN_UNREACHABLE_CODE = YES; 244 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 245 | CODE_SIGN_IDENTITY = "iPhone Developer"; 246 | COPY_PHASE_STRIP = NO; 247 | DEBUG_INFORMATION_FORMAT = dwarf; 248 | ENABLE_STRICT_OBJC_MSGSEND = YES; 249 | ENABLE_TESTABILITY = YES; 250 | GCC_C_LANGUAGE_STANDARD = gnu11; 251 | GCC_DYNAMIC_NO_PIC = NO; 252 | GCC_NO_COMMON_BLOCKS = YES; 253 | GCC_OPTIMIZATION_LEVEL = 0; 254 | GCC_PREPROCESSOR_DEFINITIONS = ( 255 | "DEBUG=1", 256 | "$(inherited)", 257 | ); 258 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 259 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 260 | GCC_WARN_UNDECLARED_SELECTOR = YES; 261 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 262 | GCC_WARN_UNUSED_FUNCTION = YES; 263 | GCC_WARN_UNUSED_VARIABLE = YES; 264 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 265 | MTL_ENABLE_DEBUG_INFO = YES; 266 | ONLY_ACTIVE_ARCH = YES; 267 | SDKROOT = iphoneos; 268 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 269 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 270 | }; 271 | name = Debug; 272 | }; 273 | 51BB171020D16AA400359606 /* Release */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ALWAYS_SEARCH_USER_PATHS = NO; 277 | CLANG_ANALYZER_NONNULL = YES; 278 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 279 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 280 | CLANG_CXX_LIBRARY = "libc++"; 281 | CLANG_ENABLE_MODULES = YES; 282 | CLANG_ENABLE_OBJC_ARC = YES; 283 | CLANG_ENABLE_OBJC_WEAK = YES; 284 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 285 | CLANG_WARN_BOOL_CONVERSION = YES; 286 | CLANG_WARN_COMMA = YES; 287 | CLANG_WARN_CONSTANT_CONVERSION = YES; 288 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 289 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 290 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 291 | CLANG_WARN_EMPTY_BODY = YES; 292 | CLANG_WARN_ENUM_CONVERSION = YES; 293 | CLANG_WARN_INFINITE_RECURSION = YES; 294 | CLANG_WARN_INT_CONVERSION = YES; 295 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 296 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 297 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 298 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 299 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 300 | CLANG_WARN_STRICT_PROTOTYPES = YES; 301 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 302 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 303 | CLANG_WARN_UNREACHABLE_CODE = YES; 304 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 305 | CODE_SIGN_IDENTITY = "iPhone Developer"; 306 | COPY_PHASE_STRIP = NO; 307 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 308 | ENABLE_NS_ASSERTIONS = NO; 309 | ENABLE_STRICT_OBJC_MSGSEND = YES; 310 | GCC_C_LANGUAGE_STANDARD = gnu11; 311 | GCC_NO_COMMON_BLOCKS = YES; 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 319 | MTL_ENABLE_DEBUG_INFO = NO; 320 | SDKROOT = iphoneos; 321 | SWIFT_COMPILATION_MODE = wholemodule; 322 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 323 | VALIDATE_PRODUCT = YES; 324 | }; 325 | name = Release; 326 | }; 327 | 51BB171220D16AA400359606 /* Debug */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 331 | CODE_SIGN_STYLE = Automatic; 332 | DEVELOPMENT_TEAM = R6RZ6S2FLK; 333 | INFOPLIST_FILE = "Musical View/Info.plist"; 334 | LD_RUNPATH_SEARCH_PATHS = ( 335 | "$(inherited)", 336 | "@executable_path/Frameworks", 337 | ); 338 | PRODUCT_BUNDLE_IDENTIFIER = "com.interactionmania.Musical-View"; 339 | PRODUCT_NAME = "$(TARGET_NAME)"; 340 | SWIFT_VERSION = 4.0; 341 | TARGETED_DEVICE_FAMILY = "1,2"; 342 | }; 343 | name = Debug; 344 | }; 345 | 51BB171320D16AA400359606 /* Release */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 349 | CODE_SIGN_STYLE = Automatic; 350 | DEVELOPMENT_TEAM = R6RZ6S2FLK; 351 | INFOPLIST_FILE = "Musical View/Info.plist"; 352 | LD_RUNPATH_SEARCH_PATHS = ( 353 | "$(inherited)", 354 | "@executable_path/Frameworks", 355 | ); 356 | PRODUCT_BUNDLE_IDENTIFIER = "com.interactionmania.Musical-View"; 357 | PRODUCT_NAME = "$(TARGET_NAME)"; 358 | SWIFT_VERSION = 4.0; 359 | TARGETED_DEVICE_FAMILY = "1,2"; 360 | }; 361 | name = Release; 362 | }; 363 | /* End XCBuildConfiguration section */ 364 | 365 | /* Begin XCConfigurationList section */ 366 | 51BB16FA20D16AA300359606 /* Build configuration list for PBXProject "Musical View" */ = { 367 | isa = XCConfigurationList; 368 | buildConfigurations = ( 369 | 51BB170F20D16AA400359606 /* Debug */, 370 | 51BB171020D16AA400359606 /* Release */, 371 | ); 372 | defaultConfigurationIsVisible = 0; 373 | defaultConfigurationName = Release; 374 | }; 375 | 51BB171120D16AA400359606 /* Build configuration list for PBXNativeTarget "Musical View" */ = { 376 | isa = XCConfigurationList; 377 | buildConfigurations = ( 378 | 51BB171220D16AA400359606 /* Debug */, 379 | 51BB171320D16AA400359606 /* Release */, 380 | ); 381 | defaultConfigurationIsVisible = 0; 382 | defaultConfigurationName = Release; 383 | }; 384 | /* End XCConfigurationList section */ 385 | }; 386 | rootObject = 51BB16F720D16AA300359606 /* Project object */; 387 | } 388 | -------------------------------------------------------------------------------- /Musical View.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Musical View.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Musical View/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Musical View 4 | // 5 | // Created by usagimaru on 2018.06.14. 6 | // Copyright © 2018年 usagimaru. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | self.window?.backgroundColor = UIColor.black 20 | return true 21 | } 22 | 23 | func applicationWillResignActive(_ application: UIApplication) { 24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 25 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 26 | } 27 | 28 | func applicationDidEnterBackground(_ application: UIApplication) { 29 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | func applicationWillEnterForeground(_ application: UIApplication) { 34 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 35 | } 36 | 37 | func applicationDidBecomeActive(_ application: UIApplication) { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | func applicationWillTerminate(_ application: UIApplication) { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Musical View/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Musical View/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Musical View/Assets.xcassets/sample.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "sample.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Musical View/Assets.xcassets/sample.imageset/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagimaru/Musical-View/c2bedd65f6d3e2313b5da52b97efb8843c70a0d8/Musical View/Assets.xcassets/sample.imageset/sample.jpg -------------------------------------------------------------------------------- /Musical View/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Musical View/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /Musical View/FrontPlayerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrontPlayerViewController.swift 3 | // Musical View 4 | // 5 | // Created by usagimaru on 2018.06.14. 6 | // Copyright © 2018 Satori Maru. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | class FrontPlayerViewController: UIViewController { 13 | 14 | private struct LayoutInfo { 15 | /// 角R 16 | let cornerRadius: CGFloat = 12 17 | /// 前景の上マージン 18 | let viewMargin: CGFloat = 60 19 | /// 背景の上マージン 20 | let backdropMargin: CGFloat = 16 21 | /// 背景の最小縮小率 22 | let backdropScaleNormal: CGFloat = 0.94 23 | /// 背景の最大拡大率 24 | let backdropScaleLimit: CGFloat = 0.98 25 | /// 背景の拡大割合を調整します(大きくするほど拡大しにくい) 26 | let backdropScalingResolution: CGFloat = 3000 27 | } 28 | private let layoutInfo = LayoutInfo() 29 | 30 | @IBOutlet private weak var butterflyHandle: ButterflyHandleView! 31 | @IBOutlet private weak var contentView: UIView! 32 | @IBOutlet private weak var scrollView: UIScrollView! 33 | @IBOutlet private weak var frontJacketFrameView: UIView! 34 | @IBOutlet private weak var backJacketFrameView: UIView! 35 | @IBOutlet private weak var frontJacketView: UIImageView! 36 | @IBOutlet private weak var backJacketView: UIImageView! 37 | @IBOutlet private weak var contentViewTopConstraint: NSLayoutConstraint! 38 | @IBOutlet private weak var contentViewHeightConstraint: NSLayoutConstraint! 39 | 40 | var jacketImage: UIImage? { 41 | didSet { 42 | self.frontJacketView?.image = jacketImage 43 | self.backJacketView?.image = jacketImage 44 | } 45 | } 46 | 47 | var contentHeight: CGFloat = 0 { 48 | didSet { 49 | self.contentViewHeightConstraint?.constant = contentHeight 50 | } 51 | } 52 | 53 | 54 | // MARK: - ステータスバー 55 | 56 | override var preferredStatusBarStyle: UIStatusBarStyle { 57 | return .lightContent 58 | } 59 | 60 | override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { 61 | return .fade 62 | } 63 | 64 | 65 | // MARK: - 66 | 67 | override func viewDidLoad() { 68 | super.viewDidLoad() 69 | 70 | setAppearance() 71 | 72 | // true 指定しないと、カスタムトランジションのモーダルビューで元VCの StatusBarStyle を上書きしてくれない 73 | self.modalPresentationCapturesStatusBarAppearance = true 74 | 75 | self.frontJacketView.image = self.jacketImage 76 | self.backJacketView.image = self.jacketImage 77 | self.contentViewHeightConstraint.constant = self.contentHeight 78 | self.butterflyHandle.direction = .bottom 79 | } 80 | 81 | override func didReceiveMemoryWarning() { 82 | super.didReceiveMemoryWarning() 83 | // Dispose of any resources that can be recreated. 84 | } 85 | 86 | 87 | // MARK: - Navigation 88 | 89 | // In a storyboard-based application, you will often want to do a little preparation before navigation 90 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 91 | // Get the new view controller using segue.destinationViewController. 92 | // Pass the selected object to the new view controller. 93 | } 94 | 95 | override func viewWillAppear(_ animated: Bool) { 96 | super.viewWillAppear(animated) 97 | self.view.layoutIfNeeded() 98 | 99 | fitJacketView() 100 | 101 | if let semiModalPresentationController = self.presentationController as? SemiModalPresentationController { 102 | semiModalPresentationController.performPresentingTransition( 103 | withFrontMargin: self.layoutInfo.viewMargin, 104 | backdropMargins: CGPoint(x: 0, y: self.layoutInfo.backdropMargin), 105 | backdropScale: self.layoutInfo.backdropScaleNormal, 106 | backdropCornerRadius: self.layoutInfo.cornerRadius, 107 | animated: animated, 108 | additionalAnimations: nil) 109 | } 110 | } 111 | 112 | override func viewDidAppear(_ animated: Bool) { 113 | super.viewDidAppear(animated) 114 | self.scrollView.delegate = self 115 | self.scrollView.flashScrollIndicators() 116 | } 117 | 118 | override func viewWillDisappear(_ animated: Bool) { 119 | super.viewWillDisappear(animated) 120 | self.scrollView.delegate = nil 121 | 122 | if let semiModalPresentationController = self.presentationController as? SemiModalPresentationController { 123 | semiModalPresentationController.performDismissingTransition( 124 | withCustomTransfrom: nil, 125 | backdropCornerRadius: 0, 126 | animated: animated, 127 | additionalAnimations: nil) 128 | } 129 | } 130 | 131 | override func viewDidDisappear(_ animated: Bool) { 132 | super.viewDidDisappear(animated) 133 | } 134 | 135 | 136 | // MARK: - 137 | 138 | private func setAppearance() { 139 | self.view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] 140 | self.view.layer.cornerRadius = self.layoutInfo.cornerRadius 141 | 142 | self.frontJacketFrameView.layer.shadowColor = UIColor.black.cgColor 143 | self.frontJacketFrameView.layer.shadowRadius = self.layoutInfo.cornerRadius 144 | self.frontJacketFrameView.layer.shadowOffset = CGSize(width: 0, height: 8) 145 | self.frontJacketFrameView.layer.shadowOpacity = 0.2 146 | } 147 | 148 | /// 内接描画の実寸を得る 149 | private func scaledFittingSize() -> CGSize? { 150 | // https://stackoverflow.com/questions/6278876/how-to-know-the-image-size-after-applying-aspect-fit-for-the-image-in-an-uiimage 151 | 152 | guard let jacketImage = self.jacketImage else {return nil} 153 | let insideRect = CGRect(x: 0, y: 0, width: self.frontJacketFrameView.width, height: self.frontJacketFrameView.width) 154 | let jacketScaledSize = AVMakeRect(aspectRatio: jacketImage.size, insideRect: insideRect).size 155 | return jacketScaledSize 156 | } 157 | 158 | private func fitJacketView() { 159 | if let scaledFittingSize = scaledFittingSize() { 160 | self.frontJacketView.size = scaledFittingSize 161 | self.frontJacketView.y = self.frontJacketFrameView.height / 2 - self.frontJacketView.height / 2 162 | } 163 | } 164 | 165 | private func updateViewTransforms(withScrollOffset y: CGFloat) { 166 | guard let semiModalPresentationController = self.presentationController as? SemiModalPresentationController else {return} 167 | 168 | // スクロール量に合わせて背景ビューのスケールを変化させる 169 | let scrollRate = y / self.layoutInfo.backdropScalingResolution 170 | semiModalPresentationController.updateBackdropTransform(withScrollRate: scrollRate, 171 | backdropScale: self.layoutInfo.backdropScaleNormal, 172 | backdropScaleLimit: self.layoutInfo.backdropScaleLimit, 173 | backdropMargins: CGPoint(x: 0, y: self.layoutInfo.backdropMargin)) 174 | 175 | let move = min(y, 0) 176 | self.view.y = self.layoutInfo.viewMargin - move 177 | self.contentViewTopConstraint.constant = move 178 | self.contentViewHeightConstraint.constant = self.contentHeight - move 179 | } 180 | 181 | private func checkDismissingCondition(withScrollOffset y: CGFloat) { 182 | // 移動量が一定値を超えたらモーダルビューを閉じる(全体の1/6くらい) 183 | if y < -self.view.height / 6 { 184 | // モーダルビューを閉じた後に一瞬スクロールビューの中身がバウンスで戻る映像が見えてしまう対策 185 | self.scrollView.isScrollEnabled = false 186 | self.scrollView.setContentOffset(self.scrollView.contentOffset, animated: false) // 慣性スクロールを強制停止 187 | self.scrollView.contentOffset = CGPoint(x: 0, y: y) 188 | self.dismiss(animated: true, completion: nil) 189 | } 190 | } 191 | 192 | } 193 | 194 | 195 | // MARK: - UIScrollViewDelegate 196 | 197 | extension FrontPlayerViewController: UIScrollViewDelegate { 198 | 199 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 200 | let y = scrollView.contentOffset.y 201 | updateViewTransforms(withScrollOffset: y) 202 | } 203 | 204 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 205 | self.butterflyHandle.spread(animated: true) 206 | } 207 | 208 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 209 | self.butterflyHandle.flap(animated: true) 210 | 211 | let y = scrollView.contentOffset.y 212 | checkDismissingCondition(withScrollOffset: y) 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /Musical View/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Musical View/SemiModalTransition/CustomTransitioningDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SemiModalTransition 3 | // 4 | // Created by usagimaru on 2017.11.05. 5 | // Copyright © 2017 Satori Maru. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { 11 | 12 | func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { 13 | // SemiModalPresentationControllerを返す 14 | let controller = SemiModalPresentationController(presentedViewController: presented, presenting: presenting) 15 | controller.dismissesOnTappingOutside = false 16 | 17 | return controller 18 | } 19 | 20 | } 21 | 22 | class SemiModalTransitionSegue: UIStoryboardSegue { 23 | 24 | private(set) var transitioningDelegatee = TransitioningDelegate() 25 | 26 | override func perform() { 27 | // 今回は遷移をStoryboardSegueで定義したので、遷移実行時にTransitioningDelegateを適用するようにした 28 | destination.modalPresentationStyle = .custom 29 | destination.transitioningDelegate = transitioningDelegatee 30 | super.perform() 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Musical View/SemiModalTransition/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | SemiModalTransition 4 | 5 | Copyright (c) 2017 Satori Maru. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Musical View/SemiModalTransition/SemiModalPresentationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SemiModalPresentationController.swift 3 | // 4 | // Created by usagimaru on 2017.11.05. 5 | // Copyright © 2017 Satori Maru. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class SemiModalPresentationController: UIPresentationController { 11 | 12 | private(set) var tapGestureRecognizer: UITapGestureRecognizer? 13 | private var overlay = UIView() 14 | 15 | /// 呼び出し先ビューのframeを決定 16 | override var frameOfPresentedViewInContainerView: CGRect { 17 | var frame = containerView!.frame 18 | frame.origin.y = frame.height - self.modalViewHeight 19 | frame.size.height = self.modalViewHeight 20 | return frame 21 | } 22 | 23 | /// 外側をタップして閉じる 24 | var dismissesOnTappingOutside: Bool = true { 25 | didSet { 26 | self.tapGestureRecognizer?.isEnabled = self.dismissesOnTappingOutside 27 | } 28 | } 29 | 30 | private var modalViewHeight: CGFloat = 0 31 | 32 | /* 33 | memo: 34 | presented... 呼び出し先ViewController 35 | presenting.. 呼び出し元ViewController 36 | */ 37 | 38 | var backdropViewController: UIViewController { 39 | return self.presentingViewController 40 | } 41 | 42 | var frontViewController: UIViewController { 43 | return self.presentedViewController 44 | } 45 | 46 | 47 | // MARK: - 48 | 49 | /// モーダルビューの高さを設定 50 | func setModalViewHeight(_ newHeight: CGFloat, animated: Bool) { 51 | guard let presentedView = self.presentedView else {return} 52 | 53 | self.modalViewHeight = newHeight 54 | 55 | let frame = self.frameOfPresentedViewInContainerView 56 | 57 | if animated == false { 58 | presentedView.frame = frame 59 | return 60 | } 61 | 62 | UIView.animateWithSystemMotion({ 63 | presentedView.frame = frame 64 | presentedView.layoutIfNeeded() 65 | }, completion: nil) 66 | } 67 | 68 | private func setupOverlay(toContainerView: UIView) { 69 | self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapOnOverlay(_:))) 70 | self.tapGestureRecognizer!.isEnabled = self.dismissesOnTappingOutside 71 | 72 | self.overlay.frame = toContainerView.bounds 73 | self.overlay.backgroundColor = UIColor.black 74 | self.overlay.alpha = 0.0 75 | self.overlay.gestureRecognizers = [self.tapGestureRecognizer!] 76 | 77 | toContainerView.insertSubview(self.overlay, at: 0) 78 | } 79 | 80 | /// 表示直前 81 | override func presentationTransitionWillBegin() { 82 | guard let containerView = self.containerView else {return} 83 | 84 | setupOverlay(toContainerView: containerView) 85 | 86 | self.frontViewController.transitionCoordinator?.animate( 87 | alongsideTransition: { [weak self] (context) in 88 | self?.overlay.alpha = 0.35 89 | }, completion: nil) 90 | } 91 | 92 | /// 隠蔽直前 93 | override func dismissalTransitionWillBegin() { 94 | self.frontViewController.transitionCoordinator?.animate( 95 | alongsideTransition: { [weak self] (context) in 96 | self?.overlay.alpha = 0.0 97 | }, completion: nil) 98 | } 99 | 100 | override func dismissalTransitionDidEnd(_ completed: Bool) { 101 | if completed { 102 | self.overlay.removeFromSuperview() 103 | } 104 | } 105 | 106 | /// ビューのレイアウト直前 107 | override func containerViewWillLayoutSubviews() { 108 | if let containerView = containerView { 109 | self.overlay.frame = containerView.bounds 110 | } 111 | 112 | self.presentedView?.frame = self.frameOfPresentedViewInContainerView 113 | } 114 | 115 | /// 背景透過部分のタップで閉じる 116 | @objc private func tapOnOverlay(_ gesture: UITapGestureRecognizer) { 117 | self.frontViewController.dismiss(animated: true, completion: nil) 118 | } 119 | 120 | 121 | // MARK: - 122 | 123 | /// 背景ビューのスケールとずれを適用するアフィン行列を返す 124 | static func backdropTransform(withScale scale: CGFloat, translates: CGPoint) -> CGAffineTransform { 125 | return CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: translates.x, y: translates.y) 126 | } 127 | 128 | /// 前景ビューを表示するトランジションを描画 129 | func performPresentingTransition(withFrontMargin frontMargin: CGFloat, 130 | backdropMargins: CGPoint, 131 | backdropScale: CGFloat, 132 | backdropCornerRadius: CGFloat, 133 | animated: Bool, 134 | additionalAnimations parallelAnimations: (() -> Swift.Void)?) { 135 | // モーダルビューの高さを設定 136 | setModalViewHeight(self.frontViewController.view.frame.size.height - frontMargin, animated: false) 137 | 138 | // 背景ビューの縮小とずれ 139 | let t = SemiModalPresentationController.backdropTransform(withScale: backdropScale, translates: backdropMargins) 140 | 141 | if animated { 142 | UIView.animateWithSystemMotion({ 143 | self.backdropViewController.view.transform = t 144 | self.backdropViewController.view.layer.cornerRadius = backdropCornerRadius 145 | self.frontViewController.setNeedsStatusBarAppearanceUpdate() 146 | parallelAnimations?() 147 | }, completion: nil) 148 | } 149 | else { 150 | self.backdropViewController.view.transform = t 151 | self.backdropViewController.view.layer.cornerRadius = backdropCornerRadius 152 | self.frontViewController.setNeedsStatusBarAppearanceUpdate() 153 | } 154 | } 155 | 156 | /// 前景ビューを閉じるトランジションを描画 157 | func performDismissingTransition(withCustomTransfrom customTransfrom: CGAffineTransform?, 158 | backdropCornerRadius: CGFloat, 159 | animated: Bool, 160 | additionalAnimations parallelAnimations: (() -> Swift.Void)?) { 161 | var t = CGAffineTransform.identity 162 | if let customTransfrom = customTransfrom { 163 | t = customTransfrom 164 | } 165 | 166 | if animated { 167 | UIView.animateWithSystemMotion({ 168 | self.backdropViewController.view.transform = t 169 | self.backdropViewController.view.layer.cornerRadius = backdropCornerRadius 170 | //self.frontViewController.setNeedsStatusBarAppearanceUpdate() 171 | parallelAnimations?() 172 | }, completion: nil) 173 | } 174 | else { 175 | self.backdropViewController.view.transform = t 176 | self.backdropViewController.view.layer.cornerRadius = backdropCornerRadius 177 | //self.frontViewController.setNeedsStatusBarAppearanceUpdate() 178 | } 179 | } 180 | 181 | /// スクロール割合によって背景ビューのスケール・ずれを動的変化させる 182 | func updateBackdropTransform(withScrollRate scrollRate: CGFloat, 183 | backdropScale: CGFloat, 184 | backdropScaleLimit: CGFloat, 185 | backdropMargins: CGPoint) { 186 | let scale = max(min(backdropScale - scrollRate, backdropScaleLimit), backdropScale) 187 | let t = SemiModalPresentationController.backdropTransform(withScale: scale, translates: backdropMargins) 188 | self.backdropViewController.view.transform = t 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /Musical View/View/ButterflyHandleView/ButterflyHandleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButterflyHandleView.swift 3 | // 4 | // Created by usagimaru on 2018.06.14. 5 | // Copyright © 2018 Satori Maru. All rights reserved. 6 | // https://github.com/usagimaru/ButterflyHandle 7 | 8 | import UIKit 9 | 10 | class ButterflyHandleView: UIView { 11 | 12 | enum direction { 13 | case top 14 | case bottom 15 | } 16 | 17 | /// 定数定義 18 | struct Constant { 19 | /// 翼の色 20 | static let defaultWingColor = UIColor(displayP3Red: 0.82, green: 0.82, blue: 0.84, alpha: 1.0) 21 | /// 片翼の傾き 22 | static let wingFoldingAngle: CGFloat = (22.5 * CGFloat.pi / 180) 23 | /// 片翼の大きさ 24 | static let wingSize = CGSize(width: 20, height: 5) 25 | } 26 | 27 | var direction: direction = .top { 28 | didSet { 29 | self.isSpreading = self._isSpreading 30 | } 31 | } 32 | 33 | private var _isSpreading: Bool = true 34 | var isSpreading: Bool = true { 35 | didSet { 36 | if isSpreading { 37 | spread(animated: false) 38 | } 39 | else { 40 | flap(animated: false) 41 | } 42 | } 43 | } 44 | 45 | private var leftWing = UIView() 46 | private var rightWing = UIView() 47 | 48 | var wingColor: UIColor = ButterflyHandleView.Constant.defaultWingColor 49 | 50 | 51 | // MARK: - 52 | 53 | override init(frame: CGRect) { 54 | super.init(frame: frame) 55 | _init() 56 | } 57 | 58 | required init?(coder aDecoder: NSCoder) { 59 | super.init(coder: aDecoder) 60 | _init() 61 | } 62 | 63 | private func _init() { 64 | self.leftWing = wingBuilder() 65 | self.rightWing = wingBuilder() 66 | 67 | let adjustJoint: CGFloat = 2 68 | let adjustYAxis: CGFloat = 2 69 | 70 | self.leftWing.x = self.width / 2 - self.leftWing.width + adjustJoint 71 | self.leftWing.y = self.height / 2 - adjustYAxis 72 | self.rightWing.x = self.width / 2 - adjustJoint 73 | self.rightWing.y = self.height / 2 - adjustYAxis 74 | 75 | self.addSubview(self.leftWing) 76 | self.addSubview(self.rightWing) 77 | } 78 | 79 | private func wingBuilder() -> UIView { 80 | let wing = UIView.initWithSize(ButterflyHandleView.Constant.wingSize) 81 | wing.clipsToBounds = true 82 | wing.backgroundColor = self.wingColor 83 | wing.cornerRadius = ButterflyHandleView.Constant.wingSize.height / 2 84 | return wing 85 | } 86 | 87 | override func awakeFromNib() { 88 | super.awakeFromNib() 89 | self.isSpreading = false 90 | } 91 | 92 | func spread(animated: Bool) { 93 | self._isSpreading = true 94 | 95 | if animated { 96 | UIView.animateWithSystemMotion({ 97 | self.leftWing.transform = CGAffineTransform(rotationAngle: 0) 98 | self.rightWing.transform = CGAffineTransform(rotationAngle: 0) 99 | }, completion: nil) 100 | } 101 | else { 102 | self.leftWing.transform = CGAffineTransform(rotationAngle: 0) 103 | self.rightWing.transform = CGAffineTransform(rotationAngle: 0) 104 | } 105 | } 106 | 107 | func flap(animated: Bool) { 108 | self._isSpreading = false 109 | 110 | let wingFoldingAngle = ButterflyHandleView.Constant.wingFoldingAngle 111 | let angle = self.direction == .top ? -wingFoldingAngle : wingFoldingAngle 112 | 113 | if animated { 114 | UIView.animateWithSystemMotion({ 115 | self.leftWing.transform = CGAffineTransform(rotationAngle: angle) 116 | self.rightWing.transform = CGAffineTransform(rotationAngle: -angle) 117 | }, completion: nil) 118 | } 119 | else { 120 | self.leftWing.transform = CGAffineTransform(rotationAngle: angle) 121 | self.rightWing.transform = CGAffineTransform(rotationAngle: -angle) 122 | } 123 | 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /Musical View/View/ButterflyHandleView/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | ButterflyHandle 4 | 5 | Copyright (c) 2018 Satori Maru. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Musical View/View/UIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView 3 | // 4 | // Created by usagimaru on 2014.10.24. 5 | // Copyright © 2016 usagimaru. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | 12 | @IBInspectable var borderColor: UIColor? { 13 | get { 14 | if let c = layer.borderColor { 15 | return UIColor(cgColor: c) 16 | } 17 | return nil 18 | } 19 | set { 20 | layer.borderColor = newValue?.cgColor 21 | } 22 | } 23 | 24 | @IBInspectable var borderWidth: CGFloat { 25 | get { 26 | return layer.borderWidth 27 | } 28 | set { 29 | layer.borderWidth = newValue 30 | } 31 | } 32 | 33 | @IBInspectable var cornerRadius: CGFloat { 34 | get { 35 | return layer.cornerRadius 36 | } 37 | set { 38 | layer.cornerRadius = newValue 39 | clipsToBounds = newValue > 0.0 40 | } 41 | } 42 | 43 | 44 | var origin: CGPoint { 45 | get { 46 | return frame.origin 47 | } 48 | set { 49 | var r = frame 50 | r.origin = newValue 51 | frame = r 52 | } 53 | } 54 | 55 | var x: CGFloat { 56 | get { 57 | return frame.origin.x 58 | } 59 | set { 60 | var r = frame 61 | r.origin.x = newValue 62 | frame = r 63 | } 64 | } 65 | 66 | var y: CGFloat { 67 | get { 68 | return frame.origin.y 69 | } 70 | set { 71 | var r = frame 72 | r.origin.y = newValue 73 | frame = r 74 | } 75 | } 76 | 77 | var size: CGSize { 78 | get { 79 | return frame.size 80 | } 81 | set { 82 | var r = frame 83 | r.size = newValue 84 | frame = r 85 | } 86 | } 87 | 88 | var width: CGFloat { 89 | get { 90 | return frame.size.width 91 | } 92 | set { 93 | var r = frame 94 | r.size.width = newValue 95 | frame = r 96 | } 97 | } 98 | 99 | var height: CGFloat { 100 | get { 101 | return self.frame.size.height 102 | } 103 | set { 104 | var r = frame 105 | r.size.height = newValue 106 | frame = r 107 | } 108 | } 109 | 110 | 111 | var viewController: UIViewController? { 112 | var resp: UIResponder? = self 113 | while let next = resp?.next { 114 | if next is UIViewController { 115 | return next as? UIViewController 116 | } 117 | 118 | resp = next 119 | } 120 | 121 | return nil 122 | } 123 | 124 | 125 | // MARK: - 126 | 127 | class func initWithSize(_ size: CGSize) -> UIView { 128 | return UIView(frame: CGRect(x: 0, y: 0, 129 | width: size.width, 130 | height: size.height)) 131 | } 132 | 133 | class func animateWithSystemMotion(_ animations: (() -> Void)?, completion: ((Bool) -> Void)?) { 134 | UIView.perform(.delete, 135 | on: [], 136 | options: [.beginFromCurrentState, .allowUserInteraction], 137 | animations: animations, 138 | completion: completion) 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /Musical View/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Musical View 4 | // 5 | // Created by usagimaru on 2018.06.14. 6 | // Copyright © 2018年 usagimaru. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | private var isInitialAction: Bool = true 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | } 18 | 19 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 20 | if let frontPlayerVC = segue.destination as? FrontPlayerViewController { 21 | // 任意のジャケット画像を仮設定 22 | let jacketImage = #imageLiteral(resourceName: "sample") 23 | frontPlayerVC.jacketImage = jacketImage 24 | 25 | // スクロールビューの長さを仮設定 26 | let contentHeight: CGFloat = 1000 27 | frontPlayerVC.contentHeight = contentHeight 28 | } 29 | } 30 | 31 | override func viewWillAppear(_ animated: Bool) { 32 | super.viewWillAppear(animated) 33 | } 34 | 35 | override func viewDidAppear(_ animated: Bool) { 36 | super.viewDidAppear(animated) 37 | 38 | if self.isInitialAction { 39 | self.performSegue(withIdentifier: "presentPlayer", sender: nil) 40 | self.isInitialAction = false 41 | } 42 | } 43 | 44 | override func viewWillDisappear(_ animated: Bool) { 45 | super.viewWillDisappear(animated) 46 | } 47 | 48 | override func viewDidDisappear(_ animated: Bool) { 49 | super.viewDidDisappear(animated) 50 | } 51 | 52 | override func didReceiveMemoryWarning() { 53 | super.didReceiveMemoryWarning() 54 | // Dispose of any resources that can be recreated. 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Musical-View 2 | 3 | This is a demo implementation of a semi-modal view like iOS Music app. 4 | 5 | 6 | 7 | # Classes 8 | 9 | - FrontPlayerViewController.swift 10 | - [SemiModalTransition](https://github.com/usagimaru/SemiModalTransition) 11 | - [ButterflyHandle](https://github.com/usagimaru/ButterflyHandle) 12 | 13 | # License 14 | 15 | See [LICENSE](LICENSE) for details. 16 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagimaru/Musical-View/c2bedd65f6d3e2313b5da52b97efb8843c70a0d8/demo.gif --------------------------------------------------------------------------------