From 5b48d2c380a86e049707b00bb4a5b483bf46435c Mon Sep 17 00:00:00 2001 From: Norlan Tibanear Date: Wed, 18 Nov 2020 15:00:39 -0500 Subject: [PATCH 1/2] Image Done --- .../Experiences.xcodeproj/project.pbxproj | 410 ++++++++++++++++++ Experiences/Experiences/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 +++++ .../Experiences/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 ++ .../Experiences/Base.lproj/Main.storyboard | 252 +++++++++++ .../Experiences/Helpers/UIImage+Scaling.swift | 39 ++ .../UIViewController+Information.swift | 21 + Experiences/Experiences/Info.plist | 66 +++ .../ExperienceController.swift | 25 ++ .../Experiences/Model/Experience.swift | 22 + Experiences/Experiences/SceneDelegate.swift | 52 +++ .../View Controller/AddExperience.swift | 177 ++++++++ .../View Controller/DetailsVC.swift | 20 + .../View Controller/ExperiencesVC.swift | 33 ++ 16 files changed, 1293 insertions(+) create mode 100644 Experiences/Experiences.xcodeproj/project.pbxproj create mode 100644 Experiences/Experiences/AppDelegate.swift create mode 100644 Experiences/Experiences/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Experiences/Experiences/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Experiences/Experiences/Assets.xcassets/Contents.json create mode 100644 Experiences/Experiences/Base.lproj/LaunchScreen.storyboard create mode 100644 Experiences/Experiences/Base.lproj/Main.storyboard create mode 100644 Experiences/Experiences/Helpers/UIImage+Scaling.swift create mode 100644 Experiences/Experiences/Helpers/UIViewController+Information.swift create mode 100644 Experiences/Experiences/Info.plist create mode 100644 Experiences/Experiences/Model Controller/ExperienceController.swift create mode 100644 Experiences/Experiences/Model/Experience.swift create mode 100644 Experiences/Experiences/SceneDelegate.swift create mode 100644 Experiences/Experiences/View Controller/AddExperience.swift create mode 100644 Experiences/Experiences/View Controller/DetailsVC.swift create mode 100644 Experiences/Experiences/View Controller/ExperiencesVC.swift diff --git a/Experiences/Experiences.xcodeproj/project.pbxproj b/Experiences/Experiences.xcodeproj/project.pbxproj new file mode 100644 index 00000000..54d60e6f --- /dev/null +++ b/Experiences/Experiences.xcodeproj/project.pbxproj @@ -0,0 +1,410 @@ +// !$*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 */; }; +/* 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 = ""; }; +/* 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 */, + ); + 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 */, + ); + 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 */, + 8604C2D12561D016008B7D36 /* SceneDelegate.swift in Sources */, + 8604C2FB25630D55008B7D36 /* Experience.swift in Sources */, + 8604C2F725630AFD008B7D36 /* AddExperience.swift in Sources */, + 8604C2F425630AD0008B7D36 /* DetailsVC.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/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..d80f7f38 --- /dev/null +++ b/Experiences/Experiences/Base.lproj/Main.storyboard @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..64346d82 --- /dev/null +++ b/Experiences/Experiences/Info.plist @@ -0,0 +1,66 @@ + + + + + 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..b00b0b09 --- /dev/null +++ b/Experiences/Experiences/Model Controller/ExperienceController.swift @@ -0,0 +1,25 @@ +// +// ExperienceController.swift +// Experiences +// +// Created by Norlan Tibanear on 11/16/20. +// + +import UIKit + + +class ExperienceController { + + var experiences = [Experience]() + static let shared = ExperienceController() + + func createExperience(with title: String) { + + let experience = Experience(title: title) + + experiences.append(experience) + + } + + +}// diff --git a/Experiences/Experiences/Model/Experience.swift b/Experiences/Experiences/Model/Experience.swift new file mode 100644 index 00000000..1bcee8fe --- /dev/null +++ b/Experiences/Experiences/Model/Experience.swift @@ -0,0 +1,22 @@ +// +// Experience.swift +// Experiences +// +// Created by Norlan Tibanear on 11/16/20. +// + +import UIKit + +struct Experience { + + let title: String? + let image: UIImage? + + + init(title: String?, image: UIImage? = nil) { + self.title = title + self.image = image + } + +} + 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..09ba19e1 --- /dev/null +++ b/Experiences/Experiences/View Controller/AddExperience.swift @@ -0,0 +1,177 @@ +// +// AddExperience.swift +// Experiences +// +// Created by Norlan Tibanear on 11/16/20. +// + +import UIKit +import Photos +import CoreImage +import CoreImage.CIFilterBuiltins + + +class AddExperience: UIViewController { + + // Outlets + @IBOutlet weak var titleTextField: UITextField! + @IBOutlet weak var addressTextField: UITextField! + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var sepiaSlider: UISlider! + @IBOutlet weak var hueSlider: UISlider! + + + // Properties + + + var originalImage: UIImage? { + didSet { + guard let originalImage = originalImage else { + scaledImage = nil + return + } + + var scaledSize = imageView.bounds.size + let scale = imageView.contentScaleFactor + + scaledSize = CGSize(width: scaledSize.width*scale, height: scaledSize.height*scale) + + guard let scaledUIImage = originalImage.imageByScaling(toSize: scaledSize) else { + scaledImage = nil + return + } + + scaledImage = CIImage(image: scaledUIImage) + } + } + + var scaledImage: CIImage? { + didSet{ + if let scaledImage = scaledImage { + imageView.image = UIImage(ciImage: scaledImage) + } + // updateImage() + } + } + + + let context = CIContext() + +// private let colorControlsFilter = CIFilter.colorControls() +// +// private let saturationFilter = CIFilter.saturationBlendMode() +// private let brightnessFilter = CIFilter.saturationBlendMode() + + + override func viewDidLoad() { + super.viewDidLoad() + + originalImage = imageView.image + + selectImage() + +// setImageViewHeight(with: 1.0) + } + +// private func image(byFiltering inputImage: CIImage) -> UIImage { +// colorControlsFilter.inputImage = inputImage +// colorControlsFilter.brightness = brightnessSlider.value +// colorControlsFilter.saturation = saturationSlider.value +// +// +// guard let outputImage = saturationFilter.outputImage else { return originalImage! } +// guard let renderImage = context.createCGImage(outputImage, from: inputImage.extent) else { return originalImage! } +// +// return UIImage(cgImage: renderImage) +// } + + +// private func updateImage() { +// if let scaledImage = scaledImage { +// imageView.image = image(byFiltering: scaledImage) +// } else { +// imageView.image = nil +// } +// } + + + func selectImage() { + imageView.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(presentPicker)) + imageView.addGestureRecognizer(tapGesture) + } + + @objc func presentPicker() { + let picker = UIImagePickerController() + picker.sourceType = .photoLibrary + picker.allowsEditing = true + picker.delegate = self + self.present(picker, animated: true, completion: nil) + } + + private func sepiaImage(byFiltering inputImage: CIImage) -> UIImage? { + let sepia = CIFilter.sepiaTone() + sepia.inputImage = inputImage + + sepia.inputImage = sepia.outputImage?.clampedToExtent() + sepia.intensity = sepiaSlider.value + + guard let outputImage = sepia.outputImage else { return nil } + guard let renderCIImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderCIImage) + }// + + private func hueImage(byFiltering inputImage: CIImage) -> UIImage? { + let hue = CIFilter.hueAdjust() + hue.inputImage = inputImage + + hue.inputImage = hue.outputImage?.clampedToExtent() + hue.angle = hueSlider.value + + guard let outputImage = hue.outputImage else { return nil } + guard let renderCIImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + + return UIImage(cgImage: renderCIImage) + }// + + + + @IBAction func addExperience(_ sender: UIButton) { + guard let title = titleTextField.text else { return } + + } + + @IBAction func sepiaChanged(_ sender: UISlider) { + guard let scaledImage = scaledImage else { return } + imageView.image = sepiaImage(byFiltering: scaledImage) + } + + @IBAction func hueChanged(_ sender: UISlider) { + guard let scaledImage = scaledImage else { return } + imageView.image = hueImage(byFiltering: scaledImage) + } + + +}// + + +extension AddExperience: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + + if let image = info[.editedImage] as? UIImage { + originalImage = image + } else if let image = info[.originalImage] as? UIImage { + originalImage = image + } + + picker.dismiss(animated: true, completion: nil) + + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true, completion: nil) + } + +}// diff --git a/Experiences/Experiences/View Controller/DetailsVC.swift b/Experiences/Experiences/View Controller/DetailsVC.swift new file mode 100644 index 00000000..21f364b3 --- /dev/null +++ b/Experiences/Experiences/View Controller/DetailsVC.swift @@ -0,0 +1,20 @@ +// +// DetailsVC.swift +// Experiences +// +// Created by Norlan Tibanear on 11/16/20. +// + +import UIKit + +class DetailsVC: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + + +}// diff --git a/Experiences/Experiences/View Controller/ExperiencesVC.swift b/Experiences/Experiences/View Controller/ExperiencesVC.swift new file mode 100644 index 00000000..d1babd91 --- /dev/null +++ b/Experiences/Experiences/View Controller/ExperiencesVC.swift @@ -0,0 +1,33 @@ +// +// ExperiencesVC.swift +// Experiences +// +// Created by Norlan Tibanear on 11/15/20. +// + +import UIKit +import MapKit + +class ExperiencesVC: UIViewController { + + // Outlets + @IBOutlet weak var mapView: MKMapView! + + + + // Properties + let experiencesController = ExperienceController() + let experience: Experience? = nil + + override func viewDidLoad() { + super.viewDidLoad() + + + } + + + + +}// + + From 7f7a3b07f22d665a5dcb01e3036c241a55c2a571 Mon Sep 17 00:00:00 2001 From: Norlan Tibanear Date: Sun, 24 Jan 2021 18:18:40 -0500 Subject: [PATCH 2/2] Sprint Done --- .../Experiences.xcodeproj/project.pbxproj | 12 + .../PlaceholderImage.imageset/Contents.json | 12 + .../PlaceholderImage.jpg | Bin 0 -> 30111 bytes .../Experiences/Base.lproj/Main.storyboard | 498 +++++++++++++----- .../Experiences/Helpers/AudioVisualizer.swift | 275 ++++++++++ Experiences/Experiences/Info.plist | 4 + .../ExperienceController.swift | 9 +- .../Experiences/Model/Experience.swift | 17 +- .../View Controller/AddExperience.swift | 231 ++++---- .../View Controller/AddImageVC.swift | 76 +++ .../Experiences/View Controller/AudioVC.swift | 334 ++++++++++++ .../View Controller/DetailsVC.swift | 51 +- .../View Controller/ExperiencesVC.swift | 46 +- 13 files changed, 1291 insertions(+), 274 deletions(-) create mode 100644 Experiences/Experiences/Assets.xcassets/PlaceholderImage.imageset/Contents.json create mode 100644 Experiences/Experiences/Assets.xcassets/PlaceholderImage.imageset/PlaceholderImage.jpg create mode 100644 Experiences/Experiences/Helpers/AudioVisualizer.swift create mode 100644 Experiences/Experiences/View Controller/AddImageVC.swift create mode 100644 Experiences/Experiences/View Controller/AudioVC.swift diff --git a/Experiences/Experiences.xcodeproj/project.pbxproj b/Experiences/Experiences.xcodeproj/project.pbxproj index 54d60e6f..31691397 100644 --- a/Experiences/Experiences.xcodeproj/project.pbxproj +++ b/Experiences/Experiences.xcodeproj/project.pbxproj @@ -19,6 +19,9 @@ 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 */ @@ -36,6 +39,9 @@ 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 */ @@ -89,6 +95,8 @@ 8604C2EA2562168D008B7D36 /* ExperiencesVC.swift */, 8604C2F325630AD0008B7D36 /* DetailsVC.swift */, 8604C2F625630AFD008B7D36 /* AddExperience.swift */, + 860E3C6625BB644D00897B08 /* AddImageVC.swift */, + 860E3C7125BCCBD700897B08 /* AudioVC.swift */, ); path = "View Controller"; sourceTree = ""; @@ -121,6 +129,7 @@ children = ( 8604C30825646E63008B7D36 /* UIViewController+Information.swift */, 8604C30B25647093008B7D36 /* UIImage+Scaling.swift */, + 860E3C7925BE0C0500897B08 /* AudioVisualizer.swift */, ); path = Helpers; sourceTree = ""; @@ -199,10 +208,13 @@ 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; 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 0000000000000000000000000000000000000000..d90319a5ae47b55e09cfbc112c78b7642fbc8a4d GIT binary patch literal 30111 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 literal 0 HcmV?d00001 diff --git a/Experiences/Experiences/Base.lproj/Main.storyboard b/Experiences/Experiences/Base.lproj/Main.storyboard index d80f7f38..606be223 100644 --- a/Experiences/Experiences/Base.lproj/Main.storyboard +++ b/Experiences/Experiences/Base.lproj/Main.storyboard @@ -1,88 +1,240 @@ - + - + - - + + - - + + - - + + + + + + + + + + + - + - - - - + + + + + + + + + + + + + + + + - - - - - - - - + - - + + + + + - + - + - - + + - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + - + - + - + - + - + - - + + + + + - - - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - + + - + - + - + + + + + + + @@ -245,8 +479,14 @@ + + + + + + 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/Info.plist b/Experiences/Experiences/Info.plist index 64346d82..0c2ff5ef 100644 --- a/Experiences/Experiences/Info.plist +++ b/Experiences/Experiences/Info.plist @@ -2,6 +2,10 @@ + NSMicrophoneUsageDescription + Allow Mic + NSLocationWhenInUseUsageDescription + Allow Location CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/Experiences/Experiences/Model Controller/ExperienceController.swift b/Experiences/Experiences/Model Controller/ExperienceController.swift index b00b0b09..977a802a 100644 --- a/Experiences/Experiences/Model Controller/ExperienceController.swift +++ b/Experiences/Experiences/Model Controller/ExperienceController.swift @@ -6,6 +6,7 @@ // import UIKit +import MapKit class ExperienceController { @@ -13,13 +14,7 @@ class ExperienceController { var experiences = [Experience]() static let shared = ExperienceController() - func createExperience(with title: String) { - - let experience = Experience(title: title) - - experiences.append(experience) - - } + }// diff --git a/Experiences/Experiences/Model/Experience.swift b/Experiences/Experiences/Model/Experience.swift index 1bcee8fe..7f7f2c88 100644 --- a/Experiences/Experiences/Model/Experience.swift +++ b/Experiences/Experiences/Model/Experience.swift @@ -6,16 +6,25 @@ // import UIKit +import MapKit -struct Experience { +class Experience: NSObject, MKAnnotation { - let title: String? - let image: UIImage? + var title: String? + var image: UIImage? + var coordinate: CLLocationCoordinate2D + var ratio: CGFloat? + var audio: URL? + var timestamp: Date - init(title: String?, image: UIImage? = nil) { + 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/View Controller/AddExperience.swift b/Experiences/Experiences/View Controller/AddExperience.swift index 09ba19e1..afd6df29 100644 --- a/Experiences/Experiences/View Controller/AddExperience.swift +++ b/Experiences/Experiences/View Controller/AddExperience.swift @@ -6,172 +6,155 @@ // import UIKit +import MapKit import Photos -import CoreImage -import CoreImage.CIFilterBuiltins +import CoreLocation +protocol AddExperienceDelegate: AnyObject { + func addNewExperience() +} -class AddExperience: UIViewController { + +class AddExperience: UIViewController, UITextFieldDelegate { // Outlets @IBOutlet weak var titleTextField: UITextField! - @IBOutlet weak var addressTextField: UITextField! @IBOutlet weak var imageView: UIImageView! - @IBOutlet weak var sepiaSlider: UISlider! - @IBOutlet weak var hueSlider: UISlider! + + @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 + } - var originalImage: UIImage? { - didSet { - guard let originalImage = originalImage else { - scaledImage = nil - return - } - - var scaledSize = imageView.bounds.size - let scale = imageView.contentScaleFactor - - scaledSize = CGSize(width: scaledSize.width*scale, height: scaledSize.height*scale) - - guard let scaledUIImage = originalImage.imageByScaling(toSize: scaledSize) else { - scaledImage = nil - return - } - - scaledImage = CIImage(image: scaledUIImage) - } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.view.endEditing(true) + return false } - var scaledImage: CIImage? { - didSet{ - if let scaledImage = scaledImage { - imageView.image = UIImage(ciImage: scaledImage) - } - // updateImage() - } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + manager.desiredAccuracy = kCLLocationAccuracyBest // battery + manager.delegate = self + manager.requestWhenInUseAuthorization() + manager.startUpdatingLocation() } - let context = CIContext() + + + @IBAction func addAudio(_ sender: Any) { + + } -// private let colorControlsFilter = CIFilter.colorControls() -// -// private let saturationFilter = CIFilter.saturationBlendMode() -// private let brightnessFilter = CIFilter.saturationBlendMode() + - override func viewDidLoad() { - super.viewDidLoad() + @IBAction func addExperience(_ sender: UIButton) { + guard let title = titleTextField.text, + let coordinate = self.manager.location?.coordinate else { return } - originalImage = imageView.image - - selectImage() + let experience = Experience(title: title, coordinate: coordinate) + + if let image = image { + experience.image = image + } -// setImageViewHeight(with: 1.0) - } - -// private func image(byFiltering inputImage: CIImage) -> UIImage { -// colorControlsFilter.inputImage = inputImage -// colorControlsFilter.brightness = brightnessSlider.value -// colorControlsFilter.saturation = saturationSlider.value -// -// -// guard let outputImage = saturationFilter.outputImage else { return originalImage! } -// guard let renderImage = context.createCGImage(outputImage, from: inputImage.extent) else { return originalImage! } -// -// return UIImage(cgImage: renderImage) -// } + if let audio = audio { + experience.audio = audio + } + + ExperienceController.shared.experiences.append(experience) + delegate?.addNewExperience() + self.dismiss(animated: true, completion: nil) + }// -// private func updateImage() { -// if let scaledImage = scaledImage { -// imageView.image = image(byFiltering: scaledImage) -// } else { -// imageView.image = 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 + } + } - func selectImage() { - imageView.isUserInteractionEnabled = true - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(presentPicker)) - imageView.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 AddExperience: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if let location = locations.first { + manager.stopUpdatingLocation() + + render(location) + } } - private func sepiaImage(byFiltering inputImage: CIImage) -> UIImage? { - let sepia = CIFilter.sepiaTone() - sepia.inputImage = inputImage + func render(_ location: CLLocation) { - sepia.inputImage = sepia.outputImage?.clampedToExtent() - sepia.intensity = sepiaSlider.value - - guard let outputImage = sepia.outputImage else { return nil } - guard let renderCIImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } - return UIImage(cgImage: renderCIImage) - }// - - private func hueImage(byFiltering inputImage: CIImage) -> UIImage? { - let hue = CIFilter.hueAdjust() - hue.inputImage = inputImage + let coordinate = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) - hue.inputImage = hue.outputImage?.clampedToExtent() - hue.angle = hueSlider.value + let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) - guard let outputImage = hue.outputImage else { return nil } - guard let renderCIImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + let region = MKCoordinateRegion(center: coordinate, span: span) + mapView.setRegion(region, animated: true) - return UIImage(cgImage: renderCIImage) - }// - + let pin = MKPointAnnotation() + pin.coordinate = coordinate + mapView.addAnnotation(pin) + } +} - @IBAction func addExperience(_ sender: UIButton) { - guard let title = titleTextField.text else { return } + + + + +extension AddExperience: addImageDelegate { + + func addImage(image: UIImage) { + + self.imageView.image = image + + self.image = image } - - @IBAction func sepiaChanged(_ sender: UISlider) { - guard let scaledImage = scaledImage else { return } - imageView.image = sepiaImage(byFiltering: scaledImage) - } - - @IBAction func hueChanged(_ sender: UISlider) { - guard let scaledImage = scaledImage else { return } - imageView.image = hueImage(byFiltering: scaledImage) + +} + +extension AddExperience: AudioDelegate { + func addAudio(url: URL) { + audio = url } -}// +} -extension AddExperience: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - - if let image = info[.editedImage] as? UIImage { - originalImage = image - } else if let image = info[.originalImage] as? UIImage { - originalImage = image - } - - picker.dismiss(animated: true, completion: nil) - - } - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - picker.dismiss(animated: true, completion: nil) - } -}// + + + 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 index 21f364b3..84b4546e 100644 --- a/Experiences/Experiences/View Controller/DetailsVC.swift +++ b/Experiences/Experiences/View Controller/DetailsVC.swift @@ -6,15 +6,64 @@ // 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() - // Do any additional setup after loading the view. + 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 index d1babd91..bf858574 100644 --- a/Experiences/Experiences/View Controller/ExperiencesVC.swift +++ b/Experiences/Experiences/View Controller/ExperiencesVC.swift @@ -8,26 +8,54 @@ import UIKit import MapKit -class ExperiencesVC: UIViewController { +class ExperiencesVC: UITableViewController { // Outlets - @IBOutlet weak var mapView: MKMapView! - - // Properties - let experiencesController = ExperienceController() - let experience: Experience? = nil +// 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() + } +}