From e9e97e4658492b38c659472950d2700a9a4d8c57 Mon Sep 17 00:00:00 2001 From: Spencer Curtis Date: Tue, 16 Oct 2018 01:24:58 -0600 Subject: [PATCH 01/12] Update README.md with instructions for adding Audio comments --- README.md | 74 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ceae6240..d4c7fc70 100644 --- a/README.md +++ b/README.md @@ -4,48 +4,62 @@ The goal of this project is to take an existing project called LambdaTimeline and add features to it throughout this sprint. -To begin with, you will take the base project which has basic functionality to create posts with images from the user's photo library, and also add comments to posts. - -For today, you will implement the ability to add filters to images you post. +Today you will be adding audio comments. ## Instructions -Please fork and clone this repository, and work from the base project in the repo. +Create a new branch in the repository called `audioComments` and work off of it from where you left off yesterday. + +**As you go through the instructions, you are welcome to change the model objects to classes if you wish. In this project it may make the project a bit less complicated.** + +You're welcome to fulfill these instructions however you want. If you'd like suggestions on how to implement something, open the disclosure triangle and there are some suggestions for most of the instructions. + +1. Create UI that allows the user to create an audio comment. The UI should allow the user to record, stop, cancel, and send the recording. +
Recording UI Suggestions +

+ + - In the `ImagePostDetailViewController`, change the `createComment` action to allow the user select whether they want to make a text comment or an audio comment, then create a new view controller with the required UI. The view controller could be presented modally or as a popover. + + - Alternatively, you could modify the `ImagePostDetailViewController` to hold the audio recording UI. + +

+
+ +2. Create a new table view cell that displays at least the author of the audio comment, and a button to play the comment. + +3. Change the `Comment` to be either a text comment or an audio comment. + +
Comment Suggestions +

+ + - In the `Comment` object, change the `text`'s type to be an optional string, and create a new `audioURL: URL?` variable as well. Modify the `dictionaryRepresentation` and the `init?(dictionary: ...)` to accomodate the `audioURL` and the now optional `text` string. -### Part 1 - Firebase Setup +

+
-Though you have a base project, you will need to modify it. To begin, run `pod install` after navigating to the repo in terminal. Work out of the generated `.xcworkspace` +4. In the `PostController`, add the ability to create a comment with the audio data that the user records, and save it to Firebase Storage, add the comment to its post, then save the post to the Firebase Database. -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”. You will only need to do the two steps under the Google section. The starter project will have that URL type already. You just need to put the right URL scheme in. You can find the URL Type in your project file in the “Info” tab at the top. -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) +
Post Controller Suggestions +

-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. + - Create a separate function to create a comment with the audio data. + - You can very easily change the `store` method to instead take in data and a `StorageReference` to accomodate for storing both Post media data and now the audio data as well. -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) +

+
+5. In the `ImagePostDetailViewController`, make sure that the audio is being fetched for the audio comments. You are welcome to fetch the audio for each audio comment however you want. -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 PM or Spencer. +
Audio Fetching Suggestions +

-### Part 2 - ~~#NoFilter~~ #Filters + - You can implement the audio fetching similar to the way images are fetched on the `PostsCollectionViewController` by using operations, an operation queue, and a new cache. Make a new subclass of `ConcurrentOperation` that fetches audio using the comment's `audioURL` and a `URLSessionDataTask`. -Now that your project is working correctly, you will implement the ability to add filters to the image(s) the user selects from their photo. +

