├── GitHubImages ├── containerStoryboard.png ├── freeform.png ├── leftMenuConstraints.png ├── leftMenuVid.gif ├── rightConstraints.png ├── runtimeAttributes.png ├── scrollingEnabled.png ├── setConstraintsFromContainers.gif └── tabbedApplicationIcon.png ├── LeftSlideoutMenu.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── LeftSlideoutMenu.xccheckout └── xcuserdata │ └── robertchen.xcuserdatad │ └── xcschemes │ ├── LeftSlideoutMenu.xcscheme │ └── xcschememanagement.plist ├── LeftSlideoutMenu ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── ContainerVC.swift ├── FirstViewController.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── first.imageset │ │ ├── Contents.json │ │ └── first.pdf │ └── second.imageset │ │ ├── Contents.json │ │ └── second.pdf ├── Info.plist ├── LeftMenu.swift ├── ModalWindow.swift ├── SecondViewController.swift └── TabVCTemplate.swift ├── LeftSlideoutMenuTests ├── Info.plist └── LeftSlideoutMenuTests.swift └── README.md /GitHubImages/containerStoryboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/containerStoryboard.png -------------------------------------------------------------------------------- /GitHubImages/freeform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/freeform.png -------------------------------------------------------------------------------- /GitHubImages/leftMenuConstraints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/leftMenuConstraints.png -------------------------------------------------------------------------------- /GitHubImages/leftMenuVid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/leftMenuVid.gif -------------------------------------------------------------------------------- /GitHubImages/rightConstraints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/rightConstraints.png -------------------------------------------------------------------------------- /GitHubImages/runtimeAttributes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/runtimeAttributes.png -------------------------------------------------------------------------------- /GitHubImages/scrollingEnabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/scrollingEnabled.png -------------------------------------------------------------------------------- /GitHubImages/setConstraintsFromContainers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/setConstraintsFromContainers.gif -------------------------------------------------------------------------------- /GitHubImages/tabbedApplicationIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/GitHubImages/tabbedApplicationIcon.png -------------------------------------------------------------------------------- /LeftSlideoutMenu.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FCDFC0851B2F7F110015115E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCDFC0841B2F7F110015115E /* AppDelegate.swift */; }; 11 | FCDFC0871B2F7F110015115E /* FirstViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCDFC0861B2F7F110015115E /* FirstViewController.swift */; }; 12 | FCDFC0891B2F7F110015115E /* SecondViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCDFC0881B2F7F110015115E /* SecondViewController.swift */; }; 13 | FCDFC08C1B2F7F110015115E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FCDFC08A1B2F7F110015115E /* Main.storyboard */; }; 14 | FCDFC08E1B2F7F120015115E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FCDFC08D1B2F7F120015115E /* Images.xcassets */; }; 15 | FCDFC0911B2F7F120015115E /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = FCDFC08F1B2F7F120015115E /* LaunchScreen.xib */; }; 16 | FCDFC09D1B2F7F120015115E /* LeftSlideoutMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCDFC09C1B2F7F120015115E /* LeftSlideoutMenuTests.swift */; }; 17 | FCDFC0A71B2F87B00015115E /* ContainerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCDFC0A61B2F87B00015115E /* ContainerVC.swift */; }; 18 | FCE2F37B1B7248BC00773729 /* LeftMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE2F37A1B7248BC00773729 /* LeftMenu.swift */; }; 19 | FCE2F37D1B724B6B00773729 /* ModalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE2F37C1B724B6B00773729 /* ModalWindow.swift */; }; 20 | FCE2F37F1B724C4D00773729 /* TabVCTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE2F37E1B724C4D00773729 /* TabVCTemplate.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | FCDFC0971B2F7F120015115E /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = FCDFC0771B2F7F110015115E /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = FCDFC07E1B2F7F110015115E; 29 | remoteInfo = LeftSlideoutMenu; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | FCDFC07F1B2F7F110015115E /* LeftSlideoutMenu.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LeftSlideoutMenu.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | FCDFC0831B2F7F110015115E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | FCDFC0841B2F7F110015115E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | FCDFC0861B2F7F110015115E /* FirstViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstViewController.swift; sourceTree = ""; }; 38 | FCDFC0881B2F7F110015115E /* SecondViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = ""; }; 39 | FCDFC08B1B2F7F110015115E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 40 | FCDFC08D1B2F7F120015115E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 41 | FCDFC0901B2F7F120015115E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 42 | FCDFC0961B2F7F120015115E /* LeftSlideoutMenuTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LeftSlideoutMenuTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | FCDFC09B1B2F7F120015115E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | FCDFC09C1B2F7F120015115E /* LeftSlideoutMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftSlideoutMenuTests.swift; sourceTree = ""; }; 45 | FCDFC0A61B2F87B00015115E /* ContainerVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerVC.swift; sourceTree = ""; }; 46 | FCE2F37A1B7248BC00773729 /* LeftMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeftMenu.swift; sourceTree = ""; }; 47 | FCE2F37C1B724B6B00773729 /* ModalWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalWindow.swift; sourceTree = ""; }; 48 | FCE2F37E1B724C4D00773729 /* TabVCTemplate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabVCTemplate.swift; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | FCDFC07C1B2F7F110015115E /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | FCDFC0931B2F7F120015115E /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXFrameworksBuildPhase section */ 67 | 68 | /* Begin PBXGroup section */ 69 | FCDFC0761B2F7F110015115E = { 70 | isa = PBXGroup; 71 | children = ( 72 | FCDFC0811B2F7F110015115E /* LeftSlideoutMenu */, 73 | FCDFC0991B2F7F120015115E /* LeftSlideoutMenuTests */, 74 | FCDFC0801B2F7F110015115E /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | FCDFC0801B2F7F110015115E /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | FCDFC07F1B2F7F110015115E /* LeftSlideoutMenu.app */, 82 | FCDFC0961B2F7F120015115E /* LeftSlideoutMenuTests.xctest */, 83 | ); 84 | name = Products; 85 | sourceTree = ""; 86 | }; 87 | FCDFC0811B2F7F110015115E /* LeftSlideoutMenu */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | FCDFC0841B2F7F110015115E /* AppDelegate.swift */, 91 | FCDFC0A61B2F87B00015115E /* ContainerVC.swift */, 92 | FCDFC0861B2F7F110015115E /* FirstViewController.swift */, 93 | FCDFC0881B2F7F110015115E /* SecondViewController.swift */, 94 | FCE2F37E1B724C4D00773729 /* TabVCTemplate.swift */, 95 | FCE2F37A1B7248BC00773729 /* LeftMenu.swift */, 96 | FCE2F37C1B724B6B00773729 /* ModalWindow.swift */, 97 | FCDFC08A1B2F7F110015115E /* Main.storyboard */, 98 | FCDFC08D1B2F7F120015115E /* Images.xcassets */, 99 | FCDFC08F1B2F7F120015115E /* LaunchScreen.xib */, 100 | FCDFC0821B2F7F110015115E /* Supporting Files */, 101 | ); 102 | path = LeftSlideoutMenu; 103 | sourceTree = ""; 104 | }; 105 | FCDFC0821B2F7F110015115E /* Supporting Files */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | FCDFC0831B2F7F110015115E /* Info.plist */, 109 | ); 110 | name = "Supporting Files"; 111 | sourceTree = ""; 112 | }; 113 | FCDFC0991B2F7F120015115E /* LeftSlideoutMenuTests */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | FCDFC09C1B2F7F120015115E /* LeftSlideoutMenuTests.swift */, 117 | FCDFC09A1B2F7F120015115E /* Supporting Files */, 118 | ); 119 | path = LeftSlideoutMenuTests; 120 | sourceTree = ""; 121 | }; 122 | FCDFC09A1B2F7F120015115E /* Supporting Files */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | FCDFC09B1B2F7F120015115E /* Info.plist */, 126 | ); 127 | name = "Supporting Files"; 128 | sourceTree = ""; 129 | }; 130 | /* End PBXGroup section */ 131 | 132 | /* Begin PBXNativeTarget section */ 133 | FCDFC07E1B2F7F110015115E /* LeftSlideoutMenu */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = FCDFC0A01B2F7F120015115E /* Build configuration list for PBXNativeTarget "LeftSlideoutMenu" */; 136 | buildPhases = ( 137 | FCDFC07B1B2F7F110015115E /* Sources */, 138 | FCDFC07C1B2F7F110015115E /* Frameworks */, 139 | FCDFC07D1B2F7F110015115E /* Resources */, 140 | ); 141 | buildRules = ( 142 | ); 143 | dependencies = ( 144 | ); 145 | name = LeftSlideoutMenu; 146 | productName = LeftSlideoutMenu; 147 | productReference = FCDFC07F1B2F7F110015115E /* LeftSlideoutMenu.app */; 148 | productType = "com.apple.product-type.application"; 149 | }; 150 | FCDFC0951B2F7F120015115E /* LeftSlideoutMenuTests */ = { 151 | isa = PBXNativeTarget; 152 | buildConfigurationList = FCDFC0A31B2F7F120015115E /* Build configuration list for PBXNativeTarget "LeftSlideoutMenuTests" */; 153 | buildPhases = ( 154 | FCDFC0921B2F7F120015115E /* Sources */, 155 | FCDFC0931B2F7F120015115E /* Frameworks */, 156 | FCDFC0941B2F7F120015115E /* Resources */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | FCDFC0981B2F7F120015115E /* PBXTargetDependency */, 162 | ); 163 | name = LeftSlideoutMenuTests; 164 | productName = LeftSlideoutMenuTests; 165 | productReference = FCDFC0961B2F7F120015115E /* LeftSlideoutMenuTests.xctest */; 166 | productType = "com.apple.product-type.bundle.unit-test"; 167 | }; 168 | /* End PBXNativeTarget section */ 169 | 170 | /* Begin PBXProject section */ 171 | FCDFC0771B2F7F110015115E /* Project object */ = { 172 | isa = PBXProject; 173 | attributes = { 174 | LastSwiftMigration = 0720; 175 | LastSwiftUpdateCheck = 0720; 176 | LastUpgradeCheck = 0910; 177 | ORGANIZATIONNAME = "Thorn Technologies"; 178 | TargetAttributes = { 179 | FCDFC07E1B2F7F110015115E = { 180 | CreatedOnToolsVersion = 6.3.1; 181 | ProvisioningStyle = Manual; 182 | }; 183 | FCDFC0951B2F7F120015115E = { 184 | CreatedOnToolsVersion = 6.3.1; 185 | TestTargetID = FCDFC07E1B2F7F110015115E; 186 | }; 187 | }; 188 | }; 189 | buildConfigurationList = FCDFC07A1B2F7F110015115E /* Build configuration list for PBXProject "LeftSlideoutMenu" */; 190 | compatibilityVersion = "Xcode 3.2"; 191 | developmentRegion = English; 192 | hasScannedForEncodings = 0; 193 | knownRegions = ( 194 | en, 195 | Base, 196 | ); 197 | mainGroup = FCDFC0761B2F7F110015115E; 198 | productRefGroup = FCDFC0801B2F7F110015115E /* Products */; 199 | projectDirPath = ""; 200 | projectRoot = ""; 201 | targets = ( 202 | FCDFC07E1B2F7F110015115E /* LeftSlideoutMenu */, 203 | FCDFC0951B2F7F120015115E /* LeftSlideoutMenuTests */, 204 | ); 205 | }; 206 | /* End PBXProject section */ 207 | 208 | /* Begin PBXResourcesBuildPhase section */ 209 | FCDFC07D1B2F7F110015115E /* Resources */ = { 210 | isa = PBXResourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | FCDFC08C1B2F7F110015115E /* Main.storyboard in Resources */, 214 | FCDFC0911B2F7F120015115E /* LaunchScreen.xib in Resources */, 215 | FCDFC08E1B2F7F120015115E /* Images.xcassets in Resources */, 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | FCDFC0941B2F7F120015115E /* Resources */ = { 220 | isa = PBXResourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | /* End PBXResourcesBuildPhase section */ 227 | 228 | /* Begin PBXSourcesBuildPhase section */ 229 | FCDFC07B1B2F7F110015115E /* Sources */ = { 230 | isa = PBXSourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | FCE2F37B1B7248BC00773729 /* LeftMenu.swift in Sources */, 234 | FCDFC0A71B2F87B00015115E /* ContainerVC.swift in Sources */, 235 | FCDFC0891B2F7F110015115E /* SecondViewController.swift in Sources */, 236 | FCE2F37D1B724B6B00773729 /* ModalWindow.swift in Sources */, 237 | FCE2F37F1B724C4D00773729 /* TabVCTemplate.swift in Sources */, 238 | FCDFC0851B2F7F110015115E /* AppDelegate.swift in Sources */, 239 | FCDFC0871B2F7F110015115E /* FirstViewController.swift in Sources */, 240 | ); 241 | runOnlyForDeploymentPostprocessing = 0; 242 | }; 243 | FCDFC0921B2F7F120015115E /* Sources */ = { 244 | isa = PBXSourcesBuildPhase; 245 | buildActionMask = 2147483647; 246 | files = ( 247 | FCDFC09D1B2F7F120015115E /* LeftSlideoutMenuTests.swift in Sources */, 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | /* End PBXSourcesBuildPhase section */ 252 | 253 | /* Begin PBXTargetDependency section */ 254 | FCDFC0981B2F7F120015115E /* PBXTargetDependency */ = { 255 | isa = PBXTargetDependency; 256 | target = FCDFC07E1B2F7F110015115E /* LeftSlideoutMenu */; 257 | targetProxy = FCDFC0971B2F7F120015115E /* PBXContainerItemProxy */; 258 | }; 259 | /* End PBXTargetDependency section */ 260 | 261 | /* Begin PBXVariantGroup section */ 262 | FCDFC08A1B2F7F110015115E /* Main.storyboard */ = { 263 | isa = PBXVariantGroup; 264 | children = ( 265 | FCDFC08B1B2F7F110015115E /* Base */, 266 | ); 267 | name = Main.storyboard; 268 | sourceTree = ""; 269 | }; 270 | FCDFC08F1B2F7F120015115E /* LaunchScreen.xib */ = { 271 | isa = PBXVariantGroup; 272 | children = ( 273 | FCDFC0901B2F7F120015115E /* Base */, 274 | ); 275 | name = LaunchScreen.xib; 276 | sourceTree = ""; 277 | }; 278 | /* End PBXVariantGroup section */ 279 | 280 | /* Begin XCBuildConfiguration section */ 281 | FCDFC09E1B2F7F120015115E /* Debug */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 286 | CLANG_CXX_LIBRARY = "libc++"; 287 | CLANG_ENABLE_MODULES = YES; 288 | CLANG_ENABLE_OBJC_ARC = YES; 289 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 290 | CLANG_WARN_BOOL_CONVERSION = YES; 291 | CLANG_WARN_COMMA = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 300 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 301 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 302 | CLANG_WARN_STRICT_PROTOTYPES = YES; 303 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 304 | CLANG_WARN_UNREACHABLE_CODE = YES; 305 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 306 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 307 | COPY_PHASE_STRIP = NO; 308 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 309 | ENABLE_STRICT_OBJC_MSGSEND = YES; 310 | ENABLE_TESTABILITY = YES; 311 | GCC_C_LANGUAGE_STANDARD = gnu99; 312 | GCC_DYNAMIC_NO_PIC = NO; 313 | GCC_NO_COMMON_BLOCKS = YES; 314 | GCC_OPTIMIZATION_LEVEL = 0; 315 | GCC_PREPROCESSOR_DEFINITIONS = ( 316 | "DEBUG=1", 317 | "$(inherited)", 318 | ); 319 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 320 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 321 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 322 | GCC_WARN_UNDECLARED_SELECTOR = YES; 323 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 324 | GCC_WARN_UNUSED_FUNCTION = YES; 325 | GCC_WARN_UNUSED_VARIABLE = YES; 326 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 327 | MTL_ENABLE_DEBUG_INFO = YES; 328 | ONLY_ACTIVE_ARCH = YES; 329 | SDKROOT = iphoneos; 330 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 331 | SWIFT_VERSION = 4.0; 332 | TARGETED_DEVICE_FAMILY = "1,2"; 333 | }; 334 | name = Debug; 335 | }; 336 | FCDFC09F1B2F7F120015115E /* Release */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ALWAYS_SEARCH_USER_PATHS = NO; 340 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 341 | CLANG_CXX_LIBRARY = "libc++"; 342 | CLANG_ENABLE_MODULES = YES; 343 | CLANG_ENABLE_OBJC_ARC = YES; 344 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 345 | CLANG_WARN_BOOL_CONVERSION = YES; 346 | CLANG_WARN_COMMA = YES; 347 | CLANG_WARN_CONSTANT_CONVERSION = YES; 348 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 349 | CLANG_WARN_EMPTY_BODY = YES; 350 | CLANG_WARN_ENUM_CONVERSION = YES; 351 | CLANG_WARN_INFINITE_RECURSION = YES; 352 | CLANG_WARN_INT_CONVERSION = YES; 353 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 356 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 357 | CLANG_WARN_STRICT_PROTOTYPES = YES; 358 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 362 | COPY_PHASE_STRIP = NO; 363 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 364 | ENABLE_NS_ASSERTIONS = NO; 365 | ENABLE_STRICT_OBJC_MSGSEND = YES; 366 | GCC_C_LANGUAGE_STANDARD = gnu99; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 369 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 370 | GCC_WARN_UNDECLARED_SELECTOR = YES; 371 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 372 | GCC_WARN_UNUSED_FUNCTION = YES; 373 | GCC_WARN_UNUSED_VARIABLE = YES; 374 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 375 | MTL_ENABLE_DEBUG_INFO = NO; 376 | SDKROOT = iphoneos; 377 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 378 | SWIFT_VERSION = 4.0; 379 | TARGETED_DEVICE_FAMILY = "1,2"; 380 | VALIDATE_PRODUCT = YES; 381 | }; 382 | name = Release; 383 | }; 384 | FCDFC0A11B2F7F120015115E /* Debug */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 388 | CODE_SIGN_STYLE = Manual; 389 | DEVELOPMENT_TEAM = ""; 390 | INFOPLIST_FILE = LeftSlideoutMenu/Info.plist; 391 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 392 | PRODUCT_BUNDLE_IDENTIFIER = "com.thorntech.$(PRODUCT_NAME:rfc1034identifier)"; 393 | PRODUCT_NAME = "$(TARGET_NAME)"; 394 | PROVISIONING_PROFILE_SPECIFIER = ""; 395 | SWIFT_VERSION = 4.0; 396 | }; 397 | name = Debug; 398 | }; 399 | FCDFC0A21B2F7F120015115E /* Release */ = { 400 | isa = XCBuildConfiguration; 401 | buildSettings = { 402 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 403 | CODE_SIGN_STYLE = Manual; 404 | DEVELOPMENT_TEAM = ""; 405 | INFOPLIST_FILE = LeftSlideoutMenu/Info.plist; 406 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 407 | PRODUCT_BUNDLE_IDENTIFIER = "com.thorntech.$(PRODUCT_NAME:rfc1034identifier)"; 408 | PRODUCT_NAME = "$(TARGET_NAME)"; 409 | PROVISIONING_PROFILE_SPECIFIER = ""; 410 | SWIFT_VERSION = 4.0; 411 | }; 412 | name = Release; 413 | }; 414 | FCDFC0A41B2F7F120015115E /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | buildSettings = { 417 | BUNDLE_LOADER = "$(TEST_HOST)"; 418 | FRAMEWORK_SEARCH_PATHS = ( 419 | "$(SDKROOT)/Developer/Library/Frameworks", 420 | "$(inherited)", 421 | ); 422 | GCC_PREPROCESSOR_DEFINITIONS = ( 423 | "DEBUG=1", 424 | "$(inherited)", 425 | ); 426 | INFOPLIST_FILE = LeftSlideoutMenuTests/Info.plist; 427 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 428 | PRODUCT_BUNDLE_IDENTIFIER = "com.thorntech.$(PRODUCT_NAME:rfc1034identifier)"; 429 | PRODUCT_NAME = "$(TARGET_NAME)"; 430 | SWIFT_VERSION = 4.0; 431 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LeftSlideoutMenu.app/LeftSlideoutMenu"; 432 | }; 433 | name = Debug; 434 | }; 435 | FCDFC0A51B2F7F120015115E /* Release */ = { 436 | isa = XCBuildConfiguration; 437 | buildSettings = { 438 | BUNDLE_LOADER = "$(TEST_HOST)"; 439 | FRAMEWORK_SEARCH_PATHS = ( 440 | "$(SDKROOT)/Developer/Library/Frameworks", 441 | "$(inherited)", 442 | ); 443 | INFOPLIST_FILE = LeftSlideoutMenuTests/Info.plist; 444 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 445 | PRODUCT_BUNDLE_IDENTIFIER = "com.thorntech.$(PRODUCT_NAME:rfc1034identifier)"; 446 | PRODUCT_NAME = "$(TARGET_NAME)"; 447 | SWIFT_VERSION = 4.0; 448 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LeftSlideoutMenu.app/LeftSlideoutMenu"; 449 | }; 450 | name = Release; 451 | }; 452 | /* End XCBuildConfiguration section */ 453 | 454 | /* Begin XCConfigurationList section */ 455 | FCDFC07A1B2F7F110015115E /* Build configuration list for PBXProject "LeftSlideoutMenu" */ = { 456 | isa = XCConfigurationList; 457 | buildConfigurations = ( 458 | FCDFC09E1B2F7F120015115E /* Debug */, 459 | FCDFC09F1B2F7F120015115E /* Release */, 460 | ); 461 | defaultConfigurationIsVisible = 0; 462 | defaultConfigurationName = Release; 463 | }; 464 | FCDFC0A01B2F7F120015115E /* Build configuration list for PBXNativeTarget "LeftSlideoutMenu" */ = { 465 | isa = XCConfigurationList; 466 | buildConfigurations = ( 467 | FCDFC0A11B2F7F120015115E /* Debug */, 468 | FCDFC0A21B2F7F120015115E /* Release */, 469 | ); 470 | defaultConfigurationIsVisible = 0; 471 | defaultConfigurationName = Release; 472 | }; 473 | FCDFC0A31B2F7F120015115E /* Build configuration list for PBXNativeTarget "LeftSlideoutMenuTests" */ = { 474 | isa = XCConfigurationList; 475 | buildConfigurations = ( 476 | FCDFC0A41B2F7F120015115E /* Debug */, 477 | FCDFC0A51B2F7F120015115E /* Release */, 478 | ); 479 | defaultConfigurationIsVisible = 0; 480 | defaultConfigurationName = Release; 481 | }; 482 | /* End XCConfigurationList section */ 483 | }; 484 | rootObject = FCDFC0771B2F7F110015115E /* Project object */; 485 | } 486 | -------------------------------------------------------------------------------- /LeftSlideoutMenu.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LeftSlideoutMenu.xcodeproj/project.xcworkspace/xcshareddata/LeftSlideoutMenu.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | FA89CF3F-5556-43FB-8DC2-286EEDCBEC50 9 | IDESourceControlProjectName 10 | LeftSlideoutMenu 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 34690A32E65B0D821D562FB56D89D8FC20C244CF 14 | https://github.com/ThornTechPublic/LeftSlideoutMenu.git 15 | 16 | IDESourceControlProjectPath 17 | LeftSlideoutMenu.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 34690A32E65B0D821D562FB56D89D8FC20C244CF 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/ThornTechPublic/LeftSlideoutMenu.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 34690A32E65B0D821D562FB56D89D8FC20C244CF 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 34690A32E65B0D821D562FB56D89D8FC20C244CF 36 | IDESourceControlWCCName 37 | LeftSlideoutMenu 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LeftSlideoutMenu.xcodeproj/xcuserdata/robertchen.xcuserdatad/xcschemes/LeftSlideoutMenu.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /LeftSlideoutMenu.xcodeproj/xcuserdata/robertchen.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LeftSlideoutMenu.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | FCDFC07E1B2F7F110015115E 16 | 17 | primary 18 | 19 | 20 | FCDFC0951B2F7F120015115E 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LeftSlideoutMenu 4 | // 5 | // Created by Robert Chen on 6/15/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | private func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // 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. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Helvetica 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 43 | 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 | 86 | 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 | 196 | 197 | 198 | 199 | 200 | 206 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/ContainerVC.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ContainerVC : UIViewController { 4 | 5 | // This value matches the left menu's width in the Storyboard 6 | let leftMenuWidth:CGFloat = 260 7 | 8 | // Need a handle to the scrollView to open and close the menu 9 | @IBOutlet weak var scrollView: UIScrollView! 10 | 11 | override func viewDidLoad() { 12 | 13 | // Initially close menu programmatically. This needs to be done on the main thread initially in order to work. 14 | DispatchQueue.main.async() { 15 | self.closeMenu(animated: false) 16 | } 17 | 18 | // Tab bar controller's child pages have a top-left button toggles the menu 19 | NotificationCenter.default.addObserver(self, selector: #selector(ContainerVC.toggleMenu), name: NSNotification.Name(rawValue: "toggleMenu"), object: nil) 20 | 21 | NotificationCenter.default.addObserver(self, selector: #selector(ContainerVC.closeMenuViaNotification), name: NSNotification.Name(rawValue: "closeMenuViaNotification"), object: nil) 22 | 23 | // Close the menu when the device rotates 24 | NotificationCenter.default.addObserver(self, selector: #selector(ContainerVC.rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil) 25 | 26 | // LeftMenu sends openModalWindow 27 | NotificationCenter.default.addObserver(self, selector: #selector(ContainerVC.openModalWindow), name: NSNotification.Name(rawValue: "openModalWindow"), object: nil) 28 | 29 | } 30 | 31 | // Cleanup notifications added in viewDidLoad 32 | deinit { 33 | NotificationCenter.default.removeObserver(self) 34 | } 35 | 36 | @objc func openModalWindow() { 37 | performSegue(withIdentifier: "openModalWindow", sender: nil) 38 | } 39 | 40 | @objc func toggleMenu() { 41 | scrollView.contentOffset.x == 0 ? closeMenu() : openMenu() 42 | } 43 | 44 | // This wrapper function is necessary because 45 | // closeMenu params do not match up with Notification 46 | @objc func closeMenuViaNotification(){ 47 | closeMenu() 48 | } 49 | 50 | // Use scrollview content offset-x to slide the menu. 51 | func closeMenu(animated:Bool = true){ 52 | scrollView.setContentOffset(CGPoint(x: leftMenuWidth, y: 0), animated: animated) 53 | } 54 | 55 | // Open is the natural state of the menu because of how the storyboard is setup. 56 | func openMenu(){ 57 | print("opening menu") 58 | scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: true) 59 | } 60 | 61 | // see http://stackoverflow.com/questions/25666269/ios8-swift-how-to-detect-orientation-change 62 | // close the menu when rotating to landscape. 63 | // Note: you have to put this on the main queue in order for it to work 64 | @objc func rotated(){ 65 | if UIDeviceOrientationIsLandscape(UIDevice.current.orientation) { 66 | DispatchQueue.main.async() { 67 | print("closing menu on rotate") 68 | self.closeMenu() 69 | } 70 | } 71 | } 72 | 73 | } 74 | 75 | extension ContainerVC : UIScrollViewDelegate { 76 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 77 | print("scrollView.contentOffset.x:: \(scrollView.contentOffset.x)") 78 | } 79 | 80 | // http://www.4byte.cn/question/49110/uiscrollview-change-contentoffset-when-change-frame.html 81 | // When paging is enabled on a Scroll View, 82 | // a private method _adjustContentOffsetIfNecessary gets called, 83 | // presumably when present whatever controller is called. 84 | // The idea is to disable paging. 85 | // But we rely on paging to snap the slideout menu in place 86 | // (if you're relying on the built-in pan gesture). 87 | // So the approach is to keep paging disabled. 88 | // But enable it at the last minute during scrollViewWillBeginDragging. 89 | // And then turn it off once the scroll view stops moving. 90 | // 91 | // Approaches that don't work: 92 | // 1. automaticallyAdjustsScrollViewInsets -- don't bother 93 | // 2. overriding _adjustContentOffsetIfNecessary -- messing with private methods is a bad idea 94 | // 3. disable paging altogether. works, but at the loss of a feature 95 | // 4. nest the scrollview inside UIView, so UIKit doesn't mess with it. may have worked before, 96 | // but not anymore. 97 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 98 | scrollView.isPagingEnabled = true 99 | } 100 | 101 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 102 | scrollView.isPagingEnabled = false 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/FirstViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class FirstViewController: TabVCTemplate { 4 | 5 | override func viewDidLoad() { 6 | super.viewDidLoad() 7 | selectedTab = 0 8 | // do stuff here 9 | } 10 | 11 | @IBAction func toggleMenu(_ sender: Any) { 12 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "toggleMenu"), object: nil) 13 | } 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /LeftSlideoutMenu/Images.xcassets/first.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "first.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /LeftSlideoutMenu/Images.xcassets/first.imageset/first.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/LeftSlideoutMenu/Images.xcassets/first.imageset/first.pdf -------------------------------------------------------------------------------- /LeftSlideoutMenu/Images.xcassets/second.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "second.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /LeftSlideoutMenu/Images.xcassets/second.imageset/second.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/LeftSlideoutMenu/d45a435d2f4aaff72644017d037de0700bd22818/LeftSlideoutMenu/Images.xcassets/second.imageset/second.pdf -------------------------------------------------------------------------------- /LeftSlideoutMenu/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarTintParameters 34 | 35 | UINavigationBar 36 | 37 | Style 38 | UIBarStyleDefault 39 | Translucent 40 | 41 | 42 | 43 | UISupportedInterfaceOrientations 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UISupportedInterfaceOrientations~ipad 50 | 51 | UIInterfaceOrientationPortrait 52 | UIInterfaceOrientationPortraitUpsideDown 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/LeftMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeftMenu.swift 3 | // LeftSlideoutMenu 4 | // 5 | // Created by Robert Chen on 8/5/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LeftMenu : UITableViewController { 12 | 13 | let menuOptions = ["Open Modal", "Open Push"] 14 | 15 | } 16 | 17 | // MARK: - UITableViewDelegate methods 18 | 19 | extension LeftMenu { 20 | 21 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 22 | tableView.deselectRow(at: indexPath as IndexPath, animated: true) 23 | 24 | switch indexPath.row { 25 | case 0: 26 | // ContainerVC.swift listens for this 27 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "openModalWindow"), object: nil) 28 | case 1: 29 | // Both FirstViewController and SecondViewController listen for this 30 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "openPushWindow"), object: nil) 31 | default: 32 | print("indexPath.row:: \(indexPath.row)") 33 | } 34 | 35 | // also close the menu 36 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "closeMenuViaNotification"), object: nil) 37 | 38 | } 39 | 40 | } 41 | 42 | // MARK: - UITableViewDataSource methods 43 | 44 | extension LeftMenu { 45 | 46 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return 2 48 | } 49 | 50 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 51 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath as IndexPath) 52 | cell.textLabel?.text = menuOptions[indexPath.row] 53 | return cell 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/ModalWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalWindow.swift 3 | // LeftSlideoutMenu 4 | // 5 | // Created by Robert Chen on 8/5/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ModalWindow : UIViewController { 12 | 13 | @IBAction func close(_ sender: Any) { 14 | dismiss(animated: true, completion: nil) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/SecondViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SecondViewController: TabVCTemplate { 4 | 5 | override func viewDidLoad() { 6 | super.viewDidLoad() 7 | selectedTab = 1 8 | // do stuff here 9 | } 10 | 11 | @IBAction func toggleMenu(_ sender: Any) { 12 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "toggleMenu"), object: nil) 13 | } 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /LeftSlideoutMenu/TabVCTemplate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabVCTemplate.swift 3 | // LeftSlideoutMenu 4 | // 5 | // Created by Robert Chen on 8/5/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TabVCTemplate : UIViewController { 12 | 13 | // placeholder for the tab's index 14 | var selectedTab = 0 15 | 16 | override func viewDidLoad() { 17 | 18 | // Sent from LeftMenu 19 | NotificationCenter.default.addObserver(self, selector: #selector(TabVCTemplate.openPushWindow), name: NSNotification.Name(rawValue: "openPushWindow"), object: nil) 20 | 21 | } 22 | 23 | deinit { 24 | NotificationCenter.default.removeObserver(self) 25 | } 26 | 27 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 28 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "closeMenuViaNotification"), object: nil) 29 | view.endEditing(true) 30 | } 31 | 32 | @objc func openPushWindow(){ 33 | if tabBarController?.selectedIndex == selectedTab { 34 | performSegue(withIdentifier: "openPushWindow", sender: nil) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /LeftSlideoutMenuTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LeftSlideoutMenuTests/LeftSlideoutMenuTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeftSlideoutMenuTests.swift 3 | // LeftSlideoutMenuTests 4 | // 5 | // Created by Robert Chen on 6/15/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class LeftSlideoutMenuTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create a Left Slideout Menu with Swift 2 | 3 | ---- 4 | 5 | **Update**: Check out this [updated blog post](http://www.thorntech.com/2016/03/ios-tutorial-make-interactive-slide-menu-swift/) which uses a better approach to creating slideout menus. 6 | 7 | ---- 8 | 9 | This is a sample project with a left slideout menu, and a two-tabbed application for the main body. The majority of the setup is done in the Storyboard. 10 | 11 | This repo is tied to [this blog post](http://www.thorntech.com/2015/06/want-to-implement-a-slideout-menu-in-your-swift-app-heres-how/). 12 | 13 | ![animated gif demo](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/leftMenuVid.gif) 14 | 15 | ## Use Case 16 | 17 | * You lean heavily on Storyboard use, and value separation of UI code over portability. 18 | * You're using Swift, and want to avoid bridging Objective-C modules when possible 19 | 20 | ## Theory 21 | 22 | Apps tend to have a top level container like a `Tab Bar Controller` that cannot be embedded inside a View. The approach here is to wrap the main body and the left menu each inside a `Container View`. Now both of these elements can be arranged inside a wrapper `View Controller`. 23 | 24 | ![storyboard image](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/containerStoryboard.png) 25 | 26 | A `ScrollView` is used to simulate opening and closing the menu by shifting the left menu on/off-screen. 27 | 28 | Since Storyboard changes aren't really portable across projects, here is a recipe on how to recreate this. 29 | 30 | ## Instructions 31 | 32 | ### Create project 33 | 34 | Start with a `Tabbed Application`. This is just to demonstrate that even a `Tab Bar Controller` can be used with slideout menus. 35 | 36 | Uncheck the box `Use Size Classes` for now, simply for the sake of screen real estate. 37 | 38 | ![tabbed application](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/tabbedApplicationIcon.png) 39 | 40 | ### Containing View Controller 41 | 42 | Drop a `View Controller` onto the Storyboard. This is your entry point into the app, and acts as a big wrapper. Think of a [Laundry Sorting Cart](http://www.amazon.com/DecoBros-Heavy-Duty-3-Bag-Laundry-Sorter/dp/B00HLSTYNS/) that can hold baskets for lights and darks. Or in this case, a Menu and Tab Bar Controller. 43 | 44 | Check the box for `Is Initial View Controller`. 45 | 46 | Change the `Simulated Size` from `Fixed` to `Freeform`. Set the `Width` to 568 so we can fit the menu and Tab Bar side-by-side. 47 | 48 | ![use freeform size](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/freeform.png) 49 | 50 | Create a new Swift file and set this View Controller's class to ContainerVC. 51 | 52 | ``` 53 | import UIKit 54 | class ContainerVC : UIViewController { 55 | } 56 | ``` 57 | 58 | ### ScrollView 59 | 60 | Inside Container View Controller, drop in a `ScrollView` and add constraints in all directions. 61 | 62 | Check the box for `Scrolling Enabled`. This allows you to pan the screen to slide the menu. You might need to disable this if your app uses horizontally scrolling elements. 63 | 64 | Check the box for `Paging Enabled`. This snaps the menu to either the open or closed state. 65 | 66 | Uncheck the box for `Bounces`. You don't really want to scroll past the right edge of the Tab Bar Controller. 67 | 68 | ![scrollview properties](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/scrollingEnabled.png) 69 | 70 | Wire the IBOutlet to ContainerVC: 71 | 72 | ``` 73 | @IBOutlet weak var scrollView: UIScrollView! 74 | ``` 75 | 76 | ### Left Container 77 | 78 | The left container holds the menu, and is not quite the full width of the screen. 79 | 80 | Drag a `Container View` into the left side of the `ScrollView`. 81 | 82 | Add constraints for top, left, and right to the containing `ScrollView`. 83 | 84 | Hard-code the width to 260. 85 | 86 | Add a constraint for `Equal height` with the ContainerVC's embedded `View`. Note: don't constrain the height to the ScrollView. 87 | 88 | ![left container constraints](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/leftMenuConstraints.png) 89 | 90 | Delete the embedded `View Controller` that came with the `Container View`. 91 | 92 | Drop a new `Table View Controller` onto the Storyboard, and connect using an `embed` segue. 93 | 94 | ### Right Container 95 | 96 | The right container holds the main body of your app, namely the `Tab Bar Controller`. 97 | 98 | Drag a second `Container View` into the right side of the `ScrollView`. 99 | 100 | Add constraints to the containing `ScrollView` for top, right, and bottom. Connect horizontally to the left `Container View` you created earlier. 101 | 102 | Constraint both `Equal height` and `Equal width` to the ContainerVC's embedded `View`. Note: do not constrain these to the ScrollView. 103 | 104 | ![right container constraints](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/rightConstraints.png) 105 | 106 | Here's a short demo of setting the `Equal height` and `width` constraints to the `View`: 107 | 108 | ![animated gif of setting constraints](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/setConstraintsFromContainers.gif) 109 | 110 | Again, delete the embedded View Controller that came free with the `Container View`. Instead, create an `embed` segue to the `Tab Bar Controller`. 111 | 112 | To create a little visual separation between the two containers, add a `Runtime Attribute` to the `Right Container`. Add `layer.shadowOpacity` with a `number` of `0.8`. 113 | 114 | ![set runtime attributes](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/GitHubImages/runtimeAttributes.png) 115 | 116 | ### First and Second Tabs 117 | 118 | Embed each tab inside a `Navigation Controller`. Doing so gives you a free `Navigation Bar`. 119 | 120 | Drag a `Bar Button Item` into the top left corner of each `Navigation Bar`. 121 | 122 | Wire an `IBAction` to each controller. These will fire off a `NSNotification` to the great-grandfather `ContainerVC` to toggle the menu. 123 | 124 | ``` 125 | @IBAction func toggleMenu(sender: AnyObject) { 126 | NSNotificationCenter.defaultCenter().postNotificationName("toggleMenu", object: nil) 127 | } 128 | ``` 129 | 130 | ### ContainerVC 131 | 132 | Most of the Storyboard configuration is done. All that's left is to copy the code from [ContainerVC.swift](https://github.com/ThornTechPublic/LeftSlideoutMenu/blob/master/LeftSlideoutMenu/ContainerVC.swift). 133 | 134 | The code accomplishes the following: 135 | 136 | * Listen for the "toggleMenu" notification 137 | * Implement the toggleMenu method by opening or closing based on the current contentOffset.x 138 | * Open and close the menu by changing the contentOffset-x 139 | 140 | ### That's it 141 | 142 | Build and run, and hopefully you have a simple slideout left menu on which to build the rest of your app. 143 | 144 | ### Still confused? 145 | 146 | The `ScrollView` is pretty confusing to begin with. One common trick is to have a rigidly defined view on the inside. In this case, the left and right container views are constrained to the device (View Controller's embedded `View`) width and height. The `ScrollView` acts like a flexible wrapper. 147 | 148 | Here's an excellent video tutorial on [Ray Wenderlich](http://www.raywenderlich.com/video-tutorials#swiftscrollview) that covers the `ScrollView`. Note parts 13 and 14 cover the Slide-out-sidebar upon which the idea of using the ScrollView for a slideout menu is based upon. 149 | --------------------------------------------------------------------------------