├── screenshots └── screenshoot.gif ├── VoiceMemo ├── Supporting Files │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── list.imageset │ │ │ ├── Untitled.png │ │ │ ├── Untitled@2x.png │ │ │ ├── Untitled@3x.png │ │ │ └── Contents.json │ │ ├── play_begin.imageset │ │ │ ├── Untitled.png │ │ │ ├── Untitled@2x.png │ │ │ ├── Untitled@3x.png │ │ │ └── Contents.json │ │ ├── play_pause.imageset │ │ │ ├── Untitled.png │ │ │ ├── Untitled@2x.png │ │ │ ├── Untitled@3x.png │ │ │ └── Contents.json │ │ ├── record_begin.imageset │ │ │ ├── Untitled.png │ │ │ ├── Untitled@2x.png │ │ │ ├── Untitled@3x.png │ │ │ └── Contents.json │ │ ├── record_pause.imageset │ │ │ ├── Untitled.png │ │ │ ├── Untitled@2x.png │ │ │ ├── Untitled@3x.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.storyboard ├── Data │ ├── DateHandler.swift │ ├── VoiceMemo.xcdatamodeld │ │ └── VoiceMemo.xcdatamodel │ │ │ └── contents │ ├── Voice.swift │ ├── FileHandler.swift │ └── CoreDataStack.swift ├── Utils │ ├── Utils.swift │ ├── AudioSessionHelper.swift │ ├── Alert.swift │ └── SegueHandlerType.swift ├── Recording │ ├── RecordingViewController+Playing.swift │ ├── RecordingViewController+Recording.swift │ ├── RecordingManager.swift │ └── RecordingViewController.swift ├── Player │ └── PlayManager.swift ├── AudioList │ ├── CircularProgressView.swift │ └── VoiceListViewController.swift └── Base.lproj │ └── Main.storyboard ├── VoiceMemo.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── .gitignore ├── VoiceMemoTests └── Supporting Files │ └── Info.plist ├── VoiceMemoUITests └── Supporting Files │ └── Info.plist └── README.md /screenshots/screenshoot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/screenshots/screenshoot.gif -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/list.imageset/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/list.imageset/Untitled.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/list.imageset/Untitled@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/list.imageset/Untitled@2x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/list.imageset/Untitled@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/list.imageset/Untitled@3x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/play_begin.imageset/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/play_begin.imageset/Untitled.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/play_pause.imageset/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/play_pause.imageset/Untitled.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/play_begin.imageset/Untitled@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/play_begin.imageset/Untitled@2x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/play_begin.imageset/Untitled@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/play_begin.imageset/Untitled@3x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/play_pause.imageset/Untitled@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/play_pause.imageset/Untitled@2x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/play_pause.imageset/Untitled@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/play_pause.imageset/Untitled@3x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/record_begin.imageset/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/record_begin.imageset/Untitled.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/record_pause.imageset/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/record_pause.imageset/Untitled.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/record_begin.imageset/Untitled@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/record_begin.imageset/Untitled@2x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/record_begin.imageset/Untitled@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/record_begin.imageset/Untitled@3x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/record_pause.imageset/Untitled@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/record_pause.imageset/Untitled@2x.png -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/record_pause.imageset/Untitled@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shannon-s-Dreamland/VoiceMemo/HEAD/VoiceMemo/Supporting Files/Assets.xcassets/record_pause.imageset/Untitled@3x.png -------------------------------------------------------------------------------- /VoiceMemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | ## Build generated 4 | build/* 5 | DerivedData 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | 18 | ## Other 19 | *.xccheckout 20 | *.moved-aside 21 | *.xcuserstate 22 | *.xcscmblueprint 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | 28 | # AppCode 29 | .idea/ 30 | 31 | # Finder 32 | .DS_Store 33 | 34 | # Jazzy 35 | docs/ 36 | -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/list.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Untitled.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Untitled@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Untitled@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/play_begin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Untitled.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Untitled@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Untitled@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/play_pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Untitled.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Untitled@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Untitled@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/record_begin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Untitled.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Untitled@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Untitled@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/record_pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Untitled.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Untitled@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Untitled@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/25/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | return true 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /VoiceMemo/Data/DateHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateHandler.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSDate { 12 | var stringValue: String { 13 | let dateFormatter: NSDateFormatter = { 14 | let formatter = NSDateFormatter() 15 | formatter.dateStyle = .ShortStyle 16 | formatter.timeStyle = .ShortStyle 17 | return formatter 18 | }() 19 | 20 | return dateFormatter.stringFromDate(self) 21 | } 22 | } -------------------------------------------------------------------------------- /VoiceMemo/Utils/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Double { 12 | func format(f: Int) -> String { 13 | return NSString(format: "%.\(f)f", self) as String 14 | } 15 | } 16 | 17 | extension String { 18 | /** 19 | 将 String 用指定的 Separator 切割成 String 数组 20 | 21 | - parameter separator: 要用的 Separator, 默认为空格 22 | 23 | - returns: 一个切割好的 String 数组 24 | */ 25 | func segmentsWithSeparator(separator: Character = " ") -> [String] { 26 | return self.characters.split(separator).map(String.init) 27 | } 28 | } -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /VoiceMemoTests/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /VoiceMemoUITests/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /VoiceMemo/Data/VoiceMemo.xcdatamodeld/VoiceMemo.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /VoiceMemo/Data/Voice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Voice+CoreDataProperties.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu 9 | // to delete and recreate this implementation file for your updated model. 10 | // 11 | 12 | import Foundation 13 | import CoreData 14 | 15 | class Voice: NSManagedObject { 16 | 17 | @NSManaged var name: String 18 | @NSManaged var date: NSDate 19 | @NSManaged var duration: Double 20 | 21 | 22 | func fetchPathURL() -> NSURL? { 23 | do { 24 | let URL = try FileHandler.getDirectoryURL().URLByAppendingPathComponent(name) 25 | return URL 26 | } catch { 27 | assertionFailure() 28 | return nil 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /VoiceMemo/Data/FileHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioFileHandler.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum FileHandlerError: ErrorType { 12 | case DirectoryCreationError 13 | } 14 | 15 | struct FileHandler { 16 | static func getDirectoryURL() throws -> NSURL { 17 | let doucumentURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0] 18 | 19 | let directoryURL = doucumentURL.URLByAppendingPathComponent("Voice") 20 | do { 21 | try NSFileManager.defaultManager().createDirectoryAtURL(directoryURL, withIntermediateDirectories: true, attributes: nil) 22 | } catch { 23 | debugPrint("无法创建磁盘目录: \(error)") 24 | throw FileHandlerError.DirectoryCreationError 25 | } 26 | return directoryURL 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /VoiceMemo/Utils/AudioSessionHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioSessionHelper.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | class AudioSessionHelper { 12 | 13 | class func setupSessionActive(active: Bool, catagory: String = AVAudioSessionCategoryPlayback) { 14 | let session = AVAudioSession.sharedInstance() 15 | if active { 16 | do { 17 | try session.setCategory(catagory) 18 | } catch { 19 | assertionFailure() 20 | } 21 | 22 | do { 23 | try session.setActive(true) 24 | } catch { 25 | assertionFailure() 26 | } 27 | } else { 28 | do { 29 | try session.setActive(false, withOptions: .NotifyOthersOnDeactivation) 30 | } catch { 31 | assertionFailure() 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 说明 3 | 4 | ![screenshot](screenshots/screenshoot.gif) 5 | 6 | 这是一个使用 AVAudioRecorder和 AVAudioPlayer 配合 Core Data 写的一个简单的音频录制和播放 Demo, 虽然界面比较粗糙, 而且因为时间问题, 应该会有点小 Bug, 但是代码和文件的管理应该能够达到生产水平, 所以修改 Bug 会十分容易! 7 | 8 | 我现在正计划写 iOS 开发的两个专题, 分别是关于36氪4.0版本的程序架构和用面向对象的方法做UI开发, 这个示例属于后者, 你可以在[这个网址](http://www.jianshu.com/users/61cd67a3e447/latest_articles)找到所有的文章, 很遗憾, 因为我们4.0的开发任务比较繁重,尽管我最近经常晚上写代码到凌晨, 但是还是 一直没有时间总结这些东西, 我是计划春节后将这两个专题的文章都完成, 算是在36氪工作半年来的一个总结. 希望回家后我的小侄子不会缠着我吧! 9 | 10 | 你可以随时关注上面的博客地址来获取最新的更新. 11 | 12 | ## 关于这个示例 13 | 14 | 这个示例不是最终版本, 从程序的大概样式也可以看出. 因为我们的客户端没有音频功能, 所以对这一块并不能说很系统的把握, 特别是 Core Data, 工作之后就没有再用过, 很多知识都是凭借模糊的记忆, 以及翻看苹果资料 Github 别人的代码书写的, 唯一可以保证的是整个的设计开发流程是自己的, 而且还是蛮不错的, 特别是对状态的管理, 最近使用 AVFoudation, 可能最大的感触就是相比较简单的界面, 里面的状态更多, 更不易管理, 使用程序中的状态机的原理是我找到的比较好的管理方案. 15 | 16 | 计划过年前的一个多月借助苹果的文档和 WWDC 的视频对这一块有一个系统的认识, 系统我可以找到一个助手 UI, 将这个 demo 逐步的完善成一个优秀的 demo, 哈哈, 好吧, 这确实是一个 demo, 只是不喜欢制造垃圾. 17 | 18 | 年底前还有一个计划就是系统的了解 AutoLayout, 打算写一个自己用的库, 实在不喜欢 SnapKit 的前缀语法, 你可以查看 [DropdownMenu](https://inatu@github.com/Shannon-s-Dreamland/DropdownMenu.git)里面的对 UIView 的扩展来了解一下大概的想法. 19 | -------------------------------------------------------------------------------- /VoiceMemo/Recording /RecordingViewController+Playing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordingViewController+Playing.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension RecordingViewController { 12 | func updatePlayStatusWithCurrentTime(currentTime: NSTimeInterval, totalTime: NSTimeInterval) { 13 | timeLabel.text = currentTime.format(2) + "s/" + totalTime.format(2) + "s" 14 | } 15 | 16 | func playerDidChangeToState(state: PlaybackState) { 17 | switch state { 18 | case .Initial, 19 | .Finished: 20 | self.state = .RecordingFinished 21 | case .Pause, 22 | .Play:() 23 | } 24 | } 25 | 26 | func handlePlaybackError() { 27 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 28 | self.state = .RecordingFinished 29 | }) 30 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("播放错误, 请重试", comment: ""), message: nil, actionTuples: [cancel]) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /VoiceMemo/Supporting Files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /VoiceMemo/Utils/Alert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertManager.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | typealias AlertHandler = ((UIAlertAction) -> Void)? 12 | typealias AlertActionTuple = (title: String, style: UIAlertActionStyle, handler: AlertHandler) 13 | 14 | protocol TextFieldAlertDelegate { 15 | func inputFinishedWithContent(content: String?) 16 | } 17 | 18 | struct Alert { 19 | private static func actionsFromTuple(tuple: [AlertActionTuple]) -> [UIAlertAction] { 20 | var actions = [UIAlertAction]() 21 | 22 | tuple.forEach { (title, style, handler) -> () in 23 | actions.append(UIAlertAction(title: title, style: style, handler: handler)) 24 | } 25 | 26 | return actions 27 | } 28 | 29 | static func showAlertWithSourceViewController(sourceVC: UIViewController, title: String?, message: String?, actionTuples: [AlertActionTuple]) { 30 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) 31 | actionsFromTuple(actionTuples).forEach { (action) -> () in 32 | alertController.addAction(action) 33 | } 34 | 35 | sourceVC.presentViewController(alertController, animated: true, completion: nil) 36 | } 37 | 38 | static func showInputAlertWithSourceViewController(sourceVC: UIViewController, title: String?, message: String?, confirmTitle: String?, delegate: TextFieldAlertDelegate) { 39 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) 40 | alertController.addTextFieldWithConfigurationHandler(nil) 41 | 42 | let confirmAction = UIAlertAction(title: confirmTitle, style: .Default) { _ in 43 | delegate.inputFinishedWithContent(alertController.textFields?.first?.text) 44 | } 45 | alertController.addAction(confirmAction) 46 | sourceVC.presentViewController(alertController, animated: true, completion: nil) 47 | 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /VoiceMemo/Utils/SegueHandlerType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Client 4 | // 5 | // Created by Shannon Wu on 10/25/15. 6 | // Copyright © 2015 36Kr. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | /// StoryboardSegue 是 UIStoryboardSegue 的别名 12 | typealias StoryboardSegue = UIStoryboardSegue 13 | /// ViewController 是 UIViewController 的别名 14 | typealias ViewController = UIViewController 15 | #elseif os(OSX) 16 | import Cocoa 17 | /// StoryboardSegue 是 NSStoryboardSegue 的别名 18 | typealias StoryboardSegue = NSStoryboardSegue 19 | /// ViewController 是 NSWindowController 的别名 20 | typealias ViewController = NSWindowController 21 | #endif 22 | 23 | /** 24 | * 添加 `SegueHandlerType` 协议, 只有 `ViewController` 才可以继承这个协议, 并且限定 25 | `SegueIdentifier` 属性的 `RawValue` 必须是 `String` 类型 26 | */ 27 | protocol SegueHandlerType { 28 | 29 | typealias SegueIdentifier: RawRepresentable 30 | 31 | } 32 | 33 | // MARK: - 添加 `SegueHandlerType` 协议, 只有 `ViewController` 才可以继承这个 34 | // 协议,并且限定 `SegueIdentifier` 属性的 `RawValue` 必须是 `String` 类型 35 | extension SegueHandlerType where Self: ViewController, SegueIdentifier.RawValue == String { 36 | 37 | /** 38 | 覆写 `UIViewController` 的 `performSegueWithIdentifier(_:sender:)` 方法, 39 | 但是接受的数据类型是 `segueIdentifier` 枚举类型, 而不是 `String` 类型 40 | 41 | - parameter segueIdentifier: 触发 `Segue` 的 `SegueIdentifier` 42 | - parameter sender: 触发 `Segue` 的发送者 43 | */ 44 | func performSegueWithIdentifier(segueIdentifier: SegueIdentifier, sender: AnyObject?) { 45 | performSegueWithIdentifier(segueIdentifier.rawValue, sender: sender) 46 | } 47 | 48 | /** 49 | 通过 `Segue` 实例获取 `SegueIdentifier` 的 enum 实例, 如果 SegueIdentifier 中没有 enum 实例, 50 | 说明发生错误, 触发 fatalError 51 | 52 | - parameter segue: 要获取 `SegueIdentifier` 的 `Segue` 实例 53 | 54 | - returns: `SegueIdentifier` 的一个 enum 实例, 或者触发 fatalError 55 | */ 56 | func segueIdentifierForSegue(segue: StoryboardSegue) -> SegueIdentifier { 57 | guard let identifier = segue.identifier, 58 | segueIdentifier = SegueIdentifier(rawValue: identifier) else { 59 | fatalError("在 \(self.dynamicType) 中, 无法通过 \(segue.identifier) 获取 SegueIdentifier 实例.") 60 | } 61 | 62 | return segueIdentifier 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /VoiceMemo/Recording /RecordingViewController+Recording.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordingViewController+ErrorHandling.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension RecordingViewController: RecordingManagerDelegate { 12 | 13 | func updateRecordedTimeInterval(duration: NSTimeInterval) { 14 | timeLabel.text = duration.format(0) + " s" 15 | } 16 | 17 | func handleRecordingInterrupted() { 18 | self.state = .Initial 19 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 20 | // 21 | }) 22 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("录制过程发生中断, 请重新录制", comment: ""), message: nil, actionTuples: [cancel]) 23 | 24 | } 25 | 26 | func hanldePermissionError() { 27 | self.state = .Initial 28 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 29 | // 30 | }) 31 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("无法访问您设备的音频, 请在个人隐私中开启", comment: ""), message: nil, actionTuples: [cancel]) 32 | } 33 | 34 | func handleRecorderFetchError() { 35 | self.state = .Initial 36 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 37 | // 38 | }) 39 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("当前无法进行音频录制, 请稍后重试", comment: ""), message: nil, actionTuples: [cancel]) 40 | } 41 | 42 | func handleRecorderEncodingError() { 43 | self.state = .Initial 44 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 45 | // 46 | }) 47 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("音频编码失败, 请稍后重试", comment: ""), message: nil, actionTuples: [cancel]) 48 | } 49 | 50 | func handleVoiceSaveError() { 51 | self.state = .RecordingFinished 52 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 53 | // 54 | }) 55 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("文件保存失败, 请尝试重新命名", comment: ""), message: nil, actionTuples: [cancel]) 56 | } 57 | } -------------------------------------------------------------------------------- /VoiceMemo/Data/CoreDataStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataCenter.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/25/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | enum CoreDataStackError: ErrorType { 13 | case CannotCreateObject 14 | } 15 | 16 | final class CoreDataStack { 17 | // MARK: - Singleton 18 | 19 | private class var manager:CoreDataStack { 20 | struct SingletonWrapper { 21 | static let singleton = CoreDataStack() 22 | } 23 | return SingletonWrapper.singleton 24 | } 25 | 26 | static func save() { 27 | manager.saveContext() 28 | } 29 | 30 | static func createNewObject() throws -> T { 31 | if let object = NSEntityDescription.insertNewObjectForEntityForName("Voice", inManagedObjectContext: manager.managedObjectContext) as? T { 32 | return object 33 | } else { 34 | debugPrint("创建Voice失败") 35 | throw CoreDataStackError.CannotCreateObject 36 | } 37 | } 38 | 39 | static var context: NSManagedObjectContext { 40 | return manager.managedObjectContext 41 | } 42 | 43 | // MARK: - Initializers 44 | 45 | private init() {} 46 | 47 | // MARK: - Core Data stack 48 | 49 | private lazy var applicationDocumentsDirectory: NSURL = { 50 | let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) 51 | return urls[urls.count-1] 52 | }() 53 | 54 | private lazy var managedObjectModel: NSManagedObjectModel = { 55 | let modelURL = NSBundle.mainBundle().URLForResource("VoiceMemo", withExtension: "momd")! 56 | return NSManagedObjectModel(contentsOfURL: modelURL)! 57 | }() 58 | 59 | private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { 60 | let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) 61 | let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite") 62 | var failureReason = "There was an error creating or loading the application's saved data." 63 | do { 64 | try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil) 65 | } catch { 66 | var dict = [String: AnyObject]() 67 | dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" 68 | dict[NSLocalizedFailureReasonErrorKey] = failureReason 69 | 70 | dict[NSUnderlyingErrorKey] = error as NSError 71 | let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) 72 | NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") 73 | abort() 74 | } 75 | 76 | return coordinator 77 | }() 78 | 79 | private lazy var managedObjectContext: NSManagedObjectContext = { 80 | let coordinator = self.persistentStoreCoordinator 81 | var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) 82 | managedObjectContext.persistentStoreCoordinator = coordinator 83 | return managedObjectContext 84 | }() 85 | 86 | private func saveContext () { 87 | if managedObjectContext.hasChanges { 88 | do { 89 | try managedObjectContext.save() 90 | } catch { 91 | let nserror = error as NSError 92 | NSLog("Unresolved error \(nserror), \(nserror.userInfo)") 93 | abort() 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /VoiceMemo/Player/PlayManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioPlayer.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | // MARK: - PlayManagerDelegate 13 | 14 | protocol PlayManagerDelegate: class { 15 | func updatePlayStatusWithCurrentTime(currentTime: NSTimeInterval, totalTime: NSTimeInterval) 16 | func playerDidChangeToState(state: PlaybackState) 17 | func handlePlaybackError() 18 | } 19 | 20 | // MARK: - Playback State 21 | 22 | enum PlaybackState { 23 | case Initial 24 | case Play 25 | case Pause 26 | case Finished 27 | } 28 | 29 | // MARK: - Play Manager 30 | 31 | class PlayManager: NSObject { 32 | 33 | // MARK: Properties 34 | 35 | private var player: AVAudioPlayer? 36 | 37 | private var playTimer: NSTimer? { 38 | willSet { 39 | if newValue == nil { 40 | playTimer?.invalidate() 41 | } 42 | } 43 | } 44 | 45 | private var state: PlaybackState = .Initial { 46 | didSet { 47 | switch state { 48 | case .Initial, 49 | .Finished: 50 | player = nil 51 | playTimer = nil 52 | currentItem = nil 53 | AudioSessionHelper.setupSessionActive(false) 54 | case .Play: 55 | playTimer = nil 56 | playTimer = NSTimer( 57 | timeInterval: 0.1, 58 | target: self, 59 | selector: "updateProgress", 60 | userInfo: nil, 61 | repeats: true) 62 | NSRunLoop.currentRunLoop().addTimer(playTimer!, forMode: NSRunLoopCommonModes) 63 | 64 | if player != nil { 65 | player?.play() 66 | return 67 | } 68 | 69 | do { 70 | guard currentItem != nil else { 71 | assertionFailure() 72 | state = .Initial 73 | return 74 | } 75 | try player = AVAudioPlayer(contentsOfURL: currentItem!) 76 | player?.delegate = self 77 | 78 | AudioSessionHelper.setupSessionActive(true) 79 | player?.play() 80 | } catch { 81 | state = .Initial 82 | delegate?.handlePlaybackError() 83 | } 84 | case .Pause: 85 | playTimer = nil 86 | player?.pause() 87 | AudioSessionHelper.setupSessionActive(false) 88 | } 89 | delegate?.playerDidChangeToState(state) 90 | } 91 | } 92 | 93 | private var currentItem: NSURL? { 94 | didSet { 95 | guard currentItem != nil else { return } 96 | state = .Play 97 | } 98 | } 99 | 100 | weak var delegate: PlayManagerDelegate? 101 | 102 | // MARK: Initializers 103 | 104 | init(delegate: PlayManagerDelegate?) { 105 | super.init() 106 | 107 | self.delegate = delegate 108 | } 109 | 110 | // MARK: Convience 111 | 112 | func playItem(item: NSURL) { 113 | state = .Initial 114 | 115 | currentItem = item 116 | } 117 | 118 | func stop() { 119 | state = .Initial 120 | } 121 | 122 | func playingVoice(voice: Voice) -> Bool { 123 | return voice.name == currentItem?.lastPathComponent 124 | } 125 | 126 | func togglePlayState() { 127 | if state == .Play { 128 | state = .Pause 129 | } else if state == .Pause { 130 | state = .Play 131 | } 132 | } 133 | 134 | func updateProgress() { 135 | if let currentTime = player?.currentTime, 136 | totalTime = player?.duration { 137 | delegate?.updatePlayStatusWithCurrentTime(currentTime, totalTime: totalTime) 138 | } 139 | } 140 | 141 | } 142 | 143 | // MARK: - AVAudioPlayerDelegate 144 | 145 | extension PlayManager: AVAudioPlayerDelegate { 146 | 147 | func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) { 148 | if flag { 149 | state = .Finished 150 | } 151 | } 152 | 153 | func audioPlayerDecodeErrorDidOccur(player: AVAudioPlayer, error: NSError?) { 154 | debugPrint(error) 155 | 156 | delegate?.handlePlaybackError() 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /VoiceMemo/AudioList/CircularProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularProgressView.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | class CircularProgressView: UIView { 13 | // MARK: Types 14 | 15 | enum Style { 16 | case Play 17 | case Pause 18 | case Stop 19 | 20 | func path(layerBounds: CGRect) -> CGPath { 21 | switch self { 22 | case .Play: 23 | let path = UIBezierPath() 24 | path.moveToPoint(CGPoint(x: layerBounds.width / 5, y: 0)) 25 | path.addLineToPoint(CGPoint(x: layerBounds.width, y: layerBounds.height / 2)) 26 | path.addLineToPoint(CGPoint(x: layerBounds.width / 5, y: layerBounds.height)) 27 | path.closePath() 28 | return path.CGPath 29 | case .Pause: 30 | var rect = CGRect(origin: CGPoint(x: layerBounds.width * 0.1, y: 0), size: CGSize(width: layerBounds.width * 0.2, height: layerBounds.height)) 31 | let path = UIBezierPath(rect: rect) 32 | rect.offsetInPlace(dx: layerBounds.width * 0.6, dy: 0) 33 | path.appendPath(UIBezierPath(rect: rect)) 34 | return path.CGPath 35 | case .Stop: 36 | let insetBounds = CGRectInset(layerBounds, layerBounds.width / 6, layerBounds.width / 6) 37 | let path = UIBezierPath(rect: insetBounds) 38 | return path.CGPath 39 | } 40 | } 41 | } 42 | 43 | // MARK: Properties 44 | 45 | lazy var backgroundLayer: CAShapeLayer = { 46 | let layer = CAShapeLayer() 47 | layer.fillColor = nil 48 | layer.lineWidth = self.lineWidth 49 | layer.strokeColor = self.backgroundLayerStrokeColor.CGColor 50 | self.layer.addSublayer(layer) 51 | 52 | return layer 53 | }() 54 | 55 | lazy var progressLayer: CAShapeLayer = { 56 | let layer = CAShapeLayer() 57 | layer.fillColor = nil 58 | layer.lineWidth = self.lineWidth 59 | layer.strokeColor = self.tintColor.CGColor 60 | self.layer.insertSublayer(layer, above: self.backgroundLayer) 61 | 62 | return layer 63 | }() 64 | 65 | lazy var iconLayer: CAShapeLayer = { 66 | let layer = CAShapeLayer() 67 | layer.fillColor = self.tintColor.CGColor 68 | self.layer.addSublayer(layer) 69 | 70 | return layer 71 | }() 72 | 73 | var iconStyle: Style = .Play { 74 | didSet { 75 | iconLayer.path = iconStyle.path(iconLayer.bounds) 76 | } 77 | } 78 | 79 | 80 | var iconLayerBounds: CGRect { 81 | return iconLayer.bounds 82 | } 83 | 84 | @IBInspectable var lineWidth: CGFloat = 3 { 85 | didSet { 86 | backgroundLayer.lineWidth = lineWidth 87 | progressLayer.lineWidth = lineWidth 88 | } 89 | } 90 | 91 | @IBInspectable var iconLayerFrameRatio: CGFloat = 0.4 { 92 | didSet { 93 | iconLayer.frame = iconLayerFrame(iconLayerBounds, ratio: iconLayerFrameRatio) 94 | iconLayer.path = iconStyle.path(iconLayerBounds) 95 | } 96 | } 97 | 98 | @IBInspectable var backgroundLayerStrokeColor: UIColor = UIColor(white: 0.8, alpha: 1) { 99 | didSet { 100 | backgroundLayer.strokeColor = backgroundLayerStrokeColor.CGColor 101 | } 102 | } 103 | 104 | @IBInspectable var progressLayerStrokeColor: UIColor? { 105 | didSet { 106 | progressLayer.strokeColor = progressLayerStrokeColor?.CGColor ?? tintColor.CGColor 107 | } 108 | } 109 | 110 | @IBInspectable var progress: CGFloat = 0.0 { 111 | didSet { 112 | progressLayer.strokeEnd = progress 113 | } 114 | } 115 | 116 | // MARK: Functions 117 | 118 | func setProgress(progress: CGFloat, animated: Bool = true) { 119 | if animated { 120 | self.progress = progress 121 | } else { 122 | self.progress = progress 123 | let animation = CABasicAnimation(keyPath: "strokeEnd") 124 | animation.fromValue = progress 125 | animation.duration = 0.0 126 | progressLayer.addAnimation(animation, forKey: nil) 127 | } 128 | } 129 | 130 | func iconLayerFrame(rect: CGRect, ratio: CGFloat) -> CGRect { 131 | let insetRatio = (1 - ratio) / 2.0 132 | return CGRectInset(rect, CGRectGetWidth(rect) * insetRatio, CGRectGetHeight (rect) * insetRatio) 133 | } 134 | 135 | func getSquareLayerFrame(rect: CGRect) -> CGRect { 136 | if rect.width != rect.height { 137 | let width = min(rect.width, rect.height) 138 | 139 | let originX = (rect.width - width) / 2 140 | let originY = (rect.height - width) / 2 141 | 142 | return CGRectMake(originX, originY, width, width) 143 | } 144 | return rect 145 | } 146 | 147 | override func layoutSubviews() { 148 | super.layoutSubviews() 149 | let squareRect = getSquareLayerFrame(layer.bounds) 150 | backgroundLayer.frame = squareRect 151 | progressLayer.frame = squareRect 152 | 153 | let innerRect = CGRectInset(squareRect, lineWidth / 2.0, lineWidth / 2.0) 154 | iconLayer.frame = iconLayerFrame(innerRect, ratio: iconLayerFrameRatio) 155 | 156 | let center = CGPointMake(squareRect.width / 2.0, squareRect.height / 2.0) 157 | let path = UIBezierPath(arcCenter: center, radius: innerRect.width / 2.0, startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(-M_PI_2 + 2.0 * M_PI), clockwise: true) 158 | backgroundLayer.path = path.CGPath 159 | progressLayer.path = path.CGPath 160 | iconLayer.path = iconStyle.path(iconLayerBounds) 161 | } 162 | 163 | override func prepareForInterfaceBuilder() { 164 | super.prepareForInterfaceBuilder() 165 | 166 | iconStyle = .Play 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /VoiceMemo/Recording /RecordingManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordingManager.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/26/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | // MARK: - RecordingManagerDelegate 13 | 14 | protocol RecordingManagerDelegate: class { 15 | func updateRecordedTimeInterval(duration: NSTimeInterval) 16 | func handleRecordingInterrupted() 17 | func hanldePermissionError() 18 | func handleRecorderFetchError() 19 | func handleRecorderEncodingError() 20 | func handleVoiceSaveError() 21 | } 22 | 23 | // MARK: - RecordingError 24 | 25 | enum RecordingError: ErrorType { 26 | case AudioExist 27 | case RecorderNotExist 28 | } 29 | 30 | // MARK: RecordingState 31 | 32 | enum RecordingState { 33 | case Initial 34 | case Finished 35 | } 36 | 37 | // MARK: - RecordingManager 38 | 39 | class RecordingManager:NSObject, AVAudioRecorderDelegate { 40 | // MARK: Properties 41 | 42 | weak var delegate: RecordingManagerDelegate? 43 | var audioDuration: NSTimeInterval { 44 | return recordDuration 45 | } 46 | private var state = RecordingState.Initial 47 | private var audioRecorder: AVAudioRecorder? 48 | private var recordTimer: NSTimer? { 49 | willSet { 50 | if newValue == nil { 51 | recordTimer?.invalidate() 52 | } 53 | } 54 | } 55 | private var recordDuration: NSTimeInterval = 0 56 | let tmpStoreURL = NSURL.fileURLWithPath(NSTemporaryDirectory()).URLByAppendingPathComponent("tmpVoice.caf") 57 | 58 | // MARK: Initializers 59 | init(delegate: RecordingManagerDelegate?) { 60 | super.init() 61 | 62 | self.delegate = delegate 63 | prepareForRecording() 64 | } 65 | 66 | // MARK: Recording Management 67 | 68 | func startNewRecording() throws { 69 | guard let recorder = self.audioRecorder else { throw RecordingError.RecorderNotExist } 70 | guard state != .Finished else { throw RecordingError.AudioExist } 71 | 72 | AudioSessionHelper.setupSessionActive(true, catagory: AVAudioSessionCategoryRecord) 73 | if recorder.prepareToRecord() { 74 | debugPrint("Start recording") 75 | 76 | audioRecorder?.record() 77 | NSNotificationCenter.defaultCenter().addObserver(self, selector: "handleInterruption:", name: AVAudioSessionInterruptionNotification, object: AVAudioSession.sharedInstance()) 78 | 79 | self.recordTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, 80 | target: self, 81 | selector: "updateDuration", 82 | userInfo: nil, 83 | repeats: true) 84 | } 85 | } 86 | 87 | func finishRecording() { 88 | recordTimer = nil 89 | state = .Finished 90 | recordDuration = audioRecorder?.currentTime ?? 0 91 | audioRecorder?.stop() 92 | AudioSessionHelper.setupSessionActive(false) 93 | } 94 | 95 | func deleteRecordedAudio() { 96 | state = .Initial 97 | } 98 | 99 | func saveRecordedAudioWithName(name: String?) { 100 | do { 101 | let voiceName = (name ?? NSLocalizedString("未命名", comment: "")) + " " + NSUUID().UUIDString 102 | 103 | let voice: Voice = try CoreDataStack.createNewObject() 104 | voice.date = NSDate() 105 | voice.duration = recordDuration 106 | voice.name = voiceName 107 | 108 | var storeURL = try FileHandler.getDirectoryURL() 109 | storeURL = storeURL.URLByAppendingPathComponent(voiceName) 110 | try NSFileManager.defaultManager().moveItemAtURL(tmpStoreURL, toURL: storeURL) 111 | CoreDataStack.save() 112 | } catch let error { 113 | debugPrint(error) 114 | delegate?.handleVoiceSaveError() 115 | } 116 | 117 | state = .Initial 118 | } 119 | 120 | // MARK: Convience 121 | 122 | private func prepareForRecording() { 123 | let session:AVAudioSession = AVAudioSession.sharedInstance() 124 | session.requestRecordPermission { granted in 125 | if granted { 126 | debugPrint("Recording permission has been granted") 127 | 128 | let recordSettings: [String : AnyObject] = [ 129 | AVFormatIDKey : NSNumber(unsignedInt: kAudioFormatLinearPCM), 130 | AVSampleRateKey : 44100.0, 131 | AVNumberOfChannelsKey : 2, 132 | AVLinearPCMBitDepthKey : 16, 133 | AVLinearPCMIsBigEndianKey : false, 134 | AVLinearPCMIsFloatKey : false, 135 | ] 136 | 137 | do { 138 | self.audioRecorder = try AVAudioRecorder(URL: self.tmpStoreURL, settings: recordSettings) 139 | self.audioRecorder?.delegate = self 140 | } catch { 141 | self.delegate?.handleRecorderFetchError() 142 | } 143 | } else { 144 | debugPrint("Recording permission has not been granted") 145 | 146 | self.delegate?.hanldePermissionError() 147 | } 148 | } 149 | } 150 | 151 | private func handleInterruption(notification: NSNotification) { 152 | if let userInfo = notification.userInfo { 153 | let interruptionType = userInfo[AVAudioSessionInterruptionTypeKey] as! UInt 154 | if interruptionType == AVAudioSessionInterruptionType.Began.rawValue { 155 | recordTimer = nil 156 | if audioRecorder?.recording == true { 157 | audioRecorder?.stop() 158 | audioRecorder?.deleteRecording() 159 | state = .Initial 160 | } 161 | } else if interruptionType == AVAudioSessionInterruptionType.Ended.rawValue { 162 | delegate?.handleRecordingInterrupted() 163 | } 164 | } 165 | } 166 | 167 | //MARK: RecordingManagerDelegate 168 | 169 | func updateDuration() { 170 | if let duration = audioRecorder?.currentTime { 171 | delegate?.updateRecordedTimeInterval(duration) 172 | } 173 | } 174 | 175 | // MARK: AVAudioRecorderDelegate 176 | 177 | func audioRecorderEncodeErrorDidOccur(recorder: AVAudioRecorder, error: NSError?) { 178 | assertionFailure() 179 | 180 | delegate?.handleRecorderEncodingError() 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /VoiceMemo/Recording /RecordingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/25/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RecordingViewController: UIViewController, SegueHandlerType, TextFieldAlertDelegate, PlayManagerDelegate { 12 | 13 | // MARK: Properties 14 | 15 | @IBOutlet weak var playButton: UIButton! 16 | @IBOutlet weak var recordImageView: UIImageView! 17 | @IBOutlet weak var saveButton: UIButton! 18 | @IBOutlet weak var timeLabel: UILabel! 19 | 20 | lazy var recordingManager: RecordingManager = { 21 | let recordingManager = RecordingManager(delegate: self) 22 | return recordingManager 23 | }() 24 | 25 | lazy var playManager: PlayManager = { 26 | let playManager = PlayManager(delegate: self) 27 | return playManager 28 | }() 29 | 30 | var audioURL: NSURL? 31 | 32 | // MARK: View Life Cycle 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | state = .Initial 38 | } 39 | 40 | override func viewWillAppear(animated: Bool) { 41 | super.viewWillAppear(animated) 42 | 43 | playManager.delegate = self 44 | } 45 | 46 | // MARK: Segue 47 | 48 | enum SegueIdentifier: String { 49 | case ShowVoiceList 50 | } 51 | 52 | override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool { 53 | if identifier == SegueIdentifier.ShowVoiceList.rawValue { 54 | switch state { 55 | case .Initial: 56 | return true 57 | default: 58 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 59 | }) 60 | let delete: AlertActionTuple = (title: NSLocalizedString("删除当前音频备忘录", comment: ""), style: .Destructive, { action in 61 | self.recordingManager.deleteRecordedAudio() 62 | self.state = .Initial 63 | self.performSegueWithIdentifier(.ShowVoiceList, sender: self) 64 | }) 65 | let save: AlertActionTuple = (title: NSLocalizedString("保存当前音频备忘录", comment: ""), style: .Default, { action in 66 | self.save() 67 | self.state = .Initial 68 | self.performSegueWithIdentifier(.ShowVoiceList, sender: self) 69 | }) 70 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("您已经录制了一个音频备忘录, 请选择如何处理当前备忘录", comment: ""), message: nil, actionTuples: [cancel, delete, save]) 71 | return false 72 | } 73 | } 74 | return true 75 | } 76 | 77 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 78 | let segueIdentifier = segueIdentifierForSegue(segue) 79 | 80 | switch segueIdentifier { 81 | case .ShowVoiceList: 82 | if let destinationVC = segue.destinationViewController as? VoiceListViewController { 83 | destinationVC.playManager = playManager 84 | } 85 | } 86 | } 87 | 88 | // MARK: State Change 89 | 90 | enum RecordingState { 91 | case Initial 92 | case Recording 93 | case RecordingFinished 94 | case Playing 95 | case PlayPaused 96 | } 97 | 98 | var state: RecordingState = .Initial { 99 | didSet { 100 | switch state { 101 | case .Initial: 102 | playButton.enabled = false 103 | playButton.setImage(UIImage(named: "play_begin"), forState: .Normal) 104 | saveButton.enabled = false 105 | saveButton.alpha = 0.8 106 | timeLabel.text = NSLocalizedString("长按录音键开始录音", comment: "") 107 | audioURL = nil 108 | case .Recording: 109 | playButton.enabled = false 110 | playButton.setImage(UIImage(named: "play_begin"), forState: .Normal) 111 | saveButton.enabled = false 112 | saveButton.alpha = 0.8 113 | timeLabel.text = NSLocalizedString("放开录音键停止录音", comment: "") 114 | case .RecordingFinished: 115 | playButton.enabled = true 116 | playButton.setImage(UIImage(named: "play_begin"), forState: .Normal) 117 | saveButton.enabled = true 118 | saveButton.alpha = 1.0 119 | audioURL = recordingManager.tmpStoreURL 120 | timeLabel.text = NSLocalizedString("点击播放按钮播放, 或者点击保存按钮保存", comment: "") 121 | case .Playing: 122 | playButton.enabled = true 123 | playButton.setImage(UIImage(named: "play_pause"), forState: .Normal) 124 | saveButton.enabled = false 125 | case .PlayPaused: 126 | playButton.enabled = true 127 | playButton.setImage(UIImage(named: "play_begin"), forState: .Normal) 128 | saveButton.enabled = true 129 | saveButton.alpha = 1.0 130 | } 131 | } 132 | } 133 | 134 | @IBAction func playAudio(sender: UIButton) { 135 | if state == .Playing { 136 | playManager.togglePlayState() 137 | state = .PlayPaused 138 | } else if state == .PlayPaused { 139 | playManager.togglePlayState() 140 | state = .Playing 141 | } else { 142 | if let URL = audioURL { 143 | playManager.playItem(URL) 144 | state = .Playing 145 | } else { 146 | assertionFailure() 147 | } 148 | state = .Playing 149 | } 150 | } 151 | 152 | @IBAction func saveAudio(sender: UIButton) { 153 | save() 154 | } 155 | 156 | func save() { 157 | Alert.showInputAlertWithSourceViewController(self, title: NSLocalizedString("标题", comment: ""), message: NSLocalizedString("请输入当前备忘录标题", comment: ""), confirmTitle: "保存", delegate: self) 158 | } 159 | 160 | func inputFinishedWithContent(content: String?) { 161 | recordingManager.saveRecordedAudioWithName(content) 162 | state = .Initial 163 | } 164 | 165 | @IBAction func recordAudio(sender: UILongPressGestureRecognizer) { 166 | switch sender.state { 167 | case .Began: 168 | do { 169 | try recordingManager.startNewRecording() 170 | state = .Recording 171 | } catch RecordingError.AudioExist { 172 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 173 | self.state = .RecordingFinished 174 | }) 175 | let delete: AlertActionTuple = (title: NSLocalizedString("删除旧的音频备忘录", comment: ""), style: .Destructive, { action in 176 | self.recordingManager.deleteRecordedAudio() 177 | self.state = .Initial 178 | }) 179 | let save: AlertActionTuple = (title: NSLocalizedString("保存音频备忘录", comment: ""), style: .Default, { action in 180 | self.save() 181 | self.state = .Initial 182 | }) 183 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("您已经录制了一个音频备忘录, 请保存旧的备忘录或者删除当前音频备忘录再进行新的录制", comment: ""), message: nil, actionTuples: [cancel, delete, save]) 184 | } catch let error { 185 | debugPrint("\(error)") 186 | handleRecordingInterrupted() 187 | } 188 | case .Ended: 189 | state = .RecordingFinished 190 | recordingManager.finishRecording() 191 | default:() 192 | } 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /VoiceMemo/AudioList/VoiceListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VoiceListViewController.swift 3 | // VoiceMemo 4 | // 5 | // Created by Shannon Wu on 12/25/15. 6 | // Copyright © 2015 Shannon's Dreamland. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | // MARK: - VoiceListCell 13 | 14 | enum VoiceListCellState { 15 | case NotCurrent 16 | case Current 17 | } 18 | 19 | class VoiceListCell: UITableViewCell { 20 | @IBOutlet weak var progressView: CircularProgressView! 21 | @IBOutlet weak var titleLabel: UILabel! 22 | @IBOutlet weak var dateLabel: UILabel! 23 | @IBOutlet weak var durationLabel: UILabel! 24 | 25 | var state = VoiceListCellState.NotCurrent { 26 | didSet { 27 | switch state { 28 | case .NotCurrent: 29 | progressView.progress = 0 30 | progressView.iconStyle = .Play 31 | case .Current:() 32 | } 33 | } 34 | } 35 | 36 | func configureCellWithVoice(voice: Voice) { 37 | titleLabel.text = voice.name.segmentsWithSeparator().first! 38 | dateLabel.text = voice.date.stringValue 39 | durationLabel.text = voice.duration.format(2) + " s" 40 | } 41 | 42 | override func prepareForReuse() { 43 | state = .NotCurrent 44 | } 45 | } 46 | 47 | // MARK: - VoiceListViewController 48 | 49 | class VoiceListViewController: UITableViewController { 50 | // MARK: Properties 51 | var fetchedResultsController : NSFetchedResultsController! 52 | var playManager: PlayManager? { 53 | didSet { 54 | playManager?.delegate = self 55 | } 56 | } 57 | var currentVoice: Voice? 58 | var currentCell: VoiceListCell? { 59 | if let indexPath = playingIndexpath, 60 | cell = tableView.cellForRowAtIndexPath(indexPath) as? VoiceListCell { 61 | return cell 62 | } 63 | return nil 64 | } 65 | var shouldRestorePlayingIndexpath = false 66 | var previousPlayingIndexpath: NSIndexPath? 67 | var previousVell: VoiceListCell? { 68 | if let indexPath = previousPlayingIndexpath, 69 | cell = tableView.cellForRowAtIndexPath(indexPath) as? VoiceListCell { 70 | return cell 71 | } 72 | return nil 73 | } 74 | 75 | var playingIndexpath: NSIndexPath? { 76 | didSet { 77 | if playingIndexpath == oldValue { 78 | if let cell = tableView.cellForRowAtIndexPath(playingIndexpath!) as? VoiceListCell { 79 | if cell.progressView.iconStyle == .Play { 80 | cell.progressView.iconStyle = .Pause 81 | } else { 82 | cell.progressView.iconStyle = .Play 83 | } 84 | 85 | playManager?.togglePlayState() 86 | return 87 | } 88 | } 89 | 90 | if previousPlayingIndexpath != nil && playingIndexpath != nil { 91 | previousVell?.state = .NotCurrent 92 | } 93 | if let indexPath = playingIndexpath, 94 | cell = tableView.cellForRowAtIndexPath(indexPath) as? VoiceListCell { 95 | let voice = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Voice 96 | cell.state = .Current 97 | if let URL = voice.fetchPathURL() { 98 | playManager?.playItem(URL) 99 | } else { 100 | assertionFailure() 101 | } 102 | } 103 | } 104 | } 105 | 106 | // MARK: View Life Cycle 107 | override func viewDidLoad() { 108 | super.viewDidLoad() 109 | 110 | let fetchRequest = NSFetchRequest(entityName: "Voice") 111 | fetchRequest.fetchBatchSize = 20 112 | let keySort = NSSortDescriptor(key: "date", ascending: false) 113 | fetchRequest.sortDescriptors = [keySort] 114 | fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, 115 | managedObjectContext: CoreDataStack.context, 116 | sectionNameKeyPath: nil, 117 | cacheName: nil) 118 | fetchedResultsController.delegate = self 119 | do { 120 | try fetchedResultsController.performFetch() 121 | } catch { 122 | debugPrint("无法获取数据库数据: \(error)") 123 | } 124 | 125 | tableView.estimatedRowHeight = 100.0 126 | tableView.tableFooterView = UIView() 127 | } 128 | 129 | // MARK: Convience 130 | 131 | func deleteVoice(voice: Voice) { 132 | playManager?.stop() 133 | 134 | do { 135 | let removeFileURL = try FileHandler.getDirectoryURL().URLByAppendingPathComponent(voice.name) 136 | try NSFileManager.defaultManager().removeItemAtURL(removeFileURL) 137 | CoreDataStack.context.deleteObject(voice) 138 | CoreDataStack.save() 139 | } catch let error { 140 | assertionFailure("\(error)") 141 | } 142 | } 143 | 144 | func restoreCell(cell: VoiceListCell) { 145 | cell.progressView.progress = 1.0 146 | cell.progressView.iconStyle = .Play 147 | } 148 | 149 | } 150 | 151 | // MARK: UITableViewDataSource 152 | 153 | extension VoiceListViewController { 154 | 155 | override func numberOfSectionsInTableView(tableView: UITableView) -> Int { 156 | return fetchedResultsController.sections!.count 157 | } 158 | 159 | override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 160 | let sectionInfo = fetchedResultsController.sections![section] 161 | return sectionInfo.numberOfObjects 162 | } 163 | 164 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 165 | return tableView.dequeueReusableCellWithIdentifier("Cell") as! VoiceListCell 166 | } 167 | 168 | override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { 169 | return true 170 | } 171 | 172 | override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { 173 | switch editingStyle { 174 | case .Delete: 175 | let voice = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Voice 176 | deleteVoice(voice) 177 | default: 178 | break 179 | } 180 | } 181 | 182 | } 183 | 184 | // MARK: UITableViewDelegate 185 | 186 | extension VoiceListViewController { 187 | override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { 188 | let voice = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Voice 189 | (cell as! VoiceListCell).configureCellWithVoice(voice) 190 | 191 | if indexPath == previousPlayingIndexpath && shouldRestorePlayingIndexpath { 192 | restoreCell(cell as! VoiceListCell) 193 | } 194 | } 195 | 196 | override func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { 197 | if indexPath == previousPlayingIndexpath { 198 | shouldRestorePlayingIndexpath = true 199 | } 200 | } 201 | 202 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 203 | playingIndexpath = indexPath 204 | previousPlayingIndexpath = nil 205 | tableView.deselectRowAtIndexPath(indexPath, animated: true) 206 | } 207 | } 208 | 209 | 210 | // MARK: NSFetchedResultsControllerDelegate 211 | 212 | extension VoiceListViewController: NSFetchedResultsControllerDelegate { 213 | 214 | func controllerWillChangeContent(controller: NSFetchedResultsController) { 215 | tableView.beginUpdates() 216 | } 217 | 218 | func controller(controller: NSFetchedResultsController, 219 | didChangeObject anObject: AnyObject, 220 | atIndexPath indexPath: NSIndexPath?, 221 | forChangeType type: NSFetchedResultsChangeType, 222 | newIndexPath: NSIndexPath?) { 223 | switch type { 224 | case .Insert, 225 | .Update, 226 | .Move: 227 | assertionFailure("没有实现这些操作") 228 | case .Delete: 229 | tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Automatic) 230 | } 231 | 232 | } 233 | 234 | func controllerDidChangeContent(controller: NSFetchedResultsController) { 235 | tableView.endUpdates() 236 | } 237 | 238 | } 239 | 240 | extension VoiceListViewController: PlayManagerDelegate { 241 | func updatePlayStatusWithCurrentTime(currentTime: NSTimeInterval, totalTime: NSTimeInterval) { 242 | let progress = currentTime / totalTime 243 | currentCell?.progressView.progress = CGFloat(progress) 244 | } 245 | 246 | func playerDidChangeToState(state: PlaybackState) { 247 | switch state { 248 | case .Play: 249 | currentCell?.progressView.iconStyle = .Pause 250 | case .Initial, 251 | .Pause: 252 | currentCell?.progressView.iconStyle = .Play 253 | case .Finished: 254 | if currentCell == nil { 255 | shouldRestorePlayingIndexpath = true 256 | } else { 257 | currentCell?.progressView.progress = 1.0 258 | currentCell?.progressView.iconStyle = .Play 259 | shouldRestorePlayingIndexpath = false 260 | } 261 | previousPlayingIndexpath = playingIndexpath 262 | playingIndexpath = nil 263 | } 264 | } 265 | 266 | func handlePlaybackError() { 267 | let cancel: AlertActionTuple = (title: NSLocalizedString("返回", comment: ""), style: .Cancel, { action in 268 | 269 | }) 270 | Alert.showAlertWithSourceViewController(self, title: NSLocalizedString("播放错误, 请重试", comment: ""), message: nil, actionTuples: [cancel]) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /VoiceMemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 35 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 126 | 132 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /VoiceMemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 610CA74C1C2E2DFE00436646 /* RecordingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610CA74B1C2E2DFE00436646 /* RecordingManager.swift */; }; 11 | 610CA74E1C2E353C00436646 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610CA74D1C2E353C00436646 /* Alert.swift */; }; 12 | 610CA7501C2E406400436646 /* PlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610CA74F1C2E406400436646 /* PlayManager.swift */; }; 13 | 610CA7521C2E411700436646 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610CA7511C2E411700436646 /* AudioSessionHelper.swift */; }; 14 | 610CA7541C2E58B400436646 /* FileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610CA7531C2E58B400436646 /* FileHandler.swift */; }; 15 | 610CA7561C2E5A6700436646 /* DateHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610CA7551C2E5A6700436646 /* DateHandler.swift */; }; 16 | 612545221C2D8AB5006281D2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612545211C2D8AB5006281D2 /* AppDelegate.swift */; }; 17 | 612545241C2D8AB5006281D2 /* RecordingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612545231C2D8AB5006281D2 /* RecordingViewController.swift */; }; 18 | 612545271C2D8AB5006281D2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 612545251C2D8AB5006281D2 /* Main.storyboard */; }; 19 | 6125452A1C2D8AB5006281D2 /* VoiceMemo.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 612545281C2D8AB5006281D2 /* VoiceMemo.xcdatamodeld */; }; 20 | 6125452C1C2D8AB5006281D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6125452B1C2D8AB5006281D2 /* Assets.xcassets */; }; 21 | 6125452F1C2D8AB5006281D2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6125452D1C2D8AB5006281D2 /* LaunchScreen.storyboard */; }; 22 | 612545571C2D8E66006281D2 /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612545561C2D8E66006281D2 /* CoreDataStack.swift */; }; 23 | 612545611C2D91E2006281D2 /* VoiceListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612545601C2D91E2006281D2 /* VoiceListViewController.swift */; }; 24 | 612545A21C2DA4E0006281D2 /* Voice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612545A11C2DA4E0006281D2 /* Voice.swift */; }; 25 | 612545A71C2DB0DE006281D2 /* SegueHandlerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612545A61C2DB0DE006281D2 /* SegueHandlerType.swift */; }; 26 | 613B807D1C2E66CB00DFFABB /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613B807C1C2E66CB00DFFABB /* Utils.swift */; }; 27 | 613B807F1C2E776900DFFABB /* RecordingViewController+Recording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613B807E1C2E776900DFFABB /* RecordingViewController+Recording.swift */; }; 28 | 618432B01C2ED14900759CBC /* RecordingViewController+Playing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618432AF1C2ED14900759CBC /* RecordingViewController+Playing.swift */; }; 29 | 618432B31C2ED8B400759CBC /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618432B21C2ED8B400759CBC /* CircularProgressView.swift */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | 612545361C2D8AB5006281D2 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 612545161C2D8AB5006281D2 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 6125451D1C2D8AB5006281D2; 38 | remoteInfo = VoiceMemo; 39 | }; 40 | 612545411C2D8AB5006281D2 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 612545161C2D8AB5006281D2 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 6125451D1C2D8AB5006281D2; 45 | remoteInfo = VoiceMemo; 46 | }; 47 | /* End PBXContainerItemProxy section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 610CA74B1C2E2DFE00436646 /* RecordingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingManager.swift; sourceTree = ""; }; 51 | 610CA74D1C2E353C00436646 /* Alert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; 52 | 610CA74F1C2E406400436646 /* PlayManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayManager.swift; sourceTree = ""; }; 53 | 610CA7511C2E411700436646 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = ""; }; 54 | 610CA7531C2E58B400436646 /* FileHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHandler.swift; sourceTree = ""; }; 55 | 610CA7551C2E5A6700436646 /* DateHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateHandler.swift; sourceTree = ""; }; 56 | 6125451E1C2D8AB5006281D2 /* VoiceMemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoiceMemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 612545211C2D8AB5006281D2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 58 | 612545231C2D8AB5006281D2 /* RecordingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingViewController.swift; sourceTree = ""; }; 59 | 612545261C2D8AB5006281D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 60 | 612545291C2D8AB5006281D2 /* VoiceMemo.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = VoiceMemo.xcdatamodel; sourceTree = ""; }; 61 | 6125452B1C2D8AB5006281D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 62 | 6125452E1C2D8AB5006281D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 63 | 612545301C2D8AB5006281D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 612545351C2D8AB5006281D2 /* VoiceMemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceMemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 6125453B1C2D8AB5006281D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 612545401C2D8AB5006281D2 /* VoiceMemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceMemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | 612545461C2D8AB5006281D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68 | 612545561C2D8E66006281D2 /* CoreDataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; 69 | 612545601C2D91E2006281D2 /* VoiceListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoiceListViewController.swift; sourceTree = ""; }; 70 | 612545A11C2DA4E0006281D2 /* Voice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Voice.swift; sourceTree = ""; }; 71 | 612545A61C2DB0DE006281D2 /* SegueHandlerType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegueHandlerType.swift; sourceTree = ""; }; 72 | 613B807C1C2E66CB00DFFABB /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 73 | 613B807E1C2E776900DFFABB /* RecordingViewController+Recording.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RecordingViewController+Recording.swift"; sourceTree = ""; }; 74 | 618432AF1C2ED14900759CBC /* RecordingViewController+Playing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RecordingViewController+Playing.swift"; sourceTree = ""; }; 75 | 618432B21C2ED8B400759CBC /* CircularProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; 76 | /* End PBXFileReference section */ 77 | 78 | /* Begin PBXFrameworksBuildPhase section */ 79 | 6125451B1C2D8AB5006281D2 /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | ); 84 | runOnlyForDeploymentPostprocessing = 0; 85 | }; 86 | 612545321C2D8AB5006281D2 /* Frameworks */ = { 87 | isa = PBXFrameworksBuildPhase; 88 | buildActionMask = 2147483647; 89 | files = ( 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | 6125453D1C2D8AB5006281D2 /* Frameworks */ = { 94 | isa = PBXFrameworksBuildPhase; 95 | buildActionMask = 2147483647; 96 | files = ( 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | /* End PBXFrameworksBuildPhase section */ 101 | 102 | /* Begin PBXGroup section */ 103 | 612545151C2D8AB5006281D2 = { 104 | isa = PBXGroup; 105 | children = ( 106 | 6125451F1C2D8AB5006281D2 /* Products */, 107 | 612545201C2D8AB5006281D2 /* VoiceMemo */, 108 | 612545381C2D8AB5006281D2 /* VoiceMemoTests */, 109 | 612545431C2D8AB5006281D2 /* VoiceMemoUITests */, 110 | ); 111 | sourceTree = ""; 112 | }; 113 | 6125451F1C2D8AB5006281D2 /* Products */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 6125451E1C2D8AB5006281D2 /* VoiceMemo.app */, 117 | 612545351C2D8AB5006281D2 /* VoiceMemoTests.xctest */, 118 | 612545401C2D8AB5006281D2 /* VoiceMemoUITests.xctest */, 119 | ); 120 | name = Products; 121 | sourceTree = ""; 122 | }; 123 | 612545201C2D8AB5006281D2 /* VoiceMemo */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 618432B11C2ED89900759CBC /* AudioList */, 127 | 612545551C2D8B7B006281D2 /* Data */, 128 | 613B80811C2E7BD800DFFABB /* Player */, 129 | 613B80801C2E779300DFFABB /* Recording */, 130 | 612545541C2D8B42006281D2 /* Supporting Files */, 131 | 612545A51C2DAB03006281D2 /* Utils */, 132 | 612545251C2D8AB5006281D2 /* Main.storyboard */, 133 | ); 134 | path = VoiceMemo; 135 | sourceTree = ""; 136 | }; 137 | 612545381C2D8AB5006281D2 /* VoiceMemoTests */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 612545531C2D8B38006281D2 /* Supporting Files */, 141 | ); 142 | path = VoiceMemoTests; 143 | sourceTree = ""; 144 | }; 145 | 612545431C2D8AB5006281D2 /* VoiceMemoUITests */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 612545521C2D8B2C006281D2 /* Supporting Files */, 149 | ); 150 | path = VoiceMemoUITests; 151 | sourceTree = ""; 152 | }; 153 | 612545521C2D8B2C006281D2 /* Supporting Files */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 612545461C2D8AB5006281D2 /* Info.plist */, 157 | ); 158 | path = "Supporting Files"; 159 | sourceTree = ""; 160 | }; 161 | 612545531C2D8B38006281D2 /* Supporting Files */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 6125453B1C2D8AB5006281D2 /* Info.plist */, 165 | ); 166 | path = "Supporting Files"; 167 | sourceTree = ""; 168 | }; 169 | 612545541C2D8B42006281D2 /* Supporting Files */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 612545211C2D8AB5006281D2 /* AppDelegate.swift */, 173 | 6125452B1C2D8AB5006281D2 /* Assets.xcassets */, 174 | 612545301C2D8AB5006281D2 /* Info.plist */, 175 | 6125452D1C2D8AB5006281D2 /* LaunchScreen.storyboard */, 176 | ); 177 | path = "Supporting Files"; 178 | sourceTree = ""; 179 | }; 180 | 612545551C2D8B7B006281D2 /* Data */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 612545561C2D8E66006281D2 /* CoreDataStack.swift */, 184 | 610CA7551C2E5A6700436646 /* DateHandler.swift */, 185 | 610CA7531C2E58B400436646 /* FileHandler.swift */, 186 | 612545A11C2DA4E0006281D2 /* Voice.swift */, 187 | 612545281C2D8AB5006281D2 /* VoiceMemo.xcdatamodeld */, 188 | ); 189 | path = Data; 190 | sourceTree = ""; 191 | }; 192 | 612545A51C2DAB03006281D2 /* Utils */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 610CA74D1C2E353C00436646 /* Alert.swift */, 196 | 610CA7511C2E411700436646 /* AudioSessionHelper.swift */, 197 | 612545A61C2DB0DE006281D2 /* SegueHandlerType.swift */, 198 | 613B807C1C2E66CB00DFFABB /* Utils.swift */, 199 | ); 200 | path = Utils; 201 | sourceTree = ""; 202 | }; 203 | 613B80801C2E779300DFFABB /* Recording */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | 610CA74B1C2E2DFE00436646 /* RecordingManager.swift */, 207 | 618432AF1C2ED14900759CBC /* RecordingViewController+Playing.swift */, 208 | 613B807E1C2E776900DFFABB /* RecordingViewController+Recording.swift */, 209 | 612545231C2D8AB5006281D2 /* RecordingViewController.swift */, 210 | ); 211 | path = "Recording "; 212 | sourceTree = ""; 213 | }; 214 | 613B80811C2E7BD800DFFABB /* Player */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 610CA74F1C2E406400436646 /* PlayManager.swift */, 218 | ); 219 | path = Player; 220 | sourceTree = ""; 221 | }; 222 | 618432B11C2ED89900759CBC /* AudioList */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | 618432B21C2ED8B400759CBC /* CircularProgressView.swift */, 226 | 612545601C2D91E2006281D2 /* VoiceListViewController.swift */, 227 | ); 228 | path = AudioList; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXGroup section */ 232 | 233 | /* Begin PBXNativeTarget section */ 234 | 6125451D1C2D8AB5006281D2 /* VoiceMemo */ = { 235 | isa = PBXNativeTarget; 236 | buildConfigurationList = 612545491C2D8AB5006281D2 /* Build configuration list for PBXNativeTarget "VoiceMemo" */; 237 | buildPhases = ( 238 | 6125451A1C2D8AB5006281D2 /* Sources */, 239 | 6125451B1C2D8AB5006281D2 /* Frameworks */, 240 | 6125451C1C2D8AB5006281D2 /* Resources */, 241 | ); 242 | buildRules = ( 243 | ); 244 | dependencies = ( 245 | ); 246 | name = VoiceMemo; 247 | productName = VoiceMemo; 248 | productReference = 6125451E1C2D8AB5006281D2 /* VoiceMemo.app */; 249 | productType = "com.apple.product-type.application"; 250 | }; 251 | 612545341C2D8AB5006281D2 /* VoiceMemoTests */ = { 252 | isa = PBXNativeTarget; 253 | buildConfigurationList = 6125454C1C2D8AB5006281D2 /* Build configuration list for PBXNativeTarget "VoiceMemoTests" */; 254 | buildPhases = ( 255 | 612545311C2D8AB5006281D2 /* Sources */, 256 | 612545321C2D8AB5006281D2 /* Frameworks */, 257 | 612545331C2D8AB5006281D2 /* Resources */, 258 | ); 259 | buildRules = ( 260 | ); 261 | dependencies = ( 262 | 612545371C2D8AB5006281D2 /* PBXTargetDependency */, 263 | ); 264 | name = VoiceMemoTests; 265 | productName = VoiceMemoTests; 266 | productReference = 612545351C2D8AB5006281D2 /* VoiceMemoTests.xctest */; 267 | productType = "com.apple.product-type.bundle.unit-test"; 268 | }; 269 | 6125453F1C2D8AB5006281D2 /* VoiceMemoUITests */ = { 270 | isa = PBXNativeTarget; 271 | buildConfigurationList = 6125454F1C2D8AB5006281D2 /* Build configuration list for PBXNativeTarget "VoiceMemoUITests" */; 272 | buildPhases = ( 273 | 6125453C1C2D8AB5006281D2 /* Sources */, 274 | 6125453D1C2D8AB5006281D2 /* Frameworks */, 275 | 6125453E1C2D8AB5006281D2 /* Resources */, 276 | ); 277 | buildRules = ( 278 | ); 279 | dependencies = ( 280 | 612545421C2D8AB5006281D2 /* PBXTargetDependency */, 281 | ); 282 | name = VoiceMemoUITests; 283 | productName = VoiceMemoUITests; 284 | productReference = 612545401C2D8AB5006281D2 /* VoiceMemoUITests.xctest */; 285 | productType = "com.apple.product-type.bundle.ui-testing"; 286 | }; 287 | /* End PBXNativeTarget section */ 288 | 289 | /* Begin PBXProject section */ 290 | 612545161C2D8AB5006281D2 /* Project object */ = { 291 | isa = PBXProject; 292 | attributes = { 293 | LastSwiftUpdateCheck = 0720; 294 | LastUpgradeCheck = 0720; 295 | ORGANIZATIONNAME = "Shannon's Dreamland"; 296 | TargetAttributes = { 297 | 6125451D1C2D8AB5006281D2 = { 298 | CreatedOnToolsVersion = 7.2; 299 | }; 300 | 612545341C2D8AB5006281D2 = { 301 | CreatedOnToolsVersion = 7.2; 302 | TestTargetID = 6125451D1C2D8AB5006281D2; 303 | }; 304 | 6125453F1C2D8AB5006281D2 = { 305 | CreatedOnToolsVersion = 7.2; 306 | TestTargetID = 6125451D1C2D8AB5006281D2; 307 | }; 308 | }; 309 | }; 310 | buildConfigurationList = 612545191C2D8AB5006281D2 /* Build configuration list for PBXProject "VoiceMemo" */; 311 | compatibilityVersion = "Xcode 3.2"; 312 | developmentRegion = English; 313 | hasScannedForEncodings = 0; 314 | knownRegions = ( 315 | en, 316 | Base, 317 | ); 318 | mainGroup = 612545151C2D8AB5006281D2; 319 | productRefGroup = 6125451F1C2D8AB5006281D2 /* Products */; 320 | projectDirPath = ""; 321 | projectRoot = ""; 322 | targets = ( 323 | 6125451D1C2D8AB5006281D2 /* VoiceMemo */, 324 | 612545341C2D8AB5006281D2 /* VoiceMemoTests */, 325 | 6125453F1C2D8AB5006281D2 /* VoiceMemoUITests */, 326 | ); 327 | }; 328 | /* End PBXProject section */ 329 | 330 | /* Begin PBXResourcesBuildPhase section */ 331 | 6125451C1C2D8AB5006281D2 /* Resources */ = { 332 | isa = PBXResourcesBuildPhase; 333 | buildActionMask = 2147483647; 334 | files = ( 335 | 6125452F1C2D8AB5006281D2 /* LaunchScreen.storyboard in Resources */, 336 | 6125452C1C2D8AB5006281D2 /* Assets.xcassets in Resources */, 337 | 612545271C2D8AB5006281D2 /* Main.storyboard in Resources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | 612545331C2D8AB5006281D2 /* Resources */ = { 342 | isa = PBXResourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | ); 346 | runOnlyForDeploymentPostprocessing = 0; 347 | }; 348 | 6125453E1C2D8AB5006281D2 /* Resources */ = { 349 | isa = PBXResourcesBuildPhase; 350 | buildActionMask = 2147483647; 351 | files = ( 352 | ); 353 | runOnlyForDeploymentPostprocessing = 0; 354 | }; 355 | /* End PBXResourcesBuildPhase section */ 356 | 357 | /* Begin PBXSourcesBuildPhase section */ 358 | 6125451A1C2D8AB5006281D2 /* Sources */ = { 359 | isa = PBXSourcesBuildPhase; 360 | buildActionMask = 2147483647; 361 | files = ( 362 | 610CA7561C2E5A6700436646 /* DateHandler.swift in Sources */, 363 | 6125452A1C2D8AB5006281D2 /* VoiceMemo.xcdatamodeld in Sources */, 364 | 613B807F1C2E776900DFFABB /* RecordingViewController+Recording.swift in Sources */, 365 | 610CA74C1C2E2DFE00436646 /* RecordingManager.swift in Sources */, 366 | 613B807D1C2E66CB00DFFABB /* Utils.swift in Sources */, 367 | 612545241C2D8AB5006281D2 /* RecordingViewController.swift in Sources */, 368 | 612545571C2D8E66006281D2 /* CoreDataStack.swift in Sources */, 369 | 618432B31C2ED8B400759CBC /* CircularProgressView.swift in Sources */, 370 | 612545221C2D8AB5006281D2 /* AppDelegate.swift in Sources */, 371 | 610CA7541C2E58B400436646 /* FileHandler.swift in Sources */, 372 | 612545611C2D91E2006281D2 /* VoiceListViewController.swift in Sources */, 373 | 612545A21C2DA4E0006281D2 /* Voice.swift in Sources */, 374 | 618432B01C2ED14900759CBC /* RecordingViewController+Playing.swift in Sources */, 375 | 610CA7501C2E406400436646 /* PlayManager.swift in Sources */, 376 | 612545A71C2DB0DE006281D2 /* SegueHandlerType.swift in Sources */, 377 | 610CA7521C2E411700436646 /* AudioSessionHelper.swift in Sources */, 378 | 610CA74E1C2E353C00436646 /* Alert.swift in Sources */, 379 | ); 380 | runOnlyForDeploymentPostprocessing = 0; 381 | }; 382 | 612545311C2D8AB5006281D2 /* Sources */ = { 383 | isa = PBXSourcesBuildPhase; 384 | buildActionMask = 2147483647; 385 | files = ( 386 | ); 387 | runOnlyForDeploymentPostprocessing = 0; 388 | }; 389 | 6125453C1C2D8AB5006281D2 /* Sources */ = { 390 | isa = PBXSourcesBuildPhase; 391 | buildActionMask = 2147483647; 392 | files = ( 393 | ); 394 | runOnlyForDeploymentPostprocessing = 0; 395 | }; 396 | /* End PBXSourcesBuildPhase section */ 397 | 398 | /* Begin PBXTargetDependency section */ 399 | 612545371C2D8AB5006281D2 /* PBXTargetDependency */ = { 400 | isa = PBXTargetDependency; 401 | target = 6125451D1C2D8AB5006281D2 /* VoiceMemo */; 402 | targetProxy = 612545361C2D8AB5006281D2 /* PBXContainerItemProxy */; 403 | }; 404 | 612545421C2D8AB5006281D2 /* PBXTargetDependency */ = { 405 | isa = PBXTargetDependency; 406 | target = 6125451D1C2D8AB5006281D2 /* VoiceMemo */; 407 | targetProxy = 612545411C2D8AB5006281D2 /* PBXContainerItemProxy */; 408 | }; 409 | /* End PBXTargetDependency section */ 410 | 411 | /* Begin PBXVariantGroup section */ 412 | 612545251C2D8AB5006281D2 /* Main.storyboard */ = { 413 | isa = PBXVariantGroup; 414 | children = ( 415 | 612545261C2D8AB5006281D2 /* Base */, 416 | ); 417 | name = Main.storyboard; 418 | path = .; 419 | sourceTree = ""; 420 | }; 421 | 6125452D1C2D8AB5006281D2 /* LaunchScreen.storyboard */ = { 422 | isa = PBXVariantGroup; 423 | children = ( 424 | 6125452E1C2D8AB5006281D2 /* Base */, 425 | ); 426 | name = LaunchScreen.storyboard; 427 | path = .; 428 | sourceTree = ""; 429 | }; 430 | /* End PBXVariantGroup section */ 431 | 432 | /* Begin XCBuildConfiguration section */ 433 | 612545471C2D8AB5006281D2 /* Debug */ = { 434 | isa = XCBuildConfiguration; 435 | buildSettings = { 436 | ALWAYS_SEARCH_USER_PATHS = NO; 437 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 438 | CLANG_CXX_LIBRARY = "libc++"; 439 | CLANG_ENABLE_MODULES = YES; 440 | CLANG_ENABLE_OBJC_ARC = YES; 441 | CLANG_WARN_BOOL_CONVERSION = YES; 442 | CLANG_WARN_CONSTANT_CONVERSION = YES; 443 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 444 | CLANG_WARN_EMPTY_BODY = YES; 445 | CLANG_WARN_ENUM_CONVERSION = YES; 446 | CLANG_WARN_INT_CONVERSION = YES; 447 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 448 | CLANG_WARN_UNREACHABLE_CODE = YES; 449 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 450 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 451 | COPY_PHASE_STRIP = NO; 452 | DEBUG_INFORMATION_FORMAT = dwarf; 453 | ENABLE_STRICT_OBJC_MSGSEND = YES; 454 | ENABLE_TESTABILITY = YES; 455 | GCC_C_LANGUAGE_STANDARD = gnu99; 456 | GCC_DYNAMIC_NO_PIC = NO; 457 | GCC_NO_COMMON_BLOCKS = YES; 458 | GCC_OPTIMIZATION_LEVEL = 0; 459 | GCC_PREPROCESSOR_DEFINITIONS = ( 460 | "DEBUG=1", 461 | "$(inherited)", 462 | ); 463 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 464 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 465 | GCC_WARN_UNDECLARED_SELECTOR = YES; 466 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 467 | GCC_WARN_UNUSED_FUNCTION = YES; 468 | GCC_WARN_UNUSED_VARIABLE = YES; 469 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 470 | MTL_ENABLE_DEBUG_INFO = YES; 471 | ONLY_ACTIVE_ARCH = YES; 472 | SDKROOT = iphoneos; 473 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 474 | }; 475 | name = Debug; 476 | }; 477 | 612545481C2D8AB5006281D2 /* Release */ = { 478 | isa = XCBuildConfiguration; 479 | buildSettings = { 480 | ALWAYS_SEARCH_USER_PATHS = NO; 481 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 482 | CLANG_CXX_LIBRARY = "libc++"; 483 | CLANG_ENABLE_MODULES = YES; 484 | CLANG_ENABLE_OBJC_ARC = YES; 485 | CLANG_WARN_BOOL_CONVERSION = YES; 486 | CLANG_WARN_CONSTANT_CONVERSION = YES; 487 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 488 | CLANG_WARN_EMPTY_BODY = YES; 489 | CLANG_WARN_ENUM_CONVERSION = YES; 490 | CLANG_WARN_INT_CONVERSION = YES; 491 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 492 | CLANG_WARN_UNREACHABLE_CODE = YES; 493 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 494 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 495 | COPY_PHASE_STRIP = NO; 496 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 497 | ENABLE_NS_ASSERTIONS = NO; 498 | ENABLE_STRICT_OBJC_MSGSEND = YES; 499 | GCC_C_LANGUAGE_STANDARD = gnu99; 500 | GCC_NO_COMMON_BLOCKS = YES; 501 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 502 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 503 | GCC_WARN_UNDECLARED_SELECTOR = YES; 504 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 505 | GCC_WARN_UNUSED_FUNCTION = YES; 506 | GCC_WARN_UNUSED_VARIABLE = YES; 507 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 508 | MTL_ENABLE_DEBUG_INFO = NO; 509 | SDKROOT = iphoneos; 510 | VALIDATE_PRODUCT = YES; 511 | }; 512 | name = Release; 513 | }; 514 | 6125454A1C2D8AB5006281D2 /* Debug */ = { 515 | isa = XCBuildConfiguration; 516 | buildSettings = { 517 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 518 | INFOPLIST_FILE = "VoiceMemo/Supporting Files/Info.plist"; 519 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 520 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 521 | PRODUCT_BUNDLE_IDENTIFIER = com.redesignapp.ios.demo.VoiceMemo; 522 | PRODUCT_NAME = "$(TARGET_NAME)"; 523 | }; 524 | name = Debug; 525 | }; 526 | 6125454B1C2D8AB5006281D2 /* Release */ = { 527 | isa = XCBuildConfiguration; 528 | buildSettings = { 529 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 530 | INFOPLIST_FILE = "VoiceMemo/Supporting Files/Info.plist"; 531 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 532 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 533 | PRODUCT_BUNDLE_IDENTIFIER = com.redesignapp.ios.demo.VoiceMemo; 534 | PRODUCT_NAME = "$(TARGET_NAME)"; 535 | }; 536 | name = Release; 537 | }; 538 | 6125454D1C2D8AB5006281D2 /* Debug */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | BUNDLE_LOADER = "$(TEST_HOST)"; 542 | INFOPLIST_FILE = "VoiceMemoTests/Supporting Files/Info.plist"; 543 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 544 | PRODUCT_BUNDLE_IDENTIFIER = com.redesignapp.ios.demo.VoiceMemoTests; 545 | PRODUCT_NAME = "$(TARGET_NAME)"; 546 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VoiceMemo.app/VoiceMemo"; 547 | }; 548 | name = Debug; 549 | }; 550 | 6125454E1C2D8AB5006281D2 /* Release */ = { 551 | isa = XCBuildConfiguration; 552 | buildSettings = { 553 | BUNDLE_LOADER = "$(TEST_HOST)"; 554 | INFOPLIST_FILE = "VoiceMemoTests/Supporting Files/Info.plist"; 555 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 556 | PRODUCT_BUNDLE_IDENTIFIER = com.redesignapp.ios.demo.VoiceMemoTests; 557 | PRODUCT_NAME = "$(TARGET_NAME)"; 558 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VoiceMemo.app/VoiceMemo"; 559 | }; 560 | name = Release; 561 | }; 562 | 612545501C2D8AB5006281D2 /* Debug */ = { 563 | isa = XCBuildConfiguration; 564 | buildSettings = { 565 | INFOPLIST_FILE = "VoiceMemoUITests/Supporting Files/Info.plist"; 566 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 567 | PRODUCT_BUNDLE_IDENTIFIER = com.redesignapp.ios.demo.VoiceMemoUITests; 568 | PRODUCT_NAME = "$(TARGET_NAME)"; 569 | TEST_TARGET_NAME = VoiceMemo; 570 | USES_XCTRUNNER = YES; 571 | }; 572 | name = Debug; 573 | }; 574 | 612545511C2D8AB5006281D2 /* Release */ = { 575 | isa = XCBuildConfiguration; 576 | buildSettings = { 577 | INFOPLIST_FILE = "VoiceMemoUITests/Supporting Files/Info.plist"; 578 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 579 | PRODUCT_BUNDLE_IDENTIFIER = com.redesignapp.ios.demo.VoiceMemoUITests; 580 | PRODUCT_NAME = "$(TARGET_NAME)"; 581 | TEST_TARGET_NAME = VoiceMemo; 582 | USES_XCTRUNNER = YES; 583 | }; 584 | name = Release; 585 | }; 586 | /* End XCBuildConfiguration section */ 587 | 588 | /* Begin XCConfigurationList section */ 589 | 612545191C2D8AB5006281D2 /* Build configuration list for PBXProject "VoiceMemo" */ = { 590 | isa = XCConfigurationList; 591 | buildConfigurations = ( 592 | 612545471C2D8AB5006281D2 /* Debug */, 593 | 612545481C2D8AB5006281D2 /* Release */, 594 | ); 595 | defaultConfigurationIsVisible = 0; 596 | defaultConfigurationName = Release; 597 | }; 598 | 612545491C2D8AB5006281D2 /* Build configuration list for PBXNativeTarget "VoiceMemo" */ = { 599 | isa = XCConfigurationList; 600 | buildConfigurations = ( 601 | 6125454A1C2D8AB5006281D2 /* Debug */, 602 | 6125454B1C2D8AB5006281D2 /* Release */, 603 | ); 604 | defaultConfigurationIsVisible = 0; 605 | defaultConfigurationName = Release; 606 | }; 607 | 6125454C1C2D8AB5006281D2 /* Build configuration list for PBXNativeTarget "VoiceMemoTests" */ = { 608 | isa = XCConfigurationList; 609 | buildConfigurations = ( 610 | 6125454D1C2D8AB5006281D2 /* Debug */, 611 | 6125454E1C2D8AB5006281D2 /* Release */, 612 | ); 613 | defaultConfigurationIsVisible = 0; 614 | defaultConfigurationName = Release; 615 | }; 616 | 6125454F1C2D8AB5006281D2 /* Build configuration list for PBXNativeTarget "VoiceMemoUITests" */ = { 617 | isa = XCConfigurationList; 618 | buildConfigurations = ( 619 | 612545501C2D8AB5006281D2 /* Debug */, 620 | 612545511C2D8AB5006281D2 /* Release */, 621 | ); 622 | defaultConfigurationIsVisible = 0; 623 | defaultConfigurationName = Release; 624 | }; 625 | /* End XCConfigurationList section */ 626 | 627 | /* Begin XCVersionGroup section */ 628 | 612545281C2D8AB5006281D2 /* VoiceMemo.xcdatamodeld */ = { 629 | isa = XCVersionGroup; 630 | children = ( 631 | 612545291C2D8AB5006281D2 /* VoiceMemo.xcdatamodel */, 632 | ); 633 | currentVersion = 612545291C2D8AB5006281D2 /* VoiceMemo.xcdatamodel */; 634 | path = VoiceMemo.xcdatamodeld; 635 | sourceTree = ""; 636 | versionGroupType = wrapper.xcdatamodel; 637 | }; 638 | /* End XCVersionGroup section */ 639 | }; 640 | rootObject = 612545161C2D8AB5006281D2 /* Project object */; 641 | } 642 | --------------------------------------------------------------------------------