Pulling It All Together
Thus far in this book, you have learned about many discrete mobile topics and created more than ten Xcode projects. Those topics were purposely implemented in individual Xcode projects with few classes. In Chapter 3, you learned the top-down development approach of using the storyboard to break the whole app into model-view-controller (MVC) content view and view controller pairs. In Chapter 4, you learned how to port smaller individual components, including user interface (UI), create read update delete (CRUD), and remote operations, piece by piece. However, all those topics were designed to be self-contained without dependencies so they could serve as independent instructions.
In the real programming world, it is the combination of features and use cases that makes your app useful and entertaining. You will surely need to understand more than one topic to complete a meaningful app. In this chapter, you will implement a simple but meaningful Swift app from start to finish using the mapping topics from Chapters 3 and 4. Nothing will be new; you will repeat the same top-down development approach as you have been using and implement one piece at a time.
Perhaps there is one thing new that I have not yet mentioned explicitly: which piece should be developed first and then what comes next. For any app, including web apps, you should go through the same thought process: you should decide the dependencies of the pieces and try to reduce the dependencies along the way. After all, there is really no right or wrong way to do it.
Again, to learn by example, your goal is to implement a RentalROI iOS app. Figure 5-1 shows the completed iOS app you will be creating.
Figure 5-1. RentalROI screens
This RentalROI app performs the following tasks:
Note This is just for exercise purposes. It would be better to calculate the amortization schedules locally without using the remote service—then you wouldn’t need to persist the result.
First, you’ll create a new Xcode project using the Single View Application template; name it RentalROI. You will be following the same porting approaches you used in Chapters 3 and 4.
Structure Your App
As usual, use top-down approach starting with storyboard tasks. Your first step is to create the Xcode storyboard as instructed in Chapter 3.
This will result in a runnable iOS app with all the content views and the view controller classes’ skeletons connected using the appropriate screen navigation pattern.
Draw Storyboard Scenes
You can clearly see four content views in Figure 5-1, and you will need to draw four storyboard scenes in Main.storyboard, covered next.
Edit Text Screen
In no particular order, let’s start with the simplest one, the second screen in Figure 5-1. You want to add a UITextField to the storyboard scene (see Chapter 4 for the detailed instructions).
Figure 5-2. EditTextView storyboard scene
Listing 5-1. EditTextViewController
import UIKit
class EditTextViewController : UIViewController, UITextFieldDelegate {
@IBOutlet weak var mEditText: UITextField!
}
Rental Property Info Screen
Continue drawing the next content view in no particular order (for example, draw the Property screen, shown in Figure 5-1). To achieve the list view L&F, you should use UITableViewController (see Chapter 3 for the detailed instructions).
Figure 5-3. Creating a Table View scene
Figure 5-4. Right Detail, Table View Cell
Listing 5-2. RentalPropertyViewController
import UIKit
class RentalPropertyViewController : UITableViewController {
}
Amortization Screen
Continue drawing the next content view in no particular order (for example, the Amortization screen, shown in Figure 5-1). It shows a list of amortization items, and a natural choice for this is UITableViewController.
Listing 5-3. AmortizationViewController
import UIKit
class AmortizationViewController: UITableViewController {
}
Monthly Details Screen
Move on to drawing the last content view, Monthly Details (shown in Figure 5-1). It has a listlike look and feel but does not really show list items. You can choose to implement this screen with labels for each line items in iOS, or you can choose to use a UITableView, as in RentalPropertyViewController. There is actually a better choice, though: using UITableViewController with static cells, with one static cell for each line.
Figure 5-5. Static Cells Table View
Figure 5-6. Two sections in the Monthly Details screen
Listing 5-4. MonthlyTermViewController IBOutlet Properties
import UIKit
class MonthlyTermViewController : UITableViewController {
@IBOutlet weak var mPaymentNo: UILabel!
@IBOutlet weak var mTotalPmt: UILabel!
@IBOutlet weak var mPrincipal: UILabel!
@IBOutlet weak var mInterest: UILabel!
@IBOutlet weak var mEscrow: UILabel!
@IBOutlet weak var mAddlPmt: UILabel!
@IBOutlet weak var mBalance: UILabel!
@IBOutlet weak var mEquity: UILabel!
@IBOutlet weak var mCashInvested: UILabel!
@IBOutlet weak var mRoi: UILabel!
}
Figure 5-7 depicts the results of the storyboard scenes’ tasks.
Figure 5-7. Four RentalROI scenes
Choose a Screen Navigation Pattern
When choosing appropriate navigation patterns, you will get good ideas from using wireframes. You normally need more than one pattern, such as a navigation stack plus navigation tabs. In this RentalROI app, you want to be able go back to a previous scene from Monthly Details to Amortization List to Property. The popular navigation stack navigation pattern is perfect for this intended behavior (see Chapter 3 for more information). For going to and from the Edit Text scene, you can choose a different navigation pattern that shows a stronger relationship to the originating context. Dialog or the iOS pop-over is the best choice (see Chapter 3).
Your immediate mission is to add the navigation patterns and draw storyboard segues to connect all the storyboard scenes to one another. Figure 5-8 shows the final storyboard with all the scenes connected.
Figure 5-8. RentalROI-connected scenes
Continue with the storyboard tasks by doing the following (see Chapter 3 for more detailed instructions):
You should have a storyboard with all scenes connected with segues, as shown in Figure 5-8.
Business Object
In object oriented programming, you create classes to present the business domain. You define properties to encapsulate the object attributes and methods for behaviors in the classes. After finishing the storyboard tasks, you naturally gain a better understanding of the business domain and the attributes you need to deal with. You don’t have to discover all of them at once. You simply create the business object when you discover any. For this RentalROI app, you only need a class that presents the rental property. Let’s create a Swift class skeleton for the RentalProperty business object class first (see Listing 5-5).
Listing 5-5. RentalProperty.swift Skeleton
import Foundation
public class RentalProperty {
}
For its attributes and behaviors, you might see some from the storyboard immediately, but you normally would not know them all in the beginning. You will discover them along the way and just fill in the blanks when you discover any.
Application Resources
Like with any GUI apps, you need images or digital assets to dress up the app. You also want to externalize strings to avoid coding these display texts directly. Do the following to implement the application resources (see Chapter 4) in this Xcode project:
Listing 5-6. Externalized Text Translation
"app_name" = "RentalROI";
"label_schedule" = "Schedule";
"label_property" = "Property";
"label_Amortization" = "Amortization";
"label_monthlydetails" = "Monthly Details";
"button_next" = "Next";
/* RentalPropertyView */
"mortgage" = "MORTGAGE";
"operations" = "OPERATIONS";
"purchasePrice" = "Purchase Price";
"downPayment" = "Down Payment %";
"loanAmount" = "Loan Amount";
"interestRate" = "Interest Rate %";
"mortgageTerm" = "Mortgage Term (Yr.)";
"escrowAmount" = "Escrow Amount";
"extraPayment" = "Extra Payment";
"expenses" = "Expenses";
"rent" = "Rent";
/* EditTextView */
"save" = "Save";
"editTextSize" = "15";
/* Monthly Details */
"MortgagePayment" = "MORTGAGE PAYMENT";
"no" = "No.";
"Principal" = "Principal";
"interest" = "Interest";
"escrow" = "Escrow";
"addlPayment" = "Add'l Payment";
"mortgageBalance" = "Mortgage Balance";
"equity" = "Equity";
"cashInvest" = "Cash Investment";
"roi" = "ROI";
Figure 5-9. Xcode project AppIcon
Implement Piece by Piece
You have all the Swift class files, storyboards, and external assets in place. Your next step is to fill in the class by implementing methods. You will start to see how your code works piece by piece!
Normally, you would discover the methods by analyzing the intended behaviors per use cases. For the purposes of this book, I will go over the existing classes one at a time, focusing on iOS SDK topics instead.
RentalProperty
RentalProperty is the model class that encapsulates the business knowledge in terms of property and mortgage attributes, and so on. Since we going to persist these attributes, I chose to implement the persistent storage logic directly inside the model class. Let’s start implementing the RentalProperty Swift class.
Listing 5-7. The RentalProperty Class Singleton Implementation
import Foundation
public class RentalProperty {
// 1. Swift stored properties to keep the application states
var purchasePrice = 0.0;
var loanAmt = 0.0;
var interestRate = 5.0;
var numOfTerms = 30;
var escrow = 0.0;
var extra = 0.0;
var expenses = 0.0;
var rent = 0.0;
struct MyStatic {
static let KEY_AMO_SAVED = "KEY_AMO_SAVED";
static let KEY_PROPERTY = "KEY_PROPERTY";
// 2a. singleton impl
private static var _sharedInstance = RentalProperty()
}
// 2b. singleton impl, private initializer
private init() {
}
// 2c. singleton accessor
class func sharedInstance() -> RentalProperty {
return MyStatic._sharedInstance
}
...
}
Listing 5-8. Create Save Data Utility Methods
class RentalProperty {
...
let userDefaults = NSUserDefaults.standardUserDefaults()
private func saveUserdefault(data:AnyObject, forKey:String) -> Bool{
userDefaults.setObject(data, forKey: forKey)
return userDefaults.synchronize()
}
private func retrieveUserdefault(key: String) -> AnyObject? {
var obj: AnyObject? = userDefaults.objectForKey(key)
return obj
}
private func deleteUserDefault(key: String) {
self.userDefaults.removeObjectForKey(key)
}
...
Listing 5-9. Loading RentalProperty Object from Storage
class RentalProperty {
...
func load() -> Bool {
var data = retrieveUserdefault(MyStatic.KEY_PROPERTY) as NSDictionary?
if var jo = data {
self.purchasePrice = jo["purchasePrice"] as Double
self.loanAmt = jo["loanAmt"] as Double
self.interestRate = jo["interestRate"] as Double
self.numOfTerms = jo["numOfTerms"] as Int
self.escrow = jo["escrow"] as Double
self.extra = jo["extra"] as Double
self.expenses = jo["expenses"] as Double
self.rent = jo["rent"] as Double
return true;
} else {
return false
}
}
...
Listing 5-10. Saving RentalProperty Object
class RentalProperty {
...
func save() -> Bool {
var jo : [NSObject : AnyObject] = [
"purchasePrice": purchasePrice,
"loanAmt" : loanAmt,
"interestRate" : interestRate,
"numOfTerms" : Double(numOfTerms),
"escrow" : escrow,
"extra" : extra,
"expenses" : expenses,
"rent" : rent]
return self.saveUserdefault(jo, forKey: MyStatic.KEY_PROPERTY)
}
...
Listing 5-11. Retrieve Amortization Schedule Array from Persistent Storage
class RentalProperty {
...
private func getAmortizationPersistentKey() -> String {
var aKey = String(format: "%.2f-%.3f-%d-%.2f-%.2f", self.loanAmt, self.interestRate, self.numOfTerms, self.escrow, self.extra);
return aKey;
}
func getSavedAmortization() -> NSArray? {
var savedKey = retrieveUserdefault(MyStatic.KEY_AMO_SAVED) as String?
var aKey = self.getAmortizationPersistentKey()
if let str = savedKey {
if(str.utf16Count > 0 && str == aKey) {
var jo = retrieveUserdefault(str) as NSArray?
return jo
}
}
return nil
}
func saveAmortization(data: NSArray) -> Bool {
var aKey = self.getAmortizationPersistentKey()
saveUserdefault(aKey, forKey: MyStatic.KEY_AMO_SAVED)
return saveUserdefault(data, forKey: aKey)
}
...
EditTextViewController
The purpose of this controller is to prompt users to enter text and return the user input to its presenting view controller. Although this EditTextView storyboard scene looks simple, it demonstrates a frequent usage in iOS apps: the presenting view controller passes data to the presented view controller (see Chapter 3). Also, frequently, the presented view controller returns data to the presenting view controller, introduced next.
Return Data to the Presenting View Controller
To return data to the presenting view controller, the iOS SDK uses a callback to the delegate pattern in many system classes. You will implement the same design pattern for your purposes. Do the following:
Listing 5-12. The Callback Protocol in EditTextViewController.swift File
// Callback protocol
protocol EditTextViewControllerDelegate {
func onTextEditSaved(vc: EditTextViewController, data: String);
func onTextEditCanceled(vc: EditTextViewController);
}
class EditTextViewController : UIViewController, UITextFieldDelegate {
...
Listing 5-13. Java Fields to Swift Stored Properties
class EditTextViewController : ... {
...
// a.
var tag: NSIndexPath!
var header = ""
var text = ""
var delegate: EditTextViewControllerDelegate?
...
@IBAction func doSave(sender: AnyObject) {
var returnText = self.mEditText.text
if(delegate != nil) {
// b. return data to the delegate
delegate!.onTextEditSaved(self, data: returnText)
}
}
@IBAction func doCancel(sender: AnyObject) {
if(delegate != nil) {
delegate!.onTextEditCanceled(self)
}
...
Listing 5-14. Presenting ViewController
// 3a implement protocol
class RentalPropertyViewController : UITableViewController, EditTextViewControllerDelegate {
...
// You will often want to do a little preparation before segue navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var identifier = segue.identifier
if identifier == "EditText" {
var indexPath = sender as NSIndexPath
var presentedVc = (segue.destinationViewController as UINavigationController).topViewController as EditTextViewController
var cell = tableView.cellForRowAtIndexPath(indexPath)!
presentedVc.tag = indexPath
presentedVc.header = cell.textLabel!.text!
presentedVc.text = cell.detailTextLabel!.text!
// 3b. assign self to the callback
presentedVc.delegate = self
} else { // AmortizationTable segue
var toFrag = segue.destinationViewController as AmortizationViewController
toFrag.monthlyTerms = sender as NSArray // TODO: a temp compilation error
}
}
// 3c. delegate protocol to receive data via callback from presented VC
func onTextEditSaved(vc: EditTextViewController, data: String) {
self.dismissViewControllerAnimated(true, completion: nil)
// TODO: use data
}
func onTextEditCanceled(vc: EditTextViewController) {
self.dismissViewControllerAnimated(true, completion: nil)
}
...
}
Handle Soft Keyboard
The rest of the code in EditTextViewController primarily handles a soft keyboard, aka on-screen keyboard. On iOS, you should use NSNotificationCenter to detect the keyboard shown or hidden and update the widget positions accordingly to accommodate the soft keyboard size. Listing 5-15 shows the completed EditTextViewController class implementation, including how to handle the soft keyboard.
Listing 5-15. EditTextViewController Completed Code
class EditTextViewController : UIViewController, UITextFieldDelegate {
@IBOutlet weak var bottomConstraint: NSLayoutConstraint!
@IBOutlet weak var mEditText: UITextField!
var tag : NSIndexPath!
var header = ""
var text = ""
var delegate: EditTextViewControllerDelegate!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = self.header
mEditText.text = self.text
// 1. Register keyboard shown notification
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "keyboardAppeared:",
name: UIKeyboardDidShowNotification, object: nil)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
showKeyboard()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
// 2. Remove keyboard notification
NSNotificationCenter.defaultCenter().removeObserver(self,
name: UIKeyboardDidShowNotification, object: nil)
}
@IBAction func doSave(sender: AnyObject) {
var returnText = self.mEditText.text
if(delegate != nil) {
delegate.onTextEditSaved(self, data: returnText)
}
}
@IBAction func doCancel(sender: AnyObject) {
if(delegate != nil) {
delegate.onTextEditCanceled(self)
}
}
// 3. shift the text field up by changing the autolayout constraint
func keyboardAppeared(notification: NSNotification) {
println(">>keyboardAppeared")
var keyboardInfo = notification.userInfo as NSDictionary!
var kbFrame = keyboardInfo.valueForKey(UIKeyboardFrameBeginUserInfoKey) as NSValue
var kbFrameRect: CGRect = kbFrame.CGRectValue()
var keyboardH = min(kbFrameRect.size.width, kbFrameRect.size.height)
UIView.animateWithDuration(0, animations: { () -> Void in
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
self.bottomConstraint.constant = self.bottomConstraint.constant + keyboardH/self.bottomConstraint.multiplier
}
}, completion: {(b) -> Void in
self.mEditText.selectAll(self);
})
}
// 4. show keyboard
private func showKeyboard() {
self.mEditText.becomeFirstResponder()
}
private func hideKeyboard() {
self.mEditText.endEditing(true)
}
}
RentalPropertyViewController
When the app is launched, this is the first content view. The purpose of this view controller is to collect user input and present the amortization schedule.
Listing 5-16 shows the typical ViewController life-cycle methods and UI code.
Listing 5-16. RentalPropertyViewController Completed Implementation
import UIKit
class RentalPropertyViewController : UITableViewController, EditTextViewControllerDelegate {
...
var _property = RentalProperty.sharedInstance()
var _savedAmortization: NSArray?
// 1a. Lifecycle callback
override func viewDidLoad() {
super.viewDidLoad()
_property.load();
}
// 1b. Lifecycle callback
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.navigationItem.title = "Property"
}
// button action handle
@IBAction func doSchedule(sender: AnyObject) {
// TODO
}
// 2. implement tableview datasource
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2 // 2a.
}
// 2b.
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 0 {
return NSLocalizedString("mortgage", comment: "")
} else {
return NSLocalizedString("operations", comment: "")
}
}
// 2c
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 7
} else {
return 2
}
}
// 2d
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("aCell", forIndexPath: indexPath) as UITableViewCell
var textLabel = cell.textLabel!
var detailTextLabel = cell.detailTextLabel!
var pos = (indexPath.section, indexPath.row)
switch (pos) {
case (0, 0):
textLabel.text = NSLocalizedString("purchasePrice", comment: "")
detailTextLabel.text = NSString(format: "%.0f", _property.purchasePrice);
case (0, 1):
textLabel.text = NSLocalizedString("downPayment", comment: "")
if (_property.purchasePrice > 0) {
var down = (1 - _property.loanAmt / _property.purchasePrice) * 100.0;
detailTextLabel.text = NSString(format: "%.0f", down);
if (_property.loanAmt == 0 && down > 0) {
_property.loanAmt = _property.purchasePrice * (1 - down / 100.0);
}
} else {
detailTextLabel.text = "0";
}
case (0, 2):
textLabel.text = NSLocalizedString("loanAmount", comment: "")
detailTextLabel.text = NSString(format: "%.2f", _property.loanAmt)
case (0, 3):
textLabel.text = NSLocalizedString("interestRate", comment: "")
detailTextLabel.text = NSString(format: "%.3f", _property.interestRate)
case (0, 4):
textLabel.text = NSLocalizedString("mortgageTerm", comment: "")
detailTextLabel.text = NSString(format: "%d", _property.numOfTerms)
case (0, 5):
textLabel.text = NSLocalizedString("escrowAmount", comment: "")
detailTextLabel.text = NSString(format: "%.0f", _property.escrow)
case (0, 6):
textLabel.text = NSLocalizedString("extraPayment", comment: "")
detailTextLabel.text = NSString(format: "%.0f", _property.escrow);
case (1, 0):
textLabel.text = NSLocalizedString("expenses", comment: "")
detailTextLabel.text = NSString(format: "%.0f", _property.expenses);
case (1, 1):
textLabel.text = NSLocalizedString("rent", comment: "")
detailTextLabel.text = NSString(format: "%.0f", _property.rent);
default:
break;
}
return cell
}
// tableView delegate
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// 3a.
self.performSegueWithIdentifier("EditText", sender: indexPath)
}
// You will often want to do a little preparation before segue navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var identifier = segue.identifier
if identifier == "EditText" {
var indexPath = sender as NSIndexPath
var presentedVc = (segue.destinationViewController as UINavigationController).topViewController as EditTextViewController
var cell = tableView.cellForRowAtIndexPath(indexPath)!
// 3b.
presentedVc.tag = indexPath
presentedVc.header = cell.textLabel!.text!
presentedVc.text = cell.detailTextLabel!.text!
presentedVc.delegate = self
} else { // AmortizationTable segue
// 4e.
// TODO: coming next
}
}
// 3c. delegate protocol to receive data from presented VC
func onTextEditSaved(vc: EditTextViewController, data: String) {
self.dismissViewControllerAnimated(true, completion: nil)
switch (vc.tag.section, vc.tag.row) {
case (0,0):
_property.purchasePrice = (data as NSString).doubleValue;
var indexPath = NSIndexPath(forRow: 0, inSection: 0)
var percent = tableView.cellForRowAtIndexPath(indexPath)!.detailTextLabel!.text!
var down = (percent as NSString).doubleValue
if (_property.purchasePrice > 0 && _property.loanAmt == 0 && down > 0) {
_property.loanAmt = _property.purchasePrice * (1 - down / 100.0)
}
break;
case (0,1):
var percentage = (data as NSString).doubleValue / 100.0;
_property.loanAmt = _property.purchasePrice * (1 - percentage);
break;
case (0,2):
_property.loanAmt = (data as NSString).doubleValue;
break;
case (0,3):
_property.interestRate = (data as NSString).doubleValue;
break;
case (0,4):
_property.numOfTerms = (data as NSString).integerValue;
break;
case (0,5):
_property.escrow = (data as NSString).doubleValue;
break;
case (0,6):
_property.extra = (data as NSString).doubleValue;
break;
case (1,0):
_property.expenses = (data as NSString).doubleValue;
break;
case (1,1):
_property.rent = (data as NSString).doubleValue;
break;
default:
break;
}
tableView.reloadData()
_property.save();
}
func onTextEditCanceled(vc: EditTextViewController) {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
Listing 5-17. doAmortization(...)
class RentalPropertyViewController : ... {
...
// define constants
struct MyStatic {
private static let URL_service_tmpl = "http://www.pdachoice.com/ras/service/amortization?loan=%.2f&rate=%.3f&terms=%d&extra=%.2f&escrow=%.2f"
private static let KEY_DATA = "data"
private static let KEY_RC = "rc"
private static let KEY_ERROR = "error"
}
// button action handle
@IBAction func doSchedule(sender: AnyObject) {
// 4a
_savedAmortization = _property.getSavedAmortization();
if (_savedAmortization != nil) {
performSegueWithIdentifier("AmortizationTable", sender: _savedAmortization!)
} else {
// 4b
var url = NSString(format: MyStatic.URL_service_tmpl, _property.loanAmt, _property.interestRate, _property.numOfTerms, _property.extra, _property.escrow)
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
var urlRequest = NSMutableURLRequest(URL: NSURL(string: url)!)
urlRequest.HTTPMethod = "GET"
urlRequest.setValue("text/html",forHTTPHeaderField: "accept")
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue(),
completionHandler: {(resp: NSURLResponse!, data: NSData!, error: NSError!) -> Void in
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue(),
completionHandler: {(resp: NSURLResponse!, data: NSData!, error: NSError!) -> Void in
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
var errMsg: String?
if error == nil {
var statusCode = (resp as NSHTTPURLResponse).statusCode
if(statusCode == 200) {
var str = NSString(data: data, encoding: NSUTF8StringEncoding)
var parseErr: NSError?
var json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: &parseErr) as NSArray?
if parseErr == nil {
// 4c. save
self._property.saveAmortization(json!)
// 4d. segue navigation
self.performSegueWithIdentifier("AmortizationTable", sender: json!)
return
} else {
errMsg = parseErr?.debugDescription
}
} else {
errMsg = "HTTP RC: (statusCode)"
}
} else {
errMsg = error.debugDescription
}
// 4f. simple error handling
var alert = UIAlertController(title: "Error", message: errMsg, preferredStyle: UIAlertControllerStyle.Alert)
var actionCancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel,
handler: {action in
// do nothing
})
alert.addAction(actionCancel)
self.presentViewController(alert, animated: true, completion: nil)
})
})
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var identifier = segue.identifier
if identifier == "EditText" {
// 3b.
...
} else { // AmortizationTable segue
// 4e.
var toVc = segue.destinationViewController as AmortizationViewController
toVc.monthlyTerms = sender as NSArray // a temp error to be fixed next
}
}
...
You should get a temporary compilation error in 4e because you haven’t defined the monthlyTerms stored properties yet. You will do that next.
AmortizationViewController
Let’s move on to AmortizationViewController. The sole purpose of this view is to render the amortization items in a TableView. Listing 5-18 shows the completed implementation.
Listing 5-18. AmortizationViewController Class, Completed Code
import UIKit
class AmortizationViewController: UITableViewController {
// 1. From the presenting view controller
var monthlyTerms: NSArray!
override func viewDidLoad() {
super.viewDidLoad()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return monthlyTerms.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("aCell") as UITableViewCell!
var textLabel = cell.textLabel!
var detailTextLabel = cell.detailTextLabel!
var pos = indexPath.row
var monthlyTerm = monthlyTerms[pos] as NSDictionary
var pmtNo = monthlyTerm["pmtNo"] as Int
var balance0 = monthlyTerm["balance0"] as Double
textLabel.text = NSString(format: "%d $%.2f", pmtNo, balance0)
var interest = monthlyTerm["interest"] as Double
var principal = monthlyTerm["principal"] as Double
var property = RentalProperty.sharedInstance();
var invested = property.purchasePrice - property.loanAmt + (property.extra * Double(pmtNo))
var net = property.rent - property.escrow - interest - property.expenses
var roi = net * 12 / invested
detailTextLabel.text = NSString(format: "Interest: %.2f Principal: %.2f ROI: %.2f", interest, principal, roi * 100);
return cell
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.navigationItem.title = NSLocalizedString("label_Amortization", comment: "")
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// 3. Present MonthlyTerm view controller
self.performSegueWithIdentifier("MonthlyTerm", sender: indexPath)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var vc = segue.destinationViewController as MonthlyTermViewController
// 4. Pass data to the presented view controller
var row = (sender! as NSIndexPath).row
vc.monthlyTerm = monthlyTerms[row] as NSDictionary // TODO: a temp error
}
}
This completes the whole AmortizationViewController Swift class implementation, with a temporarily compilation error in step 4. You will define the monthlyTerm stored property next.
MonthlyTermViewController
Let’s move on to MonthlyTermViewController. It renders the detailed information for the selected month, as shown in Listing 5-19.
Listing 5-19. AmortizationViewController Class Completed Code
import UIKit
class MonthlyTermViewController : UITableViewController {
@IBOutlet weak var mPaymentNo: UILabel!
@IBOutlet weak var mTotalPmt: UILabel!
@IBOutlet weak var mPrincipal: UILabel!
@IBOutlet weak var mInterest: UILabel!
@IBOutlet weak var mEscrow: UILabel!
@IBOutlet weak var mAddlPmt: UILabel!
@IBOutlet weak var mBalance: UILabel!
@IBOutlet weak var mEquity: UILabel!
@IBOutlet weak var mCashInvested: UILabel!
@IBOutlet weak var mRoi: UILabel!
// 1. From the presenting view controller
var monthlyTerm: NSDictionary!
override func viewDidLoad() {
super.viewDidLoad()
// 3. Fill the widget with data before view appears.
var principal = self.monthlyTerm["principal"] as Double
var interest = self.monthlyTerm["interest"] as Double
var escrow = self.monthlyTerm["escrow"] as Double
var extra = self.monthlyTerm["extra"] as Double
var balance = (self.monthlyTerm["balance0"] as Double) - principal
var paymentPeriod = self.monthlyTerm["pmtNo"] as Int
var totalPmt = principal + interest + escrow + extra
self.mTotalPmt.text = NSString(format: "$%.2f", totalPmt)
self.mPaymentNo.text = NSString(format: "No. %d", paymentPeriod)
self.mPrincipal.text = NSString(format: "$%.2f", principal)
self.mInterest.text = NSString(format: "$%.2f", interest)
self.mEscrow.text = NSString(format: "$%.2f", escrow)
self.mAddlPmt.text = NSString(format: "$%.2f", extra)
self.mBalance.text = NSString(format: "$%.2f", balance)
var property = RentalProperty.sharedInstance();
var invested = property.purchasePrice - property.loanAmt + (property.extra * Double(paymentPeriod))
var net = property.rent - escrow - interest - property.expenses;
var roi = net * 12 / invested
self.mEquity.text = NSString(format: "$%.2f", property.purchasePrice - balance)
self.mCashInvested.text = NSString(format: "$%.2f", invested)
self.mRoi.text = NSString(format: "%.2f%% ($%.2f/mo)", roi * 100, net)
}
}
This completes the whole MonthlyTermViewController Swift class implementation.
All the class translations are completed. Build and run the iOS RentalROI app and do some testing. Your app should look like Figure 5-1.
Summary
This chapter showed you how to apply the individual topics introduced in Chapters 3 and 4—such as creating master list details, creating drill-down navigation, exchanging data between view controllers, using basic UI widgets, saving data, and using remote services—all in one simple and meaningful app.
Although the RentalROI app is not complicated enough to show you all the topics in this book, the general iOS app development steps remain the same. You started with the Xcode storyboard and created view controllers to pair with the storyboard scenes. Then, you implemented storyboard segues to connect the view controller together. The result was a set of connected view controllers. With the storyboard in place, you started following the use case paths and discovering the business objects and methods, and the dots started connecting to each other.
When you encounter platform-specific SDK or topics, use this book’s table of contents to find the instructions that will guide you through the iOS SDK.