diff --git a/Example/SRC/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/SRC/Assets.xcassets/AppIcon.appiconset/Contents.json index eeea76c..1d060ed 100644 --- a/Example/SRC/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/SRC/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", @@ -30,6 +40,16 @@ "size" : "60x60", "scale" : "3x" }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, { "idiom" : "ipad", "size" : "29x29", diff --git a/Example/SRC/Base.lproj/LaunchScreen.storyboard b/Example/SRC/Base.lproj/LaunchScreen.storyboard index 2e721e1..93abbb1 100644 --- a/Example/SRC/Base.lproj/LaunchScreen.storyboard +++ b/Example/SRC/Base.lproj/LaunchScreen.storyboard @@ -1,7 +1,8 @@ - - + + - + + @@ -13,10 +14,9 @@ - + - - + diff --git a/Example/SRC/Base.lproj/Main.storyboard b/Example/SRC/Base.lproj/Main.storyboard index 012fc2b..e201333 100644 --- a/Example/SRC/Base.lproj/Main.storyboard +++ b/Example/SRC/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - - + + - - + + @@ -15,31 +15,31 @@ - + - - - + + + - - + @@ -50,24 +50,23 @@ - - + - + - - + + @@ -76,30 +75,25 @@ - + - - + - - - - + - + @@ -110,9 +104,8 @@ - - + @@ -131,28 +124,31 @@ - + @@ -168,7 +164,6 @@ - @@ -179,7 +174,6 @@ - diff --git a/Example/SRC/Info.plist b/Example/SRC/Info.plist index 40c6215..c6c344a 100644 --- a/Example/SRC/Info.plist +++ b/Example/SRC/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 3.1 CFBundleSignature ???? CFBundleVersion - 1 + 4 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/Example/SRC/VideoCollectionReusableView.swift b/Example/SRC/VideoCollectionReusableView.swift index 43f9ba0..4777338 100644 --- a/Example/SRC/VideoCollectionReusableView.swift +++ b/Example/SRC/VideoCollectionReusableView.swift @@ -49,7 +49,7 @@ class VideoCollectionReusableView: UICollectionReusableView { } } - let date = NSDateFormatter.localizedStringFromDate(collection.startDate!, dateStyle:.LongStyle, timeStyle:.NoStyle) + let date = DateFormatter.localizedString(from: collection.startDate!, dateStyle:.long, timeStyle:.none) if self.titleLabel.text != nil { self.dateLabel.text = date @@ -76,30 +76,33 @@ class VideoCollectionReusableView: UICollectionReusableView { if (self.subtitleLabel.text?.characters.count == 0) { - self.titleLabel.frame = CGRectMake(horizontalBordersOffset, - titleLabelTopOffsetWithNoSubtitle, - CGRectGetWidth(self.bounds) - dateLabelWidth, - CGRectGetHeight(self.bounds) - titleLabelTopOffsetWithNoSubtitle) + self.titleLabel.frame = CGRect(origin: CGPoint(x: horizontalBordersOffset, + y: titleLabelTopOffsetWithNoSubtitle ), + size: CGSize(width: self.bounds.width - dateLabelWidth, + height: self.bounds.height - titleLabelTopOffsetWithNoSubtitle )) + + + + self.dateLabel.frame = CGRect( origin: CGPoint(x: self.bounds.width - dateLabelWidth - horizontalBordersOffset, + y: 0 + titleLabelTopOffsetWithNoSubtitle ), + size: CGSize(width: dateLabelWidth, + height: self.bounds.height - titleLabelTopOffsetWithNoSubtitle )) - self.dateLabel.frame = CGRectMake(CGRectGetWidth(self.bounds) - dateLabelWidth - horizontalBordersOffset, - 0 + titleLabelTopOffsetWithNoSubtitle, - dateLabelWidth, - CGRectGetHeight(self.bounds) - titleLabelTopOffsetWithNoSubtitle) } else { - self.titleLabel.frame = CGRectMake(horizontalBordersOffset, - titleLabelTopOffset, - CGRectGetWidth(self.bounds) - dateLabelWidth - 2 * horizontalBordersOffset, - CGRectGetHeight(self.titleLabel.bounds)) + self.titleLabel.frame = CGRect( origin: CGPoint(x: horizontalBordersOffset, + y: titleLabelTopOffset ), + size: CGSize(width: self.bounds.width - dateLabelWidth - 2 * horizontalBordersOffset, + height: self.titleLabel.bounds.height )) - self.subtitleLabel.frame = CGRectMake(horizontalBordersOffset, - CGRectGetHeight(self.bounds) - subtitleLabelBottomOffset - CGRectGetHeight(self.subtitleLabel.bounds), - CGRectGetWidth(self.bounds) - dateLabelWidth - 2 * horizontalBordersOffset, - CGRectGetHeight(self.subtitleLabel.bounds)) + self.subtitleLabel.frame = CGRect( origin: CGPoint(x: horizontalBordersOffset, + y: self.bounds.height - subtitleLabelBottomOffset - self.subtitleLabel.bounds.height ), + size: CGSize(width: self.bounds.width - dateLabelWidth - 2 * horizontalBordersOffset, + height: self.subtitleLabel.bounds.height)) - self.dateLabel.frame = CGRectMake(CGRectGetWidth(self.bounds) - dateLabelWidth - horizontalBordersOffset, - titleLabelTopOffset, - dateLabelWidth, - CGRectGetHeight(self.dateLabel.bounds) ) + self.dateLabel.frame = CGRect( origin: CGPoint(x: self.bounds.width - dateLabelWidth - horizontalBordersOffset, + y: titleLabelTopOffset ), + size: CGSize(width: dateLabelWidth, + height: self.dateLabel.bounds.height )) } } } diff --git a/Example/SRC/VideoCollectionViewCell.swift b/Example/SRC/VideoCollectionViewCell.swift index eebe0f0..08d66e4 100644 --- a/Example/SRC/VideoCollectionViewCell.swift +++ b/Example/SRC/VideoCollectionViewCell.swift @@ -20,29 +20,29 @@ class VideoCollectionViewCell: UICollectionViewCell { guard let videoSource = videoSource else { return } - self.timeLabel.text = self.stringWithTime(videoSource.duration) + self.timeLabel.text = self.stringWithTime(time: videoSource.duration) self.timeLabelContainer.layer.cornerRadius = self.timeLabelContainer.bounds.size.height/2; - imageManager.requestImageForAsset(videoSource, targetSize:self.bounds.size, contentMode:.AspectFill, options:nil, resultHandler: {result, _ in + imageManager.requestImage(for: videoSource, targetSize:self.bounds.size, contentMode:.aspectFill, options:nil, resultHandler: {result, _ in self.imageView.image = result; }) } } - func stringWithTime(time:NSTimeInterval) -> String { - var seconds = time; + func stringWithTime(time:TimeInterval) -> String { + var seconds = time.int; var minutes = seconds/60; seconds = seconds%60; let hours = minutes/60; minutes = minutes%60; if hours > 0 { - return String(format: "%d:%02d:%02d", Int(hours), Int(minutes), Int(seconds)); + return String(format: "%d:%d:%d", hours, minutes, seconds); } else if minutes > 0 { - return String(format:"%d:%02d", Int(minutes), Int(seconds)); + return String(format:"%d:%d", minutes, seconds); } else { - return String(format:"0:%02d", Int(seconds)); + return String(format:"0:%d", seconds); } } } diff --git a/Example/SRC/VideoCollectionViewController.swift b/Example/SRC/VideoCollectionViewController.swift index 8eea1c1..adbafed 100644 --- a/Example/SRC/VideoCollectionViewController.swift +++ b/Example/SRC/VideoCollectionViewController.swift @@ -12,16 +12,16 @@ import Photos @objc class VideoCollectionViewController: UICollectionViewController { - var assetsFetchResults: [PHFetchResult] = [] + var assetsFetchResults: [PHFetchResult] = [PHFetchResult]() var moments: [PHAssetCollection] = [] var userAlbumsFetchPredicate = NSPredicate(format: "estimatedAssetCount > 0") var userAlbumsFetchSortDescriptors = [NSSortDescriptor(key: "startDate", ascending: false)] - var inAlbumItemsFetchPredicate = NSPredicate(format: "mediaType == %d", PHAssetMediaType.Video.rawValue) + var inAlbumItemsFetchPredicate = NSPredicate(format: "mediaType == %d", PHAssetMediaType.video.rawValue) var selectedSnapshotView: UIView? - private struct Constants { + fileprivate struct Constants { static let collectionViewCellReuseId = "video_collection_view_cell" static let collectionHeaderReuseId = "video_collection_view_header" static let collectionFooterReuseId = "FooterView" @@ -44,11 +44,11 @@ class VideoCollectionViewController: UICollectionViewController { // MARK: - Constuctor/Destructor required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - PHPhotoLibrary.sharedPhotoLibrary().registerChangeObserver(self) + PHPhotoLibrary.shared().register(self) } deinit{ - PHPhotoLibrary.sharedPhotoLibrary().unregisterChangeObserver(self) + PHPhotoLibrary.shared().unregisterChangeObserver(self) } // MARK: - View Controller Lifetime @@ -57,10 +57,10 @@ class VideoCollectionViewController: UICollectionViewController { // Hide nav bar bottom line let navBarHairlineImageView: UIImageView? = navigationController?.barHairlineImageView() - navBarHairlineImageView?.hidden = true; + navBarHairlineImageView?.isHidden = true; } - override func viewDidAppear(animated: Bool) { + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) clearApplicationTmpDirectory() @@ -74,17 +74,17 @@ class VideoCollectionViewController: UICollectionViewController { // MARK: - UICollectionViewDataSource extension VideoCollectionViewController { - override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { + override func numberOfSections(in collectionView: UICollectionView) -> Int { return assetsFetchResults.count } - override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return assetsFetchResults[section].count } - override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCellWithReuseIdentifier(Constants.collectionViewCellReuseId, forIndexPath:indexPath) + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.collectionViewCellReuseId, for:indexPath) if let cell = cell as? VideoCollectionViewCell, let asset = self.assetsFetchResults[indexPath.section][indexPath.row] as? PHAsset { @@ -96,14 +96,14 @@ extension VideoCollectionViewController { return cell } - override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { + override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - let reuseId = Constants.collectionSupplementaryElementReuseIdForKind(kind) - let reusableView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: reuseId, forIndexPath: indexPath) + let reuseId = Constants.collectionSupplementaryElementReuseIdForKind(kind: kind) + let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseId, for: indexPath) switch (kind, reusableView) { case (UICollectionElementKindSectionHeader, let headerView as VideoCollectionReusableView): - headerView.configureWithCollection(self.moments[indexPath.section]) + headerView.configureWithCollection(collection: self.moments[indexPath.section]) case (UICollectionElementKindSectionFooter, _): () default: fatalError() @@ -119,20 +119,20 @@ extension VideoCollectionViewController: UICollectionViewDelegateFlowLayout { func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { let count = floor(self.view.bounds.width/Constants.preview_width); - let width = CGRectGetWidth(self.view.bounds)/count; - return CGSizeMake(width, Constants.preview_height); + let width = self.view.bounds.width/count; + return CGSize(width: width,height: Constants.preview_height); } func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets { - return UIEdgeInsetsZero + return .zero } } // MARK: - Navigation/Transition extension VideoCollectionViewController { - override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { - let controller = segue.destinationViewController as! ViewController + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + let controller = segue.destination as! ViewController let cell = sender as! VideoCollectionViewCell controller.phAsset = cell.videoSource } @@ -143,30 +143,28 @@ extension VideoCollectionViewController { extension VideoCollectionViewController { func updateAssetsFetchResultsAndMoments() { - var assets = [PHFetchResult]() + var assets = [PHFetchResult]() var moments = [PHAssetCollection]() let userAlbumsFetchOptions = PHFetchOptions() userAlbumsFetchOptions.predicate = userAlbumsFetchPredicate userAlbumsFetchOptions.sortDescriptors = userAlbumsFetchSortDescriptors - let userAlbumsFetchResult = PHAssetCollection.fetchMomentsWithOptions(userAlbumsFetchOptions) + let userAlbumsFetchResult = PHAssetCollection.fetchMoments(with: userAlbumsFetchOptions) let inAlbumItemsFetchOptions = PHFetchOptions() inAlbumItemsFetchOptions.predicate = inAlbumItemsFetchPredicate - userAlbumsFetchResult.enumerateObjectsUsingBlock { (collection, _, _) -> Void in - guard let collection = collection as? PHAssetCollection else { - return - } + + userAlbumsFetchResult.enumerateObjects ({ (collection, _, _) -> Void in - let assetsFetchResult = PHAsset.fetchAssetsInAssetCollection(collection, options: inAlbumItemsFetchOptions) + let assetsFetchResult = PHAsset.fetchAssets(in: collection, options: inAlbumItemsFetchOptions) if assetsFetchResult.count > 0 { - assets.append(assetsFetchResult) + assets.append(assetsFetchResult as! PHFetchResult) moments.append(collection) } - } + }) self.moments = moments self.assetsFetchResults = assets @@ -176,20 +174,21 @@ extension VideoCollectionViewController { // MARK: - View Controller Auto Rotation extension VideoCollectionViewController { - override func shouldAutorotate() -> Bool { + + override var shouldAutorotate: Bool { return false } - override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { - return .Portrait + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait } } // MARK: - PHPhotoLibraryChangeObserver extension VideoCollectionViewController: PHPhotoLibraryChangeObserver { - func photoLibraryDidChange(changeInstance: PHChange) { - dispatch_async(dispatch_get_main_queue()) { () -> Void in + func photoLibraryDidChange(_ changeInstance: PHChange) { + DispatchQueue.main.async { () -> Void in self.updateAssetsFetchResultsAndMoments() self.collectionView?.reloadData() } @@ -208,9 +207,9 @@ extension UINavigationController { // MARK: - UIView ex extension UIView { - func findSubview(predicate: (T) -> (Bool)) -> T? { + func findSubview(_ predicate: (T) -> (Bool)) -> T? { - if let self_ = self as? T where predicate(self_) { + if let self_ = self as? T , predicate(self_) { return self_ } @@ -226,10 +225,10 @@ extension UIView { // MARK: - Utility func clearApplicationTmpDirectory() { do { - let tmpDirectoryContent = try NSFileManager.defaultManager().contentsOfDirectoryAtPath(NSTemporaryDirectory()) + let tmpDirectoryContent = try FileManager.default.contentsOfDirectory(atPath: NSTemporaryDirectory()) for file in tmpDirectoryContent { let filePath = NSTemporaryDirectory() + file - try NSFileManager.defaultManager().removeItemAtPath(filePath) + try FileManager.default.removeItem(atPath: filePath) } } catch (let error) { print("\(#function), catched error:\(error)") diff --git a/Example/SRC/ViewController.swift b/Example/SRC/ViewController.swift index 154f115..46c9f38 100644 --- a/Example/SRC/ViewController.swift +++ b/Example/SRC/ViewController.swift @@ -27,15 +27,15 @@ class ViewController: UIViewController, DVGDiagramMovementsDelegate { let options = PHContentEditingInputRequestOptions() options.canHandleAdjustmentData = {_ in return false} - phAsset.requestContentEditingInputWithOptions(options) { contentEditingInput, info in + phAsset.requestContentEditingInput(with: options) { contentEditingInput, info in print(contentEditingInput, info) - dispatch_async(dispatch_get_main_queue()) { + DispatchQueue.main.async { if let asset = contentEditingInput?.avAsset { self.waveform.asset = asset self.configureWaveform() } else { print(info[PHContentEditingInputResultIsInCloudKey]) - if let value = info[PHContentEditingInputResultIsInCloudKey] as? Int where value == 1 { + if let value = info[PHContentEditingInputResultIsInCloudKey] as? Int, value == 1 { self.showAlert("Load video from iCloud first") } else { self.showAlert("Can't get audio from this video.") @@ -46,34 +46,34 @@ class ViewController: UIViewController, DVGDiagramMovementsDelegate { } } - func showAlert(message: String) { - let alert = UIAlertController(title: nil, message: message, preferredStyle: .Alert) - alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil)) - self.presentViewController(alert, animated: true, completion: nil) + func showAlert(_ message: String) { + let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) + self.present(alert, animated: true, completion: nil) } func configureWaveform() { self.waveform.movementDelegate = self let waveform1 = self.waveform.maxValuesWaveform() - waveform1?.lineColor = UIColor.redColor() + waveform1?.lineColor = UIColor.red let waveform2 = self.waveform.avgValuesWaveform() - waveform2?.lineColor = UIColor.greenColor() + waveform2?.lineColor = UIColor.green self.waveform.numberOfPointsOnThePlot = 2000 } - func diagramDidSelect(dataRange: DataRange) { + func diagramDidSelect(_ dataRange: DataRange) { print("\(#function), dataRange: \(dataRange)") } - func diagramMoved(scale scale: Double, start: Double) { + func diagramMoved(scale: Double, start: Double) { print("\(#function), scale: \(scale), start: \(start)") } @IBAction func readAudioAndDrawWaveform() { self.waveform.readAndDrawSynchronously({[weak self] in - if $0 != nil { - print("error:", $0!) + if $0.1 != nil { + print("error:", $0.1) self?.showAlert("Can't read asset") } else { print("waveform finished drawing") diff --git a/Example/Waveform.xcodeproj/project.pbxproj b/Example/Waveform.xcodeproj/project.pbxproj index 7e138b0..c203f67 100644 --- a/Example/Waveform.xcodeproj/project.pbxproj +++ b/Example/Waveform.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 27FC622C1DE7686900D08082 /* waweform-icon-button-play-slider.png in Resources */ = {isa = PBXBuildFile; fileRef = 27FC622A1DE7686900D08082 /* waweform-icon-button-play-slider.png */; }; + 27FC622D1DE7686900D08082 /* waweform-icon-button-selection.png in Resources */ = {isa = PBXBuildFile; fileRef = 27FC622B1DE7686900D08082 /* waweform-icon-button-selection.png */; }; 8BD39F7C1CC5648B005E8947 /* AudioSamplesReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD39F621CC5648B005E8947 /* AudioSamplesReader.swift */; }; 8BD39F7D1CC5648B005E8947 /* ScalableChannelsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD39F631CC5648B005E8947 /* ScalableChannelsContainer.swift */; }; 8BD39F7E1CC5648B005E8947 /* DVGAudioWaveformDiagram.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD39F641CC5648B005E8947 /* DVGAudioWaveformDiagram.swift */; }; @@ -42,6 +44,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 27FC622A1DE7686900D08082 /* waweform-icon-button-play-slider.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "waweform-icon-button-play-slider.png"; path = "../Resources/waweform-icon-button-play-slider.png"; sourceTree = ""; }; + 27FC622B1DE7686900D08082 /* waweform-icon-button-selection.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "waweform-icon-button-selection.png"; path = "../Resources/waweform-icon-button-selection.png"; sourceTree = ""; }; 8B3F34EE1C243BA6006A4B67 /* Waveform.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Waveform.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8BD39F621CC5648B005E8947 /* AudioSamplesReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioSamplesReader.swift; sourceTree = ""; }; 8BD39F631CC5648B005E8947 /* ScalableChannelsContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalableChannelsContainer.swift; sourceTree = ""; }; @@ -89,11 +93,21 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 27FC621B1DE732FE00D08082 /* Resources */ = { + isa = PBXGroup; + children = ( + 27FC622A1DE7686900D08082 /* waweform-icon-button-play-slider.png */, + 27FC622B1DE7686900D08082 /* waweform-icon-button-selection.png */, + ); + name = Resources; + path = ../Source; + sourceTree = ""; + }; 8B3F34E51C243BA6006A4B67 = { isa = PBXGroup; children = ( + 27FC621B1DE732FE00D08082 /* Resources */, 8BD39F601CC5648B005E8947 /* Source */, - 8BD39ED91CC559FA005E8947 /* Waveform */, 8B3F34EF1C243BA6006A4B67 /* Products */, ); sourceTree = ""; @@ -119,12 +133,14 @@ 8BD39F9E1CC565E9005E8947 /* ViewController.swift */, 8BD39F9F1CC565E9005E8947 /* Waveform-Bridging-Header.h */, ); - path = Waveform; + name = Waveform; + path = ../Example/Waveform; sourceTree = ""; }; 8BD39F601CC5648B005E8947 /* Source */ = { isa = PBXGroup; children = ( + 8BD39ED91CC559FA005E8947 /* Waveform */, 8BD39F611CC5648B005E8947 /* Data Source */, 8BD39F641CC5648B005E8947 /* DVGAudioWaveformDiagram.swift */, 8BD39F651CC5648B005E8947 /* DVGAudioWaveformDiagramModel.swift */, @@ -227,6 +243,8 @@ TargetAttributes = { 8B3F34ED1C243BA6006A4B67 = { CreatedOnToolsVersion = 7.2; + DevelopmentTeam = VVU5C37FM9; + LastSwiftMigration = 0800; }; }; }; @@ -255,6 +273,8 @@ files = ( 8BD39FA41CC565E9005E8947 /* Info.plist in Resources */, 8BD39FA31CC565E9005E8947 /* Main.storyboard in Resources */, + 27FC622C1DE7686900D08082 /* waweform-icon-button-play-slider.png in Resources */, + 27FC622D1DE7686900D08082 /* waweform-icon-button-selection.png in Resources */, 8BD39FA11CC565E9005E8947 /* Assets.xcassets in Resources */, 8BD39FA21CC565E9005E8947 /* LaunchScreen.storyboard in Resources */, ); @@ -389,6 +409,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -399,7 +420,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OBJC_INTERFACE_HEADER_NAME = "Waveform-Swift.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -411,11 +432,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = VVU5C37FM9; INFOPLIST_FILE = SRC/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = denivip.Waveform; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "SRC/Waveform-Bridging-Header.h"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -425,11 +449,16 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = VVU5C37FM9; + GCC_OPTIMIZATION_LEVEL = 0; INFOPLIST_FILE = SRC/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = denivip.Waveform; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "SRC/Waveform-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/Resources/waweform-icon-button-play-slider.png b/Resources/waweform-icon-button-play-slider.png new file mode 100644 index 0000000..3102aec Binary files /dev/null and b/Resources/waweform-icon-button-play-slider.png differ diff --git a/Resources/waweform-icon-button-selection.png b/Resources/waweform-icon-button-selection.png new file mode 100644 index 0000000..9454b98 Binary files /dev/null and b/Resources/waweform-icon-button-selection.png differ diff --git a/Source/DVGAudioWaveformDiagram.swift b/Source/DVGAudioWaveformDiagram.swift index 54aebc5..b758515 100644 --- a/Source/DVGAudioWaveformDiagram.swift +++ b/Source/DVGAudioWaveformDiagram.swift @@ -12,8 +12,8 @@ import UIKit public class DVGAudioWaveformDiagram: UIView { + //MARK: - Properties var panToSelect: UIPanGestureRecognizer! - var tapToSelect: UILongPressGestureRecognizer! var pan: UIPanGestureRecognizer! var pinch: UIPinchGestureRecognizer! @@ -32,6 +32,7 @@ class DVGAudioWaveformDiagram: UIView { } } + //MARK: - Initialization override init(frame: CGRect) { super.init(frame: frame) self.setup() @@ -64,13 +65,6 @@ class DVGAudioWaveformDiagram: UIView { self.addGestureRecognizer(panToSelect) self.panToSelect = panToSelect panToSelect.maximumNumberOfTouches = 1 - - let tap = UILongPressGestureRecognizer(target: self, action: #selector(DVGAudioWaveformDiagram.handleTapToSelect(_:))) - tap.delegate = self - tap.minimumPressDuration = 0.08 - self.addGestureRecognizer(tap) - self.tapToSelect = tap - tap.numberOfTouchesRequired = 1 let pinch = UIPinchGestureRecognizer(target: self, action: #selector(DVGAudioWaveformDiagram.handlePinch(_:))) pinch.delegate = self @@ -99,56 +93,152 @@ class DVGAudioWaveformDiagram: UIView { self.playbackPositionView = playbackPositionView } + + //MARK: - Gestures private var panStartLocation: CGFloat? + private var panStartVelocityX: CGFloat? + + var delayedTask : DispatchWorkItem? + + private var selectionUI : DataRange? { + if selection == nil { + return nil + } + + let convertedSelection = selection?.convertToGeometry(dataSource!.geometry) + + return DataRange(location: convertedSelection!.location * bounds.width.double , + length: convertedSelection!.length * bounds.width.double) + } + private var startSelection: DataRange? + + enum PanAction { + case moving, moveLeftSlider, moveRightSlider, selectNewArea + } + private var action: PanAction? + - func handlePanToSelect(pan: UIPanGestureRecognizer) { + func handlePanToSelect(_ pan: UIPanGestureRecognizer) { + switch pan.state { - case .Began: + case .began: + if self.panStartLocation == nil { - self.panStartLocation = pan.locationInView(self).x + self.panStartLocation = pan.location(in: self).x + self.panStartVelocityX = pan.velocity(in: self).x } - self.configureSelectionFromPosition(panStartLocation!, toPosition: pan.locationInView(self).x) - case .Failed: + + if (selectionUI == nil){ + self.configureSelectionFromPosition(panStartLocation!, + toPosition: pan.location(in: self).x, + anchor: panStartVelocityX!.double < 0.0 ? .right : .left ) + action = .selectNewArea + } else if selectionUI!.length >= minSelectionWidth.double - 1 { + + let sliderHalfWidth = 20.0 + + let letftSliderRange = (selectionUI!.location - sliderHalfWidth)..<(selectionUI!.location + sliderHalfWidth) + if letftSliderRange.contains(panStartLocation!.double) { + action = .moveLeftSlider + } + + let righSliderRange = (selectionUI!.location + selectionUI!.length - sliderHalfWidth)..<(selectionUI!.location + selectionUI!.length + sliderHalfWidth) + if righSliderRange.contains(panStartLocation!.double) { + action = .moveRightSlider + } + + let middleRange = letftSliderRange.upperBound.. _endPosition && abs(endPosition - startPosition) < minSelectionWidth { + startPosition = endPosition - minSelectionWidth + } + if _startPosition <= _endPosition { + startPosition = startPosition - minSelectionWidth + } + break + + case .left: + if _startPosition < _endPosition && abs(endPosition - startPosition) < minSelectionWidth { + endPosition = startPosition + minSelectionWidth + } + if _startPosition >= _endPosition { + endPosition = endPosition + minSelectionWidth + } + break + } startPosition = max(0, startPosition) endPosition = min(endPosition, self.bounds.width) - let width = max(endPosition - startPosition, minSelectionWidth) + let width = endPosition - startPosition let range = DataRange( location: Double(startPosition / self.bounds.width), @@ -237,22 +351,12 @@ class DVGAudioWaveformDiagram: UIView { } extension DVGAudioWaveformDiagram: UIGestureRecognizerDelegate { - public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { - switch (gestureRecognizer, otherGestureRecognizer) { - case (panToSelect, tapToSelect): - fallthrough - case (tapToSelect, panToSelect): - return true - default: + public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool + { return false - } } - override public func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer == self.panToSelect { - self.tapToSelect.enabled = false - self.tapToSelect.enabled = true - } + override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return true } } diff --git a/Source/DVGAudioWaveformDiagramModel.swift b/Source/DVGAudioWaveformDiagramModel.swift index 67c2aa9..9f06405 100644 --- a/Source/DVGAudioWaveformDiagramModel.swift +++ b/Source/DVGAudioWaveformDiagramModel.swift @@ -11,8 +11,8 @@ import UIKit @objc protocol DVGDiagramMovementsDelegate: class { - func diagramDidSelect(dataRange: DataRange) - func diagramMoved(scale scale: Double, start: Double) + func diagramDidSelect(_ dataRange: DataRange) + func diagramMoved(scale: Double, start: Double) } @objc @@ -23,17 +23,17 @@ class DVGAudioWaveformDiagramModel: DiagramModel, DVGDiagramDelegate { var originalSelection: DataRange? - public func diagramDidSelect(dataRange: DataRange) { + public func diagramDidSelect(_ dataRange: DataRange) { self.originalSelection = dataRange self.movementsDelegate?.diagramDidSelect(dataRange) } - override func zoom(start start: CGFloat, scale: CGFloat) { + override func zoom(start: CGFloat, scale: CGFloat) { super.zoom(start: start, scale: scale) self.movementsDelegate?.diagramMoved(scale: self.geometry.scale, start: self.geometry.start) } - override func moveToPosition(start: CGFloat) { + override func moveToPosition(_ start: CGFloat) { super.moveToPosition(start) self.movementsDelegate?.diagramMoved(scale: self.geometry.scale, start: self.geometry.start) } -} \ No newline at end of file +} diff --git a/Source/DVGWaveformView.swift b/Source/DVGWaveformView.swift index 2770253..e871722 100644 --- a/Source/DVGWaveformView.swift +++ b/Source/DVGWaveformView.swift @@ -33,7 +33,7 @@ class DVGWaveformController: NSObject { //MARK: - //MARK: - Configuration //MARK: - Internal configuration - func addPlotViewToContainerView(containerView: UIView) { + func addPlotViewToContainerView(_ containerView: UIView) { let diagram = DVGAudioWaveformDiagram() diagram.translatesAutoresizingMaskIntoConstraints = false @@ -57,7 +57,7 @@ class DVGWaveformController: NSObject { } //MARK: - For external configuration - func waveformWithIdentifier(identifier: String) -> Plot? { + func waveformWithIdentifier(_ identifier: String) -> Plot? { return self.diagram?.waveformDiagramView.plotWithIdentifier(identifier) } @@ -71,7 +71,7 @@ class DVGWaveformController: NSObject { //MARK: - //MARK: - Reading - func readAndDrawSynchronously(completion: (Bool, NSError?) -> ()) { + func readAndDrawSynchronously(_ completion: @escaping (Bool, NSError?) -> ()) { if self.samplesReader == nil { completion(false, NSError(domain: "",code: -1, userInfo: nil)) @@ -101,7 +101,13 @@ class DVGWaveformController: NSObject { } } - public func addDataSource(dataSource: ChannelSource) { + func stopLoading(){ + self.samplesReader?.cancel() + self.diagram?.waveformDiagramView.stopSynchingWithDataSource() + } + + public func addDataSource(_ dataSource: ChannelSource) { + channelSourceMapper.addChannelSource(dataSource) self.diagramViewModel.channelsSource = channelSourceMapper @@ -114,11 +120,12 @@ class DVGWaveformController: NSObject { //MARK: - //MARK: - Private vars - private var diagram: DVGAudioWaveformDiagram? - private var diagramViewModel = DVGAudioWaveformDiagramModel() - private var samplesReader: AudioSamplesReader? - private var waveformDataSource = ScalableChannelsContainer() - private var channelSourceMapper = ChannelSourceMapper() + + fileprivate var diagram: DVGAudioWaveformDiagram? + fileprivate var diagramViewModel = DVGAudioWaveformDiagramModel() + fileprivate var samplesReader: AudioSamplesReader? + fileprivate var waveformDataSource = ScalableChannelsContainer() + fileprivate var channelSourceMapper = ChannelSourceMapper() //MARK: - Public vars weak var movementDelegate: DVGDiagramMovementsDelegate? @@ -128,6 +135,7 @@ class DVGWaveformController: NSObject { self.samplesReader = AudioSamplesReader(asset: asset) self.configure() self.samplesReader?.samplesHandler = waveformDataSource + diagramViewModel.maxScale = asset.duration.seconds } } } @@ -140,7 +148,7 @@ class DVGWaveformController: NSObject { var scale: CGFloat = 1.0 @objc var playbackRelativePosition: NSNumber? { - get { return self._playbackRelativePosition } + get { return self._playbackRelativePosition as NSNumber? } set { self._playbackRelativePosition = newValue == nil ? nil : CGFloat(newValue!) } } @@ -149,17 +157,17 @@ class DVGWaveformController: NSObject { set { self.diagram?.playbackRelativePosition = newValue } } - var progress: NSProgress? { + var progress: Progress? { return self.samplesReader?.progress } } ////MARK: - DiagramViewModelDelegate extension DVGWaveformController: DVGDiagramMovementsDelegate { - func diagramDidSelect(dataRange: DataRange) { + func diagramDidSelect(_ dataRange: DataRange) { self.movementDelegate?.diagramDidSelect(dataRange) } - func diagramMoved(scale scale: Double, start: Double) { + func diagramMoved(scale: Double, start: Double) { self.waveformDataSource.reset(DataRange(location: start, length: 1/scale)) self.movementDelegate?.diagramMoved(scale: scale, start: start) } diff --git a/Source/Data Source/AudioSamplesReader.swift b/Source/Data Source/AudioSamplesReader.swift index 3d90a32..422303b 100644 --- a/Source/Data Source/AudioSamplesReader.swift +++ b/Source/Data Source/AudioSamplesReader.swift @@ -29,9 +29,13 @@ class AudioSamplesReader: NSObject { var nativeAudioFormat: AudioFormat? var samplesReadAudioFormat = Constants.DefaultAudioFormat - var progress = NSProgress() + var progress = Progress() - func readAudioFormat(completionBlock: (AudioFormat?, SamplesReaderError?) -> ()) { + func cancel(){ + self.readingRoutine?.cancelReading() + } + + func readAudioFormat(completionBlock: @escaping (AudioFormat?, SamplesReaderError?) -> ()) { dispatch_asynch_on_global_processing_queue { do { self.nativeAudioFormat = try self.readAudioFormat() @@ -50,17 +54,24 @@ class AudioSamplesReader: NSObject { func readAudioFormat() throws -> AudioFormat { - let formatDescription = try soundFormatDescription() + guard let formatDescription = try? soundFormatDescription() else { + throw SamplesReaderError.UnknownError(nil) + } - print("DEBUG Audio format description => \(formatDescription)") - let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription).memory + #if DEBUG + print("DEBUG Audio format description => \(formatDescription)") + #endif + guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription)?.pointee + else { + throw SamplesReaderError.UnknownError(nil) + } let format = AudioFormat(samplesRate: Int(asbd.mSampleRate), bitsDepth: Int(asbd.mBitsPerChannel), numberOfChannels: Int(asbd.mChannelsPerFrame)) nativeAudioFormat = format return format } func assetAudioTrack() throws -> AVAssetTrack { - guard let sound = asset.tracksWithMediaType(AVMediaTypeAudio).first else { + guard let sound = asset.tracks(withMediaType: AVMediaTypeAudio).first else { throw SamplesReaderError.NoSound } return sound @@ -72,55 +83,46 @@ class AudioSamplesReader: NSObject { } return formatDescription as! CMAudioFormatDescription } - - private func audioReadingSettingsForFormat(audioFormat: AudioFormat) -> [String: AnyObject] { + + fileprivate static func audioReadingSettings(forFormat audioFormat: AudioFormat) -> [String: AnyObject] { return [ - AVFormatIDKey : NSNumber(unsignedInt: kAudioFormatLinearPCM), - AVSampleRateKey : audioFormat.samplesRate, - AVNumberOfChannelsKey : audioFormat.numberOfChannels, - AVLinearPCMBitDepthKey : audioFormat.bitsDepth > 0 ? audioFormat.bitsDepth : 16, - AVLinearPCMIsBigEndianKey : false, - AVLinearPCMIsFloatKey : false, - AVLinearPCMIsNonInterleaved : false + AVFormatIDKey : NSNumber(value: kAudioFormatLinearPCM), + AVSampleRateKey : audioFormat.samplesRate as AnyObject, + AVNumberOfChannelsKey : audioFormat.numberOfChannels as AnyObject, + AVLinearPCMBitDepthKey : (audioFormat.bitsDepth > 0 ? audioFormat.bitsDepth : 16) as AnyObject, + AVLinearPCMIsBigEndianKey : false as AnyObject, + AVLinearPCMIsFloatKey : false as AnyObject, + AVLinearPCMIsNonInterleaved : false as AnyObject ] } - - func readSamples(audioFormat: AudioFormat? = nil, completion: (ErrorType?) -> ()) { + + func readSamples(_ audioFormat: AudioFormat? = nil, completion: @escaping (Error?) -> ()) { dispatch_asynch_on_global_processing_queue({ try self.readSamples(audioFormat) }, onCatch: completion) } - func readSamples(audioFormat: AudioFormat? = nil) throws { + func readSamples(_ audioFormat: AudioFormat? = nil) throws { if let format = audioFormat { samplesReadAudioFormat = format } try self.prepareForReading() try self.read() } - - private func prepareForReading() throws { - - let sound = try assetAudioTrack() - - let assetReader: AVAssetReader - do { - assetReader = try AVAssetReader(asset: asset) - } catch let error as NSError { - throw SamplesReaderError.UnknownError(error) - } - let settings = audioReadingSettingsForFormat(samplesReadAudioFormat) - - let readerOutput = AVAssetReaderTrackOutput(track: sound, outputSettings: settings) - - assetReader.addOutput(readerOutput) - assetReader.timeRange = CMTimeRange(start: kCMTimeZero, duration: asset.duration) + private func prepareForReading() throws { if samplesHandler == nil { print("\(#function)[\(#line)] Caution!!! There is no samples handler") } - - self.readingRoutine = SamplesReadingRoutine(assetReader: assetReader, readerOutput: readerOutput, audioFormat: samplesReadAudioFormat, samplesHandler: samplesHandler, progress: self.progress) + + do { + self.readingRoutine = try SamplesReadingRoutine(asset: asset, + audioFormat: samplesReadAudioFormat, + samplesHandler: samplesHandler, + progress: self.progress) + } catch { + throw error + } } private func read() throws { @@ -134,20 +136,56 @@ class AudioSamplesReader: NSObject { final class SamplesReadingRoutine { - let assetReader: AVAssetReader - let readerOutput: AVAssetReaderOutput + var assetReader: AVAssetReader + var readerOutput: AVAssetReaderOutput let audioFormat: AudioFormat weak var samplesHandler: AudioSamplesHandler? - - let progress: NSProgress + var lastTimestamp: CMTime = kCMTimeZero + let asset : AVAsset + + let progress: Progress lazy var estimatedSamplesCount: Int = { return Int(self.assetReader.asset.duration.seconds * Double(self.audioFormat.samplesRate)) }() - init(assetReader: AVAssetReader, readerOutput: AVAssetReaderOutput, audioFormat: AudioFormat, samplesHandler: AudioSamplesHandler?, progress: NSProgress) { - self.assetReader = assetReader - self.readerOutput = readerOutput + func restart() throws { + let cortege = try SamplesReadingRoutine.getAssetReaderAndOutput(asset: asset, audioFormat: audioFormat) + assetReader = cortege.0 + readerOutput = cortege.1 + assetReader.timeRange = CMTimeRange(start: lastTimestamp, duration: asset.duration) + try startReading() + } + + static func getAssetReaderAndOutput(asset: AVAsset, audioFormat: AudioFormat) throws -> (AVAssetReader, AVAssetReaderOutput) { + let sound : AVAssetTrack + sound = try asset.tracks(withMediaType: AVMediaTypeAudio).first! + + let assetReader: AVAssetReader + do { + assetReader = try AVAssetReader(asset: asset) + } catch let error as NSError { + throw SamplesReaderError.UnknownError(error) + } + + let settings = AudioSamplesReader.audioReadingSettings(forFormat: audioFormat) + + let readerOutput = AVAssetReaderTrackOutput(track: sound, outputSettings: settings) + + assetReader.add(readerOutput) + assetReader.timeRange = CMTimeRange(start: kCMTimeZero, duration: asset.duration) + + return (assetReader, readerOutput) + } + + init(asset: AVAsset, audioFormat: AudioFormat, samplesHandler: AudioSamplesHandler?, progress: Progress) throws { + + self.asset = asset + + let cortege = try SamplesReadingRoutine.getAssetReaderAndOutput(asset: asset, audioFormat: audioFormat) + + self.assetReader = cortege.0 + self.readerOutput = cortege.1 self.audioFormat = audioFormat self.samplesHandler = samplesHandler self.progress = progress @@ -155,12 +193,12 @@ final class SamplesReadingRoutine { } var isReading: Bool { - return assetReader.status == .Reading + return assetReader.status == .reading } func startReading() throws { if !assetReader.startReading() { - throw SamplesReaderError.CantReadSamples(assetReader.error) + throw SamplesReaderError.CantReadSamples(assetReader.error as NSError?) } } @@ -176,18 +214,25 @@ final class SamplesReadingRoutine { try readNextSamples() } catch (_ as NoMoreSampleBuffersAvailable) { break + } catch (_ as UnknownError){ + try restart() } catch { cancelReading() throw error } } try checkStatusOfAssetReaderOnComplete() - self.samplesHandler?.didStopReadSamples(Int(self.progress.completedUnitCount)) + self.samplesHandler?.didStopReadSamples(count: Int(self.progress.completedUnitCount)) } func readNextSamples() throws { guard let sampleBuffer = readerOutput.copyNextSampleBuffer() else { - throw NoMoreSampleBuffersAvailable() + + if assetReader.status == .failed, assetReader.error != nil { + throw UnknownError() + } else { + throw NoMoreSampleBuffersAvailable() + } } // Get buffer @@ -195,29 +240,37 @@ final class SamplesReadingRoutine { throw SamplesReaderError.UnknownError(nil) } + let duration = CMSampleBufferGetOutputDuration(sampleBuffer) + let presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + + lastTimestamp = presentationTimeStamp + duration + let length = CMBlockBufferGetDataLength(buffer) // Append new data - let tempBytes = UnsafeMutablePointer.alloc(length) - var returnedPointer: UnsafeMutablePointer = nil + let tempBytes = UnsafeMutableRawPointer.allocate(bytes: length, alignedTo: 0) + var returnedPointer: UnsafeMutablePointer? if CMBlockBufferAccessDataBytes(buffer, 0, length, tempBytes, &returnedPointer) != kCMBlockBufferNoErr { throw NoEnoughData() } - tempBytes.destroy(length) - tempBytes.dealloc(length) + tempBytes.deallocate(bytes: length, alignedTo: 0) + + guard (returnedPointer != nil) else { + throw SamplesReaderError.UnknownError(nil) + } - let samplesContainer = AudioSamplesContainer(buffer: returnedPointer, length: length, numberOfChannels: audioFormat.numberOfChannels) + let samplesContainer = AudioSamplesContainer(buffer: returnedPointer!, length: length, numberOfChannels: audioFormat.numberOfChannels) samplesHandler?.handleSamples(samplesContainer) progress.completedUnitCount += samplesContainer.samplesCount } func checkStatusOfAssetReaderOnComplete() throws { switch assetReader.status { - case .Unknown, .Failed, .Reading: - throw SamplesReaderError.UnknownError(assetReader.error) - case .Cancelled, .Completed: + case .unknown, .failed, .reading: + throw SamplesReaderError.UnknownError(assetReader.error as NSError?) + case .cancelled, .completed: return } } diff --git a/Source/Data Source/ScalableChannelsContainer.swift b/Source/Data Source/ScalableChannelsContainer.swift index 049a378..6285fb2 100644 --- a/Source/Data Source/ScalableChannelsContainer.swift +++ b/Source/Data Source/ScalableChannelsContainer.swift @@ -22,7 +22,7 @@ class ScalableChannelsContainer: NSObject, ChannelSource, AudioSamplesHandler { //MARK: - //MARK: - Inner configuration - func configure(neededSamplesCount neededSamplesCount: Int, estimatedSampleCount: Int) { + func configure(neededSamplesCount: Int, estimatedSampleCount: Int) { print("estimatedSampleCount ", estimatedSampleCount) @@ -63,7 +63,7 @@ class ScalableChannelsContainer: NSObject, ChannelSource, AudioSamplesHandler { } - func reset(dataRange: DataRange) { + func reset(_ dataRange: DataRange) { assert(self.channels.count > 0, "you should configure channels first. see method above") let scale = 1.0 / dataRange.length @@ -75,7 +75,8 @@ class ScalableChannelsContainer: NSObject, ChannelSource, AudioSamplesHandler { } } - public func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int) { + public func willStartReadSamples(estimatedSampleCount: Int) { + configure(neededSamplesCount: neededSamplesCount, estimatedSampleCount: estimatedSampleCount) } @@ -85,7 +86,7 @@ class ScalableChannelsContainer: NSObject, ChannelSource, AudioSamplesHandler { } } - public func handleSamples(samplesContainer: AudioSamplesContainer) { + public func handleSamples(_ samplesContainer: AudioSamplesContainer) { for channelIndex in 0.. Channel { + public func channelAtIndex(_ index: Int) -> Channel { return channels[index + scaleIndex * channelsCount] } } //MARK: - -//MARK: - Utility +//MARK: - Utility. Data Range. @objc public final diff --git a/Source/Utility/AudioSamplesContainer.swift b/Source/Utility/AudioSamplesContainer.swift index b114660..11738bf 100644 --- a/Source/Utility/AudioSamplesContainer.swift +++ b/Source/Utility/AudioSamplesContainer.swift @@ -15,8 +15,9 @@ class AudioSamplesContainer: NSObject { let numberOfChannels: Int init(buffer: UnsafePointer, length: Int, numberOfChannels: Int) { - self.buffer = UnsafePointer(buffer) - self.samplesCount = length * sizeof(T)/sizeof(Int16) / numberOfChannels + + self.buffer = UnsafeRawPointer(buffer).assumingMemoryBound(to: Int16.self) + self.samplesCount = length * MemoryLayout.size/MemoryLayout.size / numberOfChannels self.numberOfChannels = numberOfChannels } @@ -28,9 +29,9 @@ class AudioSamplesContainer: NSObject { self.init(buffer: buffer_int8, length: length, numberOfChannels: numberOfChannels) } - func sample(channelIndex channelIndex: Int, sampleIndex: Int) -> Int16 { + func sample(channelIndex: Int, sampleIndex: Int) -> Int16 { assert(channelIndex < numberOfChannels) assert(sampleIndex < samplesCount) return buffer[numberOfChannels * sampleIndex + channelIndex] } -} \ No newline at end of file +} diff --git a/Source/Utility/Buffer.swift b/Source/Utility/Buffer.swift index 11a0b5c..7b3bd12 100644 --- a/Source/Utility/Buffer.swift +++ b/Source/Utility/Buffer.swift @@ -14,70 +14,82 @@ class Buffer { var maxValue = -Double.infinity var minValue = Double.infinity var count = 0 - var buffer: UnsafeMutablePointer = nil - var _buffer: UnsafeMutablePointer = nil - private var space = 0 + var buffer: UnsafeMutableRawPointer? + var _buffer: UnsafeMutablePointer? + fileprivate var space = 0 - func appendValue(value: Double) { + func append(value: Double) { if maxValue < value { maxValue = value } if minValue > value { minValue = value } if space == count { let newSpace = max(space * 2, 16) - self.moveSpaceTo(newSpace) - _buffer = UnsafeMutablePointer(buffer) + self.moveSpace(to: newSpace) + _buffer = UnsafeMutablePointer(buffer?.assumingMemoryBound(to: DefaultNumberType.self)) } - (UnsafeMutablePointer(buffer) + count).initialize(value) + (UnsafeMutablePointer((buffer?.assumingMemoryBound(to: DefaultNumberType.self))!) + count).initialize(to: value) count += 1 } - private - func moveSpaceTo(newSpace: Int) { - let newPtr = UnsafeMutablePointer.alloc(newSpace) + fileprivate + func moveSpace(to newSpace: Int) { + let newPtr = UnsafeMutablePointer.allocate(capacity: newSpace) - newPtr.moveInitializeFrom(UnsafeMutablePointer(buffer), count: count) + newPtr.moveInitialize(from: UnsafeMutablePointer((buffer?.assumingMemoryBound(to: DefaultNumberType.self))!), count: count) - buffer.dealloc(count) + buffer?.deallocate(bytes: count, alignedTo: 0) - buffer = UnsafeMutablePointer(newPtr) + buffer = UnsafeMutableRawPointer(newPtr) space = newSpace } - func valueAtIndex(index: Int) -> Double { - return _buffer[index] + + subscript(index: Int) -> Double { + return _buffer?[index] ?? Double.infinity + } + + func value(atIndex index: Int) -> Double { + return _buffer?[index] ?? Double.infinity } } public final class GenericBuffer: Buffer { - var __buffer: UnsafeMutablePointer = nil + var __buffer: UnsafeMutablePointer? - override final func appendValue(value: Double) { + override final func append(value: Double) { + if maxValue < value { maxValue = value } if minValue > value { minValue = value } if space == count { let newSpace = max(space * 2, 16) - self.moveSpaceTo(newSpace) + self.moveSpace(to: newSpace) } - (__buffer + count).initialize(T(value)) + guard (__buffer != nil) else {return} + (__buffer! + count).initialize(to: T(value)) count += 1 } - override final func moveSpaceTo(newSpace: Int) { - let newPtr = UnsafeMutablePointer.alloc(newSpace) - - newPtr.moveInitializeFrom(__buffer, count: count) + override final func moveSpace(to newSpace: Int) { - __buffer.dealloc(count) + let newPtr = UnsafeMutablePointer.allocate(capacity: newSpace) - __buffer = newPtr + if (__buffer == nil){ + __buffer = newPtr + } else { + newPtr.moveInitialize(from: __buffer!, count: count) + __buffer!.deallocate(capacity: count) + __buffer = newPtr + } space = newSpace } - override final func valueAtIndex(index: Int) -> Double { - return __buffer[index].double + + override subscript(index: Int) -> Double { + return __buffer?[index].double ?? Double.infinity } + deinit { - __buffer.destroy(space) - __buffer.dealloc(space) + __buffer?.deinitialize(count: space) + __buffer?.deallocate(capacity: space) } -} \ No newline at end of file +} diff --git a/Source/Utility/Channel.swift b/Source/Utility/Channel.swift index 775073a..666f578 100644 --- a/Source/Utility/Channel.swift +++ b/Source/Utility/Channel.swift @@ -14,6 +14,7 @@ class Channel: NSObject { let logicProvider: LogicProvider let buffer: Buffer + let semaphore = DispatchSemaphore.init(value: 1) public init(logicProvider: LogicProvider, buffer: Buffer = GenericBuffer()) { self.logicProvider = logicProvider self.buffer = buffer @@ -24,7 +25,7 @@ class Channel: NSObject { public var blockSize = 1 public var count: Int { return buffer.count } public var totalCount: Int = 0 - lazy public var identifier: String = { return "\(self.logicProvider.dynamicType)" }() + lazy public var identifier: String = { return "\(type(of: self.logicProvider))" }() private var currentBlockSize = 0 public var maxValue: Double { return buffer.maxValue } @@ -33,11 +34,14 @@ class Channel: NSObject { public subscript(index: Int) -> Double { get { - return buffer.valueAtIndex(index) + semaphore.wait() + let result = buffer[index] + semaphore.signal() + return result } } - public func handleValue(value: U) { + public func handle(value: U) { if currentBlockSize == blockSize { self.clear() currentBlockSize = 0 @@ -46,8 +50,10 @@ class Channel: NSObject { self.logicProvider.handleValue(value.double) } - func appendValueToBuffer(value: Double) { - buffer.appendValue(value) + func appendValueToBuffer(_ value: Double) { + semaphore.wait() + buffer.append(value: value) + semaphore.signal() onUpdate() } @@ -62,4 +68,4 @@ class Channel: NSObject { //TODO: Clear odd space self.clear() } -} \ No newline at end of file +} diff --git a/Source/Utility/DiagramGeometry.swift b/Source/Utility/DiagramGeometry.swift index 6c55bcb..6362e17 100644 --- a/Source/Utility/DiagramGeometry.swift +++ b/Source/Utility/DiagramGeometry.swift @@ -20,32 +20,32 @@ class DiagramGeometry: NSObject { } extension Double { - func convertToGeometry(geometry: DiagramGeometry) -> Double { + func convertToGeometry(_ geometry: DiagramGeometry) -> Double { return (self - geometry.start) * geometry.scale } - func convertFromGeometry(geometry: DiagramGeometry) -> Double { + func convertFromGeometry(_ geometry: DiagramGeometry) -> Double { return self/geometry.scale + geometry.start } } extension CGFloat { - func convertToGeometry(geometry: DiagramGeometry) -> CGFloat { + func convertToGeometry(_ geometry: DiagramGeometry) -> CGFloat { return CGFloat((Double(self) - geometry.start) * geometry.scale) } - func convertFromGeometry(geometry: DiagramGeometry) -> CGFloat { + func convertFromGeometry(_ geometry: DiagramGeometry) -> CGFloat { return CGFloat(Double(self)/geometry.scale + geometry.start) } } extension DataRange { - func convertToGeometry(geometry: DiagramGeometry) -> DataRange { + func convertToGeometry(_ geometry: DiagramGeometry) -> DataRange { let location = self.location.convertToGeometry(geometry) let length = self.length * geometry.scale return DataRange(location: location, length: length) } - func convertFromGeometry(geometry: DiagramGeometry) -> DataRange { + func convertFromGeometry(_ geometry: DiagramGeometry) -> DataRange { let location = self.location.convertFromGeometry(geometry) let length = self.length / geometry.scale return DataRange(location: location, length: length) } -} \ No newline at end of file +} diff --git a/Source/Utility/Dispatch.swift b/Source/Utility/Dispatch.swift index 7da0526..4b5132e 100644 --- a/Source/Utility/Dispatch.swift +++ b/Source/Utility/Dispatch.swift @@ -8,17 +8,20 @@ import Foundation -let processingQueue = dispatch_queue_create("ru.denivip.waveform.processing", DISPATCH_QUEUE_SERIAL) +let processingQueue = DispatchQueue(label: "ru.denivip.waveform.processing")// dispatch_queue_create("ru.denivip.waveform.processing", DISPATCH_QUEUE_SERIAL) -public func dispatch_asynch_on_global_processing_queue(block: dispatch_block_t) { - if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(processingQueue)) { - autoreleasepool(block) +public func dispatch_asynch_on_global_processing_queue(block: @escaping ()->() ) { + + + + if processingQueue.label == String(cString:__dispatch_queue_get_label(nil),encoding: .utf8) { + autoreleasepool(invoking: block) } else { - dispatch_async(processingQueue, block); + processingQueue.async(execute: block) } } -public func dispatch_asynch_on_global_processing_queue(body: () throws -> (), onCatch: (ErrorType?) -> ()) { +public func dispatch_asynch_on_global_processing_queue(_ body: @escaping () throws -> (), onCatch: @escaping (Error?) -> ()) { dispatch_asynch_on_global_processing_queue { do { try body() @@ -28,4 +31,4 @@ public func dispatch_asynch_on_global_processing_queue(body: () throws -> (), on onCatch(error) } } -} \ No newline at end of file +} diff --git a/Source/Utility/Error.swift b/Source/Utility/Error.swift index 1129704..be23416 100644 --- a/Source/Utility/Error.swift +++ b/Source/Utility/Error.swift @@ -9,7 +9,7 @@ import Foundation public -enum SamplesReaderError: ErrorType { +enum SamplesReaderError: Error { case NoSound case InvalidAudioFormat case CantReadSamples(NSError?) @@ -17,5 +17,6 @@ enum SamplesReaderError: ErrorType { case SampleReaderNotReady } -struct NoMoreSampleBuffersAvailable: ErrorType {} -struct NoEnoughData: ErrorType {} \ No newline at end of file +struct NoMoreSampleBuffersAvailable: Error {} +struct NoEnoughData: Error {} +struct UnknownError: Error {} diff --git a/Source/Utility/LogicProvider.swift b/Source/Utility/LogicProvider.swift index 7093155..36df3e6 100644 --- a/Source/Utility/LogicProvider.swift +++ b/Source/Utility/LogicProvider.swift @@ -11,7 +11,7 @@ import Foundation public class LogicProvider { weak internal var channel: Channel? - public func handleValue(value: Double) {} + public func handleValue(_ value: Double) {} public func clear() {} } @@ -21,7 +21,7 @@ class MaxValueLogicProvider: LogicProvider { private var max: Double? public override init(){} - public override func handleValue(value: Double) { + public override func handleValue(_ value: Double) { if max == nil { max = value } else if value > max! { @@ -42,7 +42,7 @@ class AverageValueLogicProvider: LogicProvider { var count = 0 public override init(){} - public override func handleValue(value: Double) { + public override func handleValue(_ value: Double) { summ = summ + value count += 1 } @@ -61,7 +61,7 @@ class AudioMaxValueLogicProvider: LogicProvider { private var max = Double(Int16.min)//-40.0 public override init(){} - public override func handleValue(value: Double) { + public override func handleValue(_ value: Double) { let value = abs(value) if value > max { max = value @@ -81,7 +81,7 @@ class AudioAverageValueLogicProvider: LogicProvider { var count = 0 public override init(){} - public override func handleValue(value: Double) { + public override func handleValue(_ value: Double) { summ = summ + abs(value) count += 1 } diff --git a/Source/Utility/Protocols.swift b/Source/Utility/Protocols.swift index 30360e4..5649550 100644 --- a/Source/Utility/Protocols.swift +++ b/Source/Utility/Protocols.swift @@ -15,7 +15,7 @@ protocol PlotDataSource: class { var pointsCount: Int { get } var needsRedraw: Bool { get set } func updateGeometry() - func pointAtIndex(index: Int) -> CGPoint + func pointAtIndex(_ index: Int) -> CGPoint } public @@ -23,18 +23,18 @@ protocol DiagramDataSource: class { var geometry: DiagramGeometry { get } var onPlotUpdate: () -> () { get set } var plotDataSourcesCount: Int { get } - func plotDataSourceAtIndex(index: Int) -> PlotDataSource + func plotDataSourceAtIndex(_ index: Int) -> PlotDataSource } public protocol DiagramDelegate: class { - func zoomAt(zoomAreaCenter: CGFloat, relativeScale: CGFloat) - func moveByDistance(relativeDeltaX: CGFloat) + func zoomAt(_ zoomAreaCenter: CGFloat, relativeScale: CGFloat) + func moveByDistance(_ relativeDeltaX: CGFloat) } public protocol DVGDiagramDelegate: class, DiagramDelegate { - func diagramDidSelect(dataRange: DataRange) + func diagramDidSelect(_ dataRange: DataRange) } @objc @@ -42,7 +42,7 @@ public protocol ChannelSource: class { var channelsCount: Int { get } var onChannelsChanged: () -> () { get set } - func channelAtIndex(index: Int) -> Channel + func channelAtIndex(_ index: Int) -> Channel } public @@ -54,14 +54,14 @@ protocol AbstractChannel: class, Identifiable { var minValue: Double { get } subscript(index: Int) -> Double { get } - func handleValue(value: U) + func handleValue(_ value: U) } public protocol AudioSamplesHandler: class { - func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int) + func willStartReadSamples(estimatedSampleCount: Int) func didStopReadSamples(count: Int) - func handleSamples(samplesContainer: AudioSamplesContainer) + func handleSamples(_ samplesContainer: AudioSamplesContainer) } @@ -74,4 +74,4 @@ extension AudioSamplesHandler { public protocol Identifiable { var identifier: String { get } -} \ No newline at end of file +} diff --git a/Source/Utility/UIKit+extensions.swift b/Source/Utility/UIKit+extensions.swift index baadafb..d5e820e 100644 --- a/Source/Utility/UIKit+extensions.swift +++ b/Source/Utility/UIKit+extensions.swift @@ -12,9 +12,9 @@ extension UIView { func attachBoundsOfSuperview(){ assert(self.superview != nil, "There are no superview") let views = ["view": self] - let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("|[view]|", options: [], metrics: nil, views: views) + let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "|[view]|", options: [], metrics: nil, views: views) self.superview!.addConstraints(horizontalConstraints) - let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: [], metrics: nil, views: views) + let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|", options: [], metrics: nil, views: views) self.superview!.addConstraints(verticalConstraints) } -} \ No newline at end of file +} diff --git a/Source/View Model/ChannelSourceMapper.swift b/Source/View Model/ChannelSourceMapper.swift index eb510d3..250f1d7 100644 --- a/Source/View Model/ChannelSourceMapper.swift +++ b/Source/View Model/ChannelSourceMapper.swift @@ -14,7 +14,7 @@ final class ChannelSourceMapper: NSObject, ChannelSource { var channelSources: [ChannelSource] = [] - func addChannelSource(channelSource: ChannelSource) { + func addChannelSource(_ channelSource: ChannelSource) { channelSources.append(channelSource) channelSource.onChannelsChanged = { [weak self] in self?.onChannelsChanged() } } @@ -24,7 +24,7 @@ class ChannelSourceMapper: NSObject, ChannelSource { } public var onChannelsChanged: () -> () = {_ in} - public func channelAtIndex(index: Int) -> Channel { + public func channelAtIndex(_ index: Int) -> Channel { var tmpIndex = index for channelSource in channelSources { if tmpIndex < channelSource.channelsCount { @@ -34,4 +34,4 @@ class ChannelSourceMapper: NSObject, ChannelSource { } fatalError() } -} \ No newline at end of file +} diff --git a/Source/View Model/DiagramModel.swift b/Source/View Model/DiagramModel.swift index d71c6c4..8de2089 100644 --- a/Source/View Model/DiagramModel.swift +++ b/Source/View Model/DiagramModel.swift @@ -13,6 +13,8 @@ import UIKit public class DiagramModel: NSObject, DiagramDataSource { + var maxScale = 100.0 + weak var channelsSource: ChannelSource? { didSet{ channelsSource?.onChannelsChanged = { @@ -23,13 +25,14 @@ class DiagramModel: NSObject, DiagramDataSource { } } - private var viewModels = [PlotModel]() + fileprivate var viewModels = [PlotModel]() public var geometry = DiagramGeometry() public var onPlotUpdate: () -> () = {} var onGeometryUpdate: () -> () = {} + public var plotDataSourcesCount: Int { return self.viewModels.count } - public func plotDataSourceAtIndex(index: Int) -> PlotDataSource { + public func plotDataSourceAtIndex(_ index: Int) -> PlotDataSource { return self.viewModels[index] } @@ -48,7 +51,7 @@ class DiagramModel: NSObject, DiagramDataSource { onPlotUpdate() } - func adjustViewModelsCountWithCount(count: Int) { + func adjustViewModelsCountWithCount(_ count: Int) { if viewModels.count == count { return } @@ -94,29 +97,29 @@ class DiagramModel: NSObject, DiagramDataSource { } extension DiagramModel: DiagramDelegate { - func zoom(start start: CGFloat, scale: CGFloat) { + func zoom(start: CGFloat, scale: CGFloat) { self.geometry = DiagramGeometry(start: Double(start), scale: Double(scale)) for viewModel in self.viewModels { viewModel.updateGeometry() } } - public func zoomAt(zoomAreaCenter: CGFloat, relativeScale: CGFloat) { - let newScale = max(1.0, relativeScale * CGFloat(self.geometry.scale)) + public func zoomAt(_ zoomAreaCenter: CGFloat, relativeScale: CGFloat) { + let newScale = min( max(1.0, relativeScale * CGFloat(self.geometry.scale)), CGFloat(maxScale)) var start = CGFloat(self.geometry.start) + zoomAreaCenter * (1/CGFloat(self.geometry.scale) - 1/newScale) start = max(0, min(start, 1 - 1/newScale)) self.zoom(start: start, scale: newScale) } - func moveToPosition(start: CGFloat) { + func moveToPosition(_ start: CGFloat) { self.geometry.start = max(0, min(Double(start), 1 - 1/self.geometry.scale)) for viewModel in self.viewModels { viewModel.updateGeometry() } } - public func moveByDistance(relativeDeltaX: CGFloat) { + public func moveByDistance(_ relativeDeltaX: CGFloat) { let relativeStart = CGFloat(self.geometry.start) - relativeDeltaX / CGFloat(self.geometry.scale) self.moveToPosition(relativeStart) } -} \ No newline at end of file +} diff --git a/Source/View Model/PlotModel.swift b/Source/View Model/PlotModel.swift index 1eb7341..4b9271f 100644 --- a/Source/View Model/PlotModel.swift +++ b/Source/View Model/PlotModel.swift @@ -47,7 +47,7 @@ class PlotModel: NSObject, PlotDataSource { public var identifier = "" public var needsRedraw = false - public func pointAtIndex(index: Int) -> CGPoint { + public func pointAtIndex(_ index: Int) -> CGPoint { guard let channel = self.channel else { return .zero } diff --git a/Source/View/Diagram.swift b/Source/View/Diagram.swift index 54923ac..5d2d442 100644 --- a/Source/View/Diagram.swift +++ b/Source/View/Diagram.swift @@ -61,7 +61,7 @@ class Diagram: UIView { self.containerView = containerView } - func adjustPlotsNumberWithCount(count: Int) { + func adjustPlotsNumberWithCount(_ count: Int) { if plots.count == count { return } @@ -78,7 +78,7 @@ class Diagram: UIView { for index in count.. Plot { + func addPlotWithDataSource(_ dataSource: PlotDataSource) -> Plot { let plot = Plot(frame: self.bounds) plot.dataSource = dataSource @@ -103,7 +103,7 @@ extension Diagram { return plot } - func plotWithIdentifier(identifier: String) -> Plot? { + func plotWithIdentifier(_ identifier: String) -> Plot? { print(plots.map({$0.identifier})) for plot in self.plots { if plot.identifier == identifier { @@ -115,7 +115,7 @@ extension Diagram { func redraw() { for plot in self.plots { - if let dataSource = plot.dataSource where dataSource.needsRedraw { + if let dataSource = plot.dataSource , dataSource.needsRedraw { plot.redraw() dataSource.needsRedraw = false } @@ -128,7 +128,7 @@ extension Diagram { self.displayLink = nil } let displayLink = CADisplayLink.init(target: self, selector: #selector(Diagram.redraw)) - displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes) + displayLink.add(to: RunLoop.current, forMode: .commonModes) self.displayLink = displayLink } @@ -136,4 +136,4 @@ extension Diagram { self.displayLink?.invalidate() self.displayLink = nil } -} \ No newline at end of file +} diff --git a/Source/View/PlaybackPositionView.swift b/Source/View/PlaybackPositionView.swift index c5538bb..0aaa210 100644 --- a/Source/View/PlaybackPositionView.swift +++ b/Source/View/PlaybackPositionView.swift @@ -11,52 +11,50 @@ import UIKit public class PlaybackPositionView: UIView { + let cursor = UIImageView(image: UIImage(named: "waweform-icon-button-play-slider", in: Bundle(for: PlaybackPositionView.self), compatibleWith: nil)!) + init() { super.init(frame: .zero) - self.opaque = false + self.isOpaque = false + cursor.frame = CGRect(x: 0, y: 0, width: cursor.image!.size.width/2.0, height: cursor.image!.size.height/2.0); + cursor.isHidden = true + addSubview(cursor) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - self.opaque = false + self.isOpaque = false } - - override public func drawRect(rect: CGRect) { - super.drawRect(rect) - guard let relativePosition = self.position else { + + func positionCursor(){ + guard let relativePosition = self.position, + relativePosition >= 0, + relativePosition <= 1 + else { + cursor.isHidden = true return } - if relativePosition < 0 || relativePosition > 1 { - return - } + cursor.isHidden = false let position = (self.bounds.width - lineWidth) * relativePosition + lineWidth/2 - guard let context = UIGraphicsGetCurrentContext() else { - fatalError("No context") - } - - CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor) - CGContextSetLineWidth(context, lineWidth) - - - - let cursor = CGPathCreateMutable() - CGPathMoveToPoint(cursor, nil, position, 0) - CGPathAddLineToPoint(cursor, nil, position, self.bounds.height) - CGContextAddPath(context, cursor) - - CGContextStrokePath(context) - + cursor.center = CGPoint(x: position, y: center.y) + } + + public override func layoutSubviews() { + positionCursor() } /// Value from 0 to 1 /// Setting value causes setNeedsDisplay method call /// Setting nil causes removing cursor var position: CGFloat? { - didSet { self.setNeedsDisplay() } + didSet { + self.positionCursor() + setNeedsLayout() + } } - var lineColor = UIColor.whiteColor() + var lineColor = UIColor.white var lineWidth: CGFloat = 2.0 } diff --git a/Source/View/Plot.swift b/Source/View/Plot.swift index 6781913..74ca502 100644 --- a/Source/View/Plot.swift +++ b/Source/View/Plot.swift @@ -18,7 +18,7 @@ class Plot: UIView { } } - var lineColor: UIColor = .blackColor() { + var lineColor: UIColor = .black{ didSet{ // self.pathLayer.strokeColor = lineColor.CGColor } @@ -30,23 +30,23 @@ class Plot: UIView { required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - self.opaque = false + self.isOpaque = false } convenience init(){ - self.init(frame: CGRectZero) - self.opaque = false + self.init(frame: .zero) + self.isOpaque = false } override init(frame: CGRect) { super.init(frame: frame) - self.opaque = false + self.isOpaque = false } func setupPathLayer() { self.pathLayer = CAShapeLayer() - self.pathLayer.strokeColor = UIColor.blackColor().CGColor + self.pathLayer.strokeColor = UIColor.black.cgColor self.pathLayer.lineWidth = 1.0 self.layer.addSublayer(self.pathLayer) @@ -62,33 +62,33 @@ class Plot: UIView { self.setNeedsDisplay() } - override public func drawRect(rect: CGRect) { + override public func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext() else { return } - CGContextSetLineWidth(context, 1/UIScreen.mainScreen().scale) - CGContextAddPath(context, self.newPathPart()) - CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor) - CGContextSetInterpolationQuality(context, .None); - CGContextSetAllowsAntialiasing(context, false); - CGContextSetShouldAntialias(context, false); - CGContextStrokePath(context) + context.setLineWidth(1/UIScreen.main.scale) + context.addPath(self.newPathPart()) + context.setStrokeColor(self.lineColor.cgColor) + context.interpolationQuality = .none + context.setAllowsAntialiasing(false) + context.setShouldAntialias(false) + context.strokePath() } - private func newPathPart() -> CGPathRef { + private func newPathPart() -> CGPath { let lineWidth: CGFloat = 1 guard let dataSource = self.dataSource else { - return CGPathCreateMutable() + return CGMutablePath() } let currentCount = dataSource.pointsCount let sourceBounds = dataSource.dataSourceFrame.size - let mPath = CGPathCreateMutable() - CGPathMoveToPoint(mPath, nil, 0, self.bounds.midY - lineWidth/2) + let mPath = CGMutablePath() + mPath.move(to: CGPoint(x: 0,y: self.bounds.midY - lineWidth/2)) let wProportion = self.bounds.size.width / sourceBounds.width let hPropostion = self.bounds.size.height / sourceBounds.height @@ -99,14 +99,15 @@ class Plot: UIView { x: point.x * wProportion, y: point.y * hPropostion / 2.0) - CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY) - CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY - adjustedPoint.y) - CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY + adjustedPoint.y) - CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY) + mPath.addLine(to: CGPoint(x: adjustedPoint.x, y: self.bounds.midY)) + mPath.addLine(to: CGPoint(x: adjustedPoint.x, y: self.bounds.midY - adjustedPoint.y)) + mPath.addLine(to: CGPoint(x: adjustedPoint.x, y: self.bounds.midY + adjustedPoint.y)) + mPath.addLine(to: CGPoint(x: adjustedPoint.x, y: self.bounds.midY)) + } - CGPathAddLineToPoint(mPath, nil, 0.0, self.bounds.midY) - CGPathCloseSubpath(mPath) + mPath.addLine(to: CGPoint(x: 0.0,y: self.bounds.midY)) + mPath.closeSubpath() return mPath } } diff --git a/Source/View/SelectionView.swift b/Source/View/SelectionView.swift index 6ee854d..91b1597 100644 --- a/Source/View/SelectionView.swift +++ b/Source/View/SelectionView.swift @@ -11,7 +11,13 @@ import UIKit @objc public class SelectionView: UIView { + + let sliderIcon = UIImage(named: "waweform-icon-button-selection", in: Bundle(for: SelectionView.self), compatibleWith: nil)! + var sliderSize: CGSize = .zero + let leftSlider = CALayer() + let rightSlider = CALayer() var selectionLayer: CALayer! + init() { super.init(frame: .zero) self.setup() @@ -24,13 +30,22 @@ class SelectionView: UIView { func setup() { self.setupSelectionLayer() + self.backgroundColor = .clear + + leftSlider.contents = sliderIcon.cgImage + rightSlider.contents = sliderIcon.cgImage + sliderSize = CGSize(width: sliderIcon.size.width/2.0, height: sliderIcon.size.height/2.0) + + self.layer.addSublayer(leftSlider) + self.layer.addSublayer(rightSlider) } func setupSelectionLayer() { let layer = CALayer() + layer.cornerRadius = 5.0 layer.borderWidth = 0.0 - layer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5).CGColor + layer.backgroundColor = UIColor.black.withAlphaComponent(0.5).cgColor self.layer.addSublayer(layer) self.selectionLayer = layer } @@ -44,20 +59,41 @@ class SelectionView: UIView { didSet{ self.layoutSelection(selection) } } - func layoutSelection(dataRange: DataRange?) { + func layoutSelection(_ dataRange: DataRange?) { + guard let dataRange = dataRange else { - self.selectionLayer.backgroundColor = UIColor.clearColor().CGColor + self.selectionLayer.backgroundColor = UIColor.clear.cgColor return } - self.selectionLayer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5).CGColor + self.selectionLayer.backgroundColor = UIColor.black.withAlphaComponent(0.5).cgColor let startLocation = self.bounds.width * CGFloat(dataRange.location) let selectionWidth = self.bounds.width * CGFloat(dataRange.length) + + if selectionWidth < sliderSize.width * 2 { + let opacity = max(Float((selectionWidth - sliderSize.width)/sliderSize.width), 0) + leftSlider.opacity = opacity + rightSlider.opacity = opacity + } else { + leftSlider.opacity = 1 + rightSlider.opacity = 1 + } + let frame = CGRect(x: startLocation, y: 0, width: selectionWidth, height: self.bounds.height) + let leftSliderFrame = CGRect(x: startLocation - sliderSize.width/2, + y: self.center.y - sliderSize.height/2, + width: sliderSize.width, + height: sliderSize.height) + let rightSliderFrame = CGRect(x: startLocation + selectionWidth - sliderSize.width/2, + y: self.center.y - sliderSize.height/2, + width: sliderSize.width, + height: sliderSize.height) CATransaction.begin() CATransaction.setDisableActions(true) self.selectionLayer.frame = frame + leftSlider.frame = leftSliderFrame + rightSlider.frame = rightSliderFrame CATransaction.commit() } -} \ No newline at end of file +} diff --git a/Waveform.podspec b/Waveform.podspec index 67d1251..efbcaeb 100644 --- a/Waveform.podspec +++ b/Waveform.podspec @@ -5,14 +5,15 @@ Pod::Spec.new do |s| s.summary = "DENIVIP Media" s.requires_arc = true - s.version = "0.0.2" + s.version = "3.1.0" s.license = { :type => "MIT", :file => "LICENSE" } s.author = { "Anton Belousov" => "belousov@denivip.ru" } s.homepage = "https://github.com/denivip/Waveform" - s.source = { :git => "https://github.com/denivip/Waveform.git", :tag => "0.0.2" } + s.source = { :git => "https://github.com/denivip/Waveform.git", :tag => "3.1.0" } s.source_files = "Source/**/*.{swift}" s.frameworks = "Foundation", "UIKit", "AVFoundation", "CoreMedia" + s.resources = "Resources/**/*.png" end \ No newline at end of file