From ca4d9ae0e0f32a640e1c31f6d222b47654c40812 Mon Sep 17 00:00:00 2001 From: Spencer Curtis Date: Tue, 1 Sep 2020 17:26:55 -0600 Subject: [PATCH 01/15] Remove Firebase functionality from project --- Cocopods-Homebrew-and-Ruby-Install-Guide.md | 143 -------------- LambdaTimeline.xcodeproj/project.pbxproj | 148 +-------------- LambdaTimeline/Helpers/Cache.swift | 25 --- .../User+DictionaryRepresentation.swift | 21 --- .../Helpers/FirebaseConvertible.swift | 13 -- LambdaTimeline/Helpers/Networking.swift | 51 ----- .../Operations/ConcurrentOperation.swift | 64 ------- .../Operations/FetchMediaOperation.swift | 59 ------ .../Model Controllers/PostController.swift | 117 ++---------- LambdaTimeline/Models/Author.swift | 36 ---- LambdaTimeline/Models/Comment.swift | 30 +-- LambdaTimeline/Models/Post.swift | 75 ++------ LambdaTimeline/Resources/AppDelegate.swift | 28 +-- .../{Base.lproj => }/Main.storyboard | 117 +++++++----- .../ImagePostDetailTableViewController.swift | 28 ++- .../ImagePostViewController.swift | 73 +++----- .../PostsCollectionViewController.swift | 101 ++-------- .../SignInViewController.swift | 76 +++----- .../Views/ImagePostCollectionViewCell.swift | 37 ++-- Podfile | 15 -- Podfile.lock | 174 ------------------ 21 files changed, 205 insertions(+), 1226 deletions(-) delete mode 100644 Cocopods-Homebrew-and-Ruby-Install-Guide.md delete mode 100644 LambdaTimeline/Helpers/Cache.swift delete mode 100644 LambdaTimeline/Helpers/Extensions/User+DictionaryRepresentation.swift delete mode 100644 LambdaTimeline/Helpers/FirebaseConvertible.swift delete mode 100644 LambdaTimeline/Helpers/Networking.swift delete mode 100644 LambdaTimeline/Helpers/Operations/ConcurrentOperation.swift delete mode 100644 LambdaTimeline/Helpers/Operations/FetchMediaOperation.swift delete mode 100644 LambdaTimeline/Models/Author.swift rename LambdaTimeline/Storyboards/{Base.lproj => }/Main.storyboard (84%) delete mode 100644 Podfile delete mode 100644 Podfile.lock diff --git a/Cocopods-Homebrew-and-Ruby-Install-Guide.md b/Cocopods-Homebrew-and-Ruby-Install-Guide.md deleted file mode 100644 index 2654e85a..00000000 --- a/Cocopods-Homebrew-and-Ruby-Install-Guide.md +++ /dev/null @@ -1,143 +0,0 @@ -# Cocopods Homebrew and Ruby Install Guide - -You'll use Hombrew (`brew`) to install Cocoapods in order to build the Firebase project. - -Homebrew is built on Ruby, and you'll want to configure Ruby (programming language) so that it's update to date. - -With the system installed version in macOS, you don't have the ability to update the libraries that come with Ruby (`gem update`). - -Gem is the tool to update Ruby packages, which is a prerequisite for Homebrew (`brew`). - -## Enter `rbenv` - -To help with managing different versions of Ruby, there is a tool called `rbenv`. - -You can use it to install a different version of Ruby, which you can then update, and then install hombrew, and then install Cocoapods. - -# When Ruby gem update fails (and how to install Cocoapods) -​ -You can't update the system version of Ruby, and you shouldn't do that anyway, since Apple manages it. Forcing it to change can cause issues with other tools that Apple ships with each verison of macOS. - -Running the command in Terminal may fail. - -`gem update` - -If you see this error, then you'll want to setup `rbenv` to manage your versions of Ruby (programming langauge). - -`Updating installed gems -Updating CFPropertyList -ERROR: While executing gem ... (Gem::FilePermissionError) - You don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory.` - - -## Install `rbenv` - -Follow the [rbenv install instructors](https://github.com/rbenv/rbenv) to setup your `.zshrc` file for `rbenv` - -### Terminal setup (zsh/bash for rbenv) - -To follow the instructions you'll either need to edit your zsh setup file, or bash. In Catalina (10.15) Apple made `zsh` the new default shell in Terminal (instead of `bash`). - -`open ~/.zshrc` - -Will allow you to open and edit your .zshrc file (in your home directory) that is used by the shell `zsh` in Catalina (If you're still using bash, edit the `~/.bashrc` file) - -## Use `rbenv` to see what versions are installed - -Run the command: - -`rbenv versions` - -``` -* system (set by /Users/paulsolt/.rbenv/version) - 2.6.2 - 2.6.3 -``` - -And you can see the current "global" version of Ruby is the "system"" default (what Apple provides, and it's read-only) - -## Install a new Ruby version that will read-write - -Run the command: - -`rbenv install 2.6.3` - -## Switch Ruby Versions - -After you install a new version of Ruby, you can switch to it as the default. - -`rbenv global 2.6.3` - -Will change Ruby across the system, unless you've specified a local version (inside a directory). - -NOTE: Sometimes you need to switch back to system with `rbenv global system` if Apple related commands are failing, but generally you'll want to stay on your version) - -## Update Ruby Gems (Package Manager for Ruby) - -Gems are either applications or libraries that you can use within Ruby projects or apps. - -Run the command to keep your gems for the current version of Ruby up to date. - -`gem update` - -## Install Homebrew - -To install [Homebrew](https://brew.sh) Run - -``` -/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" -``` - -## Install Cocoapods (if you don't have it installed) - -```brew install cocoapods``` - -## (Optional) Update Cocoapods - -Update your current cocoapods version to keep it up to date after it's installed: - -``` -brew upgrade cocoapods -``` - -## Test pod - -Type the command test pod is installed, you should see normal output. - -``` -Usage: - - $ pod COMMAND - - CocoaPods, the Cocoa library package manager. - ... -``` - - -## (Extra) Occasionally you get errors running pod / gem / etc. - -Errors are one of the challenges you'll need to overcome as a developer. Maintaining your Mac is part of the job. - -I Google these errors or I follow the instructions provided in the error (read the message). Usually they have a hint. - -On my system I had overwritten an install of Ruby 2.6.3 that I believe is causing this issue. It's warning me every time I try to use the command `pod` (not good) - -``` -pod - -Ignoring json-2.3.0 because its extensions are not built. Try: gem pristine json --version 2.3.0 -Ignoring json-2.3.0 because its extensions are not built. Try: gem pristine json --version 2.3.0 -... - -``` - -Run a command to install gems with the current version you installed (2.6.3 in this case). - -Keep in mind that ruby gems are install per install of Ruby, so you might need to do this again if there's a new version of Ruby to install. - -``` -rbenv global 2.6.3 -gem pristine --all -``` - -After running `gem pristine` (and waiting 5-10 minutes) the errors went away. diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj index 95ea1e3d..b2af5414 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -3,63 +3,45 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */; }; - 46463781216FDE4B00E7FF73 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4646377F216FDE4B00E7FF73 /* Main.storyboard */; }; 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 */; }; - 464637992170048900E7FF73 /* FirebaseConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 464637982170048900E7FF73 /* FirebaseConvertible.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 */; }; - 46CFE6F32170757F00E7FF73 /* User+DictionaryRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F22170757F00E7FF73 /* User+DictionaryRepresentation.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 */; }; - 46CFE6FB21714E6100E7FF73 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6FA21714E6100E7FF73 /* Author.swift */; }; - 46CFE6FD217154F500E7FF73 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6FC217154F500E7FF73 /* Networking.swift */; }; - 46CFE6FF2171556D00E7FF73 /* ConcurrentOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6FE2171556D00E7FF73 /* ConcurrentOperation.swift */; }; - 46CFE7012171559500E7FF73 /* FetchMediaOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE7002171559500E7FF73 /* FetchMediaOperation.swift */; }; 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */; }; - 46D571F32172D43B00E7FF73 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F22172D43B00E7FF73 /* Cache.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 */; }; - E453457955DF907FCBD117F1 /* Pods_LambdaTimeline.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA39CACB677861CF84322FB5 /* Pods_LambdaTimeline.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 33A2B89DE9E21538434BD640 /* Pods-LambdaTimeline.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LambdaTimeline.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline.debug.xcconfig"; sourceTree = ""; }; 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 = ""; }; - 46463780216FDE4B00E7FF73 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; 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 = ""; }; - 464637982170048900E7FF73 /* FirebaseConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseConvertible.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 = ""; }; - 46CFE6F22170757F00E7FF73 /* User+DictionaryRepresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "User+DictionaryRepresentation.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 = ""; }; - 46CFE6FA21714E6100E7FF73 /* Author.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = ""; }; - 46CFE6FC217154F500E7FF73 /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; }; - 46CFE6FE2171556D00E7FF73 /* ConcurrentOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentOperation.swift; sourceTree = ""; }; - 46CFE7002171559500E7FF73 /* FetchMediaOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchMediaOperation.swift; sourceTree = ""; }; 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostCollectionViewCell.swift; sourceTree = ""; }; - 46D571F22172D43B00E7FF73 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.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 = ""; }; - 5D2CCD6C68779B0A70AC37FA /* Pods-LambdaTimeline.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LambdaTimeline.release.xcconfig"; path = "Pods/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline.release.xcconfig"; sourceTree = ""; }; - AA39CACB677861CF84322FB5 /* Pods_LambdaTimeline.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LambdaTimeline.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,29 +49,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E453457955DF907FCBD117F1 /* Pods_LambdaTimeline.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 2B7D7DE8669C7C706445FCCC /* Pods */ = { - isa = PBXGroup; - children = ( - 33A2B89DE9E21538434BD640 /* Pods-LambdaTimeline.debug.xcconfig */, - 5D2CCD6C68779B0A70AC37FA /* Pods-LambdaTimeline.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; 4646376F216FDE4B00E7FF73 = { isa = PBXGroup; children = ( 4646377A216FDE4B00E7FF73 /* LambdaTimeline */, 46463779216FDE4B00E7FF73 /* Products */, - 2B7D7DE8669C7C706445FCCC /* Pods */, - 7C2A25429FC0F968FC1D35E7 /* Frameworks */, ); sourceTree = ""; }; @@ -119,7 +89,6 @@ isa = PBXGroup; children = ( 4646378F216FFD1B00E7FF73 /* Post.swift */, - 46CFE6FA21714E6100E7FF73 /* Author.swift */, 46463791216FFDD900E7FF73 /* Comment.swift */, ); path = Models; @@ -138,7 +107,7 @@ 46463797216FFF7400E7FF73 /* Storyboards */ = { isa = PBXGroup; children = ( - 4646377F216FDE4B00E7FF73 /* Main.storyboard */, + 46D1A48824FF0BC4008D1CA7 /* Main.storyboard */, 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */, ); path = Storyboards; @@ -171,23 +140,10 @@ path = Views; sourceTree = ""; }; - 46CFE7052171573200E7FF73 /* Operations */ = { - isa = PBXGroup; - children = ( - 46CFE7002171559500E7FF73 /* FetchMediaOperation.swift */, - 46CFE6FE2171556D00E7FF73 /* ConcurrentOperation.swift */, - ); - path = Operations; - sourceTree = ""; - }; 46CFE7062171573F00E7FF73 /* Helpers */ = { isa = PBXGroup; children = ( - 46CFE7052171573200E7FF73 /* Operations */, 46D571F62173D6D200E7FF73 /* Extensions */, - 46D571F22172D43B00E7FF73 /* Cache.swift */, - 464637982170048900E7FF73 /* FirebaseConvertible.swift */, - 46CFE6FC217154F500E7FF73 /* Networking.swift */, 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */, ); path = Helpers; @@ -196,21 +152,12 @@ 46D571F62173D6D200E7FF73 /* Extensions */ = { isa = PBXGroup; children = ( - 46CFE6F22170757F00E7FF73 /* User+DictionaryRepresentation.swift */, 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */, 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */, ); path = Extensions; sourceTree = ""; }; - 7C2A25429FC0F968FC1D35E7 /* Frameworks */ = { - isa = PBXGroup; - children = ( - AA39CACB677861CF84322FB5 /* Pods_LambdaTimeline.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -218,12 +165,9 @@ isa = PBXNativeTarget; buildConfigurationList = 4646378A216FDE4C00E7FF73 /* Build configuration list for PBXNativeTarget "LambdaTimeline" */; buildPhases = ( - 4E9D00EBF902E9530D554E98 /* [CP] Check Pods Manifest.lock */, 46463774216FDE4B00E7FF73 /* Sources */, 46463775216FDE4B00E7FF73 /* Frameworks */, 46463776216FDE4B00E7FF73 /* Resources */, - 931381193B5D2856F9B0CA8C /* [CP] Embed Pods Frameworks */, - 8108F7AF1127B3F2AAFC77E1 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -273,93 +217,27 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 46D1A48924FF0BC4008D1CA7 /* Main.storyboard in Resources */, 46463786216FDE4C00E7FF73 /* LaunchScreen.storyboard in Resources */, 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */, - 46463781216FDE4B00E7FF73 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 4E9D00EBF902E9530D554E98 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-LambdaTimeline-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 8108F7AF1127B3F2AAFC77E1 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 931381193B5D2856F9B0CA8C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 46463774216FDE4B00E7FF73 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */, - 46CFE6FB21714E6100E7FF73 /* Author.swift in Sources */, 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, - 46CFE6FD217154F500E7FF73 /* Networking.swift in Sources */, 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, - 46CFE6FF2171556D00E7FF73 /* ConcurrentOperation.swift in Sources */, 46463790216FFD1B00E7FF73 /* Post.swift in Sources */, - 46CFE6F32170757F00E7FF73 /* User+DictionaryRepresentation.swift in Sources */, - 46D571F32172D43B00E7FF73 /* Cache.swift in Sources */, - 46CFE7012171559500E7FF73 /* FetchMediaOperation.swift in Sources */, 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */, 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */, 4646379C2170091A00E7FF73 /* PostController.swift in Sources */, - 464637992170048900E7FF73 /* FirebaseConvertible.swift in Sources */, 46A0366A21700F5100E7FF73 /* PostsCollectionViewController.swift in Sources */, 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */, 46463792216FFDD900E7FF73 /* Comment.swift in Sources */, @@ -370,14 +248,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 4646377F216FDE4B00E7FF73 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 46463780216FDE4B00E7FF73 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -507,17 +377,17 @@ }; 4646378B216FDE4C00E7FF73 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 33A2B89DE9E21538434BD640 /* Pods-LambdaTimeline.debug.xcconfig */; 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 = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.LambdaSchool.LambdaTimeline; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -526,17 +396,17 @@ }; 4646378C216FDE4C00E7FF73 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5D2CCD6C68779B0A70AC37FA /* Pods-LambdaTimeline.release.xcconfig */; 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 = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.LambdaSchool.LambdaTimeline; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/LambdaTimeline/Helpers/Cache.swift b/LambdaTimeline/Helpers/Cache.swift deleted file mode 100644 index 42996051..00000000 --- a/LambdaTimeline/Helpers/Cache.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Cache.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/13/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation - -class Cache { - - func cache(value: Value, for key: Key) { - queue.async { - self.cache[key] = value - } - } - - func value(for key: Key) -> Value? { - return queue.sync { cache[key] } - } - - private var cache = [Key : Value]() - private let queue = DispatchQueue(label: "com.LambdaSchool.LambdaTimeline.CacheQueue") -} diff --git a/LambdaTimeline/Helpers/Extensions/User+DictionaryRepresentation.swift b/LambdaTimeline/Helpers/Extensions/User+DictionaryRepresentation.swift deleted file mode 100644 index 7740ab89..00000000 --- a/LambdaTimeline/Helpers/Extensions/User+DictionaryRepresentation.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// User+DictionaryRepresentation.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/12/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation -import FirebaseAuth - -extension User { - - private static let uidKey = "uid" - private static let displayNameKey = "displayName" - - var dictionaryRepresentation: [String: String] { - return [User.uidKey: uid, - User.displayNameKey: displayName ?? "No display name"] - } -} diff --git a/LambdaTimeline/Helpers/FirebaseConvertible.swift b/LambdaTimeline/Helpers/FirebaseConvertible.swift deleted file mode 100644 index a3608644..00000000 --- a/LambdaTimeline/Helpers/FirebaseConvertible.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// FirebaseConvertible.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/11/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation - -protocol FirebaseConvertible { - var dictionaryRepresentation: [String: Any] { get } -} diff --git a/LambdaTimeline/Helpers/Networking.swift b/LambdaTimeline/Helpers/Networking.swift deleted file mode 100644 index 90e171bf..00000000 --- a/LambdaTimeline/Helpers/Networking.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Networking.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/12/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation - -enum HTTPMethod: String { - case get = "GET" - case post = "POST" - case put = "PUT" - case patch = "PATCH" - case delete = "DELETE" -} - -enum Networking { - - static func performRequestFor(url: URL, httpMethod: HTTPMethod, parameters: [String: String]? = nil, headers: [String: String]? = nil, body: Data? = nil, completion: @escaping (Data?, URLResponse?, Error?) -> Void) { - - var formattedURL: URL? = url - - if let parameters = parameters { formattedURL = format(url: url, with: parameters) } - - guard let requestURL = formattedURL else { fatalError("requestURL is nil") } - - var request = URLRequest(url: requestURL) - - request.httpMethod = httpMethod.rawValue - request.httpBody = body - - if let headers = headers { - headers.forEach { (key, value) in - request.setValue(value, forHTTPHeaderField: key) - } - } - - URLSession.shared.dataTask(with: request, completionHandler: completion).resume() - } - - static private func format(url: URL, with queryParameters: [String: String]) -> URL? { - - var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) - - urlComponents?.queryItems = queryParameters.compactMap({ URLQueryItem(name: $0.key, value: $0.value) }) - - return urlComponents?.url ?? nil - } -} diff --git a/LambdaTimeline/Helpers/Operations/ConcurrentOperation.swift b/LambdaTimeline/Helpers/Operations/ConcurrentOperation.swift deleted file mode 100644 index 53121118..00000000 --- a/LambdaTimeline/Helpers/Operations/ConcurrentOperation.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ConcurrentOperation.swift -// LambdaTimeline -// -// Created by Andrew Madsen -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation - -class ConcurrentOperation: Operation { - - // MARK: Types - - enum State: String { - case isReady, isExecuting, isFinished - } - - // MARK: Properties - - private var _state = State.isReady - - private let stateQueue = DispatchQueue(label: "com.LambdaSchool.Astronomy.ConcurrentOperationStateQueue") - var state: State { - get { - var result: State? - let queue = self.stateQueue - queue.sync { - result = _state - } - return result! - } - - set { - let oldValue = state - willChangeValue(forKey: newValue.rawValue) - willChangeValue(forKey: oldValue.rawValue) - - stateQueue.sync { self._state = newValue } - - didChangeValue(forKey: oldValue.rawValue) - didChangeValue(forKey: newValue.rawValue) - } - } - - // MARK: NSOperation - - override dynamic var isReady: Bool { - return super.isReady && state == .isReady - } - - override dynamic var isExecuting: Bool { - return state == .isExecuting - } - - override dynamic var isFinished: Bool { - return state == .isFinished - } - - override var isAsynchronous: Bool { - return true - } - -} diff --git a/LambdaTimeline/Helpers/Operations/FetchMediaOperation.swift b/LambdaTimeline/Helpers/Operations/FetchMediaOperation.swift deleted file mode 100644 index 7632555b..00000000 --- a/LambdaTimeline/Helpers/Operations/FetchMediaOperation.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// FetchMediaOperation.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/12/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation - -class FetchMediaOperation: ConcurrentOperation { - - init(post: Post, postController: PostController, session: URLSession = URLSession.shared) { - self.post = post - self.postController = postController - self.session = session - super.init() - } - - override func start() { - state = .isExecuting - - let url = post.mediaURL - - let task = session.dataTask(with: url) { (data, response, error) in - defer { self.state = .isFinished } - if self.isCancelled { return } - if let error = error { - NSLog("Error fetching data for \(self.post): \(error)") - return - } - - guard let data = data else { - NSLog("No data returned from fetch media operation data task.") - return - } - - self.mediaData = data - } - task.resume() - dataTask = task - } - - override func cancel() { - dataTask?.cancel() - super.cancel() - } - - // MARK: Properties - - let post: Post - let postController: PostController - var mediaData: Data? - - private let session: URLSession - - private var dataTask: URLSessionDataTask? - -} diff --git a/LambdaTimeline/Model Controllers/PostController.swift b/LambdaTimeline/Model Controllers/PostController.swift index 29ce3869..3df87148 100644 --- a/LambdaTimeline/Model Controllers/PostController.swift +++ b/LambdaTimeline/Model Controllers/PostController.swift @@ -6,122 +6,31 @@ // Copyright © 2018 Lambda School. All rights reserved. // -import Foundation -import FirebaseAuth -import FirebaseDatabase -import FirebaseStorage +import UIKit class PostController { - func createPost(with title: String, ofType mediaType: MediaType, mediaData: Data, ratio: CGFloat? = nil, completion: @escaping (Bool) -> Void = { _ in }) { - - guard let currentUser = Auth.auth().currentUser, - let author = Author(user: currentUser) else { return } - - store(mediaData: mediaData, mediaType: mediaType) { (mediaURL) in - - guard let mediaURL = mediaURL else { completion(false); return } - - let imagePost = Post(title: title, mediaURL: mediaURL, ratio: ratio, author: author) - - self.postsRef.childByAutoId().setValue(imagePost.dictionaryRepresentation) { (error, ref) in - if let error = error { - NSLog("Error posting image post: \(error)") - completion(false) - } - - completion(true) - } - } + var posts: [Post] = [] + + var currentUser: String? { + UserDefaults.standard.string(forKey: "username") } - func addComment(with text: String, to post: inout Post) { + func createImagePost(with title: String, image: UIImage, ratio: CGFloat?) { - guard let currentUser = Auth.auth().currentUser, - let author = Author(user: currentUser) else { return } + guard let currentUser = currentUser else { return } - let comment = Comment(text: text, author: author) - post.comments.append(comment) - - savePostToFirebase(post) - } - - func observePosts(completion: @escaping (Error?) -> Void) { + let post = Post(title: title, mediaType: .image(image), ratio: ratio, author: currentUser) - postsRef.observe(.value, with: { (snapshot) in - - guard let postDictionaries = snapshot.value as? [String: [String: Any]] else { return } - - var posts: [Post] = [] - - for (key, value) in postDictionaries { - - guard let post = Post(dictionary: value, id: key) else { continue } - - posts.append(post) - } - - self.posts = posts.sorted(by: { $0.timestamp > $1.timestamp }) - - completion(nil) - - }) { (error) in - NSLog("Error fetching posts: \(error)") - } + posts.append(post) } - func savePostToFirebase(_ post: Post, completion: (Error?) -> Void = { _ in }) { - - guard let postID = post.id else { return } + func addComment(with text: String, to post: inout Post) { - let ref = postsRef.child(postID) + guard let currentUser = currentUser else { return } - ref.setValue(post.dictionaryRepresentation) - } + let comment = Comment(text: text, author: currentUser) + post.comments.append(comment) - private func store(mediaData: Data, mediaType: MediaType, completion: @escaping (URL?) -> Void) { - - let mediaID = UUID().uuidString - - let mediaRef = storageRef.child(mediaType.rawValue).child(mediaID) - - let uploadTask = mediaRef.putData(mediaData, metadata: nil) { (metadata, error) in - if let error = error { - NSLog("Error storing media data: \(error)") - completion(nil) - return - } - - if metadata == nil { - NSLog("No metadata returned from upload task.") - completion(nil) - return - } - - mediaRef.downloadURL(completion: { (url, error) in - - if let error = error { - NSLog("Error getting download url of media: \(error)") - } - - guard let url = url else { - NSLog("Download url is nil. Unable to create a Media object") - - completion(nil) - return - } - completion(url) - }) - } - - uploadTask.resume() } - - var posts: [Post] = [] - let currentUser = Auth.auth().currentUser - let postsRef = Database.database().reference().child("posts") - - let storageRef = Storage.storage().reference() - - } diff --git a/LambdaTimeline/Models/Author.swift b/LambdaTimeline/Models/Author.swift deleted file mode 100644 index 4d847ad1..00000000 --- a/LambdaTimeline/Models/Author.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Author.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/12/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation -import FirebaseAuth - -struct Author: FirebaseConvertible, Equatable { - - init?(user: User) { - self.init(dictionary: user.dictionaryRepresentation) - } - - init?(dictionary: [String: Any]) { - guard let uid = dictionary[Author.uidKey] as? String, - let displayName = dictionary[Author.displayNameKey] as? String else { return nil } - - self.uid = uid - self.displayName = displayName - } - - let uid: String - let displayName: String? - - private static let uidKey = "uid" - private static let displayNameKey = "displayName" - - var dictionaryRepresentation: [String: Any] { - return [Author.uidKey: uid, - Author.displayNameKey: displayName ?? "No display name"] - } -} diff --git a/LambdaTimeline/Models/Comment.swift b/LambdaTimeline/Models/Comment.swift index a71ffe4d..a7dad6f5 100644 --- a/LambdaTimeline/Models/Comment.swift +++ b/LambdaTimeline/Models/Comment.swift @@ -7,41 +7,27 @@ // import Foundation -import FirebaseAuth -class Comment: FirebaseConvertible, Equatable { +class Comment: Hashable { static private let textKey = "text" - static private let author = "author" + static private let authorKey = "author" static private let timestampKey = "timestamp" - let text: String - let author: Author + let text: String? + let author: String let timestamp: Date - init(text: String, author: Author, timestamp: Date = Date()) { + init(text: String? = nil, author: String, timestamp: Date = Date()) { self.text = text self.author = author self.timestamp = timestamp } - init?(dictionary: [String : Any]) { - guard let text = dictionary[Comment.textKey] as? String, - let authorDictionary = dictionary[Comment.author] as? [String: Any], - let author = Author(dictionary: authorDictionary), - let timestampTimeInterval = dictionary[Comment.timestampKey] as? TimeInterval else { return nil } - - self.text = text - self.author = author - self.timestamp = Date(timeIntervalSince1970: timestampTimeInterval) - } - - var dictionaryRepresentation: [String: Any] { - return [Comment.textKey: text, - Comment.author: author.dictionaryRepresentation, - Comment.timestampKey: timestamp.timeIntervalSince1970] + 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 index ec961e9f..15e1d65c 100644 --- a/LambdaTimeline/Models/Post.swift +++ b/LambdaTimeline/Models/Post.swift @@ -6,74 +6,35 @@ // Copyright © 2018 Lambda School. All rights reserved. // -import Foundation -import FirebaseAuth +import UIKit -enum MediaType: String { - case image +enum MediaType { + case image(UIImage) } -class Post { +class Post: Equatable { - init(title: String, mediaURL: URL, ratio: CGFloat? = nil, author: Author, timestamp: Date = Date()) { - self.mediaURL = mediaURL - self.ratio = ratio - self.mediaType = .image - self.author = author - self.comments = [Comment(text: title, author: author)] - self.timestamp = timestamp - } - - init?(dictionary: [String : Any], id: String) { - guard let mediaURLString = dictionary[Post.mediaKey] as? String, - let mediaURL = URL(string: mediaURLString), - let mediaTypeString = dictionary[Post.mediaTypeKey] as? String, - let mediaType = MediaType(rawValue: mediaTypeString), - let authorDictionary = dictionary[Post.authorKey] as? [String: Any], - let author = Author(dictionary: authorDictionary), - let timestampTimeInterval = dictionary[Post.timestampKey] as? TimeInterval, - let captionDictionaries = dictionary[Post.commentsKey] as? [[String: Any]] else { return nil } - - self.mediaURL = mediaURL - self.mediaType = mediaType - self.ratio = dictionary[Post.ratioKey] as? CGFloat - self.author = author - self.timestamp = Date(timeIntervalSince1970: timestampTimeInterval) - self.comments = captionDictionaries.compactMap({ Comment(dictionary: $0) }) - self.id = id - } - - var dictionaryRepresentation: [String : Any] { - var dict: [String: Any] = [Post.mediaKey: mediaURL.absoluteString, - Post.mediaTypeKey: mediaType.rawValue, - Post.commentsKey: comments.map({ $0.dictionaryRepresentation }), - Post.authorKey: author.dictionaryRepresentation, - Post.timestampKey: timestamp.timeIntervalSince1970] - - guard let ratio = self.ratio else { return dict } - - dict[Post.ratioKey] = ratio - - return dict - } - - var mediaURL: URL let mediaType: MediaType - let author: Author + let author: String let timestamp: Date var comments: [Comment] - var id: String? var ratio: CGFloat? + var id: String? var title: String? { return comments.first?.text } - static private let mediaKey = "media" - static private let ratioKey = "ratio" - static private let mediaTypeKey = "mediaType" - static private let authorKey = "author" - static private let commentsKey = "comments" - static private let timestampKey = "timestamp" - static private let idKey = "id" + init(title: String, mediaType: MediaType, ratio: CGFloat?, author: String, timestamp: Date = Date()) { + self.mediaType = mediaType + self.ratio = ratio + self.author = author + self.comments = [Comment(text: title, author: author)] + 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 index d889d46c..a44cc8ca 100644 --- a/LambdaTimeline/Resources/AppDelegate.swift +++ b/LambdaTimeline/Resources/AppDelegate.swift @@ -7,40 +7,14 @@ // import UIKit -import Firebase -import GoogleSignIn @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate, GIDSignInDelegate { +class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - FirebaseApp.configure() - - let signIn = GIDSignIn.sharedInstance() - signIn?.clientID = FirebaseApp.app()?.options.clientID - - if Auth.auth().currentUser != nil { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let postsNavigationController = storyboard.instantiateViewController(withIdentifier: "PostsNavigationController") - window?.rootViewController = postsNavigationController - window?.makeKeyAndVisible() - } - return true } - - func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, - withError error: Error!) { - if let error = error { - if (error as NSError).code == GIDSignInErrorCode.hasNoAuthInKeychain.rawValue { - print("Error: The user has not signed in before or they have since signed out.") - } else { - print("\(error.localizedDescription)") - } - return - } - } } diff --git a/LambdaTimeline/Storyboards/Base.lproj/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard similarity index 84% rename from LambdaTimeline/Storyboards/Base.lproj/Main.storyboard rename to LambdaTimeline/Storyboards/Main.storyboard index fcec352d..9e2d25d6 100644 --- a/LambdaTimeline/Storyboards/Base.lproj/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -1,9 +1,9 @@ - - + + - + @@ -12,10 +12,10 @@ - - + + - + @@ -24,7 +24,7 @@ - + @@ -88,11 +88,6 @@ - - - - - @@ -106,30 +101,30 @@ - + - + - + - + - + - + - @@ -151,7 +145,7 @@ - + @@ -168,12 +162,12 @@ - + - + - + @@ -195,15 +189,16 @@ - + - + - + + - + @@ -213,59 +208,80 @@ - + - + - - + + + + + + + + + + - + - + - + - + - + @@ -274,16 +290,16 @@ - + - @@ -305,22 +320,22 @@ - - + + - + - + + + + diff --git a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift index 31b43fa3..c108edd7 100644 --- a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift @@ -10,6 +10,16 @@ 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() @@ -17,15 +27,14 @@ class ImagePostDetailTableViewController: UITableViewController { func updateViews() { - guard let imageData = imageData, - let image = UIImage(data: imageData) else { return } + guard case MediaType.image(let image) = post.mediaType else { return } title = post?.title imageView.image = image titleLabel.text = post.title - authorLabel.text = post.author.displayName + authorLabel.text = post.author } // MARK: - Table view data source @@ -70,19 +79,8 @@ class ImagePostDetailTableViewController: UITableViewController { let comment = post?.comments[indexPath.row + 1] cell.textLabel?.text = comment?.text - cell.detailTextLabel?.text = comment?.author.displayName + cell.detailTextLabel?.text = comment?.author return cell } - - var post: Post! - var postController: PostController! - var imageData: Data? - - - - @IBOutlet weak var imageView: UIImageView! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var authorLabel: UILabel! - @IBOutlet weak var imageViewAspectRatioConstraint: NSLayoutConstraint! } diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift index 93c4b93e..9cffbbbb 100644 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -11,29 +11,20 @@ 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) - - updateViews() - } - - func updateViews() { - - guard let imageData = imageData, - let image = UIImage(data: imageData) else { - title = "New Post" - return - } - - title = post?.title - - setImageViewHeight(with: image.ratio) - - imageView.image = image - - chooseImageButton.setTitle("", for: []) } private func presentImagePickerController() { @@ -44,11 +35,9 @@ class ImagePostViewController: ShiftableViewController { } let imagePicker = UIImagePickerController() - imagePicker.delegate = self - imagePicker.sourceType = .photoLibrary - + present(imagePicker, animated: true, completion: nil) } @@ -56,24 +45,15 @@ class ImagePostViewController: ShiftableViewController { view.endEditing(true) - guard let imageData = imageView.image?.jpegData(compressionQuality: 0.1), + 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.createPost(with: title, ofType: .image, mediaData: imageData, ratio: imageView.image?.ratio) { (success) in - guard success else { - DispatchQueue.main.async { - self.presentInformationalAlertController(title: "Error", message: "Unable to create post. Try again.") - } + presentInformationalAlertController(title: "Uh-oh", message: "Make sure that you add a photo and a caption before posting.") return - } - - DispatchQueue.main.async { - self.navigationController?.popViewController(animated: true) - } } + + postController.createImagePost(with: title, image: image, ratio: image.ratio) + + navigationController?.popViewController(animated: true) } @IBAction func chooseImage(_ sender: Any) { @@ -100,29 +80,22 @@ class ImagePostViewController: ShiftableViewController { 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.") - - @unknown default: - print("FatalError") + default: + break } presentImagePickerController() } + @IBAction func addFilter(_ sender: Any) { + + } + func setImageViewHeight(with aspectRatio: CGFloat) { imageHeightConstraint.constant = imageView.frame.size.width * aspectRatio view.layoutSubviews() } - - var postController: PostController! - var post: Post? - var imageData: Data? - - @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! } extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { diff --git a/LambdaTimeline/View Controllers/PostsCollectionViewController.swift b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift index 7a8191b4..b9ed6b54 100644 --- a/LambdaTimeline/View Controllers/PostsCollectionViewController.swift +++ b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift @@ -7,29 +7,15 @@ // import UIKit -import FirebaseAuth -import FirebaseUI class PostsCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { - override func viewDidLoad() { - super.viewDidLoad() - - postController.observePosts { (_) in - DispatchQueue.main.async { - self.collectionView.reloadData() - } - } - } + var postController: PostController! - @IBAction func signout(_ sender: Any) { - let firebaseAuth = Auth.auth() - do { - try firebaseAuth.signOut() - } catch let signOutError as NSError { - print ("Error signing out: %@", signOutError) - } - dismiss(animated: true, completion: nil) + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + collectionView.reloadData() } @IBAction func addPost(_ sender: Any) { @@ -51,10 +37,11 @@ class PostsCollectionViewController: UICollectionViewController, UICollectionVie // MARK: UICollectionViewDataSource override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return postController.posts.count + return postController?.posts.count ?? 0 } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let post = postController.posts[indexPath.row] switch post.mediaType { @@ -64,8 +51,6 @@ class PostsCollectionViewController: UICollectionViewController, UICollectionVie cell.post = post - loadImage(for: cell, forItemAt: indexPath) - return cell } } @@ -76,14 +61,9 @@ class PostsCollectionViewController: UICollectionViewController, UICollectionVie let post = postController.posts[indexPath.row] - switch post.mediaType { - - case .image: - - guard let ratio = post.ratio else { return size } - - size.height = size.width * ratio - } + guard let ratio = post.ratio else { return size } + + size.height = size.width * ratio return size } @@ -98,59 +78,7 @@ class PostsCollectionViewController: UICollectionViewController, UICollectionVie } } - override func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) { - - guard let postID = postController.posts[indexPath.row].id else { return } - operations[postID]?.cancel() - } - func loadImage(for imagePostCell: ImagePostCollectionViewCell, forItemAt indexPath: IndexPath) { - let post = postController.posts[indexPath.row] - - guard let postID = post.id else { return } - - if let mediaData = cache.value(for: postID), - let image = UIImage(data: mediaData) { - imagePostCell.setImage(image) - self.collectionView.reloadItems(at: [indexPath]) - return - } - - let fetchOp = FetchMediaOperation(post: post, postController: postController) - - let cacheOp = BlockOperation { - if let data = fetchOp.mediaData { - self.cache.cache(value: data, for: postID) - DispatchQueue.main.async { - self.collectionView.reloadItems(at: [indexPath]) - } - } - } - - let completionOp = BlockOperation { - defer { self.operations.removeValue(forKey: postID) } - - if let currentIndexPath = self.collectionView?.indexPath(for: imagePostCell), - currentIndexPath != indexPath { - print("Got image for now-reused cell") - return - } - - if let data = fetchOp.mediaData { - imagePostCell.setImage(UIImage(data: data)) - self.collectionView.reloadItems(at: [indexPath]) - } - } - - cacheOp.addDependency(fetchOp) - completionOp.addDependency(fetchOp) - - mediaFetchQueue.addOperation(fetchOp) - mediaFetchQueue.addOperation(cacheOp) - OperationQueue.main.addOperation(completionOp) - - operations[postID] = fetchOp - } // MARK: - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation @@ -163,17 +91,10 @@ class PostsCollectionViewController: UICollectionViewController, UICollectionVie let destinationVC = segue.destination as? ImagePostDetailTableViewController - guard let indexPath = collectionView.indexPathsForSelectedItems?.first, - let postID = postController.posts[indexPath.row].id else { return } + guard let indexPath = collectionView.indexPathsForSelectedItems?.first else { return } destinationVC?.postController = postController destinationVC?.post = postController.posts[indexPath.row] - destinationVC?.imageData = cache.value(for: postID) } } - - private let postController = PostController() - private var operations = [String : Operation]() - private let mediaFetchQueue = OperationQueue() - private let cache = Cache() } diff --git a/LambdaTimeline/View Controllers/SignInViewController.swift b/LambdaTimeline/View Controllers/SignInViewController.swift index 5b44b832..c81bb748 100644 --- a/LambdaTimeline/View Controllers/SignInViewController.swift +++ b/LambdaTimeline/View Controllers/SignInViewController.swift @@ -7,70 +7,38 @@ // import UIKit -import Firebase -import GoogleSignIn class SignInViewController: UIViewController { - + + @IBOutlet weak var nameTextField: UITextField! + + let postController = PostController() + override func viewDidLoad() { super.viewDidLoad() - GIDSignIn.sharedInstance()?.presentingViewController = self - GIDSignIn.sharedInstance()?.delegate = self - - setUpSignInButton() } - @IBAction func googleSignIn(_ sender: Any) { - GIDSignIn.sharedInstance()?.signIn() + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + segueIfUsernameExists() } -} - -extension SignInViewController: GIDSignInDelegate { - func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) { - - if let error = error { - print("Error signing in with Google: \(error)") - return - } - - guard let authentication = user.authentication else { return } - - let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken, accessToken: authentication.accessToken) - - Auth.auth().signIn(with: credential) { (authResult, error) in - if let error = error { - print("Error signing in with Google: \(error)") - return - } - - DispatchQueue.main.async { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let postsNavigationController = storyboard.instantiateViewController(withIdentifier: "PostsNavigationController") - postsNavigationController.modalPresentationStyle = .fullScreen - self.present(postsNavigationController, animated: true, completion: nil) - } - } + + @IBAction func getStarted(_ sender: Any) { + UserDefaults.standard.set(nameTextField.text, forKey: "username") + segueIfUsernameExists() } - func sign(_ signIn: GIDSignIn!, didDisconnectWith user: GIDGoogleUser!, withError error: Error!) { - print("User disconnected") + func segueIfUsernameExists() { + if UserDefaults.standard.string(forKey: "username") != nil { + performSegue(withIdentifier: "ModalPostsVC", sender: nil) + } } - func setUpSignInButton() { - - let button = GIDSignInButton() - - button.translatesAutoresizingMaskIntoConstraints = false - - view.addSubview(button) - - - let buttonCenterXConstraint = button.centerXAnchor.constraint(equalTo: view.centerXAnchor) - let buttonCenterYConstraint = button.centerYAnchor.constraint(equalTo: view.centerYAnchor) - let buttonWidthConstraint = button.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5) - - view.addConstraints([buttonCenterXConstraint, - buttonCenterYConstraint, - buttonWidthConstraint]) + 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 index 3841cf65..f83f73ed 100644 --- a/LambdaTimeline/Views/ImagePostCollectionViewCell.swift +++ b/LambdaTimeline/Views/ImagePostCollectionViewCell.swift @@ -10,10 +10,22 @@ 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() @@ -23,32 +35,21 @@ class ImagePostCollectionViewCell: UICollectionViewCell { } func updateViews() { - guard let post = post else { return } - + guard let post = post, + case MediaType.image(let image) = post.mediaType else { return } + titleLabel.text = post.title - authorLabel.text = post.author.displayName + authorLabel.text = post.author + imageView.image = image } - + func setupLabelBackgroundView() { labelBackgroundView.layer.cornerRadius = 8 -// labelBackgroundView.layer.borderColor = UIColor.white.cgColor -// labelBackgroundView.layer.borderWidth = 0.5 labelBackgroundView.clipsToBounds = true } func setImage(_ image: UIImage?) { imageView.image = image } - - var post: Post? { - didSet { - updateViews() - } - } - -@IBOutlet weak var imageView: UIImageView! -@IBOutlet weak var titleLabel: UILabel! -@IBOutlet weak var authorLabel: UILabel! -@IBOutlet weak var labelBackgroundView: UIView! - } + diff --git a/Podfile b/Podfile deleted file mode 100644 index 7d5e126d..00000000 --- a/Podfile +++ /dev/null @@ -1,15 +0,0 @@ -# Uncomment the next line to define a global platform for your project -platform :ios, '13.0' - -target 'LambdaTimeline' do - # Comment the next line if you're not using Swift and don't want to use dynamic frameworks - use_frameworks! - - # Pods for LambdaTimeline - pod 'Firebase/Core' - pod 'Firebase/Database' - pod 'Firebase/Storage' - pod 'Firebase/Auth' - pod 'FirebaseUI/Google' - pod 'GoogleSignIn' -end diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index 9d835784..00000000 --- a/Podfile.lock +++ /dev/null @@ -1,174 +0,0 @@ -PODS: - - AppAuth (1.3.0): - - AppAuth/Core (= 1.3.0) - - AppAuth/ExternalUserAgent (= 1.3.0) - - AppAuth/Core (1.3.0) - - AppAuth/ExternalUserAgent (1.3.0) - - Firebase/Auth (6.19.0): - - Firebase/CoreOnly - - FirebaseAuth (~> 6.4.3) - - Firebase/Core (6.19.0): - - Firebase/CoreOnly - - FirebaseAnalytics (= 6.3.1) - - Firebase/CoreOnly (6.19.0): - - FirebaseCore (= 6.6.4) - - Firebase/Database (6.19.0): - - Firebase/CoreOnly - - FirebaseDatabase (~> 6.1.4) - - Firebase/Storage (6.19.0): - - Firebase/CoreOnly - - FirebaseStorage (~> 3.6.0) - - FirebaseAnalytics (6.3.1): - - FirebaseCore (~> 6.6) - - FirebaseInstallations (~> 1.1) - - GoogleAppMeasurement (= 6.3.1) - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/MethodSwizzler (~> 6.0) - - GoogleUtilities/Network (~> 6.0) - - "GoogleUtilities/NSData+zlib (~> 6.0)" - - nanopb (= 0.3.9011) - - FirebaseAuth (6.4.3): - - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 6.6) - - GoogleUtilities/AppDelegateSwizzler (~> 6.5) - - GoogleUtilities/Environment (~> 6.5) - - GTMSessionFetcher/Core (~> 1.1) - - FirebaseAuthInterop (1.1.0) - - FirebaseCore (6.6.4): - - FirebaseCoreDiagnostics (~> 1.2) - - FirebaseCoreDiagnosticsInterop (~> 1.2) - - GoogleUtilities/Environment (~> 6.5) - - GoogleUtilities/Logger (~> 6.5) - - FirebaseCoreDiagnostics (1.2.2): - - FirebaseCoreDiagnosticsInterop (~> 1.2) - - GoogleDataTransportCCTSupport (~> 2.0) - - GoogleUtilities/Environment (~> 6.5) - - GoogleUtilities/Logger (~> 6.5) - - nanopb (~> 0.3.901) - - FirebaseCoreDiagnosticsInterop (1.2.0) - - FirebaseDatabase (6.1.4): - - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 6.0) - - leveldb-library (~> 1.22) - - FirebaseInstallations (1.1.0): - - FirebaseCore (~> 6.6) - - GoogleUtilities/UserDefaults (~> 6.5) - - PromisesObjC (~> 1.2) - - FirebaseStorage (3.6.0): - - FirebaseAuthInterop (~> 1.1) - - FirebaseCore (~> 6.6) - - GTMSessionFetcher/Core (~> 1.1) - - FirebaseUI/Auth (8.4.2): - - Firebase/Auth (~> 6.0) - - GoogleUtilities/UserDefaults - - FirebaseUI/Google (8.4.2): - - FirebaseUI/Auth - - GoogleSignIn (~> 5.0) - - GoogleAppMeasurement (6.3.1): - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/MethodSwizzler (~> 6.0) - - GoogleUtilities/Network (~> 6.0) - - "GoogleUtilities/NSData+zlib (~> 6.0)" - - nanopb (= 0.3.9011) - - GoogleDataTransport (5.0.0) - - GoogleDataTransportCCTSupport (2.0.0): - - GoogleDataTransport (~> 5.0) - - nanopb (~> 0.3.901) - - GoogleSignIn (5.0.2): - - AppAuth (~> 1.2) - - GTMAppAuth (~> 1.0) - - GTMSessionFetcher/Core (~> 1.1) - - GoogleUtilities/AppDelegateSwizzler (6.5.1): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Environment (6.5.1) - - GoogleUtilities/Logger (6.5.1): - - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (6.5.1): - - GoogleUtilities/Logger - - GoogleUtilities/Network (6.5.1): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (6.5.1)" - - GoogleUtilities/Reachability (6.5.1): - - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (6.5.1): - - GoogleUtilities/Logger - - GTMAppAuth (1.0.0): - - AppAuth/Core (~> 1.0) - - GTMSessionFetcher (~> 1.1) - - GTMSessionFetcher (1.3.1): - - GTMSessionFetcher/Full (= 1.3.1) - - GTMSessionFetcher/Core (1.3.1) - - GTMSessionFetcher/Full (1.3.1): - - GTMSessionFetcher/Core (= 1.3.1) - - leveldb-library (1.22) - - nanopb (0.3.9011): - - nanopb/decode (= 0.3.9011) - - nanopb/encode (= 0.3.9011) - - nanopb/decode (0.3.9011) - - nanopb/encode (0.3.9011) - - PromisesObjC (1.2.8) - -DEPENDENCIES: - - Firebase/Auth - - Firebase/Core - - Firebase/Database - - Firebase/Storage - - FirebaseUI/Google - - GoogleSignIn - -SPEC REPOS: - trunk: - - AppAuth - - Firebase - - FirebaseAnalytics - - FirebaseAuth - - FirebaseAuthInterop - - FirebaseCore - - FirebaseCoreDiagnostics - - FirebaseCoreDiagnosticsInterop - - FirebaseDatabase - - FirebaseInstallations - - FirebaseStorage - - FirebaseUI - - GoogleAppMeasurement - - GoogleDataTransport - - GoogleDataTransportCCTSupport - - GoogleSignIn - - GoogleUtilities - - GTMAppAuth - - GTMSessionFetcher - - leveldb-library - - nanopb - - PromisesObjC - -SPEC CHECKSUMS: - AppAuth: 73574f3013a1e65b9601a3ddc8b3158cce68c09d - Firebase: d55ddb0e0bb3207166cddc028647833d8a1b5b6f - FirebaseAnalytics: 572e467f3d977825266e8ccd52674aa3e6f47eac - FirebaseAuth: 5ce2b03a3d7fe56b7a6e4c5ec7ff1522890b1d6f - FirebaseAuthInterop: a0f37ae05833af156e72028f648d313f7e7592e9 - FirebaseCore: ed0a24c758a57c2b88c5efa8e6a8195e868af589 - FirebaseCoreDiagnostics: e9b4cd8ba60dee0f2d13347332e4b7898cca5b61 - FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 - FirebaseDatabase: 0144e0706a4761f1b0e8679572eba8095ddb59be - FirebaseInstallations: 575cd32f2aec0feeb0e44f5d0110a09e5e60b47b - FirebaseStorage: b43ee271294227e1a3225544b073f3b49b427f46 - FirebaseUI: e57e9b9c4340631151fbe67a14206d23d0974f37 - GoogleAppMeasurement: c29d405ff76e18551b5d158eaba6753fda8c7542 - GoogleDataTransport: a857c6a002d201b524dd4bc2ed7e7355ed07e785 - GoogleDataTransportCCTSupport: 32f75fbe904c82772fcbb6b6bd4525bfb6f2a862 - GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213 - GoogleUtilities: 06eb53bb579efe7099152735900dd04bf09e7275 - GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e - GTMSessionFetcher: cea130bbfe5a7edc8d06d3f0d17288c32ffe9925 - leveldb-library: 55d93ee664b4007aac644a782d11da33fba316f7 - nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd - PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6 - -PODFILE CHECKSUM: 28424f1a99ace6e65529f57867177322416678fa - -COCOAPODS: 1.9.1 From 80ee64da7fc5a2fe61ec2cccf49e68dc86a39fa3 Mon Sep 17 00:00:00 2001 From: Spencer Curtis Date: Tue, 1 Sep 2020 17:29:24 -0600 Subject: [PATCH 02/15] Update README to reflect removal of Firebase --- README.md | 52 +++++----------------------------------------------- 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index adfee3d4..d294b91f 100644 --- a/README.md +++ b/README.md @@ -23,57 +23,15 @@ Create a new Xcode project called `ImageFilterEditor` to use as a playground for 2. Create an `ImagePostViewController` that will get a starting image and filter it. 1. Add whatever UI elements you want to control the filters with the selected image. 2. For filters that require other parameters use a slider, UITextField, segmented control, or UIPanGestureRecognizer. -3. Ensure that the controls to add your filters, adjust them, etc. are only available to the user at the apropriate time. +3. Ensure that the controls to add your filters, adjust them, etc. are only available to the user at the appropriate time. 1. For example, you shouldn't let the user add a filter if they haven't selected an image yet. And it doesn't make sense to show the adjustment UI if they selected a filter that has no adjustment. 4. You can use a Collection or Table View to select filters with additional detail screens to fine tune amounts of filters (i.e.: Instagram or Apple Photo) -### Part 2 - Firebase Setup +### Part 2: Feature Integration -Though you have a base project, you will need to modify it. - -**Cocoapods** - -To begin, run `pod install` after navigating to the repo in terminal. (If you have issues with [Cocoapods, Homebrew, Ruby, follow this guide](Cocopods-Homebrew-and-Ruby-Install-Guide.md)) Work out of the generated `.xcworkspace` - - pod install - -NOTE: If you run `pod update` there may be breaking changes from newer versions of Firebase. The current Podfile.lock versions work with Swift 5.1 and Xcode 11. - -**Firebase Setup** - -[Watch the iOS Firebase Authentication Video](https://youtu.be/vGeuZtHmcMM) to follow these steps -[![Watch the iOS Firebase Authentication Video](https://tk-assets.lambdaschool.com/a8f55011-48ef-4eb9-af2b-17a7b92fae64_FirebaseVideo.png)](https://youtu.be/vGeuZtHmcMM) - -1. Create a new Firebase project (or use an existing one). -2. Change the project's bundle identifier to your own bundle identifier (e.g. `com.JohnSmith.LambdaTimeline`) -3. In the "Project Overview" in your Firebase project, you will need to add your app as we are using the Firebase SDK in our Xcode project. You will need to add the "GoogleService-Info.plist" file that will be given to you when you add the app. -4. Please refer to this page: https://firebase.google.com/docs/auth/ios/firebaseui and follow the steps under the “Set up sign-in methods”. - 1. You will only need to do the two steps under the [Google section](https://firebase.google.com/docs/auth/ios/firebaseui#google). - 2. The starter project will have a URL type already. You just need to paste the right URL scheme in. - 3. You can find the URL Type in your project file in the `Project Settings > Info > URL Types (expand) > URL Schemes` -5. In the Firebase project's database, change the rules to: -``` JSON -{ - "rules": { - ".read": "auth != null", - ".write": "auth != null" - } -} -``` -This will allow only users of the app who are authenticated to access the database. (Authentication is already taken care of in the starter project) - -6. In the left pane of your Firebase project under "Develop", click the Storage item. Click the "Get Started" button and it will pull up a modal window about security rules. Simply click "Got it". It will set Storage's rules to allow access to any authenticated user, which works great for our uses. - -Firebase Storage is essentially a Google Drive for data in your Firebase. It makes sense to use Storage in this application as we will be storing images, audio, and video data. If you're curious as to how Database and Storage interact, feel free to read Firebase's Storage documentation and look at the code in the base project. Particularly in the `Post`, `Media` and `PostController` objects. (Don't feel like you have to, however) - -At this point, run the app on your simulator or physical device in order to make sure that you've set up your Firebase Project correctly. If set up correctly, you should be able to create posts, comment on them, and have them get sent to Firebase. You should also be able to re-run the app and have the posts and comments get fetched correctly. If this does not work, the likely scenario is that you've not set up your Firebase project correctly. If you can't figure out what's wrong, please reach out to your TL. - -*Note: If you have a free Apple Developer account you may need to reuse your Bundle ID from a previous project to run on a real device (Apple limits you to three unique identifiers).* - -### Part 3: Feature Integration - -1. Integrate your `ImagePostViewController` into the Lambda Timeline Firebase project. -2. You should be able to create a new post, select a photo, edit the photo, and post it to your Firebase server. +1. Integrate your `ImagePostViewController` into the Lambda Timeline project. +2. You should be able to create a new post, select a photo, edit the photo, and create an image post. +3. Be sure that you are able to add comments to posts. ## Go Further From 8d31ab91250b4f70329d12f18fd49764449c382c Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Wed, 28 Oct 2020 19:54:24 -0700 Subject: [PATCH 03/15] Started Project --- LambdaTimeline.xcodeproj/project.pbxproj | 4 +++- .../xcshareddata/xcschemes/LambdaTimeline.xcscheme | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj index b2af5414..f8001cfa 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -185,7 +185,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1000; - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "Lambda School"; TargetAttributes = { 46463777216FDE4B00E7FF73 = { @@ -285,6 +285,7 @@ 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; @@ -346,6 +347,7 @@ 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; diff --git a/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme b/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme index d1c2f96e..8b66f88a 100644 --- a/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme +++ b/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme @@ -1,6 +1,6 @@ Date: Thu, 29 Oct 2020 16:05:00 -0700 Subject: [PATCH 04/15] Storyboard --- LambdaTimeline/Storyboards/Main.storyboard | 231 +++++++++++++++--- .../ImagePostViewController.swift | 12 +- 2 files changed, 200 insertions(+), 43 deletions(-) diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index 9e2d25d6..81e834fd 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -1,10 +1,11 @@ - - + + - + + @@ -13,9 +14,9 @@ - + - + @@ -24,7 +25,7 @@ - + @@ -108,23 +109,23 @@ - + - + - + - + - + - + + + + + + + + - + + @@ -170,7 +226,6 @@ - @@ -191,6 +246,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -198,7 +352,7 @@ - + @@ -215,35 +369,36 @@ - + - - + - + + @@ -257,7 +412,6 @@ - @@ -273,15 +427,15 @@ - + - + - + - + @@ -290,16 +444,16 @@ - + - + - + - + + + + diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift index 9cffbbbb..18800fae 100644 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -33,12 +33,12 @@ class ImagePostViewController: ShiftableViewController { 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) + DispatchQueue.main.async { + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.sourceType = .photoLibrary + self.present(imagePicker, animated: true, completion: nil) + } } @IBAction func createPost(_ sender: Any) { From 0e22b208f905c7d74b56caa729c0982aca56762a Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Thu, 29 Oct 2020 17:31:07 -0700 Subject: [PATCH 05/15] Filters --- LambdaTimeline.xcodeproj/project.pbxproj | 12 +- .../Helpers/Extensions/UIImage+Ratio.swift | 29 ++ LambdaTimeline/Storyboards/Main.storyboard | 72 ++-- .../ImageFilterViewController.swift | 316 ++++++++++++++++++ .../ImagePostViewController.swift | 66 +++- 5 files changed, 462 insertions(+), 33 deletions(-) create mode 100644 LambdaTimeline/View Controllers/ImageFilterViewController.swift diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj index f8001cfa..cf708b98 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1AC9C5CE254B86BD00CDD572 /* ImageFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */; }; 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 */; }; @@ -25,6 +26,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilterViewController.swift; sourceTree = ""; }; 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 = ""; }; @@ -128,6 +130,7 @@ 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */, 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, + 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -232,6 +235,7 @@ files = ( 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */, 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, + 1AC9C5CE254B86BD00CDD572 /* ImageFilterViewController.swift in Sources */, 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, 46463790216FFD1B00E7FF73 /* Post.swift in Sources */, @@ -311,7 +315,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -367,7 +371,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -384,7 +388,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -403,7 +407,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift b/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift index 79bb32d7..cf844b92 100644 --- a/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift +++ b/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift @@ -13,3 +13,32 @@ extension UIImage { return size.height / size.width } } + +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/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index 81e834fd..17631c46 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -107,7 +107,7 @@ - + @@ -153,9 +153,9 @@ @@ -236,6 +242,9 @@ + + + @@ -246,52 +255,58 @@ - + - + - + - + - + - + - + + + + - + + + + - + + + + @@ -340,10 +361,21 @@ + + + + + + + + + + + - + diff --git a/LambdaTimeline/View Controllers/ImageFilterViewController.swift b/LambdaTimeline/View Controllers/ImageFilterViewController.swift new file mode 100644 index 00000000..524fbd4d --- /dev/null +++ b/LambdaTimeline/View Controllers/ImageFilterViewController.swift @@ -0,0 +1,316 @@ +// +// ImageFilterViewController.swift +// LambdaTimeline +// +// Created by Cora Jacobson on 10/29/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import CoreImage +import CoreImage.CIFilterBuiltins + +enum FilterType: String, CaseIterable { + case none = "No Filter" + case hueAdjust = "Hue Adjust" + case colorMonochrome = "Color Monochrome" + case bumpDistortion = "Bump Distortion" + case twirlDistortion = "Twirl Distortion" + case crystallize = "Crystallize" +} + +protocol FilterVCDelegate: AnyObject { + func updateImage(originalImage: UIImage, filteredImage: UIImage?) +} + +class ImageFilterViewController: UIViewController { + + @IBOutlet private var imageView: UIImageView! + @IBOutlet private var pickerView: UIPickerView! + @IBOutlet private var label1: UILabel! + @IBOutlet private var slider1: UISlider! + @IBOutlet private var label2: UILabel! + @IBOutlet private var slider2: UISlider! + @IBOutlet private var label3: UILabel! + @IBOutlet private var slider3: UISlider! + @IBOutlet private var imageHeightConstraint: NSLayoutConstraint! + + var originalImage: UIImage? + var filteredImage: UIImage? + var scaledImage: CIImage? { + didSet { + updateImage() + } + } + + private let filters = FilterType.allCases.map { $0.rawValue } + private var activeFilter: FilterType = .none + private var touchPoint: CGPoint? + private let context = CIContext() + weak var delegate: FilterVCDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + pickerView.delegate = self + pickerView.dataSource = self + setUpView() + } + + @IBAction func slider1Changed(_ sender: UISlider) { + updateImage() + } + + @IBAction func slider2Changed(_ sender: UISlider) { + updateImage() + } + + @IBAction func slider3Changed(_ sender: UISlider) { + updateImage() + } + + @IBAction func applyFilters(_ sender: UIButton) { + if let originImage = originalImage { + self.delegate?.updateImage(originalImage: originImage, filteredImage: filteredImage) + } + dismiss(animated: true, completion: nil) + } + + private func setUpView() { + if let filteredImage = filteredImage { + imageView.image = filteredImage + var scaledSize = imageView.bounds.size + let scale = imageView.contentScaleFactor + scaledSize.width *= scale + scaledSize.height *= scale + + guard let scaledUIImage = filteredImage.imageByScaling(toSize: scaledSize) else { + scaledImage = nil + return + } + scaledImage = CIImage(image: scaledUIImage) + } else if let originalImage = originalImage { + imageView.image = originalImage + var scaledSize = imageView.bounds.size + let scale = imageView.contentScaleFactor + scaledSize.width *= scale + scaledSize.height *= scale + + guard let scaledUIImage = originalImage.imageByScaling(toSize: scaledSize) else { + scaledImage = nil + return + } + scaledImage = CIImage(image: scaledUIImage) + } + imageView.isUserInteractionEnabled = true + label1.isHidden = true + label2.isHidden = true + label3.isHidden = true + slider1.isHidden = true + slider2.isHidden = true + slider3.isHidden = true + } + + private func setImageViewHeight(with aspectRatio: CGFloat) { + imageHeightConstraint.constant = imageView.frame.size.width * aspectRatio + view.layoutSubviews() + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + if let touch = touches.first { + let position = touch.location(in: view) + if imageView.frame.contains(position) { + let xPosition = position.x - imageView.frame.minX + let yPosition = imageView.bounds.height - (position.y - imageView.frame.minY) + touchPoint = CGPoint(x: xPosition, y: yPosition) + updateImage() + } + } + } + + private func updateImage() { + if let scaledImage = scaledImage { + switch activeFilter { + case .hueAdjust: + imageView.image = hueAdjust(byFiltering: scaledImage) + filteredImage = imageView.image + case .colorMonochrome: + imageView.image = colorMonochrome(byFiltering: scaledImage) + filteredImage = imageView.image + case .bumpDistortion: + imageView.image = bumpDistortion(byFiltering: scaledImage) + filteredImage = imageView.image + case .twirlDistortion: + imageView.image = twirlDistortion(byFiltering: scaledImage) + filteredImage = imageView.image + case .crystallize: + imageView.image = crystallize(byFiltering: scaledImage) + filteredImage = imageView.image + default: + imageView.image = originalImage + filteredImage = nil + } + } else { + imageView.image = nil + filteredImage = nil + } + } + + private func noFilterSetUp() { + slider1.isHidden = true + slider2.isHidden = true + slider3.isHidden = true + activeFilter = .none + imageView.image = originalImage + } + + private func hueAdjustSetUp() { + slider1.isHidden = false + slider1.maximumValue = (.pi * 2) + slider1.setValue(.pi, animated: true) + slider2.isHidden = true + slider3.isHidden = true + activeFilter = .hueAdjust + imageView.image = originalImage + updateImage() + } + + private func colorMonochromeSetUp() { + slider1.isHidden = false + slider1.maximumValue = 1 + slider1.setValue(0.75, animated: true) + slider2.isHidden = false + slider2.maximumValue = 1 + slider2.setValue(0, animated: true) + slider3.isHidden = false + slider3.maximumValue = 1 + slider3.setValue(1, animated: true) + activeFilter = .colorMonochrome + imageView.image = originalImage + updateImage() + } + + private func bumpDistortionSetUp() { + slider1.isHidden = false + slider1.maximumValue = 300 + slider1.setValue(150, animated: true) + slider2.isHidden = false + slider2.maximumValue = 1 + slider2.setValue(0.5, animated: true) + slider3.isHidden = true + activeFilter = .bumpDistortion + imageView.image = originalImage + updateImage() + } + + private func twirlDistortionSetUp() { + slider1.isHidden = false + slider1.maximumValue = 200 + slider1.setValue(100, animated: true) + slider2.isHidden = false + slider2.maximumValue = (.pi * 2) + slider2.setValue(.pi, animated: true) + slider3.isHidden = true + activeFilter = .twirlDistortion + imageView.image = originalImage + updateImage() + } + + private func crystallizeSetUp() { + slider1.isHidden = false + slider1.maximumValue = 20 + slider1.setValue(10, animated: true) + slider2.isHidden = true + slider3.isHidden = true + activeFilter = .crystallize + imageView.image = originalImage + updateImage() + } + + private func hueAdjust(byFiltering inputImage: CIImage) -> UIImage? { + let hueAdjustFilter = CIFilter.hueAdjust() + hueAdjustFilter.inputImage = inputImage + hueAdjustFilter.angle = slider1.value + guard let outputImage = hueAdjustFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + + private func colorMonochrome(byFiltering inputImage: CIImage) -> UIImage? { + let colorMonochromeFilter = CIFilter.colorMonochrome() + colorMonochromeFilter.inputImage = inputImage + colorMonochromeFilter.color = CIColor(red: CGFloat(slider1.value), green: CGFloat(slider2.value), blue: CGFloat(slider3.value), alpha: 1) + guard let outputImage = colorMonochromeFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + + private func bumpDistortion(byFiltering inputImage: CIImage) -> UIImage? { + let bumpDistortionFilter = CIFilter.bumpDistortion() + bumpDistortionFilter.inputImage = inputImage + bumpDistortionFilter.radius = slider1.value + bumpDistortionFilter.scale = slider2.value + let centerWidth = CGFloat(inputImage.extent.width / 2) + let centerHeight = CGFloat(inputImage.extent.height / 2) + let center = CGPoint(x: centerWidth, y: centerHeight) + bumpDistortionFilter.center = touchPoint ?? center + guard let outputImage = bumpDistortionFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + + private func twirlDistortion(byFiltering inputImage: CIImage) -> UIImage? { + let twirlDistortionFilter = CIFilter.twirlDistortion() + twirlDistortionFilter.inputImage = inputImage + twirlDistortionFilter.radius = slider1.value + twirlDistortionFilter.angle = slider2.value + let centerWidth = CGFloat(inputImage.extent.width / 2) + let centerHeight = CGFloat(inputImage.extent.height / 2) + let center = CGPoint(x: centerWidth, y: centerHeight) + twirlDistortionFilter.center = touchPoint ?? center + guard let outputImage = twirlDistortionFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + + private func crystallize(byFiltering inputImage: CIImage) -> UIImage? { + let crystallizeFilter = CIFilter.crystallize() + crystallizeFilter.inputImage = inputImage + crystallizeFilter.radius = slider1.value + guard let outputImage = crystallizeFilter.outputImage else { return nil } + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + return UIImage(cgImage: renderedCGImage) + } + +} + +extension ImageFilterViewController: UIPickerViewDelegate, UIPickerViewDataSource { + func numberOfComponents(in pickerView: UIPickerView) -> Int { + 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + filters.count + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + filters[row] + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + switch row { + case 1: + hueAdjustSetUp() + case 2: + colorMonochromeSetUp() + case 3: + bumpDistortionSetUp() + case 4: + twirlDistortionSetUp() + case 5: + crystallizeSetUp() + default: + noFilterSetUp() + } + } + +} diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift index 18800fae..6858a9ab 100644 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -11,20 +11,40 @@ 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! + @IBOutlet private var imageView: UIImageView! + @IBOutlet private var titleTextField: UITextField! + @IBOutlet private var chooseImageButton: UIButton! + @IBOutlet private var imageHeightConstraint: NSLayoutConstraint! + @IBOutlet private var postButton: UIBarButtonItem! + + @IBOutlet private var filterImageButton: UIButton! + @IBOutlet private var filterAgainButton: UIButton! + @IBOutlet private var clearFiltersButton: UIButton! var postController: PostController! var post: Post? var imageData: Data? + var originalImage: UIImage? { + didSet { + filterImageButton.isHidden = false + } + } + var filteredImage: UIImage? { + didSet { + self.filterImageButton.isHidden = true + self.filterAgainButton.isHidden = false + self.clearFiltersButton.isHidden = false + self.imageView.image = self.filteredImage + } + } + override func viewDidLoad() { super.viewDidLoad() - setImageViewHeight(with: 1.0) + filterImageButton.isHidden = true + filterAgainButton.isHidden = true + clearFiltersButton.isHidden = true } private func presentImagePickerController() { @@ -86,8 +106,29 @@ class ImagePostViewController: ShiftableViewController { presentImagePickerController() } - @IBAction func addFilter(_ sender: Any) { - + @IBAction func filterImage(_ sender: UIButton) { + let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let filterVC = storyBoard.instantiateViewController(withIdentifier: "ImageFilterViewController") as! ImageFilterViewController + filterVC.originalImage = originalImage + filterVC.delegate = self + self.present(filterVC, animated: true, completion: nil) + } + + @IBAction func filterAgain(_ sender: UIButton) { + let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let filterVC = storyBoard.instantiateViewController(withIdentifier: "ImageFilterViewController") as! ImageFilterViewController + filterVC.originalImage = originalImage + filterVC.filteredImage = filteredImage + filterVC.delegate = self + self.present(filterVC, animated: true, completion: nil) + } + + @IBAction func clearFilters(_ sender: UIButton) { + filteredImage = nil + imageView.image = originalImage + filterImageButton.isHidden = false + filterAgainButton.isHidden = true + clearFiltersButton.isHidden = true } func setImageViewHeight(with aspectRatio: CGFloat) { @@ -109,7 +150,7 @@ extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigation guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return } imageView.image = image - + originalImage = image setImageViewHeight(with: image.ratio) } @@ -117,3 +158,10 @@ extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigation picker.dismiss(animated: true, completion: nil) } } + +extension ImagePostViewController: FilterVCDelegate { + func updateImage(originalImage: UIImage, filteredImage: UIImage?) { + self.originalImage = originalImage + self.filteredImage = filteredImage + } +} From 3767ed8245451a6fc88e42aed8a3a55470d24f44 Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Thu, 29 Oct 2020 17:36:11 -0700 Subject: [PATCH 06/15] Bug Fix - CommentCell --- LambdaTimeline/Storyboards/Main.storyboard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index 17631c46..4e1199c4 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -506,7 +506,7 @@ - + From a86af4cc28c65a9b4b56063c8e6856e5769fd1bc Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Thu, 29 Oct 2020 18:02:15 -0700 Subject: [PATCH 07/15] Bug Fix - imageView in FilterVC --- .../ImageFilterViewController.swift | 43 +++++++++++++++---- .../ImagePostViewController.swift | 1 + 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/LambdaTimeline/View Controllers/ImageFilterViewController.swift b/LambdaTimeline/View Controllers/ImageFilterViewController.swift index 524fbd4d..d50a166d 100644 --- a/LambdaTimeline/View Controllers/ImageFilterViewController.swift +++ b/LambdaTimeline/View Controllers/ImageFilterViewController.swift @@ -146,8 +146,11 @@ class ImageFilterViewController: UIViewController { imageView.image = crystallize(byFiltering: scaledImage) filteredImage = imageView.image default: - imageView.image = originalImage - filteredImage = nil + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } } } else { imageView.image = nil @@ -160,7 +163,11 @@ class ImageFilterViewController: UIViewController { slider2.isHidden = true slider3.isHidden = true activeFilter = .none - imageView.image = originalImage + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } } private func hueAdjustSetUp() { @@ -170,7 +177,11 @@ class ImageFilterViewController: UIViewController { slider2.isHidden = true slider3.isHidden = true activeFilter = .hueAdjust - imageView.image = originalImage + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } updateImage() } @@ -185,7 +196,11 @@ class ImageFilterViewController: UIViewController { slider3.maximumValue = 1 slider3.setValue(1, animated: true) activeFilter = .colorMonochrome - imageView.image = originalImage + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } updateImage() } @@ -198,7 +213,11 @@ class ImageFilterViewController: UIViewController { slider2.setValue(0.5, animated: true) slider3.isHidden = true activeFilter = .bumpDistortion - imageView.image = originalImage + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } updateImage() } @@ -211,7 +230,11 @@ class ImageFilterViewController: UIViewController { slider2.setValue(.pi, animated: true) slider3.isHidden = true activeFilter = .twirlDistortion - imageView.image = originalImage + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } updateImage() } @@ -222,7 +245,11 @@ class ImageFilterViewController: UIViewController { slider2.isHidden = true slider3.isHidden = true activeFilter = .crystallize - imageView.image = originalImage + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } updateImage() } diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift index 6858a9ab..9e8899a2 100644 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -110,6 +110,7 @@ class ImagePostViewController: ShiftableViewController { let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) let filterVC = storyBoard.instantiateViewController(withIdentifier: "ImageFilterViewController") as! ImageFilterViewController filterVC.originalImage = originalImage + filterVC.filteredImage = nil filterVC.delegate = self self.present(filterVC, animated: true, completion: nil) } From ef2ca76ac4f5889bff3bd695d0ccc649bb9992ee Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Sat, 31 Oct 2020 11:34:25 -0700 Subject: [PATCH 08/15] Step 1 - added optional audioURL to Comment --- LambdaTimeline/Models/Comment.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LambdaTimeline/Models/Comment.swift b/LambdaTimeline/Models/Comment.swift index a7dad6f5..01d9126b 100644 --- a/LambdaTimeline/Models/Comment.swift +++ b/LambdaTimeline/Models/Comment.swift @@ -15,11 +15,13 @@ class Comment: Hashable { static private let timestampKey = "timestamp" let text: String? + let audioURL: URL? let author: String let timestamp: Date - init(text: String? = nil, author: String, timestamp: Date = Date()) { + init(text: String? = nil, audioURL: URL? = nil, author: String, timestamp: Date = Date()) { self.text = text + self.audioURL = audioURL self.author = author self.timestamp = timestamp } From ee2764e94a0c6c1d3688f23d4a93da50f2860e32 Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Sat, 31 Oct 2020 11:55:47 -0700 Subject: [PATCH 09/15] Step 2 - added function to allow creation of a comment with an audioURL --- LambdaTimeline/Model Controllers/PostController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/LambdaTimeline/Model Controllers/PostController.swift b/LambdaTimeline/Model Controllers/PostController.swift index 3df87148..190d59bf 100644 --- a/LambdaTimeline/Model Controllers/PostController.swift +++ b/LambdaTimeline/Model Controllers/PostController.swift @@ -33,4 +33,11 @@ class PostController { post.comments.append(comment) } + + func addAudioComment(with url: URL, to post: inout Post) { + guard let currentUser = currentUser else { return } + let comment = Comment(audioURL: url, author: currentUser) + post.comments.append(comment) + } + } From a05886f1e69ca8a2dd5bbbe7b0adbe52bd6a209e Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Thu, 5 Nov 2020 13:16:24 -0800 Subject: [PATCH 10/15] Step 3 - Audio Recorder --- LambdaTimeline.xcodeproj/project.pbxproj | 8 + LambdaTimeline/Resources/Info.plist | 2 + LambdaTimeline/Storyboards/Main.storyboard | 121 ++++++- .../AudioRecorderController.swift | 299 ++++++++++++++++++ .../ImagePostDetailTableViewController.swift | 20 +- LambdaTimeline/Views/AudioVisualizer.swift | 275 ++++++++++++++++ 6 files changed, 722 insertions(+), 3 deletions(-) create mode 100644 LambdaTimeline/View Controllers/AudioRecorderController.swift create mode 100644 LambdaTimeline/Views/AudioVisualizer.swift diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj index cf708b98..65eda2e4 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1A51ECEB255390B900643EB1 /* AudioRecorderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A51ECEA255390B900643EB1 /* AudioRecorderController.swift */; }; + 1A51ECEE2553938000643EB1 /* AudioVisualizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A51ECED2553938000643EB1 /* AudioVisualizer.swift */; }; 1AC9C5CE254B86BD00CDD572 /* ImageFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */; }; 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */; }; 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46463782216FDE4C00E7FF73 /* Assets.xcassets */; }; @@ -26,6 +28,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1A51ECEA255390B900643EB1 /* AudioRecorderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderController.swift; sourceTree = ""; }; + 1A51ECED2553938000643EB1 /* AudioVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVisualizer.swift; sourceTree = ""; }; 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilterViewController.swift; sourceTree = ""; }; 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 = ""; }; @@ -131,6 +135,7 @@ 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */, + 1A51ECEA255390B900643EB1 /* AudioRecorderController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -139,6 +144,7 @@ isa = PBXGroup; children = ( 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */, + 1A51ECED2553938000643EB1 /* AudioVisualizer.swift */, ); path = Views; sourceTree = ""; @@ -237,6 +243,8 @@ 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, 1AC9C5CE254B86BD00CDD572 /* ImageFilterViewController.swift in Sources */, 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, + 1A51ECEE2553938000643EB1 /* AudioVisualizer.swift in Sources */, + 1A51ECEB255390B900643EB1 /* AudioRecorderController.swift in Sources */, 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, 46463790216FFD1B00E7FF73 /* Post.swift in Sources */, 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */, diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist index dab10c0a..acb4ea99 100644 --- a/LambdaTimeline/Resources/Info.plist +++ b/LambdaTimeline/Resources/Info.plist @@ -2,6 +2,8 @@ + NSMicrophoneUsageDescription + $(PRODUCT_NAME) needs access to your microphone to record audio CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index 4e1199c4..e4dde766 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -2,7 +2,6 @@ - @@ -555,9 +554,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LambdaTimeline/View Controllers/AudioRecorderController.swift b/LambdaTimeline/View Controllers/AudioRecorderController.swift new file mode 100644 index 00000000..0265997c --- /dev/null +++ b/LambdaTimeline/View Controllers/AudioRecorderController.swift @@ -0,0 +1,299 @@ +// +// AudioRecorderController.swift +// LambdaTimeline +// +// Created by Cora Jacobson on 11/4/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import AVFoundation + +protocol AudioRecorderDelegate: AnyObject { + func updatePostWithAudioComment(url: URL) +} + +class AudioRecorderController: UIViewController { + + // MARK: - Outlets + + @IBOutlet private var playButton: UIButton! + @IBOutlet private var recordButton: UIButton! + @IBOutlet private var timeElapsedLabel: UILabel! + @IBOutlet private var timeRemainingLabel: UILabel! + @IBOutlet private var timeSlider: UISlider! + @IBOutlet private var audioVisualizer: AudioVisualizer! + @IBOutlet private var saveButton: UIButton! + + // MARK: - Properties + + var post: Post! + weak var delegate: AudioRecorderDelegate! + + var audioPlayer: AVAudioPlayer? { + didSet { + guard let audioPlayer = audioPlayer else { return } + audioPlayer.delegate = self + audioPlayer.isMeteringEnabled = true + updateViews() + } + } + + var recordingURL: URL? + var audioRecorder: AVAudioRecorder? + + private lazy var timeIntervalFormatter: DateComponentsFormatter = { + let formatting = DateComponentsFormatter() + formatting.unitsStyle = .positional + formatting.zeroFormattingBehavior = .pad + formatting.allowedUnits = [.minute, .second] + return formatting + }() + + // MARK: - View Controller Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + timeElapsedLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeElapsedLabel.font.pointSize, + weight: .regular) + timeRemainingLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeRemainingLabel.font.pointSize, + weight: .regular) + saveButton.isHidden = true +// playButton.isHidden = true + } + + private 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 + + weak var timer: Timer? + + func startTimer() { + timer?.invalidate() + + timer = Timer.scheduledTimer(withTimeInterval: 0.030, repeats: true) { [weak self] (_) in + guard let self = self else { return } + + self.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: []) + } + + func play() { + guard let recordingURL = recordingURL else { return } + do { + try prepareAudioSession() + audioPlayer = try AVAudioPlayer(contentsOf: recordingURL) + audioPlayer?.play() + updateViews() + startTimer() + } catch { + print("Cannot play audio: \(error)") + } + } + + func pause() { + audioPlayer?.pause() + updateViews() + cancelTimer() + } + + + // MARK: - Recording + + var isRecording: Bool { + audioRecorder?.isRecording ?? false + } + + func createNewRecordingURL() -> URL { + let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + + let name = ISO8601DateFormatter.string(from: Date(), timeZone: .current, formatOptions: .withInternetDateTime) + let file = documents.appendingPathComponent(name, isDirectory: false).appendingPathExtension("caf") + + print("recording URL: \(file)") + + return file + } + + func requestPermissionOrStartRecording() { + switch AVAudioSession.sharedInstance().recordPermission { + case .undetermined: + AVAudioSession.sharedInstance().requestRecordPermission { granted in + guard granted == true else { + print("We need microphone access") + return + } + + print("Recording permission has been granted!") + // NOTE: Invite the user to tap record again, since we just interrupted them, and they may not have been ready to record + } + case .denied: + print("Microphone access has been blocked.") + + let alertController = UIAlertController(title: "Microphone Access Denied", message: "Please allow this app to access your Microphone.", preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "Open Settings", style: .default) { (_) in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + }) + + alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil)) + + present(alertController, animated: true, completion: nil) + case .granted: + startRecording() + @unknown default: + break + } + } + + func startRecording() { + do { + try prepareAudioSession() + } catch { + print("Cannot record audio: \(error)") + return + } + + recordingURL = createNewRecordingURL() + let format = AVAudioFormat(standardFormatWithSampleRate: 44_100, channels: 1)! + do { + audioRecorder = try AVAudioRecorder(url: recordingURL!, format: format) + audioRecorder?.delegate = self + audioRecorder?.isMeteringEnabled = true + audioRecorder?.record() + startTimer() + } catch { + preconditionFailure("The audio recorder could not be created with \(recordingURL!) and \(format)") + } + } + + func stopRecording() { + audioRecorder?.stop() + cancelTimer() + } + + // MARK: - Actions + + @IBAction func togglePlayback(_ sender: Any) { + if isPlaying { + pause() + } else { + play() + } + } + + @IBAction func updateCurrentTime(_ sender: UISlider) { + if isPlaying { + pause() + } + audioPlayer?.currentTime = TimeInterval(sender.value) + updateViews() + } + + @IBAction func toggleRecording(_ sender: Any) { + if isRecording { + stopRecording() + } else { + requestPermissionOrStartRecording() + } + } + + @IBAction func saveAudioButton(_ sender: UIButton) { + guard let recordingURL = recordingURL else { return } + delegate.updatePostWithAudioComment(url: recordingURL) + dismiss(animated: true, completion: nil) + } +} + +extension AudioRecorderController: 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 AudioRecorderController: AVAudioRecorderDelegate { + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if let recordingURL = recordingURL { + saveButton.isHidden = false + updateViews() + do { + audioPlayer = try AVAudioPlayer(contentsOf: recordingURL) + playButton.isHidden = false + } catch { + print("Error playing back recording") + } + } + } + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + if let error = error { + print("Audio recorder error: \(error)") + } + } +} diff --git a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift index c108edd7..7bbc8599 100644 --- a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift @@ -41,7 +41,7 @@ class ImagePostDetailTableViewController: UITableViewController { @IBAction func createComment(_ sender: Any) { - let alert = UIAlertController(title: "Add a comment", message: "Write your comment below:", preferredStyle: .alert) + let alert = UIAlertController(title: "Add a comment", message: "Write your comment below or choose Audio Comment", preferredStyle: .alert) var commentTextField: UITextField? @@ -50,7 +50,7 @@ class ImagePostDetailTableViewController: UITableViewController { commentTextField = textField } - let addCommentAction = UIAlertAction(title: "Add Comment", style: .default) { (_) in + let addCommentAction = UIAlertAction(title: "Add Text Comment", style: .default) { (_) in guard let commentText = commentTextField?.text else { return } @@ -61,9 +61,18 @@ class ImagePostDetailTableViewController: UITableViewController { } } + let addAudioCommentAction = UIAlertAction(title: "Add Audio Comment", style: .default) { (_) in + let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let audioVC = storyBoard.instantiateViewController(withIdentifier: "AudioRecorderController") as! AudioRecorderController + audioVC.delegate = self + audioVC.post = self.post + self.present(audioVC, animated: true, completion: nil) + } + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alert.addAction(addCommentAction) + alert.addAction(addAudioCommentAction) alert.addAction(cancelAction) present(alert, animated: true, completion: nil) @@ -84,3 +93,10 @@ class ImagePostDetailTableViewController: UITableViewController { return cell } } + +extension ImagePostDetailTableViewController: AudioRecorderDelegate { + func updatePostWithAudioComment(url: URL) { + postController.addAudioComment(with: url, to: &post) + tableView.reloadData() + } +} diff --git a/LambdaTimeline/Views/AudioVisualizer.swift b/LambdaTimeline/Views/AudioVisualizer.swift new file mode 100644 index 00000000..697eb8f2 --- /dev/null +++ b/LambdaTimeline/Views/AudioVisualizer.swift @@ -0,0 +1,275 @@ +// +// AudioVisualizer.swift +// LambdaTimeline +// +// Created by Cora Jacobson on 11/4/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit + +@IBDesignable +public class AudioVisualizer: UIView { + + // MARK: IBInspectable Properties + + /// The width of a bar in points. + @IBInspectable public var barWidth: CGFloat = 10 { + didSet { + updateBars() + } + } + + /// The corner radius of a bar in points. If less than `0`, then it will default to half of the width of the bar. + @IBInspectable public var barCornerRadius: CGFloat = -1 { + didSet { + updateBars() + } + } + + /// The spacing between bars in points. + @IBInspectable public var barSpacing: CGFloat = 4 { + didSet { + updateBars() + } + } + + /// The color of a bar. + @IBInspectable public var barColor: UIColor = .systemGray { + didSet { + for bar in bars { + bar.backgroundColor = barColor + } + } + } + + /// The amount of time before a bar decays into the adjacent spot + @IBInspectable public var decaySpeed: Double = 0.01 { + didSet { + decayTimer?.invalidate() + decayTimer = nil + } + } + + /// The fraction the newest value will decay by if not updated by the time a decay starts + @IBInspectable public var decayAmount: Double = 0.8 + + // MARK: Internal Properties + + private var bars = [UIView]() + private var values = [Double]() + + private weak var decayTimer: Timer? + private var newestValue: Double = 0 + + // MARK: - Object Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + + initialSetup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + initialSetup() + } + + func initialSetup() { + // Pre-fill values for Interface Builder preview + #if TARGET_INTERFACE_BUILDER + values = [ + 0.19767167952644904, + 0.30975147553721694, + 0.2717680681330001, + 0.25914398105158504, + 0.3413322535900626, + 0.311223010327166, + 0.3302641160440099, + 0.303853272136915, + 0.2659123465612464, + 0.2860924489760262, + 0.26477145407733543, + 0.23180693200970012, + 0.24445487891619533, + 0.21484121767935302, + 0.19688917099112885, + 0.19020094289324854, + 0.17402194532721785, + 0.1600055988294578, + 0.15120753744055154, + 0.13789741397752767, + 0.13231033268544698, + 0.1270923459375989, + 0.1121238175344413, + 0.12400069790665748, + 0.24978783142512598, + 0.233063298365594, + 0.5375441947045457, + 0.47456518731446534, + 0.5236630241490436, + 0.4692151822551929, + 0.4255172022748686, + 0.46023063710569184, + 0.42934908823397355, + 0.37221041959882545, + 0.4685050055667653, + 0.4209394065681193, + 0.46643118034506187, + 0.4292307341708633, + 0.3814422662003417, + 0.4386719969186142, + 0.3956598546828729 + ] + #endif + + // Build the inner bars + self.updateBars() + } + + deinit { + // Invalidate the timer if it is still active + decayTimer?.invalidate() + } + + // MARK: - Layout + override public func layoutSubviews() { + updateBars() + } + + private func updateBars() { + // Clean up old bars + for bar in bars { + bar.removeFromSuperview() + } + + var newBars = [UIView]() + + + // Make sure the width of a bar and spacing is greater than 0, and that the available width is also greater than 0 + guard round(barWidth) > 0, barSpacing >= 0, bounds.width > 0, bounds.height > 0 else { + // Not enough information to create a single bar, so bail early + bars = [] + return + } + + // Calculate number of bars we will be able to display + var numberOfBarsToCreate = Int(bounds.width/(barWidth + barSpacing)) + + // Helper function for creating bars + func createBar(_ positionFromCenter: Int) { + let bar = UIView(frame: frame(forBar: positionFromCenter)) + bar.backgroundColor = barColor + bar.layer.cornerRadius = (barCornerRadius < 0 || barCornerRadius > barWidth/2) ? floor(barWidth/3) : barCornerRadius + + numberOfBarsToCreate -= 1 + newBars.append(bar) + self.addSubview(bar) + } + + // Create the center bar + createBar(0) + + // Keep creating bars in pairs until there is no more room + var position = 1 + while numberOfBarsToCreate > 0 { + // Create the symmetric pairs of bars starting from the center + createBar(-position) + createBar(position) + + position += 1 + } + + bars = newBars + } + + /// Calculate the frame of a particular bar + /// - Parameter positionFromCenter: The distance of the bar from the center (which is 0) + private func frame(forBar positionFromCenter: Int) -> CGRect { + let valueIndex = Int(positionFromCenter.magnitude) + + return frame(forBar: positionFromCenter, value: (valueIndex < values.count) ? values[valueIndex] : 0) + } + + /// Calculate the frame of a particular bar, specifying a value + /// - Parameter positionFromCenter: The distance of the bar from the center (which is 0) + private func frame(forBar positionFromCenter: Int, value: Double) -> CGRect { + let maxValue = (1 - CGFloat(positionFromCenter.magnitude)*(barWidth + barSpacing)/bounds.width/2)*bounds.height/2 + let height = CGFloat(value)*maxValue + + return CGRect(x: floor(bounds.width/2) + CGFloat(positionFromCenter)*(barWidth + barSpacing) - barWidth/2, y: floor(bounds.height/2) - height, width: barWidth, height: height*2) + } + + // MARK: - Animation + + /// Start the decay timer, but only if if hasn't been created yet + private func startTimer() { + guard decayTimer == nil else { return } + + decayTimer = Timer.scheduledTimer(withTimeInterval: decaySpeed, repeats: true) { [weak self] (_) in + guard let self = self else { return } + + self.decayNewestValue() + } + } + + private func decayNewestValue() { + values.insert(newestValue, at: 0) + + // Trim the end of the values array if there are too many for the number of bars + let currentCount = values.count + let maxCount = (bars.count + 1)/2 + /* + Note that the amount of bars will always be either 0, or an odd number (since the bars are counted in pairs after the first central bar), so we chose a "transformation" (a mathematical function) that satisfies this: value index = floor((bar index + 1)/2) + + Bar index: 0 1 2 3 4 5 6 7 8 9 ... + (valid bar index): 0 1 - 3 - 5 - 7 - 9 ... + Value index: 0 1 1 2 2 3 3 4 4 5 ... + + */ + if currentCount > maxCount { + values.removeSubrange(maxCount ..< currentCount) + } + + // Update the frames of each bar + for (positionFromCenter, value) in values.enumerated() { + if positionFromCenter == 0 { + bars[0].frame = frame(forBar: positionFromCenter, value: value) + } else { + bars[positionFromCenter*2 - 1].frame = frame(forBar: -positionFromCenter, value: value) + bars[positionFromCenter*2].frame = frame(forBar: positionFromCenter, value: value) + } + } + + // Decay the newest value + newestValue = newestValue*decayAmount + + // Check if the values are empty + let totalValue = values.reduce(0.0) { $0 + $1 } + if totalValue <= 0.000001 { + // Note that total value may never reach 0, but this is small enough to clear everything out + decayTimer?.invalidate() + decayTimer = nil + } + } + + // MARK: - Public Methods + + /// Add a value to the visualizer. Be sure to call `AVAudioPlayer.isMeteringEnabled = true`, and `AVAudioPlayer.updateMeters()` before every call to `AVAudioPlayer.averagePower(forChannel: 0)` + /// - Parameter decibelValue: The value you would get out of `AVAudioPlayer.averagePower(forChannel: 0)` + public func addValue(decibelValue: Float) { + addValue(decibelValue: Double(decibelValue)) + } + + /// Add a value to the visualizer. Be sure to call `AVAudioPlayer.isMeteringEnabled = true`, and `AVAudioPlayer.updateMeters()` before every call to `AVAudioPlayer.averagePower(forChannel: 0)` + /// - Parameter decibelValue: The value you would get out of `AVAudioPlayer.averagePower(forChannel: 0)` + public func addValue(decibelValue: Double) { + let normalizedValue = __exp10(decibelValue/20) + + newestValue = normalizedValue + + startTimer() + } + +} From 471fee832676348d54e5e2e801fd78c6b42990a7 Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Thu, 5 Nov 2020 14:41:57 -0800 Subject: [PATCH 11/15] Completed Project with Stretch Goal --- LambdaTimeline/Storyboards/Main.storyboard | 32 +++++++++++++++++-- .../AudioRecorderController.swift | 30 +++++++++++++++-- .../ImagePostDetailTableViewController.swift | 31 +++++++++++++----- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index e4dde766..172644dc 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -529,6 +529,31 @@ + + + + + + + + + + + + + + + + + + + @@ -537,6 +562,7 @@ + @@ -559,11 +585,11 @@ - + - + @@ -619,7 +645,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -702,7 +806,9 @@ + + diff --git a/LambdaTimeline/View Controllers/CameraViewController.swift b/LambdaTimeline/View Controllers/CameraViewController.swift new file mode 100644 index 00000000..44133f03 --- /dev/null +++ b/LambdaTimeline/View Controllers/CameraViewController.swift @@ -0,0 +1,176 @@ +// +// CameraViewController.swift +// LambdaTimeline +// +// Created by Cora Jacobson on 11/5/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import AVFoundation +import AVKit + +class CameraViewController: UIViewController { + + lazy private var captureSession = AVCaptureSession() + lazy private var fileOutput = AVCaptureMovieFileOutput() + lazy private var player = AVPlayer() + private var playerView: VideoPlayerView! + + @IBOutlet private var playButton: UIButton! + @IBOutlet private var recordButton: UIButton! + @IBOutlet private var saveButton: UIButton! + @IBOutlet private var cameraView: CameraPreviewView! + + var outputURL: URL? + + override func viewDidLoad() { + super.viewDidLoad() + cameraView.videoPlayerLayer.videoGravity = .resizeAspectFill + playButton.isHidden = true + saveButton.isHidden = true + setUpCamera() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + captureSession.startRunning() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + captureSession.stopRunning() + } + + func playMovie(url: URL) { + player.replaceCurrentItem(with: AVPlayerItem(url: url)) + + if playerView == nil { + playerView = VideoPlayerView() + playerView.player = player + + var topRect = view.bounds + topRect.size.width /= 4 + topRect.size.height /= 4 + topRect.origin.y = view.layoutMargins.top + topRect.origin.x = view.bounds.size.width - topRect.size.width + + playerView.frame = topRect + view.addSubview(playerView) + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(playRecording(_:))) + playerView.addGestureRecognizer(tapGesture) + } + + player.play() + } + + @IBAction func playRecording(_ sender: UITapGestureRecognizer) { + guard sender.state == .ended else { return } + let playerVC = AVPlayerViewController() + playerVC.player = player + self.present(playerVC, animated: true, completion: nil) + } + + private func setUpCamera() { + let camera = bestCamera + let microphone = bestMicrophone + + captureSession.beginConfiguration() + guard let cameraInput = try? AVCaptureDeviceInput(device: camera) else { + preconditionFailure("Can't create an input from the camera.") + } + + guard let microphoneInput = try? AVCaptureDeviceInput(device: microphone) else { + preconditionFailure("Can't create an input from the microphone.") + } + + guard captureSession.canAddInput(cameraInput) else { + preconditionFailure("This session can't handle this type of input: \(cameraInput)") + } + + captureSession.addInput(cameraInput) + + guard captureSession.canAddInput(microphoneInput) else { + preconditionFailure("This session can't handle this type of input: \(microphoneInput)") + } + + captureSession.addInput(microphoneInput) + + if captureSession.canSetSessionPreset(.hd1920x1080) { + captureSession.sessionPreset = .hd1920x1080 + } + + guard captureSession.canAddOutput(fileOutput) else { + preconditionFailure("This session can't handle this type of output: \(fileOutput)") + } + captureSession.addOutput(fileOutput) + + captureSession.commitConfiguration() + + cameraView.session = captureSession + } + + private var bestCamera: AVCaptureDevice { + if let device = AVCaptureDevice.default(.builtInUltraWideCamera, for: .video, position: .back) { + return device + } else if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { + return device + } + preconditionFailure("No cameras on device with specs that we need") + } + + private var bestMicrophone: AVCaptureDevice { + if let device = AVCaptureDevice.default(for: .audio) { + return device + } + preconditionFailure("No microphones on device with specs that we need") + } + + @IBAction func playButtonPressed(_ sender: UIButton) { + guard let outputURL = outputURL else { return } + playMovie(url: outputURL) + } + + @IBAction func saveButtonPressed(_ sender: UIButton) { + } + + @IBAction func recordButtonPressed(_ sender: Any) { + if fileOutput.isRecording { + fileOutput.stopRecording() + } else { + fileOutput.startRecording(to: newRecordingURL(), recordingDelegate: self) + } + } + + private func newRecordingURL() -> URL { + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime] + + let name = formatter.string(from: Date()) + let fileURL = documentsDirectory.appendingPathComponent(name).appendingPathExtension("mov") + return fileURL + } + + private func updateViews() { + recordButton.isSelected = fileOutput.isRecording + } +} + +extension CameraViewController: AVCaptureFileOutputRecordingDelegate { + func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) { + updateViews() + } + + func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { + if let error = error { + print("Error saving video: \(error)") + } + outputURL = outputFileURL + playButton.isHidden = false + saveButton.isHidden = false + updateViews() + } +} diff --git a/LambdaTimeline/Views/CameraPreviewView.swift b/LambdaTimeline/Views/CameraPreviewView.swift new file mode 100644 index 00000000..bdf57d5b --- /dev/null +++ b/LambdaTimeline/Views/CameraPreviewView.swift @@ -0,0 +1,26 @@ +// +// CameraPreviewView.swift +// LambdaTimeline +// +// Created by Cora Jacobson on 11/5/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import AVFoundation + +class CameraPreviewView: UIView { + + override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } + + var videoPlayerLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } + + var session: AVCaptureSession? { + get { return videoPlayerLayer.session } + set { videoPlayerLayer.session = newValue } + } +} diff --git a/LambdaTimeline/Views/VideoPlayerView.swift b/LambdaTimeline/Views/VideoPlayerView.swift new file mode 100644 index 00000000..2276e206 --- /dev/null +++ b/LambdaTimeline/Views/VideoPlayerView.swift @@ -0,0 +1,26 @@ +// +// VideoPlayerView.swift +// LambdaTimeline +// +// Created by Cora Jacobson on 11/5/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import AVFoundation + +class VideoPlayerView: UIView { + + override class var layerClass: AnyClass { + return AVPlayerLayer.self + } + + var videoPlayerLayer: AVPlayerLayer { + return layer as! AVPlayerLayer + } + + var player: AVPlayer? { + get { return videoPlayerLayer.player } + set { videoPlayerLayer.player = newValue } + } +} From 2b70fb594149be2c442cb9c60b6a8f5785662d3c Mon Sep 17 00:00:00 2001 From: Cora Jacobson Date: Thu, 5 Nov 2020 17:36:57 -0800 Subject: [PATCH 13/15] CollectionViewCell --- LambdaTimeline.xcodeproj/project.pbxproj | 4 ++ .../Model Controllers/PostController.swift | 6 ++ LambdaTimeline/Models/Post.swift | 3 +- LambdaTimeline/Resources/Info.plist | 4 ++ LambdaTimeline/Storyboards/Main.storyboard | 67 +++++++++++++++++- .../CameraViewController.swift | 52 ++++++++++++++ .../PostsCollectionViewController.swift | 11 ++- .../Views/VideoPostCollectionViewCell.swift | 69 +++++++++++++++++++ 8 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 LambdaTimeline/Views/VideoPostCollectionViewCell.swift diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj index 39ff9a3e..380173f3 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 1A51ECF32554C41800643EB1 /* CameraPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A51ECF22554C41800643EB1 /* CameraPreviewView.swift */; }; 1A51ECF62554C45E00643EB1 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A51ECF52554C45E00643EB1 /* VideoPlayerView.swift */; }; 1A51ECF92554C4A700643EB1 /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A51ECF82554C4A700643EB1 /* CameraViewController.swift */; }; + 1A51ECFC2554D05F00643EB1 /* VideoPostCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A51ECFB2554D05F00643EB1 /* VideoPostCollectionViewCell.swift */; }; 1AC9C5CE254B86BD00CDD572 /* ImageFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */; }; 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */; }; 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46463782216FDE4C00E7FF73 /* Assets.xcassets */; }; @@ -36,6 +37,7 @@ 1A51ECF22554C41800643EB1 /* CameraPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreviewView.swift; sourceTree = ""; }; 1A51ECF52554C45E00643EB1 /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; 1A51ECF82554C4A700643EB1 /* CameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; + 1A51ECFB2554D05F00643EB1 /* VideoPostCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPostCollectionViewCell.swift; sourceTree = ""; }; 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilterViewController.swift; sourceTree = ""; }; 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 = ""; }; @@ -154,6 +156,7 @@ 1A51ECED2553938000643EB1 /* AudioVisualizer.swift */, 1A51ECF22554C41800643EB1 /* CameraPreviewView.swift */, 1A51ECF52554C45E00643EB1 /* VideoPlayerView.swift */, + 1A51ECFB2554D05F00643EB1 /* VideoPostCollectionViewCell.swift */, ); path = Views; sourceTree = ""; @@ -257,6 +260,7 @@ 1A51ECEB255390B900643EB1 /* AudioRecorderController.swift in Sources */, 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, 46463790216FFD1B00E7FF73 /* Post.swift in Sources */, + 1A51ECFC2554D05F00643EB1 /* VideoPostCollectionViewCell.swift in Sources */, 1A51ECF32554C41800643EB1 /* CameraPreviewView.swift in Sources */, 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */, 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */, diff --git a/LambdaTimeline/Model Controllers/PostController.swift b/LambdaTimeline/Model Controllers/PostController.swift index 190d59bf..9acdb0c3 100644 --- a/LambdaTimeline/Model Controllers/PostController.swift +++ b/LambdaTimeline/Model Controllers/PostController.swift @@ -25,6 +25,12 @@ class PostController { posts.append(post) } + func createVideoPost(with title: String, videoURL: URL) { + guard let currentUser = currentUser else { return } + let post = Post(title: title, mediaType: .video(videoURL), author: currentUser) + posts.append(post) + } + func addComment(with text: String, to post: inout Post) { guard let currentUser = currentUser else { return } diff --git a/LambdaTimeline/Models/Post.swift b/LambdaTimeline/Models/Post.swift index 15e1d65c..c7e03299 100644 --- a/LambdaTimeline/Models/Post.swift +++ b/LambdaTimeline/Models/Post.swift @@ -10,6 +10,7 @@ import UIKit enum MediaType { case image(UIImage) + case video(URL) } class Post: Equatable { @@ -25,7 +26,7 @@ class Post: Equatable { return comments.first?.text } - init(title: String, mediaType: MediaType, ratio: CGFloat?, author: String, timestamp: Date = Date()) { + init(title: String, mediaType: MediaType, ratio: CGFloat? = nil, author: String, timestamp: Date = Date()) { self.mediaType = mediaType self.ratio = ratio self.author = author diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist index acb4ea99..499daccd 100644 --- a/LambdaTimeline/Resources/Info.plist +++ b/LambdaTimeline/Resources/Info.plist @@ -2,6 +2,10 @@ + UIFileSharingEnabled + + NSCameraUsageDescription + $(PRODUCT_NAME) needs access to your camera to record video NSMicrophoneUsageDescription $(PRODUCT_NAME) needs access to your microphone to record audio CFBundleDevelopmentRegion diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index 3c5ff03b..4304a4f0 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -23,9 +23,68 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -97,6 +156,7 @@ + @@ -112,10 +172,10 @@ - + - +