diff --git a/Experiences/Default-568h@2x.png b/Experiences/Default-568h@2x.png new file mode 100644 index 00000000..0891b7aa Binary files /dev/null and b/Experiences/Default-568h@2x.png differ diff --git a/Experiences/Experiences.xcodeproj/project.pbxproj b/Experiences/Experiences.xcodeproj/project.pbxproj new file mode 100644 index 00000000..a8ba0f12 --- /dev/null +++ b/Experiences/Experiences.xcodeproj/project.pbxproj @@ -0,0 +1,421 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 44C4131E2558E65D00FA8165 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C4131D2558E65D00FA8165 /* AppDelegate.swift */; }; + 44C413202558E65D00FA8165 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C4131F2558E65D00FA8165 /* SceneDelegate.swift */; }; + 44C413252558E65D00FA8165 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 44C413232558E65D00FA8165 /* Main.storyboard */; }; + 44C413272558E66100FA8165 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 44C413262558E66100FA8165 /* Assets.xcassets */; }; + 44C4132A2558E66100FA8165 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 44C413282558E66100FA8165 /* LaunchScreen.storyboard */; }; + 44D7DC002558ECCF00BAF797 /* MapKitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D7DBFF2558ECCF00BAF797 /* MapKitViewController.swift */; }; + 44D7DC022558ED0A00BAF797 /* NewExperienceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D7DC012558ED0A00BAF797 /* NewExperienceViewController.swift */; }; + 44D7DC042558EDA200BAF797 /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D7DC032558EDA200BAF797 /* CameraViewController.swift */; }; + 44D7DC082558F3B200BAF797 /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D7DC072558F3B200BAF797 /* CameraPreview.swift */; }; + 44D7DC0A2558F45A00BAF797 /* Experience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D7DC092558F45A00BAF797 /* Experience.swift */; }; + 44D7DC0C2558F46B00BAF797 /* ExperienceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D7DC0B2558F46B00BAF797 /* ExperienceController.swift */; }; + 44D7DC0E2558F52E00BAF797 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 44D7DC0D2558F52D00BAF797 /* Default-568h@2x.png */; }; + 44D7DC102558F69700BAF797 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D7DC0F2558F69700BAF797 /* VideoPlayer.swift */; }; + 44D7DC122559065300BAF797 /* ExperienceDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D7DC112559065200BAF797 /* ExperienceDetailView.swift */; }; + 44FD7A2D255A5BFD00C22EF3 /* piano copy.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 44FD7A2C255A5BFC00C22EF3 /* piano copy.mp3 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 44C4131A2558E65D00FA8165 /* Experiences.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Experiences.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 44C4131D2558E65D00FA8165 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 44C4131F2558E65D00FA8165 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 44C413242558E65D00FA8165 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 44C413262558E66100FA8165 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 44C413292558E66100FA8165 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 44C4132B2558E66100FA8165 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 44D7DBFF2558ECCF00BAF797 /* MapKitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapKitViewController.swift; sourceTree = ""; }; + 44D7DC012558ED0A00BAF797 /* NewExperienceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewExperienceViewController.swift; sourceTree = ""; }; + 44D7DC032558EDA200BAF797 /* CameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; + 44D7DC072558F3B200BAF797 /* CameraPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = ""; }; + 44D7DC092558F45A00BAF797 /* Experience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Experience.swift; sourceTree = ""; }; + 44D7DC0B2558F46B00BAF797 /* ExperienceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceController.swift; sourceTree = ""; }; + 44D7DC0D2558F52D00BAF797 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 44D7DC0F2558F69700BAF797 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; + 44D7DC112559065200BAF797 /* ExperienceDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceDetailView.swift; sourceTree = ""; }; + 44FD7A2C255A5BFC00C22EF3 /* piano copy.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = "piano copy.mp3"; path = "../../../ios-guided-project-audio-recorder-starter/SimpleAudioRecorder/piano copy.mp3"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 44C413172558E65D00FA8165 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 44C413112558E65D00FA8165 = { + isa = PBXGroup; + children = ( + 44D7DC0D2558F52D00BAF797 /* Default-568h@2x.png */, + 44C4131C2558E65D00FA8165 /* Experiences */, + 44C4131B2558E65D00FA8165 /* Products */, + ); + sourceTree = ""; + }; + 44C4131B2558E65D00FA8165 /* Products */ = { + isa = PBXGroup; + children = ( + 44C4131A2558E65D00FA8165 /* Experiences.app */, + ); + name = Products; + sourceTree = ""; + }; + 44C4131C2558E65D00FA8165 /* Experiences */ = { + isa = PBXGroup; + children = ( + 44D7DC062558F3A300BAF797 /* Controllers */, + 44D7DC052558F39A00BAF797 /* Model */, + 44C413262558E66100FA8165 /* Assets.xcassets */, + 44C4132B2558E66100FA8165 /* Info.plist */, + 44FD7A2C255A5BFC00C22EF3 /* piano copy.mp3 */, + 44C413312558E6CB00FA8165 /* Scene */, + 44C413322558E6E400FA8165 /* Views */, + 44C413332558E6FB00FA8165 /* Storyboard */, + ); + path = Experiences; + sourceTree = ""; + }; + 44C413312558E6CB00FA8165 /* Scene */ = { + isa = PBXGroup; + children = ( + 44C4131F2558E65D00FA8165 /* SceneDelegate.swift */, + 44C4131D2558E65D00FA8165 /* AppDelegate.swift */, + ); + path = Scene; + sourceTree = ""; + }; + 44C413322558E6E400FA8165 /* Views */ = { + isa = PBXGroup; + children = ( + 44D7DBFF2558ECCF00BAF797 /* MapKitViewController.swift */, + 44D7DC012558ED0A00BAF797 /* NewExperienceViewController.swift */, + 44D7DC032558EDA200BAF797 /* CameraViewController.swift */, + 44D7DC112559065200BAF797 /* ExperienceDetailView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 44C413332558E6FB00FA8165 /* Storyboard */ = { + isa = PBXGroup; + children = ( + 44C413232558E65D00FA8165 /* Main.storyboard */, + 44C413282558E66100FA8165 /* LaunchScreen.storyboard */, + ); + path = Storyboard; + sourceTree = ""; + }; + 44D7DC052558F39A00BAF797 /* Model */ = { + isa = PBXGroup; + children = ( + 44D7DC092558F45A00BAF797 /* Experience.swift */, + ); + path = Model; + sourceTree = ""; + }; + 44D7DC062558F3A300BAF797 /* Controllers */ = { + isa = PBXGroup; + children = ( + 44D7DC072558F3B200BAF797 /* CameraPreview.swift */, + 44D7DC0B2558F46B00BAF797 /* ExperienceController.swift */, + 44D7DC0F2558F69700BAF797 /* VideoPlayer.swift */, + ); + path = Controllers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 44C413192558E65D00FA8165 /* Experiences */ = { + isa = PBXNativeTarget; + buildConfigurationList = 44C4132E2558E66100FA8165 /* Build configuration list for PBXNativeTarget "Experiences" */; + buildPhases = ( + 44C413162558E65D00FA8165 /* Sources */, + 44C413172558E65D00FA8165 /* Frameworks */, + 44C413182558E65D00FA8165 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Experiences; + productName = Experiences; + productReference = 44C4131A2558E65D00FA8165 /* Experiences.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 44C413122558E65D00FA8165 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1170; + LastUpgradeCheck = 1170; + ORGANIZATIONNAME = "Craig Belinfante"; + TargetAttributes = { + 44C413192558E65D00FA8165 = { + CreatedOnToolsVersion = 11.7; + }; + }; + }; + buildConfigurationList = 44C413152558E65D00FA8165 /* Build configuration list for PBXProject "Experiences" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 44C413112558E65D00FA8165; + productRefGroup = 44C4131B2558E65D00FA8165 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 44C413192558E65D00FA8165 /* Experiences */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 44C413182558E65D00FA8165 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 44C4132A2558E66100FA8165 /* LaunchScreen.storyboard in Resources */, + 44D7DC0E2558F52E00BAF797 /* Default-568h@2x.png in Resources */, + 44C413272558E66100FA8165 /* Assets.xcassets in Resources */, + 44C413252558E65D00FA8165 /* Main.storyboard in Resources */, + 44FD7A2D255A5BFD00C22EF3 /* piano copy.mp3 in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 44C413162558E65D00FA8165 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 44D7DC102558F69700BAF797 /* VideoPlayer.swift in Sources */, + 44D7DC122559065300BAF797 /* ExperienceDetailView.swift in Sources */, + 44D7DC002558ECCF00BAF797 /* MapKitViewController.swift in Sources */, + 44D7DC0C2558F46B00BAF797 /* ExperienceController.swift in Sources */, + 44D7DC042558EDA200BAF797 /* CameraViewController.swift in Sources */, + 44C4131E2558E65D00FA8165 /* AppDelegate.swift in Sources */, + 44D7DC0A2558F45A00BAF797 /* Experience.swift in Sources */, + 44D7DC082558F3B200BAF797 /* CameraPreview.swift in Sources */, + 44C413202558E65D00FA8165 /* SceneDelegate.swift in Sources */, + 44D7DC022558ED0A00BAF797 /* NewExperienceViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 44C413232558E65D00FA8165 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 44C413242558E65D00FA8165 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 44C413282558E66100FA8165 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 44C413292558E66100FA8165 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 44C4132C2558E66100FA8165 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.7; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 44C4132D2558E66100FA8165 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.7; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 44C4132F2558E66100FA8165 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = HQ2TDS9ZTM; + INFOPLIST_FILE = Experiences/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.7; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.lambdaschool.ExperiencesCraigBelinfante; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 44C413302558E66100FA8165 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = HQ2TDS9ZTM; + INFOPLIST_FILE = Experiences/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.7; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.lambdaschool.ExperiencesCraigBelinfante; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 44C413152558E65D00FA8165 /* Build configuration list for PBXProject "Experiences" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 44C4132C2558E66100FA8165 /* Debug */, + 44C4132D2558E66100FA8165 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 44C4132E2558E66100FA8165 /* Build configuration list for PBXNativeTarget "Experiences" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 44C4132F2558E66100FA8165 /* Debug */, + 44C413302558E66100FA8165 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 44C413122558E65D00FA8165 /* Project object */; +} diff --git a/Experiences/Experiences/Assets.xcassets/AppIcon.appiconset/Contents.json b/Experiences/Experiences/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/Experiences/Experiences/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Experiences/Experiences/Assets.xcassets/Contents.json b/Experiences/Experiences/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Experiences/Experiences/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Experiences/Experiences/Assets.xcassets/Record.imageset/Contents.json b/Experiences/Experiences/Assets.xcassets/Record.imageset/Contents.json new file mode 100644 index 00000000..39b0bb32 --- /dev/null +++ b/Experiences/Experiences/Assets.xcassets/Record.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Record copy.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Experiences/Experiences/Assets.xcassets/Record.imageset/Record copy.pdf b/Experiences/Experiences/Assets.xcassets/Record.imageset/Record copy.pdf new file mode 100644 index 00000000..f4b07935 Binary files /dev/null and b/Experiences/Experiences/Assets.xcassets/Record.imageset/Record copy.pdf differ diff --git a/Experiences/Experiences/Assets.xcassets/Stop.imageset/Contents.json b/Experiences/Experiences/Assets.xcassets/Stop.imageset/Contents.json new file mode 100644 index 00000000..5aafc6d9 --- /dev/null +++ b/Experiences/Experiences/Assets.xcassets/Stop.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Stop copy.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Experiences/Experiences/Assets.xcassets/Stop.imageset/Stop copy.pdf b/Experiences/Experiences/Assets.xcassets/Stop.imageset/Stop copy.pdf new file mode 100644 index 00000000..7aee81c0 Binary files /dev/null and b/Experiences/Experiences/Assets.xcassets/Stop.imageset/Stop copy.pdf differ diff --git a/Experiences/Experiences/Controllers/CameraPreview.swift b/Experiences/Experiences/Controllers/CameraPreview.swift new file mode 100644 index 00000000..9e5c2c67 --- /dev/null +++ b/Experiences/Experiences/Controllers/CameraPreview.swift @@ -0,0 +1,27 @@ +// +// CameraPreview.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import Foundation +import AVFoundation +import MapKit + +class CameraPreviewView: UIView { + + override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } + + var videoPlayerLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } + + var session: AVCaptureSession? { + get { return videoPlayerLayer.session } + set { videoPlayerLayer.session = newValue } + } +} diff --git a/Experiences/Experiences/Controllers/ExperienceController.swift b/Experiences/Experiences/Controllers/ExperienceController.swift new file mode 100644 index 00000000..3bc8ac38 --- /dev/null +++ b/Experiences/Experiences/Controllers/ExperienceController.swift @@ -0,0 +1,19 @@ +// +// ExperienceController.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import MapKit + +class ExperienceController { + private (set) var experience: [Experience] = [] + + var currentLocation: CLLocationCoordinate2D? + + func addNewExperience(newExperience: Experience) { + experience.append(newExperience) + } +} diff --git a/Experiences/Experiences/Controllers/VideoPlayer.swift b/Experiences/Experiences/Controllers/VideoPlayer.swift new file mode 100644 index 00000000..1a35c840 --- /dev/null +++ b/Experiences/Experiences/Controllers/VideoPlayer.swift @@ -0,0 +1,27 @@ +// +// VideoPlayer.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import Foundation +import AVFoundation +import AVKit + +class VideoPlayerView: UIView { + + override class var layerClass: AnyClass { + return AVPlayerLayer.self + } + + var videoPlayerLayer: AVPlayerLayer { + return layer as! AVPlayerLayer + } + + var player: AVPlayer? { + get { return videoPlayerLayer.player } + set { videoPlayerLayer.player = newValue } + } +} diff --git a/Experiences/Experiences/Info.plist b/Experiences/Experiences/Info.plist new file mode 100644 index 00000000..4f0c44bd --- /dev/null +++ b/Experiences/Experiences/Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSCameraUsageDescription + $(PRODUCT_NAME) needs the camera to record videos. + NSMicrophoneUsageDescription + $(PRODUCT_NAME) needs the microphone to record audio. + NSLocationWhenInUseUsageDescription + $(PRODUCT_NAME) needs the location to create an experience. + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Experiences/Experiences/Model/Experience.swift b/Experiences/Experiences/Model/Experience.swift new file mode 100644 index 00000000..e9b36739 --- /dev/null +++ b/Experiences/Experiences/Model/Experience.swift @@ -0,0 +1,27 @@ +// +// Experience.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import Foundation +import MapKit +import AVFoundation + +class Experience: NSObject, MKAnnotation { + var title: String? + var coordinate: CLLocationCoordinate2D + + init(title: String, coordinate: CLLocationCoordinate2D) { + self.title = title + self.coordinate = coordinate + audio = title + video = title + } + + var audio: String? + var video: String? + var image: UIImage? +} diff --git a/Experiences/Experiences/Scene/AppDelegate.swift b/Experiences/Experiences/Scene/AppDelegate.swift new file mode 100644 index 00000000..0c3ab802 --- /dev/null +++ b/Experiences/Experiences/Scene/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/Experiences/Experiences/Scene/SceneDelegate.swift b/Experiences/Experiences/Scene/SceneDelegate.swift new file mode 100644 index 00000000..31faae9d --- /dev/null +++ b/Experiences/Experiences/Scene/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/Experiences/Experiences/Storyboard/Base.lproj/LaunchScreen.storyboard b/Experiences/Experiences/Storyboard/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Experiences/Experiences/Storyboard/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experiences/Experiences/Storyboard/Base.lproj/Main.storyboard b/Experiences/Experiences/Storyboard/Base.lproj/Main.storyboard new file mode 100644 index 00000000..680b13dd --- /dev/null +++ b/Experiences/Experiences/Storyboard/Base.lproj/Main.storyboard @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experiences/Experiences/Views/CameraViewController.swift b/Experiences/Experiences/Views/CameraViewController.swift new file mode 100644 index 00000000..a177a6cd --- /dev/null +++ b/Experiences/Experiences/Views/CameraViewController.swift @@ -0,0 +1,185 @@ +// +// CameraViewController.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import UIKit +import AVFoundation +import AVKit + +class CameraViewController: UIViewController { + + var experienceController: ExperienceController? + var fileTitle: String? + var audioRecorder: AVAudioRecorder? + + lazy private var captureSession = AVCaptureSession() + lazy private var fileOutput = AVCaptureMovieFileOutput() + + lazy private var player = AVPlayer() + private var playerView: VideoPlayerView! + + @IBOutlet weak var recordButton: UIButton! + @IBOutlet var cameraView: CameraPreviewView! + + + override func viewDidLoad() { + super.viewDidLoad() + + cameraView.videoPlayerLayer.videoGravity = .resizeAspectFill + setupCamera() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + captureSession.startRunning() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + captureSession.stopRunning() + } + + func updateViews() { + if fileOutput.isRecording { + recordButton.setImage(UIImage(named: "Stop"), for: .normal) + } else { + recordButton.setImage(UIImage(named: "Record"), for: .normal) + } + } + + func playMovie(url: URL) { + player.replaceCurrentItem(with: AVPlayerItem(url: url)) + + if playerView == nil { + playerView = VideoPlayerView() + playerView.player = player + + var topRect = view.bounds + topRect.size.width /= 4 + topRect.size.height /= 4 + topRect.origin.y = view.layoutMargins.top + topRect.origin.x = view.bounds.size.width - topRect.size.width + + playerView.frame = topRect + view.addSubview(playerView) + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(playRecording(_:))) + playerView.addGestureRecognizer(tapGesture) + } + + player.play() + + } + + @IBAction func playRecording(_ sender: UITapGestureRecognizer) { + guard sender.state == .ended else { return } + + let playerVC = AVPlayerViewController() + playerVC.player = player + + self.present(playerVC, animated: true, completion: nil) + } + + @IBAction func recordButtonPressed(_ sender: UIButton) { + if fileOutput.isRecording { + fileOutput.stopRecording() + } else { + fileOutput.startRecording(to: newRecordingURL(), recordingDelegate: self) + } + } + + private func setupCamera() { + let camera = bestCamera + let microphone = bestMicrophone + + captureSession.beginConfiguration() + + guard let cameraInput = try? AVCaptureDeviceInput(device: camera) else { + preconditionFailure("Can't create an input from the camera.") + } + + guard let microphoneInput = try? AVCaptureDeviceInput(device: microphone) else { + preconditionFailure("Can't create an input from the microphone.") + } + + guard captureSession.canAddInput(cameraInput) else { + preconditionFailure("This session can't handle this type of camera input") + } + captureSession.addInput(cameraInput) + + guard captureSession.canAddInput(microphoneInput) else { + preconditionFailure("This session can't handle this type of microphone input") + } + captureSession.addInput(microphoneInput) + + if captureSession.canSetSessionPreset(.hd1920x1080) { + captureSession.sessionPreset = .hd1920x1080 + } + + guard captureSession.canAddOutput(fileOutput) else { + preconditionFailure("This session can't handle this type of file output") + } + captureSession.addOutput(fileOutput) + + captureSession.commitConfiguration() + + cameraView.session = captureSession + } + + private var bestCamera: AVCaptureDevice { + if let device = AVCaptureDevice.default(.builtInUltraWideCamera, for: .video, position: .back) { + return device + } else if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { + return device + } + + preconditionFailure("No cameras on device match our specifications.") + } + + private var bestMicrophone: AVCaptureDevice { + if let device = AVCaptureDevice.default(for: .audio) { + return device + } + + preconditionFailure("No microphones on device match our specifications") + } + + private func newRecordingURL() -> URL { + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime] + + let name = formatter.string(from: Date()) + let fileURL = documentsDirectory.appendingPathComponent(name).appendingPathExtension("mov") + return fileURL + } + + +} + +extension CameraViewController: AVCaptureFileOutputRecordingDelegate { + + func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) { + DispatchQueue.main.async { + self.updateViews() + } + } + + func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { + guard let currentLoaction = experienceController?.currentLocation, let fileTitle = fileTitle else {return} + + let experience = Experience(title: fileTitle, coordinate: currentLoaction) + experience.video = String(outputFileURL.absoluteString) + experienceController?.addNewExperience(newExperience: experience) + dismiss(animated: true, completion: nil) + + playMovie(url: outputFileURL) + updateViews() + } + +} diff --git a/Experiences/Experiences/Views/ExperienceDetailView.swift b/Experiences/Experiences/Views/ExperienceDetailView.swift new file mode 100644 index 00000000..a1733337 --- /dev/null +++ b/Experiences/Experiences/Views/ExperienceDetailView.swift @@ -0,0 +1,14 @@ +// +// ExperienceDetailView.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import Foundation +import UIKit + +class ExperienceDetailView: UIView { + var experience: Experience? +} diff --git a/Experiences/Experiences/Views/MapKitViewController.swift b/Experiences/Experiences/Views/MapKitViewController.swift new file mode 100644 index 00000000..410108a7 --- /dev/null +++ b/Experiences/Experiences/Views/MapKitViewController.swift @@ -0,0 +1,140 @@ +// +// MapKitViewController.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import UIKit +import MapKit +import AVFoundation + +enum ReuseIdentifier { + static let mapAnnotation = "ExperienceAnnotationView" +} + +class MapKitViewController: UIViewController { + + var videoPlayer: AVPlayer? + var recordingURL: URL? + var audioPlayer: AVAudioPlayer? { + didSet { + guard let audioPlayer = audioPlayer else { return } + + audioPlayer.delegate = self + audioPlayer.isMeteringEnabled = true + updateViews() + } + } + var audioRecorder: AVAudioRecorder? + + let experienceController = ExperienceController() + let locationManager = CLLocationManager() + + @IBOutlet weak var mapView: MKMapView! + @IBOutlet weak var addPost: UIButton! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + mapView.reloadInputViews() + mapView.delegate = self + mapView?.addAnnotations(experienceController.experience) + mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: ReuseIdentifier.mapAnnotation) + } + + override func viewDidLoad() { + super.viewDidLoad() + requestLocationAccess() + } + + func updateViews() { + + } + + func playMovie(url: URL) -> AVPlayerLayer{ + videoPlayer = AVPlayer(url: url) + let playerLayer = AVPlayerLayer(player: videoPlayer) + playerLayer.frame = .zero + return playerLayer + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "NewExperienceController" { + guard let detailVC = segue.destination as? NewExperienceViewController else { return } + experienceController.currentLocation = locationManager.location?.coordinate + detailVC.experienceController = experienceController + } + } + +} + +extension MapKitViewController: MKMapViewDelegate { + func requestLocationAccess() { + let status = CLLocationManager.authorizationStatus() + + switch status { + case .authorizedAlways, .authorizedWhenInUse: + return + case .denied, .restricted: + print("location access denied") + default: + locationManager.requestWhenInUseAuthorization() + } + } + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + + guard let experience = annotation as? Experience else {return nil} + + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: ReuseIdentifier.mapAnnotation, for: annotation) as! MKMarkerAnnotationView + + annotationView.glyphImage = #imageLiteral(resourceName: "QuakeIcon") + annotationView.canShowCallout = true + + let detailView = ExperienceDetailView(frame: .zero) + detailView.experience = experience + annotationView.detailCalloutAccessoryView = detailView + + return annotationView + } + + func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + guard let fileTitle = view.annotation?.title else { return } + let _ = String(fileTitle!) + let _ = audioPlayer?.duration + audioPlayer?.play() + } +} + +extension MapKitViewController: AVAudioPlayerDelegate { + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + updateViews() + } + + func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + if let error = error { + print("Audio Player Error: \(error)") + } + } + +} + +extension MapKitViewController: AVAudioRecorderDelegate { + + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if let recordingURL = recordingURL { + audioPlayer = try? AVAudioPlayer(contentsOf: recordingURL) + } + recordingURL = nil + } + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + if let error = error { + print("Audio Recorder Error: \(error)") + } + } + +} diff --git a/Experiences/Experiences/Views/NewExperienceViewController.swift b/Experiences/Experiences/Views/NewExperienceViewController.swift new file mode 100644 index 00000000..00ee6eb0 --- /dev/null +++ b/Experiences/Experiences/Views/NewExperienceViewController.swift @@ -0,0 +1,264 @@ +// +// NewExperienceViewController.swift +// Experiences +// +// Created by Craig Belinfante on 11/8/20. +// Copyright © 2020 Craig Belinfante. All rights reserved. +// + +import UIKit +import MapKit +import AVFoundation + +class NewExperienceViewController: UIViewController { + + var experienceController: ExperienceController? + var currentImage: UIImage? { + didSet { + prepareForRecord() + } + } + var recordingURL: URL? + var audioRecorder: AVAudioRecorder? + var audioPlayer: AVAudioPlayer? { + didSet { + guard let audioPlayer = audioPlayer else { return } + + audioPlayer.delegate = self + audioPlayer.isMeteringEnabled = true + updateViews() + } + } + var isRecording: Bool { + audioRecorder?.isRecording ?? false + } + var isPlaying: Bool { + audioPlayer?.isPlaying ?? false + } + + @IBOutlet weak var experienceTextField: UITextField! + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var recordButton: UIButton! + @IBOutlet weak var recordAudioButton: UIButton! + @IBOutlet weak var playButton: UIButton! + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + override func viewDidLoad() { + super.viewDidLoad() + loadAudio() + recordButton.isEnabled = false + } + + @IBAction func addPhoto(_ sender: UIButton) { + guard let title = experienceTextField.text, !title.isEmpty else {return} + guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { + fatalError("Cannot add photo") + } + + let picker = UIImagePickerController() + picker.sourceType = .photoLibrary + picker.allowsEditing = true + picker.delegate = self + present(picker, animated: true) + } + + @IBAction func recordButtonPressed(_ sender: UIButton) { + guard currentImage != nil else {return} + } + + + @IBAction func toggleAudioRecording(_ sender: UIButton) { + guard let title = experienceTextField.text, !title.isEmpty else {return} + // guard currentImage != nil else {return} + + if isRecording { + stopRecording() + } else { + requestPermissionOrStartRecording() + } + } + + @IBAction func playAudio(_ sender: UIButton) { + if isPlaying { + pause() + } else { + play() + } + } + + func updateViews() { + playButton.isEnabled = !isRecording + recordAudioButton.isEnabled = !isPlaying + + playButton.isSelected = isPlaying + recordAudioButton.isSelected = isRecording + } + + func play() { + do { + try prepareAudioSession() + audioPlayer?.play() + updateViews() + } catch { + print("Cannot play audio: \(error)") + } + } + + func pause() { + audioPlayer?.pause() + updateViews() + } + + func prepareForRecord() { + imageView.image = currentImage! + recordButton.isEnabled = true + recordAudioButton.isEnabled = true + } + + func prepareAudioSession() throws { + let session = AVAudioSession.sharedInstance() + try session.setCategory(.playAndRecord, options: [.defaultToSpeaker]) + try session.setActive(true, options: []) + } + + func createNewAudioRecordingURL() -> URL { + let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + + let name = ISO8601DateFormatter.string(from: Date(), timeZone: .current, formatOptions: .withInternetDateTime) + let file = documents.appendingPathComponent(name, isDirectory: false).appendingPathExtension("caf") + + print("recording URL: \(file)") + + return file + } + + func newVideoRecordingURL(fileTitle: String) -> URL { + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let url = documentsDirectory.appendingPathComponent(fileTitle).appendingPathExtension("mp3") + return url + } + + func startRecording() { + do { + try prepareAudioSession() + } catch { + print("Cannot record audio: \(error)") + return + } + + recordingURL = createNewAudioRecordingURL() + + let format = AVAudioFormat(standardFormatWithSampleRate: 44_100, channels: 1)! + + do { + audioRecorder = try AVAudioRecorder(url: recordingURL!, format: format) + audioRecorder?.delegate = self + audioRecorder?.isMeteringEnabled = true + audioRecorder?.record() + updateViews() + } catch { + preconditionFailure("The audio recorder could not be created with \(recordingURL!) and \(format)") + } + } + + func stopRecording() { + audioRecorder?.stop() + updateViews() + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "CameraViewController" { + guard let VC = segue.destination as? CameraViewController, + let fileTitle = experienceTextField.text, !fileTitle.isEmpty else {return} + + VC.fileTitle = fileTitle + VC.experienceController = experienceController + } + } + + func requestPermissionOrStartRecording() { + switch AVAudioSession.sharedInstance().recordPermission { + case .undetermined: + AVAudioSession.sharedInstance().requestRecordPermission { granted in + guard granted == true else { + print("We need microphone access") + return + } + + print("Recording permission has been granted!") + // NOTE: Invite the user to tap record again, since we just interrupted them, and they may not have been ready to record + } + case .denied: + print("Microphone access has been blocked.") + + let alertController = UIAlertController(title: "Microphone Access Denied", message: "Please allow this app to access your Microphone.", preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "Open Settings", style: .default) { (_) in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + }) + + alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil)) + + present(alertController, animated: true, completion: nil) + case .granted: + startRecording() + @unknown default: + break + } + } + + func loadAudio() { + let songURL = Bundle.main.url(forResource: "piano copy", withExtension: "mp3")! + + do { + audioPlayer = try AVAudioPlayer(contentsOf: songURL) + } catch { + preconditionFailure("Failure to load audio file: \(error)") + } + } + +} + +extension NewExperienceViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + guard let image = info[.originalImage] as? UIImage else { return } + imageView.image = image + dismiss(animated: true) + currentImage = image + } +} + +extension NewExperienceViewController: AVAudioPlayerDelegate { + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + updateViews() + } + + func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + if let error = error { + print("Audio Player Error: \(error)") + } + } + +} + +extension NewExperienceViewController: AVAudioRecorderDelegate { + + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if let recordingURL = recordingURL { + audioPlayer = try? AVAudioPlayer(contentsOf: recordingURL) + } + recordingURL = nil + } + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + if let error = error { + print("Audio Recorder Error: \(error)") + } + } + +}