diff --git a/Experiences/Experiences.xcodeproj/project.pbxproj b/Experiences/Experiences.xcodeproj/project.pbxproj new file mode 100644 index 00000000..31691397 --- /dev/null +++ b/Experiences/Experiences.xcodeproj/project.pbxproj @@ -0,0 +1,422 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 8604C2CF2561D016008B7D36 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C2CE2561D016008B7D36 /* AppDelegate.swift */; }; + 8604C2D12561D016008B7D36 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C2D02561D016008B7D36 /* SceneDelegate.swift */; }; + 8604C2D62561D016008B7D36 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8604C2D42561D016008B7D36 /* Main.storyboard */; }; + 8604C2D82561D017008B7D36 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8604C2D72561D017008B7D36 /* Assets.xcassets */; }; + 8604C2DB2561D017008B7D36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8604C2D92561D017008B7D36 /* LaunchScreen.storyboard */; }; + 8604C2EB2562168D008B7D36 /* ExperiencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C2EA2562168D008B7D36 /* ExperiencesVC.swift */; }; + 8604C2F425630AD0008B7D36 /* DetailsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C2F325630AD0008B7D36 /* DetailsVC.swift */; }; + 8604C2F725630AFD008B7D36 /* AddExperience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C2F625630AFD008B7D36 /* AddExperience.swift */; }; + 8604C2FB25630D55008B7D36 /* Experience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C2FA25630D55008B7D36 /* Experience.swift */; }; + 8604C2FF25630E70008B7D36 /* ExperienceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C2FE25630E70008B7D36 /* ExperienceController.swift */; }; + 8604C30925646E63008B7D36 /* UIViewController+Information.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C30825646E63008B7D36 /* UIViewController+Information.swift */; }; + 8604C30C25647093008B7D36 /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604C30B25647093008B7D36 /* UIImage+Scaling.swift */; }; + 860E3C6725BB644D00897B08 /* AddImageVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 860E3C6625BB644D00897B08 /* AddImageVC.swift */; }; + 860E3C7225BCCBD700897B08 /* AudioVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 860E3C7125BCCBD700897B08 /* AudioVC.swift */; }; + 860E3C7A25BE0C0500897B08 /* AudioVisualizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 860E3C7925BE0C0500897B08 /* AudioVisualizer.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8604C2CB2561D016008B7D36 /* Experiences.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Experiences.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8604C2CE2561D016008B7D36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 8604C2D02561D016008B7D36 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 8604C2D52561D016008B7D36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 8604C2D72561D017008B7D36 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8604C2DA2561D017008B7D36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 8604C2DC2561D017008B7D36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8604C2EA2562168D008B7D36 /* ExperiencesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiencesVC.swift; sourceTree = ""; }; + 8604C2F325630AD0008B7D36 /* DetailsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsVC.swift; sourceTree = ""; }; + 8604C2F625630AFD008B7D36 /* AddExperience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExperience.swift; sourceTree = ""; }; + 8604C2FA25630D55008B7D36 /* Experience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Experience.swift; sourceTree = ""; }; + 8604C2FE25630E70008B7D36 /* ExperienceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceController.swift; sourceTree = ""; }; + 8604C30825646E63008B7D36 /* UIViewController+Information.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Information.swift"; sourceTree = ""; }; + 8604C30B25647093008B7D36 /* UIImage+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Scaling.swift"; sourceTree = ""; }; + 860E3C6625BB644D00897B08 /* AddImageVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddImageVC.swift; sourceTree = ""; }; + 860E3C7125BCCBD700897B08 /* AudioVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVC.swift; sourceTree = ""; }; + 860E3C7925BE0C0500897B08 /* AudioVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVisualizer.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8604C2C82561D016008B7D36 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8604C2C22561D016008B7D36 = { + isa = PBXGroup; + children = ( + 8604C2CD2561D016008B7D36 /* Experiences */, + 8604C2CC2561D016008B7D36 /* Products */, + ); + sourceTree = ""; + }; + 8604C2CC2561D016008B7D36 /* Products */ = { + isa = PBXGroup; + children = ( + 8604C2CB2561D016008B7D36 /* Experiences.app */, + ); + name = Products; + sourceTree = ""; + }; + 8604C2CD2561D016008B7D36 /* Experiences */ = { + isa = PBXGroup; + children = ( + 8604C30725646E3F008B7D36 /* Helpers */, + 8604C2CE2561D016008B7D36 /* AppDelegate.swift */, + 8604C2D02561D016008B7D36 /* SceneDelegate.swift */, + 8604C2F925630D31008B7D36 /* Model */, + 8604C2FD25630E4A008B7D36 /* Model Controller */, + 8604C2EE256217E6008B7D36 /* Views */, + 8604C2E92562164F008B7D36 /* View Controller */, + 8604C2D42561D016008B7D36 /* Main.storyboard */, + 8604C2D72561D017008B7D36 /* Assets.xcassets */, + 8604C2D92561D017008B7D36 /* LaunchScreen.storyboard */, + 8604C2DC2561D017008B7D36 /* Info.plist */, + ); + path = Experiences; + sourceTree = ""; + }; + 8604C2E92562164F008B7D36 /* View Controller */ = { + isa = PBXGroup; + children = ( + 8604C2EA2562168D008B7D36 /* ExperiencesVC.swift */, + 8604C2F325630AD0008B7D36 /* DetailsVC.swift */, + 8604C2F625630AFD008B7D36 /* AddExperience.swift */, + 860E3C6625BB644D00897B08 /* AddImageVC.swift */, + 860E3C7125BCCBD700897B08 /* AudioVC.swift */, + ); + path = "View Controller"; + sourceTree = ""; + }; + 8604C2EE256217E6008B7D36 /* Views */ = { + isa = PBXGroup; + children = ( + ); + path = Views; + sourceTree = ""; + }; + 8604C2F925630D31008B7D36 /* Model */ = { + isa = PBXGroup; + children = ( + 8604C2FA25630D55008B7D36 /* Experience.swift */, + ); + path = Model; + sourceTree = ""; + }; + 8604C2FD25630E4A008B7D36 /* Model Controller */ = { + isa = PBXGroup; + children = ( + 8604C2FE25630E70008B7D36 /* ExperienceController.swift */, + ); + path = "Model Controller"; + sourceTree = ""; + }; + 8604C30725646E3F008B7D36 /* Helpers */ = { + isa = PBXGroup; + children = ( + 8604C30825646E63008B7D36 /* UIViewController+Information.swift */, + 8604C30B25647093008B7D36 /* UIImage+Scaling.swift */, + 860E3C7925BE0C0500897B08 /* AudioVisualizer.swift */, + ); + path = Helpers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8604C2CA2561D016008B7D36 /* Experiences */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8604C2DF2561D017008B7D36 /* Build configuration list for PBXNativeTarget "Experiences" */; + buildPhases = ( + 8604C2C72561D016008B7D36 /* Sources */, + 8604C2C82561D016008B7D36 /* Frameworks */, + 8604C2C92561D016008B7D36 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Experiences; + productName = Experiences; + productReference = 8604C2CB2561D016008B7D36 /* Experiences.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8604C2C32561D016008B7D36 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1210; + LastUpgradeCheck = 1210; + TargetAttributes = { + 8604C2CA2561D016008B7D36 = { + CreatedOnToolsVersion = 12.1; + }; + }; + }; + buildConfigurationList = 8604C2C62561D016008B7D36 /* Build configuration list for PBXProject "Experiences" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8604C2C22561D016008B7D36; + productRefGroup = 8604C2CC2561D016008B7D36 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8604C2CA2561D016008B7D36 /* Experiences */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8604C2C92561D016008B7D36 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8604C2DB2561D017008B7D36 /* LaunchScreen.storyboard in Resources */, + 8604C2D82561D017008B7D36 /* Assets.xcassets in Resources */, + 8604C2D62561D016008B7D36 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8604C2C72561D016008B7D36 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8604C30C25647093008B7D36 /* UIImage+Scaling.swift in Sources */, + 8604C2CF2561D016008B7D36 /* AppDelegate.swift in Sources */, + 8604C2FF25630E70008B7D36 /* ExperienceController.swift in Sources */, + 8604C2EB2562168D008B7D36 /* ExperiencesVC.swift in Sources */, + 860E3C7A25BE0C0500897B08 /* AudioVisualizer.swift in Sources */, + 8604C2D12561D016008B7D36 /* SceneDelegate.swift in Sources */, + 860E3C7225BCCBD700897B08 /* AudioVC.swift in Sources */, + 8604C2FB25630D55008B7D36 /* Experience.swift in Sources */, + 8604C2F725630AFD008B7D36 /* AddExperience.swift in Sources */, + 8604C2F425630AD0008B7D36 /* DetailsVC.swift in Sources */, + 860E3C6725BB644D00897B08 /* AddImageVC.swift in Sources */, + 8604C30925646E63008B7D36 /* UIViewController+Information.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 8604C2D42561D016008B7D36 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 8604C2D52561D016008B7D36 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 8604C2D92561D017008B7D36 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 8604C2DA2561D017008B7D36 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 8604C2DD2561D017008B7D36 /* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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 = 14.1; + 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; + }; + 8604C2DE2561D017008B7D36 /* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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 = 14.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8604C2E02561D017008B7D36 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Q2395UCK67; + INFOPLIST_FILE = Experiences/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.experiences.Experiences; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 8604C2E12561D017008B7D36 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Q2395UCK67; + INFOPLIST_FILE = Experiences/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.experiences.Experiences; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8604C2C62561D016008B7D36 /* Build configuration list for PBXProject "Experiences" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8604C2DD2561D017008B7D36 /* Debug */, + 8604C2DE2561D017008B7D36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8604C2DF2561D017008B7D36 /* Build configuration list for PBXNativeTarget "Experiences" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8604C2E02561D017008B7D36 /* Debug */, + 8604C2E12561D017008B7D36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8604C2C32561D016008B7D36 /* Project object */; +} diff --git a/Experiences/Experiences/AppDelegate.swift b/Experiences/Experiences/AppDelegate.swift new file mode 100644 index 00000000..f1b3ea82 --- /dev/null +++ b/Experiences/Experiences/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// Experiences +// +// Created by Norlan Tibanear on 11/15/20. +// + +import UIKit + +@main +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/Assets.xcassets/AccentColor.colorset/Contents.json b/Experiences/Experiences/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Experiences/Experiences/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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/PlaceholderImage.imageset/Contents.json b/Experiences/Experiences/Assets.xcassets/PlaceholderImage.imageset/Contents.json new file mode 100644 index 00000000..efa4bdfa --- /dev/null +++ b/Experiences/Experiences/Assets.xcassets/PlaceholderImage.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "PlaceholderImage.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Experiences/Experiences/Assets.xcassets/PlaceholderImage.imageset/PlaceholderImage.jpg b/Experiences/Experiences/Assets.xcassets/PlaceholderImage.imageset/PlaceholderImage.jpg new file mode 100644 index 00000000..d90319a5 Binary files /dev/null and b/Experiences/Experiences/Assets.xcassets/PlaceholderImage.imageset/PlaceholderImage.jpg differ diff --git a/Experiences/Experiences/Base.lproj/LaunchScreen.storyboard b/Experiences/Experiences/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Experiences/Experiences/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experiences/Experiences/Base.lproj/Main.storyboard b/Experiences/Experiences/Base.lproj/Main.storyboard new file mode 100644 index 00000000..606be223 --- /dev/null +++ b/Experiences/Experiences/Base.lproj/Main.storyboard @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experiences/Experiences/Helpers/AudioVisualizer.swift b/Experiences/Experiences/Helpers/AudioVisualizer.swift new file mode 100644 index 00000000..0ab7c274 --- /dev/null +++ b/Experiences/Experiences/Helpers/AudioVisualizer.swift @@ -0,0 +1,275 @@ +// +// AudioVisualizer.swift +// Experiences +// +// Created by Norlan Tibanear on 1/24/21. +// + +import UIKit + +@IBDesignable +public class AudioVisualizer: UIView { + + // MARK: IBInspectable Properties + + /// The width of a bar in points. + @IBInspectable public var barWidth: CGFloat = 10 { + didSet { + updateBars() + } + } + + /// The corner radius of a bar in points. If less than `0`, then it will default to half of the width of the bar. + @IBInspectable public var barCornerRadius: CGFloat = -1 { + didSet { + updateBars() + } + } + + /// The spacing between bars in points. + @IBInspectable public var barSpacing: CGFloat = 4 { + didSet { + updateBars() + } + } + + /// The color of a bar. + @IBInspectable public var barColor: UIColor = .systemGray { + didSet { + for bar in bars { + bar.backgroundColor = barColor + } + } + } + + /// The amount of time before a bar decays into the adjacent spot + @IBInspectable public var decaySpeed: Double = 0.01 { + didSet { + decayTimer?.invalidate() + decayTimer = nil + } + } + + /// The fraction the newest value will decay by if not updated by the time a decay starts + @IBInspectable public var decayAmount: Double = 0.8 + + // MARK: Internal Properties + + private var bars = [UIView]() + private var values = [Double]() + + private weak var decayTimer: Timer? + private var newestValue: Double = 0 + + // MARK: - Object Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + + initialSetup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + initialSetup() + } + + func initialSetup() { + // Pre-fill values for Interface Builder preview + #if TARGET_INTERFACE_BUILDER + values = [ + 0.19767167952644904, + 0.30975147553721694, + 0.2717680681330001, + 0.25914398105158504, + 0.3413322535900626, + 0.311223010327166, + 0.3302641160440099, + 0.303853272136915, + 0.2659123465612464, + 0.2860924489760262, + 0.26477145407733543, + 0.23180693200970012, + 0.24445487891619533, + 0.21484121767935302, + 0.19688917099112885, + 0.19020094289324854, + 0.17402194532721785, + 0.1600055988294578, + 0.15120753744055154, + 0.13789741397752767, + 0.13231033268544698, + 0.1270923459375989, + 0.1121238175344413, + 0.12400069790665748, + 0.24978783142512598, + 0.233063298365594, + 0.5375441947045457, + 0.47456518731446534, + 0.5236630241490436, + 0.4692151822551929, + 0.4255172022748686, + 0.46023063710569184, + 0.42934908823397355, + 0.37221041959882545, + 0.4685050055667653, + 0.4209394065681193, + 0.46643118034506187, + 0.4292307341708633, + 0.3814422662003417, + 0.4386719969186142, + 0.3956598546828729 + ] + #endif + + // Build the inner bars + self.updateBars() + } + + deinit { + // Invalidate the timer if it is still active + decayTimer?.invalidate() + } + + // MARK: - Layout + override public func layoutSubviews() { + updateBars() + } + + private func updateBars() { + // Clean up old bars + for bar in bars { + bar.removeFromSuperview() + } + + var newBars = [UIView]() + + + // Make sure the width of a bar and spacing is greater than 0, and that the available width is also greater than 0 + guard round(barWidth) > 0, barSpacing >= 0, bounds.width > 0, bounds.height > 0 else { + // Not enough information to create a single bar, so bail early + bars = [] + return + } + + // Calculate number of bars we will be able to display + var numberOfBarsToCreate = Int(bounds.width/(barWidth + barSpacing)) + + // Helper function for creating bars + func createBar(_ positionFromCenter: Int) { + let bar = UIView(frame: frame(forBar: positionFromCenter)) + bar.backgroundColor = barColor + bar.layer.cornerRadius = (barCornerRadius < 0 || barCornerRadius > barWidth/2) ? floor(barWidth/3) : barCornerRadius + + numberOfBarsToCreate -= 1 + newBars.append(bar) + self.addSubview(bar) + } + + // Create the center bar + createBar(0) + + // Keep creating bars in pairs until there is no more room + var position = 1 + while numberOfBarsToCreate > 0 { + // Create the symmetric pairs of bars starting from the center + createBar(-position) + createBar(position) + + position += 1 + } + + bars = newBars + } + + /// Calculate the frame of a particular bar + /// - Parameter positionFromCenter: The distance of the bar from the center (which is 0) + private func frame(forBar positionFromCenter: Int) -> CGRect { + let valueIndex = Int(positionFromCenter.magnitude) + + return frame(forBar: positionFromCenter, value: (valueIndex < values.count) ? values[valueIndex] : 0) + } + + /// Calculate the frame of a particular bar, specifying a value + /// - Parameter positionFromCenter: The distance of the bar from the center (which is 0) + private func frame(forBar positionFromCenter: Int, value: Double) -> CGRect { + let maxValue = (1 - CGFloat(positionFromCenter.magnitude)*(barWidth + barSpacing)/bounds.width/2)*bounds.height/2 + let height = CGFloat(value)*maxValue + + return CGRect(x: floor(bounds.width/2) + CGFloat(positionFromCenter)*(barWidth + barSpacing) - barWidth/2, y: floor(bounds.height/2) - height, width: barWidth, height: height*2) + } + + // MARK: - Animation + + /// Start the decay timer, but only if if hasn't been created yet + private func startTimer() { + guard decayTimer == nil else { return } + + decayTimer = Timer.scheduledTimer(withTimeInterval: decaySpeed, repeats: true) { [weak self] (_) in + guard let self = self else { return } + + self.decayNewestValue() + } + } + + private func decayNewestValue() { + values.insert(newestValue, at: 0) + + // Trim the end of the values array if there are too many for the number of bars + let currentCount = values.count + let maxCount = (bars.count + 1)/2 + /* + Note that the amount of bars will always be either 0, or an odd number (since the bars are counted in pairs after the first central bar), so we chose a "transformation" (a mathematical function) that satisfies this: value index = floor((bar index + 1)/2) + + Bar index: 0 1 2 3 4 5 6 7 8 9 ... + (valid bar index): 0 1 - 3 - 5 - 7 - 9 ... + Value index: 0 1 1 2 2 3 3 4 4 5 ... + + */ + if currentCount > maxCount { + values.removeSubrange(maxCount ..< currentCount) + } + + // Update the frames of each bar + for (positionFromCenter, value) in values.enumerated() { + if positionFromCenter == 0 { + bars[0].frame = frame(forBar: positionFromCenter, value: value) + } else { + bars[positionFromCenter*2 - 1].frame = frame(forBar: -positionFromCenter, value: value) + bars[positionFromCenter*2].frame = frame(forBar: positionFromCenter, value: value) + } + } + + // Decay the newest value + newestValue = newestValue*decayAmount + + // Check if the values are empty + let totalValue = values.reduce(0.0) { $0 + $1 } + if totalValue <= 0.000001 { + // Note that total value may never reach 0, but this is small enough to clear everything out + decayTimer?.invalidate() + decayTimer = nil + } + } + + // MARK: - Public Methods + + /// Add a value to the visualizer. Be sure to call `AVAudioPlayer.isMeteringEnabled = true`, and `AVAudioPlayer.updateMeters()` before every call to `AVAudioPlayer.averagePower(forChannel: 0)` + /// - Parameter decibelValue: The value you would get out of `AVAudioPlayer.averagePower(forChannel: 0)` + public func addValue(decibelValue: Float) { + addValue(decibelValue: Double(decibelValue)) + } + + /// Add a value to the visualizer. Be sure to call `AVAudioPlayer.isMeteringEnabled = true`, and `AVAudioPlayer.updateMeters()` before every call to `AVAudioPlayer.averagePower(forChannel: 0)` + /// - Parameter decibelValue: The value you would get out of `AVAudioPlayer.averagePower(forChannel: 0)` + public func addValue(decibelValue: Double) { + let normalizedValue = __exp10(decibelValue/20) + + newestValue = normalizedValue + + startTimer() + } + +} + diff --git a/Experiences/Experiences/Helpers/UIImage+Scaling.swift b/Experiences/Experiences/Helpers/UIImage+Scaling.swift new file mode 100644 index 00000000..c62f8d17 --- /dev/null +++ b/Experiences/Experiences/Helpers/UIImage+Scaling.swift @@ -0,0 +1,39 @@ +// +// UIImage+Scaling.swift +// Experiences +// +// Created by Norlan Tibanear on 11/17/20. +// + +import Foundation +import UIKit + +extension UIImage { + + /// Resize the image to a max dimension from size parameter + func imageByScaling(toSize size: CGSize) -> UIImage? { + guard size.width > 0 && size.height > 0 else { return nil } + + let originalAspectRatio = self.size.width/self.size.height + var correctedSize = size + + if correctedSize.width > correctedSize.width*originalAspectRatio { + correctedSize.width = correctedSize.width*originalAspectRatio + } else { + correctedSize.height = correctedSize.height/originalAspectRatio + } + + return UIGraphicsImageRenderer(size: correctedSize, format: imageRendererFormat).image { context in + draw(in: CGRect(origin: .zero, size: correctedSize)) + } + } + + /// Renders the image if the pixel data was rotated due to orientation of camera + var flattened: UIImage { + if imageOrientation == .up { return self } + return UIGraphicsImageRenderer(size: size, format: imageRendererFormat).image { context in + draw(at: .zero) + } + } + +} diff --git a/Experiences/Experiences/Helpers/UIViewController+Information.swift b/Experiences/Experiences/Helpers/UIViewController+Information.swift new file mode 100644 index 00000000..a105604c --- /dev/null +++ b/Experiences/Experiences/Helpers/UIViewController+Information.swift @@ -0,0 +1,21 @@ +// +// UIViewController+Information.swift +// Experiences +// +// Created by Norlan Tibanear on 11/17/20. +// + +import UIKit + +extension UIViewController { + + func presentInformationalAlertController(title: String?, message: String?, dismissActionCompletion: ((UIAlertAction) -> Void)? = nil, completion: (() -> Void)? = nil) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let dismissAction = UIAlertAction(title: "Dismiss", style: .cancel, handler: dismissActionCompletion) + + alertController.addAction(dismissAction) + + present(alertController, animated: true, completion: completion) + } + +} diff --git a/Experiences/Experiences/Info.plist b/Experiences/Experiences/Info.plist new file mode 100644 index 00000000..0c2ff5ef --- /dev/null +++ b/Experiences/Experiences/Info.plist @@ -0,0 +1,70 @@ + + + + + NSMicrophoneUsageDescription + Allow Mic + NSLocationWhenInUseUsageDescription + Allow Location + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Experiences + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Experiences/Experiences/Model Controller/ExperienceController.swift b/Experiences/Experiences/Model Controller/ExperienceController.swift new file mode 100644 index 00000000..977a802a --- /dev/null +++ b/Experiences/Experiences/Model Controller/ExperienceController.swift @@ -0,0 +1,20 @@ +// +// ExperienceController.swift +// Experiences +// +// Created by Norlan Tibanear on 11/16/20. +// + +import UIKit +import MapKit + + +class ExperienceController { + + var experiences = [Experience]() + static let shared = ExperienceController() + + + + +}// diff --git a/Experiences/Experiences/Model/Experience.swift b/Experiences/Experiences/Model/Experience.swift new file mode 100644 index 00000000..7f7f2c88 --- /dev/null +++ b/Experiences/Experiences/Model/Experience.swift @@ -0,0 +1,31 @@ +// +// Experience.swift +// Experiences +// +// Created by Norlan Tibanear on 11/16/20. +// + +import UIKit +import MapKit + +class Experience: NSObject, MKAnnotation { + + var title: String? + var image: UIImage? + var coordinate: CLLocationCoordinate2D + var ratio: CGFloat? + var audio: URL? + var timestamp: Date + + + init(title: String?, image: UIImage? = nil, coordinate: CLLocationCoordinate2D, ratio: CGFloat? = nil, audio: URL? = nil, timestamp: Date = Date()) { + self.title = title + self.image = image + self.coordinate = coordinate + self.ratio = ratio + self.audio = audio + self.timestamp = timestamp + } + +} + diff --git a/Experiences/Experiences/SceneDelegate.swift b/Experiences/Experiences/SceneDelegate.swift new file mode 100644 index 00000000..84f81735 --- /dev/null +++ b/Experiences/Experiences/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// Experiences +// +// Created by Norlan Tibanear on 11/15/20. +// + +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 necessarily 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/View Controller/AddExperience.swift b/Experiences/Experiences/View Controller/AddExperience.swift new file mode 100644 index 00000000..afd6df29 --- /dev/null +++ b/Experiences/Experiences/View Controller/AddExperience.swift @@ -0,0 +1,160 @@ +// +// AddExperience.swift +// Experiences +// +// Created by Norlan Tibanear on 11/16/20. +// + +import UIKit +import MapKit +import Photos +import CoreLocation + +protocol AddExperienceDelegate: AnyObject { + func addNewExperience() +} + + +class AddExperience: UIViewController, UITextFieldDelegate { + + // Outlets + @IBOutlet weak var titleTextField: UITextField! + @IBOutlet weak var imageView: UIImageView! + + @IBOutlet weak var mapView: MKMapView! + + + // Properties + + var image: UIImage? + var audio: URL? + var coordinate: CLLocationCoordinate2D? + fileprivate let manager = CLLocationManager() + var span = MKCoordinateSpan(latitudeDelta: 0.15, longitudeDelta: 0.15) + var currentLocation: CLLocationCoordinate2D? + + weak var delegate: AddExperienceDelegate? + var experience: Experience? + + override func viewDidLoad() { + super.viewDidLoad() + + self.titleTextField.delegate = self + } + + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.view.endEditing(true) + return false + } + + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + manager.desiredAccuracy = kCLLocationAccuracyBest // battery + manager.delegate = self + manager.requestWhenInUseAuthorization() + manager.startUpdatingLocation() + } + + + + + @IBAction func addAudio(_ sender: Any) { + + } + + + + + @IBAction func addExperience(_ sender: UIButton) { + guard let title = titleTextField.text, + let coordinate = self.manager.location?.coordinate else { return } + + let experience = Experience(title: title, coordinate: coordinate) + + if let image = image { + experience.image = image + } + + if let audio = audio { + experience.audio = audio + } + + ExperienceController.shared.experiences.append(experience) + delegate?.addNewExperience() + self.dismiss(animated: true, completion: nil) + }// + + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "goToAddImageVCSegue" { + let addImageVc = segue.destination as! AddImageVC + addImageVc.delegate = self + } else if segue.identifier == "goToAudioSegue" { + let audioVC = segue.destination as! AudioVC + audioVC.delegate = self + } + } + + + +}// CLASS + + +extension AddExperience: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if let location = locations.first { + manager.stopUpdatingLocation() + + render(location) + } + } + + func render(_ location: CLLocation) { + + let coordinate = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) + + let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) + + let region = MKCoordinateRegion(center: coordinate, span: span) + mapView.setRegion(region, animated: true) + + let pin = MKPointAnnotation() + pin.coordinate = coordinate + mapView.addAnnotation(pin) + } +} + + + + + + +extension AddExperience: addImageDelegate { + + func addImage(image: UIImage) { + + self.imageView.image = image + + self.image = image + + } + +} + +extension AddExperience: AudioDelegate { + func addAudio(url: URL) { + audio = url + } + + +} + + + + + + + + diff --git a/Experiences/Experiences/View Controller/AddImageVC.swift b/Experiences/Experiences/View Controller/AddImageVC.swift new file mode 100644 index 00000000..19e791ae --- /dev/null +++ b/Experiences/Experiences/View Controller/AddImageVC.swift @@ -0,0 +1,76 @@ +// +// AddImageVC.swift +// Experiences +// +// Created by Norlan Tibanear on 1/22/21. +// + +import UIKit +import Photos +import CoreImage +import CoreImage.CIFilterBuiltins + +protocol addImageDelegate: AnyObject { + func addImage(image: UIImage) +} + +class AddImageVC: UIViewController { + + // Outlets + @IBOutlet weak var photo: UIImageView! + + + // Properties + var image: UIImage? = nil + + weak var delegate: addImageDelegate? + + + override func viewDidLoad() { + super.viewDidLoad() + + setupImageView() + } + + @IBAction func saveImage(_ sender: Any) { + guard let image = photo.image else { return } + delegate?.addImage(image: image) + + dismiss(animated: true, completion: nil) + } + + func setupImageView() { + photo.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(presentPicker)) + photo.addGestureRecognizer(tapGesture) + } + + @objc func presentPicker() { + let picker = UIImagePickerController() + picker.sourceType = .photoLibrary + picker.allowsEditing = true + picker.delegate = self + self.present(picker, animated: true, completion: nil) + }// + + +}// CLASS + + +extension AddImageVC: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + if let imageSelected = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { + image = imageSelected + photo.image = imageSelected + } + if let imageOriginal = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + + image = imageOriginal + photo.image = imageOriginal + } + picker.dismiss(animated: true, completion: nil) + } + + +}// diff --git a/Experiences/Experiences/View Controller/AudioVC.swift b/Experiences/Experiences/View Controller/AudioVC.swift new file mode 100644 index 00000000..aa51d6ed --- /dev/null +++ b/Experiences/Experiences/View Controller/AudioVC.swift @@ -0,0 +1,334 @@ +// +// AudioVC.swift +// Experiences +// +// Created by Norlan Tibanear on 1/23/21. +// + +import UIKit +import AVFoundation + +protocol AudioDelegate: AnyObject { + func addAudio(url: URL) +} + +class AudioVC: UIViewController { + + // Outlets + @IBOutlet weak var beginTimeLabel: UILabel! + @IBOutlet weak var endTimeLabel: UILabel! + @IBOutlet weak var audioSlider: UISlider! + + @IBOutlet weak var playButton: UIButton! + @IBOutlet weak var micButton: UIButton! + @IBOutlet weak var saveButton: UIButton! + @IBOutlet weak var audioVisualizer: AudioVisualizer! + + // Properties + weak var delegate: AudioDelegate? + + + var audioPlayer: AVAudioPlayer? { + didSet { + guard let audioPlayer = audioPlayer else { return } + audioPlayer.delegate = self + audioPlayer.isMeteringEnabled = true + configureView() + } + } + + private lazy var timeIntervalFormatter: DateComponentsFormatter = { + let formatting = DateComponentsFormatter() + formatting.unitsStyle = .positional + formatting.zeroFormattingBehavior = .pad + formatting.allowedUnits = [.minute, .second] + return formatting + }() + + var recordingURL: URL? + var audioRecorder: AVAudioRecorder? + var playOnlyMode: Bool = false + + + + override func viewDidLoad() { + super.viewDidLoad() + saveButton.isHidden = true + playButton.isHidden = true + + configureView() + setPlayOnly() + } + + + + + private func setPlayOnly() { + if playOnlyMode { + loadAudio() + micButton.isEnabled = false + playButton.isHidden = false + saveButton.isHidden = false + saveButton.setTitle("Back", for: .normal) + configureView() + } + } + + private func configureView() { + playButton.isEnabled = !isRecording + micButton.isEnabled = !isPlaying + audioSlider.isEnabled = !isRecording + playButton.isSelected = isPlaying + micButton.isSelected = isRecording + if !isRecording { + let elapsedTime = audioPlayer?.currentTime ?? 0 + let duration = audioPlayer?.duration ?? 0 + let timeRemaining = duration.rounded() - elapsedTime + beginTimeLabel.text = timeIntervalFormatter.string(from: elapsedTime) + audioSlider.minimumValue = 0 + audioSlider.maximumValue = Float(duration) + audioSlider.value = Float(elapsedTime) + endTimeLabel.text = "-" + timeIntervalFormatter.string(from: timeRemaining)! + } else { + let elapsedTime = audioRecorder?.currentTime ?? 0 + beginTimeLabel.text = "--:--" + audioSlider.minimumValue = 0 + audioSlider.maximumValue = 1 + audioSlider.value = 0 + endTimeLabel.text = timeIntervalFormatter.string(from: elapsedTime) + } + } + + + deinit { + timer?.invalidate() + } + + + weak var timer: Timer? + + func startTimer() { + timer?.invalidate() + + timer = Timer.scheduledTimer(withTimeInterval: 0.030, repeats: true) { [weak self] (_) in + guard let self = self else { return } + + self.configureView() + + if let audioRecorder = self.audioRecorder, + self.isRecording == true { + + audioRecorder.updateMeters() +// self.audioVisualizer.addValue(decibelValue: audioRecorder.averagePower(forChannel: 0)) + } + + if let audioPlayer = self.audioPlayer, + self.isPlaying == true { + + audioPlayer.updateMeters() +// self.audioVisualizer.addValue(decibelValue: audioPlayer.averagePower(forChannel: 0)) + } + } + } + + func cancelTimer() { + timer?.invalidate() + timer = nil + } + + // Playback + + var isPlaying: Bool { + audioPlayer?.isPlaying ?? false + } + + func loadAudio() { + guard let recordingURL = recordingURL else { return } + do { + audioPlayer = try AVAudioPlayer(contentsOf: recordingURL) + } catch { + preconditionFailure("Failure to load audio file: \(error)") + } + } + + func prepareAudioSession() throws { + let session = AVAudioSession.sharedInstance() + try session.setCategory(.playAndRecord, options: [.defaultToSpeaker]) + try session.setActive(true, options: []) + } + + func play() { + guard let recordingURL = recordingURL else { return } + do { + try prepareAudioSession() + audioPlayer = try AVAudioPlayer(contentsOf: recordingURL) + audioPlayer?.play() + configureView() + startTimer() + } catch { + print("Cannot play audio: \(error)") + } + } + + func pause() { + audioPlayer?.pause() + configureView() + cancelTimer() + } + + + // Recording + + var isRecording: Bool { + audioRecorder?.isRecording ?? false + } + + func createNewRecordingURL() -> 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 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!") + } + 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 startRecording() { + do { + try prepareAudioSession() + } catch { + print("Cannot record audio: \(error)") + return + } + + recordingURL = createNewRecordingURL() + let format = AVAudioFormat(standardFormatWithSampleRate: 44_100, channels: 1)! + do { + audioRecorder = try AVAudioRecorder(url: recordingURL!, format: format) + audioRecorder?.delegate = self + audioRecorder?.isMeteringEnabled = true + audioRecorder?.record() + startTimer() + } catch { + preconditionFailure("The audio recorder could not be created with \(recordingURL!) and \(format)") + } + } + + func stopRecording() { + audioRecorder?.stop() + cancelTimer() + } + + + + + @IBAction func playBtn(_ sender: UIButton) { + if isPlaying { + pause() + } else { + play() + + } + + + } + + + @IBAction func recordBtn(_ sender: UIButton) { + if isRecording { + stopRecording() + } else { + requestPermissionOrStartRecording() + } + } + + @IBAction func sliderBtn(_ sender: UISlider) { + if isPlaying { + pause() + } + audioPlayer?.currentTime = TimeInterval(sender.value) + configureView() + + } + + @IBAction func saveBtn(_ sender: UIButton) { + guard !playOnlyMode else { + dismiss(animated: true, completion: nil) + return + } + guard let recordingURL = recordingURL else { return } + delegate?.addAudio(url: recordingURL) + audioRecorder = nil + dismiss(animated: true, completion: nil) + } + + + +}// CLASS + + +extension AudioVC: AVAudioPlayerDelegate { + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + configureView() + cancelTimer() + } + + func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + if let error = error { + print("Audio player error: \(error)") + } + } +} + +extension AudioVC: AVAudioRecorderDelegate { + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if let recordingURL = recordingURL { + saveButton.isHidden = false + configureView() + do { + audioPlayer = try AVAudioPlayer(contentsOf: recordingURL) + playButton.isHidden = false + } catch { + print("Error playing back recording") + } + } + } + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + if let error = error { + print("Audio recorder error: \(error)") + } + } +} diff --git a/Experiences/Experiences/View Controller/DetailsVC.swift b/Experiences/Experiences/View Controller/DetailsVC.swift new file mode 100644 index 00000000..84b4546e --- /dev/null +++ b/Experiences/Experiences/View Controller/DetailsVC.swift @@ -0,0 +1,69 @@ +// +// DetailsVC.swift +// Experiences +// +// Created by Norlan Tibanear on 11/16/20. +// + +import UIKit +import MapKit + +class DetailsVC: UIViewController { + + // Outlets + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var audioButton: UIButton! + @IBOutlet weak var mapView: MKMapView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var dateLabel: UILabel! + + + private lazy var dateFormatter: DateFormatter = { + let result = DateFormatter() + result.dateStyle = .medium + return result + }() + + + // Properties + var experience: Experience? + var image: UIImage? + + override func viewDidLoad() { + super.viewDidLoad() + + configureView() + configureMapView() + } + + func configureView() { + guard let experience = experience else { return } + titleLabel.text = experience.title + + + if let image = experience.image { + imageView.image = image + } + + dateLabel.text = dateFormatter.string(from: experience.timestamp) + + }// + + func configureMapView() { + guard let experience = experience else { return } + let span = MKCoordinateSpan(latitudeDelta: 0.15, longitudeDelta: 0.15) + let coordinateRegion = MKCoordinateRegion(center: experience.coordinate, span: span) + mapView.setRegion(coordinateRegion, animated: true) + mapView.addAnnotation(experience) + } + + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "playMessageSegue" { + let message = segue.destination as! AudioVC + message.recordingURL = experience?.audio + message.playOnlyMode = true + } + } + +}// diff --git a/Experiences/Experiences/View Controller/ExperiencesVC.swift b/Experiences/Experiences/View Controller/ExperiencesVC.swift new file mode 100644 index 00000000..bf858574 --- /dev/null +++ b/Experiences/Experiences/View Controller/ExperiencesVC.swift @@ -0,0 +1,61 @@ +// +// ExperiencesVC.swift +// Experiences +// +// Created by Norlan Tibanear on 11/15/20. +// + +import UIKit +import MapKit + +class ExperiencesVC: UITableViewController { + + // Outlets + + // Properties +// let experiencesController = ExperienceController() +// let experience: Experience? = nil + + override func viewDidLoad() { + super.viewDidLoad() + tableView.reloadData() + + }// + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableView.reloadData() + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return ExperienceController.shared.experiences.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + + let experiences = ExperienceController.shared.experiences + cell.textLabel?.text = experiences[indexPath.row].title + + return cell + } + + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "goToDetailVCSegue", + let indexPath = tableView.indexPathForSelectedRow { + let detailVC = segue.destination as! DetailsVC + detailVC.experience = ExperienceController.shared.experiences[indexPath.row] + } else if segue.identifier == "goToAddExperienceVCSegue" { + let addExperienceVC = segue.destination as! AddExperience + addExperienceVC.delegate = self + } + } + +}// + +extension ExperiencesVC: AddExperienceDelegate { + func addNewExperience() { + tableView.reloadData() + } +}