├── .gitignore ├── HalfScreenPresentation.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── HalfScreenPresentation ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CustomModalViewController.swift ├── Info.plist ├── SceneDelegate.swift └── ViewController.swift ├── img.png ├── readme.md └── screenshot.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | *.DS_Store 26 | 27 | # fastlane 28 | *.mobileprovision 29 | *.certSigningRequest 30 | *.p12 31 | *.cer 32 | .env 33 | env.beta 34 | fastlane/README.md 35 | fastlane/report.xml 36 | *.app.dSYM.zip 37 | 38 | ## Obj-C/Swift specific 39 | *.hmap 40 | *.ipa 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 47 | # 48 | Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | Carthage/Checkouts 54 | 55 | Carthage/Build 56 | *.xcworkspacedata 57 | 58 | # R.swift 59 | *.generated.swift 60 | -------------------------------------------------------------------------------- /HalfScreenPresentation.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 86131C2A266C7F0F007421A7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86131C29266C7F0F007421A7 /* AppDelegate.swift */; }; 11 | 86131C2C266C7F0F007421A7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86131C2B266C7F0F007421A7 /* SceneDelegate.swift */; }; 12 | 86131C2E266C7F0F007421A7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86131C2D266C7F0F007421A7 /* ViewController.swift */; }; 13 | 86131C31266C7F0F007421A7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 86131C2F266C7F0F007421A7 /* Main.storyboard */; }; 14 | 86131C33266C7F19007421A7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 86131C32266C7F19007421A7 /* Assets.xcassets */; }; 15 | 86131C36266C7F19007421A7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 86131C34266C7F19007421A7 /* LaunchScreen.storyboard */; }; 16 | 86131C3E266CB420007421A7 /* CustomModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86131C3D266CB420007421A7 /* CustomModalViewController.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 86131C26266C7F0F007421A7 /* HalfScreenPresentation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HalfScreenPresentation.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 86131C29266C7F0F007421A7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 86131C2B266C7F0F007421A7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 23 | 86131C2D266C7F0F007421A7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 24 | 86131C30266C7F0F007421A7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 25 | 86131C32266C7F19007421A7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 86131C35266C7F19007421A7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 27 | 86131C37266C7F19007421A7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | 86131C3D266CB420007421A7 /* CustomModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomModalViewController.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 86131C23266C7F0F007421A7 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 86131C1D266C7F0F007421A7 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 86131C28266C7F0F007421A7 /* HalfScreenPresentation */, 46 | 86131C27266C7F0F007421A7 /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | 86131C27266C7F0F007421A7 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 86131C26266C7F0F007421A7 /* HalfScreenPresentation.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | 86131C28266C7F0F007421A7 /* HalfScreenPresentation */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 86131C29266C7F0F007421A7 /* AppDelegate.swift */, 62 | 86131C2B266C7F0F007421A7 /* SceneDelegate.swift */, 63 | 86131C2D266C7F0F007421A7 /* ViewController.swift */, 64 | 86131C3D266CB420007421A7 /* CustomModalViewController.swift */, 65 | 86131C2F266C7F0F007421A7 /* Main.storyboard */, 66 | 86131C32266C7F19007421A7 /* Assets.xcassets */, 67 | 86131C34266C7F19007421A7 /* LaunchScreen.storyboard */, 68 | 86131C37266C7F19007421A7 /* Info.plist */, 69 | ); 70 | path = HalfScreenPresentation; 71 | sourceTree = ""; 72 | }; 73 | /* End PBXGroup section */ 74 | 75 | /* Begin PBXNativeTarget section */ 76 | 86131C25266C7F0F007421A7 /* HalfScreenPresentation */ = { 77 | isa = PBXNativeTarget; 78 | buildConfigurationList = 86131C3A266C7F19007421A7 /* Build configuration list for PBXNativeTarget "HalfScreenPresentation" */; 79 | buildPhases = ( 80 | 86131C22266C7F0F007421A7 /* Sources */, 81 | 86131C23266C7F0F007421A7 /* Frameworks */, 82 | 86131C24266C7F0F007421A7 /* Resources */, 83 | ); 84 | buildRules = ( 85 | ); 86 | dependencies = ( 87 | ); 88 | name = HalfScreenPresentation; 89 | productName = HalfScreenPresentation; 90 | productReference = 86131C26266C7F0F007421A7 /* HalfScreenPresentation.app */; 91 | productType = "com.apple.product-type.application"; 92 | }; 93 | /* End PBXNativeTarget section */ 94 | 95 | /* Begin PBXProject section */ 96 | 86131C1E266C7F0F007421A7 /* Project object */ = { 97 | isa = PBXProject; 98 | attributes = { 99 | LastSwiftUpdateCheck = 1250; 100 | LastUpgradeCheck = 1250; 101 | TargetAttributes = { 102 | 86131C25266C7F0F007421A7 = { 103 | CreatedOnToolsVersion = 12.5; 104 | }; 105 | }; 106 | }; 107 | buildConfigurationList = 86131C21266C7F0F007421A7 /* Build configuration list for PBXProject "HalfScreenPresentation" */; 108 | compatibilityVersion = "Xcode 9.3"; 109 | developmentRegion = en; 110 | hasScannedForEncodings = 0; 111 | knownRegions = ( 112 | en, 113 | Base, 114 | ); 115 | mainGroup = 86131C1D266C7F0F007421A7; 116 | productRefGroup = 86131C27266C7F0F007421A7 /* Products */; 117 | projectDirPath = ""; 118 | projectRoot = ""; 119 | targets = ( 120 | 86131C25266C7F0F007421A7 /* HalfScreenPresentation */, 121 | ); 122 | }; 123 | /* End PBXProject section */ 124 | 125 | /* Begin PBXResourcesBuildPhase section */ 126 | 86131C24266C7F0F007421A7 /* Resources */ = { 127 | isa = PBXResourcesBuildPhase; 128 | buildActionMask = 2147483647; 129 | files = ( 130 | 86131C36266C7F19007421A7 /* LaunchScreen.storyboard in Resources */, 131 | 86131C33266C7F19007421A7 /* Assets.xcassets in Resources */, 132 | 86131C31266C7F0F007421A7 /* Main.storyboard in Resources */, 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | }; 136 | /* End PBXResourcesBuildPhase section */ 137 | 138 | /* Begin PBXSourcesBuildPhase section */ 139 | 86131C22266C7F0F007421A7 /* Sources */ = { 140 | isa = PBXSourcesBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | 86131C2E266C7F0F007421A7 /* ViewController.swift in Sources */, 144 | 86131C3E266CB420007421A7 /* CustomModalViewController.swift in Sources */, 145 | 86131C2A266C7F0F007421A7 /* AppDelegate.swift in Sources */, 146 | 86131C2C266C7F0F007421A7 /* SceneDelegate.swift in Sources */, 147 | ); 148 | runOnlyForDeploymentPostprocessing = 0; 149 | }; 150 | /* End PBXSourcesBuildPhase section */ 151 | 152 | /* Begin PBXVariantGroup section */ 153 | 86131C2F266C7F0F007421A7 /* Main.storyboard */ = { 154 | isa = PBXVariantGroup; 155 | children = ( 156 | 86131C30266C7F0F007421A7 /* Base */, 157 | ); 158 | name = Main.storyboard; 159 | sourceTree = ""; 160 | }; 161 | 86131C34266C7F19007421A7 /* LaunchScreen.storyboard */ = { 162 | isa = PBXVariantGroup; 163 | children = ( 164 | 86131C35266C7F19007421A7 /* Base */, 165 | ); 166 | name = LaunchScreen.storyboard; 167 | sourceTree = ""; 168 | }; 169 | /* End PBXVariantGroup section */ 170 | 171 | /* Begin XCBuildConfiguration section */ 172 | 86131C38266C7F19007421A7 /* Debug */ = { 173 | isa = XCBuildConfiguration; 174 | buildSettings = { 175 | ALWAYS_SEARCH_USER_PATHS = NO; 176 | CLANG_ANALYZER_NONNULL = YES; 177 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 179 | CLANG_CXX_LIBRARY = "libc++"; 180 | CLANG_ENABLE_MODULES = YES; 181 | CLANG_ENABLE_OBJC_ARC = YES; 182 | CLANG_ENABLE_OBJC_WEAK = YES; 183 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 184 | CLANG_WARN_BOOL_CONVERSION = YES; 185 | CLANG_WARN_COMMA = YES; 186 | CLANG_WARN_CONSTANT_CONVERSION = YES; 187 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 188 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 189 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 190 | CLANG_WARN_EMPTY_BODY = YES; 191 | CLANG_WARN_ENUM_CONVERSION = YES; 192 | CLANG_WARN_INFINITE_RECURSION = YES; 193 | CLANG_WARN_INT_CONVERSION = YES; 194 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 195 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 196 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 197 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 198 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 199 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 200 | CLANG_WARN_STRICT_PROTOTYPES = YES; 201 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 202 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 203 | CLANG_WARN_UNREACHABLE_CODE = YES; 204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 205 | COPY_PHASE_STRIP = NO; 206 | DEBUG_INFORMATION_FORMAT = dwarf; 207 | ENABLE_STRICT_OBJC_MSGSEND = YES; 208 | ENABLE_TESTABILITY = YES; 209 | GCC_C_LANGUAGE_STANDARD = gnu11; 210 | GCC_DYNAMIC_NO_PIC = NO; 211 | GCC_NO_COMMON_BLOCKS = YES; 212 | GCC_OPTIMIZATION_LEVEL = 0; 213 | GCC_PREPROCESSOR_DEFINITIONS = ( 214 | "DEBUG=1", 215 | "$(inherited)", 216 | ); 217 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 218 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 219 | GCC_WARN_UNDECLARED_SELECTOR = YES; 220 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 221 | GCC_WARN_UNUSED_FUNCTION = YES; 222 | GCC_WARN_UNUSED_VARIABLE = YES; 223 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 224 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 225 | MTL_FAST_MATH = YES; 226 | ONLY_ACTIVE_ARCH = YES; 227 | SDKROOT = iphoneos; 228 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 229 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 230 | }; 231 | name = Debug; 232 | }; 233 | 86131C39266C7F19007421A7 /* Release */ = { 234 | isa = XCBuildConfiguration; 235 | buildSettings = { 236 | ALWAYS_SEARCH_USER_PATHS = NO; 237 | CLANG_ANALYZER_NONNULL = YES; 238 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_ENABLE_OBJC_WEAK = YES; 244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 245 | CLANG_WARN_BOOL_CONVERSION = YES; 246 | CLANG_WARN_COMMA = YES; 247 | CLANG_WARN_CONSTANT_CONVERSION = YES; 248 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 249 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 250 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 251 | CLANG_WARN_EMPTY_BODY = YES; 252 | CLANG_WARN_ENUM_CONVERSION = YES; 253 | CLANG_WARN_INFINITE_RECURSION = YES; 254 | CLANG_WARN_INT_CONVERSION = YES; 255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 259 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 260 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 261 | CLANG_WARN_STRICT_PROTOTYPES = YES; 262 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 263 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 264 | CLANG_WARN_UNREACHABLE_CODE = YES; 265 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 266 | COPY_PHASE_STRIP = NO; 267 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 268 | ENABLE_NS_ASSERTIONS = NO; 269 | ENABLE_STRICT_OBJC_MSGSEND = YES; 270 | GCC_C_LANGUAGE_STANDARD = gnu11; 271 | GCC_NO_COMMON_BLOCKS = YES; 272 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 273 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 274 | GCC_WARN_UNDECLARED_SELECTOR = YES; 275 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 276 | GCC_WARN_UNUSED_FUNCTION = YES; 277 | GCC_WARN_UNUSED_VARIABLE = YES; 278 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 279 | MTL_ENABLE_DEBUG_INFO = NO; 280 | MTL_FAST_MATH = YES; 281 | SDKROOT = iphoneos; 282 | SWIFT_COMPILATION_MODE = wholemodule; 283 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 284 | VALIDATE_PRODUCT = YES; 285 | }; 286 | name = Release; 287 | }; 288 | 86131C3B266C7F19007421A7 /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 292 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 293 | CODE_SIGN_STYLE = Automatic; 294 | DEVELOPMENT_TEAM = ZM68Y4E2HM; 295 | INFOPLIST_FILE = HalfScreenPresentation/Info.plist; 296 | LD_RUNPATH_SEARCH_PATHS = ( 297 | "$(inherited)", 298 | "@executable_path/Frameworks", 299 | ); 300 | PRODUCT_BUNDLE_IDENTIFIER = com.brontoxx.HalfScreenPresentation; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SWIFT_VERSION = 5.0; 303 | TARGETED_DEVICE_FAMILY = "1,2"; 304 | }; 305 | name = Debug; 306 | }; 307 | 86131C3C266C7F19007421A7 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 312 | CODE_SIGN_STYLE = Automatic; 313 | DEVELOPMENT_TEAM = ZM68Y4E2HM; 314 | INFOPLIST_FILE = HalfScreenPresentation/Info.plist; 315 | LD_RUNPATH_SEARCH_PATHS = ( 316 | "$(inherited)", 317 | "@executable_path/Frameworks", 318 | ); 319 | PRODUCT_BUNDLE_IDENTIFIER = com.brontoxx.HalfScreenPresentation; 320 | PRODUCT_NAME = "$(TARGET_NAME)"; 321 | SWIFT_VERSION = 5.0; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | }; 324 | name = Release; 325 | }; 326 | /* End XCBuildConfiguration section */ 327 | 328 | /* Begin XCConfigurationList section */ 329 | 86131C21266C7F0F007421A7 /* Build configuration list for PBXProject "HalfScreenPresentation" */ = { 330 | isa = XCConfigurationList; 331 | buildConfigurations = ( 332 | 86131C38266C7F19007421A7 /* Debug */, 333 | 86131C39266C7F19007421A7 /* Release */, 334 | ); 335 | defaultConfigurationIsVisible = 0; 336 | defaultConfigurationName = Release; 337 | }; 338 | 86131C3A266C7F19007421A7 /* Build configuration list for PBXNativeTarget "HalfScreenPresentation" */ = { 339 | isa = XCConfigurationList; 340 | buildConfigurations = ( 341 | 86131C3B266C7F19007421A7 /* Debug */, 342 | 86131C3C266C7F19007421A7 /* Release */, 343 | ); 344 | defaultConfigurationIsVisible = 0; 345 | defaultConfigurationName = Release; 346 | }; 347 | /* End XCConfigurationList section */ 348 | }; 349 | rootObject = 86131C1E266C7F0F007421A7 /* Project object */; 350 | } 351 | -------------------------------------------------------------------------------- /HalfScreenPresentation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /HalfScreenPresentation/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // HalfScreenPresentation 4 | // 5 | // Created by Hafiz on 06/06/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /HalfScreenPresentation/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 | -------------------------------------------------------------------------------- /HalfScreenPresentation/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /HalfScreenPresentation/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HalfScreenPresentation/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 | -------------------------------------------------------------------------------- /HalfScreenPresentation/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 | 24 | 25 | -------------------------------------------------------------------------------- /HalfScreenPresentation/CustomModalViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomModalViewController.swift 3 | // HalfScreenPresentation 4 | // 5 | // Created by Hafiz on 06/06/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class CustomModalViewController: UIViewController { 11 | // define lazy views 12 | lazy var titleLabel: UILabel = { 13 | let label = UILabel() 14 | label.text = "Get Started" 15 | label.font = .boldSystemFont(ofSize: 20) 16 | return label 17 | }() 18 | 19 | lazy var notesLabel: UILabel = { 20 | let label = UILabel() 21 | label.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sem fringilla ut morbi tincidunt augue interdum. \n\nUt morbi tincidunt augue interdum velit euismod in pellentesque massa. Pulvinar etiam non quam lacus suspendisse faucibus interdum posuere. Mi in nulla posuere sollicitudin aliquam ultrices sagittis orci a. Eget nullam non nisi est sit amet. Odio pellentesque diam volutpat commodo. Id eu nisl nunc mi ipsum faucibus vitae.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sem fringilla ut morbi tincidunt augue interdum. Ut morbi tincidunt augue interdum velit euismod in pellentesque massa." 22 | label.font = .systemFont(ofSize: 16) 23 | label.textColor = .darkGray 24 | label.numberOfLines = 0 25 | return label 26 | }() 27 | 28 | lazy var contentStackView: UIStackView = { 29 | let spacer = UIView() 30 | let stackView = UIStackView(arrangedSubviews: [titleLabel, notesLabel, spacer]) 31 | stackView.axis = .vertical 32 | stackView.spacing = 12.0 33 | return stackView 34 | }() 35 | 36 | lazy var containerView: UIView = { 37 | let view = UIView() 38 | view.backgroundColor = .white 39 | view.layer.cornerRadius = 16 40 | view.clipsToBounds = true 41 | return view 42 | }() 43 | 44 | let maxDimmedAlpha: CGFloat = 0.6 45 | lazy var dimmedView: UIView = { 46 | let view = UIView() 47 | view.backgroundColor = .black 48 | view.alpha = maxDimmedAlpha 49 | return view 50 | }() 51 | 52 | // Constants 53 | let defaultHeight: CGFloat = 300 54 | let dismissibleHeight: CGFloat = 200 55 | let maximumContainerHeight: CGFloat = UIScreen.main.bounds.height - 64 56 | // keep current new height, initial is default height 57 | var currentContainerHeight: CGFloat = 300 58 | 59 | // Dynamic container constraint 60 | var containerViewHeightConstraint: NSLayoutConstraint? 61 | var containerViewBottomConstraint: NSLayoutConstraint? 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | setupView() 66 | setupConstraints() 67 | // tap gesture on dimmed view to dismiss 68 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleCloseAction)) 69 | dimmedView.addGestureRecognizer(tapGesture) 70 | 71 | setupPanGesture() 72 | } 73 | 74 | @objc func handleCloseAction() { 75 | animateDismissView() 76 | } 77 | 78 | override func viewDidAppear(_ animated: Bool) { 79 | super.viewDidAppear(animated) 80 | animateShowDimmedView() 81 | animatePresentContainer() 82 | } 83 | 84 | func setupView() { 85 | view.backgroundColor = .clear 86 | } 87 | 88 | func setupConstraints() { 89 | // Add subviews 90 | view.addSubview(dimmedView) 91 | view.addSubview(containerView) 92 | dimmedView.translatesAutoresizingMaskIntoConstraints = false 93 | containerView.translatesAutoresizingMaskIntoConstraints = false 94 | 95 | containerView.addSubview(contentStackView) 96 | contentStackView.translatesAutoresizingMaskIntoConstraints = false 97 | 98 | // Set static constraints 99 | NSLayoutConstraint.activate([ 100 | // set dimmedView edges to superview 101 | dimmedView.topAnchor.constraint(equalTo: view.topAnchor), 102 | dimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 103 | dimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 104 | dimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 105 | // set container static constraint (trailing & leading) 106 | containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 107 | containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 108 | // content stackView 109 | contentStackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 32), 110 | contentStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20), 111 | contentStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), 112 | contentStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), 113 | ]) 114 | 115 | // Set dynamic constraints 116 | // First, set container to default height 117 | // after panning, the height can expand 118 | containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: defaultHeight) 119 | 120 | // By setting the height to default height, the container will be hide below the bottom anchor view 121 | // Later, will bring it up by set it to 0 122 | // set the constant to default height to bring it down again 123 | containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: defaultHeight) 124 | // Activate constraints 125 | containerViewHeightConstraint?.isActive = true 126 | containerViewBottomConstraint?.isActive = true 127 | } 128 | 129 | func setupPanGesture() { 130 | // add pan gesture recognizer to the view controller's view (the whole screen) 131 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(gesture:))) 132 | // change to false to immediately listen on gesture movement 133 | panGesture.delaysTouchesBegan = false 134 | panGesture.delaysTouchesEnded = false 135 | view.addGestureRecognizer(panGesture) 136 | } 137 | 138 | // MARK: Pan gesture handler 139 | @objc func handlePanGesture(gesture: UIPanGestureRecognizer) { 140 | let translation = gesture.translation(in: view) 141 | // Drag to top will be minus value and vice versa 142 | print("Pan gesture y offset: \(translation.y)") 143 | 144 | // Get drag direction 145 | let isDraggingDown = translation.y > 0 146 | print("Dragging direction: \(isDraggingDown ? "going down" : "going up")") 147 | 148 | // New height is based on value of dragging plus current container height 149 | let newHeight = currentContainerHeight - translation.y 150 | 151 | // Handle based on gesture state 152 | switch gesture.state { 153 | case .changed: 154 | // This state will occur when user is dragging 155 | if newHeight < maximumContainerHeight { 156 | // Keep updating the height constraint 157 | containerViewHeightConstraint?.constant = newHeight 158 | // refresh layout 159 | view.layoutIfNeeded() 160 | } 161 | case .ended: 162 | // This happens when user stop drag, 163 | // so we will get the last height of container 164 | 165 | // Condition 1: If new height is below min, dismiss controller 166 | if newHeight < dismissibleHeight { 167 | self.animateDismissView() 168 | } 169 | else if newHeight < defaultHeight { 170 | // Condition 2: If new height is below default, animate back to default 171 | animateContainerHeight(defaultHeight) 172 | } 173 | else if newHeight < maximumContainerHeight && isDraggingDown { 174 | // Condition 3: If new height is below max and going down, set to default height 175 | animateContainerHeight(defaultHeight) 176 | } 177 | else if newHeight > defaultHeight && !isDraggingDown { 178 | // Condition 4: If new height is below max and going up, set to max height at top 179 | animateContainerHeight(maximumContainerHeight) 180 | } 181 | default: 182 | break 183 | } 184 | } 185 | 186 | func animateContainerHeight(_ height: CGFloat) { 187 | UIView.animate(withDuration: 0.4) { 188 | // Update container height 189 | self.containerViewHeightConstraint?.constant = height 190 | // Call this to trigger refresh constraint 191 | self.view.layoutIfNeeded() 192 | } 193 | // Save current height 194 | currentContainerHeight = height 195 | } 196 | 197 | // MARK: Present and dismiss animation 198 | func animatePresentContainer() { 199 | // update bottom constraint in animation block 200 | UIView.animate(withDuration: 0.3) { 201 | self.containerViewBottomConstraint?.constant = 0 202 | // call this to trigger refresh constraint 203 | self.view.layoutIfNeeded() 204 | } 205 | } 206 | 207 | func animateShowDimmedView() { 208 | dimmedView.alpha = 0 209 | UIView.animate(withDuration: 0.4) { 210 | self.dimmedView.alpha = self.maxDimmedAlpha 211 | } 212 | } 213 | 214 | func animateDismissView() { 215 | // hide blur view 216 | dimmedView.alpha = maxDimmedAlpha 217 | UIView.animate(withDuration: 0.4) { 218 | self.dimmedView.alpha = 0 219 | } completion: { _ in 220 | // once done, dismiss without animation 221 | self.dismiss(animated: false) 222 | } 223 | // hide main view by updating bottom constraint in animation block 224 | UIView.animate(withDuration: 0.3) { 225 | self.containerViewBottomConstraint?.constant = self.defaultHeight 226 | // call this to trigger refresh constraint 227 | self.view.layoutIfNeeded() 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /HalfScreenPresentation/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /HalfScreenPresentation/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // HalfScreenPresentation 4 | // 5 | // Created by Hafiz on 06/06/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /HalfScreenPresentation/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // HalfScreenPresentation 4 | // 5 | // Created by Hafiz on 06/06/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | // Defined UI views 13 | lazy var titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.text = "Lorem Ipsum" 16 | label.font = .boldSystemFont(ofSize: 32) 17 | return label 18 | }() 19 | 20 | lazy var textView: UITextView = { 21 | let textView = UITextView(frame: .zero) 22 | textView.font = UIFont.systemFont(ofSize: 16) 23 | textView.isEditable = false 24 | textView.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sem fringilla ut morbi tincidunt augue interdum. Ut morbi tincidunt augue interdum velit euismod in pellentesque massa. Pulvinar etiam non quam lacus suspendisse faucibus interdum posuere. Mi in nulla posuere sollicitudin aliquam ultrices sagittis orci a. Eget nullam non nisi est sit amet. Odio pellentesque diam volutpat commodo. Id eu nisl nunc mi ipsum faucibus vitae. " 25 | return textView 26 | }() 27 | 28 | lazy var registerButton: UIButton = { 29 | let button = UIButton() 30 | button.setTitle("Get Started", for: .normal) 31 | button.setTitleColor(.white, for: .normal) 32 | button.backgroundColor = view.tintColor 33 | button.layer.cornerRadius = 8 34 | button.clipsToBounds = true 35 | return button 36 | }() 37 | 38 | lazy var containerStackView: UIStackView = { 39 | let spacer = UIView() 40 | let stackView = UIStackView(arrangedSubviews: [titleLabel, textView, spacer, registerButton]) 41 | stackView.axis = .vertical 42 | stackView.spacing = 16.0 43 | return stackView 44 | }() 45 | 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | setupView() 49 | setupConstraints() 50 | // Add action 51 | registerButton.addTarget(self, action: #selector(presentModalController), for: .touchUpInside) 52 | } 53 | 54 | func setupView() { 55 | // cosmetics 56 | view.backgroundColor = .systemBackground 57 | } 58 | 59 | // Add subviews and set constraints 60 | func setupConstraints() { 61 | view.addSubview(containerStackView) 62 | containerStackView.translatesAutoresizingMaskIntoConstraints = false 63 | 64 | let safeArea = view.safeAreaLayoutGuide 65 | // Call .activate method to enable the defined constraints 66 | NSLayoutConstraint.activate([ 67 | // 6. Set containerStackView edges to superview with 24 spacing 68 | containerStackView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 24), 69 | containerStackView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -24), 70 | containerStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 24), 71 | containerStackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -24), 72 | // 7. Set button height 73 | registerButton.heightAnchor.constraint(equalToConstant: 50) 74 | ]) 75 | } 76 | 77 | // To be updated 78 | @objc func presentModalController() { 79 | let vc = CustomModalViewController() 80 | vc.modalPresentationStyle = .overCurrentContext 81 | // keep false 82 | // modal animation will be handled in VC itself 83 | self.present(vc, animated: false) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmhafiz/CustomModalVC/ea38b7ee3442701fe49c4161d0677da5344693f4/img.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | iOS simple project to create half-screen modal view controller with pan / draggable (expand and dismiss animation) 3 | 4 | Visit my [article](https://betterprogramming.pub/how-to-present-a-bottom-sheet-view-controller-in-ios-a5a3e2047af9) on medium.com for detail explanation. Thanks for your claps and share! 5 | 6 | ![screenshot](screenshot.gif) 7 | 8 | ### Specification 9 | 10 | - Xcode 12+ 11 | - Swift 5 12 | 13 | ### UI Inspiration 14 | ![img](img.png) 15 | 16 | ### License 17 | Licensed under the [MIT license](http://opensource.org/licenses/MIT) 18 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmhafiz/CustomModalVC/ea38b7ee3442701fe49c4161d0677da5344693f4/screenshot.gif --------------------------------------------------------------------------------