├── .gitignore ├── LICENSE ├── README.md ├── VisualEffectsShadow.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── VisualEffectsShadow.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── VisualEffectsShadow ├── AppDelegate.swift ├── Assets.xcassets └── AppIcon.appiconset │ └── Contents.json ├── GripBarView.swift ├── Info.plist ├── MainViewController.swift ├── PassThroughView.swift ├── SampleCode.xcconfig └── UIImage+Shadow.swift /.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 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | ## AppCode 35 | .idea/ 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Brian Coyner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Add a drop shadow to a `UIVisualEffectView` 2 | 3 | This demo shows how to add a drop shadow to a `UIVisualEffectView`. The solution involves using/ creating a 9-part `UIImage` to represent the shadow. The effect is similar to the iOS 10 Maps app. 4 | 5 | ![ShadowImage](https://briancoyner.github.io/images/2017-05-15-uivisualeffectview-with-drop-shadow/screen-shot.png) 6 | 7 | ## Related Post 8 | 9 | View related post: https://briancoyner.github.io/articles/2017-05-15-uivisualeffectview-with-drop-shadow/ 10 | 11 | ## Sample Project Notes 12 | 13 | The sample project contains a simple iOS app and Xcode Playground. The creation of the 9-part shadow image is shared by the iOS app and Playground using an Embedded Framework. 14 | -------------------------------------------------------------------------------- /VisualEffectsShadow.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5D5D20CD1EC65ED5001B226E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5D20C41EC65ED5001B226E /* AppDelegate.swift */; }; 11 | 5D5D20CE1EC65ED5001B226E /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5D20C51EC65ED5001B226E /* MainViewController.swift */; }; 12 | 5D5D20D01EC65ED5001B226E /* PassThroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5D20C71EC65ED5001B226E /* PassThroughView.swift */; }; 13 | 5D5D20D11EC65ED5001B226E /* GripBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5D20C81EC65ED5001B226E /* GripBarView.swift */; }; 14 | 5D5D20D51EC65ED5001B226E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D5D20CC1EC65ED5001B226E /* Assets.xcassets */; }; 15 | 5D8645F41EC904C700F801FF /* UIImage+Shadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8645F21EC9045500F801FF /* UIImage+Shadow.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 5D1E54D31F4E573D003FD1ED /* SampleCode.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SampleCode.xcconfig; sourceTree = ""; }; 20 | 5D5D20C41EC65ED5001B226E /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | 5D5D20C51EC65ED5001B226E /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 22 | 5D5D20C71EC65ED5001B226E /* PassThroughView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassThroughView.swift; sourceTree = ""; }; 23 | 5D5D20C81EC65ED5001B226E /* GripBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GripBarView.swift; sourceTree = ""; }; 24 | 5D5D20CC1EC65ED5001B226E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 5D8645F21EC9045500F801FF /* UIImage+Shadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Shadow.swift"; sourceTree = ""; }; 26 | 5D9C1B0B21FCA4CB008F0F9B /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 27 | 5DBDB18F1EC65E24008C4DC1 /* VisualEffectsShadow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisualEffectsShadow.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 5DBDB19E1EC65E24008C4DC1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 5DBDB18C1EC65E24008C4DC1 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 5D9C1B0A21FCA4CB008F0F9B /* Frameworks */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | 5D9C1B0B21FCA4CB008F0F9B /* UIKit.framework */, 46 | ); 47 | name = Frameworks; 48 | sourceTree = ""; 49 | }; 50 | 5DBDB1861EC65E24008C4DC1 = { 51 | isa = PBXGroup; 52 | children = ( 53 | 5DBDB1911EC65E24008C4DC1 /* VisualEffectsShadow */, 54 | 5DBDB1901EC65E24008C4DC1 /* Products */, 55 | 5D9C1B0A21FCA4CB008F0F9B /* Frameworks */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | 5DBDB1901EC65E24008C4DC1 /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 5DBDB18F1EC65E24008C4DC1 /* VisualEffectsShadow.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | 5DBDB1911EC65E24008C4DC1 /* VisualEffectsShadow */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 5D5D20C41EC65ED5001B226E /* AppDelegate.swift */, 71 | 5D5D20C51EC65ED5001B226E /* MainViewController.swift */, 72 | 5D5D20C71EC65ED5001B226E /* PassThroughView.swift */, 73 | 5D5D20C81EC65ED5001B226E /* GripBarView.swift */, 74 | 5D8645F21EC9045500F801FF /* UIImage+Shadow.swift */, 75 | 5D5D20CC1EC65ED5001B226E /* Assets.xcassets */, 76 | 5DBDB19E1EC65E24008C4DC1 /* Info.plist */, 77 | 5D1E54D31F4E573D003FD1ED /* SampleCode.xcconfig */, 78 | ); 79 | path = VisualEffectsShadow; 80 | sourceTree = ""; 81 | }; 82 | /* End PBXGroup section */ 83 | 84 | /* Begin PBXNativeTarget section */ 85 | 5DBDB18E1EC65E24008C4DC1 /* VisualEffectsShadow */ = { 86 | isa = PBXNativeTarget; 87 | buildConfigurationList = 5DBDB1A11EC65E24008C4DC1 /* Build configuration list for PBXNativeTarget "VisualEffectsShadow" */; 88 | buildPhases = ( 89 | 5DBDB18B1EC65E24008C4DC1 /* Sources */, 90 | 5DBDB18C1EC65E24008C4DC1 /* Frameworks */, 91 | 5DBDB18D1EC65E24008C4DC1 /* Resources */, 92 | ); 93 | buildRules = ( 94 | ); 95 | dependencies = ( 96 | ); 97 | name = VisualEffectsShadow; 98 | productName = VisualEffectsShadow; 99 | productReference = 5DBDB18F1EC65E24008C4DC1 /* VisualEffectsShadow.app */; 100 | productType = "com.apple.product-type.application"; 101 | }; 102 | /* End PBXNativeTarget section */ 103 | 104 | /* Begin PBXProject section */ 105 | 5DBDB1871EC65E24008C4DC1 /* Project object */ = { 106 | isa = PBXProject; 107 | attributes = { 108 | LastSwiftUpdateCheck = 0830; 109 | LastUpgradeCheck = 1100; 110 | ORGANIZATIONNAME = "High Rail, LLC"; 111 | TargetAttributes = { 112 | 5DBDB18E1EC65E24008C4DC1 = { 113 | CreatedOnToolsVersion = 8.3.2; 114 | LastSwiftMigration = 1020; 115 | ProvisioningStyle = Automatic; 116 | }; 117 | }; 118 | }; 119 | buildConfigurationList = 5DBDB18A1EC65E24008C4DC1 /* Build configuration list for PBXProject "VisualEffectsShadow" */; 120 | compatibilityVersion = "Xcode 11.0"; 121 | developmentRegion = en; 122 | hasScannedForEncodings = 0; 123 | knownRegions = ( 124 | en, 125 | Base, 126 | ); 127 | mainGroup = 5DBDB1861EC65E24008C4DC1; 128 | productRefGroup = 5DBDB1901EC65E24008C4DC1 /* Products */; 129 | projectDirPath = ""; 130 | projectRoot = ""; 131 | targets = ( 132 | 5DBDB18E1EC65E24008C4DC1 /* VisualEffectsShadow */, 133 | ); 134 | }; 135 | /* End PBXProject section */ 136 | 137 | /* Begin PBXResourcesBuildPhase section */ 138 | 5DBDB18D1EC65E24008C4DC1 /* Resources */ = { 139 | isa = PBXResourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 5D5D20D51EC65ED5001B226E /* Assets.xcassets in Resources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXResourcesBuildPhase section */ 147 | 148 | /* Begin PBXSourcesBuildPhase section */ 149 | 5DBDB18B1EC65E24008C4DC1 /* Sources */ = { 150 | isa = PBXSourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | 5D5D20CD1EC65ED5001B226E /* AppDelegate.swift in Sources */, 154 | 5D5D20D11EC65ED5001B226E /* GripBarView.swift in Sources */, 155 | 5D8645F41EC904C700F801FF /* UIImage+Shadow.swift in Sources */, 156 | 5D5D20CE1EC65ED5001B226E /* MainViewController.swift in Sources */, 157 | 5D5D20D01EC65ED5001B226E /* PassThroughView.swift in Sources */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXSourcesBuildPhase section */ 162 | 163 | /* Begin XCBuildConfiguration section */ 164 | 5DBDB19F1EC65E24008C4DC1 /* Debug */ = { 165 | isa = XCBuildConfiguration; 166 | baseConfigurationReference = 5D1E54D31F4E573D003FD1ED /* SampleCode.xcconfig */; 167 | buildSettings = { 168 | ALWAYS_SEARCH_USER_PATHS = NO; 169 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 170 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 171 | CLANG_ENABLE_OBJC_ARC = YES; 172 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 173 | ENABLE_STRICT_OBJC_MSGSEND = YES; 174 | ENABLE_TESTABILITY = YES; 175 | GCC_DYNAMIC_NO_PIC = NO; 176 | GCC_NO_COMMON_BLOCKS = YES; 177 | GCC_PREPROCESSOR_DEFINITIONS = ( 178 | "DEBUG=1", 179 | "$(inherited)", 180 | ); 181 | SDKROOT = iphoneos; 182 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 183 | SWIFT_COMPILATION_MODE = singlefile; 184 | }; 185 | name = Debug; 186 | }; 187 | 5DBDB1A01EC65E24008C4DC1 /* Release */ = { 188 | isa = XCBuildConfiguration; 189 | baseConfigurationReference = 5D1E54D31F4E573D003FD1ED /* SampleCode.xcconfig */; 190 | buildSettings = { 191 | ALWAYS_SEARCH_USER_PATHS = NO; 192 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 193 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 194 | CLANG_ENABLE_OBJC_ARC = YES; 195 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 196 | ENABLE_NS_ASSERTIONS = NO; 197 | ENABLE_STRICT_OBJC_MSGSEND = YES; 198 | GCC_NO_COMMON_BLOCKS = YES; 199 | SDKROOT = iphoneos; 200 | SWIFT_COMPILATION_MODE = wholemodule; 201 | }; 202 | name = Release; 203 | }; 204 | 5DBDB1A21EC65E24008C4DC1 /* Debug */ = { 205 | isa = XCBuildConfiguration; 206 | baseConfigurationReference = 5D1E54D31F4E573D003FD1ED /* SampleCode.xcconfig */; 207 | buildSettings = { 208 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 209 | CODE_SIGN_STYLE = Automatic; 210 | DEVELOPMENT_TEAM = KC5B683642; 211 | INFOPLIST_FILE = VisualEffectsShadow/Info.plist; 212 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 213 | LD_RUNPATH_SEARCH_PATHS = ( 214 | "$(inherited)", 215 | "@executable_path/Frameworks", 216 | ); 217 | PRODUCT_BUNDLE_IDENTIFIER = com.highrailcompany.VisualEffectsShadow; 218 | PRODUCT_NAME = "$(TARGET_NAME)"; 219 | PROVISIONING_PROFILE_SPECIFIER = ""; 220 | TARGETED_DEVICE_FAMILY = "1,2"; 221 | }; 222 | name = Debug; 223 | }; 224 | 5DBDB1A31EC65E24008C4DC1 /* Release */ = { 225 | isa = XCBuildConfiguration; 226 | baseConfigurationReference = 5D1E54D31F4E573D003FD1ED /* SampleCode.xcconfig */; 227 | buildSettings = { 228 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 229 | CODE_SIGN_STYLE = Automatic; 230 | DEVELOPMENT_TEAM = KC5B683642; 231 | INFOPLIST_FILE = VisualEffectsShadow/Info.plist; 232 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 233 | LD_RUNPATH_SEARCH_PATHS = ( 234 | "$(inherited)", 235 | "@executable_path/Frameworks", 236 | ); 237 | PRODUCT_BUNDLE_IDENTIFIER = com.highrailcompany.VisualEffectsShadow; 238 | PRODUCT_NAME = "$(TARGET_NAME)"; 239 | PROVISIONING_PROFILE_SPECIFIER = ""; 240 | TARGETED_DEVICE_FAMILY = "1,2"; 241 | }; 242 | name = Release; 243 | }; 244 | /* End XCBuildConfiguration section */ 245 | 246 | /* Begin XCConfigurationList section */ 247 | 5DBDB18A1EC65E24008C4DC1 /* Build configuration list for PBXProject "VisualEffectsShadow" */ = { 248 | isa = XCConfigurationList; 249 | buildConfigurations = ( 250 | 5DBDB19F1EC65E24008C4DC1 /* Debug */, 251 | 5DBDB1A01EC65E24008C4DC1 /* Release */, 252 | ); 253 | defaultConfigurationIsVisible = 0; 254 | defaultConfigurationName = Release; 255 | }; 256 | 5DBDB1A11EC65E24008C4DC1 /* Build configuration list for PBXNativeTarget "VisualEffectsShadow" */ = { 257 | isa = XCConfigurationList; 258 | buildConfigurations = ( 259 | 5DBDB1A21EC65E24008C4DC1 /* Debug */, 260 | 5DBDB1A31EC65E24008C4DC1 /* Release */, 261 | ); 262 | defaultConfigurationIsVisible = 0; 263 | defaultConfigurationName = Release; 264 | }; 265 | /* End XCConfigurationList section */ 266 | }; 267 | rootObject = 5DBDB1871EC65E24008C4DC1 /* Project object */; 268 | } 269 | -------------------------------------------------------------------------------- /VisualEffectsShadow.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VisualEffectsShadow.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VisualEffectsShadow.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Latest 7 | 8 | 9 | -------------------------------------------------------------------------------- /VisualEffectsShadow.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VisualEffectsShadow.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VisualEffectsShadow/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Brian M Coyner on 5/11/17. 3 | // Copyright © 2017 Brian Coyner. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | import UIKit 8 | 9 | @UIApplicationMain 10 | final class AppDelegate: UIResponder, UIApplicationDelegate { 11 | var window: UIWindow? = UIWindow() 12 | } 13 | 14 | extension AppDelegate { 15 | 16 | func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | let mainViewController = MainViewController() 18 | window?.rootViewController = UINavigationController(rootViewController: mainViewController) 19 | window?.makeKeyAndVisible() 20 | 21 | return true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /VisualEffectsShadow/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "size" : "20x20", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "size" : "20x20", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "size" : "29x29", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "29x29", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "40x40", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "40x40", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "76x76", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "76x76", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "83.5x83.5", 46 | "scale" : "2x" 47 | }, 48 | { 49 | "idiom" : "ios-marketing", 50 | "size" : "1024x1024", 51 | "scale" : "1x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /VisualEffectsShadow/GripBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Brian M Coyner on 4/17/17. 3 | // Copyright © 2017 Brian Coyner. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | import UIKit 8 | 9 | /// A simple view subclass that draws a small "capsule" in the center of the view. 10 | 11 | final class GripBarView: UIView { 12 | 13 | fileprivate lazy var barLayer = self.lazyBarLayer() 14 | fileprivate lazy var separatorLine = self.lazySeparatorLine() 15 | 16 | init() { 17 | super.init(frame: CGRect()) 18 | 19 | selfInit() 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | super.init(coder: aDecoder) 24 | 25 | selfInit() 26 | } 27 | 28 | private func selfInit() { 29 | layer.addSublayer(barLayer) 30 | 31 | addSubview(separatorLine) 32 | NSLayoutConstraint.activate([ 33 | separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor), 34 | separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), 35 | separatorLine.topAnchor.constraint(equalTo: topAnchor), 36 | separatorLine.heightAnchor.constraint(equalToConstant: 0.5) 37 | ]) 38 | } 39 | } 40 | 41 | extension GripBarView { 42 | 43 | override func layoutSubviews() { 44 | super.layoutSubviews() 45 | 46 | let width = bounds.width * 0.15 47 | let height: CGFloat = 4.0 48 | let rect = CGRect(x: bounds.midX - (width / 2.0), y: bounds.midY - (height / 2.0), width: width, height: height) 49 | barLayer.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height / 2.0).cgPath 50 | } 51 | 52 | override func tintColorDidChange() { 53 | super.tintColorDidChange() 54 | 55 | barLayer.fillColor = tintColor.cgColor 56 | } 57 | } 58 | 59 | extension GripBarView { 60 | 61 | fileprivate func lazySeparatorLine() -> UIView { 62 | let view = UIView() 63 | view.translatesAutoresizingMaskIntoConstraints = false 64 | view.backgroundColor = .separator 65 | 66 | return view 67 | } 68 | 69 | fileprivate func lazyBarLayer() -> CAShapeLayer { 70 | let layer = CAShapeLayer() 71 | layer.fillColor = tintColor.cgColor 72 | 73 | return layer 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /VisualEffectsShadow/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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /VisualEffectsShadow/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Brian M Coyner on 5/11/17. 3 | // Copyright © 2017 Brian Coyner. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | import UIKit 8 | import MapKit 9 | 10 | /// A "demo" view controller with the following: 11 | /// - full screen `MKMapView` 12 | /// - a resizable `PassThroughView` centered over the map view 13 | /// 14 | /// You can switch the map type between `.standard` and `.hybrid` by tapping a "Switch Map Type" button. 15 | /// - `.standard` renders the visual effect view as "light" (with shadow) 16 | /// - `.hybrid` renders the visual effect view as "dark" (no shadow) 17 | /// 18 | /// There are two "debug" options in this demo: 19 | /// - show/ hide the generated shadow image cap inset lines 20 | /// - show/ hide the shadow 21 | /// 22 | /// The button text colors are the standard "blue" tint color. The example could be improved by adapting 23 | /// the button text color to the "light" and "dark" blur effects (similar to the iOS Maps app). 24 | 25 | final class MainViewController: UIViewController { 26 | 27 | fileprivate lazy var mapView = self.lazyMapView() 28 | fileprivate lazy var passThroughView = self.lazyPassThroughView() 29 | 30 | fileprivate var passThroughViewHeightConstraint: NSLayoutConstraint! 31 | fileprivate var passThroughViewBottomConstraint: NSLayoutConstraint! 32 | } 33 | 34 | extension MainViewController { 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | title = "Visual Effect View + Shadow" 40 | 41 | view.addSubview(mapView) 42 | view.addSubview(passThroughView) 43 | 44 | passThroughViewHeightConstraint = passThroughView.heightAnchor.constraint(equalToConstant: 0) 45 | passThroughViewBottomConstraint = passThroughView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32) 46 | 47 | NSLayoutConstraint.activate([ 48 | mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 49 | mapView.topAnchor.constraint(equalTo: view.topAnchor), 50 | mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 51 | mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 52 | 53 | passThroughView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16.0), 54 | passThroughView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 55 | passThroughView.widthAnchor.constraint(greaterThanOrEqualToConstant: 240), 56 | passThroughViewBottomConstraint 57 | ]) 58 | 59 | // Let's always start by showing the "standard" map type. 60 | transition(to: .standard) 61 | } 62 | } 63 | 64 | extension MainViewController { 65 | 66 | fileprivate func transition(to mapType: MKMapType) { 67 | 68 | // If the user selects a "dark" map type (i.e. arial imagery), then 69 | // force this view controller and its subviews to use "dark mode". 70 | // This mimics the Maps app. 71 | 72 | switch mapType { 73 | case .standard: 74 | mapView.mapType = .standard 75 | overrideUserInterfaceStyle = .unspecified 76 | default: 77 | mapView.mapType = .hybrid 78 | overrideUserInterfaceStyle = .dark 79 | } 80 | } 81 | } 82 | 83 | // MARK: User Interactions (Buttons) 84 | 85 | extension MainViewController { 86 | 87 | @objc 88 | fileprivate func userWantsToSwitchMapType() { 89 | let mapType: MKMapType = (mapView.mapType == .standard) ? .hybrid : .standard 90 | transition(to: mapType) 91 | } 92 | 93 | @objc 94 | fileprivate func userWantsToToggleCapInsetLines() { 95 | passThroughView.showCapInsetLines = !passThroughView.showCapInsetLines 96 | } 97 | 98 | @objc 99 | fileprivate func userWantsToToggleShadow() { 100 | passThroughView.showShadow = !passThroughView.showShadow 101 | } 102 | } 103 | 104 | // MARK: User Interactions (Resizing Debug View) 105 | 106 | extension MainViewController { 107 | 108 | @objc 109 | fileprivate func userDidPan(_ gestureRecognizer: UIPanGestureRecognizer) { 110 | 111 | let translation = gestureRecognizer.translation(in: gestureRecognizer.view) 112 | 113 | switch gestureRecognizer.state { 114 | case .began: 115 | passThroughViewHeightConstraint.constant = passThroughView.bounds.height 116 | passThroughViewHeightConstraint.isActive = true 117 | passThroughViewBottomConstraint.isActive = false 118 | case .changed: 119 | 120 | passThroughViewHeightConstraint.constant = passThroughViewHeightConstraint.constant + translation.y 121 | gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view) 122 | case .ended, .cancelled: 123 | 124 | let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view).y 125 | if didUserFlickViewDown(basedOnVelocity: velocity) || didUserDragViewIntoBottomLayoutMargin() { 126 | passThroughViewHeightConstraint.isActive = false 127 | passThroughViewBottomConstraint.isActive = true 128 | } else if didUserFlickViewUp(basedOnVelocity: velocity) || didUserDragViewTooSmall() { 129 | passThroughViewHeightConstraint.isActive = true 130 | passThroughViewBottomConstraint.isActive = false 131 | passThroughViewHeightConstraint.constant = 144.0 132 | } 133 | 134 | UIView.animate( 135 | withDuration: 0.6, 136 | delay: 0.0, 137 | usingSpringWithDamping: 0.7, 138 | initialSpringVelocity: 0, 139 | options: .curveLinear, 140 | animations: { 141 | self.view.layoutIfNeeded() 142 | }, 143 | completion: nil 144 | ) 145 | default: 146 | break 147 | } 148 | } 149 | 150 | private func didUserFlickViewDown(basedOnVelocity velocity: CGFloat) -> Bool { 151 | return didUserFlickView(basedOnVelocity: velocity) 152 | } 153 | 154 | private func didUserFlickViewUp(basedOnVelocity velocity: CGFloat) -> Bool { 155 | return didUserFlickView(basedOnVelocity: abs(velocity)) 156 | } 157 | 158 | private func didUserFlickView(basedOnVelocity velocity: CGFloat) -> Bool { 159 | return velocity > 973 160 | } 161 | 162 | private func didUserDragViewIntoBottomLayoutMargin() -> Bool { 163 | return passThroughView.bounds.height + passThroughView.frame.origin.y > view.frame.height + passThroughViewBottomConstraint.constant 164 | } 165 | 166 | private func didUserDragViewTooSmall() -> Bool { 167 | return passThroughView.bounds.height < 144 // arbitrary value for this demo. 168 | } 169 | } 170 | 171 | // MARK: Lazy View Creation 172 | 173 | extension MainViewController { 174 | 175 | fileprivate func lazyMapView() -> MKMapView { 176 | let view = MKMapView() 177 | view.translatesAutoresizingMaskIntoConstraints = false 178 | 179 | view.mapType = .standard 180 | view.showsTraffic = true 181 | view.showsBuildings = true 182 | 183 | return view 184 | } 185 | 186 | fileprivate func lazyPassThroughView() -> PassThroughView { 187 | let view = PassThroughView() 188 | view.translatesAutoresizingMaskIntoConstraints = false 189 | 190 | addContent(to: view.contentView) 191 | 192 | return view 193 | } 194 | } 195 | 196 | extension MainViewController { 197 | 198 | fileprivate func addContent(to contentView: UIView) { 199 | let stackView = makeStackView() 200 | contentView.addSubview(stackView) 201 | 202 | let bottomGripBar = makeGripBarView() 203 | contentView.addSubview(bottomGripBar) 204 | 205 | NSLayoutConstraint.activate([ 206 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor), 207 | stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 208 | stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 209 | stackView.bottomAnchor.constraint(equalTo: bottomGripBar.topAnchor), 210 | 211 | bottomGripBar.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 212 | bottomGripBar.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 213 | bottomGripBar.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 214 | bottomGripBar.heightAnchor.constraint(equalToConstant: 28.0) 215 | ]) 216 | } 217 | 218 | private func makeStackView() -> UIStackView { 219 | let stackView = UIStackView(arrangedSubviews: [ 220 | makeSwitchMapTypeButton(), 221 | makeToggleShadowButton(), 222 | makeToggleCapInsetsButton() 223 | ]) 224 | 225 | stackView.translatesAutoresizingMaskIntoConstraints = false 226 | stackView.spacing = 2 227 | stackView.axis = .vertical 228 | stackView.distribution = .fillEqually 229 | stackView.alignment = .fill 230 | 231 | return stackView 232 | } 233 | 234 | private func makeSwitchMapTypeButton() -> UIButton { 235 | return makeButton(with: "Switch Map Type", selector: #selector(userWantsToSwitchMapType)) 236 | } 237 | 238 | private func makeToggleCapInsetsButton() -> UIButton { 239 | return makeButton(with: "Toggle Cap Insets", selector: #selector(userWantsToToggleCapInsetLines)) 240 | } 241 | 242 | private func makeToggleShadowButton() -> UIButton { 243 | return makeButton(with: "Toggle Shadow", selector: #selector(userWantsToToggleShadow)) 244 | } 245 | 246 | private func makeButton(with title: String, selector: Selector) -> UIButton { 247 | let view = UIButton(type: .system) 248 | view.translatesAutoresizingMaskIntoConstraints = false 249 | 250 | view.setTitle(title, for: .normal) 251 | view.addTarget(self, action: selector, for: .primaryActionTriggered) 252 | 253 | return view 254 | } 255 | 256 | private func makeGripBarView() -> GripBarView { 257 | let view = GripBarView() 258 | view.translatesAutoresizingMaskIntoConstraints = false 259 | 260 | // This is a semantic color that adapts to style changes. 261 | view.tintColor = .separator 262 | 263 | view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(userDidPan(_:)))) 264 | 265 | return view 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /VisualEffectsShadow/PassThroughView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Brian M Coyner on 4/18/17. 3 | // Copyright © 2017 Brian Coyner. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | import UIKit 8 | 9 | /// The view has the following features: 10 | /// - drop shadow provided by a dynamically generated 9-part image. 11 | /// - rounded corners 12 | /// - live blur effect 13 | /// - automatically adjusts for changes to the `UIUserInterfaceStyle` (i.e. light/dark) 14 | /// - if `.light`, then a drop shadow displays; blur effect is "light material" 15 | /// - if `.dark`, then the drop shadow is hidden; blur effect is "dark material" 16 | /// 17 | /// Developers add subviews to the `contentView` property. 18 | /// Developers should not add subviews directly to this view. 19 | /// 20 | /// - SeeAlso: `UIImage+Shadow` 21 | 22 | final class PassThroughView: UIView { 23 | 24 | // Developers add subviews to the `contentView`. 25 | var contentView: UIView { 26 | return visualEffectView.contentView 27 | } 28 | 29 | // Debug option for drawing the shadow image cap insets. 30 | var showCapInsetLines: Bool = false { 31 | didSet { 32 | shadowView.image = resizeableShadowImage( 33 | withCornerRadius: Properties.cornerRadius, 34 | shadow: Properties.shadow, 35 | shouldDrawCapInsets: showCapInsetLines 36 | ) 37 | } 38 | } 39 | 40 | // Debug option for showing/ hiding the shadow 41 | var showShadow: Bool = true { 42 | didSet { 43 | shadowView.isHidden = !showShadow 44 | } 45 | } 46 | 47 | fileprivate lazy var shadowView = self.lazyShadowView() 48 | fileprivate lazy var visualEffectView = self.lazyVisualEffectView() 49 | 50 | convenience init() { 51 | self.init(frame: CGRect()) 52 | } 53 | 54 | override init(frame: CGRect) { 55 | super.init(frame: frame) 56 | 57 | self.selfInit() 58 | } 59 | 60 | required init?(coder aDecoder: NSCoder) { 61 | super.init(coder: aDecoder) 62 | 63 | self.selfInit() 64 | } 65 | 66 | private func selfInit() { 67 | backgroundColor = .clear 68 | 69 | // Putting the shadow view under the visual effect view helps 70 | // reduce any strange image view artifacts that may appear. 71 | 72 | addSubview(shadowView) 73 | addSubview(visualEffectView) 74 | 75 | let blurRadius = Properties.shadow.blur 76 | NSLayoutConstraint.activate([ 77 | visualEffectView.topAnchor.constraint(equalTo: topAnchor), 78 | visualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), 79 | visualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), 80 | visualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), 81 | 82 | shadowView.topAnchor.constraint(equalTo: topAnchor, constant: -blurRadius), 83 | shadowView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: blurRadius), 84 | shadowView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: blurRadius), 85 | shadowView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -blurRadius), 86 | ]) 87 | } 88 | } 89 | 90 | extension PassThroughView { 91 | 92 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 93 | super.traitCollectionDidChange(previousTraitCollection) 94 | 95 | if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { 96 | transition(to: traitCollection.userInterfaceStyle) 97 | } 98 | } 99 | 100 | private func transition(to style: UIUserInterfaceStyle) { 101 | switch style { 102 | case .light, .unspecified: 103 | showShadow = true 104 | case .dark: 105 | showShadow = false 106 | @unknown default: 107 | fatalError() 108 | } 109 | } 110 | } 111 | 112 | extension PassThroughView { 113 | 114 | fileprivate func lazyVisualEffectView() -> UIVisualEffectView { 115 | 116 | // The "system thin" material automatically adapts to changes to the `UIUserInterfaceStyle`. 117 | // The only we need to do here is show/ hide the shadow. 118 | let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial)) 119 | view.translatesAutoresizingMaskIntoConstraints = false 120 | view.layer.cornerRadius = Properties.cornerRadius 121 | view.layer.masksToBounds = true 122 | 123 | return view 124 | } 125 | 126 | fileprivate func lazyShadowView() -> UIImageView { 127 | 128 | let image = resizeableShadowImage( 129 | withCornerRadius: Properties.cornerRadius, 130 | shadow: Properties.shadow, 131 | shouldDrawCapInsets: showCapInsetLines 132 | ) 133 | 134 | let view = UIImageView(image: image) 135 | view.translatesAutoresizingMaskIntoConstraints = false 136 | 137 | return view 138 | } 139 | 140 | fileprivate func resizeableShadowImage( 141 | withCornerRadius cornerRadius: CGFloat, 142 | shadow: Shadow, 143 | shouldDrawCapInsets: Bool 144 | ) -> UIImage { 145 | 146 | // Trial and error: a multiple of 5 seems to create a decent shadow image for our purposes. 147 | // It's not a perfect fit with the visual effect view's corner. However, putting the image 148 | // view under the visual effect view should mask any issues. 149 | let sideLength: CGFloat = cornerRadius * 5 150 | return UIImage.resizableShadowImage( 151 | withSideLength: sideLength, 152 | cornerRadius: cornerRadius, 153 | shadow: shadow, 154 | shouldDrawCapInsets: showCapInsetLines 155 | ) 156 | } 157 | } 158 | 159 | extension PassThroughView { 160 | 161 | private enum Properties { 162 | static let cornerRadius: CGFloat = 10.0 163 | static let shadow: Shadow = Shadow(offset: CGSize(), blur: 6.0, color: .systemGray) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /VisualEffectsShadow/SampleCode.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Brian M Coyner on 4/17/17. 3 | // Copyright © 2017 Brian Coyner. All rights reserved. 4 | // 5 | 6 | SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM} 7 | 8 | IPHONEOS_DEPLOYMENT_TARGET = 13.0 9 | 10 | TARGETED_DEVICE_FAMILY = 1,2 11 | 12 | SWIFT_VERSION = 5.1 13 | SWIFT_SWIFT3_OBJC_INFERENCE = Default 14 | 15 | // Analyzer 16 | RUN_CLANG_STATIC_ANALYZER = YES 17 | CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES 18 | CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES 19 | CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES 20 | 21 | CLANG_ENABLE_MODULES = YES 22 | CLANG_ENABLE_OBJC_ARC = YES 23 | 24 | COPY_PHASE_STRIP = NO 25 | ENABLE_BITCODE = YES 26 | 27 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES 28 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 29 | CLANG_WARN_EMPTY_BODY = YES 30 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 31 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 32 | CLANG_WARN_UNREACHABLE_CODE = YES 33 | 34 | GCC_WARN_ABOUT_RETURN_TYPE = YES 35 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES 36 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES 37 | GCC_WARN_SHADOW = YES 38 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 39 | GCC_WARN_UNKNOWN_PRAGMAS = YES 40 | GCC_WARN_UNUSED_VARIABLE = YES 41 | GCC_WARN_UNDECLARED_SELECTOR = YES 42 | 43 | GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES 44 | GCC_TREAT_WARNINGS_AS_ERRORS = YES 45 | GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES 46 | 47 | CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES 48 | CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES 49 | CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES 50 | 51 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES 52 | CLANG_WARN_BOOL_CONVERSION = YES 53 | CLANG_WARN_CONSTANT_CONVERSION = YES 54 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 55 | CLANG_WARN_ENUM_CONVERSION = YES 56 | CLANG_WARN_INT_CONVERSION = YES 57 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES 58 | CLANG_WARN_INFINITE_RECURSION = YES 59 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR 60 | CLANG_WARN_STRICT_PROTOTYPES = YES 61 | CLANG_WARN_COMMA = YES 62 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 63 | CLANG_WARN_UNREACHABLE_CODE = YES 64 | GCC_WARN_UNUSED_FUNCTION = YES 65 | GCC_WARN_UNUSED_VARIABLE = YES 66 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES 67 | CLANG_WARN_SUSPICIOUS_MOVE = YES 68 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES 69 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 70 | CLANG_ANALYZER_NONNULL = YES 71 | 72 | ONLY_ACTIVE_ARCH = YES 73 | VALIDATE_PRODUCT = NO 74 | SWIFT_OPTIMIZATION_LEVEL = -Onone 75 | GCC_OPTIMIZATION_LEVEL = 0 76 | SWIFT_ENFORCE_EXCLUSIVE_ACCESS = full 77 | -------------------------------------------------------------------------------- /VisualEffectsShadow/UIImage+Shadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Brian M Coyner on 5/11/17. 3 | // Copyright © 2017 Brian Coyner. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | import UIKit 8 | import CoreGraphics 9 | 10 | public struct Shadow { 11 | let offset: CGSize 12 | let blur: CGFloat 13 | let color: UIColor 14 | 15 | public init(offset: CGSize, blur: CGFloat, color: UIColor) { 16 | self.offset = offset 17 | self.blur = blur 18 | self.color = color 19 | } 20 | } 21 | 22 | extension UIImage { 23 | 24 | /// This function creates a 9-part template image used to add a shadow effect 25 | /// around a `UIVisualEffectView`. 26 | /// 27 | /// - Parameters: 28 | /// - sideLength: the width and height of the 9-part image. 29 | /// - cornerRadius: the corner radius (if rounded corners are desired). 30 | /// - shadow: describes the offset, blur and color of the desired shadow. 31 | /// - shouldDrawCapInsets: if `true`, debug lines are added to the image to help visualize the 9-part image. 32 | /// - Returns: a 9-part template image. 33 | public static func resizableShadowImage( 34 | withSideLength sideLength: CGFloat, 35 | cornerRadius: CGFloat, 36 | shadow: Shadow, 37 | shouldDrawCapInsets: Bool = false 38 | ) -> UIImage { 39 | 40 | let lengthAdjustment = sideLength + (shadow.blur * 2.0) 41 | let graphicContextSize = CGSize(width: lengthAdjustment, height: lengthAdjustment) 42 | let capInset = cornerRadius + shadow.blur 43 | 44 | let renderer = UIGraphicsImageRenderer(size: graphicContextSize) 45 | let shadowImage = renderer.image { (context) in 46 | drawSquareShadowImage( 47 | withSideLength: sideLength, 48 | cornerRadius: cornerRadius, 49 | shadow: shadow, 50 | shouldDrawCapInsets: shouldDrawCapInsets, 51 | in: context 52 | ) 53 | 54 | if shouldDrawCapInsets { 55 | drawCapInsets(capInset, in: context) 56 | } 57 | } 58 | 59 | // Now let's make the square shadow image resizable based on the cap inset. 60 | let edgeInsets = UIEdgeInsets(top: capInset, left: capInset, bottom: capInset, right: capInset) 61 | return shadowImage.resizableImage(withCapInsets: edgeInsets, resizingMode: .tile) // you can play around with `.stretch`, too. 62 | } 63 | 64 | private static func drawSquareShadowImage( 65 | withSideLength sideLength: CGFloat, 66 | cornerRadius: CGFloat, 67 | shadow: Shadow, 68 | shouldDrawCapInsets: Bool = false, 69 | in context: UIGraphicsImageRendererContext 70 | ) { 71 | // The image is a square, which makes it easier to set up the cap insets. 72 | // 73 | // Note: this implementation assumes an offset of CGSize(0, 0) 74 | 75 | let cgContext = context.cgContext 76 | 77 | // This cuts a "hole" in the image leaving only the "shadow" border. 78 | let roundedRect = CGRect(x: shadow.blur, y: shadow.blur, width: sideLength, height: sideLength) 79 | let shadowPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius) 80 | 81 | cgContext.addRect(cgContext.boundingBoxOfClipPath) 82 | cgContext.addPath(shadowPath.cgPath) 83 | cgContext.clip(using: .evenOdd) 84 | 85 | // Finally, let's draw the shadow 86 | let color = shadow.color.cgColor 87 | cgContext.setStrokeColor(color) 88 | cgContext.addPath(shadowPath.cgPath) 89 | cgContext.setShadow(offset: shadow.offset, blur: shadow.blur, color: color) 90 | cgContext.fillPath() 91 | } 92 | 93 | private static func drawCapInsets( 94 | _ capInset: CGFloat, 95 | in context: UIGraphicsImageRendererContext 96 | ) { 97 | 98 | let cgContext = context.cgContext 99 | cgContext.setStrokeColor(UIColor.purple.cgColor) 100 | cgContext.beginPath() 101 | 102 | let debugRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: context.format.bounds.size) 103 | 104 | // horizontal top line 105 | cgContext.move(to: CGPoint(x: debugRect.origin.x, y: debugRect.origin.y + capInset)) 106 | cgContext.addLine(to: CGPoint(x: debugRect.size.width + capInset, y: debugRect.origin.y + capInset)) 107 | 108 | // horizontal bottom line 109 | cgContext.move(to: CGPoint(x: debugRect.origin.x, y: debugRect.size.height - capInset)) 110 | cgContext.addLine(to: CGPoint(x: debugRect.size.width + capInset, y: debugRect.size.height - capInset)) 111 | 112 | // vertical left line 113 | cgContext.move(to: CGPoint(x: debugRect.origin.x + capInset, y: debugRect.origin.y)) 114 | cgContext.addLine(to: CGPoint(x: debugRect.origin.x + capInset, y: debugRect.size.height)) 115 | 116 | // vertical right line 117 | cgContext.move(to: CGPoint(x: debugRect.size.width - capInset, y: debugRect.origin.y)) 118 | cgContext.addLine(to: CGPoint(x: debugRect.size.width - capInset, y: debugRect.size.height)) 119 | 120 | cgContext.strokePath() 121 | 122 | // Finally, adding a red border around the entire image. 123 | cgContext.addRect(debugRect.insetBy(dx: 0.5, dy: 0.5)) 124 | cgContext.setStrokeColor(UIColor.red.cgColor) 125 | cgContext.strokePath() 126 | } 127 | } 128 | --------------------------------------------------------------------------------