From 327f1b0eb399a8eca527b4607ce6e5891496570c Mon Sep 17 00:00:00 2001 From: Robert Vance Date: Fri, 30 Oct 2020 21:53:18 -0600 Subject: [PATCH 1/4] Completed Project? --- .../project.pbxproj | 350 +++++++++++++ .../ImageFilterEditor/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 147 ++++++ .../ImagePostViewController.swift | 153 ++++++ .../ImageFilterEditor/Info.plist | 70 +++ .../ImageFilterEditor/SceneDelegate.swift | 52 ++ .../ImageFilterEditor/UIImage+Scaling.swift | 37 ++ LambdaTimeline.xcodeproj/project.pbxproj | 448 ++++++++++++++++ .../xcschemes/LambdaTimeline.xcscheme | 78 +++ .../Helpers/Extensions/AudioVisualizer.swift | 275 ++++++++++ .../Helpers/Extensions/UIImage+Ratio.swift | 15 + .../UIViewController+InformationalAlert.swift | 21 + .../Helpers/ShiftableViewController.swift | 126 +++++ .../Model Controllers/PostController.swift | 43 ++ LambdaTimeline/Models/Comment.swift | 37 ++ LambdaTimeline/Models/Post.swift | 44 ++ LambdaTimeline/Resources/AppDelegate.swift | 20 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../Resources/Assets.xcassets/Contents.json | 6 + LambdaTimeline/Resources/Info.plist | 58 +++ .../Base.lproj/LaunchScreen.storyboard | 25 + LambdaTimeline/Storyboards/Main.storyboard | 483 ++++++++++++++++++ .../AudioCommentViewController.swift | 295 +++++++++++ .../ImagePostDetailTableViewController.swift | 114 +++++ .../ImagePostViewController.swift | 119 +++++ .../PostsCollectionViewController.swift | 100 ++++ .../SignInViewController.swift | 44 ++ .../Views/ImagePostCollectionViewCell.swift | 55 ++ 32 files changed, 3489 insertions(+) create mode 100644 ImageFilterEditor/ImageFilterEditor.xcodeproj/project.pbxproj create mode 100644 ImageFilterEditor/ImageFilterEditor/AppDelegate.swift create mode 100644 ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ImageFilterEditor/ImageFilterEditor/Assets.xcassets/Contents.json create mode 100644 ImageFilterEditor/ImageFilterEditor/Base.lproj/LaunchScreen.storyboard create mode 100644 ImageFilterEditor/ImageFilterEditor/Base.lproj/Main.storyboard create mode 100644 ImageFilterEditor/ImageFilterEditor/ImagePostViewController.swift create mode 100644 ImageFilterEditor/ImageFilterEditor/Info.plist create mode 100644 ImageFilterEditor/ImageFilterEditor/SceneDelegate.swift create mode 100644 ImageFilterEditor/ImageFilterEditor/UIImage+Scaling.swift create mode 100644 LambdaTimeline.xcodeproj/project.pbxproj create mode 100644 LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme create mode 100644 LambdaTimeline/Helpers/Extensions/AudioVisualizer.swift create mode 100644 LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift create mode 100644 LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift create mode 100644 LambdaTimeline/Helpers/ShiftableViewController.swift create mode 100644 LambdaTimeline/Model Controllers/PostController.swift create mode 100644 LambdaTimeline/Models/Comment.swift create mode 100644 LambdaTimeline/Models/Post.swift create mode 100644 LambdaTimeline/Resources/AppDelegate.swift create mode 100644 LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 LambdaTimeline/Resources/Assets.xcassets/Contents.json create mode 100644 LambdaTimeline/Resources/Info.plist create mode 100644 LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard create mode 100644 LambdaTimeline/Storyboards/Main.storyboard create mode 100644 LambdaTimeline/View Controllers/AudioCommentViewController.swift create mode 100644 LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift create mode 100644 LambdaTimeline/View Controllers/ImagePostViewController.swift create mode 100644 LambdaTimeline/View Controllers/PostsCollectionViewController.swift create mode 100644 LambdaTimeline/View Controllers/SignInViewController.swift create mode 100644 LambdaTimeline/Views/ImagePostCollectionViewCell.swift diff --git a/ImageFilterEditor/ImageFilterEditor.xcodeproj/project.pbxproj b/ImageFilterEditor/ImageFilterEditor.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c7746f55 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor.xcodeproj/project.pbxproj @@ -0,0 +1,350 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + E41CA4A5254A5B570053699D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41CA4A4254A5B570053699D /* AppDelegate.swift */; }; + E41CA4A7254A5B570053699D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41CA4A6254A5B570053699D /* SceneDelegate.swift */; }; + E41CA4AC254A5B570053699D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E41CA4AA254A5B570053699D /* Main.storyboard */; }; + E41CA4AE254A5B580053699D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E41CA4AD254A5B580053699D /* Assets.xcassets */; }; + E41CA4B1254A5B580053699D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E41CA4AF254A5B580053699D /* LaunchScreen.storyboard */; }; + E45CAC9E254A5BC900CAFE8D /* ImagePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45CAC9D254A5BC900CAFE8D /* ImagePostViewController.swift */; }; + E45CACA1254A5EB100CAFE8D /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45CACA0254A5EB100CAFE8D /* UIImage+Scaling.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + E41CA4A1254A5B570053699D /* ImageFilterEditor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImageFilterEditor.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E41CA4A4254A5B570053699D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + E41CA4A6254A5B570053699D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + E41CA4AB254A5B570053699D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + E41CA4AD254A5B580053699D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E41CA4B0254A5B580053699D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + E41CA4B2254A5B580053699D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E45CAC9D254A5BC900CAFE8D /* ImagePostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostViewController.swift; sourceTree = ""; }; + E45CACA0254A5EB100CAFE8D /* UIImage+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Scaling.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E41CA49E254A5B570053699D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E41CA498254A5B570053699D = { + isa = PBXGroup; + children = ( + E41CA4A3254A5B570053699D /* ImageFilterEditor */, + E41CA4A2254A5B570053699D /* Products */, + ); + sourceTree = ""; + }; + E41CA4A2254A5B570053699D /* Products */ = { + isa = PBXGroup; + children = ( + E41CA4A1254A5B570053699D /* ImageFilterEditor.app */, + ); + name = Products; + sourceTree = ""; + }; + E41CA4A3254A5B570053699D /* ImageFilterEditor */ = { + isa = PBXGroup; + children = ( + E41CA4A4254A5B570053699D /* AppDelegate.swift */, + E41CA4A6254A5B570053699D /* SceneDelegate.swift */, + E41CA4AA254A5B570053699D /* Main.storyboard */, + E41CA4AD254A5B580053699D /* Assets.xcassets */, + E41CA4AF254A5B580053699D /* LaunchScreen.storyboard */, + E41CA4B2254A5B580053699D /* Info.plist */, + E45CAC9D254A5BC900CAFE8D /* ImagePostViewController.swift */, + E45CACA0254A5EB100CAFE8D /* UIImage+Scaling.swift */, + ); + path = ImageFilterEditor; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E41CA4A0254A5B570053699D /* ImageFilterEditor */ = { + isa = PBXNativeTarget; + buildConfigurationList = E41CA4B5254A5B580053699D /* Build configuration list for PBXNativeTarget "ImageFilterEditor" */; + buildPhases = ( + E41CA49D254A5B570053699D /* Sources */, + E41CA49E254A5B570053699D /* Frameworks */, + E41CA49F254A5B570053699D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ImageFilterEditor; + productName = ImageFilterEditor; + productReference = E41CA4A1254A5B570053699D /* ImageFilterEditor.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E41CA499254A5B570053699D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1200; + LastUpgradeCheck = 1200; + TargetAttributes = { + E41CA4A0254A5B570053699D = { + CreatedOnToolsVersion = 12.0.1; + }; + }; + }; + buildConfigurationList = E41CA49C254A5B570053699D /* Build configuration list for PBXProject "ImageFilterEditor" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E41CA498254A5B570053699D; + productRefGroup = E41CA4A2254A5B570053699D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E41CA4A0254A5B570053699D /* ImageFilterEditor */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E41CA49F254A5B570053699D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E41CA4B1254A5B580053699D /* LaunchScreen.storyboard in Resources */, + E41CA4AE254A5B580053699D /* Assets.xcassets in Resources */, + E41CA4AC254A5B570053699D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E41CA49D254A5B570053699D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E41CA4A5254A5B570053699D /* AppDelegate.swift in Sources */, + E45CACA1254A5EB100CAFE8D /* UIImage+Scaling.swift in Sources */, + E45CAC9E254A5BC900CAFE8D /* ImagePostViewController.swift in Sources */, + E41CA4A7254A5B570053699D /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + E41CA4AA254A5B570053699D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E41CA4AB254A5B570053699D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + E41CA4AF254A5B580053699D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E41CA4B0254A5B580053699D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + E41CA4B3254A5B580053699D /* 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.0; + 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; + }; + E41CA4B4254A5B580053699D /* 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.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + E41CA4B6254A5B580053699D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4487ZJU795; + INFOPLIST_FILE = ImageFilterEditor/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.robvance.ImageFilterEditor; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E41CA4B7254A5B580053699D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4487ZJU795; + INFOPLIST_FILE = ImageFilterEditor/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.robvance.ImageFilterEditor; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E41CA49C254A5B570053699D /* Build configuration list for PBXProject "ImageFilterEditor" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E41CA4B3254A5B580053699D /* Debug */, + E41CA4B4254A5B580053699D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E41CA4B5254A5B580053699D /* Build configuration list for PBXNativeTarget "ImageFilterEditor" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E41CA4B6254A5B580053699D /* Debug */, + E41CA4B7254A5B580053699D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E41CA499254A5B570053699D /* Project object */; +} diff --git a/ImageFilterEditor/ImageFilterEditor/AppDelegate.swift b/ImageFilterEditor/ImageFilterEditor/AppDelegate.swift new file mode 100644 index 00000000..e3916d4d --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// ImageFilterEditor +// +// Created by Rob Vance on 10/28/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/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AccentColor.colorset/Contents.json b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AppIcon.appiconset/Contents.json b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/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/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/Contents.json b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFilterEditor/ImageFilterEditor/Base.lproj/LaunchScreen.storyboard b/ImageFilterEditor/ImageFilterEditor/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageFilterEditor/ImageFilterEditor/Base.lproj/Main.storyboard b/ImageFilterEditor/ImageFilterEditor/Base.lproj/Main.storyboard new file mode 100644 index 00000000..5ea3b610 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Base.lproj/Main.storyboard @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageFilterEditor/ImageFilterEditor/ImagePostViewController.swift b/ImageFilterEditor/ImageFilterEditor/ImagePostViewController.swift new file mode 100644 index 00000000..a6d0b567 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/ImagePostViewController.swift @@ -0,0 +1,153 @@ +// +// ImagePostViewController.swift +// ImageFilterEditor +// +// Created by Rob Vance on 10/28/20. +// + +import UIKit +import CoreImage +import CoreImage.CIFilterBuiltins +import Photos + +class ImagePostViewController: UIViewController { + + // MARK: - IBOutlets - + @IBOutlet weak var brightnessSlider: UISlider! + @IBOutlet weak var contrastSlider: UISlider! + @IBOutlet weak var blurSlider: UISlider! + @IBOutlet weak var saturationSlider: UISlider! + @IBOutlet weak var vignetteSlider: UISlider! + @IBOutlet weak var choosePhotoButton: UIButton! + @IBOutlet weak var imageView: UIImageView! + + // Mark: - Properties - + + private var orignalImage: UIImage? { + didSet { + guard let orignalImage = orignalImage else { + scaledImage = nil + return + } + + var scaledSize = imageView.bounds.size + let scale = imageView.contentScaleFactor + + scaledSize.width *= scale + scaledSize.height *= scale + guard let scaledUIImage = orignalImage.imageByScaling(toSize: scaledSize) else { + scaledImage = nil + return + } + scaledImage = CIImage(image: scaledUIImage) + } + } + private var scaledImage: CIImage? { + didSet { + updateImage() + } + } + private let context = CIContext() + private let colorControlsFilter = CIFilter.colorControls() + private let blurFilter = CIFilter.gaussianBlur() + + + override func viewDidLoad() { + super.viewDidLoad() + orignalImage = imageView.image + } + + + private func image(byFiltering image: CIImage) -> UIImage? { + let inputImage = image + + colorControlsFilter.inputImage = inputImage + colorControlsFilter.saturation = saturationSlider.value + colorControlsFilter.brightness = brightnessSlider.value + colorControlsFilter.contrast = contrastSlider.value + + // blur filter + let blurFilter = CIFilter.gaussianBlur() + blurFilter.inputImage = colorControlsFilter.outputImage?.clampedToExtent() + blurFilter.radius = blurSlider.value + + // vignette filter + let vignetteFilter = CIFilter.vignette() + vignetteFilter.inputImage = blurFilter.outputImage?.clampedToExtent() + vignetteFilter.intensity = vignetteSlider.value + vignetteFilter.radius = vignetteSlider.value + + guard let outputImage = blurFilter.outputImage else { return nil } + + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + + return UIImage(cgImage: renderedCGImage) + } + + private func updateImage() { + if let scaledImage = scaledImage { + imageView.image = image(byFiltering: scaledImage) + } else { + imageView.image = nil + } + } + + // MARK: - IBActions - + + @IBAction func choosePhotoTapped(_ sender: Any) { + guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { + print("Photo library is not available.") + return + } + let imagePicker = UIImagePickerController() + imagePicker.sourceType = .photoLibrary + imagePicker.delegate = self + + present(imagePicker, animated: true) + } + + @IBAction func savePhotoTapped(_ sender: Any) { + + } + + // MARK: - Sliders - + @IBAction func brightnessChanged(_ sender: Any) { + updateImage() + } + + @IBAction func contrastChanged(_ sender: Any) { + updateImage() + } + + @IBAction func saturationChanged(_ sender: Any) { + updateImage() + } + + @IBAction func blurChanged(_ sender: Any) { + updateImage() + } + + @IBAction func vignetteChanged(_ sender: Any) { + updateImage() + } + +} + +extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + if let image = info[.editedImage] as? UIImage { + orignalImage = image + } else if let image = info[.originalImage] as? UIImage { + orignalImage = image + } + + picker.dismiss(animated: true, completion: nil) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true, completion: nil) + } + +} diff --git a/ImageFilterEditor/ImageFilterEditor/Info.plist b/ImageFilterEditor/ImageFilterEditor/Info.plist new file mode 100644 index 00000000..d0141709 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME) needs access to your library to filter images + NSPhotoLibraryAddUsageDescription + $(PRODUCT_NAME)needs access to your library to save filtered images + 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 + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ImageFilterEditor/ImageFilterEditor/SceneDelegate.swift b/ImageFilterEditor/ImageFilterEditor/SceneDelegate.swift new file mode 100644 index 00000000..7f093900 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// ImageFilterEditor +// +// Created by Rob Vance on 10/28/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/ImageFilterEditor/ImageFilterEditor/UIImage+Scaling.swift b/ImageFilterEditor/ImageFilterEditor/UIImage+Scaling.swift new file mode 100644 index 00000000..9cc56207 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/UIImage+Scaling.swift @@ -0,0 +1,37 @@ +// +// UIImage+Scaling.swift +// ImageFilterEditor +// +// Created by Rob Vance on 10/28/20. +// + +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/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj new file mode 100644 index 00000000..18c2b005 --- /dev/null +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -0,0 +1,448 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */; }; + 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46463782216FDE4C00E7FF73 /* Assets.xcassets */; }; + 46463786216FDE4C00E7FF73 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */; }; + 46463790216FFD1B00E7FF73 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646378F216FFD1B00E7FF73 /* Post.swift */; }; + 46463792216FFDD900E7FF73 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46463791216FFDD900E7FF73 /* Comment.swift */; }; + 4646379C2170091A00E7FF73 /* PostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646379B2170091A00E7FF73 /* PostController.swift */; }; + 46A0366A21700F5100E7FF73 /* PostsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */; }; + 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A0366C2170158900E7FF73 /* SignInViewController.swift */; }; + 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */; }; + 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */; }; + 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */; }; + 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */; }; + 46D1A48924FF0BC4008D1CA7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46D1A48824FF0BC4008D1CA7 /* Main.storyboard */; }; + 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */; }; + 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */; }; + E4EC7FDB254D0E9200499DC0 /* AudioVisualizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */; }; + E4F93081254D00870093AFF1 /* AudioCommentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LambdaTimeline.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 46463782216FDE4C00E7FF73 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 46463785216FDE4C00E7FF73 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 46463787216FDE4C00E7FF73 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4646378F216FFD1B00E7FF73 /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; + 46463791216FFDD900E7FF73 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + 4646379B2170091A00E7FF73 /* PostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostController.swift; sourceTree = ""; }; + 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsCollectionViewController.swift; sourceTree = ""; }; + 46A0366C2170158900E7FF73 /* SignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; + 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostViewController.swift; sourceTree = ""; }; + 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+InformationalAlert.swift"; sourceTree = ""; }; + 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShiftableViewController.swift; sourceTree = ""; }; + 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostCollectionViewCell.swift; sourceTree = ""; }; + 46D1A48824FF0BC4008D1CA7 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; + 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Ratio.swift"; sourceTree = ""; }; + 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostDetailTableViewController.swift; sourceTree = ""; }; + E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVisualizer.swift; sourceTree = ""; }; + E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioCommentViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 46463775216FDE4B00E7FF73 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4646376F216FDE4B00E7FF73 = { + isa = PBXGroup; + children = ( + 4646377A216FDE4B00E7FF73 /* LambdaTimeline */, + 46463779216FDE4B00E7FF73 /* Products */, + ); + sourceTree = ""; + }; + 46463779216FDE4B00E7FF73 /* Products */ = { + isa = PBXGroup; + children = ( + 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */, + ); + name = Products; + sourceTree = ""; + }; + 4646377A216FDE4B00E7FF73 /* LambdaTimeline */ = { + isa = PBXGroup; + children = ( + 46CFE7042171572900E7FF73 /* Views */, + 46A0366B21700F6100E7FF73 /* View Controllers */, + 4646379A2170090900E7FF73 /* Model Controllers */, + 46463795216FFF5800E7FF73 /* Models */, + 46463797216FFF7400E7FF73 /* Storyboards */, + 46CFE7062171573F00E7FF73 /* Helpers */, + 46463796216FFF6900E7FF73 /* Resources */, + ); + path = LambdaTimeline; + sourceTree = ""; + }; + 46463795216FFF5800E7FF73 /* Models */ = { + isa = PBXGroup; + children = ( + 4646378F216FFD1B00E7FF73 /* Post.swift */, + 46463791216FFDD900E7FF73 /* Comment.swift */, + ); + path = Models; + sourceTree = ""; + }; + 46463796216FFF6900E7FF73 /* Resources */ = { + isa = PBXGroup; + children = ( + 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */, + 46463782216FDE4C00E7FF73 /* Assets.xcassets */, + 46463787216FDE4C00E7FF73 /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 46463797216FFF7400E7FF73 /* Storyboards */ = { + isa = PBXGroup; + children = ( + 46D1A48824FF0BC4008D1CA7 /* Main.storyboard */, + 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */, + ); + path = Storyboards; + sourceTree = ""; + }; + 4646379A2170090900E7FF73 /* Model Controllers */ = { + isa = PBXGroup; + children = ( + 4646379B2170091A00E7FF73 /* PostController.swift */, + ); + path = "Model Controllers"; + sourceTree = ""; + }; + 46A0366B21700F6100E7FF73 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 46A0366C2170158900E7FF73 /* SignInViewController.swift */, + 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */, + 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, + 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, + E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 46CFE7042171572900E7FF73 /* Views */ = { + isa = PBXGroup; + children = ( + 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */, + ); + path = Views; + sourceTree = ""; + }; + 46CFE7062171573F00E7FF73 /* Helpers */ = { + isa = PBXGroup; + children = ( + 46D571F62173D6D200E7FF73 /* Extensions */, + 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 46D571F62173D6D200E7FF73 /* Extensions */ = { + isa = PBXGroup; + children = ( + E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */, + 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */, + 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */, + ); + path = Extensions; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 46463777216FDE4B00E7FF73 /* LambdaTimeline */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4646378A216FDE4C00E7FF73 /* Build configuration list for PBXNativeTarget "LambdaTimeline" */; + buildPhases = ( + 46463774216FDE4B00E7FF73 /* Sources */, + 46463775216FDE4B00E7FF73 /* Frameworks */, + 46463776216FDE4B00E7FF73 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LambdaTimeline; + productName = LambdaTimeline; + productReference = 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 46463770216FDE4B00E7FF73 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1000; + LastUpgradeCheck = 1000; + ORGANIZATIONNAME = "Lambda School"; + TargetAttributes = { + 46463777216FDE4B00E7FF73 = { + CreatedOnToolsVersion = 10.0; + LastSwiftMigration = 1130; + }; + }; + }; + buildConfigurationList = 46463773216FDE4B00E7FF73 /* Build configuration list for PBXProject "LambdaTimeline" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4646376F216FDE4B00E7FF73; + productRefGroup = 46463779216FDE4B00E7FF73 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 46463777216FDE4B00E7FF73 /* LambdaTimeline */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 46463776216FDE4B00E7FF73 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 46D1A48924FF0BC4008D1CA7 /* Main.storyboard in Resources */, + 46463786216FDE4C00E7FF73 /* LaunchScreen.storyboard in Resources */, + 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 46463774216FDE4B00E7FF73 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E4EC7FDB254D0E9200499DC0 /* AudioVisualizer.swift in Sources */, + 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */, + 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, + 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, + 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, + 46463790216FFD1B00E7FF73 /* Post.swift in Sources */, + 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */, + 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */, + 4646379C2170091A00E7FF73 /* PostController.swift in Sources */, + 46A0366A21700F5100E7FF73 /* PostsCollectionViewController.swift in Sources */, + 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */, + 46463792216FFDD900E7FF73 /* Comment.swift in Sources */, + 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.swift in Sources */, + E4F93081254D00870093AFF1 /* AudioCommentViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 46463785216FDE4C00E7FF73 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 46463788216FDE4C00E7FF73 /* 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; + CODE_SIGN_IDENTITY = "iPhone Developer"; + 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 = 12.0; + 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; + }; + 46463789216FDE4C00E7FF73 /* 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; + CODE_SIGN_IDENTITY = "iPhone Developer"; + 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4646378B216FDE4C00E7FF73 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.LambdaSchool.LambdaTimeline; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4646378C216FDE4C00E7FF73 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.LambdaSchool.LambdaTimeline; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 46463773216FDE4B00E7FF73 /* Build configuration list for PBXProject "LambdaTimeline" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 46463788216FDE4C00E7FF73 /* Debug */, + 46463789216FDE4C00E7FF73 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4646378A216FDE4C00E7FF73 /* Build configuration list for PBXNativeTarget "LambdaTimeline" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4646378B216FDE4C00E7FF73 /* Debug */, + 4646378C216FDE4C00E7FF73 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 46463770216FDE4B00E7FF73 /* Project object */; +} diff --git a/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme b/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme new file mode 100644 index 00000000..d1c2f96e --- /dev/null +++ b/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LambdaTimeline/Helpers/Extensions/AudioVisualizer.swift b/LambdaTimeline/Helpers/Extensions/AudioVisualizer.swift new file mode 100644 index 00000000..83eceaf7 --- /dev/null +++ b/LambdaTimeline/Helpers/Extensions/AudioVisualizer.swift @@ -0,0 +1,275 @@ +// +// AudioVisualizer.swift +// LambdaTimeline +// +// Created by Rob Vance on 10/30/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit + +@IBDesignable +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 = .systemGreen { + 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/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift b/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift new file mode 100644 index 00000000..79bb32d7 --- /dev/null +++ b/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift @@ -0,0 +1,15 @@ +// +// UIImage+Ratio.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/14/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +extension UIImage { + var ratio: CGFloat { + return size.height / size.width + } +} diff --git a/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift b/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift new file mode 100644 index 00000000..d4480303 --- /dev/null +++ b/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift @@ -0,0 +1,21 @@ +// +// UIViewController+InformationalAlert.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/12/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +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/LambdaTimeline/Helpers/ShiftableViewController.swift b/LambdaTimeline/Helpers/ShiftableViewController.swift new file mode 100644 index 00000000..5923d54f --- /dev/null +++ b/LambdaTimeline/Helpers/ShiftableViewController.swift @@ -0,0 +1,126 @@ +// +// Created by Spencer Curtis. +// Copyright © 2017-2018 Spencer Curtis. All rights reserved. +// + +/* + All you need to do is set your subclass of ShiftableViewController as the delegate for all + UITextFields and UITextViews that you want to be shifted up so the keyboard doesn't obscure it. + */ + +import UIKit + +class ShiftableViewController: UIViewController, UITextFieldDelegate, UITextViewDelegate, UIGestureRecognizerDelegate { + + var currentYShiftForKeyboard: CGFloat = 0 + + var textFieldBeingEdited: UITextField? + var textViewBeingEdited: UITextView? + + var keyboardDismissTapGestureRecognizer: UITapGestureRecognizer! + + override func viewDidLoad() { + super.viewDidLoad() + + setupKeyboardDismissTapGestureRecognizer() + + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + @objc func stopEditingTextInput() { + if let textField = self.textFieldBeingEdited { + + textField.resignFirstResponder() + + self.textFieldBeingEdited = nil + self.textViewBeingEdited = nil + } else if let textView = self.textViewBeingEdited { + + textView.resignFirstResponder() + + self.textFieldBeingEdited = nil + self.textViewBeingEdited = nil + } + + guard keyboardDismissTapGestureRecognizer.isEnabled else { return } + + keyboardDismissTapGestureRecognizer.isEnabled = false + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + textFieldBeingEdited = textField + } + + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + textViewBeingEdited = textView + return true + } + + @objc func keyboardWillShow(notification: Notification) { + + keyboardDismissTapGestureRecognizer.isEnabled = true + + var keyboardSize: CGRect = .zero + + if let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, + keyboardRect.height != 0 { + keyboardSize = keyboardRect + } else if let keyboardRect = notification.userInfo?["UIKeyboardBoundsUserInfoKey"] as? CGRect { + keyboardSize = keyboardRect + } + + if let textField = textFieldBeingEdited { + if self.view.frame.origin.y == 0 { + + let yShift = yShiftWhenKeyboardAppearsFor(textInput: textField, keyboardSize: keyboardSize, nextY: keyboardSize.height) + self.currentYShiftForKeyboard = yShift + self.view.frame.origin.y -= yShift + } + } else if let textView = textViewBeingEdited { + if self.view.frame.origin.y == 0 { + + let yShift = yShiftWhenKeyboardAppearsFor(textInput: textView, keyboardSize: keyboardSize, nextY: keyboardSize.height) + self.currentYShiftForKeyboard = yShift + self.view.frame.origin.y -= yShift + } + } + } + + @objc func yShiftWhenKeyboardAppearsFor(textInput: UIView, keyboardSize: CGRect, nextY: CGFloat) -> CGFloat { + + let textFieldOrigin = self.view.convert(textInput.frame, from: textInput.superview!).origin.y + let textFieldBottomY = textFieldOrigin + textInput.frame.size.height + + // This is the y point that the textField's bottom can be at before it gets covered by the keyboard + let maximumY = self.view.frame.height - (keyboardSize.height + view.safeAreaInsets.bottom) + + if textFieldBottomY > maximumY { + // This makes the view shift the right amount to have the text field being edited just above they keyboard if it would have been covered by the keyboard. + return textFieldBottomY - maximumY + } else { + // It would go off the screen if moved, and it won't be obscured by the keyboard. + return 0 + } + } + + @objc func keyboardWillHide(notification: Notification) { + + if self.view.frame.origin.y != 0 { + + self.view.frame.origin.y += currentYShiftForKeyboard + } + + stopEditingTextInput() + } + + @objc func setupKeyboardDismissTapGestureRecognizer() { + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(stopEditingTextInput)) + tapGestureRecognizer.numberOfTapsRequired = 1 + + view.addGestureRecognizer(tapGestureRecognizer) + + keyboardDismissTapGestureRecognizer = tapGestureRecognizer + } +} diff --git a/LambdaTimeline/Model Controllers/PostController.swift b/LambdaTimeline/Model Controllers/PostController.swift new file mode 100644 index 00000000..492b07ab --- /dev/null +++ b/LambdaTimeline/Model Controllers/PostController.swift @@ -0,0 +1,43 @@ +// +// PostController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class PostController { + + static let shared = PostController() + var posts: [Post] = [] + + var currentUser: String? { + UserDefaults.standard.string(forKey: "username") + } + + func createImagePost(with title: String, image: UIImage, ratio: CGFloat?, audioURL: URL?) { + + guard let currentUser = currentUser else { return } + + let post = Post(title: title, mediaType: .image(image), ratio: ratio, author: currentUser, audioURL: audioURL) + + posts.append(post) + } + + func addComment(with text: String, to post: inout Post) { + + guard let currentUser = currentUser else { return } + + let comment = Comment(text: text, author: currentUser, audioURL: nil) + post.comments.append(comment) + + } + func addAudioComment(with url: URL, to post: inout Post) { + guard let currentUser = currentUser else { return } + + let comment = Comment(author: currentUser, audioURL: url) + post.comments.append(comment) + } +} diff --git a/LambdaTimeline/Models/Comment.swift b/LambdaTimeline/Models/Comment.swift new file mode 100644 index 00000000..e8770549 --- /dev/null +++ b/LambdaTimeline/Models/Comment.swift @@ -0,0 +1,37 @@ +// +// Comment.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import Foundation + +class Comment: Hashable { + + static private let textKey = "text" + static private let authorKey = "author" + static private let timestampKey = "timestamp" + + let text: String? + let author: String + let timestamp: Date + let audioURL: URL? + + init(text: String? = nil, author: String, timestamp: Date = Date(), audioURL: URL?) { + self.text = text + self.author = author + self.timestamp = timestamp + self.audioURL = audioURL + } + + func hash(into hasher: inout Hasher) { + hasher.combine(timestamp.hashValue ^ author.hashValue) + } + + static func ==(lhs: Comment, rhs: Comment) -> Bool { + return lhs.author == rhs.author && + lhs.timestamp == rhs.timestamp + } +} diff --git a/LambdaTimeline/Models/Post.swift b/LambdaTimeline/Models/Post.swift new file mode 100644 index 00000000..f6b070e8 --- /dev/null +++ b/LambdaTimeline/Models/Post.swift @@ -0,0 +1,44 @@ +// +// Post.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +enum MediaType { + case image(UIImage) +} + +class Post: Equatable { + + let mediaType: MediaType + let author: String + let timestamp: Date + var comments: [Comment] + var ratio: CGFloat? + var id: String? + + var title: String? { + comments.first?.text + } + + var audioURL: URL? { + comments.first?.audioURL + } + + init(title: String, mediaType: MediaType, ratio: CGFloat?, author: String, timestamp: Date = Date(), audioURL: URL?) { + self.mediaType = mediaType + self.ratio = ratio + self.author = author + self.comments = [Comment(text: title, author: author, audioURL: audioURL)] + self.timestamp = timestamp + self.id = UUID().uuidString + } + + static func ==(lhs: Post, rhs: Post) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/LambdaTimeline/Resources/AppDelegate.swift b/LambdaTimeline/Resources/AppDelegate.swift new file mode 100644 index 00000000..a44cc8ca --- /dev/null +++ b/LambdaTimeline/Resources/AppDelegate.swift @@ -0,0 +1,20 @@ +// +// AppDelegate.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } +} + diff --git a/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d8db8d65 --- /dev/null +++ b/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LambdaTimeline/Resources/Assets.xcassets/Contents.json b/LambdaTimeline/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/LambdaTimeline/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist new file mode 100644 index 00000000..dab10c0a --- /dev/null +++ b/LambdaTimeline/Resources/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + Copy-paste-your-REVERSED_CLIENT_ID-from-GoogleService-Info.plist + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSPhotoLibraryUsageDescription + In order to allow you to add photos to posts + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard b/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..bfa36129 --- /dev/null +++ b/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard new file mode 100644 index 00000000..d86b272b --- /dev/null +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -0,0 +1,483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LambdaTimeline/View Controllers/AudioCommentViewController.swift b/LambdaTimeline/View Controllers/AudioCommentViewController.swift new file mode 100644 index 00000000..5f3f802e --- /dev/null +++ b/LambdaTimeline/View Controllers/AudioCommentViewController.swift @@ -0,0 +1,295 @@ +// +// AudioCommentViewController.swift +// LambdaTimeline +// +// Created by Rob Vance on 10/30/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import AVFoundation + +protocol VoiceCommentDelegate { + func reloadData() +} + +class AudioCommentViewController: UIViewController { + + // Mark: - IBOutlets - + @IBOutlet weak var timeElapsedLabel: UILabel! + @IBOutlet weak var timeRemainingLabel: UILabel! + @IBOutlet weak var timeSlider: UISlider! + @IBOutlet weak var playButton: UIButton! + @IBOutlet weak var recordButton: UIButton! + @IBOutlet weak var audioVisualizer: AudioVisualizer! + + + // Mark: - Properties - + var audioPlayer: AVAudioPlayer?{ + didSet { + guard let audioPlayer = audioPlayer else { return } + + audioPlayer.delegate = self + audioPlayer.isMeteringEnabled = true + updateViews() + } + } + + weak var timer: Timer? + var audioRecorder: AVAudioRecorder? + + let postController = PostController.shared + var post: Post! + var delegate: VoiceCommentDelegate? + var recordingURL: URL? + + 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() + + // Use a font that won't jump around as values change + timeElapsedLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeElapsedLabel.font.pointSize, + weight: .regular) + timeRemainingLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeRemainingLabel.font.pointSize, + weight: .regular) + } + + func updateViews() { + playButton.isEnabled = !isRecording + recordButton.isEnabled = !isPlaying + timeSlider.isEnabled = !isRecording + playButton.isSelected = isPlaying + recordButton.isSelected = isRecording + if !isRecording { + let elapsedTime = audioPlayer?.currentTime ?? 0 + let duration = audioPlayer?.duration ?? 0 + let timeRemaining = duration.rounded() - elapsedTime + timeElapsedLabel.text = timeIntervalFormatter.string(from: elapsedTime) + timeSlider.minimumValue = 0 + timeSlider.maximumValue = Float(duration) + timeSlider.value = Float(elapsedTime) + timeRemainingLabel.text = "-" + timeIntervalFormatter.string(from: timeRemaining)! + } else { + let elapsedTime = audioRecorder?.currentTime ?? 0 + timeElapsedLabel.text = "--:--" + timeSlider.minimumValue = 0 + timeSlider.maximumValue = 1 + timeSlider.value = 0 + timeRemainingLabel.text = timeIntervalFormatter.string(from: elapsedTime) + } + } + + deinit { + timer?.invalidate() + } + + // MARK: - 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() + 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 + } + + // MARK: - Playback + var isPlaying: Bool { + audioPlayer?.isPlaying ?? false + } + + 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("Can't play audio: \(error)") + } + } + + func pause() { + audioPlayer?.pause() + updateViews() + cancelTimer() + } + + + // MARK: - Recording + + var isRecording: Bool { + audioRecorder?.isRecording ?? false + } + + + 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 newRecordingURL() -> URL { + let fm = FileManager.default + let documentsDir = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + + let randomId = Int.random(in: 0...1_000_00) + + return documentsDir.appendingPathComponent("TestRecording" + "\(randomId)").appendingPathExtension("caf") + } + + func startRecording() { + do{ + try prepareAudioSession() + } catch { + print("Can't record audio: \(error)") + return + } + + recordingURL = newRecordingURL() + + let format = AVAudioFormat(standardFormatWithSampleRate: 44_100, channels: 1)! + do { + audioRecorder = try AVAudioRecorder(url: recordingURL!, format: format) + audioRecorder?.delegate = self + audioRecorder?.isMeteringEnabled = true + audioRecorder?.record() + updateViews() + startTimer() + } catch { + preconditionFailure("The audio recorder could not be created with \(recordingURL!) and format \(format)") + } + } + + func stopRecording() { + audioRecorder?.stop() + updateViews() + cancelTimer() + } + + @IBAction func saveRecording(_ sender: Any) { + self.postController.addAudioComment(with: recordingURL!, to: &self.post) + self.delegate?.reloadData() + } + + // 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() + } + } +} + +extension AudioCommentViewController: 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 AudioCommentViewController: AVAudioRecorderDelegate { + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if let recordingURL = recordingURL { + audioPlayer = try? AVAudioPlayer(contentsOf: recordingURL) + } + cancelTimer() + } + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + if let error = error{ + print("Recoder Player Error: \(error)") + } + } +} diff --git a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift new file mode 100644 index 00000000..d742ebd1 --- /dev/null +++ b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift @@ -0,0 +1,114 @@ +// +// ImagePostDetailTableViewController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/14/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class ImagePostDetailTableViewController: UITableViewController { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var authorLabel: UILabel! + @IBOutlet weak var imageViewAspectRatioConstraint: NSLayoutConstraint! + @IBOutlet weak var commentButton: UIBarButtonItem! + + var post: Post! + var postController: PostController! + var imageData: Data? + + override func viewDidLoad() { + super.viewDidLoad() + updateViews() + } + + func updateViews() { + + guard case MediaType.image(let image) = post.mediaType else { return } + + title = post?.title + + imageView.image = image + + titleLabel.text = post.title + authorLabel.text = post.author + } + + // MARK: - Table view data source + + @IBAction func createComment(_ sender: Any) { + + let alert = UIAlertController(title: "New comment", message: "What type of comment would you like to create?", preferredStyle: .actionSheet) + + let textPostAction = UIAlertAction(title: "Text", style: .default) { (_) in + let alert = UIAlertController(title: "add a comment", message: "Write your comment below", preferredStyle: .alert) + + var commentTextField: UITextField? + + alert.addTextField { (textField) in + textField.placeholder = "Comment:" + commentTextField = textField + } + let addCommentAction = UIAlertAction(title: "Add Comment", style: .default) { (_) in + guard let commentText = commentTextField?.text else { return } + + self.postController.addComment(with: commentText, to: &self.post!) + + DispatchQueue.main.async { + self.tableView.reloadData() + + } + } + + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alert.addAction(addCommentAction) + alert.addAction(cancelAction) + + self.present(alert, animated: true, completion: nil) + } + + let voicePostAction = UIAlertAction(title: "Voice", style: .default) { _ in + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: "AudioCommentController") as! AudioCommentViewController + viewController.post = self.post + viewController.delegate = self + self.navigationController?.pushViewController(viewController, animated: true) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alert.addAction(textPostAction) + alert.addAction(cancelAction) + alert.addAction(voicePostAction) + + self.present(alert, animated: true, completion: nil) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return (post?.comments.count ?? 0) - 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "CommentCell", for: indexPath) + + let comment = post?.comments[indexPath.row + 1] + + cell.textLabel?.text = comment?.text + cell.detailTextLabel?.text = comment?.author + + return cell + } +} + +extension ImagePostDetailTableViewController: VoiceCommentDelegate { + func reloadData() { + tableView.reloadData() + } + + +} diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift new file mode 100644 index 00000000..f5f9823a --- /dev/null +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -0,0 +1,119 @@ +// +// ImagePostViewController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/12/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit +import Photos + +class ImagePostViewController: ShiftableViewController { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleTextField: UITextField! + @IBOutlet weak var chooseImageButton: UIButton! + @IBOutlet weak var imageHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var postButton: UIBarButtonItem! + + var postController: PostController! + var post: Post? + var imageData: Data? + + override func viewDidLoad() { + super.viewDidLoad() + + setImageViewHeight(with: 1.0) + } + + private func presentImagePickerController() { + + guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { + presentInformationalAlertController(title: "Error", message: "The photo library is unavailable") + return + } + + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.sourceType = .photoLibrary + + present(imagePicker, animated: true, completion: nil) + } + + @IBAction func createPost(_ sender: Any) { + + view.endEditing(true) + + guard let image = imageView.image, + let title = titleTextField.text, title != "" else { + presentInformationalAlertController(title: "Uh-oh", message: "Make sure that you add a photo and a caption before posting.") + return + } + + postController.createImagePost(with: title, image: image, ratio: image.ratio, audioURL: nil) + + navigationController?.popViewController(animated: true) + } + + @IBAction func chooseImage(_ sender: Any) { + + let authorizationStatus = PHPhotoLibrary.authorizationStatus() + + switch authorizationStatus { + case .authorized: + presentImagePickerController() + case .notDetermined: + + PHPhotoLibrary.requestAuthorization { (status) in + + guard status == .authorized else { + NSLog("User did not authorize access to the photo library") + self.presentInformationalAlertController(title: "Error", message: "In order to access the photo library, you must allow this application access to it.") + return + } + + self.presentImagePickerController() + } + + case .denied: + self.presentInformationalAlertController(title: "Error", message: "In order to access the photo library, you must allow this application access to it.") + case .restricted: + self.presentInformationalAlertController(title: "Error", message: "Unable to access the photo library. Your device's restrictions do not allow access.") + default: + break + } + presentImagePickerController() + } + + @IBAction func addFilter(_ sender: Any) { + + } + + func setImageViewHeight(with aspectRatio: CGFloat) { + + imageHeightConstraint.constant = imageView.frame.size.width * aspectRatio + + view.layoutSubviews() + } +} + +extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + chooseImageButton.setTitle("", for: []) + + picker.dismiss(animated: true, completion: nil) + + guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return } + + imageView.image = image + + setImageViewHeight(with: image.ratio) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true, completion: nil) + } +} diff --git a/LambdaTimeline/View Controllers/PostsCollectionViewController.swift b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift new file mode 100644 index 00000000..b9ed6b54 --- /dev/null +++ b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift @@ -0,0 +1,100 @@ +// +// PostsCollectionViewController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class PostsCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { + + var postController: PostController! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + collectionView.reloadData() + } + + @IBAction func addPost(_ sender: Any) { + + let alert = UIAlertController(title: "New Post", message: "Which kind of post do you want to create?", preferredStyle: .actionSheet) + + let imagePostAction = UIAlertAction(title: "Image", style: .default) { (_) in + self.performSegue(withIdentifier: "AddImagePost", sender: nil) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alert.addAction(imagePostAction) + alert.addAction(cancelAction) + + self.present(alert, animated: true, completion: nil) + } + + // MARK: UICollectionViewDataSource + + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return postController?.posts.count ?? 0 + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + let post = postController.posts[indexPath.row] + + switch post.mediaType { + + case .image: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImagePostCell", for: indexPath) as? ImagePostCollectionViewCell else { return UICollectionViewCell() } + + cell.post = post + + return cell + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + var size = CGSize(width: view.frame.width, height: view.frame.width) + + let post = postController.posts[indexPath.row] + + guard let ratio = post.ratio else { return size } + + size.height = size.width * ratio + + return size + } + + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let cell = collectionView.cellForItem(at: indexPath) + + if let cell = cell as? ImagePostCollectionViewCell, + cell.imageView.image != nil { + self.performSegue(withIdentifier: "ViewImagePost", sender: nil) + } + } + + + // 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?) { + if segue.identifier == "AddImagePost" { + let destinationVC = segue.destination as? ImagePostViewController + destinationVC?.postController = postController + + } else if segue.identifier == "ViewImagePost" { + + let destinationVC = segue.destination as? ImagePostDetailTableViewController + + guard let indexPath = collectionView.indexPathsForSelectedItems?.first else { return } + + destinationVC?.postController = postController + destinationVC?.post = postController.posts[indexPath.row] + } + } +} diff --git a/LambdaTimeline/View Controllers/SignInViewController.swift b/LambdaTimeline/View Controllers/SignInViewController.swift new file mode 100644 index 00000000..c81bb748 --- /dev/null +++ b/LambdaTimeline/View Controllers/SignInViewController.swift @@ -0,0 +1,44 @@ +// +// SignInViewController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class SignInViewController: UIViewController { + + @IBOutlet weak var nameTextField: UITextField! + + let postController = PostController() + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + segueIfUsernameExists() + } + + @IBAction func getStarted(_ sender: Any) { + UserDefaults.standard.set(nameTextField.text, forKey: "username") + segueIfUsernameExists() + } + + func segueIfUsernameExists() { + if UserDefaults.standard.string(forKey: "username") != nil { + performSegue(withIdentifier: "ModalPostsVC", sender: nil) + } + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "ModalPostsVC" { + guard let postsVC = (segue.destination as? UINavigationController)?.topViewController as? PostsCollectionViewController else { return } + + postsVC.postController = postController + } + } +} diff --git a/LambdaTimeline/Views/ImagePostCollectionViewCell.swift b/LambdaTimeline/Views/ImagePostCollectionViewCell.swift new file mode 100644 index 00000000..f83f73ed --- /dev/null +++ b/LambdaTimeline/Views/ImagePostCollectionViewCell.swift @@ -0,0 +1,55 @@ +// +// ImagePostCollectionViewCell.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/12/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class ImagePostCollectionViewCell: UICollectionViewCell { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var authorLabel: UILabel! + @IBOutlet weak var labelBackgroundView: UIView! + + var post: Post? { + didSet { + updateViews() + } + } + + override func layoutSubviews() { + super.layoutSubviews() + setupLabelBackgroundView() + } + + override func prepareForReuse() { + super.prepareForReuse() + + imageView.image = nil + titleLabel.text = "" + authorLabel.text = "" + } + + func updateViews() { + guard let post = post, + case MediaType.image(let image) = post.mediaType else { return } + + titleLabel.text = post.title + authorLabel.text = post.author + imageView.image = image + } + + func setupLabelBackgroundView() { + labelBackgroundView.layer.cornerRadius = 8 + labelBackgroundView.clipsToBounds = true + } + + func setImage(_ image: UIImage?) { + imageView.image = image + } +} + From b446308061929d2ec4458de53cf3c099fa97dc0c Mon Sep 17 00:00:00 2001 From: Robert Vance Date: Fri, 30 Oct 2020 21:55:33 -0600 Subject: [PATCH 2/4] Added Privacy usage to plist --- LambdaTimeline/Resources/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist index dab10c0a..2f358730 100644 --- a/LambdaTimeline/Resources/Info.plist +++ b/LambdaTimeline/Resources/Info.plist @@ -12,6 +12,8 @@ 6.0 CFBundleName $(PRODUCT_NAME) + NSMotionUsageDescription + $(PRODUCT_NAME) uses the microphone to record audio CFBundlePackageType APPL CFBundleShortVersionString From abc03a43eb3de186e1ddf53d05c97eda539c954f Mon Sep 17 00:00:00 2001 From: Robert Vance Date: Thu, 5 Nov 2020 21:14:05 -0700 Subject: [PATCH 3/4] Most of Mapkit completed --- LambdaTimeline.xcodeproj/project.pbxproj | 8 ++ .../Model Controllers/PostController.swift | 5 +- LambdaTimeline/Models/CoordinateRegion.swift | 45 ++++++++++ LambdaTimeline/Models/Post.swift | 21 ++++- LambdaTimeline/Resources/Info.plist | 2 + LambdaTimeline/Storyboards/Main.storyboard | 83 +++++++++++++++++-- .../ImagePostViewController.swift | 31 ++++++- .../PostLocationViewController.swift | 41 +++++++++ 8 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 LambdaTimeline/Models/CoordinateRegion.swift create mode 100644 LambdaTimeline/View Controllers/PostLocationViewController.swift diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj index 18c2b005..c4f6b24c 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */; }; 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */; }; E4EC7FDB254D0E9200499DC0 /* AudioVisualizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */; }; + E4EC7FE82554F1D800499DC0 /* CoordinateRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EC7FE72554F1D800499DC0 /* CoordinateRegion.swift */; }; + E4EC7FEB2555023100499DC0 /* PostLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EC7FEA2555023100499DC0 /* PostLocationViewController.swift */; }; E4F93081254D00870093AFF1 /* AudioCommentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */; }; /* End PBXBuildFile section */ @@ -45,6 +47,8 @@ 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Ratio.swift"; sourceTree = ""; }; 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostDetailTableViewController.swift; sourceTree = ""; }; E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVisualizer.swift; sourceTree = ""; }; + E4EC7FE72554F1D800499DC0 /* CoordinateRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinateRegion.swift; sourceTree = ""; }; + E4EC7FEA2555023100499DC0 /* PostLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostLocationViewController.swift; sourceTree = ""; }; E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioCommentViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -94,6 +98,7 @@ children = ( 4646378F216FFD1B00E7FF73 /* Post.swift */, 46463791216FFDD900E7FF73 /* Comment.swift */, + E4EC7FE72554F1D800499DC0 /* CoordinateRegion.swift */, ); path = Models; sourceTree = ""; @@ -133,6 +138,7 @@ 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */, + E4EC7FEA2555023100499DC0 /* PostLocationViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -238,6 +244,7 @@ files = ( E4EC7FDB254D0E9200499DC0 /* AudioVisualizer.swift in Sources */, 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */, + E4EC7FEB2555023100499DC0 /* PostLocationViewController.swift in Sources */, 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, @@ -249,6 +256,7 @@ 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */, 46463792216FFDD900E7FF73 /* Comment.swift in Sources */, 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.swift in Sources */, + E4EC7FE82554F1D800499DC0 /* CoordinateRegion.swift in Sources */, E4F93081254D00870093AFF1 /* AudioCommentViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/LambdaTimeline/Model Controllers/PostController.swift b/LambdaTimeline/Model Controllers/PostController.swift index 492b07ab..98871e1f 100644 --- a/LambdaTimeline/Model Controllers/PostController.swift +++ b/LambdaTimeline/Model Controllers/PostController.swift @@ -7,6 +7,7 @@ // import UIKit +import MapKit class PostController { @@ -17,11 +18,11 @@ class PostController { UserDefaults.standard.string(forKey: "username") } - func createImagePost(with title: String, image: UIImage, ratio: CGFloat?, audioURL: URL?) { + func createImagePost(with title: String, image: UIImage, ratio: CGFloat?, audioURL: URL?, location: CLLocationCoordinate2D) { guard let currentUser = currentUser else { return } - let post = Post(title: title, mediaType: .image(image), ratio: ratio, author: currentUser, audioURL: audioURL) + let post = Post(title: title, mediaType: .image(image), ratio: ratio, author: currentUser, audioURL: audioURL, location: location) posts.append(post) } diff --git a/LambdaTimeline/Models/CoordinateRegion.swift b/LambdaTimeline/Models/CoordinateRegion.swift new file mode 100644 index 00000000..efa30edc --- /dev/null +++ b/LambdaTimeline/Models/CoordinateRegion.swift @@ -0,0 +1,45 @@ +// +// CoordinateRegion.swift +// LambdaTimeline +// +// Created by Rob Vance on 11/5/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import Foundation +import CoreLocation +import MapKit + +struct CoordinateRegion { + + init(origin: (longitude: Double, latitude: Double), size: (width: Double, height: Double)) { + self.origin = (longitude: min(max(origin.longitude, -180), 180), latitude: min(max(origin.latitude, -90), 90)) + let farPoint = (longitude: self.origin.longitude + size.width, latitude: self.origin.latitude + size.height) + self.farPoint = (longitude: min(max(farPoint.longitude, -180), 180), latitude: min(max(farPoint.latitude, -90), 90)) + } + + init(originLong: Double, originLat: Double, width: Double, height: Double) { + self.init(origin: (longitude: originLong, latitude: originLat), size: (width: width, height: height)) + } + + init(originCoordinate: CLLocationCoordinate2D, width: Double, height: Double) { + let origin = (longitude: originCoordinate.longitude, latitude: originCoordinate.latitude) + self.init(origin: origin, size: (width: width, height: height)) + } + + init(mapRect: MKMapRect) { + let region = MKCoordinateRegion(mapRect) + let origin = (longitude: region.center.longitude - region.span.longitudeDelta/2.0, + latitude: region.center.latitude - region.span.latitudeDelta/2.0) + let size = (width: region.span.longitudeDelta, height: region.span.latitudeDelta) + self.init(origin: origin, size: size) + } + + // MARK: Properties + + var origin: (longitude: Double, latitude: Double) + var farPoint: (longitude: Double, latitude: Double) + var size: (width: Double, height: Double) { + return (width: farPoint.longitude - origin.longitude, height: farPoint.latitude - origin.latitude) + } +} diff --git a/LambdaTimeline/Models/Post.swift b/LambdaTimeline/Models/Post.swift index f6b070e8..f1082915 100644 --- a/LambdaTimeline/Models/Post.swift +++ b/LambdaTimeline/Models/Post.swift @@ -7,12 +7,17 @@ // import UIKit +import MapKit enum MediaType { case image(UIImage) } -class Post: Equatable { +class Post: NSObject { + + struct Locations { + static let currentLocation = CLLocationCoordinate2D(latitude: 32.8844 , longitude: 117.2390) + } let mediaType: MediaType let author: String @@ -20,6 +25,7 @@ class Post: Equatable { var comments: [Comment] var ratio: CGFloat? var id: String? + let location: CLLocationCoordinate2D var title: String? { comments.first?.text @@ -29,16 +35,27 @@ class Post: Equatable { comments.first?.audioURL } - init(title: String, mediaType: MediaType, ratio: CGFloat?, author: String, timestamp: Date = Date(), audioURL: URL?) { + init(title: String, mediaType: MediaType, ratio: CGFloat?, author: String, timestamp: Date = Date(), audioURL: URL?, location: CLLocationCoordinate2D? = nil) { self.mediaType = mediaType self.ratio = ratio self.author = author self.comments = [Comment(text: title, author: author, audioURL: audioURL)] self.timestamp = timestamp self.id = UUID().uuidString + self.location = location ?? Locations.currentLocation } static func ==(lhs: Post, rhs: Post) -> Bool { return lhs.id == rhs.id } } + +extension Post: MKAnnotation { + var coordinate: CLLocationCoordinate2D { + CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude) + } + var postTitle: String? { title } + var subtitle: String? { author } + + +} diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist index 2f358730..050d824e 100644 --- a/LambdaTimeline/Resources/Info.plist +++ b/LambdaTimeline/Resources/Info.plist @@ -14,6 +14,8 @@ $(PRODUCT_NAME) NSMotionUsageDescription $(PRODUCT_NAME) uses the microphone to record audio + NSLocationWhenInUseUsageDescription + $(PRODUCT_NAME) uses your GPS to save your location. CFBundlePackageType APPL CFBundleShortVersionString diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index d86b272b..1980769b 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -102,7 +102,7 @@ - + @@ -113,13 +113,19 @@ - + - + + + + + + + - + @@ -146,11 +152,17 @@ - + + + + + + + @@ -184,6 +196,7 @@ + @@ -209,7 +222,39 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -262,7 +307,7 @@ - + @@ -368,7 +413,7 @@ - + @@ -455,7 +500,24 @@ - + + + + + + + + + + + + + + + + + + @@ -479,5 +541,8 @@ + + + diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift index f5f9823a..30c4dce3 100644 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -8,6 +8,7 @@ import UIKit import Photos +import MapKit class ImagePostViewController: ShiftableViewController { @@ -16,17 +17,31 @@ class ImagePostViewController: ShiftableViewController { @IBOutlet weak var chooseImageButton: UIButton! @IBOutlet weak var imageHeightConstraint: NSLayoutConstraint! @IBOutlet weak var postButton: UIBarButtonItem! + @IBOutlet weak var locationSwitch: UISwitch! var postController: PostController! var post: Post? var imageData: Data? + let locationManager = CLLocationManager() override func viewDidLoad() { super.viewDidLoad() - + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() setImageViewHeight(with: 1.0) } + @IBAction func locationSwitchOn(_ sender: UISwitch) { + if sender.isOn { + if CLLocationManager.locationServicesEnabled() { + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.startUpdatingLocation() + } + } + } + + + private func presentImagePickerController() { guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { @@ -47,11 +62,13 @@ class ImagePostViewController: ShiftableViewController { guard let image = imageView.image, let title = titleTextField.text, title != "" else { - presentInformationalAlertController(title: "Uh-oh", message: "Make sure that you add a photo and a caption before posting.") + presentInformationalAlertController(title: "Oops", message: "Make sure that you add a photo and a caption before posting.") return } + + let currentLocation = locationManager.location?.coordinate - postController.createImagePost(with: title, image: image, ratio: image.ratio, audioURL: nil) + postController.createImagePost(with: title, image: image, ratio: image.ratio, audioURL: nil, location: currentLocation!) navigationController?.popViewController(animated: true) } @@ -117,3 +134,11 @@ extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigation picker.dismiss(animated: true, completion: nil) } } + +extension ImagePostViewController: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let locationValue: CLLocationCoordinate2D = manager.location?.coordinate else { return } + print("locations - \(locationValue.latitude) \(locationValue.longitude)") + } + +} diff --git a/LambdaTimeline/View Controllers/PostLocationViewController.swift b/LambdaTimeline/View Controllers/PostLocationViewController.swift new file mode 100644 index 00000000..3f9a79a5 --- /dev/null +++ b/LambdaTimeline/View Controllers/PostLocationViewController.swift @@ -0,0 +1,41 @@ +// +// PostLocationViewController.swift +// LambdaTimeline +// +// Created by Rob Vance on 11/5/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import MapKit + +class PostLocationViewController: UIViewController, MKMapViewDelegate { + + @IBOutlet weak var mapView: MKMapView! + + var location: CLLocationCoordinate2D? + var postTitle: String? + var postAuthor: String? + + override func viewDidLoad() { + super.viewDidLoad() + mapView.mapType = .standard + mapView.register(MKAnnotation.self, forAnnotationViewWithReuseIdentifier: .reuseIdentifier) + mapView.delegate = self + + // Do any additional setup after loading the view. + } + func setPinWithMKPointAnnotation(location: CLLocationCoordinate2D) { + let annotation = MKPointAnnotation() + annotation.coordinate = location + annotation.title = postTitle + annotation.subtitle = postAuthor + let coordinateRegion = MKCoordinateRegion(center: annotation.coordinate, latitudinalMeters: 150, longitudinalMeters: 150) + mapView.setRegion(coordinateRegion, animated: true) + mapView.addAnnotation(annotation) + } +} + +extension String { + static let reuseIdentifier = "PostLocationView" +} From c5cfcdc524f0962855fe29524affec0e18efd80c Mon Sep 17 00:00:00 2001 From: Robert Vance Date: Thu, 5 Nov 2020 21:35:27 -0700 Subject: [PATCH 4/4] Almost Done --- LambdaTimeline/Storyboards/Main.storyboard | 34 +++++++++------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index 1980769b..fc548875 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -205,11 +205,11 @@ - + - + @@ -224,7 +224,7 @@ - + @@ -233,7 +233,7 @@ - + @@ -248,13 +248,16 @@ + + + - + @@ -508,11 +511,14 @@ + + + @@ -520,6 +526,9 @@ + + + @@ -529,20 +538,5 @@ - - - - - - - - - - - - - - -