This challenge tasks us with building an app that lets users:
- take photos of things that interest them
- add captions to those photos
- show them in a table view
- tapping the caption should show the picture in a new view controller
The order of things
First of all I want to plan ahead what needs to be done.
- Create the basic table-view-controller that loads rows from a
captions
string array. - Add a ‘+’ button for picking the image and add an action method
- Design the detail-view-controller in Interface Builder with an
ImageView
and connect the elements. - Create properties and implement TableView Data Source and Delegate methods.
- Prepare the detail-view-controller to receive the image and configure the picker.
- Create new custom type for the Picture so that it contains both image name and caption and adjust the TableView methods
- Set up what is happening when the camera has finished.
- Give the user a way to edit the caption in the Detail View Controller
- Save and load the Data
- Optionally follow points 21+ for delegation
What I am doing
I will keep track here of what I am doing so that I can trace back my steps and see where the errors may be.
- Change ViewController class inheritance to
UITableViewController
- In Main.storyboard: cancel the present view controller, drag a new table-view controller, make it load from our ViewController class, set it as Is Initial View Controller, embed it in a Navigation Controller. Then, select the navigation controller and, in the Attributes Inspector, check “Prefers Large Titles”. Select the table-view cell and give it an identifier of “Caption”. Add a bar button item to the top right of the view controller and change its System Item to Camera.
- Switch to the Assistant Editor and create an action called “cameraTapped” on this button.
- Switch to Standard Editor and create a new view controller. In Attributes Inspector give it a title of “Detail View Controller” (for the document outline) and, in the Identity Inspector, give it a Storyboard ID of “Detail” to be able to call it back in code.
- Drag an
UIImageView
to the detail-view-controller and pin it to all sides of the view. In the Attributes Inspector change its content mode to “Aspect Fit”. - Create a new Cocoa Touch Class based on
UIViewController
and call it “DetailViewController”. Make the appropriate view controller adopt this class and connect the image view via an outlet to the Swift file. - In ViewController.swift add a
var captions = [String]()
; implement thenumberOfRowsInSection
method with a return value ofcaptions.count
; implement thecellForRowAt
method with the dequeuing of the “Caption” cell, the setting of itscell.textLabel?.text
tocaptions[indexPath.row]
; add thedidSelectRowAt
method and instantiate the new view controller with aselectedImage
property, a title (both pointing to the same string by now) and pushing it. - In DetailViewController.swift add the
var selectedImage: String?
line to avoid errors. Build and Run to check the app is working up until now.
Remember to put some test value inside the array otherwise nothing will work and you will start to make panic (as I did!). I simply added a loop for 100 or so numeric values in viewDidLoad
to append to the captions
array. So far, so good.
- In DetailViewController.swift add the image loading conditional binding to
viewDidLoad
. Optional, implement theviewWillAppear
andviewWillDisappear
methods to hide the navigation bars and allow for full viewing of the image. - In ViewController.swift add conformance to
UIImagePickerControllerDelegate
andUINavigationControllerDelegate
; configure the picker in thecameraTapped
action method. - Create a new CocoaTouch Class for the Picture custom type, a class that subclasses
NSObject
and conforms to theCodable
protocol. Give it acaption
andimage
String variables and a class initialiser; modify ViewController.swift to have two separate arrays of Strings, one for image names and one for captions; perform the necessary changes inviewDidLoad
and in the table-view methods. - Delete the two properties at the top of ViewController.swift and replace them with a single
var pictures = [Picture]()
. Modify thenumberOfRows
method accordingly and set thetextLabel
to respond to theimage
property and thedetailTextLabel
to thecaption
. In thedidSelectRowAt
do the same for theselectedImage
property and thetitle
one. This will probably change again later. No idea how things will turn out now. - Write the
didFinishPickingMediaWithInfo
method plus the helpergetDocumentsDirectory
method. This is still incomplete as it is missing save functionality. - Inside DetailViewController.swift, create a right-bar-button-item with the “Compose” style and attach an
editCaption
method to it. Now this allows me to change the title of this view-controller but it does not change the subtitle of our original cell. - Inside ViewController.swift cancel the loading loop inside
viewDidLoad
and add atitle = "Captioned"
call! - Give permissions to use the camera in the Info.plist file. Build and run; the app launches to an empty table-view (which seems good), the camera button works, the capturing does as well but then nothing gets created in the table-view… mmm… –
- Inside DetailViewController’s
viewDidLoad
don’t forget to load the image from the path where it was saved… just using the String will not work… How was I supposed to know that only Zeus knows but fine… - Create the save method and call it wherever you reload the table views.
- Create a load method.
- Now, create a new branch to finish the challenge in the ordinary way
- Checkout to the old branch, erase the bar button item from the detail view controller and the accompanying method and, back in ViewController.swift complete the
didFinishPickingMediaWithInfo
method with a closure attached to the dismiss method. This will just work. - Checkout to the new branch which should revert the working directory to what it was before and proceed as follows.
- In DetailViewController.swift, above the class declaration, add the following protocol:
protocol DetailViewControllerDelegate: class {
func detailViewController(_ controller: DetailViewController, didFinishEditing item: Picture)
}
- In the
viewWillDisappear
method add this code:
if let picture = selectedPicture {
delegate?.detailViewController(self, didFinishEditing: picture)
}
- In ViewController.swift, in
didSelectRowAt
adddetailVC.delegate = self
- After the
ViewController
’s class add the following extension:
extension ViewController: DetailViewControllerDelegate {
func detailViewController(_ controller: DetailViewController, didFinishEditing item: Picture) {
if let index = pictures.firstIndex(of: item) {
let indexPath = IndexPath(row: index, section: 0)
if let cell = tableView.cellForRow(at: indexPath) {
cell.textLabel?.text = pictures[indexPath.row].caption
cell.detailTextLabel?.text = pictures[indexPath.row].image
save()
}
}
}
}
Done!
Plenty of ways to do it better? Sure! Does it work as it is now? Yes it does and it is awesome!
The only thing I don’t understand is why Xcode made such a mess with my branching and git repositories. So… if you want the version without the extension, this is the repo you want to check, otherwise go here.
As always, thank you so much for keeping up with me!
See you on the next one!
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!