+
-1. You must add at least 5 filters. [This page](https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/#//apple_ref/doc/filter/ci/CIFalseColor) lists the filters that you can use. Note that some simply take in an `inputImage` parameter, while others have more parameters such as the `CIMotionBlur`, `CIColorControls`, etc. Use at least two or three of filters with a bit more complexity than just the `inputImage`. -2. Add whatever UI elements you want to the `ImagePostViewController` in order for them to add filters to their image after they've selected one. For the filters that require other parameters, add UI to allow the user to adjust the filter such as a slider for brightness, blur amount, etc. -3. Ensure that the controls to add your filters, adjust them, etc. are only available to the user at the apropriate time. 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. +6. Implement the ability to play a comment's audio from the new audio comment cell from step 2. As you implement the `AVAudioRecorder`, remember to add a microphone usage description in the Info.plist. ## Go Further -- Clean up the UI of the app, either with the UI you added to support filters. You're welcome to touch up the UI overall if you wish as well. -- Allow for undoing and redoing of filter effects. +- Add a label (if you don't have one already) to your recording UI that will show the recording time as the user is recording. +- Change the audio comment cell to display the duration of the audio, as well as show the current time the audio is at when playing. From f15b3fe92d989e6e35ad55f384f34d76d2a72a43 Mon Sep 17 00:00:00 2001 From: Spencer Curtis Date: Mon, 5 Nov 2018 12:05:24 -0700 Subject: [PATCH 02/12] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index d4c7fc70..496c0228 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,6 @@ Today you will be adding audio comments. Create a new branch in the repository called `audioComments` and work off of it from where you left off yesterday. -**As you go through the instructions, you are welcome to change the model objects to classes if you wish. In this project it may make the project a bit less complicated.** - You're welcome to fulfill these instructions however you want. If you'd like suggestions on how to implement something, open the disclosure triangle and there are some suggestions for most of the instructions. 1. Create UI that allows the user to create an audio comment. The UI should allow the user to record, stop, cancel, and send the recording. From d8e8f1b88eb529d3dbc7460cca6f24ee253faff4 Mon Sep 17 00:00:00 2001 From: Paul Solt Date: Tue, 5 May 2020 14:34:24 -0400 Subject: [PATCH 03/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 496c0228..4a5b6d69 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Today you will be adding audio comments. ## Instructions -Create a new branch in the repository called `audioComments` and work off of it from where you left off yesterday. +Create a new branch in the repository called `audio` and work off of it from where you left off yesterday. You're welcome to fulfill these instructions however you want. If you'd like suggestions on how to implement something, open the disclosure triangle and there are some suggestions for most of the instructions. From 4f7f1c5e092b99bc68b1601ae57567b49d950008 Mon Sep 17 00:00:00 2001 From: Paul Solt Date: Tue, 5 May 2020 14:57:42 -0400 Subject: [PATCH 04/12] Added notes for creating an Audio Prototyping Xcode project --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a5b6d69..e8bcd37c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,24 @@ Create a new branch in the repository called `audio` and work off of it from whe You're welcome to fulfill these instructions however you want. If you'd like suggestions on how to implement something, open the disclosure triangle and there are some suggestions for most of the instructions. -1. Create UI that allows the user to create an audio comment. The UI should allow the user to record, stop, cancel, and send the recording. +### Audio UI Prototyping + +Your first goal is to work on the audio functionality to prototype how it should behave. Building and testing with Firebase is slow, so you can speed up your development by working in issolation on this feature change. + +1. Create a new Xcode project for prototyping called `AudioComments` +2. Create UI that allows the user to create an audio comment. + 1. The UI should allow the user to record, stop, cancel, and send the recording. +3. Create Table View UI that displays audio comments in a custom table view cell. + 1. The UI should allow the user to play, pause, and scrub through a recording. + + +For inspiration, look at how the Phone app works with Voicemail, or how the Voice Memos app works. + +### Lambda Timeline Audio Integration + +Integrate your custom recording UI into the Lambda Timeline project. + +1. Users should be able to create an audio comment (in addition to a text comment).
Recording UI Suggestions

From 9b8239c0439fcbc2df5d6602d9fd5e5d6bd093c2 Mon Sep 17 00:00:00 2001 From: Paul Solt Date: Tue, 5 May 2020 15:02:49 -0400 Subject: [PATCH 05/12] Added part number headings --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8bcd37c..bf89dde2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Create a new branch in the repository called `audio` and work off of it from whe You're welcome to fulfill these instructions however you want. If you'd like suggestions on how to implement something, open the disclosure triangle and there are some suggestions for most of the instructions. -### Audio UI Prototyping +### Part 1: Audio UI Prototyping Your first goal is to work on the audio functionality to prototype how it should behave. Building and testing with Firebase is slow, so you can speed up your development by working in issolation on this feature change. @@ -25,7 +25,7 @@ Your first goal is to work on the audio functionality to prototype how it should For inspiration, look at how the Phone app works with Voicemail, or how the Voice Memos app works. -### Lambda Timeline Audio Integration +### Part 2: Lambda Timeline Audio Integration Integrate your custom recording UI into the Lambda Timeline project. From c0f62b0fb3b2503ba4953e966e9ba71530a0c42d Mon Sep 17 00:00:00 2001 From: Spencer Curtis Date: Thu, 3 Sep 2020 16:35:45 -0600 Subject: [PATCH 06/12] Remove project --- LambdaTimeline.xcodeproj/project.pbxproj | 591 ------------------ LambdaTimeline/Helpers/Cache.swift | 25 - .../Helpers/Extensions/UIImage+Ratio.swift | 15 - .../UIViewController+InformationalAlert.swift | 21 - .../User+DictionaryRepresentation.swift | 21 - .../Helpers/FirebaseConvertible.swift | 13 - LambdaTimeline/Helpers/Networking.swift | 51 -- .../Operations/ConcurrentOperation.swift | 64 -- .../Operations/FetchMediaOperation.swift | 59 -- .../Helpers/ShiftableViewController.swift | 126 ---- .../Model Controllers/PostController.swift | 127 ---- LambdaTimeline/Models/Author.swift | 36 -- LambdaTimeline/Models/Comment.swift | 44 -- LambdaTimeline/Models/Post.swift | 79 --- LambdaTimeline/Resources/AppDelegate.swift | 41 -- .../AppIcon.appiconset/Contents.json | 98 --- .../Resources/Assets.xcassets/Contents.json | 6 - LambdaTimeline/Resources/Info.plist | 58 -- .../Base.lproj/LaunchScreen.storyboard | 25 - .../Storyboards/Base.lproj/Main.storyboard | 354 ----------- .../ImagePostDetailTableViewController.swift | 88 --- .../ImagePostViewController.swift | 144 ----- .../PostsCollectionViewController.swift | 169 ----- .../SignInViewController.swift | 74 --- .../Views/ImagePostCollectionViewCell.swift | 54 -- Podfile | 15 - Podfile.lock | 145 ----- 27 files changed, 2543 deletions(-) delete mode 100644 LambdaTimeline.xcodeproj/project.pbxproj delete mode 100644 LambdaTimeline/Helpers/Cache.swift delete mode 100644 LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift delete mode 100644 LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift delete mode 100644 LambdaTimeline/Helpers/Extensions/User+DictionaryRepresentation.swift delete mode 100644 LambdaTimeline/Helpers/FirebaseConvertible.swift delete mode 100644 LambdaTimeline/Helpers/Networking.swift delete mode 100644 LambdaTimeline/Helpers/Operations/ConcurrentOperation.swift delete mode 100644 LambdaTimeline/Helpers/Operations/FetchMediaOperation.swift delete mode 100644 LambdaTimeline/Helpers/ShiftableViewController.swift delete mode 100644 LambdaTimeline/Model Controllers/PostController.swift delete mode 100644 LambdaTimeline/Models/Author.swift delete mode 100644 LambdaTimeline/Models/Comment.swift delete mode 100644 LambdaTimeline/Models/Post.swift delete mode 100644 LambdaTimeline/Resources/AppDelegate.swift delete mode 100644 LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 LambdaTimeline/Resources/Assets.xcassets/Contents.json delete mode 100644 LambdaTimeline/Resources/Info.plist delete mode 100644 LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard delete mode 100644 LambdaTimeline/Storyboards/Base.lproj/Main.storyboard delete mode 100644 LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift delete mode 100644 LambdaTimeline/View Controllers/ImagePostViewController.swift delete mode 100644 LambdaTimeline/View Controllers/PostsCollectionViewController.swift delete mode 100644 LambdaTimeline/View Controllers/SignInViewController.swift delete mode 100644 LambdaTimeline/Views/ImagePostCollectionViewCell.swift delete mode 100644 Podfile delete mode 100644 Podfile.lock diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj deleted file mode 100644 index a8d63c01..00000000 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ /dev/null @@ -1,591 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */; }; - 46463781216FDE4B00E7FF73 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4646377F216FDE4B00E7FF73 /* Main.storyboard */; }; - 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46463782216FDE4C00E7FF73 /* Assets.xcassets */; }; - 46463786216FDE4C00E7FF73 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */; }; - 46463790216FFD1B00E7FF73 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646378F216FFD1B00E7FF73 /* Post.swift */; }; - 46463792216FFDD900E7FF73 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46463791216FFDD900E7FF73 /* Comment.swift */; }; - 464637992170048900E7FF73 /* FirebaseConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 464637982170048900E7FF73 /* FirebaseConvertible.swift */; }; - 4646379C2170091A00E7FF73 /* PostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646379B2170091A00E7FF73 /* PostController.swift */; }; - 46A0366A21700F5100E7FF73 /* PostsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */; }; - 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A0366C2170158900E7FF73 /* SignInViewController.swift */; }; - 46CFE6F32170757F00E7FF73 /* User+DictionaryRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F22170757F00E7FF73 /* User+DictionaryRepresentation.swift */; }; - 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */; }; - 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */; }; - 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */; }; - 46CFE6FB21714E6100E7FF73 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6FA21714E6100E7FF73 /* Author.swift */; }; - 46CFE6FD217154F500E7FF73 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6FC217154F500E7FF73 /* Networking.swift */; }; - 46CFE6FF2171556D00E7FF73 /* ConcurrentOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6FE2171556D00E7FF73 /* ConcurrentOperation.swift */; }; - 46CFE7012171559500E7FF73 /* FetchMediaOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE7002171559500E7FF73 /* FetchMediaOperation.swift */; }; - 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */; }; - 46D571F32172D43B00E7FF73 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F22172D43B00E7FF73 /* Cache.swift */; }; - 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */; }; - 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */; }; - E453457955DF907FCBD117F1 /* Pods_LambdaTimeline.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA39CACB677861CF84322FB5 /* Pods_LambdaTimeline.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 33A2B89DE9E21538434BD640 /* Pods-LambdaTimeline.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LambdaTimeline.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline.debug.xcconfig"; sourceTree = ""; }; - 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LambdaTimeline.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 46463780216FDE4B00E7FF73 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 46463782216FDE4C00E7FF73 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 46463785216FDE4C00E7FF73 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 46463787216FDE4C00E7FF73 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 4646378F216FFD1B00E7FF73 /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; - 46463791216FFDD900E7FF73 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; - 464637982170048900E7FF73 /* FirebaseConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseConvertible.swift; sourceTree = ""; }; - 4646379B2170091A00E7FF73 /* PostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostController.swift; sourceTree = ""; }; - 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsCollectionViewController.swift; sourceTree = ""; }; - 46A0366C2170158900E7FF73 /* SignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; - 46CFE6F22170757F00E7FF73 /* User+DictionaryRepresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "User+DictionaryRepresentation.swift"; sourceTree = ""; }; - 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostViewController.swift; sourceTree = ""; }; - 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+InformationalAlert.swift"; sourceTree = ""; }; - 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShiftableViewController.swift; sourceTree = ""; }; - 46CFE6FA21714E6100E7FF73 /* Author.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = ""; }; - 46CFE6FC217154F500E7FF73 /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; }; - 46CFE6FE2171556D00E7FF73 /* ConcurrentOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentOperation.swift; sourceTree = ""; }; - 46CFE7002171559500E7FF73 /* FetchMediaOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchMediaOperation.swift; sourceTree = ""; }; - 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostCollectionViewCell.swift; sourceTree = ""; }; - 46D571F22172D43B00E7FF73 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; - 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 */ - 46463775216FDE4B00E7FF73 /* Frameworks */ = { - 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 = ""; - }; - 46463779216FDE4B00E7FF73 /* Products */ = { - isa = PBXGroup; - children = ( - 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */, - ); - name = Products; - sourceTree = ""; - }; - 4646377A216FDE4B00E7FF73 /* LambdaTimeline */ = { - isa = PBXGroup; - children = ( - 46CFE7042171572900E7FF73 /* Views */, - 46A0366B21700F6100E7FF73 /* View Controllers */, - 4646379A2170090900E7FF73 /* Model Controllers */, - 46463795216FFF5800E7FF73 /* Models */, - 46463797216FFF7400E7FF73 /* Storyboards */, - 46CFE7062171573F00E7FF73 /* Helpers */, - 46463796216FFF6900E7FF73 /* Resources */, - ); - path = LambdaTimeline; - sourceTree = ""; - }; - 46463795216FFF5800E7FF73 /* Models */ = { - isa = PBXGroup; - children = ( - 4646378F216FFD1B00E7FF73 /* Post.swift */, - 46CFE6FA21714E6100E7FF73 /* Author.swift */, - 46463791216FFDD900E7FF73 /* Comment.swift */, - ); - path = Models; - sourceTree = ""; - }; - 46463796216FFF6900E7FF73 /* Resources */ = { - isa = PBXGroup; - children = ( - 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */, - 46463782216FDE4C00E7FF73 /* Assets.xcassets */, - 46463787216FDE4C00E7FF73 /* Info.plist */, - ); - path = Resources; - sourceTree = ""; - }; - 46463797216FFF7400E7FF73 /* Storyboards */ = { - isa = PBXGroup; - children = ( - 4646377F216FDE4B00E7FF73 /* Main.storyboard */, - 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */, - ); - path = Storyboards; - sourceTree = ""; - }; - 4646379A2170090900E7FF73 /* Model Controllers */ = { - isa = PBXGroup; - children = ( - 4646379B2170091A00E7FF73 /* PostController.swift */, - ); - path = "Model Controllers"; - sourceTree = ""; - }; - 46A0366B21700F6100E7FF73 /* View Controllers */ = { - isa = PBXGroup; - children = ( - 46A0366C2170158900E7FF73 /* SignInViewController.swift */, - 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */, - 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, - 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, - ); - path = "View Controllers"; - sourceTree = ""; - }; - 46CFE7042171572900E7FF73 /* Views */ = { - isa = PBXGroup; - children = ( - 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.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; - sourceTree = ""; - }; - 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 */ - 46463777216FDE4B00E7FF73 /* LambdaTimeline */ = { - 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 = ( - ); - dependencies = ( - ); - name = LambdaTimeline; - productName = LambdaTimeline; - productReference = 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 46463770216FDE4B00E7FF73 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1000; - LastUpgradeCheck = 1000; - ORGANIZATIONNAME = "Lambda School"; - TargetAttributes = { - 46463777216FDE4B00E7FF73 = { - CreatedOnToolsVersion = 10.0; - }; - }; - }; - buildConfigurationList = 46463773216FDE4B00E7FF73 /* Build configuration list for PBXProject "LambdaTimeline" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 4646376F216FDE4B00E7FF73; - productRefGroup = 46463779216FDE4B00E7FF73 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 46463777216FDE4B00E7FF73 /* LambdaTimeline */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 46463776216FDE4B00E7FF73 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 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 = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseUI/FirebaseAuthUI.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseUI/FirebaseGoogleAuthUI.bundle", - "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - ); - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseAuthUI.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseGoogleAuthUI.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 931381193B5D2856F9B0CA8C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/GTMOAuth2/GTMOAuth2.framework", - "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", - "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", - "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMOAuth2.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-LambdaTimeline/Pods-LambdaTimeline-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 46463774216FDE4B00E7FF73 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */, - 46CFE6FB21714E6100E7FF73 /* Author.swift in Sources */, - 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, - 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, - 46CFE6FD217154F500E7FF73 /* Networking.swift in Sources */, - 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, - 46CFE6FF2171556D00E7FF73 /* ConcurrentOperation.swift in Sources */, - 46463790216FFD1B00E7FF73 /* Post.swift in Sources */, - 46CFE6F32170757F00E7FF73 /* User+DictionaryRepresentation.swift in Sources */, - 46D571F32172D43B00E7FF73 /* Cache.swift in Sources */, - 46CFE7012171559500E7FF73 /* FetchMediaOperation.swift in Sources */, - 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */, - 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */, - 4646379C2170091A00E7FF73 /* PostController.swift in Sources */, - 464637992170048900E7FF73 /* FirebaseConvertible.swift in Sources */, - 46A0366A21700F5100E7FF73 /* PostsCollectionViewController.swift in Sources */, - 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */, - 46463792216FFDD900E7FF73 /* Comment.swift in Sources */, - 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.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 = ( - 46463785216FDE4C00E7FF73 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 46463788216FDE4C00E7FF73 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 46463789216FDE4C00E7FF73 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 4646378B216FDE4C00E7FF73 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33A2B89DE9E21538434BD640 /* Pods-LambdaTimeline.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 4646378C216FDE4C00E7FF73 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 5D2CCD6C68779B0A70AC37FA /* Pods-LambdaTimeline.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 46463773216FDE4B00E7FF73 /* Build configuration list for PBXProject "LambdaTimeline" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 46463788216FDE4C00E7FF73 /* Debug */, - 46463789216FDE4C00E7FF73 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 4646378A216FDE4C00E7FF73 /* Build configuration list for PBXNativeTarget "LambdaTimeline" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4646378B216FDE4C00E7FF73 /* Debug */, - 4646378C216FDE4C00E7FF73 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 46463770216FDE4B00E7FF73 /* Project object */; -} diff --git a/LambdaTimeline/Helpers/Cache.swift b/LambdaTimeline/Helpers/Cache.swift deleted file mode 100644 index 42996051..00000000 --- a/LambdaTimeline/Helpers/Cache.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Cache.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/13/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation - -class Cache { - - func cache(value: Value, for key: Key) { - queue.async { - self.cache[key] = value - } - } - - func value(for key: Key) -> Value? { - return queue.sync { cache[key] } - } - - private var cache = [Key : Value]() - private let queue = DispatchQueue(label: "com.LambdaSchool.LambdaTimeline.CacheQueue") -} diff --git a/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift b/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift deleted file mode 100644 index 79bb32d7..00000000 --- a/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// UIImage+Ratio.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/14/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import UIKit - -extension UIImage { - var ratio: CGFloat { - return size.height / size.width - } -} diff --git a/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift b/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift deleted file mode 100644 index d4480303..00000000 --- a/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// UIViewController+InformationalAlert.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/12/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import UIKit - -extension UIViewController { - - func presentInformationalAlertController(title: String?, message: String?, dismissActionCompletion: ((UIAlertAction) -> Void)? = nil, completion: (() -> Void)? = nil) { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let dismissAction = UIAlertAction(title: "Dismiss", style: .cancel, handler: dismissActionCompletion) - - alertController.addAction(dismissAction) - - present(alertController, animated: true, completion: completion) - } -} diff --git a/LambdaTimeline/Helpers/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/Helpers/ShiftableViewController.swift b/LambdaTimeline/Helpers/ShiftableViewController.swift deleted file mode 100644 index 5923d54f..00000000 --- a/LambdaTimeline/Helpers/ShiftableViewController.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// Created by Spencer Curtis. -// Copyright © 2017-2018 Spencer Curtis. All rights reserved. -// - -/* - All you need to do is set your subclass of ShiftableViewController as the delegate for all - UITextFields and UITextViews that you want to be shifted up so the keyboard doesn't obscure it. - */ - -import UIKit - -class ShiftableViewController: UIViewController, UITextFieldDelegate, UITextViewDelegate, UIGestureRecognizerDelegate { - - var currentYShiftForKeyboard: CGFloat = 0 - - var textFieldBeingEdited: UITextField? - var textViewBeingEdited: UITextView? - - var keyboardDismissTapGestureRecognizer: UITapGestureRecognizer! - - override func viewDidLoad() { - super.viewDidLoad() - - setupKeyboardDismissTapGestureRecognizer() - - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - @objc func stopEditingTextInput() { - if let textField = self.textFieldBeingEdited { - - textField.resignFirstResponder() - - self.textFieldBeingEdited = nil - self.textViewBeingEdited = nil - } else if let textView = self.textViewBeingEdited { - - textView.resignFirstResponder() - - self.textFieldBeingEdited = nil - self.textViewBeingEdited = nil - } - - guard keyboardDismissTapGestureRecognizer.isEnabled else { return } - - keyboardDismissTapGestureRecognizer.isEnabled = false - } - - func textFieldDidBeginEditing(_ textField: UITextField) { - textFieldBeingEdited = textField - } - - func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { - textViewBeingEdited = textView - return true - } - - @objc func keyboardWillShow(notification: Notification) { - - keyboardDismissTapGestureRecognizer.isEnabled = true - - var keyboardSize: CGRect = .zero - - if let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, - keyboardRect.height != 0 { - keyboardSize = keyboardRect - } else if let keyboardRect = notification.userInfo?["UIKeyboardBoundsUserInfoKey"] as? CGRect { - keyboardSize = keyboardRect - } - - if let textField = textFieldBeingEdited { - if self.view.frame.origin.y == 0 { - - let yShift = yShiftWhenKeyboardAppearsFor(textInput: textField, keyboardSize: keyboardSize, nextY: keyboardSize.height) - self.currentYShiftForKeyboard = yShift - self.view.frame.origin.y -= yShift - } - } else if let textView = textViewBeingEdited { - if self.view.frame.origin.y == 0 { - - let yShift = yShiftWhenKeyboardAppearsFor(textInput: textView, keyboardSize: keyboardSize, nextY: keyboardSize.height) - self.currentYShiftForKeyboard = yShift - self.view.frame.origin.y -= yShift - } - } - } - - @objc func yShiftWhenKeyboardAppearsFor(textInput: UIView, keyboardSize: CGRect, nextY: CGFloat) -> CGFloat { - - let textFieldOrigin = self.view.convert(textInput.frame, from: textInput.superview!).origin.y - let textFieldBottomY = textFieldOrigin + textInput.frame.size.height - - // This is the y point that the textField's bottom can be at before it gets covered by the keyboard - let maximumY = self.view.frame.height - (keyboardSize.height + view.safeAreaInsets.bottom) - - if textFieldBottomY > maximumY { - // This makes the view shift the right amount to have the text field being edited just above they keyboard if it would have been covered by the keyboard. - return textFieldBottomY - maximumY - } else { - // It would go off the screen if moved, and it won't be obscured by the keyboard. - return 0 - } - } - - @objc func keyboardWillHide(notification: Notification) { - - if self.view.frame.origin.y != 0 { - - self.view.frame.origin.y += currentYShiftForKeyboard - } - - stopEditingTextInput() - } - - @objc func setupKeyboardDismissTapGestureRecognizer() { - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(stopEditingTextInput)) - tapGestureRecognizer.numberOfTapsRequired = 1 - - view.addGestureRecognizer(tapGestureRecognizer) - - keyboardDismissTapGestureRecognizer = tapGestureRecognizer - } -} diff --git a/LambdaTimeline/Model Controllers/PostController.swift b/LambdaTimeline/Model Controllers/PostController.swift deleted file mode 100644 index 29ce3869..00000000 --- a/LambdaTimeline/Model Controllers/PostController.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// PostController.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/11/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation -import FirebaseAuth -import FirebaseDatabase -import FirebaseStorage - -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) - } - } - } - - func addComment(with text: String, to post: inout Post) { - - guard let currentUser = Auth.auth().currentUser, - let author = Author(user: currentUser) else { return } - - let comment = Comment(text: text, author: author) - post.comments.append(comment) - - savePostToFirebase(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 savePostToFirebase(_ post: Post, completion: (Error?) -> Void = { _ in }) { - - guard let postID = post.id else { return } - - let ref = postsRef.child(postID) - - ref.setValue(post.dictionaryRepresentation) - } - - private func store(mediaData: Data, mediaType: MediaType, completion: @escaping (URL?) -> Void) { - - let mediaID = UUID().uuidString - - let mediaRef = storageRef.child(mediaType.rawValue).child(mediaID) - - let uploadTask = mediaRef.putData(mediaData, metadata: nil) { (metadata, error) in - if let error = error { - NSLog("Error storing media data: \(error)") - completion(nil) - return - } - - if metadata == nil { - NSLog("No metadata returned from upload task.") - completion(nil) - return - } - - mediaRef.downloadURL(completion: { (url, error) in - - if let error = error { - NSLog("Error getting download url of media: \(error)") - } - - guard let url = url else { - NSLog("Download url is nil. Unable to create a Media object") - - completion(nil) - return - } - completion(url) - }) - } - - uploadTask.resume() - } - - var posts: [Post] = [] - let currentUser = Auth.auth().currentUser - let postsRef = Database.database().reference().child("posts") - - let storageRef = Storage.storage().reference() - - -} diff --git a/LambdaTimeline/Models/Author.swift b/LambdaTimeline/Models/Author.swift deleted file mode 100644 index 4d847ad1..00000000 --- a/LambdaTimeline/Models/Author.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Author.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/12/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation -import FirebaseAuth - -struct Author: FirebaseConvertible, Equatable { - - init?(user: User) { - self.init(dictionary: user.dictionaryRepresentation) - } - - init?(dictionary: [String: Any]) { - guard let uid = dictionary[Author.uidKey] as? String, - let displayName = dictionary[Author.displayNameKey] as? String else { return nil } - - self.uid = uid - self.displayName = displayName - } - - let uid: String - let displayName: String? - - private static let uidKey = "uid" - private static let displayNameKey = "displayName" - - var dictionaryRepresentation: [String: Any] { - return [Author.uidKey: uid, - Author.displayNameKey: displayName ?? "No display name"] - } -} diff --git a/LambdaTimeline/Models/Comment.swift b/LambdaTimeline/Models/Comment.swift deleted file mode 100644 index cbbf4304..00000000 --- a/LambdaTimeline/Models/Comment.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Comment.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/11/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation -import FirebaseAuth - -struct Comment: FirebaseConvertible, Equatable { - - static private let textKey = "text" - static private let author = "author" - static private let timestampKey = "timestamp" - - let text: String - let author: Author - let timestamp: Date - - init(text: String, author: Author, timestamp: Date = Date()) { - self.text = text - self.author = author - self.timestamp = timestamp - } - - init?(dictionary: [String : Any]) { - guard let text = dictionary[Comment.textKey] as? String, - let authorDictionary = dictionary[Comment.author] as? [String: Any], - let author = Author(dictionary: authorDictionary), - let timestampTimeInterval = dictionary[Comment.timestampKey] as? TimeInterval else { return nil } - - self.text = text - self.author = author - self.timestamp = Date(timeIntervalSince1970: timestampTimeInterval) - } - - var dictionaryRepresentation: [String: Any] { - return [Comment.textKey: text, - Comment.author: author.dictionaryRepresentation, - Comment.timestampKey: timestamp.timeIntervalSince1970] - } -} diff --git a/LambdaTimeline/Models/Post.swift b/LambdaTimeline/Models/Post.swift deleted file mode 100644 index 00cad0f2..00000000 --- a/LambdaTimeline/Models/Post.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Post.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/11/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import Foundation -import FirebaseAuth - -enum MediaType: String { - case image -} - -struct Post { - - 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 timestamp: Date - var comments: [Comment] - var id: String? - var ratio: CGFloat? - - 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" -} diff --git a/LambdaTimeline/Resources/AppDelegate.swift b/LambdaTimeline/Resources/AppDelegate.swift deleted file mode 100644 index 057832b1..00000000 --- a/LambdaTimeline/Resources/AppDelegate.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// AppDelegate.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/11/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import UIKit -import Firebase -import GoogleSignIn - -@UIApplicationMain -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 application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - return GIDSignIn.sharedInstance().handle(url, - sourceApplication:options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String, - annotation: [:]) - } -} - diff --git a/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d8db8d65..00000000 --- a/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/LambdaTimeline/Resources/Assets.xcassets/Contents.json b/LambdaTimeline/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164c..00000000 --- a/LambdaTimeline/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist deleted file mode 100644 index f35b24fa..00000000 --- a/LambdaTimeline/Resources/Info.plist +++ /dev/null @@ -1,58 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - com.googleusercontent.apps.873272785897-7rlq5dqbnu9sdhg6nqhl4cn3drnl7uad - - - - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSPhotoLibraryUsageDescription - In order to allow you to add photos to posts - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard b/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index bfa36129..00000000 --- a/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LambdaTimeline/Storyboards/Base.lproj/Main.storyboard b/LambdaTimeline/Storyboards/Base.lproj/Main.storyboard deleted file mode 100644 index 7c1134e0..00000000 --- a/LambdaTimeline/Storyboards/Base.lproj/Main.storyboard +++ /dev/null @@ -1,354 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift deleted file mode 100644 index 31b43fa3..00000000 --- a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// ImagePostDetailTableViewController.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/14/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import UIKit - -class ImagePostDetailTableViewController: UITableViewController { - - override func viewDidLoad() { - super.viewDidLoad() - updateViews() - } - - func updateViews() { - - guard let imageData = imageData, - let image = UIImage(data: imageData) else { return } - - title = post?.title - - imageView.image = image - - titleLabel.text = post.title - authorLabel.text = post.author.displayName - } - - // MARK: - Table view data source - - @IBAction func createComment(_ sender: Any) { - - let alert = UIAlertController(title: "Add a comment", message: "Write your comment below:", preferredStyle: .alert) - - var commentTextField: UITextField? - - alert.addTextField { (textField) in - textField.placeholder = "Comment:" - commentTextField = textField - } - - let addCommentAction = UIAlertAction(title: "Add Comment", style: .default) { (_) in - - guard let commentText = commentTextField?.text else { return } - - self.postController.addComment(with: commentText, to: &self.post!) - - DispatchQueue.main.async { - self.tableView.reloadData() - } - } - - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - - alert.addAction(addCommentAction) - alert.addAction(cancelAction) - - present(alert, animated: true, completion: nil) - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return (post?.comments.count ?? 0) - 1 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "CommentCell", for: indexPath) - - let comment = post?.comments[indexPath.row + 1] - - cell.textLabel?.text = comment?.text - cell.detailTextLabel?.text = comment?.author.displayName - - return cell - } - - var post: Post! - var postController: PostController! - var imageData: Data? - - - - @IBOutlet weak var imageView: UIImageView! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var authorLabel: UILabel! - @IBOutlet weak var imageViewAspectRatioConstraint: NSLayoutConstraint! -} diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift deleted file mode 100644 index c30bca9a..00000000 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// ImagePostViewController.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/12/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import UIKit -import Photos - -class ImagePostViewController: ShiftableViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - setImageViewHeight(with: 1.0) - - updateViews() - } - - func updateViews() { - - guard let imageData = imageData, - let image = UIImage(data: imageData) else { - title = "New Post" - return - } - - title = post?.title - - setImageViewHeight(with: image.ratio) - - imageView.image = image - - chooseImageButton.setTitle("", for: []) - } - - private func presentImagePickerController() { - - guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { - presentInformationalAlertController(title: "Error", message: "The photo library is unavailable") - return - } - - let imagePicker = UIImagePickerController() - - imagePicker.delegate = self - - imagePicker.sourceType = .photoLibrary - - present(imagePicker, animated: true, completion: nil) - } - - @IBAction func createPost(_ sender: Any) { - - view.endEditing(true) - - guard let imageData = imageView.image?.jpegData(compressionQuality: 0.1), - 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.") - } - return - } - - DispatchQueue.main.async { - self.navigationController?.popViewController(animated: true) - } - } - } - - @IBAction func chooseImage(_ sender: Any) { - - let authorizationStatus = PHPhotoLibrary.authorizationStatus() - - switch authorizationStatus { - case .authorized: - presentImagePickerController() - case .notDetermined: - - PHPhotoLibrary.requestAuthorization { (status) in - - guard status == .authorized else { - NSLog("User did not authorize access to the photo library") - self.presentInformationalAlertController(title: "Error", message: "In order to access the photo library, you must allow this application access to it.") - return - } - - self.presentImagePickerController() - } - - case .denied: - self.presentInformationalAlertController(title: "Error", message: "In order to access the photo library, you must allow this application access to it.") - case .restricted: - self.presentInformationalAlertController(title: "Error", message: "Unable to access the photo library. Your device's restrictions do not allow access.") - - } - presentImagePickerController() - } - - 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 { - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - - chooseImageButton.setTitle("", for: []) - - picker.dismiss(animated: true, completion: nil) - - guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return } - - imageView.image = image - - setImageViewHeight(with: image.ratio) - } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - picker.dismiss(animated: true, completion: nil) - } -} diff --git a/LambdaTimeline/View Controllers/PostsCollectionViewController.swift b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift deleted file mode 100644 index 3843e060..00000000 --- a/LambdaTimeline/View Controllers/PostsCollectionViewController.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// PostsCollectionViewController.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/11/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import UIKit -import FirebaseAuth -import FirebaseUI - -class PostsCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { - - override func viewDidLoad() { - super.viewDidLoad() - - postController.observePosts { (_) in - DispatchQueue.main.async { - self.collectionView.reloadData() - } - } - } - - @IBAction func addPost(_ sender: Any) { - - let alert = UIAlertController(title: "New Post", message: "Which kind of post do you want to create?", preferredStyle: .actionSheet) - - let imagePostAction = UIAlertAction(title: "Image", style: .default) { (_) in - self.performSegue(withIdentifier: "AddImagePost", sender: nil) - } - - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - - alert.addAction(imagePostAction) - alert.addAction(cancelAction) - - self.present(alert, animated: true, completion: nil) - } - - // MARK: UICollectionViewDataSource - - override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return postController.posts.count - } - - 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 - } - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - - var size = CGSize(width: view.frame.width, height: view.frame.width) - - let post = postController.posts[indexPath.row] - - switch post.mediaType { - - case .image: - - 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 } - - destinationVC?.postController = postController - destinationVC?.post = postController.posts[indexPath.row] - destinationVC?.imageData = cache.value(for: postID) - } - } - - private let postController = PostController() - private var operations = [String : Operation]() - private let mediaFetchQueue = OperationQueue() - private let cache = Cache() -} diff --git a/LambdaTimeline/View Controllers/SignInViewController.swift b/LambdaTimeline/View Controllers/SignInViewController.swift deleted file mode 100644 index 2b1e2d01..00000000 --- a/LambdaTimeline/View Controllers/SignInViewController.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// SignInViewController.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/11/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import UIKit -import Firebase -import GoogleSignIn - -class SignInViewController: UIViewController, GIDSignInDelegate, GIDSignInUIDelegate { - - - override func viewDidLoad() { - super.viewDidLoad() - - let signIn = GIDSignIn.sharedInstance() - - signIn?.delegate = self - signIn?.uiDelegate = self - signIn?.signInSilently() - - setUpSignInButton() - } - - func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) { - - if let error = error { - NSLog("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().signInAndRetrieveData(with: credential) { (authResult, error) in - if let error = error { - NSLog("Error signing in with Google: \(error)") - return - } - - DispatchQueue.main.async { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let postsNavigationController = storyboard.instantiateViewController(withIdentifier: "PostsNavigationController") - self.present(postsNavigationController, animated: true, completion: nil) - } - } - } - - func sign(_ signIn: GIDSignIn!, didDisconnectWith user: GIDGoogleUser!, withError error: Error!) { - print("User disconnected") - } - - 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]) - } -} diff --git a/LambdaTimeline/Views/ImagePostCollectionViewCell.swift b/LambdaTimeline/Views/ImagePostCollectionViewCell.swift deleted file mode 100644 index 3841cf65..00000000 --- a/LambdaTimeline/Views/ImagePostCollectionViewCell.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// ImagePostCollectionViewCell.swift -// LambdaTimeline -// -// Created by Spencer Curtis on 10/12/18. -// Copyright © 2018 Lambda School. All rights reserved. -// - -import UIKit - -class ImagePostCollectionViewCell: UICollectionViewCell { - - override func layoutSubviews() { - super.layoutSubviews() - setupLabelBackgroundView() - } - override func prepareForReuse() { - super.prepareForReuse() - - imageView.image = nil - titleLabel.text = "" - authorLabel.text = "" - } - - func updateViews() { - guard let post = post else { return } - - titleLabel.text = post.title - authorLabel.text = post.author.displayName - } - - func setupLabelBackgroundView() { - labelBackgroundView.layer.cornerRadius = 8 -// labelBackgroundView.layer.borderColor = UIColor.white.cgColor -// labelBackgroundView.layer.borderWidth = 0.5 - labelBackgroundView.clipsToBounds = true - } - - func setImage(_ image: UIImage?) { - imageView.image = image - } - - var post: Post? { - didSet { - updateViews() - } - } - -@IBOutlet weak var imageView: UIImageView! -@IBOutlet weak var titleLabel: UILabel! -@IBOutlet weak var authorLabel: UILabel! -@IBOutlet weak var labelBackgroundView: UIView! - -} diff --git a/Podfile b/Podfile deleted file mode 100644 index 67626d26..00000000 --- a/Podfile +++ /dev/null @@ -1,15 +0,0 @@ -# Uncomment the next line to define a global platform for your project -platform :ios, '12.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' - -end diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index 34235820..00000000 --- a/Podfile.lock +++ /dev/null @@ -1,145 +0,0 @@ -PODS: - - Firebase/Auth (5.8.0): - - Firebase/CoreOnly - - FirebaseAuth (= 5.0.4) - - Firebase/Core (5.8.0): - - Firebase/CoreOnly - - FirebaseAnalytics (= 5.1.2) - - Firebase/CoreOnly (5.8.0): - - FirebaseCore (= 5.1.3) - - Firebase/Database (5.8.0): - - Firebase/CoreOnly - - FirebaseDatabase (= 5.0.3) - - Firebase/Storage (5.8.0): - - Firebase/CoreOnly - - FirebaseStorage (= 3.0.2) - - FirebaseAnalytics (5.1.2): - - FirebaseCore (~> 5.1) - - FirebaseInstanceID (~> 3.2) - - GoogleAppMeasurement (~> 5.1) - - GoogleUtilities/AppDelegateSwizzler (~> 5.2.0) - - GoogleUtilities/MethodSwizzler (~> 5.2.0) - - GoogleUtilities/Network (~> 5.2) - - "GoogleUtilities/NSData+zlib (~> 5.2)" - - nanopb (~> 0.3) - - FirebaseAuth (5.0.4): - - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 5.0) - - GoogleUtilities/Environment (~> 5.2) - - GTMSessionFetcher/Core (~> 1.1) - - FirebaseAuthInterop (1.0.0) - - FirebaseCore (5.1.3): - - GoogleUtilities/Logger (~> 5.2) - - FirebaseDatabase (5.0.3): - - FirebaseCore (~> 5.0) - - leveldb-library (~> 1.18) - - FirebaseInstanceID (3.2.1): - - FirebaseCore (~> 5.1) - - GoogleUtilities/Environment (~> 5.2) - - FirebaseStorage (3.0.2): - - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 5.0) - - GTMSessionFetcher/Core (~> 1.1) - - FirebaseUI/Auth (5.2.2): - - Firebase/Auth (~> 5.0) - - FirebaseUI/Google (5.2.2): - - FirebaseUI/Auth - - GoogleSignIn (~> 4.0) - - GoogleAppMeasurement (5.1.2): - - GoogleUtilities/AppDelegateSwizzler (~> 5.2.0) - - GoogleUtilities/MethodSwizzler (~> 5.2.0) - - GoogleUtilities/Network (~> 5.2) - - "GoogleUtilities/NSData+zlib (~> 5.2)" - - nanopb (~> 0.3) - - GoogleSignIn (4.2.0): - - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" - - "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)" - - GTMOAuth2 (~> 1.0) - - GTMSessionFetcher/Core (~> 1.1) - - GoogleToolboxForMac/DebugUtils (2.1.4): - - GoogleToolboxForMac/Defines (= 2.1.4) - - GoogleToolboxForMac/Defines (2.1.4) - - "GoogleToolboxForMac/NSDictionary+URLArguments (2.1.4)": - - GoogleToolboxForMac/DebugUtils (= 2.1.4) - - GoogleToolboxForMac/Defines (= 2.1.4) - - "GoogleToolboxForMac/NSString+URLArguments (= 2.1.4)" - - "GoogleToolboxForMac/NSString+URLArguments (2.1.4)" - - GoogleUtilities/AppDelegateSwizzler (5.2.3): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Environment (5.2.3) - - GoogleUtilities/Logger (5.2.3): - - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (5.2.3): - - GoogleUtilities/Logger - - GoogleUtilities/Network (5.2.3): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (5.2.3)" - - GoogleUtilities/Reachability (5.2.3): - - GoogleUtilities/Logger - - GTMOAuth2 (1.1.6): - - GTMSessionFetcher (~> 1.1) - - GTMSessionFetcher (1.2.0): - - GTMSessionFetcher/Full (= 1.2.0) - - GTMSessionFetcher/Core (1.2.0) - - GTMSessionFetcher/Full (1.2.0): - - GTMSessionFetcher/Core (= 1.2.0) - - leveldb-library (1.20) - - nanopb (0.3.8): - - nanopb/decode (= 0.3.8) - - nanopb/encode (= 0.3.8) - - nanopb/decode (0.3.8) - - nanopb/encode (0.3.8) - -DEPENDENCIES: - - Firebase/Auth - - Firebase/Core - - Firebase/Database - - Firebase/Storage - - FirebaseUI/Google - -SPEC REPOS: - https://github.com/cocoapods/specs.git: - - Firebase - - FirebaseAnalytics - - FirebaseAuth - - FirebaseAuthInterop - - FirebaseCore - - FirebaseDatabase - - FirebaseInstanceID - - FirebaseStorage - - FirebaseUI - - GoogleAppMeasurement - - GoogleSignIn - - GoogleToolboxForMac - - GoogleUtilities - - GTMOAuth2 - - GTMSessionFetcher - - leveldb-library - - nanopb - -SPEC CHECKSUMS: - Firebase: 25812f43e7a53b11ae2f0a5f4c6d12faeb1f7cd7 - FirebaseAnalytics: df15839e9c6ca6bd14d2e8ab6b0c672e6c49097e - FirebaseAuth: 504b198ceb3472dca5c65bb95544ea44cfc9439e - FirebaseAuthInterop: 0ffa57668be100582bb7643d4fcb7615496c41fc - FirebaseCore: 27bd80e5bfaaf9552a1f5cacb4c7e8bb925bab22 - FirebaseDatabase: e2bcbc106adc4b11a2da3ec2eb63c0c4a44f2f54 - FirebaseInstanceID: ea5af6920d0a4a29b40459d055bebe4a6c1333c4 - FirebaseStorage: fd82e5e5c474897e19972b34b22ac0f589dce04e - FirebaseUI: 09519bf436a055cd696bf68687d624423150e4c0 - GoogleAppMeasurement: fc4a4c3fe0144db9313cbf443ffe62e6b1d6268c - GoogleSignIn: 591e46382014e591269f862ba6e7bc0fbd793532 - GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f - GoogleUtilities: 6f681e27050c5e130325e89fa0316dfca826f954 - GTMOAuth2: c77fe325e4acd453837e72d91e3b5f13116857b2 - GTMSessionFetcher: 0c4baf0a73acd0041bf9f71ea018deedab5ea84e - leveldb-library: 08cba283675b7ed2d99629a4bc5fd052cd2bb6a5 - nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 - -PODFILE CHECKSUM: 05eb2a7c149e4b5105c37446a97414485c09ec52 - -COCOAPODS: 1.5.3 From e6694023430dd49f1fdf36a6c7ec349485aececd Mon Sep 17 00:00:00 2001 From: Spencer Curtis Date: Thu, 3 Sep 2020 16:37:44 -0600 Subject: [PATCH 07/12] Update README.md --- README.md | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index bf89dde2..e0eb0d51 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Today you will be adding audio comments. ## Instructions -Create a new branch in the repository called `audio` and work off of it from where you left off yesterday. +Create a new branch in the repository called `audio` or `audio/noFirebase` and work off of it from where you left off yesterday. You're welcome to fulfill these instructions however you want. If you'd like suggestions on how to implement something, open the disclosure triangle and there are some suggestions for most of the instructions. @@ -22,27 +22,13 @@ Your first goal is to work on the audio functionality to prototype how it should 3. Create Table View UI that displays audio comments in a custom table view cell. 1. The UI should allow the user to play, pause, and scrub through a recording. - For inspiration, look at how the Phone app works with Voicemail, or how the Voice Memos app works. ### Part 2: Lambda Timeline Audio Integration Integrate your custom recording UI into the Lambda Timeline project. -1. Users should be able to create an audio comment (in addition to a text comment). -

Recording UI Suggestions -

- - - In the `ImagePostDetailViewController`, change the `createComment` action to allow the user select whether they want to make a text comment or an audio comment, then create a new view controller with the required UI. The view controller could be presented modally or as a popover. - - - Alternatively, you could modify the `ImagePostDetailViewController` to hold the audio recording UI. - -

-
- -2. Create a new table view cell that displays at least the author of the audio comment, and a button to play the comment. - -3. Change the `Comment` to be either a text comment or an audio comment. +1. Change the `Comment` to be either a text comment or an audio comment.
Comment Suggestions

@@ -52,7 +38,7 @@ Integrate your custom recording UI into the Lambda Timeline project.

-4. In the `PostController`, add the ability to create a comment with the audio data that the user records, and save it to Firebase Storage, add the comment to its post, then save the post to the Firebase Database. +2. In the `PostController`, add the ability to create a comment with the audio data that the user records, and save it to Firebase Storage, add the comment to its post, then save the post to the Firebase Database.
Post Controller Suggestions

@@ -62,6 +48,19 @@ Integrate your custom recording UI into the Lambda Timeline project.

+3. Users should be able to create an audio comment (in addition to a text comment). +
Recording UI Suggestions +

+ + - In the `ImagePostDetailViewController`, change the `createComment` action to allow the user select whether they want to make a text comment or an audio comment, then create a new view controller with the required UI. The view controller could be presented modally or as a popover. + + - Alternatively, you could modify the `ImagePostDetailViewController` to hold the audio recording UI. + +

+
+ +4. Create a new table view cell that displays at least the author of the audio comment, and a button to play the comment. + 5. In the `ImagePostDetailViewController`, make sure that the audio is being fetched for the audio comments. You are welcome to fetch the audio for each audio comment however you want.
Audio Fetching Suggestions From e46e449ed85f1dad43a8449de8ec740972a98f03 Mon Sep 17 00:00:00 2001 From: Spencer Curtis Date: Thu, 3 Sep 2020 16:39:34 -0600 Subject: [PATCH 08/12] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e0eb0d51..1c1a0901 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ Create a new branch in the repository called `audio` or `audio/noFirebase` and w You're welcome to fulfill these instructions however you want. If you'd like suggestions on how to implement something, open the disclosure triangle and there are some suggestions for most of the instructions. -### Part 1: Audio UI Prototyping +### Part 0 (optional): Audio UI Prototyping -Your first goal is to work on the audio functionality to prototype how it should behave. Building and testing with Firebase is slow, so you can speed up your development by working in issolation on this feature change. +If you choose to, you can prototype this audio feature and the accompanying UI. If you would rather implement it in the Timeline project to begin with, skip to part 1. 1. Create a new Xcode project for prototyping called `AudioComments` 2. Create UI that allows the user to create an audio comment. @@ -24,7 +24,7 @@ Your first goal is to work on the audio functionality to prototype how it should For inspiration, look at how the Phone app works with Voicemail, or how the Voice Memos app works. -### Part 2: Lambda Timeline Audio Integration +### Part 1: Lambda Timeline Audio Integration Integrate your custom recording UI into the Lambda Timeline project. From 327f1b0eb399a8eca527b4607ce6e5891496570c Mon Sep 17 00:00:00 2001 From: Robert Vance Date: Fri, 30 Oct 2020 21:53:18 -0600 Subject: [PATCH 09/12] Completed Project? --- .../project.pbxproj | 350 +++++++++++++ .../ImageFilterEditor/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 147 ++++++ .../ImagePostViewController.swift | 153 ++++++ .../ImageFilterEditor/Info.plist | 70 +++ .../ImageFilterEditor/SceneDelegate.swift | 52 ++ .../ImageFilterEditor/UIImage+Scaling.swift | 37 ++ LambdaTimeline.xcodeproj/project.pbxproj | 448 ++++++++++++++++ .../xcschemes/LambdaTimeline.xcscheme | 78 +++ .../Helpers/Extensions/AudioVisualizer.swift | 275 ++++++++++ .../Helpers/Extensions/UIImage+Ratio.swift | 15 + .../UIViewController+InformationalAlert.swift | 21 + .../Helpers/ShiftableViewController.swift | 126 +++++ .../Model Controllers/PostController.swift | 43 ++ LambdaTimeline/Models/Comment.swift | 37 ++ LambdaTimeline/Models/Post.swift | 44 ++ LambdaTimeline/Resources/AppDelegate.swift | 20 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../Resources/Assets.xcassets/Contents.json | 6 + LambdaTimeline/Resources/Info.plist | 58 +++ .../Base.lproj/LaunchScreen.storyboard | 25 + LambdaTimeline/Storyboards/Main.storyboard | 483 ++++++++++++++++++ .../AudioCommentViewController.swift | 295 +++++++++++ .../ImagePostDetailTableViewController.swift | 114 +++++ .../ImagePostViewController.swift | 119 +++++ .../PostsCollectionViewController.swift | 100 ++++ .../SignInViewController.swift | 44 ++ .../Views/ImagePostCollectionViewCell.swift | 55 ++ 32 files changed, 3489 insertions(+) create mode 100644 ImageFilterEditor/ImageFilterEditor.xcodeproj/project.pbxproj create mode 100644 ImageFilterEditor/ImageFilterEditor/AppDelegate.swift create mode 100644 ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ImageFilterEditor/ImageFilterEditor/Assets.xcassets/Contents.json create mode 100644 ImageFilterEditor/ImageFilterEditor/Base.lproj/LaunchScreen.storyboard create mode 100644 ImageFilterEditor/ImageFilterEditor/Base.lproj/Main.storyboard create mode 100644 ImageFilterEditor/ImageFilterEditor/ImagePostViewController.swift create mode 100644 ImageFilterEditor/ImageFilterEditor/Info.plist create mode 100644 ImageFilterEditor/ImageFilterEditor/SceneDelegate.swift create mode 100644 ImageFilterEditor/ImageFilterEditor/UIImage+Scaling.swift create mode 100644 LambdaTimeline.xcodeproj/project.pbxproj create mode 100644 LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme create mode 100644 LambdaTimeline/Helpers/Extensions/AudioVisualizer.swift create mode 100644 LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift create mode 100644 LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift create mode 100644 LambdaTimeline/Helpers/ShiftableViewController.swift create mode 100644 LambdaTimeline/Model Controllers/PostController.swift create mode 100644 LambdaTimeline/Models/Comment.swift create mode 100644 LambdaTimeline/Models/Post.swift create mode 100644 LambdaTimeline/Resources/AppDelegate.swift create mode 100644 LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 LambdaTimeline/Resources/Assets.xcassets/Contents.json create mode 100644 LambdaTimeline/Resources/Info.plist create mode 100644 LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard create mode 100644 LambdaTimeline/Storyboards/Main.storyboard create mode 100644 LambdaTimeline/View Controllers/AudioCommentViewController.swift create mode 100644 LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift create mode 100644 LambdaTimeline/View Controllers/ImagePostViewController.swift create mode 100644 LambdaTimeline/View Controllers/PostsCollectionViewController.swift create mode 100644 LambdaTimeline/View Controllers/SignInViewController.swift create mode 100644 LambdaTimeline/Views/ImagePostCollectionViewCell.swift diff --git a/ImageFilterEditor/ImageFilterEditor.xcodeproj/project.pbxproj b/ImageFilterEditor/ImageFilterEditor.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c7746f55 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor.xcodeproj/project.pbxproj @@ -0,0 +1,350 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + E41CA4A5254A5B570053699D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41CA4A4254A5B570053699D /* AppDelegate.swift */; }; + E41CA4A7254A5B570053699D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41CA4A6254A5B570053699D /* SceneDelegate.swift */; }; + E41CA4AC254A5B570053699D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E41CA4AA254A5B570053699D /* Main.storyboard */; }; + E41CA4AE254A5B580053699D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E41CA4AD254A5B580053699D /* Assets.xcassets */; }; + E41CA4B1254A5B580053699D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E41CA4AF254A5B580053699D /* LaunchScreen.storyboard */; }; + E45CAC9E254A5BC900CAFE8D /* ImagePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45CAC9D254A5BC900CAFE8D /* ImagePostViewController.swift */; }; + E45CACA1254A5EB100CAFE8D /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45CACA0254A5EB100CAFE8D /* UIImage+Scaling.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + E41CA4A1254A5B570053699D /* ImageFilterEditor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImageFilterEditor.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E41CA4A4254A5B570053699D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + E41CA4A6254A5B570053699D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + E41CA4AB254A5B570053699D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + E41CA4AD254A5B580053699D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E41CA4B0254A5B580053699D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + E41CA4B2254A5B580053699D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E45CAC9D254A5BC900CAFE8D /* ImagePostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostViewController.swift; sourceTree = ""; }; + E45CACA0254A5EB100CAFE8D /* UIImage+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Scaling.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E41CA49E254A5B570053699D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E41CA498254A5B570053699D = { + isa = PBXGroup; + children = ( + E41CA4A3254A5B570053699D /* ImageFilterEditor */, + E41CA4A2254A5B570053699D /* Products */, + ); + sourceTree = ""; + }; + E41CA4A2254A5B570053699D /* Products */ = { + isa = PBXGroup; + children = ( + E41CA4A1254A5B570053699D /* ImageFilterEditor.app */, + ); + name = Products; + sourceTree = ""; + }; + E41CA4A3254A5B570053699D /* ImageFilterEditor */ = { + isa = PBXGroup; + children = ( + E41CA4A4254A5B570053699D /* AppDelegate.swift */, + E41CA4A6254A5B570053699D /* SceneDelegate.swift */, + E41CA4AA254A5B570053699D /* Main.storyboard */, + E41CA4AD254A5B580053699D /* Assets.xcassets */, + E41CA4AF254A5B580053699D /* LaunchScreen.storyboard */, + E41CA4B2254A5B580053699D /* Info.plist */, + E45CAC9D254A5BC900CAFE8D /* ImagePostViewController.swift */, + E45CACA0254A5EB100CAFE8D /* UIImage+Scaling.swift */, + ); + path = ImageFilterEditor; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E41CA4A0254A5B570053699D /* ImageFilterEditor */ = { + isa = PBXNativeTarget; + buildConfigurationList = E41CA4B5254A5B580053699D /* Build configuration list for PBXNativeTarget "ImageFilterEditor" */; + buildPhases = ( + E41CA49D254A5B570053699D /* Sources */, + E41CA49E254A5B570053699D /* Frameworks */, + E41CA49F254A5B570053699D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ImageFilterEditor; + productName = ImageFilterEditor; + productReference = E41CA4A1254A5B570053699D /* ImageFilterEditor.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E41CA499254A5B570053699D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1200; + LastUpgradeCheck = 1200; + TargetAttributes = { + E41CA4A0254A5B570053699D = { + CreatedOnToolsVersion = 12.0.1; + }; + }; + }; + buildConfigurationList = E41CA49C254A5B570053699D /* Build configuration list for PBXProject "ImageFilterEditor" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E41CA498254A5B570053699D; + productRefGroup = E41CA4A2254A5B570053699D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E41CA4A0254A5B570053699D /* ImageFilterEditor */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E41CA49F254A5B570053699D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E41CA4B1254A5B580053699D /* LaunchScreen.storyboard in Resources */, + E41CA4AE254A5B580053699D /* Assets.xcassets in Resources */, + E41CA4AC254A5B570053699D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E41CA49D254A5B570053699D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E41CA4A5254A5B570053699D /* AppDelegate.swift in Sources */, + E45CACA1254A5EB100CAFE8D /* UIImage+Scaling.swift in Sources */, + E45CAC9E254A5BC900CAFE8D /* ImagePostViewController.swift in Sources */, + E41CA4A7254A5B570053699D /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + E41CA4AA254A5B570053699D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E41CA4AB254A5B570053699D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + E41CA4AF254A5B580053699D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E41CA4B0254A5B580053699D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + E41CA4B3254A5B580053699D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + E41CA4B4254A5B580053699D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + E41CA4B6254A5B580053699D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4487ZJU795; + INFOPLIST_FILE = ImageFilterEditor/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.robvance.ImageFilterEditor; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E41CA4B7254A5B580053699D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4487ZJU795; + INFOPLIST_FILE = ImageFilterEditor/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.robvance.ImageFilterEditor; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E41CA49C254A5B570053699D /* Build configuration list for PBXProject "ImageFilterEditor" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E41CA4B3254A5B580053699D /* Debug */, + E41CA4B4254A5B580053699D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E41CA4B5254A5B580053699D /* Build configuration list for PBXNativeTarget "ImageFilterEditor" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E41CA4B6254A5B580053699D /* Debug */, + E41CA4B7254A5B580053699D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E41CA499254A5B570053699D /* Project object */; +} diff --git a/ImageFilterEditor/ImageFilterEditor/AppDelegate.swift b/ImageFilterEditor/ImageFilterEditor/AppDelegate.swift new file mode 100644 index 00000000..e3916d4d --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// ImageFilterEditor +// +// Created by Rob Vance on 10/28/20. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AccentColor.colorset/Contents.json b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AppIcon.appiconset/Contents.json b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/Contents.json b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFilterEditor/ImageFilterEditor/Base.lproj/LaunchScreen.storyboard b/ImageFilterEditor/ImageFilterEditor/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageFilterEditor/ImageFilterEditor/Base.lproj/Main.storyboard b/ImageFilterEditor/ImageFilterEditor/Base.lproj/Main.storyboard new file mode 100644 index 00000000..5ea3b610 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Base.lproj/Main.storyboard @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageFilterEditor/ImageFilterEditor/ImagePostViewController.swift b/ImageFilterEditor/ImageFilterEditor/ImagePostViewController.swift new file mode 100644 index 00000000..a6d0b567 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/ImagePostViewController.swift @@ -0,0 +1,153 @@ +// +// ImagePostViewController.swift +// ImageFilterEditor +// +// Created by Rob Vance on 10/28/20. +// + +import UIKit +import CoreImage +import CoreImage.CIFilterBuiltins +import Photos + +class ImagePostViewController: UIViewController { + + // MARK: - IBOutlets - + @IBOutlet weak var brightnessSlider: UISlider! + @IBOutlet weak var contrastSlider: UISlider! + @IBOutlet weak var blurSlider: UISlider! + @IBOutlet weak var saturationSlider: UISlider! + @IBOutlet weak var vignetteSlider: UISlider! + @IBOutlet weak var choosePhotoButton: UIButton! + @IBOutlet weak var imageView: UIImageView! + + // Mark: - Properties - + + private var orignalImage: UIImage? { + didSet { + guard let orignalImage = orignalImage else { + scaledImage = nil + return + } + + var scaledSize = imageView.bounds.size + let scale = imageView.contentScaleFactor + + scaledSize.width *= scale + scaledSize.height *= scale + guard let scaledUIImage = orignalImage.imageByScaling(toSize: scaledSize) else { + scaledImage = nil + return + } + scaledImage = CIImage(image: scaledUIImage) + } + } + private var scaledImage: CIImage? { + didSet { + updateImage() + } + } + private let context = CIContext() + private let colorControlsFilter = CIFilter.colorControls() + private let blurFilter = CIFilter.gaussianBlur() + + + override func viewDidLoad() { + super.viewDidLoad() + orignalImage = imageView.image + } + + + private func image(byFiltering image: CIImage) -> UIImage? { + let inputImage = image + + colorControlsFilter.inputImage = inputImage + colorControlsFilter.saturation = saturationSlider.value + colorControlsFilter.brightness = brightnessSlider.value + colorControlsFilter.contrast = contrastSlider.value + + // blur filter + let blurFilter = CIFilter.gaussianBlur() + blurFilter.inputImage = colorControlsFilter.outputImage?.clampedToExtent() + blurFilter.radius = blurSlider.value + + // vignette filter + let vignetteFilter = CIFilter.vignette() + vignetteFilter.inputImage = blurFilter.outputImage?.clampedToExtent() + vignetteFilter.intensity = vignetteSlider.value + vignetteFilter.radius = vignetteSlider.value + + guard let outputImage = blurFilter.outputImage else { return nil } + + guard let renderedCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { return nil } + + return UIImage(cgImage: renderedCGImage) + } + + private func updateImage() { + if let scaledImage = scaledImage { + imageView.image = image(byFiltering: scaledImage) + } else { + imageView.image = nil + } + } + + // MARK: - IBActions - + + @IBAction func choosePhotoTapped(_ sender: Any) { + guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { + print("Photo library is not available.") + return + } + let imagePicker = UIImagePickerController() + imagePicker.sourceType = .photoLibrary + imagePicker.delegate = self + + present(imagePicker, animated: true) + } + + @IBAction func savePhotoTapped(_ sender: Any) { + + } + + // MARK: - Sliders - + @IBAction func brightnessChanged(_ sender: Any) { + updateImage() + } + + @IBAction func contrastChanged(_ sender: Any) { + updateImage() + } + + @IBAction func saturationChanged(_ sender: Any) { + updateImage() + } + + @IBAction func blurChanged(_ sender: Any) { + updateImage() + } + + @IBAction func vignetteChanged(_ sender: Any) { + updateImage() + } + +} + +extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + if let image = info[.editedImage] as? UIImage { + orignalImage = image + } else if let image = info[.originalImage] as? UIImage { + orignalImage = image + } + + picker.dismiss(animated: true, completion: nil) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true, completion: nil) + } + +} diff --git a/ImageFilterEditor/ImageFilterEditor/Info.plist b/ImageFilterEditor/ImageFilterEditor/Info.plist new file mode 100644 index 00000000..d0141709 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME) needs access to your library to filter images + NSPhotoLibraryAddUsageDescription + $(PRODUCT_NAME)needs access to your library to save filtered images + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ImageFilterEditor/ImageFilterEditor/SceneDelegate.swift b/ImageFilterEditor/ImageFilterEditor/SceneDelegate.swift new file mode 100644 index 00000000..7f093900 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// ImageFilterEditor +// +// Created by Rob Vance on 10/28/20. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/ImageFilterEditor/ImageFilterEditor/UIImage+Scaling.swift b/ImageFilterEditor/ImageFilterEditor/UIImage+Scaling.swift new file mode 100644 index 00000000..9cc56207 --- /dev/null +++ b/ImageFilterEditor/ImageFilterEditor/UIImage+Scaling.swift @@ -0,0 +1,37 @@ +// +// UIImage+Scaling.swift +// ImageFilterEditor +// +// Created by Rob Vance on 10/28/20. +// + +import UIKit + +extension UIImage { + + /// Resize the image to a max dimension from size parameter + func imageByScaling(toSize size: CGSize) -> UIImage? { + guard size.width > 0 && size.height > 0 else { return nil } + + let originalAspectRatio = self.size.width/self.size.height + var correctedSize = size + + if correctedSize.width > correctedSize.width*originalAspectRatio { + correctedSize.width = correctedSize.width*originalAspectRatio + } else { + correctedSize.height = correctedSize.height/originalAspectRatio + } + + return UIGraphicsImageRenderer(size: correctedSize, format: imageRendererFormat).image { context in + draw(in: CGRect(origin: .zero, size: correctedSize)) + } + } + + /// Renders the image if the pixel data was rotated due to orientation of camera + var flattened: UIImage { + if imageOrientation == .up { return self } + return UIGraphicsImageRenderer(size: size, format: imageRendererFormat).image { context in + draw(at: .zero) + } + } +} diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj new file mode 100644 index 00000000..18c2b005 --- /dev/null +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -0,0 +1,448 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */; }; + 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46463782216FDE4C00E7FF73 /* Assets.xcassets */; }; + 46463786216FDE4C00E7FF73 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */; }; + 46463790216FFD1B00E7FF73 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646378F216FFD1B00E7FF73 /* Post.swift */; }; + 46463792216FFDD900E7FF73 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46463791216FFDD900E7FF73 /* Comment.swift */; }; + 4646379C2170091A00E7FF73 /* PostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4646379B2170091A00E7FF73 /* PostController.swift */; }; + 46A0366A21700F5100E7FF73 /* PostsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */; }; + 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A0366C2170158900E7FF73 /* SignInViewController.swift */; }; + 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */; }; + 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */; }; + 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */; }; + 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */; }; + 46D1A48924FF0BC4008D1CA7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46D1A48824FF0BC4008D1CA7 /* Main.storyboard */; }; + 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */; }; + 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */; }; + E4EC7FDB254D0E9200499DC0 /* AudioVisualizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */; }; + E4F93081254D00870093AFF1 /* AudioCommentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LambdaTimeline.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 46463782216FDE4C00E7FF73 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 46463785216FDE4C00E7FF73 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 46463787216FDE4C00E7FF73 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4646378F216FFD1B00E7FF73 /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; + 46463791216FFDD900E7FF73 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + 4646379B2170091A00E7FF73 /* PostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostController.swift; sourceTree = ""; }; + 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsCollectionViewController.swift; sourceTree = ""; }; + 46A0366C2170158900E7FF73 /* SignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; + 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostViewController.swift; sourceTree = ""; }; + 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+InformationalAlert.swift"; sourceTree = ""; }; + 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShiftableViewController.swift; sourceTree = ""; }; + 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostCollectionViewCell.swift; sourceTree = ""; }; + 46D1A48824FF0BC4008D1CA7 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; + 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Ratio.swift"; sourceTree = ""; }; + 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostDetailTableViewController.swift; sourceTree = ""; }; + E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVisualizer.swift; sourceTree = ""; }; + E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioCommentViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 46463775216FDE4B00E7FF73 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4646376F216FDE4B00E7FF73 = { + isa = PBXGroup; + children = ( + 4646377A216FDE4B00E7FF73 /* LambdaTimeline */, + 46463779216FDE4B00E7FF73 /* Products */, + ); + sourceTree = ""; + }; + 46463779216FDE4B00E7FF73 /* Products */ = { + isa = PBXGroup; + children = ( + 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */, + ); + name = Products; + sourceTree = ""; + }; + 4646377A216FDE4B00E7FF73 /* LambdaTimeline */ = { + isa = PBXGroup; + children = ( + 46CFE7042171572900E7FF73 /* Views */, + 46A0366B21700F6100E7FF73 /* View Controllers */, + 4646379A2170090900E7FF73 /* Model Controllers */, + 46463795216FFF5800E7FF73 /* Models */, + 46463797216FFF7400E7FF73 /* Storyboards */, + 46CFE7062171573F00E7FF73 /* Helpers */, + 46463796216FFF6900E7FF73 /* Resources */, + ); + path = LambdaTimeline; + sourceTree = ""; + }; + 46463795216FFF5800E7FF73 /* Models */ = { + isa = PBXGroup; + children = ( + 4646378F216FFD1B00E7FF73 /* Post.swift */, + 46463791216FFDD900E7FF73 /* Comment.swift */, + ); + path = Models; + sourceTree = ""; + }; + 46463796216FFF6900E7FF73 /* Resources */ = { + isa = PBXGroup; + children = ( + 4646377B216FDE4B00E7FF73 /* AppDelegate.swift */, + 46463782216FDE4C00E7FF73 /* Assets.xcassets */, + 46463787216FDE4C00E7FF73 /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 46463797216FFF7400E7FF73 /* Storyboards */ = { + isa = PBXGroup; + children = ( + 46D1A48824FF0BC4008D1CA7 /* Main.storyboard */, + 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */, + ); + path = Storyboards; + sourceTree = ""; + }; + 4646379A2170090900E7FF73 /* Model Controllers */ = { + isa = PBXGroup; + children = ( + 4646379B2170091A00E7FF73 /* PostController.swift */, + ); + path = "Model Controllers"; + sourceTree = ""; + }; + 46A0366B21700F6100E7FF73 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 46A0366C2170158900E7FF73 /* SignInViewController.swift */, + 46A0366921700F5100E7FF73 /* PostsCollectionViewController.swift */, + 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, + 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, + E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 46CFE7042171572900E7FF73 /* Views */ = { + isa = PBXGroup; + children = ( + 46CFE7022171572600E7FF73 /* ImagePostCollectionViewCell.swift */, + ); + path = Views; + sourceTree = ""; + }; + 46CFE7062171573F00E7FF73 /* Helpers */ = { + isa = PBXGroup; + children = ( + 46D571F62173D6D200E7FF73 /* Extensions */, + 46CFE6F82170862C00E7FF73 /* ShiftableViewController.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 46D571F62173D6D200E7FF73 /* Extensions */ = { + isa = PBXGroup; + children = ( + E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */, + 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */, + 46CFE6F621707FA600E7FF73 /* UIViewController+InformationalAlert.swift */, + ); + path = Extensions; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 46463777216FDE4B00E7FF73 /* LambdaTimeline */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4646378A216FDE4C00E7FF73 /* Build configuration list for PBXNativeTarget "LambdaTimeline" */; + buildPhases = ( + 46463774216FDE4B00E7FF73 /* Sources */, + 46463775216FDE4B00E7FF73 /* Frameworks */, + 46463776216FDE4B00E7FF73 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LambdaTimeline; + productName = LambdaTimeline; + productReference = 46463778216FDE4B00E7FF73 /* LambdaTimeline.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 46463770216FDE4B00E7FF73 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1000; + LastUpgradeCheck = 1000; + ORGANIZATIONNAME = "Lambda School"; + TargetAttributes = { + 46463777216FDE4B00E7FF73 = { + CreatedOnToolsVersion = 10.0; + LastSwiftMigration = 1130; + }; + }; + }; + buildConfigurationList = 46463773216FDE4B00E7FF73 /* Build configuration list for PBXProject "LambdaTimeline" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4646376F216FDE4B00E7FF73; + productRefGroup = 46463779216FDE4B00E7FF73 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 46463777216FDE4B00E7FF73 /* LambdaTimeline */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 46463776216FDE4B00E7FF73 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 46D1A48924FF0BC4008D1CA7 /* Main.storyboard in Resources */, + 46463786216FDE4C00E7FF73 /* LaunchScreen.storyboard in Resources */, + 46463783216FDE4C00E7FF73 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 46463774216FDE4B00E7FF73 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E4EC7FDB254D0E9200499DC0 /* AudioVisualizer.swift in Sources */, + 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */, + 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, + 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, + 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, + 46463790216FFD1B00E7FF73 /* Post.swift in Sources */, + 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */, + 46CFE7032171572600E7FF73 /* ImagePostCollectionViewCell.swift in Sources */, + 4646379C2170091A00E7FF73 /* PostController.swift in Sources */, + 46A0366A21700F5100E7FF73 /* PostsCollectionViewController.swift in Sources */, + 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */, + 46463792216FFDD900E7FF73 /* Comment.swift in Sources */, + 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.swift in Sources */, + E4F93081254D00870093AFF1 /* AudioCommentViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 46463784216FDE4C00E7FF73 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 46463785216FDE4C00E7FF73 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 46463788216FDE4C00E7FF73 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 46463789216FDE4C00E7FF73 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4646378B216FDE4C00E7FF73 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.LambdaSchool.LambdaTimeline; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4646378C216FDE4C00E7FF73 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/LambdaTimeline/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.LambdaSchool.LambdaTimeline; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 46463773216FDE4B00E7FF73 /* Build configuration list for PBXProject "LambdaTimeline" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 46463788216FDE4C00E7FF73 /* Debug */, + 46463789216FDE4C00E7FF73 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4646378A216FDE4C00E7FF73 /* Build configuration list for PBXNativeTarget "LambdaTimeline" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4646378B216FDE4C00E7FF73 /* Debug */, + 4646378C216FDE4C00E7FF73 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 46463770216FDE4B00E7FF73 /* Project object */; +} diff --git a/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme b/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme new file mode 100644 index 00000000..d1c2f96e --- /dev/null +++ b/LambdaTimeline.xcodeproj/xcshareddata/xcschemes/LambdaTimeline.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LambdaTimeline/Helpers/Extensions/AudioVisualizer.swift b/LambdaTimeline/Helpers/Extensions/AudioVisualizer.swift new file mode 100644 index 00000000..83eceaf7 --- /dev/null +++ b/LambdaTimeline/Helpers/Extensions/AudioVisualizer.swift @@ -0,0 +1,275 @@ +// +// AudioVisualizer.swift +// LambdaTimeline +// +// Created by Rob Vance on 10/30/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit + +@IBDesignable +class AudioVisualizer: UIView { + + // MARK: IBInspectable Properties + + /// The width of a bar in points. + @IBInspectable public var barWidth: CGFloat = 10 { + didSet { + updateBars() + } + } + + /// The corner radius of a bar in points. If less than `0`, then it will default to half of the width of the bar. + @IBInspectable public var barCornerRadius: CGFloat = -1 { + didSet { + updateBars() + } + } + + /// The spacing between bars in points. + @IBInspectable public var barSpacing: CGFloat = 4 { + didSet { + updateBars() + } + } + + /// The color of a bar. + @IBInspectable public var barColor: UIColor = .systemGreen { + didSet { + for bar in bars { + bar.backgroundColor = barColor + } + } + } + + /// The amount of time before a bar decays into the adjacent spot + @IBInspectable public var decaySpeed: Double = 0.01 { + didSet { + decayTimer?.invalidate() + decayTimer = nil + } + } + + /// The fraction the newest value will decay by if not updated by the time a decay starts + @IBInspectable public var decayAmount: Double = 0.8 + + // MARK: Internal Properties + + private var bars = [UIView]() + private var values = [Double]() + + private weak var decayTimer: Timer? + private var newestValue: Double = 0 + + // MARK: - Object Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + + initialSetup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + initialSetup() + } + + func initialSetup() { + // Pre-fill values for Interface Builder preview + #if TARGET_INTERFACE_BUILDER + values = [ + 0.19767167952644904, + 0.30975147553721694, + 0.2717680681330001, + 0.25914398105158504, + 0.3413322535900626, + 0.311223010327166, + 0.3302641160440099, + 0.303853272136915, + 0.2659123465612464, + 0.2860924489760262, + 0.26477145407733543, + 0.23180693200970012, + 0.24445487891619533, + 0.21484121767935302, + 0.19688917099112885, + 0.19020094289324854, + 0.17402194532721785, + 0.1600055988294578, + 0.15120753744055154, + 0.13789741397752767, + 0.13231033268544698, + 0.1270923459375989, + 0.1121238175344413, + 0.12400069790665748, + 0.24978783142512598, + 0.233063298365594, + 0.5375441947045457, + 0.47456518731446534, + 0.5236630241490436, + 0.4692151822551929, + 0.4255172022748686, + 0.46023063710569184, + 0.42934908823397355, + 0.37221041959882545, + 0.4685050055667653, + 0.4209394065681193, + 0.46643118034506187, + 0.4292307341708633, + 0.3814422662003417, + 0.4386719969186142, + 0.3956598546828729 + ] + #endif + + // Build the inner bars + self.updateBars() + } + + deinit { + // Invalidate the timer if it is still active + decayTimer?.invalidate() + } + + // MARK: - Layout + override public func layoutSubviews() { + updateBars() + } + + private func updateBars() { + // Clean up old bars + for bar in bars { + bar.removeFromSuperview() + } + + var newBars = [UIView]() + + + // Make sure the width of a bar and spacing is greater than 0, and that the available width is also greater than 0 + guard round(barWidth) > 0, barSpacing >= 0, bounds.width > 0, bounds.height > 0 else { + // Not enough information to create a single bar, so bail early + bars = [] + return + } + + // Calculate number of bars we will be able to display + var numberOfBarsToCreate = Int(bounds.width/(barWidth + barSpacing)) + + // Helper function for creating bars + func createBar(_ positionFromCenter: Int) { + let bar = UIView(frame: frame(forBar: positionFromCenter)) + bar.backgroundColor = barColor + bar.layer.cornerRadius = (barCornerRadius < 0 || barCornerRadius > barWidth/2) ? floor(barWidth/3) : barCornerRadius + + numberOfBarsToCreate -= 1 + newBars.append(bar) + self.addSubview(bar) + } + + // Create the center bar + createBar(0) + + // Keep creating bars in pairs until there is no more room + var position = 1 + while numberOfBarsToCreate > 0 { + // Create the symmetric pairs of bars starting from the center + createBar(-position) + createBar(position) + + position += 1 + } + + bars = newBars + } + + /// Calculate the frame of a particular bar + /// - Parameter positionFromCenter: The distance of the bar from the center (which is 0) + private func frame(forBar positionFromCenter: Int) -> CGRect { + let valueIndex = Int(positionFromCenter.magnitude) + + return frame(forBar: positionFromCenter, value: (valueIndex < values.count) ? values[valueIndex] : 0) + } + + /// Calculate the frame of a particular bar, specifying a value + /// - Parameter positionFromCenter: The distance of the bar from the center (which is 0) + private func frame(forBar positionFromCenter: Int, value: Double) -> CGRect { + let maxValue = (1 - CGFloat(positionFromCenter.magnitude)*(barWidth + barSpacing)/bounds.width/2)*bounds.height/2 + let height = CGFloat(value)*maxValue + + return CGRect(x: floor(bounds.width/2) + CGFloat(positionFromCenter)*(barWidth + barSpacing) - barWidth/2, y: floor(bounds.height/2) - height, width: barWidth, height: height*2) + } + + // MARK: - Animation + + /// Start the decay timer, but only if if hasn't been created yet + private func startTimer() { + guard decayTimer == nil else { return } + + decayTimer = Timer.scheduledTimer(withTimeInterval: decaySpeed, repeats: true) { [weak self] (_) in + guard let self = self else { return } + + self.decayNewestValue() + } + } + + private func decayNewestValue() { + values.insert(newestValue, at: 0) + + // Trim the end of the values array if there are too many for the number of bars + let currentCount = values.count + let maxCount = (bars.count + 1)/2 + /* + Note that the amount of bars will always be either 0, or an odd number (since the bars are counted in pairs after the first central bar), so we chose a "transformation" (a mathematical function) that satisfies this: value index = floor((bar index + 1)/2) + + Bar index: 0 1 2 3 4 5 6 7 8 9 ... + (valid bar index): 0 1 - 3 - 5 - 7 - 9 ... + Value index: 0 1 1 2 2 3 3 4 4 5 ... + + */ + if currentCount > maxCount { + values.removeSubrange(maxCount ..< currentCount) + } + + // Update the frames of each bar + for (positionFromCenter, value) in values.enumerated() { + if positionFromCenter == 0 { + bars[0].frame = frame(forBar: positionFromCenter, value: value) + } else { + bars[positionFromCenter*2 - 1].frame = frame(forBar: -positionFromCenter, value: value) + bars[positionFromCenter*2].frame = frame(forBar: positionFromCenter, value: value) + } + } + + // Decay the newest value + newestValue = newestValue*decayAmount + + // Check if the values are empty + let totalValue = values.reduce(0.0) { $0 + $1 } + if totalValue <= 0.000001 { + // Note that total value may never reach 0, but this is small enough to clear everything out + decayTimer?.invalidate() + decayTimer = nil + } + } + + // MARK: - Public Methods + + /// Add a value to the visualizer. Be sure to call `AVAudioPlayer.isMeteringEnabled = true`, and `AVAudioPlayer.updateMeters()` before every call to `AVAudioPlayer.averagePower(forChannel: 0)` + /// - Parameter decibelValue: The value you would get out of `AVAudioPlayer.averagePower(forChannel: 0)` + public func addValue(decibelValue: Float) { + addValue(decibelValue: Double(decibelValue)) + } + + /// Add a value to the visualizer. Be sure to call `AVAudioPlayer.isMeteringEnabled = true`, and `AVAudioPlayer.updateMeters()` before every call to `AVAudioPlayer.averagePower(forChannel: 0)` + /// - Parameter decibelValue: The value you would get out of `AVAudioPlayer.averagePower(forChannel: 0)` + public func addValue(decibelValue: Double) { + let normalizedValue = __exp10(decibelValue/20) + + newestValue = normalizedValue + + startTimer() + } + +} diff --git a/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift b/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift new file mode 100644 index 00000000..79bb32d7 --- /dev/null +++ b/LambdaTimeline/Helpers/Extensions/UIImage+Ratio.swift @@ -0,0 +1,15 @@ +// +// UIImage+Ratio.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/14/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +extension UIImage { + var ratio: CGFloat { + return size.height / size.width + } +} diff --git a/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift b/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift new file mode 100644 index 00000000..d4480303 --- /dev/null +++ b/LambdaTimeline/Helpers/Extensions/UIViewController+InformationalAlert.swift @@ -0,0 +1,21 @@ +// +// UIViewController+InformationalAlert.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/12/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +extension UIViewController { + + func presentInformationalAlertController(title: String?, message: String?, dismissActionCompletion: ((UIAlertAction) -> Void)? = nil, completion: (() -> Void)? = nil) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let dismissAction = UIAlertAction(title: "Dismiss", style: .cancel, handler: dismissActionCompletion) + + alertController.addAction(dismissAction) + + present(alertController, animated: true, completion: completion) + } +} diff --git a/LambdaTimeline/Helpers/ShiftableViewController.swift b/LambdaTimeline/Helpers/ShiftableViewController.swift new file mode 100644 index 00000000..5923d54f --- /dev/null +++ b/LambdaTimeline/Helpers/ShiftableViewController.swift @@ -0,0 +1,126 @@ +// +// Created by Spencer Curtis. +// Copyright © 2017-2018 Spencer Curtis. All rights reserved. +// + +/* + All you need to do is set your subclass of ShiftableViewController as the delegate for all + UITextFields and UITextViews that you want to be shifted up so the keyboard doesn't obscure it. + */ + +import UIKit + +class ShiftableViewController: UIViewController, UITextFieldDelegate, UITextViewDelegate, UIGestureRecognizerDelegate { + + var currentYShiftForKeyboard: CGFloat = 0 + + var textFieldBeingEdited: UITextField? + var textViewBeingEdited: UITextView? + + var keyboardDismissTapGestureRecognizer: UITapGestureRecognizer! + + override func viewDidLoad() { + super.viewDidLoad() + + setupKeyboardDismissTapGestureRecognizer() + + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + @objc func stopEditingTextInput() { + if let textField = self.textFieldBeingEdited { + + textField.resignFirstResponder() + + self.textFieldBeingEdited = nil + self.textViewBeingEdited = nil + } else if let textView = self.textViewBeingEdited { + + textView.resignFirstResponder() + + self.textFieldBeingEdited = nil + self.textViewBeingEdited = nil + } + + guard keyboardDismissTapGestureRecognizer.isEnabled else { return } + + keyboardDismissTapGestureRecognizer.isEnabled = false + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + textFieldBeingEdited = textField + } + + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + textViewBeingEdited = textView + return true + } + + @objc func keyboardWillShow(notification: Notification) { + + keyboardDismissTapGestureRecognizer.isEnabled = true + + var keyboardSize: CGRect = .zero + + if let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, + keyboardRect.height != 0 { + keyboardSize = keyboardRect + } else if let keyboardRect = notification.userInfo?["UIKeyboardBoundsUserInfoKey"] as? CGRect { + keyboardSize = keyboardRect + } + + if let textField = textFieldBeingEdited { + if self.view.frame.origin.y == 0 { + + let yShift = yShiftWhenKeyboardAppearsFor(textInput: textField, keyboardSize: keyboardSize, nextY: keyboardSize.height) + self.currentYShiftForKeyboard = yShift + self.view.frame.origin.y -= yShift + } + } else if let textView = textViewBeingEdited { + if self.view.frame.origin.y == 0 { + + let yShift = yShiftWhenKeyboardAppearsFor(textInput: textView, keyboardSize: keyboardSize, nextY: keyboardSize.height) + self.currentYShiftForKeyboard = yShift + self.view.frame.origin.y -= yShift + } + } + } + + @objc func yShiftWhenKeyboardAppearsFor(textInput: UIView, keyboardSize: CGRect, nextY: CGFloat) -> CGFloat { + + let textFieldOrigin = self.view.convert(textInput.frame, from: textInput.superview!).origin.y + let textFieldBottomY = textFieldOrigin + textInput.frame.size.height + + // This is the y point that the textField's bottom can be at before it gets covered by the keyboard + let maximumY = self.view.frame.height - (keyboardSize.height + view.safeAreaInsets.bottom) + + if textFieldBottomY > maximumY { + // This makes the view shift the right amount to have the text field being edited just above they keyboard if it would have been covered by the keyboard. + return textFieldBottomY - maximumY + } else { + // It would go off the screen if moved, and it won't be obscured by the keyboard. + return 0 + } + } + + @objc func keyboardWillHide(notification: Notification) { + + if self.view.frame.origin.y != 0 { + + self.view.frame.origin.y += currentYShiftForKeyboard + } + + stopEditingTextInput() + } + + @objc func setupKeyboardDismissTapGestureRecognizer() { + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(stopEditingTextInput)) + tapGestureRecognizer.numberOfTapsRequired = 1 + + view.addGestureRecognizer(tapGestureRecognizer) + + keyboardDismissTapGestureRecognizer = tapGestureRecognizer + } +} diff --git a/LambdaTimeline/Model Controllers/PostController.swift b/LambdaTimeline/Model Controllers/PostController.swift new file mode 100644 index 00000000..492b07ab --- /dev/null +++ b/LambdaTimeline/Model Controllers/PostController.swift @@ -0,0 +1,43 @@ +// +// PostController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class PostController { + + static let shared = PostController() + var posts: [Post] = [] + + var currentUser: String? { + UserDefaults.standard.string(forKey: "username") + } + + func createImagePost(with title: String, image: UIImage, ratio: CGFloat?, audioURL: URL?) { + + guard let currentUser = currentUser else { return } + + let post = Post(title: title, mediaType: .image(image), ratio: ratio, author: currentUser, audioURL: audioURL) + + posts.append(post) + } + + func addComment(with text: String, to post: inout Post) { + + guard let currentUser = currentUser else { return } + + let comment = Comment(text: text, author: currentUser, audioURL: nil) + post.comments.append(comment) + + } + func addAudioComment(with url: URL, to post: inout Post) { + guard let currentUser = currentUser else { return } + + let comment = Comment(author: currentUser, audioURL: url) + post.comments.append(comment) + } +} diff --git a/LambdaTimeline/Models/Comment.swift b/LambdaTimeline/Models/Comment.swift new file mode 100644 index 00000000..e8770549 --- /dev/null +++ b/LambdaTimeline/Models/Comment.swift @@ -0,0 +1,37 @@ +// +// Comment.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import Foundation + +class Comment: Hashable { + + static private let textKey = "text" + static private let authorKey = "author" + static private let timestampKey = "timestamp" + + let text: String? + let author: String + let timestamp: Date + let audioURL: URL? + + init(text: String? = nil, author: String, timestamp: Date = Date(), audioURL: URL?) { + self.text = text + self.author = author + self.timestamp = timestamp + self.audioURL = audioURL + } + + func hash(into hasher: inout Hasher) { + hasher.combine(timestamp.hashValue ^ author.hashValue) + } + + static func ==(lhs: Comment, rhs: Comment) -> Bool { + return lhs.author == rhs.author && + lhs.timestamp == rhs.timestamp + } +} diff --git a/LambdaTimeline/Models/Post.swift b/LambdaTimeline/Models/Post.swift new file mode 100644 index 00000000..f6b070e8 --- /dev/null +++ b/LambdaTimeline/Models/Post.swift @@ -0,0 +1,44 @@ +// +// Post.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +enum MediaType { + case image(UIImage) +} + +class Post: Equatable { + + let mediaType: MediaType + let author: String + let timestamp: Date + var comments: [Comment] + var ratio: CGFloat? + var id: String? + + var title: String? { + comments.first?.text + } + + var audioURL: URL? { + comments.first?.audioURL + } + + init(title: String, mediaType: MediaType, ratio: CGFloat?, author: String, timestamp: Date = Date(), audioURL: URL?) { + self.mediaType = mediaType + self.ratio = ratio + self.author = author + self.comments = [Comment(text: title, author: author, audioURL: audioURL)] + self.timestamp = timestamp + self.id = UUID().uuidString + } + + static func ==(lhs: Post, rhs: Post) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/LambdaTimeline/Resources/AppDelegate.swift b/LambdaTimeline/Resources/AppDelegate.swift new file mode 100644 index 00000000..a44cc8ca --- /dev/null +++ b/LambdaTimeline/Resources/AppDelegate.swift @@ -0,0 +1,20 @@ +// +// AppDelegate.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } +} + diff --git a/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d8db8d65 --- /dev/null +++ b/LambdaTimeline/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LambdaTimeline/Resources/Assets.xcassets/Contents.json b/LambdaTimeline/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/LambdaTimeline/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist new file mode 100644 index 00000000..dab10c0a --- /dev/null +++ b/LambdaTimeline/Resources/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + Copy-paste-your-REVERSED_CLIENT_ID-from-GoogleService-Info.plist + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSPhotoLibraryUsageDescription + In order to allow you to add photos to posts + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard b/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..bfa36129 --- /dev/null +++ b/LambdaTimeline/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard new file mode 100644 index 00000000..d86b272b --- /dev/null +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -0,0 +1,483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LambdaTimeline/View Controllers/AudioCommentViewController.swift b/LambdaTimeline/View Controllers/AudioCommentViewController.swift new file mode 100644 index 00000000..5f3f802e --- /dev/null +++ b/LambdaTimeline/View Controllers/AudioCommentViewController.swift @@ -0,0 +1,295 @@ +// +// AudioCommentViewController.swift +// LambdaTimeline +// +// Created by Rob Vance on 10/30/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import AVFoundation + +protocol VoiceCommentDelegate { + func reloadData() +} + +class AudioCommentViewController: UIViewController { + + // Mark: - IBOutlets - + @IBOutlet weak var timeElapsedLabel: UILabel! + @IBOutlet weak var timeRemainingLabel: UILabel! + @IBOutlet weak var timeSlider: UISlider! + @IBOutlet weak var playButton: UIButton! + @IBOutlet weak var recordButton: UIButton! + @IBOutlet weak var audioVisualizer: AudioVisualizer! + + + // Mark: - Properties - + var audioPlayer: AVAudioPlayer?{ + didSet { + guard let audioPlayer = audioPlayer else { return } + + audioPlayer.delegate = self + audioPlayer.isMeteringEnabled = true + updateViews() + } + } + + weak var timer: Timer? + var audioRecorder: AVAudioRecorder? + + let postController = PostController.shared + var post: Post! + var delegate: VoiceCommentDelegate? + var recordingURL: URL? + + private lazy var timeIntervalFormatter: DateComponentsFormatter = { + // NOTE: DateComponentFormatter is good for minutes/hours/seconds + // DateComponentsFormatter is not good for milliseconds, use DateFormatter instead) + + let formatting = DateComponentsFormatter() + formatting.unitsStyle = .positional // 00:00 mm:ss + formatting.zeroFormattingBehavior = .pad + formatting.allowedUnits = [.minute, .second] + return formatting + }() + + // MARK: - View Controller Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Use a font that won't jump around as values change + timeElapsedLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeElapsedLabel.font.pointSize, + weight: .regular) + timeRemainingLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeRemainingLabel.font.pointSize, + weight: .regular) + } + + func updateViews() { + playButton.isEnabled = !isRecording + recordButton.isEnabled = !isPlaying + timeSlider.isEnabled = !isRecording + playButton.isSelected = isPlaying + recordButton.isSelected = isRecording + if !isRecording { + let elapsedTime = audioPlayer?.currentTime ?? 0 + let duration = audioPlayer?.duration ?? 0 + let timeRemaining = duration.rounded() - elapsedTime + timeElapsedLabel.text = timeIntervalFormatter.string(from: elapsedTime) + timeSlider.minimumValue = 0 + timeSlider.maximumValue = Float(duration) + timeSlider.value = Float(elapsedTime) + timeRemainingLabel.text = "-" + timeIntervalFormatter.string(from: timeRemaining)! + } else { + let elapsedTime = audioRecorder?.currentTime ?? 0 + timeElapsedLabel.text = "--:--" + timeSlider.minimumValue = 0 + timeSlider.maximumValue = 1 + timeSlider.value = 0 + timeRemainingLabel.text = timeIntervalFormatter.string(from: elapsedTime) + } + } + + deinit { + timer?.invalidate() + } + + // MARK: - Timer + func startTimer() { + timer?.invalidate() + + timer = Timer.scheduledTimer(withTimeInterval: 0.030, repeats: true) { [weak self] (_) in + guard let self = self else { return } + + self.updateViews() + + if let audioRecorder = self.audioRecorder, + self.isRecording == true { + + audioRecorder.updateMeters() + self.audioVisualizer.addValue(decibelValue: audioRecorder.averagePower(forChannel: 0)) + + } + + if let audioPlayer = self.audioPlayer, + self.isPlaying == true { + + audioPlayer.updateMeters() + self.audioVisualizer.addValue(decibelValue: audioPlayer.averagePower(forChannel: 0)) + } + } + } + + func cancelTimer() { + timer?.invalidate() + timer = nil + } + + // MARK: - Playback + var isPlaying: Bool { + audioPlayer?.isPlaying ?? false + } + + func prepareAudioSession() throws { + let session = AVAudioSession.sharedInstance() + try session.setCategory(.playAndRecord, options: [.defaultToSpeaker]) + try session.setActive(true, options: []) // can fail if on a phone call, for instance + } + + func play() { + do{ + try prepareAudioSession() + audioPlayer?.play() + updateViews() + startTimer() + } catch { + print("Can't play audio: \(error)") + } + } + + func pause() { + audioPlayer?.pause() + updateViews() + cancelTimer() + } + + + // MARK: - Recording + + var isRecording: Bool { + audioRecorder?.isRecording ?? false + } + + + func requestPermissionOrStartRecording() { + switch AVAudioSession.sharedInstance().recordPermission { + case .undetermined: + AVAudioSession.sharedInstance().requestRecordPermission { granted in + guard granted == true else { + print("We need microphone access") + return + } + + print("Recording permission has been granted!") + // NOTE: Invite the user to tap record again, since we just interrupted them, and they may not have been ready to record + } + case .denied: + print("Microphone access has been blocked.") + + let alertController = UIAlertController(title: "Microphone Access Denied", message: "Please allow this app to access your Microphone.", preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "Open Settings", style: .default) { (_) in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + }) + + alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil)) + + present(alertController, animated: true, completion: nil) + case .granted: + startRecording() + @unknown default: + break + } + } + + func newRecordingURL() -> URL { + let fm = FileManager.default + let documentsDir = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + + let randomId = Int.random(in: 0...1_000_00) + + return documentsDir.appendingPathComponent("TestRecording" + "\(randomId)").appendingPathExtension("caf") + } + + func startRecording() { + do{ + try prepareAudioSession() + } catch { + print("Can't record audio: \(error)") + return + } + + recordingURL = newRecordingURL() + + let format = AVAudioFormat(standardFormatWithSampleRate: 44_100, channels: 1)! + do { + audioRecorder = try AVAudioRecorder(url: recordingURL!, format: format) + audioRecorder?.delegate = self + audioRecorder?.isMeteringEnabled = true + audioRecorder?.record() + updateViews() + startTimer() + } catch { + preconditionFailure("The audio recorder could not be created with \(recordingURL!) and format \(format)") + } + } + + func stopRecording() { + audioRecorder?.stop() + updateViews() + cancelTimer() + } + + @IBAction func saveRecording(_ sender: Any) { + self.postController.addAudioComment(with: recordingURL!, to: &self.post) + self.delegate?.reloadData() + } + + // MARK: - Actions + + @IBAction func togglePlayback(_ sender: Any) { + if isPlaying { + pause() + } else { + play() + } + } + + @IBAction func updateCurrentTime(_ sender: UISlider) { + if isPlaying{ + pause() + } + + audioPlayer?.currentTime = TimeInterval(sender.value) + updateViews() + } + + @IBAction func toggleRecording(_ sender: Any) { + if isRecording { + stopRecording() + } else { + requestPermissionOrStartRecording() + } + } +} + +extension AudioCommentViewController: AVAudioPlayerDelegate { + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + updateViews() + cancelTimer() + } + + func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + if let error = error { + print("Audio Player Error: \(error)") + } + } +} + + +extension AudioCommentViewController: AVAudioRecorderDelegate { + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if let recordingURL = recordingURL { + audioPlayer = try? AVAudioPlayer(contentsOf: recordingURL) + } + cancelTimer() + } + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + if let error = error{ + print("Recoder Player Error: \(error)") + } + } +} diff --git a/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift new file mode 100644 index 00000000..d742ebd1 --- /dev/null +++ b/LambdaTimeline/View Controllers/ImagePostDetailTableViewController.swift @@ -0,0 +1,114 @@ +// +// ImagePostDetailTableViewController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/14/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class ImagePostDetailTableViewController: UITableViewController { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var authorLabel: UILabel! + @IBOutlet weak var imageViewAspectRatioConstraint: NSLayoutConstraint! + @IBOutlet weak var commentButton: UIBarButtonItem! + + var post: Post! + var postController: PostController! + var imageData: Data? + + override func viewDidLoad() { + super.viewDidLoad() + updateViews() + } + + func updateViews() { + + guard case MediaType.image(let image) = post.mediaType else { return } + + title = post?.title + + imageView.image = image + + titleLabel.text = post.title + authorLabel.text = post.author + } + + // MARK: - Table view data source + + @IBAction func createComment(_ sender: Any) { + + let alert = UIAlertController(title: "New comment", message: "What type of comment would you like to create?", preferredStyle: .actionSheet) + + let textPostAction = UIAlertAction(title: "Text", style: .default) { (_) in + let alert = UIAlertController(title: "add a comment", message: "Write your comment below", preferredStyle: .alert) + + var commentTextField: UITextField? + + alert.addTextField { (textField) in + textField.placeholder = "Comment:" + commentTextField = textField + } + let addCommentAction = UIAlertAction(title: "Add Comment", style: .default) { (_) in + guard let commentText = commentTextField?.text else { return } + + self.postController.addComment(with: commentText, to: &self.post!) + + DispatchQueue.main.async { + self.tableView.reloadData() + + } + } + + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alert.addAction(addCommentAction) + alert.addAction(cancelAction) + + self.present(alert, animated: true, completion: nil) + } + + let voicePostAction = UIAlertAction(title: "Voice", style: .default) { _ in + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: "AudioCommentController") as! AudioCommentViewController + viewController.post = self.post + viewController.delegate = self + self.navigationController?.pushViewController(viewController, animated: true) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alert.addAction(textPostAction) + alert.addAction(cancelAction) + alert.addAction(voicePostAction) + + self.present(alert, animated: true, completion: nil) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return (post?.comments.count ?? 0) - 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "CommentCell", for: indexPath) + + let comment = post?.comments[indexPath.row + 1] + + cell.textLabel?.text = comment?.text + cell.detailTextLabel?.text = comment?.author + + return cell + } +} + +extension ImagePostDetailTableViewController: VoiceCommentDelegate { + func reloadData() { + tableView.reloadData() + } + + +} diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift new file mode 100644 index 00000000..f5f9823a --- /dev/null +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -0,0 +1,119 @@ +// +// ImagePostViewController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/12/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit +import Photos + +class ImagePostViewController: ShiftableViewController { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleTextField: UITextField! + @IBOutlet weak var chooseImageButton: UIButton! + @IBOutlet weak var imageHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var postButton: UIBarButtonItem! + + var postController: PostController! + var post: Post? + var imageData: Data? + + override func viewDidLoad() { + super.viewDidLoad() + + setImageViewHeight(with: 1.0) + } + + private func presentImagePickerController() { + + guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { + presentInformationalAlertController(title: "Error", message: "The photo library is unavailable") + return + } + + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.sourceType = .photoLibrary + + present(imagePicker, animated: true, completion: nil) + } + + @IBAction func createPost(_ sender: Any) { + + view.endEditing(true) + + guard let image = imageView.image, + let title = titleTextField.text, title != "" else { + presentInformationalAlertController(title: "Uh-oh", message: "Make sure that you add a photo and a caption before posting.") + return + } + + postController.createImagePost(with: title, image: image, ratio: image.ratio, audioURL: nil) + + navigationController?.popViewController(animated: true) + } + + @IBAction func chooseImage(_ sender: Any) { + + let authorizationStatus = PHPhotoLibrary.authorizationStatus() + + switch authorizationStatus { + case .authorized: + presentImagePickerController() + case .notDetermined: + + PHPhotoLibrary.requestAuthorization { (status) in + + guard status == .authorized else { + NSLog("User did not authorize access to the photo library") + self.presentInformationalAlertController(title: "Error", message: "In order to access the photo library, you must allow this application access to it.") + return + } + + self.presentImagePickerController() + } + + case .denied: + self.presentInformationalAlertController(title: "Error", message: "In order to access the photo library, you must allow this application access to it.") + case .restricted: + self.presentInformationalAlertController(title: "Error", message: "Unable to access the photo library. Your device's restrictions do not allow access.") + default: + break + } + presentImagePickerController() + } + + @IBAction func addFilter(_ sender: Any) { + + } + + func setImageViewHeight(with aspectRatio: CGFloat) { + + imageHeightConstraint.constant = imageView.frame.size.width * aspectRatio + + view.layoutSubviews() + } +} + +extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + chooseImageButton.setTitle("", for: []) + + picker.dismiss(animated: true, completion: nil) + + guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return } + + imageView.image = image + + setImageViewHeight(with: image.ratio) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true, completion: nil) + } +} diff --git a/LambdaTimeline/View Controllers/PostsCollectionViewController.swift b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift new file mode 100644 index 00000000..b9ed6b54 --- /dev/null +++ b/LambdaTimeline/View Controllers/PostsCollectionViewController.swift @@ -0,0 +1,100 @@ +// +// PostsCollectionViewController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class PostsCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { + + var postController: PostController! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + collectionView.reloadData() + } + + @IBAction func addPost(_ sender: Any) { + + let alert = UIAlertController(title: "New Post", message: "Which kind of post do you want to create?", preferredStyle: .actionSheet) + + let imagePostAction = UIAlertAction(title: "Image", style: .default) { (_) in + self.performSegue(withIdentifier: "AddImagePost", sender: nil) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alert.addAction(imagePostAction) + alert.addAction(cancelAction) + + self.present(alert, animated: true, completion: nil) + } + + // MARK: UICollectionViewDataSource + + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return postController?.posts.count ?? 0 + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + let post = postController.posts[indexPath.row] + + switch post.mediaType { + + case .image: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImagePostCell", for: indexPath) as? ImagePostCollectionViewCell else { return UICollectionViewCell() } + + cell.post = post + + return cell + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + var size = CGSize(width: view.frame.width, height: view.frame.width) + + let post = postController.posts[indexPath.row] + + guard let ratio = post.ratio else { return size } + + size.height = size.width * ratio + + return size + } + + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let cell = collectionView.cellForItem(at: indexPath) + + if let cell = cell as? ImagePostCollectionViewCell, + cell.imageView.image != nil { + self.performSegue(withIdentifier: "ViewImagePost", sender: nil) + } + } + + + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "AddImagePost" { + let destinationVC = segue.destination as? ImagePostViewController + destinationVC?.postController = postController + + } else if segue.identifier == "ViewImagePost" { + + let destinationVC = segue.destination as? ImagePostDetailTableViewController + + guard let indexPath = collectionView.indexPathsForSelectedItems?.first else { return } + + destinationVC?.postController = postController + destinationVC?.post = postController.posts[indexPath.row] + } + } +} diff --git a/LambdaTimeline/View Controllers/SignInViewController.swift b/LambdaTimeline/View Controllers/SignInViewController.swift new file mode 100644 index 00000000..c81bb748 --- /dev/null +++ b/LambdaTimeline/View Controllers/SignInViewController.swift @@ -0,0 +1,44 @@ +// +// SignInViewController.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/11/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class SignInViewController: UIViewController { + + @IBOutlet weak var nameTextField: UITextField! + + let postController = PostController() + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + segueIfUsernameExists() + } + + @IBAction func getStarted(_ sender: Any) { + UserDefaults.standard.set(nameTextField.text, forKey: "username") + segueIfUsernameExists() + } + + func segueIfUsernameExists() { + if UserDefaults.standard.string(forKey: "username") != nil { + performSegue(withIdentifier: "ModalPostsVC", sender: nil) + } + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "ModalPostsVC" { + guard let postsVC = (segue.destination as? UINavigationController)?.topViewController as? PostsCollectionViewController else { return } + + postsVC.postController = postController + } + } +} diff --git a/LambdaTimeline/Views/ImagePostCollectionViewCell.swift b/LambdaTimeline/Views/ImagePostCollectionViewCell.swift new file mode 100644 index 00000000..f83f73ed --- /dev/null +++ b/LambdaTimeline/Views/ImagePostCollectionViewCell.swift @@ -0,0 +1,55 @@ +// +// ImagePostCollectionViewCell.swift +// LambdaTimeline +// +// Created by Spencer Curtis on 10/12/18. +// Copyright © 2018 Lambda School. All rights reserved. +// + +import UIKit + +class ImagePostCollectionViewCell: UICollectionViewCell { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var authorLabel: UILabel! + @IBOutlet weak var labelBackgroundView: UIView! + + var post: Post? { + didSet { + updateViews() + } + } + + override func layoutSubviews() { + super.layoutSubviews() + setupLabelBackgroundView() + } + + override func prepareForReuse() { + super.prepareForReuse() + + imageView.image = nil + titleLabel.text = "" + authorLabel.text = "" + } + + func updateViews() { + guard let post = post, + case MediaType.image(let image) = post.mediaType else { return } + + titleLabel.text = post.title + authorLabel.text = post.author + imageView.image = image + } + + func setupLabelBackgroundView() { + labelBackgroundView.layer.cornerRadius = 8 + labelBackgroundView.clipsToBounds = true + } + + func setImage(_ image: UIImage?) { + imageView.image = image + } +} + From b446308061929d2ec4458de53cf3c099fa97dc0c Mon Sep 17 00:00:00 2001 From: Robert Vance Date: Fri, 30 Oct 2020 21:55:33 -0600 Subject: [PATCH 10/12] Added Privacy usage to plist --- LambdaTimeline/Resources/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist index dab10c0a..2f358730 100644 --- a/LambdaTimeline/Resources/Info.plist +++ b/LambdaTimeline/Resources/Info.plist @@ -12,6 +12,8 @@ 6.0 CFBundleName $(PRODUCT_NAME) + NSMotionUsageDescription + $(PRODUCT_NAME) uses the microphone to record audio CFBundlePackageType APPL CFBundleShortVersionString From abc03a43eb3de186e1ddf53d05c97eda539c954f Mon Sep 17 00:00:00 2001 From: Robert Vance Date: Thu, 5 Nov 2020 21:14:05 -0700 Subject: [PATCH 11/12] Most of Mapkit completed --- LambdaTimeline.xcodeproj/project.pbxproj | 8 ++ .../Model Controllers/PostController.swift | 5 +- LambdaTimeline/Models/CoordinateRegion.swift | 45 ++++++++++ LambdaTimeline/Models/Post.swift | 21 ++++- LambdaTimeline/Resources/Info.plist | 2 + LambdaTimeline/Storyboards/Main.storyboard | 83 +++++++++++++++++-- .../ImagePostViewController.swift | 31 ++++++- .../PostLocationViewController.swift | 41 +++++++++ 8 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 LambdaTimeline/Models/CoordinateRegion.swift create mode 100644 LambdaTimeline/View Controllers/PostLocationViewController.swift diff --git a/LambdaTimeline.xcodeproj/project.pbxproj b/LambdaTimeline.xcodeproj/project.pbxproj index 18c2b005..c4f6b24c 100644 --- a/LambdaTimeline.xcodeproj/project.pbxproj +++ b/LambdaTimeline.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */; }; 46D571F82173FC2700E7FF73 /* ImagePostDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */; }; E4EC7FDB254D0E9200499DC0 /* AudioVisualizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */; }; + E4EC7FE82554F1D800499DC0 /* CoordinateRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EC7FE72554F1D800499DC0 /* CoordinateRegion.swift */; }; + E4EC7FEB2555023100499DC0 /* PostLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EC7FEA2555023100499DC0 /* PostLocationViewController.swift */; }; E4F93081254D00870093AFF1 /* AudioCommentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */; }; /* End PBXBuildFile section */ @@ -45,6 +47,8 @@ 46D571F42173CF3E00E7FF73 /* UIImage+Ratio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Ratio.swift"; sourceTree = ""; }; 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePostDetailTableViewController.swift; sourceTree = ""; }; E4EC7FDA254D0E9200499DC0 /* AudioVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVisualizer.swift; sourceTree = ""; }; + E4EC7FE72554F1D800499DC0 /* CoordinateRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinateRegion.swift; sourceTree = ""; }; + E4EC7FEA2555023100499DC0 /* PostLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostLocationViewController.swift; sourceTree = ""; }; E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioCommentViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -94,6 +98,7 @@ children = ( 4646378F216FFD1B00E7FF73 /* Post.swift */, 46463791216FFDD900E7FF73 /* Comment.swift */, + E4EC7FE72554F1D800499DC0 /* CoordinateRegion.swift */, ); path = Models; sourceTree = ""; @@ -133,6 +138,7 @@ 46CFE6F421707D0000E7FF73 /* ImagePostViewController.swift */, 46D571F72173FC2700E7FF73 /* ImagePostDetailTableViewController.swift */, E4F93080254D00870093AFF1 /* AudioCommentViewController.swift */, + E4EC7FEA2555023100499DC0 /* PostLocationViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -238,6 +244,7 @@ files = ( E4EC7FDB254D0E9200499DC0 /* AudioVisualizer.swift in Sources */, 46A0366D2170158900E7FF73 /* SignInViewController.swift in Sources */, + E4EC7FEB2555023100499DC0 /* PostLocationViewController.swift in Sources */, 46CFE6F92170862C00E7FF73 /* ShiftableViewController.swift in Sources */, 4646377C216FDE4B00E7FF73 /* AppDelegate.swift in Sources */, 46CFE6F521707D0000E7FF73 /* ImagePostViewController.swift in Sources */, @@ -249,6 +256,7 @@ 46D571F52173CF3E00E7FF73 /* UIImage+Ratio.swift in Sources */, 46463792216FFDD900E7FF73 /* Comment.swift in Sources */, 46CFE6F721707FA600E7FF73 /* UIViewController+InformationalAlert.swift in Sources */, + E4EC7FE82554F1D800499DC0 /* CoordinateRegion.swift in Sources */, E4F93081254D00870093AFF1 /* AudioCommentViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/LambdaTimeline/Model Controllers/PostController.swift b/LambdaTimeline/Model Controllers/PostController.swift index 492b07ab..98871e1f 100644 --- a/LambdaTimeline/Model Controllers/PostController.swift +++ b/LambdaTimeline/Model Controllers/PostController.swift @@ -7,6 +7,7 @@ // import UIKit +import MapKit class PostController { @@ -17,11 +18,11 @@ class PostController { UserDefaults.standard.string(forKey: "username") } - func createImagePost(with title: String, image: UIImage, ratio: CGFloat?, audioURL: URL?) { + func createImagePost(with title: String, image: UIImage, ratio: CGFloat?, audioURL: URL?, location: CLLocationCoordinate2D) { guard let currentUser = currentUser else { return } - let post = Post(title: title, mediaType: .image(image), ratio: ratio, author: currentUser, audioURL: audioURL) + let post = Post(title: title, mediaType: .image(image), ratio: ratio, author: currentUser, audioURL: audioURL, location: location) posts.append(post) } diff --git a/LambdaTimeline/Models/CoordinateRegion.swift b/LambdaTimeline/Models/CoordinateRegion.swift new file mode 100644 index 00000000..efa30edc --- /dev/null +++ b/LambdaTimeline/Models/CoordinateRegion.swift @@ -0,0 +1,45 @@ +// +// CoordinateRegion.swift +// LambdaTimeline +// +// Created by Rob Vance on 11/5/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import Foundation +import CoreLocation +import MapKit + +struct CoordinateRegion { + + init(origin: (longitude: Double, latitude: Double), size: (width: Double, height: Double)) { + self.origin = (longitude: min(max(origin.longitude, -180), 180), latitude: min(max(origin.latitude, -90), 90)) + let farPoint = (longitude: self.origin.longitude + size.width, latitude: self.origin.latitude + size.height) + self.farPoint = (longitude: min(max(farPoint.longitude, -180), 180), latitude: min(max(farPoint.latitude, -90), 90)) + } + + init(originLong: Double, originLat: Double, width: Double, height: Double) { + self.init(origin: (longitude: originLong, latitude: originLat), size: (width: width, height: height)) + } + + init(originCoordinate: CLLocationCoordinate2D, width: Double, height: Double) { + let origin = (longitude: originCoordinate.longitude, latitude: originCoordinate.latitude) + self.init(origin: origin, size: (width: width, height: height)) + } + + init(mapRect: MKMapRect) { + let region = MKCoordinateRegion(mapRect) + let origin = (longitude: region.center.longitude - region.span.longitudeDelta/2.0, + latitude: region.center.latitude - region.span.latitudeDelta/2.0) + let size = (width: region.span.longitudeDelta, height: region.span.latitudeDelta) + self.init(origin: origin, size: size) + } + + // MARK: Properties + + var origin: (longitude: Double, latitude: Double) + var farPoint: (longitude: Double, latitude: Double) + var size: (width: Double, height: Double) { + return (width: farPoint.longitude - origin.longitude, height: farPoint.latitude - origin.latitude) + } +} diff --git a/LambdaTimeline/Models/Post.swift b/LambdaTimeline/Models/Post.swift index f6b070e8..f1082915 100644 --- a/LambdaTimeline/Models/Post.swift +++ b/LambdaTimeline/Models/Post.swift @@ -7,12 +7,17 @@ // import UIKit +import MapKit enum MediaType { case image(UIImage) } -class Post: Equatable { +class Post: NSObject { + + struct Locations { + static let currentLocation = CLLocationCoordinate2D(latitude: 32.8844 , longitude: 117.2390) + } let mediaType: MediaType let author: String @@ -20,6 +25,7 @@ class Post: Equatable { var comments: [Comment] var ratio: CGFloat? var id: String? + let location: CLLocationCoordinate2D var title: String? { comments.first?.text @@ -29,16 +35,27 @@ class Post: Equatable { comments.first?.audioURL } - init(title: String, mediaType: MediaType, ratio: CGFloat?, author: String, timestamp: Date = Date(), audioURL: URL?) { + init(title: String, mediaType: MediaType, ratio: CGFloat?, author: String, timestamp: Date = Date(), audioURL: URL?, location: CLLocationCoordinate2D? = nil) { self.mediaType = mediaType self.ratio = ratio self.author = author self.comments = [Comment(text: title, author: author, audioURL: audioURL)] self.timestamp = timestamp self.id = UUID().uuidString + self.location = location ?? Locations.currentLocation } static func ==(lhs: Post, rhs: Post) -> Bool { return lhs.id == rhs.id } } + +extension Post: MKAnnotation { + var coordinate: CLLocationCoordinate2D { + CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude) + } + var postTitle: String? { title } + var subtitle: String? { author } + + +} diff --git a/LambdaTimeline/Resources/Info.plist b/LambdaTimeline/Resources/Info.plist index 2f358730..050d824e 100644 --- a/LambdaTimeline/Resources/Info.plist +++ b/LambdaTimeline/Resources/Info.plist @@ -14,6 +14,8 @@ $(PRODUCT_NAME) NSMotionUsageDescription $(PRODUCT_NAME) uses the microphone to record audio + NSLocationWhenInUseUsageDescription + $(PRODUCT_NAME) uses your GPS to save your location. CFBundlePackageType APPL CFBundleShortVersionString diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index d86b272b..1980769b 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -102,7 +102,7 @@ - + @@ -113,13 +113,19 @@ - + - + + + + + + + - + @@ -146,11 +152,17 @@ - + + + + + + + @@ -184,6 +196,7 @@ + @@ -209,7 +222,39 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -262,7 +307,7 @@ - + @@ -368,7 +413,7 @@ - + @@ -455,7 +500,24 @@ - + + + + + + + + + + + + + + + + + + @@ -479,5 +541,8 @@ + + + diff --git a/LambdaTimeline/View Controllers/ImagePostViewController.swift b/LambdaTimeline/View Controllers/ImagePostViewController.swift index f5f9823a..30c4dce3 100644 --- a/LambdaTimeline/View Controllers/ImagePostViewController.swift +++ b/LambdaTimeline/View Controllers/ImagePostViewController.swift @@ -8,6 +8,7 @@ import UIKit import Photos +import MapKit class ImagePostViewController: ShiftableViewController { @@ -16,17 +17,31 @@ class ImagePostViewController: ShiftableViewController { @IBOutlet weak var chooseImageButton: UIButton! @IBOutlet weak var imageHeightConstraint: NSLayoutConstraint! @IBOutlet weak var postButton: UIBarButtonItem! + @IBOutlet weak var locationSwitch: UISwitch! var postController: PostController! var post: Post? var imageData: Data? + let locationManager = CLLocationManager() override func viewDidLoad() { super.viewDidLoad() - + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() setImageViewHeight(with: 1.0) } + @IBAction func locationSwitchOn(_ sender: UISwitch) { + if sender.isOn { + if CLLocationManager.locationServicesEnabled() { + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.startUpdatingLocation() + } + } + } + + + private func presentImagePickerController() { guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { @@ -47,11 +62,13 @@ class ImagePostViewController: ShiftableViewController { guard let image = imageView.image, let title = titleTextField.text, title != "" else { - presentInformationalAlertController(title: "Uh-oh", message: "Make sure that you add a photo and a caption before posting.") + presentInformationalAlertController(title: "Oops", message: "Make sure that you add a photo and a caption before posting.") return } + + let currentLocation = locationManager.location?.coordinate - postController.createImagePost(with: title, image: image, ratio: image.ratio, audioURL: nil) + postController.createImagePost(with: title, image: image, ratio: image.ratio, audioURL: nil, location: currentLocation!) navigationController?.popViewController(animated: true) } @@ -117,3 +134,11 @@ extension ImagePostViewController: UIImagePickerControllerDelegate, UINavigation picker.dismiss(animated: true, completion: nil) } } + +extension ImagePostViewController: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let locationValue: CLLocationCoordinate2D = manager.location?.coordinate else { return } + print("locations - \(locationValue.latitude) \(locationValue.longitude)") + } + +} diff --git a/LambdaTimeline/View Controllers/PostLocationViewController.swift b/LambdaTimeline/View Controllers/PostLocationViewController.swift new file mode 100644 index 00000000..3f9a79a5 --- /dev/null +++ b/LambdaTimeline/View Controllers/PostLocationViewController.swift @@ -0,0 +1,41 @@ +// +// PostLocationViewController.swift +// LambdaTimeline +// +// Created by Rob Vance on 11/5/20. +// Copyright © 2020 Lambda School. All rights reserved. +// + +import UIKit +import MapKit + +class PostLocationViewController: UIViewController, MKMapViewDelegate { + + @IBOutlet weak var mapView: MKMapView! + + var location: CLLocationCoordinate2D? + var postTitle: String? + var postAuthor: String? + + override func viewDidLoad() { + super.viewDidLoad() + mapView.mapType = .standard + mapView.register(MKAnnotation.self, forAnnotationViewWithReuseIdentifier: .reuseIdentifier) + mapView.delegate = self + + // Do any additional setup after loading the view. + } + func setPinWithMKPointAnnotation(location: CLLocationCoordinate2D) { + let annotation = MKPointAnnotation() + annotation.coordinate = location + annotation.title = postTitle + annotation.subtitle = postAuthor + let coordinateRegion = MKCoordinateRegion(center: annotation.coordinate, latitudinalMeters: 150, longitudinalMeters: 150) + mapView.setRegion(coordinateRegion, animated: true) + mapView.addAnnotation(annotation) + } +} + +extension String { + static let reuseIdentifier = "PostLocationView" +} From c5cfcdc524f0962855fe29524affec0e18efd80c Mon Sep 17 00:00:00 2001 From: Robert Vance Date: Thu, 5 Nov 2020 21:35:27 -0700 Subject: [PATCH 12/12] Almost Done --- LambdaTimeline/Storyboards/Main.storyboard | 34 +++++++++------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/LambdaTimeline/Storyboards/Main.storyboard b/LambdaTimeline/Storyboards/Main.storyboard index 1980769b..fc548875 100644 --- a/LambdaTimeline/Storyboards/Main.storyboard +++ b/LambdaTimeline/Storyboards/Main.storyboard @@ -205,11 +205,11 @@ - + - + @@ -224,7 +224,7 @@ - + @@ -233,7 +233,7 @@ - + @@ -248,13 +248,16 @@ + + + - + @@ -508,11 +511,14 @@ + + + @@ -520,6 +526,9 @@ + + + @@ -529,20 +538,5 @@ - - - - - - - - - - - - - - -