diff --git a/Experiences/Experiences.xcodeproj/project.pbxproj b/Experiences/Experiences.xcodeproj/project.pbxproj new file mode 100644 index 00000000..a2c082bc --- /dev/null +++ b/Experiences/Experiences.xcodeproj/project.pbxproj @@ -0,0 +1,413 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 11C1F8F724C281CC00A57223 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C1F8F624C281CC00A57223 /* AppDelegate.swift */; }; + 11C1F8F924C281CC00A57223 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C1F8F824C281CC00A57223 /* SceneDelegate.swift */; }; + 11C1F8FE24C281CC00A57223 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 11C1F8FC24C281CC00A57223 /* Main.storyboard */; }; + 11C1F90024C281D200A57223 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 11C1F8FF24C281D200A57223 /* Assets.xcassets */; }; + 11C1F90324C281D200A57223 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 11C1F90124C281D200A57223 /* LaunchScreen.storyboard */; }; + 11C1F91124C2867C00A57223 /* ExperienceTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C1F91024C2867C00A57223 /* ExperienceTableViewController.swift */; }; + 11C1F91324C2869500A57223 /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C1F91224C2869500A57223 /* CameraViewController.swift */; }; + 11C1F91524C286A500A57223 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C1F91424C286A500A57223 /* MapViewController.swift */; }; + 11C1F91724C2888800A57223 /* RecordingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C1F91624C2888800A57223 /* RecordingViewController.swift */; }; + 11C1F91924C28BDF00A57223 /* PhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C1F91824C28BDF00A57223 /* PhotoViewController.swift */; }; + 11C1F91C24C292EE00A57223 /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C1F91B24C292EE00A57223 /* UIImage+Scaling.swift */; }; + 11CC099024CCD3500031D055 /* LocationServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CC098F24CCD3500031D055 /* LocationServices.swift */; }; + 11CC099224CCD3780031D055 /* Experience + MKAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CC099124CCD3780031D055 /* Experience + MKAnnotation.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 11C1F8F324C281CC00A57223 /* Experiences.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Experiences.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 11C1F8F624C281CC00A57223 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 11C1F8F824C281CC00A57223 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 11C1F8FD24C281CC00A57223 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 11C1F8FF24C281D200A57223 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 11C1F90224C281D200A57223 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 11C1F90424C281D200A57223 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 11C1F91024C2867C00A57223 /* ExperienceTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceTableViewController.swift; sourceTree = ""; }; + 11C1F91224C2869500A57223 /* CameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; + 11C1F91424C286A500A57223 /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = ""; }; + 11C1F91624C2888800A57223 /* RecordingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingViewController.swift; sourceTree = ""; }; + 11C1F91824C28BDF00A57223 /* PhotoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoViewController.swift; sourceTree = ""; }; + 11C1F91B24C292EE00A57223 /* UIImage+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Scaling.swift"; sourceTree = ""; }; + 11C1F91D24C2931300A57223 /* PhotoFilter.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = PhotoFilter.entitlements; path = "../../../../../Core Image/Guided Projects/ios-guided-project-core-image-starter/PhotoFilter/PhotoFilter.entitlements"; sourceTree = ""; }; + 11CC098F24CCD3500031D055 /* LocationServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LocationServices.swift; path = Experiences/LocationServices.swift; sourceTree = SOURCE_ROOT; }; + 11CC099124CCD3780031D055 /* Experience + MKAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Experience + MKAnnotation.swift"; path = "Experiences/Experience + MKAnnotation.swift"; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 11C1F8F024C281CC00A57223 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1131642B24C29DB6005A72B2 /* Extra map files */ = { + isa = PBXGroup; + children = ( + 11CC098F24CCD3500031D055 /* LocationServices.swift */, + 11CC099124CCD3780031D055 /* Experience + MKAnnotation.swift */, + ); + path = "Extra map files"; + sourceTree = ""; + }; + 11C1F8EA24C281CB00A57223 = { + isa = PBXGroup; + children = ( + 11C1F8F524C281CC00A57223 /* Experiences */, + 11C1F8F424C281CC00A57223 /* Products */, + ); + sourceTree = ""; + }; + 11C1F8F424C281CC00A57223 /* Products */ = { + isa = PBXGroup; + children = ( + 11C1F8F324C281CC00A57223 /* Experiences.app */, + ); + name = Products; + sourceTree = ""; + }; + 11C1F8F524C281CC00A57223 /* Experiences */ = { + isa = PBXGroup; + children = ( + 1131642B24C29DB6005A72B2 /* Extra map files */, + 11C1F91A24C292C600A57223 /* Extra photo Files */, + 11C1F90C24C282D700A57223 /* Views */, + 11C1F90B24C282CD00A57223 /* Storyboards */, + 11C1F90A24C282AE00A57223 /* Resources */, + 11C1F90424C281D200A57223 /* Info.plist */, + ); + path = Experiences; + sourceTree = ""; + }; + 11C1F90A24C282AE00A57223 /* Resources */ = { + isa = PBXGroup; + children = ( + 11C1F8F624C281CC00A57223 /* AppDelegate.swift */, + 11C1F8F824C281CC00A57223 /* SceneDelegate.swift */, + 11C1F8FF24C281D200A57223 /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + 11C1F90B24C282CD00A57223 /* Storyboards */ = { + isa = PBXGroup; + children = ( + 11C1F90124C281D200A57223 /* LaunchScreen.storyboard */, + 11C1F8FC24C281CC00A57223 /* Main.storyboard */, + ); + path = Storyboards; + sourceTree = ""; + }; + 11C1F90C24C282D700A57223 /* Views */ = { + isa = PBXGroup; + children = ( + 11C1F91024C2867C00A57223 /* ExperienceTableViewController.swift */, + 11C1F91224C2869500A57223 /* CameraViewController.swift */, + 11C1F91424C286A500A57223 /* MapViewController.swift */, + 11C1F91624C2888800A57223 /* RecordingViewController.swift */, + 11C1F91824C28BDF00A57223 /* PhotoViewController.swift */, + ); + path = Views; + sourceTree = ""; + }; + 11C1F91A24C292C600A57223 /* Extra photo Files */ = { + isa = PBXGroup; + children = ( + 11C1F91B24C292EE00A57223 /* UIImage+Scaling.swift */, + 11C1F91D24C2931300A57223 /* PhotoFilter.entitlements */, + ); + path = "Extra photo Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 11C1F8F224C281CC00A57223 /* Experiences */ = { + isa = PBXNativeTarget; + buildConfigurationList = 11C1F90724C281D200A57223 /* Build configuration list for PBXNativeTarget "Experiences" */; + buildPhases = ( + 11C1F8EF24C281CC00A57223 /* Sources */, + 11C1F8F024C281CC00A57223 /* Frameworks */, + 11C1F8F124C281CC00A57223 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Experiences; + productName = Experiences; + productReference = 11C1F8F324C281CC00A57223 /* Experiences.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 11C1F8EB24C281CC00A57223 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1150; + LastUpgradeCheck = 1150; + ORGANIZATIONNAME = "Kevin Stewart"; + TargetAttributes = { + 11C1F8F224C281CC00A57223 = { + CreatedOnToolsVersion = 11.5; + }; + }; + }; + buildConfigurationList = 11C1F8EE24C281CC00A57223 /* Build configuration list for PBXProject "Experiences" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 11C1F8EA24C281CB00A57223; + productRefGroup = 11C1F8F424C281CC00A57223 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 11C1F8F224C281CC00A57223 /* Experiences */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 11C1F8F124C281CC00A57223 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 11C1F90324C281D200A57223 /* LaunchScreen.storyboard in Resources */, + 11C1F90024C281D200A57223 /* Assets.xcassets in Resources */, + 11C1F8FE24C281CC00A57223 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 11C1F8EF24C281CC00A57223 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 11C1F91C24C292EE00A57223 /* UIImage+Scaling.swift in Sources */, + 11C1F91524C286A500A57223 /* MapViewController.swift in Sources */, + 11CC099224CCD3780031D055 /* Experience + MKAnnotation.swift in Sources */, + 11C1F91324C2869500A57223 /* CameraViewController.swift in Sources */, + 11C1F91724C2888800A57223 /* RecordingViewController.swift in Sources */, + 11C1F91124C2867C00A57223 /* ExperienceTableViewController.swift in Sources */, + 11C1F91924C28BDF00A57223 /* PhotoViewController.swift in Sources */, + 11C1F8F724C281CC00A57223 /* AppDelegate.swift in Sources */, + 11CC099024CCD3500031D055 /* LocationServices.swift in Sources */, + 11C1F8F924C281CC00A57223 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 11C1F8FC24C281CC00A57223 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 11C1F8FD24C281CC00A57223 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 11C1F90124C281D200A57223 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 11C1F90224C281D200A57223 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 11C1F90524C281D200A57223 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + 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; + }; + 11C1F90624C281D200A57223 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 11C1F90824C281D200A57223 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = V5ARGK4ZJP; + INFOPLIST_FILE = Experiences/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.kevinStewart.about.Experiences; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 11C1F90924C281D200A57223 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = V5ARGK4ZJP; + INFOPLIST_FILE = Experiences/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.kevinStewart.about.Experiences; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 11C1F8EE24C281CC00A57223 /* Build configuration list for PBXProject "Experiences" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 11C1F90524C281D200A57223 /* Debug */, + 11C1F90624C281D200A57223 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 11C1F90724C281D200A57223 /* Build configuration list for PBXNativeTarget "Experiences" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 11C1F90824C281D200A57223 /* Debug */, + 11C1F90924C281D200A57223 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 11C1F8EB24C281CC00A57223 /* Project object */; +} diff --git a/Experiences/Experiences/Experience + MKAnnotation.swift b/Experiences/Experiences/Experience + MKAnnotation.swift new file mode 100644 index 00000000..4ea3f691 --- /dev/null +++ b/Experiences/Experiences/Experience + MKAnnotation.swift @@ -0,0 +1,20 @@ +// +// Experience+MKAnnotation.swift +// Experiences +// +// Created by Kevin Stewart on 7/23/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import MapKit + +extension Experience: MKAnnotation { +var coordinate: CLLocationCoordinate2D { + return currentLocation +} + +var title: String? { + name +} + +} diff --git a/Experiences/Experiences/Extra photo Files/UIImage+Scaling.swift b/Experiences/Experiences/Extra photo Files/UIImage+Scaling.swift new file mode 100644 index 00000000..10bf4b04 --- /dev/null +++ b/Experiences/Experiences/Extra photo Files/UIImage+Scaling.swift @@ -0,0 +1,39 @@ +// +// UIImage+Scaling.swift +// Experiences +// +// Created by Kevin Stewart on 7/17/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +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/Info.plist b/Experiences/Experiences/Info.plist new file mode 100644 index 00000000..0c2fd28a --- /dev/null +++ b/Experiences/Experiences/Info.plist @@ -0,0 +1,74 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + NSLocationWhenInUseUsageDescription + $(PRODUCT_NAME) uses your location to show where you are on the map. + NSMicrophoneUsageDescription + $(PRODUCT_NAME) uses the microphone to record audio + NSPhotoLibraryAddUsageDescription + $(PRODUCT_NAME) uses needs access to camera to add photos + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME) uses the photo library + NSCameraUsageDescription + $(PRODUCT_NAME) needs access to the camera to record videos + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Experiences/Experiences/LocationServices.swift b/Experiences/Experiences/LocationServices.swift new file mode 100644 index 00000000..f2333983 --- /dev/null +++ b/Experiences/Experiences/LocationServices.swift @@ -0,0 +1,79 @@ +// +// File.swift +// Experiences +// +// Created by Kevin Stewart on 7/23/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import Foundation +import CoreLocation + +protocol LocationServiceDelegate { + func tracingLocation(_ currentLocation: CLLocation) + func tracingLocationDidFailWithError(_ error: NSError) +} + +class LocationService: NSObject, CLLocationManagerDelegate { + + static let sharedInstance = LocationService() + + var currentLocation: CLLocation? + var locationManager: CLLocationManager? + var delegate: LocationServiceDelegate? + + override init() { + super.init() + + self.locationManager = CLLocationManager() + guard let locationManager = self.locationManager else { + return + } + + if CLLocationManager.authorizationStatus() == .notDetermined { + locationManager.requestAlwaysAuthorization() + } + + locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + locationManager.delegate = self + } + + func getOneTimeLocation() { + print("Starting location updates") + self.locationManager?.requestLocation() + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + + guard let location = locations.last else { + return + } + + currentLocation = location + updateLocation(location) + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + + updateLocationDidFailWithError(error as NSError) + } + + fileprivate func updateLocation(_ currentLocation: CLLocation) { + + guard let delegate = self.delegate else { + return + } + + delegate.tracingLocation(currentLocation) + } + + fileprivate func updateLocationDidFailWithError(_ error: NSError) { + + guard let delegate = self.delegate else { + return + } + + delegate.tracingLocationDidFailWithError(error) + } + +} diff --git a/Experiences/Experiences/Resources/AppDelegate.swift b/Experiences/Experiences/Resources/AppDelegate.swift new file mode 100644 index 00000000..1e44724c --- /dev/null +++ b/Experiences/Experiences/Resources/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// Experiences +// +// Created by Kevin Stewart on 7/17/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/Experiences/Experiences/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Experiences/Experiences/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/Experiences/Experiences/Resources/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/Resources/Assets.xcassets/Contents.json b/Experiences/Experiences/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Experiences/Experiences/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Experiences/Experiences/Resources/Assets.xcassets/Record.imageset/Contents.json b/Experiences/Experiences/Resources/Assets.xcassets/Record.imageset/Contents.json new file mode 100644 index 00000000..3b3bccc3 --- /dev/null +++ b/Experiences/Experiences/Resources/Assets.xcassets/Record.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Record.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Experiences/Experiences/Resources/Assets.xcassets/Record.imageset/Record.pdf b/Experiences/Experiences/Resources/Assets.xcassets/Record.imageset/Record.pdf new file mode 100644 index 00000000..f4b07935 Binary files /dev/null and b/Experiences/Experiences/Resources/Assets.xcassets/Record.imageset/Record.pdf differ diff --git a/Experiences/Experiences/Resources/Assets.xcassets/Stop.imageset/Contents.json b/Experiences/Experiences/Resources/Assets.xcassets/Stop.imageset/Contents.json new file mode 100644 index 00000000..a75715ae --- /dev/null +++ b/Experiences/Experiences/Resources/Assets.xcassets/Stop.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Stop.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Experiences/Experiences/Resources/Assets.xcassets/Stop.imageset/Stop.pdf b/Experiences/Experiences/Resources/Assets.xcassets/Stop.imageset/Stop.pdf new file mode 100644 index 00000000..7aee81c0 Binary files /dev/null and b/Experiences/Experiences/Resources/Assets.xcassets/Stop.imageset/Stop.pdf differ diff --git a/Experiences/Experiences/Resources/SceneDelegate.swift b/Experiences/Experiences/Resources/SceneDelegate.swift new file mode 100644 index 00000000..140b68d5 --- /dev/null +++ b/Experiences/Experiences/Resources/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// Experiences +// +// Created by Kevin Stewart on 7/17/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/Experiences/Experiences/Storyboards/Base.lproj/LaunchScreen.storyboard b/Experiences/Experiences/Storyboards/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Experiences/Experiences/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experiences/Experiences/Storyboards/Base.lproj/Main.storyboard b/Experiences/Experiences/Storyboards/Base.lproj/Main.storyboard new file mode 100644 index 00000000..7e2ee281 --- /dev/null +++ b/Experiences/Experiences/Storyboards/Base.lproj/Main.storyboard @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experiences/Experiences/Views/CameraViewController.swift b/Experiences/Experiences/Views/CameraViewController.swift new file mode 100644 index 00000000..c46a2f62 --- /dev/null +++ b/Experiences/Experiences/Views/CameraViewController.swift @@ -0,0 +1,35 @@ +// +// CameraViewController.swift +// Experiences +// +// Created by Kevin Stewart on 7/17/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import UIKit +import AVFoundation +import AVKit + +class CameraViewController: UIViewController { + + // MARK: - Outlets + @IBOutlet var cameraButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/Experiences/Experiences/Views/ExperienceTableViewController.swift b/Experiences/Experiences/Views/ExperienceTableViewController.swift new file mode 100644 index 00000000..2c16e429 --- /dev/null +++ b/Experiences/Experiences/Views/ExperienceTableViewController.swift @@ -0,0 +1,78 @@ +// +// ExperienceTableViewController.swift +// Experiences +// +// Created by Kevin Stewart on 7/17/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import UIKit +import MapKit + +class Experience: NSObject { + + var name: String + var url: URL + + init(name: String, + url: URL) { + self.name = name + self.url = url + + super.init() + } +} + +class ExperienceController { + var userLocation: CLLocationCoordinate2D? + var experiences: [Experience] = [] +} + +class ExperienceTableViewController: UITableViewController { + + var experienceController = ExperienceController() + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableView.reloadData() + } + + // MARK: - Table view data source + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + // #warning Incomplete implementation, return the number of rows + print(experienceController.experiences.count) + return experienceController.experiences.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "ExperienceCell", for: indexPath) + + let experience = experienceController.experiences[indexPath.row] + cell.textLabel?.text = experience.name + cell.detailTextLabel?.text = "\(Date())" + + return cell + } + + // MARK: - Navigation + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "RecordingSegue" { + guard let addRecordingVC = segue.destination as? RecordingViewController else { return } + addRecordingVC.delegate = self + } else if segue.identifier == "PhotoSegue" { + guard let addPhotoVC = segue.destination as? PhotoViewController else { return } + addPhotoVC.delegate = self + } else if segue.identifier == "ExperienceMapSegue" { + guard let experienceMapVC = segue.destination as? MapViewController else { return } + experienceMapVC.experienceController = self.experienceController + } + } +} +extension ExperienceTableViewController: AddExperienceDelegate { + func experienceWasAdded(experience: Experience) { + experienceController.experiences.append(experience) + tableView.reloadData() + } +} diff --git a/Experiences/Experiences/Views/MapViewController.swift b/Experiences/Experiences/Views/MapViewController.swift new file mode 100644 index 00000000..0a74d8c9 --- /dev/null +++ b/Experiences/Experiences/Views/MapViewController.swift @@ -0,0 +1,157 @@ +// +// MapViewController.swift +// Experiences +// +// Created by Kevin Stewart on 7/17/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import UIKit +import MapKit +import CoreLocation + +var currentLocation: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, + longitude: 0) + +class MapViewController: UIViewController { + + //MARK: - Outlets + @IBOutlet var experienceMapView: MKMapView! + + //MARK: - Properties + private var userTrackingButton: MKUserTrackingButton! + let locationManager = CLLocationManager() + let regionInMeters: Double = 10_000 + var experienceController: ExperienceController? + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + experienceMapView.delegate = self + experienceMapView.register(MKMarkerAnnotationView.self, + forAnnotationViewWithReuseIdentifier: .annotationReuseIdentifier) + guard let experiences = experienceController?.experiences else { + print("Did not unwrap experiences.") + return + } + print("Name for first annotation: \(experiences.first?.name)") + experienceMapView.addAnnotations(experiences) + print("The count of experiences: \(experienceController?.experiences.count)") + } + + override func viewDidLoad() { + super.viewDidLoad() + locationManager.requestWhenInUseAuthorization() + + userTrackingButton = MKUserTrackingButton(mapView: experienceMapView) + userTrackingButton.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(userTrackingButton) + + NSLayoutConstraint.activate([ + userTrackingButton.leadingAnchor.constraint(equalTo: experienceMapView.leadingAnchor, + constant: 20), + experienceMapView.bottomAnchor.constraint(equalTo: userTrackingButton.bottomAnchor, + constant: 20) + ]) + + experienceMapView.delegate = self + checkLocationServices() + experienceMapView.register(MKMarkerAnnotationView.self, + forAnnotationViewWithReuseIdentifier: .annotationReuseIdentifier) + guard let experiences = experienceController?.experiences else { + print("Did not unwrap experiences.") + return + } + experienceMapView.addAnnotations(experiences) + } + + func checkLocationServices() { + if CLLocationManager.locationServicesEnabled() { + // setup location manager + setupLocationManager() + checkLocationAuthorization() + } else { + // Show alert letting user know they have to turn this on + print("Go to your Settings > Privacy > Location Services > turn on") + } + } + + func setupLocationManager() { + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBest + } + + func checkLocationAuthorization() { + switch CLLocationManager.authorizationStatus() { + case .authorizedWhenInUse: + experienceMapView.showsUserLocation = true + centerViewOnUserLocation() + locationManager.startUpdatingLocation() + break + case .denied: + break + case .notDetermined: + locationManager.requestWhenInUseAuthorization() + case .restricted: + break + case .authorizedAlways: + break + default: + break + } + } + + func centerViewOnUserLocation() { + if let location = locationManager.location?.coordinate { + let region = MKCoordinateRegion.init(center: location, + latitudinalMeters: regionInMeters, + longitudinalMeters: regionInMeters) + experienceMapView.setRegion(region, animated: true) + print(currentLocation) + currentLocation = location + print(currentLocation) + } + } +} +// +extension MapViewController: MKMapViewDelegate { + func mapView(_ mapView: MKMapView, + viewFor annotation: MKAnnotation) -> MKAnnotationView? { + guard let experience = annotation as? Experience else { return nil } + + guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: .annotationReuseIdentifier, + for: experience) as? MKMarkerAnnotationView else { + fatalError("Missing the registered map annotation view") + } + annotationView.canShowCallout = true + + print(annotationView) + return annotationView + } +} +extension MapViewController: CLLocationManagerDelegate { + + // Updates users location + func locationManager(_ manager: CLLocationManager, + didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last else { return } + + let center = CLLocationCoordinate2D(latitude: location.coordinate.latitude, + longitude: location.coordinate.longitude) + let span = MKCoordinateSpan(latitudeDelta: 0.07, + longitudeDelta: 0.07) + let region = MKCoordinateRegion(center: center, span: span) + + experienceMapView.setRegion(region, animated: true) + locationManager.stopUpdatingLocation() + } + + // Alerts when authorization changes + func locationManager(_ manager: CLLocationManager, + didChangeAuthorization status: CLAuthorizationStatus) { + checkLocationAuthorization() + } +} + +extension String { + static let annotationReuseIdentifier = "Experience Annotation" +} diff --git a/Experiences/Experiences/Views/PhotoViewController.swift b/Experiences/Experiences/Views/PhotoViewController.swift new file mode 100644 index 00000000..0b599b8f --- /dev/null +++ b/Experiences/Experiences/Views/PhotoViewController.swift @@ -0,0 +1,177 @@ +// +// PhotoViewController.swift +// Experiences +// +// Created by Kevin Stewart on 7/17/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import UIKit +import CoreImage +import CoreImage.CIFilterBuiltins +import Photos + +protocol AddExperienceDelegate { + func experienceWasAdded(experience: Experience) +} + +class PhotoViewController: UIViewController { + + //MARK: - Outlets + @IBOutlet var imageView: UIImageView! + @IBOutlet var imageSlider: UISlider! + @IBOutlet var saveButton: UIBarButtonItem! + @IBOutlet var choosePhotoButton: UIBarButtonItem! + @IBOutlet var titleTextField: UITextField! + + //MARK: - Properties and computed properties + var experienceController: ExperienceController? + var experience: Experience? { + didSet { + updateViews() + } + } + + var imageURL: URL? + + var origionalImage: UIImage? { + didSet { + guard let origionalImage = origionalImage else { + scaledImage = nil + return + } + + let scale = UIScreen.main.scale + + var scaledSize = imageView.bounds.size + scaledSize = CGSize(width: scaledSize.width * scale, + height: scaledSize.height * scale) + guard let scaledUIImage = origionalImage.imageByScaling(toSize: scaledSize) else { + scaledImage = nil + return + } + + scaledImage = CIImage(image: scaledUIImage) + } + } + + var scaledImage: CIImage? { + didSet { + updateImage() + } + } + + var delegate: AddExperienceDelegate? + private let context = CIContext() + private let sepiaToneFilter = CIFilter.sepiaTone() + + override func viewDidLoad() { + super.viewDidLoad() + origionalImage = imageView.image + } + + //MARK: - Methods + + 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("jpeg") + + return file + } + + private func updateViews() { + titleTextField.text = experience?.title + } + + private func image(byFiltering inputImage: CIImage) -> UIImage? { + sepiaToneFilter.inputImage = inputImage + sepiaToneFilter.intensity = imageSlider.value + + guard let outputImage = sepiaToneFilter.outputImage else { return nil } + guard let renderCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + + return UIImage(cgImage: renderCGImage) + } + + private func updateImage() { + if let scaledImage = scaledImage { + imageView.image = image(byFiltering: scaledImage) + } else { + imageView.image = nil + } + } + + private func presentImagePickerController() { + guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { + print("Could not access photo library.") + return + } + let imagePicker = UIImagePickerController() + imagePicker.sourceType = .photoLibrary + imagePicker.delegate = self + + present(imagePicker, animated: true, completion: nil) + } + // MARK: Actions + + @IBAction func choosePhotoButtonPressed(_ sender: Any) { + presentImagePickerController() + } + + @IBAction func savePhotoButtonPressed(_ sender: UIBarButtonItem) { + guard let originalImage = origionalImage?.flattened, + let ciImage = CIImage(image: originalImage) else { return } + + guard let processedImage = image(byFiltering: ciImage) else { return } + + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAsset(from: processedImage) + }) { (success, error) in + if let error = error { + print("Error saving photo: \(error)") + return + } else { + DispatchQueue.main.async { + self.presentSuccessfulSaveAlert() + } + } + } + guard let newPhotoTitle = titleTextField.text, + !newPhotoTitle.isEmpty else { return } + let imageURL = createNewRecordingURL() + delegate?.experienceWasAdded(experience: Experience(name: newPhotoTitle, + url: imageURL)) + navigationController?.popToRootViewController(animated: true) + } + + private func presentSuccessfulSaveAlert() { + let alert = UIAlertController(title: "Photo Saved!", message: "The photo has been saved to your Photo Library!", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + present(alert, animated: true, completion: nil) + navigationController?.popToRootViewController(animated: true) + } + + // MARK: Slider events + + @IBAction func brightnessChanged(_ sender: UISlider) { + updateImage() + } +} + +extension PhotoViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + if let image = info[.editedImage] as? UIImage { + origionalImage = image + } else if let image = info[.originalImage] as? UIImage { + origionalImage = image + } + picker.dismiss(animated: true, completion: nil) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true, completion: nil) + } +} diff --git a/Experiences/Experiences/Views/RecordingViewController.swift b/Experiences/Experiences/Views/RecordingViewController.swift new file mode 100644 index 00000000..d416667b --- /dev/null +++ b/Experiences/Experiences/Views/RecordingViewController.swift @@ -0,0 +1,326 @@ +// +// RecordingViewController.swift +// Experiences +// +// Created by Kevin Stewart on 7/17/20. +// Copyright © 2020 Kevin Stewart. All rights reserved. +// + +import UIKit +import AVFoundation +import CoreLocation + + +class RecordingViewController: UIViewController { + + //MARK: - Outlets + @IBOutlet var playButton: UIButton! + @IBOutlet var timeRemainingLabel: UILabel! + @IBOutlet var timeElapsedLabel: UILabel! + @IBOutlet var timeSlider: UISlider! + @IBOutlet var recordButton: UIButton! + @IBOutlet var titleTextField: UITextField! + @IBOutlet var saveButton: UIBarButtonItem! + + //MARK: - Properties + var audioPlayer: AVAudioPlayer? { + didSet { + guard let audioPlayer = audioPlayer else { return } + audioPlayer.delegate = self + audioPlayer.isMeteringEnabled = true + updateViews() + } + } + var location: LocationService? + var mapViewController: MapViewController? + var delegate: AddExperienceDelegate! + var experienceController: ExperienceController? + var experience: Experience? { + didSet { + updateViews() + } + } + weak var timer: Timer? + var recordingURL: URL? + var audioRecorder: AVAudioRecorder? + + // Formatter for time time remaining and time elapsed + private lazy var timeIntervalFormatter: DateComponentsFormatter = { + // NOTE: DateComponentFormatter is good for minutes/hours/seconds + // DateComponentsFormatter is not good for milliseconds, use DateFormatter instead) + + let formatting = DateComponentsFormatter() + formatting.unitsStyle = .positional // 00:00 mm:ss + formatting.zeroFormattingBehavior = .pad + formatting.allowedUnits = [.minute, .second] + return formatting + }() + + // MARK: - View Controller Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + timeElapsedLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeElapsedLabel.font.pointSize, + weight: .regular) + timeRemainingLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeRemainingLabel.font.pointSize, + weight: .regular) + updateViews() + } + + //MARK: - Actions + @IBAction func togglePlayback(_ sender: Any) { + if isPlaying { + pause() + } else { + play() + } + } + + @IBAction func updateCurrentTime(_ sender: UISlider) { + if isPlaying { + pause() + } + + audioPlayer?.currentTime = TimeInterval(sender.value) + updateViews() + } + + @IBAction func toggleRecording(_ sender: Any) { + if isRecording { + stopRecording() + } else { + requestPermissionOrStartRecording() + } + } + + @IBAction func saveButtonTapped(_ sender: UIBarButtonItem) { + guard let newRecordingTitle = titleTextField.text, + !newRecordingTitle.isEmpty, + let recording = recordingURL else { return } + delegate.experienceWasAdded(experience: Experience(name: newRecordingTitle, + url: recording)) + navigationController?.popToRootViewController(animated: true) + } + + //MARK: - Methods + func updateViews() { + guard isViewLoaded else { return } + playButton.isEnabled = !isRecording + recordButton.isEnabled = !isPlaying + timeSlider.isEnabled = !isRecording + + playButton.isSelected = isPlaying + recordButton.isSelected = isRecording + titleTextField.text = experience?.title + + if !isRecording { + let elapsedTime = audioPlayer?.currentTime ?? 0 + let duration = audioPlayer?.duration ?? 0 + let timeRemaining = duration.rounded() - elapsedTime + + timeElapsedLabel.text = timeIntervalFormatter.string(for: elapsedTime) + + timeSlider.minimumValue = 0 + timeSlider.maximumValue = Float(duration) + timeSlider.value = Float(elapsedTime) + + timeRemainingLabel.text = "-" + timeIntervalFormatter.string(from: timeRemaining)! + } else { + let elapsedtime = audioRecorder?.currentTime ?? 0 + + timeSlider.minimumValue = 0 + timeSlider.maximumValue = 1 + timeSlider.value = 0 + + timeElapsedLabel.text = "--:--" + timeRemainingLabel.text = timeIntervalFormatter.string(from: elapsedtime) + } + } + + deinit { + timer?.invalidate() + } + + // Timer + func startTimer() { + timer?.invalidate() + + timer = Timer.scheduledTimer(withTimeInterval: 0.030, repeats: true) { [weak self] (_) in + guard let self = self else { return } + + self.updateViews() + + if let audioRecorder = self.audioRecorder, + self.isRecording == true { + + audioRecorder.updateMeters() + + } + + if let audioPlayer = self.audioPlayer, + self.isPlaying == true { + + audioPlayer.updateMeters() + } + } + } + + func cancelTimer() { + timer?.invalidate() + timer = nil + } + + // Playback + + var isPlaying: Bool { + audioPlayer?.isPlaying ?? false + } + + func loadAudio() { + let songURL = Bundle.main.url(forResource: "piano", withExtension: "mp3")! + if let experience = experience { + audioPlayer = try? AVAudioPlayer(contentsOf: experience.url) + return + } + audioPlayer = try? AVAudioPlayer(contentsOf: songURL) + + } + + + func prepareAudioSession() throws { + let session = AVAudioSession.sharedInstance() + try session.setCategory(.playAndRecord, options: [.defaultToSpeaker]) + try session.setActive(true, options: []) // can fail if on a phone call, for instance + } + + + func play() { + do { + try prepareAudioSession() + audioPlayer?.play() + updateViews() + startTimer() + } catch { + print("Cannot play audio: \(error)") + } + } + + func pause() { + audioPlayer?.pause() + updateViews() + 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") + // caf- Core Audio Folder + + // 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!") + // NOTE: Invite the user to tap record again, since we just interrupted them, and they may not have been ready to record + } + case .denied: + print("Microphone access has been blocked.") + + let alertController = UIAlertController(title: "Microphone Access Denied", message: "Please allow this app to access your Microphone.", preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "Open Settings", style: .default) { (_) in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + }) + + alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil)) + + present(alertController, animated: true, completion: nil) + case .granted: + startRecording() + @unknown default: + break + } + } + + + func startRecording() { + do { + try prepareAudioSession() + } catch { + print("Cannot record audio: \(error)") + return + } + + let 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() + self.recordingURL = recordingURL + updateViews() + startTimer() + } catch { + preconditionFailure("The audio recorder could not be created with \(recordingURL) and \(format)") + } + } + + func stopRecording() { + audioRecorder?.stop() + updateViews() + cancelTimer() + } +} + + +extension RecordingViewController: AVAudioPlayerDelegate { + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + updateViews() + cancelTimer() + } + + func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + if let error = error { + print("Audio Player Error: \(error)") + } + } +} + + +extension RecordingViewController: AVAudioRecorderDelegate { + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if let recordingURL = recordingURL { + audioPlayer = try? AVAudioPlayer(contentsOf: recordingURL) + } + + audioRecorder = nil + cancelTimer() + } + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + if let error = error { + print("Audio Recorder Error: \(error)") + } + } +}