├── DSSwipableFilterView.xcodeproj ├── project.pbxproj └── xcuserdata │ └── darkospasovski.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── DSSwipableFilterView ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── DSSwipableFilterView │ ├── DSFilter.swift │ ├── DSFilterView.swift │ ├── DSSwipableFilterView.swift │ └── DSSwipableFilterViewDataSource.swift ├── GalleryMediaFilteringViewController.swift ├── Info.plist ├── LiveCameraFilteringViewController.swift ├── UIImage+Transform.swift └── ViewController.swift ├── README.md └── preview.gif /DSSwipableFilterView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7B8B402C1FADC7F70001F73E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B402B1FADC7F70001F73E /* AppDelegate.swift */; }; 11 | 7B8B402E1FADC7F70001F73E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B402D1FADC7F70001F73E /* ViewController.swift */; }; 12 | 7B8B40311FADC7F70001F73E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B8B402F1FADC7F70001F73E /* Main.storyboard */; }; 13 | 7B8B40331FADC7F70001F73E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B8B40321FADC7F70001F73E /* Assets.xcassets */; }; 14 | 7B8B40361FADC7F70001F73E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B8B40341FADC7F70001F73E /* LaunchScreen.storyboard */; }; 15 | 7B8B40421FADC81B0001F73E /* DSSwipableFilterViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B403E1FADC81B0001F73E /* DSSwipableFilterViewDataSource.swift */; }; 16 | 7B8B40431FADC81B0001F73E /* DSFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B403F1FADC81B0001F73E /* DSFilter.swift */; }; 17 | 7B8B40441FADC81B0001F73E /* DSSwipableFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B40401FADC81B0001F73E /* DSSwipableFilterView.swift */; }; 18 | 7B8B40451FADC81B0001F73E /* DSFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B40411FADC81B0001F73E /* DSFilterView.swift */; }; 19 | 7B8B40471FADC8B20001F73E /* LiveCameraFilteringViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B40461FADC8B20001F73E /* LiveCameraFilteringViewController.swift */; }; 20 | 7B8B40491FADC8BE0001F73E /* GalleryMediaFilteringViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B40481FADC8BE0001F73E /* GalleryMediaFilteringViewController.swift */; }; 21 | 7B8B404B1FADCCAF0001F73E /* UIImage+Transform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8B404A1FADCCAF0001F73E /* UIImage+Transform.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 7B8B40281FADC7F70001F73E /* DSSwipableFilterView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DSSwipableFilterView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 7B8B402B1FADC7F70001F73E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 27 | 7B8B402D1FADC7F70001F73E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 28 | 7B8B40301FADC7F70001F73E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | 7B8B40321FADC7F70001F73E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 7B8B40351FADC7F70001F73E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | 7B8B40371FADC7F70001F73E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | 7B8B403E1FADC81B0001F73E /* DSSwipableFilterViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DSSwipableFilterViewDataSource.swift; sourceTree = ""; }; 33 | 7B8B403F1FADC81B0001F73E /* DSFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DSFilter.swift; sourceTree = ""; }; 34 | 7B8B40401FADC81B0001F73E /* DSSwipableFilterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DSSwipableFilterView.swift; sourceTree = ""; }; 35 | 7B8B40411FADC81B0001F73E /* DSFilterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DSFilterView.swift; sourceTree = ""; }; 36 | 7B8B40461FADC8B20001F73E /* LiveCameraFilteringViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveCameraFilteringViewController.swift; sourceTree = ""; }; 37 | 7B8B40481FADC8BE0001F73E /* GalleryMediaFilteringViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryMediaFilteringViewController.swift; sourceTree = ""; }; 38 | 7B8B404A1FADCCAF0001F73E /* UIImage+Transform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Transform.swift"; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 7B8B40251FADC7F70001F73E /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | 7B8B401F1FADC7F70001F73E = { 53 | isa = PBXGroup; 54 | children = ( 55 | 7B8B402A1FADC7F70001F73E /* DSSwipableFilterView */, 56 | 7B8B40291FADC7F70001F73E /* Products */, 57 | ); 58 | sourceTree = ""; 59 | }; 60 | 7B8B40291FADC7F70001F73E /* Products */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 7B8B40281FADC7F70001F73E /* DSSwipableFilterView.app */, 64 | ); 65 | name = Products; 66 | sourceTree = ""; 67 | }; 68 | 7B8B402A1FADC7F70001F73E /* DSSwipableFilterView */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 7B8B403D1FADC81B0001F73E /* DSSwipableFilterView */, 72 | 7B8B402B1FADC7F70001F73E /* AppDelegate.swift */, 73 | 7B8B402D1FADC7F70001F73E /* ViewController.swift */, 74 | 7B8B402F1FADC7F70001F73E /* Main.storyboard */, 75 | 7B8B40321FADC7F70001F73E /* Assets.xcassets */, 76 | 7B8B40341FADC7F70001F73E /* LaunchScreen.storyboard */, 77 | 7B8B40371FADC7F70001F73E /* Info.plist */, 78 | 7B8B40461FADC8B20001F73E /* LiveCameraFilteringViewController.swift */, 79 | 7B8B40481FADC8BE0001F73E /* GalleryMediaFilteringViewController.swift */, 80 | 7B8B404A1FADCCAF0001F73E /* UIImage+Transform.swift */, 81 | ); 82 | path = DSSwipableFilterView; 83 | sourceTree = ""; 84 | }; 85 | 7B8B403D1FADC81B0001F73E /* DSSwipableFilterView */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 7B8B403E1FADC81B0001F73E /* DSSwipableFilterViewDataSource.swift */, 89 | 7B8B403F1FADC81B0001F73E /* DSFilter.swift */, 90 | 7B8B40401FADC81B0001F73E /* DSSwipableFilterView.swift */, 91 | 7B8B40411FADC81B0001F73E /* DSFilterView.swift */, 92 | ); 93 | path = DSSwipableFilterView; 94 | sourceTree = ""; 95 | }; 96 | /* End PBXGroup section */ 97 | 98 | /* Begin PBXNativeTarget section */ 99 | 7B8B40271FADC7F70001F73E /* DSSwipableFilterView */ = { 100 | isa = PBXNativeTarget; 101 | buildConfigurationList = 7B8B403A1FADC7F70001F73E /* Build configuration list for PBXNativeTarget "DSSwipableFilterView" */; 102 | buildPhases = ( 103 | 7B8B40241FADC7F70001F73E /* Sources */, 104 | 7B8B40251FADC7F70001F73E /* Frameworks */, 105 | 7B8B40261FADC7F70001F73E /* Resources */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = DSSwipableFilterView; 112 | productName = DSSwipableFilterView; 113 | productReference = 7B8B40281FADC7F70001F73E /* DSSwipableFilterView.app */; 114 | productType = "com.apple.product-type.application"; 115 | }; 116 | /* End PBXNativeTarget section */ 117 | 118 | /* Begin PBXProject section */ 119 | 7B8B40201FADC7F70001F73E /* Project object */ = { 120 | isa = PBXProject; 121 | attributes = { 122 | LastSwiftUpdateCheck = 0910; 123 | LastUpgradeCheck = 0910; 124 | ORGANIZATIONNAME = "Darko Spasovski"; 125 | TargetAttributes = { 126 | 7B8B40271FADC7F70001F73E = { 127 | CreatedOnToolsVersion = 9.1; 128 | ProvisioningStyle = Manual; 129 | }; 130 | }; 131 | }; 132 | buildConfigurationList = 7B8B40231FADC7F70001F73E /* Build configuration list for PBXProject "DSSwipableFilterView" */; 133 | compatibilityVersion = "Xcode 8.0"; 134 | developmentRegion = en; 135 | hasScannedForEncodings = 0; 136 | knownRegions = ( 137 | en, 138 | Base, 139 | ); 140 | mainGroup = 7B8B401F1FADC7F70001F73E; 141 | productRefGroup = 7B8B40291FADC7F70001F73E /* Products */; 142 | projectDirPath = ""; 143 | projectRoot = ""; 144 | targets = ( 145 | 7B8B40271FADC7F70001F73E /* DSSwipableFilterView */, 146 | ); 147 | }; 148 | /* End PBXProject section */ 149 | 150 | /* Begin PBXResourcesBuildPhase section */ 151 | 7B8B40261FADC7F70001F73E /* Resources */ = { 152 | isa = PBXResourcesBuildPhase; 153 | buildActionMask = 2147483647; 154 | files = ( 155 | 7B8B40361FADC7F70001F73E /* LaunchScreen.storyboard in Resources */, 156 | 7B8B40331FADC7F70001F73E /* Assets.xcassets in Resources */, 157 | 7B8B40311FADC7F70001F73E /* Main.storyboard in Resources */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXResourcesBuildPhase section */ 162 | 163 | /* Begin PBXSourcesBuildPhase section */ 164 | 7B8B40241FADC7F70001F73E /* Sources */ = { 165 | isa = PBXSourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | 7B8B40471FADC8B20001F73E /* LiveCameraFilteringViewController.swift in Sources */, 169 | 7B8B402E1FADC7F70001F73E /* ViewController.swift in Sources */, 170 | 7B8B40421FADC81B0001F73E /* DSSwipableFilterViewDataSource.swift in Sources */, 171 | 7B8B404B1FADCCAF0001F73E /* UIImage+Transform.swift in Sources */, 172 | 7B8B40441FADC81B0001F73E /* DSSwipableFilterView.swift in Sources */, 173 | 7B8B40431FADC81B0001F73E /* DSFilter.swift in Sources */, 174 | 7B8B402C1FADC7F70001F73E /* AppDelegate.swift in Sources */, 175 | 7B8B40451FADC81B0001F73E /* DSFilterView.swift in Sources */, 176 | 7B8B40491FADC8BE0001F73E /* GalleryMediaFilteringViewController.swift in Sources */, 177 | ); 178 | runOnlyForDeploymentPostprocessing = 0; 179 | }; 180 | /* End PBXSourcesBuildPhase section */ 181 | 182 | /* Begin PBXVariantGroup section */ 183 | 7B8B402F1FADC7F70001F73E /* Main.storyboard */ = { 184 | isa = PBXVariantGroup; 185 | children = ( 186 | 7B8B40301FADC7F70001F73E /* Base */, 187 | ); 188 | name = Main.storyboard; 189 | sourceTree = ""; 190 | }; 191 | 7B8B40341FADC7F70001F73E /* LaunchScreen.storyboard */ = { 192 | isa = PBXVariantGroup; 193 | children = ( 194 | 7B8B40351FADC7F70001F73E /* Base */, 195 | ); 196 | name = LaunchScreen.storyboard; 197 | sourceTree = ""; 198 | }; 199 | /* End PBXVariantGroup section */ 200 | 201 | /* Begin XCBuildConfiguration section */ 202 | 7B8B40381FADC7F70001F73E /* Debug */ = { 203 | isa = XCBuildConfiguration; 204 | buildSettings = { 205 | ALWAYS_SEARCH_USER_PATHS = NO; 206 | CLANG_ANALYZER_NONNULL = YES; 207 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 208 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 209 | CLANG_CXX_LIBRARY = "libc++"; 210 | CLANG_ENABLE_MODULES = YES; 211 | CLANG_ENABLE_OBJC_ARC = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 218 | CLANG_WARN_EMPTY_BODY = YES; 219 | CLANG_WARN_ENUM_CONVERSION = YES; 220 | CLANG_WARN_INFINITE_RECURSION = YES; 221 | CLANG_WARN_INT_CONVERSION = YES; 222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 225 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 226 | CLANG_WARN_STRICT_PROTOTYPES = YES; 227 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 228 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 229 | CLANG_WARN_UNREACHABLE_CODE = YES; 230 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 231 | CODE_SIGN_IDENTITY = "iPhone Developer"; 232 | COPY_PHASE_STRIP = NO; 233 | DEBUG_INFORMATION_FORMAT = dwarf; 234 | ENABLE_STRICT_OBJC_MSGSEND = YES; 235 | ENABLE_TESTABILITY = YES; 236 | GCC_C_LANGUAGE_STANDARD = gnu11; 237 | GCC_DYNAMIC_NO_PIC = NO; 238 | GCC_NO_COMMON_BLOCKS = YES; 239 | GCC_OPTIMIZATION_LEVEL = 0; 240 | GCC_PREPROCESSOR_DEFINITIONS = ( 241 | "DEBUG=1", 242 | "$(inherited)", 243 | ); 244 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 245 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 246 | GCC_WARN_UNDECLARED_SELECTOR = YES; 247 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 248 | GCC_WARN_UNUSED_FUNCTION = YES; 249 | GCC_WARN_UNUSED_VARIABLE = YES; 250 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 251 | MTL_ENABLE_DEBUG_INFO = YES; 252 | ONLY_ACTIVE_ARCH = YES; 253 | SDKROOT = iphoneos; 254 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 255 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 256 | }; 257 | name = Debug; 258 | }; 259 | 7B8B40391FADC7F70001F73E /* Release */ = { 260 | isa = XCBuildConfiguration; 261 | buildSettings = { 262 | ALWAYS_SEARCH_USER_PATHS = NO; 263 | CLANG_ANALYZER_NONNULL = YES; 264 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 266 | CLANG_CXX_LIBRARY = "libc++"; 267 | CLANG_ENABLE_MODULES = YES; 268 | CLANG_ENABLE_OBJC_ARC = YES; 269 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 270 | CLANG_WARN_BOOL_CONVERSION = YES; 271 | CLANG_WARN_COMMA = YES; 272 | CLANG_WARN_CONSTANT_CONVERSION = YES; 273 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 274 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 275 | CLANG_WARN_EMPTY_BODY = YES; 276 | CLANG_WARN_ENUM_CONVERSION = YES; 277 | CLANG_WARN_INFINITE_RECURSION = YES; 278 | CLANG_WARN_INT_CONVERSION = YES; 279 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 281 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 282 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 283 | CLANG_WARN_STRICT_PROTOTYPES = YES; 284 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 285 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 286 | CLANG_WARN_UNREACHABLE_CODE = YES; 287 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 288 | CODE_SIGN_IDENTITY = "iPhone Developer"; 289 | COPY_PHASE_STRIP = NO; 290 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 291 | ENABLE_NS_ASSERTIONS = NO; 292 | ENABLE_STRICT_OBJC_MSGSEND = YES; 293 | GCC_C_LANGUAGE_STANDARD = gnu11; 294 | GCC_NO_COMMON_BLOCKS = YES; 295 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 296 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 297 | GCC_WARN_UNDECLARED_SELECTOR = YES; 298 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 299 | GCC_WARN_UNUSED_FUNCTION = YES; 300 | GCC_WARN_UNUSED_VARIABLE = YES; 301 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 302 | MTL_ENABLE_DEBUG_INFO = NO; 303 | SDKROOT = iphoneos; 304 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 305 | VALIDATE_PRODUCT = YES; 306 | }; 307 | name = Release; 308 | }; 309 | 7B8B403B1FADC7F70001F73E /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 313 | CODE_SIGN_STYLE = Manual; 314 | DEVELOPMENT_TEAM = 2LEJV6KBBG; 315 | INFOPLIST_FILE = DSSwipableFilterView/Info.plist; 316 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 317 | PRODUCT_BUNDLE_IDENTIFIER = com.bocka.test; 318 | PRODUCT_NAME = "$(TARGET_NAME)"; 319 | PROVISIONING_PROFILE = "4f5c8094-bca3-47cc-98a9-af7db3fba02e"; 320 | PROVISIONING_PROFILE_SPECIFIER = BockaTestProfile; 321 | SWIFT_VERSION = 4.0; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | }; 324 | name = Debug; 325 | }; 326 | 7B8B403C1FADC7F70001F73E /* Release */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 330 | CODE_SIGN_STYLE = Manual; 331 | DEVELOPMENT_TEAM = 2LEJV6KBBG; 332 | INFOPLIST_FILE = DSSwipableFilterView/Info.plist; 333 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 334 | PRODUCT_BUNDLE_IDENTIFIER = com.bocka.test; 335 | PRODUCT_NAME = "$(TARGET_NAME)"; 336 | PROVISIONING_PROFILE = "4f5c8094-bca3-47cc-98a9-af7db3fba02e"; 337 | PROVISIONING_PROFILE_SPECIFIER = BockaTestProfile; 338 | SWIFT_VERSION = 4.0; 339 | TARGETED_DEVICE_FAMILY = "1,2"; 340 | }; 341 | name = Release; 342 | }; 343 | /* End XCBuildConfiguration section */ 344 | 345 | /* Begin XCConfigurationList section */ 346 | 7B8B40231FADC7F70001F73E /* Build configuration list for PBXProject "DSSwipableFilterView" */ = { 347 | isa = XCConfigurationList; 348 | buildConfigurations = ( 349 | 7B8B40381FADC7F70001F73E /* Debug */, 350 | 7B8B40391FADC7F70001F73E /* Release */, 351 | ); 352 | defaultConfigurationIsVisible = 0; 353 | defaultConfigurationName = Release; 354 | }; 355 | 7B8B403A1FADC7F70001F73E /* Build configuration list for PBXNativeTarget "DSSwipableFilterView" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | 7B8B403B1FADC7F70001F73E /* Debug */, 359 | 7B8B403C1FADC7F70001F73E /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | /* End XCConfigurationList section */ 365 | }; 366 | rootObject = 7B8B40201FADC7F70001F73E /* Project object */; 367 | } 368 | -------------------------------------------------------------------------------- /DSSwipableFilterView.xcodeproj/xcuserdata/darkospasovski.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | DSSwipableFilterView.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DSSwipableFilterView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DSSwipableFilterView 4 | // 5 | // Created by Darko Spasovski on 11/4/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | 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 invalidate graphics rendering callbacks. 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 active 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 | -------------------------------------------------------------------------------- /DSSwipableFilterView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /DSSwipableFilterView/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DSSwipableFilterView/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 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 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /DSSwipableFilterView/DSSwipableFilterView/DSFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DSFilter.swift 3 | // TestCameraAUUU 4 | // 5 | // Created by Darko Spasovski on 10/19/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreImage 11 | 12 | public enum FilterType { 13 | case ciFilter 14 | case combinedFilter 15 | case locationFilter 16 | } 17 | 18 | public enum LocationFilterType { 19 | case none 20 | case country 21 | case city 22 | } 23 | 24 | open class DSFilter { 25 | 26 | fileprivate var name:String? 27 | fileprivate var filter: CIFilter? 28 | fileprivate var combinedFilters: [CIFilter] = [CIFilter]() 29 | 30 | var type: FilterType = .ciFilter 31 | var locationType: LocationFilterType = .none 32 | 33 | public init(name: String, filter: CIFilter){ 34 | self.name = filter.name 35 | self.filter = filter 36 | } 37 | 38 | public init(name: String, filter: CIFilter, type: FilterType){ 39 | self.name = name 40 | self.filter = filter 41 | self.type = type 42 | } 43 | 44 | public init(text: String, locationType: LocationFilterType){ 45 | self.name = text 46 | self.type = .locationFilter 47 | self.locationType = locationType 48 | } 49 | 50 | public init(name: String, type: FilterType){ 51 | self.name = name 52 | self.type = type 53 | if name != "No Filter" { 54 | filter = CIFilter(name: name) 55 | filter!.setDefaults() 56 | } 57 | } 58 | 59 | public init(combinedFilter: [CIFilter]){ 60 | self.name = "Combined" 61 | self.combinedFilters = combinedFilter 62 | self.type = .combinedFilter 63 | } 64 | 65 | public init(name: String) { 66 | self.name = name 67 | 68 | if name != "No Filter" { 69 | filter = CIFilter(name: name) 70 | filter!.setDefaults() 71 | } 72 | } 73 | 74 | open func getName() -> String { 75 | return name! 76 | } 77 | 78 | open func getFilter() -> CIFilter? { 79 | return filter 80 | } 81 | 82 | open func filter(image: CIImage) -> CIImage { 83 | switch type { 84 | case .ciFilter: 85 | return filterOne(image:image) 86 | case .combinedFilter: 87 | return combineFiltersOn(image: image) 88 | case .locationFilter: 89 | return image 90 | } 91 | } 92 | 93 | fileprivate func combineFiltersOn(image: CIImage) -> CIImage{ 94 | let firstFilter = combinedFilters.first! 95 | firstFilter.setValue(image, forKey: kCIInputImageKey) 96 | let firstImage = firstFilter.value(forKey: kCIOutputImageKey) as! CIImage 97 | 98 | let secondFilter = combinedFilters.last! 99 | secondFilter.setValue(firstImage, forKey: kCIInputImageKey) 100 | let secondImage = secondFilter.value(forKey: kCIOutputImageKey) as! CIImage 101 | 102 | return secondImage 103 | } 104 | 105 | fileprivate func filterOne(image: CIImage) -> CIImage { 106 | if name == "No Filter" { 107 | return image 108 | } 109 | filter!.setValue(image, forKey: kCIInputImageKey) 110 | let filteredImage = filter!.value(forKey: kCIOutputImageKey) as! CIImage 111 | return filteredImage 112 | } 113 | 114 | 115 | } 116 | -------------------------------------------------------------------------------- /DSSwipableFilterView/DSSwipableFilterView/DSFilterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DSFilterView.swift 3 | // TestCameraAUUU 4 | // 5 | // Created by Darko Spasovski on 10/19/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import GLKit 11 | import CoreImage 12 | 13 | enum ScrollingDirection{ 14 | case forward 15 | case backward 16 | case steady 17 | } 18 | 19 | class DSFilterView: GLKView { 20 | 21 | fileprivate var newFilterPosition: CGFloat = 0.0 22 | fileprivate var image: CIImage? 23 | fileprivate var ciContext: CIContext? 24 | 25 | fileprivate var lastPosition = 0.0 26 | 27 | var scrollViewDirection:ScrollingDirection = .steady 28 | var mainFilter: DSFilter! 29 | var partFilter: DSFilter! 30 | 31 | override init(frame: CGRect){ 32 | super.init(frame: frame, context: EAGLContext(api: .openGLES2)!) 33 | EAGLContext.setCurrent(self.context) 34 | self.ciContext = CIContext(eaglContext: self.context) 35 | self.mainFilter = DSFilter(name: "No Filter") 36 | self.partFilter = DSFilter(name: "No Filter") 37 | self.contentMode = .scaleAspectFit 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder){ 41 | super.init(coder: aDecoder) 42 | self.context = EAGLContext(api: .openGLES2)! 43 | EAGLContext.setCurrent(self.context) 44 | self.ciContext = CIContext(eaglContext: self.context) 45 | self.contentMode = .scaleAspectFit 46 | } 47 | 48 | func setRenderImage(image: CIImage){ 49 | self.image = image 50 | setNeedsDisplay() 51 | } 52 | 53 | func setNewFilterPosition(x: CGFloat) { 54 | lastPosition = Double(newFilterPosition) 55 | newFilterPosition = x 56 | } 57 | 58 | func getComposedImageOf(sourceImage: CIImage, overImage: CIImage, cropRect: CGRect) -> CIImage { 59 | let cropedSecond = sourceImage.cropped(to: cropRect) 60 | let output = cropedSecond.composited(over: overImage) 61 | return output 62 | } 63 | 64 | func getFilteredCapturedImage() -> UIImage { 65 | if let capturedImage = self.image { 66 | let cgImage = ciContext?.createCGImage(mainFilter.filter(image: capturedImage), from: capturedImage.extent) 67 | if let image = cgImage { 68 | return UIImage(cgImage: image); 69 | } 70 | } 71 | return UIImage() 72 | } 73 | 74 | override func draw(_ rect: CGRect){ 75 | 76 | let scale = UIScreen.main.scale 77 | let newFrame = CGRect(x: rect.minX, y: rect.minY, width: rect.width * scale, height: rect.height * scale) 78 | 79 | if let image = image{ 80 | 81 | let filterOneFrame = (newFrame.width - (CGFloat(newFilterPosition) * scale)) * (image.extent.width/newFrame.width) 82 | let lastOneFrame = (newFrame.width - (CGFloat(lastPosition) * scale)) * (image.extent.width/newFrame.width) 83 | let cropRect = CGRect(x: (image.extent.width - filterOneFrame), y: 0, width:image.extent.width, height: image.extent.height) 84 | 85 | if scrollViewDirection == .forward { 86 | if filterOneFrame == 0.0 && lastOneFrame >= (image.extent.width - 10){ 87 | ciContext?.draw(self.partFilter.filter(image: image), in: newFrame, from: image.extent) 88 | }else if filterOneFrame == 0.0 && lastOneFrame < 15{ 89 | ciContext?.draw(self.mainFilter.filter(image: image), in: newFrame, from: image.extent) 90 | }else{ 91 | ciContext?.draw(getComposedImageOf(sourceImage: self.partFilter.filter(image: image), overImage: self.mainFilter.filter(image: image), cropRect: cropRect), in: newFrame, from: image.extent) 92 | } 93 | }else if scrollViewDirection == .backward{ 94 | if filterOneFrame == 0.0 && lastOneFrame < 15 { 95 | ciContext?.draw(self.partFilter.filter(image: image), in: newFrame, from: image.extent) 96 | }else if filterOneFrame == 0.0 && lastOneFrame >= (image.extent.width - 10) { 97 | ciContext?.draw(self.mainFilter.filter(image: image), in: newFrame, from: image.extent) 98 | }else { 99 | ciContext?.draw(getComposedImageOf(sourceImage: self.mainFilter.filter(image: image), overImage: self.partFilter.filter(image: image), cropRect: cropRect), in: newFrame, from: image.extent) 100 | } 101 | }else if scrollViewDirection == .steady { 102 | ciContext?.draw(self.mainFilter.filter(image: image), in: newFrame, from: image.extent) 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /DSSwipableFilterView/DSSwipableFilterView/DSSwipableFilterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DSSwipableFilterView.swift 3 | // TestCameraAUUU 4 | // 5 | // Created by Darko Spasovski on 10/19/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class DSSwipableFilterView: UIView { 12 | 13 | 14 | fileprivate var scrollView: UIScrollView 15 | fileprivate var numberOfPages: Int 16 | fileprivate var startingIndex: Int 17 | fileprivate var currentIndex: Int 18 | fileprivate var data = [DSFilter]() 19 | fileprivate var filterView: DSFilterView 20 | 21 | open weak var dataSource: DSSwipableFilterViewDataSource? 22 | 23 | var isPlayingLibraryVideo = false 24 | 25 | fileprivate var lastContentOffset:CGFloat = 0.0 26 | 27 | public override init(frame: CGRect) { 28 | 29 | numberOfPages = 3 30 | startingIndex = 0 31 | currentIndex = 0 32 | scrollView = UIScrollView(frame: frame) 33 | filterView = DSFilterView(frame: frame) 34 | 35 | super.init(frame: frame) 36 | 37 | scrollView.delegate = self 38 | scrollView.isPagingEnabled = true 39 | scrollView.bounces = false 40 | scrollView.showsHorizontalScrollIndicator = false 41 | scrollView.showsVerticalScrollIndicator = false 42 | addSubview(scrollView) 43 | } 44 | 45 | required public init?(coder aDecoder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | open func reloadData() { 50 | cleanData() 51 | loadData() 52 | presentData() 53 | } 54 | 55 | open func getFilteredImage() -> UIImage { 56 | let filterImage = filterView.getFilteredCapturedImage() 57 | UIGraphicsBeginImageContextWithOptions(frame.size, false, UIScreen.main.scale) 58 | let context = UIGraphicsGetCurrentContext() 59 | 60 | filterImage.draw(in: bounds) 61 | let image = UIGraphicsGetImageFromCurrentImageContext() 62 | UIGraphicsEndImageContext() 63 | return image! 64 | } 65 | 66 | open func setRenderImage(image: CIImage){ 67 | renderImage(image: image) 68 | } 69 | 70 | open func setRenderImage(image: UIImage) { 71 | let ciImage = CIImage(cgImage: image.cgImage!) 72 | let fixedImage = ciImage.transformed(by: image.transformToFixImage()) 73 | setRenderImage(image: fixedImage) 74 | } 75 | 76 | fileprivate func renderImage(image: CIImage) { 77 | filterView.setRenderImage(image: isPlayingLibraryVideo ? image.scaleAndResize(forRect: frame, and: contentMode) : image) 78 | } 79 | 80 | fileprivate func cleanData() { 81 | currentIndex = 0 82 | for v in scrollView.subviews { 83 | v.removeFromSuperview() 84 | } 85 | data.removeAll() 86 | } 87 | 88 | fileprivate func loadData() { 89 | 90 | numberOfPages = dataSource!.numberOfFilters(self) 91 | startingIndex = dataSource!.startAtIndex(self) 92 | scrollView.contentSize = CGSize(width: frame.width*(CGFloat(numberOfPages)), height: frame.height) 93 | 94 | for i in 0..= 2{ 105 | filterView.mainFilter = data[0] 106 | filterView.partFilter = data[1] 107 | } 108 | insertSubview(filterView, belowSubview: scrollView) 109 | 110 | for i in 0.. CGFloat { 118 | return frame.size.width*CGFloat(index) 119 | } 120 | 121 | } 122 | 123 | extension DSSwipableFilterView: UIScrollViewDelegate { 124 | 125 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 126 | if decelerate { 127 | scrollView.isUserInteractionEnabled = false 128 | } 129 | } 130 | 131 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 132 | let index = Int(scrollView.contentOffset.x/scrollView.frame.width) 133 | let newPosition = fabs(scrollView.contentOffset.x - (CGFloat(index)*scrollView.frame.width)) 134 | 135 | let x = scrollView.frame.width - newPosition 136 | 137 | if filterView.scrollViewDirection == .steady { 138 | if x > scrollView.frame.width/2.0 { 139 | filterView.scrollViewDirection = .forward 140 | }else{ 141 | filterView.scrollViewDirection = .backward 142 | } 143 | } 144 | 145 | filterView.mainFilter = data[currentIndex] 146 | 147 | if index < currentIndex { 148 | filterView.partFilter = data[currentIndex-1] 149 | }else if index > currentIndex { 150 | filterView.partFilter = data[currentIndex+1] 151 | }else{ 152 | if (index+1) < data.count { 153 | filterView.partFilter = data[index+1] 154 | } 155 | } 156 | 157 | filterView.setNewFilterPosition(x: x) 158 | filterView.setNeedsDisplay() 159 | } 160 | 161 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 162 | let index = Int(scrollView.contentOffset.x/scrollView.frame.width) 163 | 164 | filterView.scrollViewDirection = .steady 165 | if currentIndex != index { 166 | filterView.mainFilter = filterView.partFilter 167 | filterView.setNeedsDisplay() 168 | } 169 | filterView.setNeedsDisplay() 170 | 171 | currentIndex = index 172 | 173 | scrollView.isUserInteractionEnabled = true 174 | } 175 | } 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /DSSwipableFilterView/DSSwipableFilterView/DSSwipableFilterViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DSSwipableFilterViewDataSourec.swift 3 | // TestCameraAUUU 4 | // 5 | // Created by Darko Spasovski on 10/19/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | 13 | public protocol DSSwipableFilterViewDataSource : class { 14 | 15 | func numberOfFilters(_ filterView: DSSwipableFilterView) -> Int 16 | 17 | func filter(_ filterView: DSSwipableFilterView, filterAtIndex index: Int) -> DSFilter 18 | 19 | func startAtIndex(_ filterView: DSSwipableFilterView) -> Int 20 | } 21 | -------------------------------------------------------------------------------- /DSSwipableFilterView/GalleryMediaFilteringViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GalleryMediaFilteringViewController.swift 3 | // DSSwipableFilterView 4 | // 5 | // Created by Darko Spasovski on 11/4/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MobileCoreServices 11 | import AVFoundation 12 | 13 | class GalleryMediaFilteringViewController: UIViewController { 14 | 15 | let filterList = [DSFilter(name: "No Filter", type: .ciFilter), 16 | DSFilter(name: "CIPhotoEffectMono", type: .ciFilter), 17 | DSFilter(name: "CIPhotoEffectChrome", type: .ciFilter), 18 | DSFilter(name: "CIPhotoEffectTransfer", type: .ciFilter), 19 | DSFilter(name: "CIPhotoEffectInstant", type: .ciFilter), 20 | DSFilter(name: "CIPhotoEffectNoir", type: .ciFilter), 21 | DSFilter(name: "CIPhotoEffectProcess", type: .ciFilter), 22 | DSFilter(name: "CIPhotoEffectTonal", type: .ciFilter)] 23 | 24 | let filterSwipeView = DSSwipableFilterView(frame: UIScreen.main.bounds) 25 | 26 | var player = AVPlayer() 27 | 28 | let videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: [String(kCVPixelBufferPixelFormatTypeKey): NSNumber(value: kCVPixelFormatType_32BGRA)]) 29 | 30 | var galleryVideoTranform: CGAffineTransform? 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | title = "Gallery media filter" 35 | prepareFilterView() 36 | openGallery() 37 | // Do any additional setup after loading the view. 38 | } 39 | 40 | override func didReceiveMemoryWarning() { 41 | super.didReceiveMemoryWarning() 42 | // Dispose of any resources that can be recreated. 43 | } 44 | 45 | override func viewDidAppear(_ animated: Bool) { 46 | super.viewDidAppear(animated) 47 | } 48 | 49 | fileprivate func prepareFilterView() { 50 | filterSwipeView.dataSource = self 51 | filterSwipeView.isUserInteractionEnabled = true 52 | filterSwipeView.isMultipleTouchEnabled = true 53 | filterSwipeView.isExclusiveTouch = false 54 | 55 | self.view.addSubview(filterSwipeView) 56 | filterSwipeView.reloadData() 57 | } 58 | 59 | fileprivate func openGallery() { 60 | let mediaPicker = UIImagePickerController() 61 | mediaPicker.sourceType = .savedPhotosAlbum 62 | mediaPicker.allowsEditing = false 63 | mediaPicker.delegate = self 64 | mediaPicker.mediaTypes = [kUTTypeImage as String, kUTTypeMovie as String] 65 | 66 | present(mediaPicker, animated: true, completion: nil) 67 | } 68 | 69 | fileprivate func preview(image: UIImage) { 70 | let ciImage = CIImage(cgImage: image.cgImage!) 71 | let fixedImage = ciImage.transformed(by: image.transformToFixImage()) 72 | filterSwipeView.setRenderImage(image: fixedImage) 73 | } 74 | 75 | fileprivate func previewVideoWith(url: URL) { 76 | galleryVideoTranform = nil 77 | 78 | let item = AVPlayerItem(url: url) 79 | player = AVPlayer(playerItem: item) 80 | player.currentItem?.add(videoOutput) 81 | setTransformFor(item: item) 82 | player.play() 83 | 84 | let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidReresh(link:))) 85 | displayLink.add(to: .main, forMode: .commonModes) 86 | 87 | } 88 | 89 | fileprivate func setTransformFor(item: AVPlayerItem) { 90 | var transform = CGAffineTransform.identity 91 | let videoTracks = item.asset.tracks(withMediaType: .video) 92 | if videoTracks.count > 0 { 93 | let track = videoTracks.first! 94 | transform = track.preferredTransform 95 | 96 | if transform.b == 1 && transform.c == -1 { 97 | transform = transform.rotated(by: CGFloat(Double.pi)) 98 | } 99 | 100 | let videoSize = track.naturalSize 101 | let viewSize = filterSwipeView.frame.size 102 | let outRect = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height).applying(transform) 103 | 104 | let viewIsWide = viewSize.width / viewSize.height > 1 105 | let videoIsWide = outRect.size.width / outRect.size.height > 1 106 | 107 | if viewIsWide != videoIsWide { 108 | transform = transform.rotated(by: CGFloat(Double.pi/2.0)) 109 | } 110 | } 111 | 112 | galleryVideoTranform = transform 113 | } 114 | 115 | @objc fileprivate func displayLinkDidReresh(link: CADisplayLink) { 116 | let itemTime = videoOutput.itemTime(forHostTime: CACurrentMediaTime()) 117 | 118 | if videoOutput.hasNewPixelBuffer(forItemTime: itemTime) { 119 | if let pixelBuffer = videoOutput.copyPixelBuffer(forItemTime: itemTime, itemTimeForDisplay: nil) { 120 | let image = CIImage(cvImageBuffer: pixelBuffer) 121 | if let transform = galleryVideoTranform { 122 | let img = image.transformed(by: transform) 123 | filterSwipeView.setRenderImage(image: img) 124 | } 125 | } 126 | } 127 | } 128 | 129 | } 130 | 131 | extension GalleryMediaFilteringViewController: DSSwipableFilterViewDataSource { 132 | 133 | func numberOfFilters(_ filterView: DSSwipableFilterView) -> Int { 134 | return filterList.count 135 | } 136 | 137 | func filter(_ filterView: DSSwipableFilterView, filterAtIndex index: Int) -> DSFilter { 138 | return filterList[index] 139 | } 140 | 141 | func startAtIndex(_ filterView: DSSwipableFilterView) -> Int { 142 | return 0 143 | } 144 | 145 | } 146 | 147 | extension GalleryMediaFilteringViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { 148 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 149 | picker.dismiss(animated: true, completion: nil) 150 | } 151 | 152 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { 153 | 154 | let url = info[UIImagePickerControllerMediaURL] 155 | let mediaType = info[UIImagePickerControllerMediaType] as! CFString 156 | picker.dismiss(animated: true, completion:nil) 157 | 158 | if mediaType == kUTTypeMovie { 159 | filterSwipeView.isPlayingLibraryVideo = true 160 | previewVideoWith(url: url as! URL) 161 | }else{ 162 | filterSwipeView.isPlayingLibraryVideo = true 163 | let image = info[UIImagePickerControllerOriginalImage] as! UIImage 164 | preview(image: image) 165 | } 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /DSSwipableFilterView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DSSwipableFilterView/LiveCameraFilteringViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiveCameraFilteringViewController.swift 3 | // DSSwipableFilterView 4 | // 5 | // Created by Darko Spasovski on 11/4/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | class LiveCameraFilteringViewController: UIViewController { 13 | 14 | let filterList = [DSFilter(name: "No Filter", type: .ciFilter), 15 | DSFilter(name: "CIPhotoEffectMono", type: .ciFilter), 16 | DSFilter(name: "CIPhotoEffectChrome", type: .ciFilter), 17 | DSFilter(name: "CIPhotoEffectTransfer", type: .ciFilter), 18 | DSFilter(name: "CIPhotoEffectInstant", type: .ciFilter), 19 | DSFilter(name: "CIPhotoEffectNoir", type: .ciFilter), 20 | DSFilter(name: "CIPhotoEffectProcess", type: .ciFilter), 21 | DSFilter(name: "CIPhotoEffectTonal", type: .ciFilter)] 22 | 23 | let filterSwipeView = DSSwipableFilterView(frame: UIScreen.main.bounds) 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | title = "Live camera filter" 29 | prepareFilterView() 30 | prepareCaptureSession() 31 | // Do any additional setup after loading the view. 32 | } 33 | 34 | override func didReceiveMemoryWarning() { 35 | super.didReceiveMemoryWarning() 36 | // Dispose of any resources that can be recreated. 37 | } 38 | 39 | fileprivate func prepareFilterView() { 40 | 41 | filterSwipeView.dataSource = self 42 | filterSwipeView.isUserInteractionEnabled = true 43 | filterSwipeView.isMultipleTouchEnabled = true 44 | filterSwipeView.isExclusiveTouch = false 45 | 46 | self.view.addSubview(filterSwipeView) 47 | filterSwipeView.reloadData() 48 | } 49 | 50 | fileprivate func prepareCaptureSession() { 51 | let captureSession = AVCaptureSession() 52 | captureSession.sessionPreset = .high 53 | 54 | let backCamera = AVCaptureDevice.default(for: .video) 55 | 56 | do{ 57 | let input = try AVCaptureDeviceInput(device: backCamera!) 58 | captureSession.addInput(input) 59 | }catch{ 60 | print("can't access camera") 61 | return 62 | } 63 | 64 | let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) 65 | previewLayer.videoGravity = .resizeAspect 66 | view.layer.addSublayer(previewLayer) 67 | 68 | let videoOutput = AVCaptureVideoDataOutput() 69 | 70 | let serialQueue = DispatchQueue(label: "dsSwipeViewSerial") 71 | videoOutput.setSampleBufferDelegate(self, queue: serialQueue) 72 | if captureSession.canAddOutput(videoOutput) 73 | { 74 | captureSession.addOutput(videoOutput) 75 | } 76 | videoOutput.connection(with: .video)?.videoOrientation = .portrait 77 | captureSession.startRunning() 78 | } 79 | 80 | 81 | /* 82 | // MARK: - Navigation 83 | 84 | // In a storyboard-based application, you will often want to do a little preparation before navigation 85 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 86 | // Get the new view controller using segue.destinationViewController. 87 | // Pass the selected object to the new view controller. 88 | } 89 | */ 90 | 91 | } 92 | 93 | extension LiveCameraFilteringViewController: AVCaptureVideoDataOutputSampleBufferDelegate { 94 | func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 95 | 96 | let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) 97 | let cameraImage = CIImage(cvPixelBuffer: pixelBuffer!) 98 | 99 | DispatchQueue.main.async { 100 | self.filterSwipeView.setRenderImage(image: cameraImage) 101 | } 102 | } 103 | } 104 | 105 | extension LiveCameraFilteringViewController: DSSwipableFilterViewDataSource { 106 | 107 | func numberOfFilters(_ filterView: DSSwipableFilterView) -> Int { 108 | return filterList.count 109 | } 110 | 111 | func filter(_ filterView: DSSwipableFilterView, filterAtIndex index: Int) -> DSFilter { 112 | return filterList[index] 113 | } 114 | 115 | func startAtIndex(_ filterView: DSSwipableFilterView) -> Int { 116 | return 0 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /DSSwipableFilterView/UIImage+Transform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Transform.swift 3 | // DSSwipableFilterView 4 | // 5 | // Created by Darko Spasovski on 11/4/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | func transformToFixImage() -> CGAffineTransform { 13 | if imageOrientation == .up { 14 | return .identity 15 | } 16 | 17 | var transform = CGAffineTransform.identity 18 | 19 | switch imageOrientation { 20 | case .down, .downMirrored: 21 | transform = transform.translatedBy(x: size.width, y: size.height) 22 | transform = transform.rotated(by: CGFloat(Double.pi)) 23 | break 24 | case .left, .leftMirrored: 25 | transform = transform.translatedBy(x: size.width, y: 0) 26 | transform = transform.rotated(by: CGFloat(Double.pi/2)) 27 | break 28 | case .right, .rightMirrored: 29 | transform = transform.translatedBy(x: 0, y: size.height) 30 | transform = transform.rotated(by: CGFloat(-Double.pi/2)) 31 | break 32 | case .up, .upMirrored: 33 | break; 34 | } 35 | 36 | switch imageOrientation { 37 | case .upMirrored, .downMirrored: 38 | transform = transform.translatedBy(x: size.width, y: 0) 39 | transform = transform.scaledBy(x: -1, y: 1) 40 | break 41 | case .leftMirrored, .rightMirrored: 42 | transform = transform.translatedBy(x: size.height, y: 0) 43 | transform = transform.scaledBy(x: -1, y: 1) 44 | break 45 | case .up, .down, .left, .right: 46 | break 47 | } 48 | 49 | return transform 50 | } 51 | } 52 | 53 | 54 | extension CIImage { 55 | 56 | func scaleAndResize(forRect rect: CGRect, and contentMode: UIViewContentMode) -> CIImage { 57 | 58 | let imageSize = extent.size 59 | 60 | var horizontalScale = rect.size.width / imageSize.width 61 | var verticalScale = rect.size.height / imageSize.height 62 | 63 | let mode = contentMode 64 | 65 | if mode == .scaleAspectFill { 66 | horizontalScale = max(horizontalScale, verticalScale) 67 | verticalScale = horizontalScale 68 | }else if mode == .scaleAspectFit { 69 | horizontalScale = max(horizontalScale, verticalScale) 70 | verticalScale = horizontalScale 71 | } 72 | 73 | return transformed(by: CGAffineTransform(scaleX: horizontalScale, y: verticalScale)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /DSSwipableFilterView/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // DSSwipableFilterView 4 | // 5 | // Created by Darko Spasovski on 11/4/17. 6 | // Copyright © 2017 Darko Spasovski. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var tableView: UITableView! 14 | var examples = ["Live camera filtering example","Media library example"] 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | title = "Example" 20 | tableView.delegate = self 21 | tableView.dataSource = self 22 | navigationController?.interactivePopGestureRecognizer?.isEnabled = false 23 | // Do any additional setup after loading the view, typically from a nib. 24 | } 25 | 26 | override func didReceiveMemoryWarning() { 27 | super.didReceiveMemoryWarning() 28 | // Dispose of any resources that can be recreated. 29 | } 30 | 31 | } 32 | 33 | extension ViewController: UITableViewDelegate, UITableViewDataSource { 34 | 35 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 36 | return examples.count 37 | } 38 | 39 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 40 | 41 | let cell = tableView.dequeueReusableCell(withIdentifier: "textCell") 42 | cell?.textLabel?.text = examples[indexPath.row] 43 | return cell! 44 | } 45 | 46 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 47 | tableView.deselectRow(at: indexPath, animated: false) 48 | if indexPath.row == 0 { 49 | performSegue(withIdentifier: "liveCameraSegue", sender: nil) 50 | }else if indexPath.row == 1 { 51 | performSegue(withIdentifier: "gallerySegue", sender: nil) 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DSSwipableFilterView 2 | 3 | 4 | 5 | Snapchat like swipeable filter view in pure swift. 6 | 7 | Doesn't matter if you want to apply directly to live camera or static image. Simple pass the UIImage/CIImage to the fuction provided. Both examples are provided. 8 | 9 | 10 | ## CocoaPods 11 | 12 | Suport over CocoaPods is coming next. 13 | 14 | 15 | ## Manual install 16 | 17 | Simply drag and drop the **DSSwipableFilterView** folder inside your project in xCode and you are good to go. The library is writen in Swift so if you want to use it in Objective-C project you would need to have the "ProjectName-Swift.h" file autogenerated by xCode for you. 18 | 19 | 20 | ## Usage 21 | 22 | After adding the library to your poject open up your viewcontroller and first inicialize the array of filters that you want to use 23 | 24 | ```Swift 25 | let filterList = [DSFilter(name: "No Filter", type: .ciFilter), 26 | DSFilter(name: "CIPhotoEffectMono", type: .ciFilter), 27 | DSFilter(name: "CIPhotoEffectChrome", type: .ciFilter), 28 | DSFilter(name: "CIPhotoEffectTransfer", type: .ciFilter), 29 | DSFilter(name: "CIPhotoEffectInstant", type: .ciFilter), 30 | DSFilter(name: "CIPhotoEffectNoir", type: .ciFilter), 31 | DSFilter(name: "CIPhotoEffectProcess", type: .ciFilter), 32 | DSFilter(name: "CIPhotoEffectTonal", type: .ciFilter)] 33 | ``` 34 | 35 | ### DSFilter 36 | 37 | DSFilter is providing few inicializers and they are with **Name** and **Type** or you can pass already created **CIFilter** directly. For now the usage of Types other then **ciFilter** and **combinedFilters** is not yet done. 38 | 39 | Initialize with Name and Type 40 | ```Swift 41 | let dsFilter = DSFilter(name: "No Filter", type: .ciFilter), 42 | ``` 43 | Initialize with CIFilter and Type 44 | ```Swift 45 | let filter = CIFilter(name: "CIPhotoEffectChrome")! 46 | let dsFilter = DSFilter(name: "DirectFilter", filter: filter, type: .ciFilter) 47 | ``` 48 | Initialize with Name and CIFIlter, here the type will be set to `.ciFilter` by default 49 | ```Swift 50 | let filter = CIFilter(name: "CIPhotoEffectChrome")! 51 | let dsFilter = DSFilter(name: "Filter", filter: filter) 52 | ``` 53 | This class also supports combination of Two CIFilters, just pass array of two CIFIlters to the init and you are good to go. More then Two coming in the next versions. 54 | 55 | ```Swift 56 | let filterOne = CIFilter(name: "CIPhotoEffectChrome")! 57 | let filterTwo = CIFilter(name: "CIPhotoEffectProcess")! 58 | 59 | let dsCombinedFilter = DSFilter(combinedFilter: [filterOne, filterTwo]) 60 | ``` 61 | **Please use "No Filter" as a name when you want to add No filters to the image** 62 | 63 | ### DSSwipableFilterView 64 | 65 | Next after you have prepared your list of DSFilters you need to prepare the DSSwipableFilterView. Simply add it in your Storyboard, Xib or create it manualy from code with preferred frame or constraints. 66 | 67 | ```Swift 68 | let filterSwipeView = DSSwipableFilterView(frame: UIScreen.main.bounds) 69 | ``` 70 | After allocating your view next is to set some properties: 71 | ```Swift 72 | filterSwipeView.dataSource = self 73 | filterSwipeView.isUserInteractionEnabled = true 74 | filterSwipeView.isMultipleTouchEnabled = true 75 | filterSwipeView.isExclusiveTouch = false 76 | 77 | self.view.addSubview(filterSwipeView) // only if you are adding via code 78 | filterSwipeView.reloadData() 79 | ``` 80 | DSSwipableFilterView provides protocol which has three functions which you need to implement: 81 | ```Swift 82 | // pass the number of filters that you have in your array of DSFilter 83 | func numberOfFilters(_ filterView: DSSwipableFilterView) -> Int 84 | 85 | //return the DSFilter at the given index 86 | func filter(_ filterView: DSSwipableFilterView, filterAtIndex index: Int) -> DSFilter 87 | 88 | //return the index of which you want to start 89 | func startAtIndex(_ filterView: DSSwipableFilterView) -> Int 90 | ``` 91 | 92 | DSSwipableFilterView provides public functions for passing the image that you want to filter, currently supports CIImage and UIImage. When you have your image ready for presentation simply call: 93 | ```Swift 94 | let uiImage = .... get your image 95 | filterSwipeView.setRenderImage(image: uiImage) 96 | ``` 97 | 98 | **If you are adding video from library please use the `isPlayingLibraryVideo` property and set it to `true` or `false` for everything else.** 99 | 100 | Thats it you are good to go! 101 | 102 | Exporting filtered UIImage is also supported just call the public function on DSSwipableFilterView `getFilteredImage() ` 103 | ```Swift 104 | let uiImage = filterSwipeView.getFilteredImage() 105 | ``` 106 | 107 | 108 | ## Next on the list 109 | 110 | - [ ] Add support landscape images without quality loss and disorientation 111 | - [ ] Add support more then Two CIFilters in the Combined DSFilter 112 | - [ ] Expan the library with also Drawing on the images 113 | 114 | 115 | 116 | ### Please feel free to comment and suggest improvements over PR's. 117 | 118 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darko55s/DSSwipableFilterView/f1de2d9c1a70c526f865af232a50209013258806/preview.gif --------------------------------------------------------------------------------