├── LICENSE.txt ├── README.md ├── Screenshots ├── ss_selected.PNG ├── ss_storyboard.PNG └── ss_unselected.PNG ├── VideoThumbnailSelectionView.podspec ├── VideoThumbnailSelectionView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── sosolakoglu.xcuserdatad │ └── xcschemes │ ├── VideoThumbnailSelectionView.xcscheme │ └── xcschememanagement.plist └── VideoThumbnailSelectionView ├── Info.plist ├── ScrollOptions.swift ├── SelectionThumb.swift ├── VideoThumbnailSelectionView.h ├── VideoThumbnailSelectionView.swift └── VideoThumbnailSelectionView.xib /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sarp Ogulcan Solakoglu 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoThumbnailSelectionView 2 | Video thumbnail (cover) selection tool similar to the one in Instagram's app. Written in Swift 2.2. 3 | 4 | ![Unselected](Screenshots/ss_unselected.PNG) 5 | ![Unselected](Screenshots/ss_selected.PNG) 6 | 7 | ## Installation 8 | ### CocoaPods 9 | To add VideoThumbnailSelectionView to your project, you can specify it in your `Podfile`: 10 | ```ruby 11 | source 'https://github.com/CocoaPods/Specs.git' 12 | platform :ios, '8.0' 13 | use_frameworks! 14 | 15 | pod 'VideoThumbnailSelectionView', '~>0.1.1' 16 | ``` 17 | 18 | ## Usage 19 | ### Initialization 20 | #### Storyboard 21 | You can add a UIView to your Storyboard and set both the **Class** and the **Module** properties to **VideoThumbnailSelectionView**. 22 | 23 | ![storyboard](Screenshots/ss_storyboard.PNG) 24 | 25 | After that link with the relevant IBOutlet. 26 | 27 | **Swift** 28 | ```swift 29 | @IBOUtlet weak var videoThumbnailSelectionView: VideoThumbnailSelectionView! 30 | ``` 31 | 32 | **Obj-c** 33 | ```obj-c 34 | @property (weak, nonatomic) IBOutlet VideoThumbnailSelectionView *videoThumbnailSelectionView; 35 | ``` 36 | 37 | #### Code 38 | You can also init the view from code. 39 | 40 | **Swift** 41 | ```swift 42 | import VideoThumbnailSelectionView 43 | . 44 | . 45 | var videoThumbnailSelectionView: VideoThumbnailSelectionView! 46 | . 47 | . 48 | . 49 | videoThumbnailSelectionView = VideoThumbnailSelectionView(frame:CGRectMake(20.0, 20.0, 325.0, 60.0)) 50 | view.addSubview(selectionView) 51 | ``` 52 | 53 | **Obj-c** 54 | ```obj-c 55 | @import VideoThumbnailSelectionView; 56 | . 57 | . 58 | @property (strong, nonatomic) VideoThumbnailSelectionView * videoThumbnailSelectionView; 59 | . 60 | . 61 | . 62 | _videoThumbnailSelectionView = [[VideoThumbnailSelectionView alloc] initWithFrame:CGRectMake(20.0, 20.0, 325.0, 60.0)]; 63 | [self.view addSubview:selectionView]; 64 | ``` 65 | 66 | ### Customization 67 | 68 | **Swift** 69 | ```swift 70 | //Alpha of the black tint on the thumbnails behind the thumb 71 | videoThumbnailSelectionView.shadeTintAlpha = 0.5 72 | //scale animation when thumb is dragged (1.0 for no animation). 73 | videoThumbnailSelectionView.zoomAnimationScale = 1.5 74 | //corner color 75 | videoThumbnailSelectionView.cornerColor = .whiteColor() 76 | //corner radius 77 | videoThumbnailSelectionView.cornerRadius = 8.0 78 | //corner thickness 79 | videoThumbnailSelectionView.cornerInsets = UIEdgeInsetsMake(6.0, 6.0, 6.0, 6.0) 80 | //called when selection changes 81 | videoThumbnailSelectionView.onUpdatedImage = { image in 82 | //do stuff with the image 83 | }; 84 | ``` 85 | 86 | **Obj-c** 87 | ```obj-c 88 | //Alpha of the black tint on the thumbnails behind the thumb 89 | _videoThumbnailSelectionView.shadeTintAlpha = 0.5; 90 | //scale animation when thumb is dragged (1.0 for no animation). 91 | _videoThumbnailSelectionView.zoomAnimationScale = 1.5; 92 | //corner color 93 | _videoThumbnailSelectionView.cornerColor = [UIColor whiteColor]; 94 | //corner radius 95 | _videoThumbnailSelectionView.cornerRadius = 8.0; 96 | //corner thickness 97 | _videoThumbnailSelectionView.cornerInsets = UIEdgeInsetsMake(6.0, 6.0, 6.0, 6.0); 98 | //called when selection changes 99 | _videoThumbnailSelectionView.onUpdatedImage = ^(UIImage *image){ 100 | //do stuff with the image 101 | }; 102 | ``` 103 | 104 | ### Loading 105 | 106 | **Swift** 107 | ```swift 108 | override func viewWillAppear(animated: Bool) { 109 | super.viewWillAppear(animated) 110 | //after customization, call this to load your asset. 111 | thumbnailSelectionView.loadVideo(asset) 112 | } 113 | ``` 114 | 115 | **Obj-c** 116 | ```obj-c 117 | - (void)viewWillAppear:(BOOL)animated { 118 | [super viewWillAppear:animated]; 119 | //after customization, call this to load your asset. 120 | [_videoThumbnailSelectionView loadVideo:asset]; 121 | } 122 | ``` 123 | 124 | ### Deleting 125 | 126 | If you need to load a new video you must do this before calling load again. 127 | 128 | **Swift** 129 | ```swift 130 | thumbnailSelectionView.deleteVideo() 131 | ``` 132 | 133 | **Obj-c** 134 | ``` 135 | [_videoThumbnailSelectionView deleteVideo]; 136 | ``` 137 | 138 | ##Notes 139 | Make sure you have 'define modules' enabled in build settings. 140 | 141 | ## Licence 142 | VideoThumbnailSelectionView is available under the MIT license. 143 | 144 | ## Author 145 | Sarp Solakoglu - sosolakoglu@gmail.com 146 | 147 | [@Follow me on Twitter](http://twitter.com/sarpsolakoglu) 148 | 149 | -------------------------------------------------------------------------------- /Screenshots/ss_selected.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarpsolakoglu/VideoThumbnailSelectionView/7227107ca6240430e493c4e03eb0d23447258abe/Screenshots/ss_selected.PNG -------------------------------------------------------------------------------- /Screenshots/ss_storyboard.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarpsolakoglu/VideoThumbnailSelectionView/7227107ca6240430e493c4e03eb0d23447258abe/Screenshots/ss_storyboard.PNG -------------------------------------------------------------------------------- /Screenshots/ss_unselected.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarpsolakoglu/VideoThumbnailSelectionView/7227107ca6240430e493c4e03eb0d23447258abe/Screenshots/ss_unselected.PNG -------------------------------------------------------------------------------- /VideoThumbnailSelectionView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "VideoThumbnailSelectionView" 4 | s.version = "0.1.1" 5 | s.summary = "Easy drag and drop library for Instagram like video thumbnail selection." 6 | 7 | s.description = <<-DESC 8 | This library is a drag and drop component for easy video thumbnail selection that is 9 | similar to Instagram's video cover selection. I am currently using this component 10 | actively in a project. 11 | DESC 12 | 13 | s.homepage = "https://github.com/sarpsolakoglu/VideoThumbnailSelectionView" 14 | 15 | s.license = "MIT" 16 | 17 | s.author = { "Sarp Ogulcan Solakoglu" => "sosolakoglu@gmail.com" } 18 | s.social_media_url = "http://twitter.com/sarpsolakoglu" 19 | 20 | s.platform = :ios, "8.0" 21 | 22 | s.source = { :git => "https://github.com/sarpsolakoglu/VideoThumbnailSelectionView.git", :tag => "0.1.1" } 23 | 24 | s.source_files = "VideoThumbnailSelectionView/*.{swift}" 25 | s.resources = "VideoThumbnailSelectionView/*.{xib}" 26 | 27 | s.frameworks = "UIKit", "AVFoundation" 28 | 29 | end 30 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E91E33781D017AB2004EF96C /* VideoThumbnailSelectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = E91E33771D017AB2004EF96C /* VideoThumbnailSelectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | E91E33831D017B4F004EF96C /* ScrollOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91E337F1D017B4F004EF96C /* ScrollOptions.swift */; }; 12 | E91E33841D017B4F004EF96C /* SelectionThumb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91E33801D017B4F004EF96C /* SelectionThumb.swift */; }; 13 | E91E33851D017B4F004EF96C /* VideoThumbnailSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91E33811D017B4F004EF96C /* VideoThumbnailSelectionView.swift */; }; 14 | E91E33861D017B4F004EF96C /* VideoThumbnailSelectionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E91E33821D017B4F004EF96C /* VideoThumbnailSelectionView.xib */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | E91E33741D017AB2004EF96C /* VideoThumbnailSelectionView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VideoThumbnailSelectionView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | E91E33771D017AB2004EF96C /* VideoThumbnailSelectionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VideoThumbnailSelectionView.h; sourceTree = ""; }; 20 | E91E33791D017AB2004EF96C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 21 | E91E337F1D017B4F004EF96C /* ScrollOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollOptions.swift; sourceTree = ""; }; 22 | E91E33801D017B4F004EF96C /* SelectionThumb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionThumb.swift; sourceTree = ""; }; 23 | E91E33811D017B4F004EF96C /* VideoThumbnailSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoThumbnailSelectionView.swift; sourceTree = ""; }; 24 | E91E33821D017B4F004EF96C /* VideoThumbnailSelectionView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VideoThumbnailSelectionView.xib; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | E91E33701D017AB2004EF96C /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | ); 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXFrameworksBuildPhase section */ 36 | 37 | /* Begin PBXGroup section */ 38 | E91E336A1D017AB2004EF96C = { 39 | isa = PBXGroup; 40 | children = ( 41 | E91E33761D017AB2004EF96C /* VideoThumbnailSelectionView */, 42 | E91E33751D017AB2004EF96C /* Products */, 43 | ); 44 | sourceTree = ""; 45 | }; 46 | E91E33751D017AB2004EF96C /* Products */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | E91E33741D017AB2004EF96C /* VideoThumbnailSelectionView.framework */, 50 | ); 51 | name = Products; 52 | sourceTree = ""; 53 | }; 54 | E91E33761D017AB2004EF96C /* VideoThumbnailSelectionView */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | E91E337F1D017B4F004EF96C /* ScrollOptions.swift */, 58 | E91E33801D017B4F004EF96C /* SelectionThumb.swift */, 59 | E91E33811D017B4F004EF96C /* VideoThumbnailSelectionView.swift */, 60 | E91E33821D017B4F004EF96C /* VideoThumbnailSelectionView.xib */, 61 | E91E33771D017AB2004EF96C /* VideoThumbnailSelectionView.h */, 62 | E91E33791D017AB2004EF96C /* Info.plist */, 63 | ); 64 | path = VideoThumbnailSelectionView; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXHeadersBuildPhase section */ 70 | E91E33711D017AB2004EF96C /* Headers */ = { 71 | isa = PBXHeadersBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | E91E33781D017AB2004EF96C /* VideoThumbnailSelectionView.h in Headers */, 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | /* End PBXHeadersBuildPhase section */ 79 | 80 | /* Begin PBXNativeTarget section */ 81 | E91E33731D017AB2004EF96C /* VideoThumbnailSelectionView */ = { 82 | isa = PBXNativeTarget; 83 | buildConfigurationList = E91E337C1D017AB2004EF96C /* Build configuration list for PBXNativeTarget "VideoThumbnailSelectionView" */; 84 | buildPhases = ( 85 | E91E336F1D017AB2004EF96C /* Sources */, 86 | E91E33701D017AB2004EF96C /* Frameworks */, 87 | E91E33711D017AB2004EF96C /* Headers */, 88 | E91E33721D017AB2004EF96C /* Resources */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = VideoThumbnailSelectionView; 95 | productName = VideoThumbnailSelectionView; 96 | productReference = E91E33741D017AB2004EF96C /* VideoThumbnailSelectionView.framework */; 97 | productType = "com.apple.product-type.framework"; 98 | }; 99 | /* End PBXNativeTarget section */ 100 | 101 | /* Begin PBXProject section */ 102 | E91E336B1D017AB2004EF96C /* Project object */ = { 103 | isa = PBXProject; 104 | attributes = { 105 | LastUpgradeCheck = 0730; 106 | ORGANIZATIONNAME = "Sarp Solakoğlu"; 107 | TargetAttributes = { 108 | E91E33731D017AB2004EF96C = { 109 | CreatedOnToolsVersion = 7.3.1; 110 | }; 111 | }; 112 | }; 113 | buildConfigurationList = E91E336E1D017AB2004EF96C /* Build configuration list for PBXProject "VideoThumbnailSelectionView" */; 114 | compatibilityVersion = "Xcode 3.2"; 115 | developmentRegion = English; 116 | hasScannedForEncodings = 0; 117 | knownRegions = ( 118 | en, 119 | ); 120 | mainGroup = E91E336A1D017AB2004EF96C; 121 | productRefGroup = E91E33751D017AB2004EF96C /* Products */; 122 | projectDirPath = ""; 123 | projectRoot = ""; 124 | targets = ( 125 | E91E33731D017AB2004EF96C /* VideoThumbnailSelectionView */, 126 | ); 127 | }; 128 | /* End PBXProject section */ 129 | 130 | /* Begin PBXResourcesBuildPhase section */ 131 | E91E33721D017AB2004EF96C /* Resources */ = { 132 | isa = PBXResourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | E91E33861D017B4F004EF96C /* VideoThumbnailSelectionView.xib in Resources */, 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | /* End PBXResourcesBuildPhase section */ 140 | 141 | /* Begin PBXSourcesBuildPhase section */ 142 | E91E336F1D017AB2004EF96C /* Sources */ = { 143 | isa = PBXSourcesBuildPhase; 144 | buildActionMask = 2147483647; 145 | files = ( 146 | E91E33851D017B4F004EF96C /* VideoThumbnailSelectionView.swift in Sources */, 147 | E91E33831D017B4F004EF96C /* ScrollOptions.swift in Sources */, 148 | E91E33841D017B4F004EF96C /* SelectionThumb.swift in Sources */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | /* End PBXSourcesBuildPhase section */ 153 | 154 | /* Begin XCBuildConfiguration section */ 155 | E91E337A1D017AB2004EF96C /* Debug */ = { 156 | isa = XCBuildConfiguration; 157 | buildSettings = { 158 | ALWAYS_SEARCH_USER_PATHS = NO; 159 | CLANG_ANALYZER_NONNULL = YES; 160 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 161 | CLANG_CXX_LIBRARY = "libc++"; 162 | CLANG_ENABLE_MODULES = YES; 163 | CLANG_ENABLE_OBJC_ARC = YES; 164 | CLANG_WARN_BOOL_CONVERSION = YES; 165 | CLANG_WARN_CONSTANT_CONVERSION = YES; 166 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 167 | CLANG_WARN_EMPTY_BODY = YES; 168 | CLANG_WARN_ENUM_CONVERSION = YES; 169 | CLANG_WARN_INT_CONVERSION = YES; 170 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 171 | CLANG_WARN_UNREACHABLE_CODE = YES; 172 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 173 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 174 | COPY_PHASE_STRIP = NO; 175 | CURRENT_PROJECT_VERSION = 1; 176 | DEBUG_INFORMATION_FORMAT = dwarf; 177 | ENABLE_STRICT_OBJC_MSGSEND = YES; 178 | ENABLE_TESTABILITY = YES; 179 | GCC_C_LANGUAGE_STANDARD = gnu99; 180 | GCC_DYNAMIC_NO_PIC = NO; 181 | GCC_NO_COMMON_BLOCKS = YES; 182 | GCC_OPTIMIZATION_LEVEL = 0; 183 | GCC_PREPROCESSOR_DEFINITIONS = ( 184 | "DEBUG=1", 185 | "$(inherited)", 186 | ); 187 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 188 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 189 | GCC_WARN_UNDECLARED_SELECTOR = YES; 190 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 191 | GCC_WARN_UNUSED_FUNCTION = YES; 192 | GCC_WARN_UNUSED_VARIABLE = YES; 193 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 194 | MTL_ENABLE_DEBUG_INFO = YES; 195 | ONLY_ACTIVE_ARCH = YES; 196 | SDKROOT = iphoneos; 197 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 198 | TARGETED_DEVICE_FAMILY = "1,2"; 199 | VERSIONING_SYSTEM = "apple-generic"; 200 | VERSION_INFO_PREFIX = ""; 201 | }; 202 | name = Debug; 203 | }; 204 | E91E337B1D017AB2004EF96C /* Release */ = { 205 | isa = XCBuildConfiguration; 206 | buildSettings = { 207 | ALWAYS_SEARCH_USER_PATHS = NO; 208 | CLANG_ANALYZER_NONNULL = YES; 209 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 210 | CLANG_CXX_LIBRARY = "libc++"; 211 | CLANG_ENABLE_MODULES = YES; 212 | CLANG_ENABLE_OBJC_ARC = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_CONSTANT_CONVERSION = YES; 215 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 216 | CLANG_WARN_EMPTY_BODY = YES; 217 | CLANG_WARN_ENUM_CONVERSION = YES; 218 | CLANG_WARN_INT_CONVERSION = YES; 219 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 220 | CLANG_WARN_UNREACHABLE_CODE = YES; 221 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 222 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 223 | COPY_PHASE_STRIP = NO; 224 | CURRENT_PROJECT_VERSION = 1; 225 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 226 | ENABLE_NS_ASSERTIONS = NO; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | GCC_C_LANGUAGE_STANDARD = gnu99; 229 | GCC_NO_COMMON_BLOCKS = YES; 230 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 231 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 232 | GCC_WARN_UNDECLARED_SELECTOR = YES; 233 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 234 | GCC_WARN_UNUSED_FUNCTION = YES; 235 | GCC_WARN_UNUSED_VARIABLE = YES; 236 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 237 | MTL_ENABLE_DEBUG_INFO = NO; 238 | SDKROOT = iphoneos; 239 | TARGETED_DEVICE_FAMILY = "1,2"; 240 | VALIDATE_PRODUCT = YES; 241 | VERSIONING_SYSTEM = "apple-generic"; 242 | VERSION_INFO_PREFIX = ""; 243 | }; 244 | name = Release; 245 | }; 246 | E91E337D1D017AB2004EF96C /* Debug */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | CLANG_ENABLE_MODULES = YES; 250 | DEFINES_MODULE = YES; 251 | DYLIB_COMPATIBILITY_VERSION = 1; 252 | DYLIB_CURRENT_VERSION = 1; 253 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 254 | INFOPLIST_FILE = VideoThumbnailSelectionView/Info.plist; 255 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 256 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 257 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 258 | PRODUCT_BUNDLE_IDENTIFIER = com.sarpsolakoglu.VideoThumbnailSelectionView; 259 | PRODUCT_NAME = "$(TARGET_NAME)"; 260 | SKIP_INSTALL = YES; 261 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 262 | }; 263 | name = Debug; 264 | }; 265 | E91E337E1D017AB2004EF96C /* Release */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | CLANG_ENABLE_MODULES = YES; 269 | DEFINES_MODULE = YES; 270 | DYLIB_COMPATIBILITY_VERSION = 1; 271 | DYLIB_CURRENT_VERSION = 1; 272 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 273 | INFOPLIST_FILE = VideoThumbnailSelectionView/Info.plist; 274 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 275 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 276 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 277 | PRODUCT_BUNDLE_IDENTIFIER = com.sarpsolakoglu.VideoThumbnailSelectionView; 278 | PRODUCT_NAME = "$(TARGET_NAME)"; 279 | SKIP_INSTALL = YES; 280 | }; 281 | name = Release; 282 | }; 283 | /* End XCBuildConfiguration section */ 284 | 285 | /* Begin XCConfigurationList section */ 286 | E91E336E1D017AB2004EF96C /* Build configuration list for PBXProject "VideoThumbnailSelectionView" */ = { 287 | isa = XCConfigurationList; 288 | buildConfigurations = ( 289 | E91E337A1D017AB2004EF96C /* Debug */, 290 | E91E337B1D017AB2004EF96C /* Release */, 291 | ); 292 | defaultConfigurationIsVisible = 0; 293 | defaultConfigurationName = Release; 294 | }; 295 | E91E337C1D017AB2004EF96C /* Build configuration list for PBXNativeTarget "VideoThumbnailSelectionView" */ = { 296 | isa = XCConfigurationList; 297 | buildConfigurations = ( 298 | E91E337D1D017AB2004EF96C /* Debug */, 299 | E91E337E1D017AB2004EF96C /* Release */, 300 | ); 301 | defaultConfigurationIsVisible = 0; 302 | }; 303 | /* End XCConfigurationList section */ 304 | }; 305 | rootObject = E91E336B1D017AB2004EF96C /* Project object */; 306 | } 307 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView.xcodeproj/xcuserdata/sosolakoglu.xcuserdatad/xcschemes/VideoThumbnailSelectionView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView.xcodeproj/xcuserdata/sosolakoglu.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | VideoThumbnailSelectionView.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | E91E33731D017AB2004EF96C 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView/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 | FMWK 17 | CFBundleShortVersionString 18 | 0.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView/ScrollOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollOptions.swift 3 | // VideoThumbnailSelectionView 4 | // 5 | // Created by Sarp Solakoğlu on 02/06/16. 6 | // Copyright © 2016 Sarp Solakoğlu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct ScrollOptions { 12 | 13 | var startPoint: CGFloat = 0.0 14 | var endPoint: CGFloat = 0.0 15 | 16 | var currentlyScrolling = false 17 | var thumbStartLocation: CGFloat = 0.0 18 | var scrollStartLocation: CGFloat = 0.0 19 | var currentLocation: CGFloat = 0.0 20 | 21 | var scrollPercent: CGFloat { 22 | get { 23 | return (currentLocation - startPoint) / (endPoint - startPoint) 24 | } 25 | } 26 | 27 | init(startPoint: CGFloat, endPoint: CGFloat) { 28 | self.startPoint = startPoint 29 | self.endPoint = endPoint 30 | } 31 | 32 | mutating func getNewLocationAccordingToPoint(x: CGFloat) -> CGFloat { 33 | let change = x - scrollStartLocation 34 | var newPoint = thumbStartLocation + change 35 | if newPoint < startPoint { 36 | newPoint = startPoint 37 | } 38 | if newPoint > endPoint { 39 | newPoint = endPoint 40 | } 41 | currentLocation = newPoint 42 | return newPoint 43 | } 44 | 45 | mutating func reset() { 46 | scrollStartLocation = 0.0 47 | currentlyScrolling = false 48 | thumbStartLocation = 0.0 49 | currentLocation = 0.0 50 | } 51 | } -------------------------------------------------------------------------------- /VideoThumbnailSelectionView/SelectionThumb.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionThumb.swift 3 | // VideoThumbnailSelectionView 4 | // 5 | // Created by Sarp Solakoğlu on 02/06/16. 6 | // Copyright © 2016 Sarp Solakoğlu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SelectionThumb: UIView { 12 | var previewImageView: UIImageView! 13 | 14 | override init(frame: CGRect) { 15 | previewImageView = UIImageView(frame: CGRectMake(2, 2, frame.size.width - 4, frame.size.height - 4)) 16 | super.init(frame: frame) 17 | backgroundColor = .whiteColor() 18 | addSubview(previewImageView) 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView/VideoThumbnailSelectionView.h: -------------------------------------------------------------------------------- 1 | // 2 | // VideoThumbnailSelectionView.h 3 | // VideoThumbnailSelectionView 4 | // 5 | // Created by Sarp Solakoğlu on 03/06/16. 6 | // Copyright © 2016 Sarp Solakoğlu. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for VideoThumbnailSelectionView. 12 | FOUNDATION_EXPORT double VideoThumbnailSelectionViewVersionNumber; 13 | 14 | //! Project version string for VideoThumbnailSelectionView. 15 | FOUNDATION_EXPORT const unsigned char VideoThumbnailSelectionViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView/VideoThumbnailSelectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoThumbnailSelectionView.swift 3 | // VideoThumbnailSelectionView 4 | // 5 | // Created by Sarp Solakoğlu on 30/05/16. 6 | // Copyright © 2016 Sarp Solakoğlu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | @objc public class VideoThumbnailSelectionView: UIView { 13 | 14 | //MARK: - Private params 15 | 16 | @IBOutlet private var view: UIView! 17 | @IBOutlet weak private var thumbnailView: UIView! 18 | @IBOutlet weak private var activityIndicator: UIActivityIndicatorView! 19 | @IBOutlet weak private var shadeView: UIView! 20 | @IBOutlet weak private var leftMargin: NSLayoutConstraint! 21 | @IBOutlet weak private var topMargin: NSLayoutConstraint! 22 | @IBOutlet weak private var rightMargin: NSLayoutConstraint! 23 | @IBOutlet weak private var bottomMargin: NSLayoutConstraint! 24 | 25 | private var selectionThumb: SelectionThumb? 26 | private var scrollOptions: ScrollOptions? 27 | private var thumbnails: [UIImageView] = [] 28 | private var videoLoaded = false 29 | private var videoLoading = false 30 | private var shouldUpdateFrame = true 31 | private var generator: AVAssetImageGenerator? 32 | private var asset: AVAsset? { 33 | didSet { 34 | if asset != nil { 35 | generator = AVAssetImageGenerator(asset: asset!) 36 | generator!.requestedTimeToleranceBefore = kCMTimeZero 37 | generator!.requestedTimeToleranceAfter = kCMTimeZero 38 | } else { 39 | generator = nil 40 | } 41 | } 42 | } 43 | 44 | //MARK: - Public params & Customization 45 | 46 | /** 47 | View calls this block when selection changes. Should be set before loading video. 48 | */ 49 | public var onUpdatedImage: (UIImage)->() = { _ in } 50 | 51 | 52 | /** 53 | The opacity of the black shade color on the thumbnails. Defaults to 0.5. 54 | */ 55 | public var shadeTintAlpha: CGFloat = 0.5 { 56 | didSet { 57 | shadeView.alpha = shadeTintAlpha 58 | } 59 | } 60 | 61 | /** 62 | The zoom level when the thumbnail is selected. 1.0 can be given for no animation. Defaults to 1.5. 63 | */ 64 | public var zoomAnimationScale: CGFloat = 1.5 65 | 66 | public var cornerRadius: CGFloat = 0 { 67 | didSet { 68 | layer.cornerRadius = cornerRadius 69 | view.layer.cornerRadius = cornerRadius 70 | thumbnailView.layer.cornerRadius = cornerRadius - 2 71 | shadeView.layer.cornerRadius = cornerRadius - 2 72 | } 73 | } 74 | 75 | /** 76 | The color of the corner of the view. 77 | */ 78 | public var cornerColor: UIColor = .whiteColor() { 79 | didSet { 80 | view.backgroundColor = cornerColor 81 | } 82 | } 83 | 84 | /** 85 | The corner thickness. 86 | */ 87 | public var cornerInsets: UIEdgeInsets = UIEdgeInsetsMake(8.0, 8.0, 8.0, 8.0) { 88 | didSet { 89 | topMargin.constant = cornerInsets.top 90 | leftMargin.constant = cornerInsets.left 91 | bottomMargin.constant = cornerInsets.bottom 92 | rightMargin.constant = cornerInsets.right 93 | layoutIfNeeded() 94 | } 95 | } 96 | 97 | required public init?(coder aDecoder: NSCoder) { 98 | super.init(coder: aDecoder) 99 | setup(frame: nil) 100 | } 101 | 102 | override public init(frame: CGRect) { 103 | super.init(frame: frame) 104 | setup(frame: frame) 105 | } 106 | 107 | /** 108 | Returns the snapshot of the video at that second. "loadVideo" should have been called before. 109 | 110 | - parameters: 111 | - forSecond: The snapshot time in seconds. 112 | - completion: UIImage is returned in this block if succesful. 113 | - failure: This block is called in case of failure. 114 | */ 115 | @objc public func snapshot(forSecond second: Float64, completion:(UIImage)->(), failure:(()->())?) { 116 | guard let generator = generator else { 117 | if failure != nil { 118 | failure!() 119 | } 120 | return 121 | } 122 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 123 | if let image = self.generateThumbnail(generator: generator, second: second) { 124 | dispatch_async(dispatch_get_main_queue(), { 125 | completion(image) 126 | }) 127 | return 128 | } else { 129 | if failure != nil { 130 | failure!() 131 | } 132 | return 133 | } 134 | } 135 | } 136 | 137 | //MARK: - Video 138 | 139 | /** 140 | Loads the video and generates the thumbnails on the view. This should be called first. 141 | 142 | - parameters: 143 | - asset: AVAsset that should be loaded. 144 | */ 145 | @objc public func loadVideo(asset: AVAsset) { 146 | 147 | if videoLoaded { return } 148 | if videoLoading { return } 149 | 150 | guard let assetVideoTrack : AVAssetTrack = asset.tracksWithMediaType(AVMediaTypeVideo)[0] else { return } 151 | 152 | self.asset = asset 153 | 154 | activityIndicator.startAnimating() 155 | 156 | dispatch_async(dispatch_get_main_queue()) { 157 | let size = assetVideoTrack.naturalSize 158 | let thumbnailHeight = self.thumbnailView.bounds.size.height 159 | let videoAspect = size.width / size.height 160 | let thumbnailWidth = thumbnailHeight * videoAspect 161 | 162 | let generator = AVAssetImageGenerator(asset: asset) 163 | generator.requestedTimeToleranceBefore = kCMTimeZero 164 | generator.requestedTimeToleranceAfter = kCMTimeZero 165 | 166 | let thumbnailCount = Int(ceil(self.thumbnailView.bounds.size.width / thumbnailWidth)) 167 | let videoDuration = CMTimeGetSeconds(asset.duration) 168 | let sampleInterval = videoDuration / Float64(thumbnailCount) 169 | 170 | self.selectionThumb = SelectionThumb(frame: CGRectMake(self.thumbnailView.frame.origin.x, self.thumbnailView.frame.origin.y, thumbnailWidth, thumbnailHeight)) 171 | 172 | self.scrollOptions = ScrollOptions(startPoint: self.leftMargin.constant + thumbnailWidth / 2, endPoint: self.view.bounds.size.width - thumbnailWidth / 2 - self.rightMargin.constant) 173 | 174 | var currentTime: Float64 = 0.0 175 | var currentX: CGFloat = 0.0 176 | for i in (0.. videoDuration { break } 178 | let image = self.generateThumbnail(generator: generator, second: currentTime) 179 | if i == 0 { 180 | self.selectionThumb!.previewImageView.image = image 181 | self.view.addSubview(self.selectionThumb!) 182 | } 183 | let imageView = UIImageView(frame: CGRectMake(currentX, 0.0, thumbnailWidth, thumbnailHeight)) 184 | imageView.contentMode = .ScaleAspectFill 185 | imageView.image = image 186 | self.thumbnailView.addSubview(imageView) 187 | self.thumbnails.append(imageView) 188 | currentX += thumbnailWidth 189 | currentTime += sampleInterval 190 | } 191 | self.activityIndicator.stopAnimating() 192 | self.videoLoaded = true 193 | self.videoLoading = false 194 | self.didScrollToPercent(0, override: true) 195 | } 196 | } 197 | 198 | /** 199 | Deletes the video and returns the view to blank state. "loadVideo" should be called if view is going to be used again. 200 | */ 201 | @objc public func deleteVideo() { 202 | if !videoLoaded { return } 203 | if videoLoading { return } 204 | 205 | for imageView in thumbnails { 206 | imageView.removeFromSuperview() 207 | } 208 | 209 | thumbnails.removeAll() 210 | 211 | selectionThumb!.removeFromSuperview() 212 | selectionThumb = nil 213 | scrollOptions = nil 214 | 215 | shouldUpdateFrame = true 216 | videoLoaded = false 217 | self.asset = nil 218 | } 219 | 220 | private func didScrollToPercent(percent: CGFloat, override: Bool) { 221 | 222 | if !shouldUpdateFrame && !override { return } 223 | 224 | guard let asset = asset else { return } 225 | 226 | shouldUpdateFrame = false 227 | let videoDuration = CMTimeGetSeconds(asset.duration) 228 | snapshot(forSecond: videoDuration * Float64(percent), completion: {[weak self] image in 229 | self?.selectionThumb?.previewImageView.image = image 230 | self?.onUpdatedImage(image) 231 | self?.shouldUpdateFrame = true 232 | }, failure: {[weak self] in 233 | self?.shouldUpdateFrame = true 234 | }) 235 | } 236 | 237 | private func generateThumbnail(generator generator: AVAssetImageGenerator, second: Float64) -> UIImage? { 238 | let time = CMTimeMake(Int64(second * 60), 60) 239 | do { 240 | let imgRef = try generator.copyCGImageAtTime(time, actualTime: nil) 241 | return UIImage(CGImage: imgRef) 242 | } catch { 243 | return nil 244 | } 245 | } 246 | 247 | //MARK: - Private loader functions 248 | private func setup(frame frame: CGRect?) { 249 | loadNib() 250 | if frame != nil { 251 | view.frame = CGRectMake(0, 0, CGRectGetWidth(frame!), CGRectGetHeight(frame!)) 252 | } 253 | addSubview(view) 254 | pinView() 255 | thumbnailView.layer.masksToBounds = true 256 | } 257 | 258 | private func loadNib() { 259 | NSBundle(forClass: VideoThumbnailSelectionView.self).loadNibNamed("VideoThumbnailSelectionView", owner: self, options: nil) 260 | } 261 | 262 | private func pinView() { 263 | view.translatesAutoresizingMaskIntoConstraints = false 264 | NSLayoutConstraint(item: view, attribute: .Leading, relatedBy: .Equal, toItem: self , attribute: .Leading, multiplier: 1, constant: 0).active = true 265 | NSLayoutConstraint(item: view, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1, constant: 0).active = true 266 | NSLayoutConstraint(item: view, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0).active = true 267 | NSLayoutConstraint(item: view, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0).active = true 268 | setNeedsLayout() 269 | layoutIfNeeded() 270 | } 271 | 272 | //MARK: - Touches 273 | override public func touchesBegan(touches: Set, withEvent event: UIEvent?) { 274 | if scrollOptions == nil { return } 275 | guard let selectionThumb = selectionThumb else { return } 276 | if let touch = touches.first { 277 | let loc = touch.locationInView(thumbnailView) 278 | if CGRectContainsPoint(selectionThumb.frame, loc) { 279 | scrollOptions!.thumbStartLocation = selectionThumb.center.x 280 | scrollOptions!.scrollStartLocation = loc.x 281 | scrollOptions!.currentlyScrolling = true 282 | 283 | if zoomAnimationScale > 1.0 { 284 | selectionThumb.layer.removeAllAnimations() 285 | UIView.animateWithDuration(0.2, animations: { 286 | selectionThumb.layer.transform = CATransform3DScale(CATransform3DIdentity, self.zoomAnimationScale, self.zoomAnimationScale, 1.0) 287 | }) 288 | } 289 | } 290 | } 291 | 292 | } 293 | 294 | override public func touchesMoved(touches: Set, withEvent event: UIEvent?) { 295 | if scrollOptions == nil { return } 296 | if !(scrollOptions!.currentlyScrolling) { return } 297 | guard let selectionThumb = selectionThumb else { return } 298 | if let touch = touches.first { 299 | let loc = touch.locationInView(thumbnailView) 300 | let newX = scrollOptions!.getNewLocationAccordingToPoint(loc.x) 301 | selectionThumb.center.x = newX 302 | //call view 303 | didScrollToPercent(scrollOptions!.scrollPercent, override: false) 304 | } 305 | } 306 | 307 | override public func touchesEnded(touches: Set, withEvent event: UIEvent?) { 308 | if scrollOptions == nil { return } 309 | if !(scrollOptions!.currentlyScrolling) { return } 310 | guard let selectionThumb = selectionThumb else { return } 311 | didScrollToPercent(scrollOptions!.scrollPercent, override: true) 312 | if zoomAnimationScale > 1.0 { 313 | selectionThumb.layer.removeAllAnimations() 314 | UIView.animateWithDuration(0.2, animations: { 315 | selectionThumb.layer.transform = CATransform3DIdentity 316 | }) 317 | } 318 | scrollOptions!.reset() 319 | } 320 | 321 | } 322 | -------------------------------------------------------------------------------- /VideoThumbnailSelectionView/VideoThumbnailSelectionView.xib: -------------------------------------------------------------------------------- 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 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | --------------------------------------------------------------------------------