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..7976f56e 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -3,63 +3,59 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 50; 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 */; }; + 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 */; }; - 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 = ""; }; + 1A51ECEA255390B900643EB1 /* AudioRecorderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderController.swift; sourceTree = ""; }; + 1A51ECED2553938000643EB1 /* AudioVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVisualizer.swift; sourceTree = ""; }; + 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 = ""; }; - 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 +63,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 +103,6 @@ isa = PBXGroup; children = ( 4646378F216FFD1B00E7FF73 /* Post.swift */, - 46CFE6FA21714E6100E7FF73 /* Author.swift */, 46463791216FFDD900E7FF73 /* Comment.swift */, ); path = Models; @@ -138,7 +121,7 @@ 46463797216FFF7400E7FF73 /* Storyboards */ = { isa = PBXGroup; children = ( - 4646377F216FDE4B00E7FF73 /* Main.storyboard */, + 46D1A48824FF0BC4008D1CA7 /* Main.storyboard */, 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */, ); path = Storyboards; @@ -159,6 +142,9 @@ 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */, 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, + 1AC9C5CD254B86BD00CDD572 /* ImageFilterViewController.swift */, + 1A51ECEA255390B900643EB1 /* AudioRecorderController.swift */, + 1A51ECF82554C4A700643EB1 /* CameraViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -167,27 +153,18 @@ isa = PBXGroup; children = ( 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */, + 1A51ECED2553938000643EB1 /* AudioVisualizer.swift */, + 1A51ECF22554C41800643EB1 /* CameraPreviewView.swift */, + 1A51ECF52554C45E00643EB1 /* VideoPlayerView.swift */, + 1A51ECFB2554D05F00643EB1 /* VideoPostCollectionViewCell.swift */, ); 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 +173,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 +186,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 = ( ); @@ -241,7 +206,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1000; - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "Lambda School"; TargetAttributes = { 46463777216FDE4B00E7FF73 = { @@ -273,111 +238,44 @@ 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 */, + 1AC9C5CE254B86BD00CDD572 /* ImageFilterViewController.swift in Sources */, + 1A51ECF62554C45E00643EB1 /* VideoPlayerView.swift in Sources */, 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, - 46CFE6FD217154F500E7FF73 /* Networking.swift in Sources */, + 1A51ECEE2553938000643EB1 /* AudioVisualizer.swift in Sources */, + 1A51ECEB255390B900643EB1 /* AudioRecorderController.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 */, + 1A51ECFC2554D05F00643EB1 /* VideoPostCollectionViewCell.swift in Sources */, + 1A51ECF32554C41800643EB1 /* CameraPreviewView.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 */, 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.swift in Sources */, + 1A51ECF92554C4A700643EB1 /* CameraViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 4646377F216FDE4B00E7FF73 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 46463780216FDE4B00E7FF73 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -415,6 +313,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; @@ -440,7 +339,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; @@ -476,6 +375,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; @@ -495,7 +395,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; @@ -507,17 +407,17 @@ }; 4646378B216FDE4C00E7FF73 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 33A2B89DE9E21538434BD640 /* Pods-LambdaTimeline.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 739A4FRA34; INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.CoraJacobson.LambdaTimeline; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -526,17 +426,17 @@ }; 4646378C216FDE4C00E7FF73 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5D2CCD6C68779B0A70AC37FA /* Pods-LambdaTimeline.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 739A4FRA34; INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.CoraJacobson.LambdaTimeline; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; 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 @@ { - - 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/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/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..9acdb0c3 100644 --- a/LambdaTimeline/Model Controllers/PostController.swift +++ b/LambdaTimeline/Model Controllers/PostController.swift @@ -6,122 +6,44 @@ // 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) + let post = Post(title: title, mediaType: .image(image), ratio: ratio, author: currentUser) - savePostToFirebase(post) + posts.append(post) } - - func observePosts(completion: @escaping (Error?) -> Void) { - - 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)") - } + + 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 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() - + 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) + } } 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..01d9126b 100644 --- a/LambdaTimeline/Models/Comment.swift +++ b/LambdaTimeline/Models/Comment.swift @@ -7,41 +7,29 @@ // 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 audioURL: URL? + let author: String let timestamp: Date - init(text: String, author: Author, 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 } - 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..c7e03299 100644 --- a/LambdaTimeline/Models/Post.swift +++ b/LambdaTimeline/Models/Post.swift @@ -6,74 +6,36 @@ // Copyright © 2018 Lambda School. All rights reserved. // -import Foundation -import FirebaseAuth +import UIKit -enum MediaType: String { - case image +enum MediaType { + case image(UIImage) + case video(URL) } -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? = nil, 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/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist index dab10c0a..f9b2185c 100644 --- a/LambdaTimeline/Resources/Info.plist +++ b/LambdaTimeline/Resources/Info.plist @@ -31,8 +31,14 @@ 1 LSRequiresIPhoneOS + NSCameraUsageDescription + $(PRODUCT_NAME) needs access to your camera to record video + NSMicrophoneUsageDescription + $(PRODUCT_NAME) needs access to your microphone to record audio NSPhotoLibraryUsageDescription In order to allow you to add photos to posts + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/LambdaTimeline/Storyboards/Base.lproj/Main.storyboard b/LambdaTimeline/Storyboards/Base.lproj/Main.storyboard deleted file mode 100644 index fcec352d..00000000 --- a/LambdaTimeline/Storyboards/Base.lproj/Main.storyboard +++ /dev/null @@ -1,357 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard new file mode 100644 index 00000000..4304a4f0 --- /dev/null +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -0,0 +1,877 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LambdaTimeline/View Controllers/AudioRecorderController.swift b/LambdaTimeline/View Controllers/AudioRecorderController.swift new file mode 100644 index 00000000..c0fe7b7e --- /dev/null +++ b/LambdaTimeline/View Controllers/AudioRecorderController.swift @@ -0,0 +1,325 @@ +// +// 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 + + 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? + var playOnlyMode: Bool = false + + 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 + updateViews() + setPlayOnly() + } + + private func setPlayOnly() { + if playOnlyMode { + loadAudio() + recordButton.isHidden = true + playButton.isHidden = false + saveButton.isHidden = false + saveButton.setTitle("Return to Post", for: .normal) + updateViews() + } + } + + 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 loadAudio() { + guard let recordingURL = recordingURL else { return } + do { + audioPlayer = try AVAudioPlayer(contentsOf: recordingURL) + } catch { + preconditionFailure("Failure to load audio file: \(error)") + } + } + + func prepareAudioSession() throws { + let session = AVAudioSession.sharedInstance() + try session.setCategory(.playAndRecord, options: [.defaultToSpeaker]) + try session.setActive(true, options: []) + } + + func play() { + guard let recordingURL = recordingURL else { return } + do { + try prepareAudioSession() + audioPlayer = try AVAudioPlayer(contentsOf: recordingURL) + audioPlayer?.play() + 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 !playOnlyMode else { + dismiss(animated: true, completion: nil) + return + } + 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/CameraViewController.swift b/LambdaTimeline/View Controllers/CameraViewController.swift new file mode 100644 index 00000000..bfc294fa --- /dev/null +++ b/LambdaTimeline/View Controllers/CameraViewController.swift @@ -0,0 +1,228 @@ +// +// 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? + var postController: PostController! + + override func viewDidLoad() { + super.viewDidLoad() + requestPermissionForCamera() + 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) { + guard let outputUrl = outputURL else { return } + let alert = UIAlertController(title: "Add a Title", message: "Please title your video.", preferredStyle: .alert) + + var titleTextField: UITextField? + + alert.addTextField { (textField) in + textField.placeholder = "Title:" + titleTextField = textField + } + + let postVideoAction = UIAlertAction(title: "Post Video", style: .default) { (_) in + guard let title = titleTextField?.text else { return } + self.postController.createVideoPost(with: title, videoURL: outputUrl) + self.navigationController?.popViewController(animated: true) + } + + let returnToPostsAction = UIAlertAction(title: "Return to Posts", style: .default) { (_) in + self.navigationController?.popViewController(animated: true) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alert.addAction(postVideoAction) + alert.addAction(returnToPostsAction) + alert.addAction(cancelAction) + + self.present(alert, animated: true, completion: nil) + } + + @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 + } + + private func requestPermissionForCamera() { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .notDetermined: + requestVideoPermission() + case .restricted: + preconditionFailure("Video is disabled. Please review device restrictions.") + case .denied: + preconditionFailure("Tell the user they can't use the app without giving permission via Settings > Privacy > Video") + case .authorized: + break + @unknown default: + preconditionFailure("A new status code was added that we need to handle") + } + } + + private func requestVideoPermission() { + AVCaptureDevice.requestAccess(for: .video) { isGranted in + guard isGranted else { + preconditionFailure("UI: Tell the user to enable permissions for Video/Camera") + } + } + } +} + +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/View Controllers/ImageFilterViewController.swift b/LambdaTimeline/View Controllers/ImageFilterViewController.swift new file mode 100644 index 00000000..d50a166d --- /dev/null +++ b/LambdaTimeline/View Controllers/ImageFilterViewController.swift @@ -0,0 +1,343 @@ +// +// 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: + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + imageView.image = originalImage + } + } + } else { + imageView.image = nil + filteredImage = nil + } + } + + private func noFilterSetUp() { + slider1.isHidden = true + slider2.isHidden = true + slider3.isHidden = true + activeFilter = .none + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + 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 + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + 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 + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + 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 + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + 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 + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + 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 + if let filteredImage = filteredImage { + imageView.image = filteredImage + } else { + 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/ImagePostDetailTableViewController.swift b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift index 31b43fa3..df17a8c8 100644 --- a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift @@ -7,32 +7,43 @@ // import UIKit +import AVKit 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! + override func viewDidLoad() { super.viewDidLoad() updateViews() } func updateViews() { - - guard let imageData = imageData, - let image = UIImage(data: imageData) else { return } - + switch post.mediaType { + case .image(let image): + imageView.image = image + case .video(let videoURL): + imageFromVideo(url: videoURL, at: 0) { image in + self.imageView.image = image + } + } title = post?.title - - imageView.image = image - titleLabel.text = post.title - authorLabel.text = post.author.displayName + authorLabel.text = post.author } // MARK: - Table view data source @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? @@ -41,7 +52,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 } @@ -52,9 +63,17 @@ 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 + 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) @@ -65,24 +84,59 @@ class ImagePostDetailTableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "CommentCell", for: indexPath) - let comment = post?.comments[indexPath.row + 1] - - cell.textLabel?.text = comment?.text - cell.detailTextLabel?.text = comment?.author.displayName - - return cell + if comment?.audioURL == nil { + let cell = tableView.dequeueReusableCell(withIdentifier: "CommentCell", for: indexPath) + cell.textLabel?.text = comment?.text + cell.detailTextLabel?.text = comment?.author + return cell + } else { + let cell = tableView.dequeueReusableCell(withIdentifier: "AudioCommentCell", for: indexPath) + cell.textLabel?.text = comment?.author + return cell + } } - var post: Post! - var postController: PostController! - var imageData: Data? - + // MARK: - Navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "playCommentSegue" { + if let audioVC = segue.destination as? AudioRecorderController, + let indexPath = tableView.indexPathForSelectedRow { + let comment = post.comments[indexPath.row + 1] + audioVC.playOnlyMode = true + audioVC.recordingURL = comment.audioURL + } + } + } - @IBOutlet weak var imageView: UIImageView! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var authorLabel: UILabel! - @IBOutlet weak var imageViewAspectRatioConstraint: NSLayoutConstraint! + private func imageFromVideo(url: URL, at time: TimeInterval, completion: @escaping (UIImage?) -> Void) { + DispatchQueue.global(qos: .background).async { + let asset = AVURLAsset(url: url) + + let assetIG = AVAssetImageGenerator(asset: asset) + assetIG.appliesPreferredTrackTransform = true + assetIG.apertureMode = AVAssetImageGenerator.ApertureMode.encodedPixels + + let cmTime = CMTime(seconds: time, preferredTimescale: 60) + let thumbnailImageRef: CGImage + do { + thumbnailImageRef = try assetIG.copyCGImage(at: cmTime, actualTime: nil) + } catch let error { + print("Error: \(error)") + return completion(nil) + } + + DispatchQueue.main.async { + completion(UIImage(cgImage: thumbnailImageRef)) + } + } + } +} + +extension ImagePostDetailTableViewController: AudioRecorderDelegate { + func updatePostWithAudioComment(url: URL) { + postController.addAudioComment(with: url, to: &post) + tableView.reloadData() + } } diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift index 93c4b93e..9e8899a2 100644 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -11,29 +11,40 @@ import Photos class ImagePostViewController: ShiftableViewController { + @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) - - 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: []) + filterImageButton.isHidden = true + filterAgainButton.isHidden = true + clearFiltersButton.isHidden = true } private func presentImagePickerController() { @@ -42,38 +53,27 @@ 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) { 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 +100,44 @@ 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 filterImage(_ sender: UIButton) { + 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) + } + + @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) { 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 { @@ -136,7 +151,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) } @@ -144,3 +159,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 + } +} diff --git a/LambdaTimeline/View Controllers/PostsCollectionViewController.swift b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift index 7a8191b4..2d9164bd 100644 --- a/LambdaTimeline/View Controllers/PostsCollectionViewController.swift +++ b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift @@ -7,29 +7,16 @@ // import UIKit -import FirebaseAuth -import FirebaseUI +import AVKit 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) { @@ -40,9 +27,14 @@ class PostsCollectionViewController: UICollectionViewController, UICollectionVie self.performSegue(withIdentifier: "AddImagePost", sender: nil) } + let videoPostAction = UIAlertAction(title: "Video", style: .default) { (_) in + self.performSegue(withIdentifier: "AddVideoPost", sender: nil) + } + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alert.addAction(imagePostAction) + alert.addAction(videoPostAction) alert.addAction(cancelAction) self.present(alert, animated: true, completion: nil) @@ -51,21 +43,24 @@ 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 { case .image: guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImagePostCell", for: indexPath) as? ImagePostCollectionViewCell else { return UICollectionViewCell() } - cell.post = post - - loadImage(for: cell, forItemAt: indexPath) - + return cell + case .video(let videoURL): + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VideoPostCell", for: indexPath) as? VideoPostCollectionViewCell else { return UICollectionViewCell() } + cell.post = post + let player = AVPlayer(url: videoURL) + cell.playerView.player = player return cell } } @@ -76,104 +71,35 @@ 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 } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let cell = collectionView.cellForItem(at: indexPath) - - if let cell = cell as? ImagePostCollectionViewCell, - cell.imageView.image != nil { self.performSegue(withIdentifier: "ViewImagePost", sender: nil) - } } - 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 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "AddImagePost" { let destinationVC = segue.destination as? ImagePostViewController destinationVC?.postController = postController } else if segue.identifier == "ViewImagePost" { - let destinationVC = segue.destination as? ImagePostDetailTableViewController - - guard let indexPath = collectionView.indexPathsForSelectedItems?.first, - 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) + + } else if segue.identifier == "AddVideoPost" { + let destinationVC = segue.destination as? CameraViewController + destinationVC?.postController = postController } } - - 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/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() + } + +} 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/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/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 } + } +} diff --git a/LambdaTimeline/Views/VideoPostCollectionViewCell.swift b/LambdaTimeline/Views/VideoPostCollectionViewCell.swift new file mode 100644 index 00000000..571702bb --- /dev/null +++ b/LambdaTimeline/Views/VideoPostCollectionViewCell.swift @@ -0,0 +1,61 @@ +// +// VideoPostCollectionViewCell.swift +// LambdaTimeline +// +// Created by Cora Jacobson on 11/5/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import AVKit + +class VideoPostCollectionViewCell: UICollectionViewCell { + + @IBOutlet weak var playerView: VideoPlayerView! + @IBOutlet private var titleLabel: UILabel! + @IBOutlet private var authorLabel: UILabel! + @IBOutlet private var labelBackgroundView: UIView! + + var post: Post? { + didSet { + updateViews() + } + } + + override func layoutSubviews() { + super.layoutSubviews() + setupLabelBackgroundView() + } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = "" + authorLabel.text = "" + } + + func updateViews() { + guard let post = post else { return } + titleLabel.text = post.title + authorLabel.text = post.author + playMovie() + } + + func setupLabelBackgroundView() { + labelBackgroundView.layer.cornerRadius = 8 + labelBackgroundView.clipsToBounds = true + } + + func playMovie() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(playRecording(_:))) + playerView.addGestureRecognizer(tapGesture) + } + + @IBAction func playRecording(_ sender: UITapGestureRecognizer) { + guard sender.state == .ended, + let post = post else { return } + if case MediaType.video(let videoURL) = post.mediaType { + playerView.player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL)) + playerView.player?.play() + } + } +} 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 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