Timeline is a simple photo sharing service. Students will bring in many concepts that they have learned, and add complex data modeling, Image Picker, Collection Views, NSURLSession, Firebase, and protocol-oriented programming,
This is a Capstone Level project spanning many class days and concepts. Most concepts will be covered during class, others are introduced during the project. Not every instruction will outline each line of code to write, but lead the student to the solution.
Students who complete this project independently are able to:
- follow a project planning framework to build a development plan
- follow a project planning framework to prioritize and manage project progress
- implement a layered tab bar based view hierarchy
- implement a related data model architecture
- add staged data to a model object controller
- use an enum to create a customizable table view
- build a reusable log in/sign-up view controller
- implement a collection view based master-detail interface
- implement a search controller
- implement a complex UITableView with multiple cell types and sections
- implement multiple custom table view cells with delegate pattern
- use an image picker to access and work with photos
- use an accessory editing view as a text field and send button
- use Firebase as a backend for storing and pulling related model objects
- implement the Firebase controller and model object controllers to work with live data
- implement a custom protocol for Firebase model objects, controllers, and live updating views
- use dispatch groups to verify task completion before returning values
- upload photos to Firebase as base64 strings
- asynchronously download photos to display
- authenticate users via e-mail
Follow the development plan included with the project to build out the basic view hierarchy, basic implementation of local model objects and model object controllers, and build staged data to lay a strong foundation for the rest of the project.
- Add a
UITableViewController
Timeline view controller, embed it in aUINavigationController
, add a + button as the rightUIBarButtonItem
, embed theUINavigationController
in aUITabBarController
, update theUITabBarItem
to describe the scene.- note: The + button will be used to add photos.
- Add a
TimelineTableViewController.swift
subclass ofUITableViewController
and assign it to the Timeline scene. - Add a
UITableViewController
Post Detail view controller, add a segue to it from a cell in the table view of the Timeline scene. - Add a
PostDetailTableViewController.swift
subclass ofUITableViewController
and assign it to the Post Detail scene. - Add a
UITableViewController
User List / Search view controller, embed it in aUINavigationController
, add a segmented control as the title view for the navigation bar with 'Friends' and 'Add Friend' segment titles, and add it to theUITabBarController
, update theUITabBarItem
to describe the scene.- note: This view will be used for search and for listing friends.
- Add a
UserSearchTableViewController.swift
subclass ofUITableViewController
and assign it to the User List/Search scene. - Add a
UIViewController
User Detail / Profile view controller, embed it in aUINavigationController
, and add it to theUITabBarController
, update theUITabBarItem
to describe the scene. Now, add a segue from a table view cell of the User Search view directly to the User Detail / Profile view controller (bypass it'sUINavigationController
). - Add a
ProfileViewController.swift
subclass ofUIViewController
and assign it to the Profile scene. - Add a
UITableViewController
Add Photo table view controller, embed inUINavigationController
, add a segue to it from the Timeline sceneUIBarButtonItem
.- note: We will use a static table view for our Add Photo view, static table views should be used sparingly, but they can be useful for a table view that will never change, such as a basic form.
- Add a
AddPhotoTableViewController.swift
subclass ofUITableViewController
and assign it to the Add Photo scene. - Add a
UIViewController
Login / Signup Choice view controller, embed it in aUINavigationController
, add a login button and a sign up button, add aPresent Modally
segue to it from theUITabBarController
. - Add a
LoginSignupChoiceViewController.swift
subclass ofUIViewController
and assign it to the Login / Signup Choice scene. - Add a
UIViewController
Login / Signup view controler, add two segues to it from the login and signup buttons on the Login / Signup Choice scene. - Add a
LoginSignupViewController.swift
subclass ofUIViewcontroller
and assign it to the Login / Signup scene.
Create a 'User' model struct that will hold a username, optional bio, and optional url.
- Create a
User.swift
file and define a newUser
struct. - Add properties for
username
,bio
,url
, and optionalidentifier
.- note: Since a
User
can exist without a bio or url,bio
andurl
are optional properties.
- note: Since a
- Add a memberwise initializer that takes parameters for each property. The parameter for
identifier
should be of type String.- Set a default parameter
nil
for thebio
andurl
properties.
- Set a default parameter
- Implement the Equatable protocol by comparing both the
username
s andidentifier
s.
Create a 'Comment' model struct that will hold a username, text, and reference to the parent Post
.
- Create a
Comment.swift
file and define a newComment
struct. - Add properties for
username
,text
,postIdentifier
, and optionalidentifier
. - Add a memberwise initializer that takes parameters for each property
- Set a default parameter
nil
for optional properties
- Set a default parameter
- Implement the Equatable protocol by comparing bolth the
username
s andidentifier
s.
Create a 'Like' model struct that will hold a username, and reference to the parent Post
.
- Create a
Like.swift
class file and define a newLike
struct - Add properties for
username
,postIdentifier
, and optionalidentifier
- Add a memberwise initializer that takes parameters for each property
- Set a default parameter
nil
for optional properties
- Set a default parameter
- Implement the Equatable protocol by comparing both the
username
s andidentifier
s.
Create a 'Post' model struct that will hold a pointer to an image, optional caption, username, array of comments, array of likes.
- Create a
Post.swift
file and define a newPost
struct. - Add properties for
imageEndPoint
,caption
,username
,comments
,likes
, and optionalidentifier
.- note: Since a
Post
can exist without a caption,caption
is an optional property.
- note: Since a
- Add a memberwise initializer that takes parameters for each property.
- Set a default parameter
nil
for optional properties, and empty arrays for thecommments
andlikes
.
- Set a default parameter
- Implement the Equatable protocol by comparing both the
username
s andidentifier
s.
The model objects will later conform to a FirebaseType protocol that will ease working with Firebase. You will add the required properties and functions at that point.
All of our calls to Firebase will be asynchronous. We will need to use completion closures for each call that reaches out to the network and returns a value.
Create a UserController
model object controller that will manage and serve User
objects to the rest of the application. The UserController
will also handle managing followers and followed accounts. As you write each function, consider how you would approach writing the implementation, consider writing comments on the steps you would take to compare to later instructions.
- Create a
UserController.swift
file and define a newUserController
class inside. - Add a
currentUser: User!
property that returns the current user as an implicitly unwrapped optional.- note: The current user's authentication details will be stored locally or set during initialization, so this can be a synchronous property.
- note: Implicitly unwrapped optionals can be treated as optionals for checking, and as regular functions, we are building an assumption that if there is no user, the login/signup screen will be presented until there is one.
- Set the default value of
currentUser
property to nil. - Add a static
sharedController
property that will help serve ourcurrentUser
consistently through the app. - Define a static function
userForIdentifier
that takes an identifier and completion closure with an optional User parameter.
One time sample included:
static func userForIdentifier(identifier: String, completion: (user: User?) -> Void) {
}
- Define a static function
fetchAllUsers
that takes a completion closure with an array of User parameter. - Define a static function
followUser
that takes a user and completion closure with a success Boolean parameter. - Define a static function
unfollowUser
that takes a user and a completion closure with a success Boolean parameter. - Define a static function
userFollowsUser
that takes a user, and a user to check against, and a completion closure with a follows Boolean parameter. - Define a static function
followedByUser
that takes a user and completion closure with an optional array of Users parameter. - Define a static function
authenticateUser
that takes an email, password, and completion closure with a success Boolean parameter and optional User parameter.- note: Will be used to authenticate against our Firebase database of users.
- Define a static function
createUser
that takes an email, username, password, optional bio, optional url, and completion closure with a success Boolean parameter and optional User parameter.- note: Will be used to create a user in Firebase.
- Define a static function
updateUser
that takes a user, username, optional bio, optional url, and completion closure with a success Boolean parameter and optional User parameter. - Define a static function
logOutCurrentUser
that takes no parameters. - Define a static function
mockUsers()
that returns an array of sample users. - Implement the
mockUsers()
function by returning an array of at least 3 initialized users - Use the
mockUsers()
function to implement staged completion closures in the rest of your static functions with completion closures. - Update the initialization of the
currentUser
to the result of the first mock user.
Create a PostController
model object controller that will manage and serve Post
objects to the rest of the application. The PostController
will also handle managing comments and likes. As you write each function, consider how you would approach writing the implementation, consider writing comments on the steps you would take to compare to later instructions.
- Create a
PostController.swift
file and define a newPostController
class inside. - Define a static function
fetchTimelineForUser
that takes a user and completion closure with an array ofPost
object parameter. - Define a static function
addPost
that takes an image, optional caption, and completion closure with a success Boolean parameter and optionalPost
parameter. - Define a static function
postFromIdentifier
that takes an identifier and completion closure with optionalPost
parameter. - Define a static function
postsForUser
that takes aUser
and completion closure with optional array ofPost
objects parameter. - Define a static function
deletePost
that takes aPost
. - Define a static function
addCommentWithTextToPost
that takes aString
,Post
, and completion closure with a success Boolean parameter and optionalPost
parameter. - Define a static function
deleteComment
that takes aComment
and completion closure with a success Boolean parameter and optionalPost
parameter. - Define a static function
addLikeToPost
that takes aPost
, and completion closure with a success Boolean parameter and optionalPost
parameter. - Define a static function
deleteLike
that takes aLike
and completion closure with a success Boolean parameter and optionalPost
parameter. - Define a static function
orderPosts
that takes an array ofPost
objects and returns a sorted array ofPost
objects. For now, return an empty array. - Define a static function
mockPosts()
function that returns an array of sample posts. - Implement the
mockPosts()
function by returning an array of at least 3 initalized posts.- note: Use a static string
-K1l4125TYvKMc7rcp5e
as the sample image identifier.
- note: Use a static string
- Use the
mockPosts()
function to implement staged completion closures in the rest of your static functions.
Create an ImageController
that will manage and serve images to the rest of the application. The ImageController
will also handle uploading and downloading photos, and add an extension on UIImage that allows converting the image to and from Base 64 encoded strings. As you write each function, consider how you would approach writing the implementation, consider writing comments on the steps you would take to compare to later instructions.
- Create a
ImageController.swift
file and define a newImageController
class inside. - Define a static function
uploadImage
that takes an image and completion closure with an identifierString
parameter.- note: We use an identifier for the image instead of a URL because we are uploading to Firebase. If we were uploading to Amazon S3 or other cloud service, we would probably return a URL instead of identifier.
- Implement a mock response by calling the completion closure with
-K1l4125TYvKMc7rcp5e
as the identifier. - Define a static function
imageForIdentifier
that takes an identifier (String) and completion closure with an optional UIImage parameter. - Implement a mock response by returning a UIImage named "MockPhoto"
- Add a sample photo to the
Assets.xcassets
folder namedMockPhoto
for you to use as staged data.
- build a reusable login/signup view controller
- implement a collection view based master-detail interface
- implement a search controller
Build the login/signup view controller, the profile view, and the user list/search controller that will be used to list friends and search users to add as friends. Do not focus too much time on polishing the views. The purpose of wiring up view controllers is to get things functional.
This is the first view that the user will see of our application. We want it to briefly describe the app and present the option for the user to Sign Up or Log In. There are many ways to lay out this view. The steps below are simply one way to do so. Experiment with stack views and autolayout to get a pleasing view. Use the Storyboard Preview feature to see your changes on various device sizes.
- Add a vertical stack view that will hold our labels and buttons, constrain it to the leading and trailing margins, 44 points from the top, and at least 225 points from the bottom.
- note: Because resizing a view based on keyboard presence is outside the scope of this project, we add a 225 point margin to the bottom to allow space for the keyboard.
- note: To keep the bottom of the
UIStackView
at least 225 points from the bottom, but allow it to be further if the content allows for it, adjust the priority of the bottom constraint to 750.
- Add a
UILabel
with the word 'Timeline' in big, bold text. - Add a
UILabel
with a short description for the app in smaller text. - Move your 'Sign Up' and 'Log In' buttons from Part 1 into the
UIStackView
and embed them in a horizontal stack view to place them side by side.
We will implement the code for this view after setting up the Signup / Login View scene.
Build a view to manage signup and login features for the application. The view will have two modes: Signup and Login. When in Signup mode, we will display all fields required to sign up a new user. When in Login mode, we will programmatically remove unnecessary fields.
- Add a vertical stack view that will hold our labels and buttons, constrain it to the leading and trailing margins, 44 points from the top, and at least 225 points from the bottom.
- note: Because resizing a view based on keyboard presence is outside the scope of this project, we add a 225 point margin to the bottom to allow space for the keyboard.
- note: To keep the bottom of the
UIStackView
at least 225 points from the bottom, but allow it to be further if the content allows for it, adjust the priority of the bottom constraint to 750.
- Add a
UITextField
with placeholder text 'Username*'. - Add a
UITextField
with placeholder text 'Email*'. - Add a
UITextField
with placeholder 'Password*'.- note: Password fields should be set to 'Secure Text Entry' so that text is obscured.
- Add a
UITextField
with placeholder 'Bio'. - Add a
UITextField
with placeholder 'Website URL'. - Add a
UIButton
with title 'Action'.- note: The title of the button will be updated to 'Sign Up' or 'Log In' based on the mode.
You will create outlets to all required elements for the view. You will implement an enum ViewMode
with .Signup
and .Login
cases. You will respond to the ViewMode
to determine what fields to display, the title for the action button, text input validation, and action implementation. You will add a fieldsAreValid
calculated property that provides a validation check on the text fields. You will respond to invalid fields or failed attempts to create or authenticate an account with an alert.
- Open the
LoginSignupViewController.swift
subclass ofUIViewController
and check that it is assigned to the associated scene inMain.storyboard
. - Add outlets for the usernameTextField, emailTextField, passwordTextField, bioTextField, urlTextField, actionButton.
- Add a
ViewMode
enum with 'Login' and 'Signup' cases. - Add a
mode
property ofViewMode
type, we will switch on this property to determine functionality, give it a default value of.Signup
.
Your view should follow the 'updateWith' pattern for updating the view elements with the details of the mode.
- Add an
updateViewBasedOnMode
function - Implement the 'updateWith' function to update all view elements to reflect the current mode.
- note: Use a switch to determine the mode. Leave all fields for .Signup, remove unnecessary fields for .Login (username, bio, url).
- note: Update the title of the 'Action' button.
- Call the
updateViewBasedOnMode()
function in theviewDidLoad()
function.
Add a calculated property that validates that there is text in the required text fields based on the mode.
- Add a calculated property named
fieldsAreValid
that returns a Bool. - Implement the calculated property get by switching the mode, and returning true only if the required text fields are not empty.
- note: Email and Password are required for logging in. Username, e-mail, and password are required for registering.
- Add an
actionButtonTapped
action from the Action button. - Implement the
actionButtonTapped
by checking the fields are valid, switching on the mode, calling the applicableUserController
functions, and responding to the closure parameters. Successful authentication or registration should dismiss the view controller. Unsuccessful authentication or registration should present an alert telling the user to try again.- note: Because you are presenting multiple alert views with potentially slightly different wording, consider creating a
presentValidationAlertWithTitle(title: String, message: String)
and implementing it to create and present the alert
- note: Because you are presenting multiple alert views with potentially slightly different wording, consider creating a
Build a check on the UserController.currentUser()
to present the Login / Signup Picker scene if there is no user logged in.
- In the
TimelineTableViewController.swift
file, we will build the check since this will be the first view of the app. - Override
viewWillAppear()
. - Implement the function to check if
UserController.currentUser()
is nil, if it is, perform the modal segue from theUITabBarController
to the Login / Signup Picker scene, otherwise, load the timeline for the current user (set a property of an array of Posts).- note: Consider adding a check to see if the view already has posts, if so, skip reloading the view.
- Test the sequence by returning
nil
from theUserController.currentUser()
function.
- Open the
LoginSignupChoiceViewController.swift
file and check that it is assigned to the associated scene inMain.storyboard
. - Add a
prepareForSegue()
function, use the segue identifier to determine what mode to set on the destination view controller.- note: You may need to add segue identifiers in
Main.storyboard
.
- note: You may need to add segue identifiers in
- Test your different modes to verify they work as expected, that the view is presented, and that the view is dismissed when the user successfully logs in or registers.
The User Search Table View will be used for any list of multiple users, and provide search functionality for that list. The default view will have the option to view current friends, or all users of the app, and search between both. Each user cell should segue to the profile view for that user.
- Open the
UserSearchTableViewController.swift
subclass ofUITableViewController
and check that it is assigned to the associated scene inMain.storyboard
. - Add a
usersDataSource
property as an empty array of Users.- note: This array will hold the users that should be displayed in the table view. Only friends when displaying the friends list, all users when adding a friend.
- Add a an outlet
modeSegmentedControl
for the segmented control. - Add a
ViewMode
Int type enum with 'Friends' and 'All' cases. - Add a calculated
mode
property ofViewMode
type, return a ViewMode initialized with a rawValue from the selected segment index onmodeSegmentedControl
.
Add functionality to the ViewMode that you can use to fetch the correct set of User
objects. We will use this in our updateViewForMode
to set the usersDataSource
array with either friends, or all users.
- Add a function to the ViewMode enum called
users
that takes a completion closure with an optional array of users as a parameter.- note: All calls to the network should be asynchronous, so we will need to call this method and then set the
usersDataSource
after the results are returned via completion closure.
- note: All calls to the network should be asynchronous, so we will need to call this method and then set the
- Implement the function by switching on the enum, performing the appropriate
UserController
function, and running the completion block with the returned users.
Your view should follow the 'updateWith' pattern for updating the view elements with the details of the mode.
- Add an
updateViewBasedOnMode()
function. - Implement the 'updateWith' function to call the
mode.users()
function, implement the completion block to set theusersDataSource
to the results, handle the case in which there are no results, and then reload the tableview with the updated array. - Call the
updateViewForMode()
function in theviewDidLoad()
function. - Add an IBAction
selectedIndexChanged
for the segmented control that updates the view with the newly updated mode.
Now that you have an array of User
objects you can implement the required UITableViewDataSource
functions to display usernames.
- Implement the
numberOfRowsInSection
by returning the count of displayed users. - Implement the
cellForRowAtIndexPath
by capturing theUser
and setting the textlabel of the cell to the username.
Check the view for functionality, you should get different results, if you return different results from your followedByUser()
function.
Search controllers typically have two views: a list view, and a search result view that displays the filtered results. You must create a 'search results view' that is overlayed on top of your list view when the search bar is actively editing, and then your main list view handles a SearchResultsUpdating
protocol function that updates the results view.
Understanding Search Controllers requires you to understand that the main view controller can (and must) implement methods that handle what is being displayed on another view controller. The results controller must also implement a way to communicate back to the main list view controller to notify it of events. This two way relationship with communication happening in both directions.
Add the Search Results Controller
- Add a
UITableViewController
Search Results scene toMain.storyboard
and assign the scene a storyboard identifier. - Create a
UserSearchResultsTableViewController.swift
subclass ofUITableViewController
and assign it to the newly created scene inMain.storyboard
. - Add a
usersResultsDataSource
property as an empty array of Users.- note: This array will hold the users that should be displayed as search results, this array will be updated by our main list view controller when the user updates the search field.
- Implement the
numberOfRowsInSection
by returning the count of displayed users. - Implement the
cellForRowAtIndexPath
by capturing theUser
and setting the textlabel of the cell to the username.
Add the UISearchController
to the UserSearchTableViewController
- Add a function
setUpSearchController()
that will initialize and assign settings to theUISearchController
. - Add an implicily unwrapped property of
UISearchController
. - Implement the function by capturing an instance of the Search Results scene as a
resultsController
inMain.storyboard
using the scene's storyboard identifier, initializing theUISearchController
with the theresultsController
, setting thesearchResultsUpdater
to self, sizing the search bar, and setting the search controller's search bar as the header of thetableView
. - Call the
setUpSearchController()
function in theviewDidLoad()
. - Adopt the
UISearchResultsUpdating
protocol and add the requiredupdateSearchResultsForSearchController(searchController: UISearchController)
function. - Implement the
UISearchResultsUpdating
function by capturing the text in the search bar and assigning the search controller'susersDataSource
to a filtered array ofUser
objects where the username contains the search term, then reload the result view'stableView
.- note: You may want to convert the search term and usernames to lowercase using
.lowercaseString
to avoid case sensitive search results.
- note: You may want to convert the search term and usernames to lowercase using
- Open the
ProfileViewController.swift
file and add an optionalUser
property that will be set in the inboundprepareForSegue
function. - Open the
UserSearchTableViewController.swift
and add/uncomment aprepareForSegue
function. - Implement the function by casting the sender to a UITableViewCell, capturing the indexPath of the cell, capturing the selected user, capturing and casting the destination view controller as a
ProfileViewController
, and assigning user to the destination view controller's property. - Temporarily add a
print(user)
to theviewDidLoad
function of theProfileViewController
to see that the user has correctly been assigned.
Run the application and identify the issue you will solve next by navigating to the Profile View via the User List. Try again using the Search Results. Consider why this doesn't work, and what avenues you could take to fix it.
The user should be able to select a user to view the Profile view for that user in both the regular list view and the search detail view. Implementing a segue from the Search Results view will only present the Profile View modally, not using the UINavigationController
to push the view. You will implement the segue, then use the UITableViewDelegate
function didSelectRowAtIndexPath
to manually perform the segue on the main list view.
-
Open the
UserSearchResultsTableViewController.swift
file and add theUITableViewDelegate
functiondidSelectRowAtIndexPath
. -
Implement the function by capturing the sending cell and telling it's
presentingViewController
to perform the segue manually.- example:
self.presentingViewController?.performSegueWithIdentifier("toProfileView", sender: sender)
One more step to get the segue working as expected. You need to update theprepareForSegue
function to get the correctUser
to theProfileViewController
. Try to work through the problem without looking at the solution code.
- example:
-
Update the
prepareForSegue
function inUserSearchTableViewController
to check ifself.tableView
can get anindexPath
for the sender. If it can, that means that the cell was fromself.tableView
and we can capture the user fromusersDataSource
using the index path. If it cannot get an indexPath fromself.tableView
, then the cell is from the table view that lives on thesearchResultsController
. If that is the case, capture the user from theusersResultsDataSource
on thesearchResultController
.- note: You can access the
searchResultsController
by calling(searchController.searchResultsController as! UserListSearchResultsTableViewController)
.
- note: You can access the
Build a view to display details about any user of the app. The view will also allow the currentUser
to update their profile photo, bio, or url.
- Add a Collection View to the
ProfileViewController
scene inMain.storyboard
. - Add a Collection Reusable View in the collection view.
- Assign ProfileViewController (self) as the
datasource
anddelegate
of the collection view. - Design the Collection Reusable View using a
UIStackView
to display a bio label, a homepage button, and a follow user button. - Assign a reuse identifier to the reusable view.
- Create a
ProfileHeaderCollectionResuableView.swift
subclass ofUICollectionReusableView
and assign it to the associated view inMain.storyboard
. - Add IBOulets for the bioLabel, urlButton, and followUserButton.
- Add a
updateWithUser(user: User)
function. - Implement the function to set the title and bioLabel, remove labels if the user doesn't have an associated value, sets the title of
followUserButton
based on whether the current user is following, removes thefollowUserButton
if the user is thecurrentUser
. - Define a
ProfileHeaderCollectionReusableViewDelegate
protocol with required functions foruserTappedFollowActionButton
anduserTappedURLButton
. - Add an optional delegate property.
- Add IBActions
urlButtonTapped
andfollowActionButtonTapped
. - Implement the action functions by calling the appropriate function on the delegate.
- Assign a reuse identifier for the included cell.
- Add a
UIImageView
that fills the cell. - Create a
ImageCollectionViewCell.swift
subclass ofUICollectionViewCell
. - Add an outlet
imageView
for the image view. - Add a function
updateWithImageIdentifier(identifier: String)
. - Implement the function to use the ImageController to get the image matching the identifier, assign the image to the image view in the completion closure.
- In the
ProfileViewController
, add auserPosts
property as an empty array ofPost
objects.- note: This array will hold the posts that should be displayed in the collection view.
- Add a
updateBasedOnUser
function. - Implement the function by setting the title of the view, using the
PostController
to fetch the posts for the user, assigning theuserPosts
to the results, and reloading the collection view in the completion closure.
Now that you have an array of userPosts
you can implement the required UICollectionViewDataSource
functions to display the posts, and update the Header View with the current user.
- Implement the
numberOfItemsInSection
by returning the count ofuserPosts
. - Implement the
cellForItemAtIndexPath
by dequeing aImageCollectionViewCell
, capturing thePost
at the indexPath, and calling theupdateWithImageIdentifier
function with the correct endpoint. - Implement the
viewForSupplementaryElementOfKind
by dequeing theProfileHeaderCollectionReusableView
and calling theupdateWithUser
with the Profile View'sUser
property.
Use the ProfileHeaderReusableViewDelegate
to implement functionality for the 'URL' and 'Follow' buttons
- Adopt the
ProfileHeaderReusableViewDelegate
protocol on theProfileViewController
. - Implement the
userTappedURLButton
function to initialize and present aSafariViewController
to the profile url. - Implement the
userTappedFollowActionButton
to use theUserController
to determine if the current user follows the user, appropriately follow or unfollow the user, and update the view with the updated user.- note: You have to define a static function
unfollowUser
(on UserController) that takes a user and a completion closure with a success Boolean parameter.
- note: You have to define a static function
- Update the
viewForSupplementaryElementOfKind
datasource function to set theProfileHeaderCollectionReusableView
delegate.
- When we set up the View Hierarchy in Part 1, you may have created a segue from the Profile View to the Post Detail View. If so, Delete that segue. Create/Recreate it from the collection view cell in
Main.storyboard
. - The profile view is displayed within a Navigation Controller when accessed through the User List / Search View, but MAY not when accessed as the third tab in our
UITabBarController
. If not, add aUINavigationController
, set theProfileViewController
as it's root view controller, and update theUITabBarController
relationship segue to point to theUINavigationController
. - Update the
viewDidLoad
function to checkself.user
for nil, if it is, assign the current user to the value.
Build functionality for the user to update their profile using the Login/Signup View we built earlier.
Update the Login / Signup View to support updating a User
.
- Open the
LoginSignupViewController.swift
file. - Add a
ViewMode
case 'Edit'. - Update the
fieldsAreValid
calculated property so that the.Edit
validates the presence of text in theusernameField
.- note: this is the only field we need to validate as it is the only required method. (email and password will be hidden, bio and url are optional)
- Add an optional
user
property. - Update the
actionButtonTapped
function to add a case for.Edit
that uses theUserController
to update the user with the current values and handles success or failure by dismissing the view or presenting a validation alert. - Add a
updateWithUser
function. - Implement the function to set
self.user
, set theViewMode
to.Edit
. - Update the
updateViewBasedOnMode
function for the new mode.
Update the Profile View Controller to support Editing
- Open the Profile View Controller scene in
Main.storyboard
. - Add a
UIBarButtonItem
to the navigation bar. Use system item 'Edit'. Set to disabled.- note: You may need to add a
UINavigationItem
.
- note: You may need to add a
- Add a modal presentation segue from the 'Edit' button to the Login/Signup View with
toEditProfile
identifier - Implement the
prepareForSegue
by checking the segue identifier, capturing the destination view controller, and updating with the user. - Override the
viewDidAppear
to update the user from Firebase and reload the view with that data.- note: Since our view for UserController.userForIdentifier always returns our first mock user, you may get some funky results when going to this view by clicking on a different user.
- In the
viewDidLoad
, where you check if user is nil, after setting the user property to currentUser, enable the bar button item.
Consider how you could modify these steps to be more efficient in relying on network requests.
Add functionality for the current user to log out.
- In the
ProfileHeaderCollectionReusableView
, use the follow button as a logout button if the user is equal to the currentUser. Set the title appropriately. - In the implementation of the
userTappedFollowActionButton()
delegate method, check if the user is equal to the currentUser. If so, log out the current user and send the user to the first view controller in the tab bar. If not, it should do the appropriate follow/unfollow action.
- implement a complex UITableView with multiple cell types and sections
- implement multiple custom table view cells with delegate pattern
- use an image picker to access and work with photos
- use an accessory editing view as a text field and send button
The Timeline view is the most important view of the application. This is where people will see and like the photos. Each cell will represent one post and will display the photo, user, number of comments, and number of likes. Be creative but follow sound design principles in how you display each post in the cell.
- Design the cell in
Main.storyboard
to display the image prominently, include labels for the number of likes and number of comments.- note: Consider displaying the photo as the full background of the cell, use a Stack View to place the labels.
- note: Consider the Content Mode and choose one that will work for most images, you can address this later in the polish portion.
- Add a
PostTableViewCell.swift
subclass ofUITableViewCell
and assign it as the class for the cell on our Timeline scene. - Add outlets for the
postImageView
,likesLabel
andcommentsLabel
. - Add a
updateWithPost(post: Post)
function. - Implement the function by assigning values to the labels and using the ImageController to fetch the image, assign the result to the image view in the completion closure.
- Open the
TimelineTableViewController.swift
subclass ofUITableViewController
and check that it is assigned to the associated scene inMain.storyboard
- Add a variable
posts
that will hold the posts for the Timeline. - Add a function
loadTimelineForUser
that takes aUser
as a parameter. - Implement the function by using the
PostController
to fetch timeline for the user, setting the results toself.posts
, and reloading the view when completed. - Update the
viewDidLoad()
function to callloadTimelineForUser
if there is a current user.- note: Keep the previously written code, we still need to present the Login view if there is no current user.
- Implement the
numberOfRowsInSection
by returning the count of posts. - Implement the
cellForRowAtIndexPath
by dequeing aPostTableViewCell
, capturing thePost
, and calling theupdateWithPost(post: Post)
function on the cell.
Implement 'Pull to Refresh' functionality on your TimelineTableViewController
- Select the scene in the Document Outline in
Main.storyboard
. - Enable refreshing in the Attributes Inspector under Table View Controller.
- Add an IBAction
userRefreshedTable
for the Refresh Control now visible in your Document Outline. - Implement the action by fetching an updated timeline from the
PostController
.- note: You must tell the refresh control to
endRefreshing
when the view is done loading.
- note: You must tell the refresh control to
Build a Post Detail View that displays the post. It should display the photo and the comments. The view should also allow the user to post comments or add likes to the post.
- Open the
PostDetailTableViewController.swift
subclass ofUITableViewController
and check that it is assigned to the associated scene inMain.storyboard
. - Add a Header view to the
UITableView
with a similar layout to thePostTableViewCell
.- note: Some photos may expand beyond this header view, choose the 'Clip Subviews' option in the Attribute Inspector.
- Add a subtitle prototype cell with a Username label and Comment label that will display the details of each comment.
- Add a button as a footer view titled 'Add Comment'.
- Add a
UINavigationItem
with a Bar Button Item titled 'Like'. - Add IBOutlets for the
headerImageView
,likesLabel
, andcommentsLabel
. - Add a property of
Post?
to the view controller. - Add a function
updateBasedOnPost()
. - Implement the function by updating the
likesLabel
,commentsLabel
, using theImageController
to set theheaderImageView
and reloading the table view. - Call
updateBasedOnPost()
inviewDidLoad()
. - Add an IBAction
likeTapped
from the 'Like' button. - Implement the
likeTapped
function by using thePostController
to add aLike
to the post, update the view with the updated post in the completion closure. - Add an IBAction
addCommentTapped
from the 'Add Comment' button. - Implement the
addCommentTapped
function to present aUIAlertController
with a textfield, an 'Add Comment' action, and a 'Cancel' action. - Implement the 'Add Comment' action to use the
PostController
to add a comment, update the view with the updated post in the completion closure. - Implement the
numberOfRowsInSection
by returning the count of comments onself.post
. - Implement the
cellForRowAtIndexPath
by dequeing aPostCommentTableViewCell
, capturing theComment
, and calling theupdateWithComment(comment: Comment)
function on the cell.
- Add a
prepareForSegue
function to theTimelineTableViewController.swift
file - Implement the function by capturing the
indexPath
, selectedPost
,destinationViewController
, and updating thedestinationViewController
with the selected post. - Add a
prepareForSegue
function to theProfileViewController.swift
file. - Implement he function by capturing the
indexPath
, selectedPost
,destinationViewController
, and updating thedestinationViewController
with the selected post.
Build a view for creating and submitting a post. The view should have a way to select a photo using the UIImagePickerController
, adding a caption, and submitting. You will use a static table view with a header and footer to create this form. This is not the only way to build this view, but is an appropriate use for a static UITableView
.
You will use a button as the header view to allow the user to select a photo. When the user has chosen a photo, display it using the button's background image property and set the title to an empty string.
You will use a static cell with a text field for capturing the caption for the post, and a 'Submit' button as the footer for the table view.
- Update the table view to use static cells.
- Add a button as the header for the table view titled 'Add Photo'.
- note: Update the title and font size, make the view larger so that the user can see the photo.
- Remove any additional table view cells, add a text field that fills the cell, provide context to the user with placeholder text.
- Add a 'Submit' button as the footer to the table view.
- Add a 'Cancel' button as the left bar button item.
- Add IBOutlets for the 'Add Photo' button and caption text field.
Add a property for storing the image for the post, present a UIImagePickerController
, and update the 'Add Photo' button to display the image.
- Add an optional
self.image
property to capture the selected image for the post. - Add an IBAction
addPhotoButtonTapped
to theAddPhotoTableViewController.swift
file. - Implement the function by instantiating a
UIImagePickerController
, setting it's delegate, presenting an alert to the user to choose 'Photo Library' or 'Camera', setting thesourceType
of the picker controller, and presenting it. - Adopt the
UINavigationControllerDelegate
andUIImagePickerControllerDelegate
protocols. - Implement the
UIImagePickerControllerDelegate
functiondidFinishPickingMediaWithInfo
to dismiss the picker view controller, capture the selected image into theself.image
property, and updating the background image of the photo button.
Follow the same pattern you used for the self.image
property by capturing the value when the user stops editing the caption text field.
- Add an optional
caption
property to capture the text when the user finishes updating the cell. - Adopt the
UITextFieldDelegate
protocol, set the delegate of the text field, and implement thetextFieldShouldReturn
function to set thecaption
property andresignFirstResponder
.
- Add an IBAction
submitButtonTapped
from the 'Submit' button. - Implement the function by checking for a value in
self.image
, if there is an image, use thePostController
to add a post with the image and caption, if there isn't an image, present an alert to the user asking them to check and try again. - Handle the
PostController
unsuccessfully uploading the image by presenting an alert to the user asking them to try again.
- Add an IBAction
cancelButtonTapped
from the 'Cancel' button. - Implement the function by dismissing the view controller.
- fix the content mode of the 'Add Photo' button to use .ScaleAspectFill
- add 'double tap to like' functionality to the cell
- make the post view a live view by observing the post
- use Firebase as a backend for storing and pulling model objects
- implement the Firebase controller and model object controllers to work with live data
- implement a custom protocol for Firebase model objects, controllers, and live updating views
It is time to implement actual funtionality for our controller objects. You will import the Firebase library into the application, create a reusable FirebaseController
helper class that will perform basic Firebase interactions for authentication and fetching and pushing data, and get the model objects ready to save to Firebase by writing and implementing a FirebaseType
protocol.
Install the Firebase iOS SDK by manually including the Firebase.framework
and its dependencies in the project.
- Open the iOS Alternative Setup documentation.
- Follow the steps to download the framework and add dependencies to the project.
- note: As of Xcode 7.1, .dylib is now .tbd when referencing dependencies and linked frameworks.
- Create a new App in Firebase with a unique subdomain of your choice to use for the project.
Create a reusable FirebaseController
class that will provide basic fetching features. If written correctly, the only reference to your current project will be the base
property that references the URL on Firebase for your application. Everything else will be migratable and reusable in other projects you build. Add to your FirebaseController
over time with the most reused features.
- Create a new
FirebaseController.swift
class and define a newFirebaseController
class. - Import Firebase.
- Add a new class property
base
that returns aFirebase
from your URL. - Add a static function
dataAtEndpoint(endpoint: String, completion: (data:AnyObject?) -> Void)
that will fetch data from an endpoint and return it via completion closure. - Implement the function to create a new Firebase reference with the endpoint string, observe a single event, and run the completion closure when the data has returned.
- note: Check to see if the data is NSNull before running the completion. This will determine what you pass to the closure's parameter.
- Add a static
observeDataAtEndpoint(endpoint: String, completion: (data: AnyObject?) -> Void)
that will observe data from and endpoint and run the completion closure each time the data at that endpoint changes. - Implement the function to create a new Firebase reference with the endpoint string, observe an event, and run the completion closure when the data has returned.
- note: Check to see if the data is NSNull before running the completion.
Note that these functions are not necessary, but will save you two lines of code each time you want to fetch or observe data in Firebase. You can build your FirebaseController
over time to be more useful to you as you recognize patterns of what you do repeatedly in Firebase that can be abstracted to this helper class.
The FirebaseController.swift
file is the perfect place to add more Firebase specific code that will help you write the rest of the application. Write a FirebaseType
protocol that will normalize and enforce the way model objects are built to save and pull from Firebase. Before writing the protocol, consider everything that you would require a model object to have to work seamlessly with Firebase.
There are 4 or 5 required properties or functions, depending on your specific implementation, that you will want to include:
identifier
endpoint
secondaryEndpoints
(depends on implementation and architecture)jsonValue
init?(json: [String: AnyObject])
(because of the way we've structured our data in this specific app, we will useinit?(json: [String: AnyObject], identifier: String)
)
With these 5 required properties/functions, we can implement a couple of great features with default protocol implementations. You will implement two:
save()
delete()
- Add a protocol definition for
FirebaseType
at the bottom of theFirebaseController.swift
file. - Add an optional gettable and settable
identifier:[String]?
property.- note: The identifier will be used to identify the object on Firebase, and when nil, tells us that the object has not yet been saved to Firebase.
- Add a gettable
endpoint: String
property.- note: The endpoint will determine where the object will be saved on Firebase.
- Add a gettable
jsonValue: [String, AnyObject]
property.- note: A JSON representation of the object that will be saved to Firebase.
- Add a faillable
init?(json: [String: AnyObject] identifier: String)
function.- note: Any instance initialized with json will come from Firebase, and will require an identifier so we know it already exists on Firebase.
- Add a
mutating func save()
function. - Add a
func delete()
function .
Using protocol extensions in Swift, we can require functions and provide default implementations for those functions for any type that conforms to the protocol.
- Define an extension to FirebaseType at the bottom of the
FirebaseController.swift
file. - Add a mutating
save()
function. - Implement the function by checking for an identifier, if there is an identifier, instantiate a Firebase reference to the endpoint with that identifier, otherwise instantiate a Firebase reference to the endpoint with a
.childByAutoID()
, and assign the identifier to the key of that base, once you have a reference to where the object should be saved, use theupdateChildValues()
function with thejsonValue
of the object. - Add a
delete()
function. - Implement the function by instantiating a Firebase reference to the object, use the
removeValue()
function to delete it from Firebase.
Adopt the FirebaseType
protocol in each of your model objects. Use the included sample JSON to build your jsonValue
calculated properties and init?(json: [String: AnyObject], identifier: String)
initializers.
Example:
"-K28xPOXBBXdCrFx-EAY" : {
"post" : "-K25Fj8qrMAtxXG3QCSn",
"text" : "I'd love to cliff dive off that.",
"username" : "calebhicks"
}
- Add private String keys for "post", "username", and "text".
- Assign a value for a computed
endpoint
property. Look at the example:
var endpoint: String {
return "/posts/\(self.postIdentifier)/comments/"
}
Saving the jsonValue
to this endpoint will put it under the post that it belongs to on Firebase.
- Implement the
jsonValue
calculated property by returning a json dictionary with thepostIdentifier
,user
, andtext
. - Implement the failable initializer by guarding against the required properties, setting any optional properties, and assigning the identifier.
Example:
"-K28OeV3MmD0l9DbNufW" : {
"post" : "-K25Fj8qrMAtxXG3QCSn",
"username" : "calebhicks"
}
.
- Add private String keys for "post" and "username".
- Assign a value for a computed
endpoint
property that saves theLike
to the post, similar to theendpoint
forComment
. - Implement the
jsonValue
calculated property by returning a json dictionary with thepostIdentifier
andusername
. - Implement the failable initializer by guarding against the required properties, setting any optional properties, and assigning the identifier.
Example:
"-K25Fj8qrMAtxXG3QCSn" : {
"username" : "hansolo",
"imageEndpoint" : "-K25Fj8p2ArUMz3awt3T",
"comments" : {
"-K28xPOXBBXdCrFx-EAY" : {
"post" : "-K25Fj8qrMAtxXG3QCSn",
"text" : "I'd love to cliff dive off that.",
"username" : "calebhicks"
},
"-K28xzlhs8ArmgB6bcCB" : {
"post" : "-K25Fj8qrMAtxXG3QCSn",
"text" : "Who wants in?",
"username" : "calebhicks"
}
},
"likes" : {
"-K28OeV3MmD0l9DbNufW" : {
"post" : "-K25Fj8qrMAtxXG3QCSn",
"username" : "calebhicks"
},
"-K28xx1BC5pnQNXDxym6" : {
"post" : "-K25Fj8qrMAtxXG3QCSn",
"username" : "calebhicks"
}
}
}
- Add private String keys for "username", "imageEndpoint", "caption", "comments", and "likes".
- Conform to the
FirebaseType
protocol. - Assign a value 'posts' for
endpoint
.- note:
Post
objects are not saved under any other object, so it has it's own independent endpoint.
- note:
- Implement the
jsonValue
calculated property by returning a json dictionary with theusername
,imageEndpoint
,comments
,likes
, and optionally add thecaption
if it exists.- note: Map the Comments and Likes to dictionaries (ex.
CommentsKey: self.comments.map({$0.jsonValue})
).
- note: Map the Comments and Likes to dictionaries (ex.
- Implement the failable initializer by guarding against the required properties, setting any optional properties, and assigning the identifier.
- note: Map the Comment and Like dictionaries to initialized model objects, use flatMap() to filter out any nil optional initialized objects.
- note: Consider the included sample solution below, break each line down, look in the documentation to understand what each part is doing.
if let commentDictionaries = json[CommentsKey] as? [String: AnyObject] {
self.comments = commentDictionaries.flatMap({Comment(json: $0.1 as! [String : AnyObject], identifier: $0.0)})
} else {
self.comments = []
}
Example:
"17c014cb-5cc1-4884-977b-471482d9e484" : {
"bio" : "I wear fancy pants. ",
"follows" : {
"c6c2fbe1-c86c-4b47-a78b-5d991c8f19fb" : true,
"f8270303-6656-453a-a2e6-8c5eeece73b7" : true
},
"url" : "http://calebhicks.com/",
"user" : "calebhicks"
}
- Add private String keys for "username", "bio", and "url".
- Assign a value 'users' for
endpoint
. - Implement the
jsonValue
calculated property by returning a json dictionary with theusername
, optionally include thebio
andurl
, if they exist. - Implement the failable initializer by guarding against the required properties, setting any optional properties, and assigning the identifier.
The PostController
is a crucial piece to the application. Do your best to write the implementation for each function with only the description here. Sample solution code is available, but should only be used after trying your best to implement each function. Each function takes parameters and returns others, do your best to translate the inputs into the outputs.
- Implement the
addPost
function to use theImageController
to upload an image, use the closure identifier to initialize a post, save it, and call the completion closure. - Implement the
postFromIdentifer
function to use theFirebaseController
to fetch data for the post (ex."posts/\(identifier)"
), unwrap the data, initialize the post, and call the completion closure. - Implement the
postsForUser
function to create aFirebase
reference query to all posts where "username" is equal to the username passed into the function, unwrap the optional data, flatMap the dictionaries intoPost
objects, order the posts, and call the completion closure.- note: Watch out for the auto closure completion Xcode creates for Firebase observe functions, it oftentimes will choose a different syntax than works.
- note: The master dictionary will contain child dictionaries that map to Posts. Use tuple accessors to correctly grab the identifier and child dictionary to map, ask for help if you do not understand the syntax.
- Implement the
deletePost
function by deleting the post. - Implement the
addCommentWithTextToPost
to check for a postIdentifier (if none, save the post, thereby getting a postIdentifier), initialize aComment
, save the comment, fetch the updated post using the identifier, and calling the completion closure with the newly fetchedPost
. - Implement the
deleteComment
function to delete the comment, fetch the updated post using the identifier, and calling the completion closure with the newly fetchedPost
. - Implement the
addLikeToPost
to check for a postIdentifier, initialize aLike
, save the like, fetch the updated post using the identifier, and calling the completion closure with the newly fetchedPost
. - Implement the
deleteLike
function to delete the like, fetch the updated post using the identifier, and calling the completion closure with the newly fetchedPost
. - Implement the
orderPosts
function to return a sorted array using the identifier of thePost
object.- note: Firebase creates the unique identifiers by using a timestamp, so sorting by the identifier sorts by timestamp.
- note: This function is particularly useful in the
fetchTimeline
function that appendsPost
objects from different users, this function sorts them back into order by time.
- note: You should not expect to see a great difference in your app functionallity today.
- upload photos to Firebase as base64 strings
- asynchronously download photos to display
- authenticate users anonymously or via e-mail
The PostController
has one more function we need to implement, fetchTimelineForUser
. First, you will need to get all of the user the current user is following. Second, for each of those users, you'll need to fetch their posts. Third, you should fetch all of the current users posts. (If the user uploads an image, they should see it on their own timeline.) Use dispatch groups to be notfied when all of your asyncronous calls are complete.
- Implement the
fetchTimelineForUser()
by first callingfollowedByUser
. In the completion closure, create an array the will hold all the posts and a dispatch group. - Enter the dispatch group. Call
postForUser
to fetch the current users posts. Once the post have been fetched leave the dispatch group. - For each user that is being followed, enter the dispatch group, fetch their posts, then once the posts have been returned, leave the dispatch group.
- When the dispatch group notifies it has completed, order the posts and call the completion closure.
The UserController
is a crucial piece to the application. Do your best to write the implementation for each function with only the description here. Sample solution code is available, but should only be used after trying your best to implement each function. Each function takes parameters and returns others, do your best to translate the inputs into the outputs.
- Add a private
kUser
for use withNSUserDefaults
and thecurrentUser
calculated property. - Implement the
currentUser
computed property to use aget
andset
to push and pull fromNSUserDefaults
.get
should guard against theuid
on theFirebaseController.base.authData
property and a userDictionary fromNSUserDefaults
, and return a User created from the results.set
should unwrap thenewValue
, if it exists, save it toNSUserDefaults
, if it does not, remove it fromNSUserDefaults
.
var currentUser: User! {
get {
guard let uid = FirebaseController.base.authData?.uid,
let userDictionary = NSUserDefaults.standardUserDefaults().valueForKey(UserKey) as? [String: AnyObject] else {
return nil
}
return User(json: userDictionary, identifier: uid)
}
set {
if let newValue = newValue {
NSUserDefaults.standardUserDefaults().setValue(newValue.jsonValue, forKey: UserKey)
NSUserDefaults.standardUserDefaults().synchronize()
} else {
NSUserDefaults.standardUserDefaults().removeObjectForKey(UserKey)
NSUserDefaults.standardUserDefaults().synchronize()
}
}
}
- Implement the
userForIdentifier
function to fetch data at the endpoint for the user, unwrap the data, initialize theUser
, and call the completion. - Implement the
fetchAllUsers
function to fetch all data at the "users" endpoint, unwrap the optional data, flatMap the dictionaries intoUser
objects, and call the completion closure. - Implement the
followUser
function to create a Firebase reference to the endpoint for followed users ("/users/(sharedController.currentUser.identifier!)/follows/(user.identifier!)"), settrue
to that endpoint, and call the completion closure. - Implement the
unfollowUser
function to do remove the value at the followed user endpoint. - Implement the
userFollowsUser
function to check if there is a value at the followed user endpoint and call the appropriate completion closure. - Implement the
followedByUser
function to fetch identifiers for all followed users, unwrap the optional data, create a holding array for users, loop through the identifiers to call theuserForIdentifier
function, append each user, and call a completion closure.- note: This implementation may be inefficient and potentially cause issues. Consider how you could better approach this problem. Experiment with potential solutions.
- Implement the
authenticateUser
function toauthUser
on theFirebaseController.base
reference, if you successfully authenticate, fetch the user using the identifier, and set thecurrentUser
property onsharedController
to the result.- note: You will need to enable E-mail Authentication on your Firebase Dashboard for this to work.
- Implement the
createUser
function tocreateUser
on theFirebaseController.base
reference, if you succeed, initialize aUser
object using theuid
and other parameters, save the user, then authenticate the user to log the user in. - Implement the
updateUser
function to initialize a newUser
object with the same identifier and new parameters, save the user (which will overwrite the updated values on the server), fetch a new copy of the user using the identifier, set thecurrentUser
property onsharedController
, and call the completion closure. - Implement the
logoutCurrentUser
function tounAuth
on theFirebaseController.base
reference, set thecurrentUser
property on thesharedController
to nil.
In an ideal world, we would host our images to Amazon S3 for fast, cheap asset hosting. However, in the spirit of building a Minimum Viable Product and using the tools we already have, we're going to host images on Firebase. Firebase does not natively support images, but it does support strings. Images can be converted to and from string values using Base64 encoding and decoding. You will build the ImageController
and an extension on UIImage
that provides the encoding and decoding for you.
- Define a new extension for
UIImage
at the bottom of theImageController.swift
file. - Create a calculated property
base64String
that returns an optional string. - Implement the calculated property by guarding a compressed
UIImageJPEGRepresentation
copy of the image represented asNSData
.- note: Play with various compression rates, the higher the compression, the faster loading images will go.
- Return the data as a string with
.base64EncodedStringWithOptions
. - Define a failable convenience initializer that takes a base64 encoded string as a parameter.
- Implement the initializer by converting the string into
NSData
(NSData(base64EncodedString: String)
) and callingself.init(data: NSData)
with the result.
- Implement the
uploadImage
function by converting the image into a base64 encoded string, creating a Firebase child reference under the "images" endpoint, setting the encoded string as the value, and calling the completion closure with the identifier of the new child.- note: Firebase references return the last segment path of the endpoint with a
.key
parameter.
- note: Firebase references return the last segment path of the endpoint with a
- Implement the
imageForIdentifier
function by fetching the data at the "images" endpoint for the image identifier, unwrapping the Base 64 string, initializing theUIImage
, and calling the completion closure with the initialized image.
- add 'double tap to like' functionality to the cell
Please refer to CONTRIBUTING.md.
© DevMountain LLC, 2015. Unauthorized use and/or duplication of this material without express and written permission from DevMountain, LLC is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to DevMountain with appropriate and specific direction to the original content.