Last active
March 18, 2018 00:20
-
-
Save mikesparr/ef611fee6cc1865ba0b7 to your computer and use it in GitHub Desktop.
Swift IOS Architecture Quick Start
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Constants.swift | |
// Goomzee | |
// | |
// Created by Michael Sparr on 11/11/15. | |
// | |
import Foundation | |
// MARK: - Environment | |
struct Environment | |
{ | |
static let Development = "development" | |
static let Test = "test" | |
static let Stage = "stage" | |
static let Production = "production" | |
static let Default = "development" | |
} | |
// MARK: - Errors & Logging | |
struct LogLevel | |
{ | |
static let Trace = "trace" | |
static let Debug = "debug" | |
static let Info = "info" | |
static let Warn = "warn" | |
static let Error = "error" | |
static let Critical = "critical" | |
} | |
enum InputError: ErrorType | |
{ | |
case InputMissing | |
case PasswordMismatch | |
case InvalidEmail | |
... | |
} | |
enum APIError: ErrorType | |
{ | |
case ConnectionLost | |
case RequestTimeout | |
... | |
} | |
enum GeneralError: ErrorType | |
{ | |
case IncompleteFeature | |
case Unknown | |
} | |
// MARK: - Storyboard | |
struct Storyboard | |
{ | |
// storyboard names | |
static let Main = "Main" | |
// storyboard IDs | |
static let WelcomeViewIdentifier = "welcomeViewController" | |
static let LoginViewIdentifier = "loginViewController" | |
static let RegisterViewIdentifier = "registerViewController" | |
static let ResetPasswordViewIdentifier = "resetPasswordViewController" | |
static let HomeViewIdentifier = "homeViewController" | |
... | |
// segues | |
static let ShowHomeIdentifier = "Show Home" | |
... | |
// collection views | |
... | |
// table views | |
static let BasicCellIdentifier = "Basic Cell" // generic cell (built-in) | |
... | |
// generic | |
static let EmptyString = "" | |
} | |
// MARK: - NSUserDefaults | |
struct Defaults | |
{ | |
static let CurrentUserKey = "myAppUser" // NSUserDefaults object | |
... | |
} | |
// MARK: - Images & Files | |
struct Images | |
{ | |
static let DefaultBackground = "DefaultBackground" | |
... | |
} | |
struct FileType | |
{ | |
static let PNG = "image/png" | |
static let JPEG = "image/jpeg" | |
static let PJPEG = "image/pjpeg" // photoshop JPeg | |
static let GIF = "image/gif" | |
static let TIFF = "image/tiff" | |
static let BMP = "image/bmp" // bitmap | |
static let SVG = "image/svg" | |
... | |
} | |
let MimeType = FileType() // alias | |
// MARK: - Localization | |
struct Currency | |
{ | |
static let USD = "USD" | |
... | |
} | |
struct Country | |
{ | |
static let UnitedStates = "US" // ISO standard abbreviation | |
... | |
} | |
struct Language | |
{ | |
static let EnglishUS = "en_US" | |
static let EnglishUK = "en_UK" | |
static let Spanish = "es_ES" | |
static let French = "fr_FR" | |
static let German = "de_DE" | |
static let Greek = "gr_GR" | |
static let Italian = "it_IT" | |
... | |
} | |
struct TimeZone | |
{ | |
// TODO: determine time zone (name or GMT offsets) | |
} | |
// MARK: - AppData & Picklists (dynamic lists) | |
// MARK: - API | |
struct API | |
{ | |
static let URLS = [ | |
Environment.Development: "http://localhost:8085/api/v1", | |
Environment.Test: "https://dev.yourdomain.com/api/v1", | |
Environment.Stage: "https://stage.yourdomain.com/api/v1", | |
Environment.Production: "https://yourdomain.com/api/v1" | |
] | |
static let AllowedFileTypes = [ | |
FileType.PNG, | |
FileType.JPEG, | |
FileType.PJPEG, | |
FileType.TIFF, | |
FileType.BMP, | |
FileType.SVG, | |
FileType.GIF, | |
FileType.SVG, | |
FileType.PDF, | |
FileType.WAV, | |
FileType.MOV | |
] | |
// constraints | |
static let MaxRequestWaitSeconds = 30 // seconds | |
static let MaxUploadSizeMegabytes = 25 // megabytes | |
static let MaxUploadFiles = 20 // file count | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Helpers.swift | |
// Goomzee | |
// | |
// Created by Michael Sparr on 11/22/15. | |
// | |
import Founation | |
import UIKit | |
class Helpers | |
{ | |
// MARK: - UIAlertViewController (called from UIViewController) | |
/** | |
* Usage: Helpers.showOKAlert("Alert", message: "Something happened", target: self) | |
*/ | |
static func showOKAlert(title: String, message: String, target: UIViewController) | |
{ | |
let alertController: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) | |
let okAction: UIAlertAction = UIAlertAction(title: Keys.ButtonOK.localized, style: .Default, handler: nil) | |
alertController.addAction(okAction) | |
target.presentViewController(alertController, animated: true, completion: nil) | |
} | |
/** | |
* Usage: Helpers.showOKHelpAlert("Notice", message: "Something happened.", target: self, handler: { (UIAlertAction) -> Void in | |
* // perform help option code here | |
* }) | |
*/ | |
static func showOKHelpAlert(title: String, message: String, target: UIViewController, handler: ((UIAlertAction) -> Void)?) | |
{ | |
let alertController: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) | |
let helpAction: UIAlertAction = UIAlertAction(title: Keys.ButtonHelp.localized, style: .Default, handler: handler) | |
let okAction: UIAlertAction = UIAlertAction(title: Keys.ButtonOK.localized, style: .Default, handler: nil) | |
alertController.addAction(helpAction) | |
alertController.addAction(okAction) | |
target.presentViewController(alertController, animated: true, completion: nil) | |
} | |
/** | |
* Usage: Helpers.showContinueAlert("Log Out", message: "Are you sure you want to log out?", target: self, handler: { (UIAlertAction) -> Void in | |
* // perform log out code here | |
* }) | |
*/ | |
static func showContinueAlert(title: String, message: String, target: UIViewController, handler: ((UIAlertAction) -> Void)?) | |
{ | |
let alertController: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) | |
let continueAction: UIAlertAction = UIAlertAction(title: Keys.ButtonContinue.localized, style: .Default, handler: handler) | |
let cancelAction: UIAlertAction = UIAlertAction(title: Keys.ButtonCancel.localized, style: .Cancel, handler: nil) | |
alertController.addAction(cancelAction) | |
alertController.addAction(continueAction) | |
target.presentViewController(alertController, animated: true, completion: nil) | |
} | |
// MARK: - UIActionSheet (TODO) | |
// MARK: - Localization | |
static func getFormattedStringFromNumber(number: Double) -> String | |
{ | |
let numberFormatter = NSNumberFormatter() | |
numberFormatter.numberStyle = .DecimalStyle | |
return numberFormatter.stringFromNumber(number)! | |
} | |
static func getFormattedStringFromDate(aDate: NSDate) -> String | |
{ | |
let dateFormatter = NSDateFormatter() | |
dateFormatter.dateStyle = .MediumStyle | |
return dateFormatter.stringFromDate(aDate) | |
} | |
} | |
// MARK: - Extensions | |
extension String | |
{ | |
var localized: String { | |
return NSLocalizedString(self, tableName: nil, bundle: NSBundle.mainBundle(), value: "", comment: "") | |
} | |
} | |
extension String | |
{ | |
func localizedWithComment(comment:String) -> String { | |
return NSLocalizedString(self, tableName: nil, bundle: NSBundle.mainBundle(), value: "", comment: comment) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Localizable.strings | |
ListIt | |
Created by Michael Sparr on 11/22/15. | |
Copyright © 2015 Goomzee Corporation. All rights reserved. | |
*/ | |
// buttons | |
"button.login" = "Login"; | |
"button.logout" = "Log Out"; | |
"button.reset" = "Reset"; | |
"button.ok" = "OK"; | |
"button.help" = "Help"; | |
"button.cancel" = "Cancel"; | |
"button.continue" = "Continue"; | |
// labels | |
"label.welcome_back" = "Welcome back,"; | |
"label.password_reset" = "Password Reset"; | |
// placeholders | |
"placeholder.email" = "Email"; | |
"placeholder.password" = "Password"; | |
"placeholder.confirm" = "Confirm Password"; | |
// titles | |
"title.alert" = "Alert"; | |
"title.notice" = "Notice"; | |
"title.input_error" = "Input Error"; | |
"title.missing_fields" = "Missing Fields"; | |
"title.logout_confirm" = "Confirm Log Out"; | |
"title.select_option" = "Select Option"; | |
"title.header" = "Header"; | |
"title.my_profile" = "My Profile"; | |
"title.about" = "About"; // info about the app | |
"title.help" = "Help"; // support or help link | |
"title.logout" = "Log Out"; | |
// instructions | |
"info.settings" = "Welcome back!"; | |
// options | |
// text | |
"text.logout_confirm" = "Are you sure you want to log out?"; | |
"text.fields_required" = "All fields are required"; | |
"text.password_mismatch" = "Passwords did not match. Please retry"; | |
"text.unknown_error" = "An unknown error occurred. Please try again"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LoginViewController.swift | |
// Goomzee | |
// | |
// Created by Michael Sparr on 11/11/15. | |
// | |
import UIKit | |
class LoginViewController: UIViewController { | |
// MARK: - Properties | |
var user: User! | |
var userManager: UserManager! | |
// MARK: - IBOutlet | |
@IBOutlet weak var usernameTextField: UITextField! | |
@IBOutlet weak var passwordTextField: UITextField! | |
// MARK: - VC Lifecycle | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
userManager = UserManager.sharedInstance // singleton | |
usernameTextField.placeholder = Keys.PlaceholderUsername.localized | |
passwordTextField.placeholder = Keys.PlaceholderPassword.localized | |
} | |
// MARK: - IBAction | |
@IBAction func loginAction(sender: AnyObject) { | |
// perform action | |
do { | |
let user = try userManager.login(usernameTextField.text!, password: passwordTextField.text!) | |
self.performSegueWithIdentifier(Storyboard.ShowHomeIdentifier, sender: user) | |
} catch InputError.InputMissing { | |
Helpers.showOKAlert(Keys.TitleMissingFields.localized, message: Keys.FieldsRequired.localized, target: self) | |
} catch { | |
debugPrint(error) | |
Helpers.showOKAlert(Keys.TitleAlert.localized, message: Keys.UnknownError.localized, target: self) | |
} | |
} | |
// MARK: - Navigation | |
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { | |
if let identifier = segue.identifier { | |
switch identifier | |
{ | |
case Storyboard.ShowHomeIdentifier: | |
// NOTE: - HomeViewController is just custom class for another view after login | |
let destinationViewController = segue.destinationViewController as! HomeViewController | |
destinationViewController.user = self.user | |
default: | |
break | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SettingsTableViewController.swift | |
// Goomzee | |
// | |
// Created by Michael Sparr on 11/16/15. | |
// | |
import UIKit | |
class SettingsTableViewController: UITableViewController { | |
// MARK: - Public API | |
var user: User! | |
var userManager: UserManager! | |
// MARK: - Private | |
private let settingsNav = [ | |
[Keys.TitleHeader.localized], // profile image | |
[Keys.TitleMyProfile.localized, ...], | |
[Keys.TitleAbout.localized, Keys.TitleHelp.localized, Keys.TitleLogout.localized] | |
] | |
// MARK: - IBOutlet | |
// MARK: - VC Lifecycle | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
userManager = UserManager.sharedInstance | |
user = userManager.getCurrentUser() | |
} | |
// MARK: - UITableViewDataSource | |
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { | |
return settingsNav.count | |
} | |
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
return settingsNav[section].count | |
} | |
// MARK: - UITableViewDelegate | |
override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { | |
if section == 0 { | |
return CGFloat.min | |
} else { | |
return 20.0 | |
} | |
} | |
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | |
return nil | |
} | |
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { | |
let section = indexPath.section | |
let sectionRows = settingsNav[section] | |
let row = sectionRows[indexPath.row] | |
switch row | |
{ | |
case Keys.ButtonLogout.localized: | |
Helpers.showContinueAlert(Keys.TitleLogoutConfirm.localized, message: Keys.LogoutConfirm.localized, target: self, handler: { (UIAlertAction) -> Void in | |
// perform log out | |
debugPrint("User logged out at \(NSDate())") | |
self.userManager.logout() | |
let storyboard: UIStoryboard = UIStoryboard(name: Storyboard.Main, bundle: nil) | |
let viewController: UIViewController = storyboard.instantiateViewControllerWithIdentifier(Storyboard.LoginViewIdentifier) as! LoginViewController | |
self.presentViewController(viewController, animated: true, completion: nil) | |
}) | |
default: | |
break | |
} | |
} | |
// MARK: - IBAction | |
// MARK: - Navigation | |
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { | |
if let identifier = segue.identifier { | |
debugPrint("Identifier from settings was \(identifier)") | |
switch identifier { | |
case Storyboard.ShowProfileIdentifier: | |
debugPrint("Sending user \(user.displayName) to viewController") | |
let destinationNavigationController = segue.destinationViewController as! UINavigationController | |
let viewController = destinationNavigationController.topViewController as! UserProfileTableViewController | |
viewController.user = user | |
default: | |
break | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Strings.swift | |
// Goomzee | |
// | |
// Created by Michael Sparr on 11/22/15. | |
// | |
import Foundation | |
struct Keys | |
{ | |
// buttons | |
static let ButtonOK = "button.ok" | |
static let ButtonHelp = "button.help" | |
static let ButtonCancel = "button.cancel" | |
static let ButtonContinue = "button.continue" | |
static let ButtonLogin = "button.login" | |
static let ButtonLogout = "button.logout" | |
// labels | |
static let LabelWecomeBack = "label.welcome_back" | |
static let LabelPasswordReset = "label.password_reset" | |
// placeholders | |
static let PlaceholderEmail = "placeholder.email" | |
static let PlaceholderPassword = "placeholder.password" | |
static let PlaceholderConfirm = "placeholder.confirm" | |
// titles | |
static let TitleAlert = "title.alert" | |
static let TitleNotice = "title.notice" | |
static let TitleInputError = "title.input_error" | |
static let TitleMissingFields = "title.missing_fields" | |
static let TitleLogoutConfirm = "title.logout_confirm" | |
static let TitleHeader = "title.header" | |
static let TitleMyProfile = "title.my_profile" | |
static let TitleAbout = "title.about" | |
static let TitleHelp = "title.help" | |
static let TitleLogout = "title.logout" | |
// instructions | |
static let InfoSettings = "info.settings" | |
// options | |
// text | |
static let LogoutConfirm = "text.logout_confirm" | |
static let FieldsRequired = "text.fields_required" | |
static let PasswordMismatch = "text.password_mismatch" | |
static let UnknownError = "text.unknown_error" | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// UserManager.swift | |
// Goomzee | |
// | |
// Created by Michael Sparr on 11/12/15. | |
// | |
import Foundation | |
struct User | |
{ | |
var id: String! | |
var displayName: String? | |
var emailAddress: String! | |
} | |
class UserManager | |
{ | |
// MARK: - Singleton | |
static let sharedInstance = UserManager() | |
private init() { | |
} | |
// MARK: - Properties | |
var user: User! | |
// MARK: - Public API | |
func login(username: String?, password: String?) throws -> User | |
{ | |
guard let username = username, let password = password | |
where !username.isEmpty && !password.isEmpty else { | |
throw InputError.InputMissing | |
} | |
// perform user login functions here | |
return self.user | |
} | |
func registerUser(username: String?, password: String?, confirm: String?) throws -> User | |
{ | |
guard let username = username, let password = password | |
where !username.isEmpty && !password.isEmpty else { | |
throw InputError.InputMissing | |
} | |
guard let confirm = confirm | |
where confirm == password else { | |
throw InputError.PasswordMismatch | |
} | |
// perform user register functionality here | |
return self.user | |
} | |
func resetPassword(email: String?) throws | |
{ | |
guard let email = email | |
where !email.isEmpty else { | |
throw InputError.InputMissing | |
} | |
// perform password reset functionality here | |
} | |
// MARK: - Private | |
... | |
} |
Localization (with i18n)
Updated the files to use localized strings along with helper extension of the String
object so you can use String.localized
where the string is the key to the Localizable.strings
item. Instead of hand-writing each key as used throughout app, instead use a Strings.swift
file with Keys struct so you can auto-complete through app.
Adding local strings file:
File > New > Resource (strings)
- name it
Localizable.strings
- tap on file and in Identity inspector tap on "Localize .." button on right and select "English"
- localize your app in base language first, then File > Editor > Add Locales > (desired language)
- export those files to translator and later import into your app
Usage:
- Add string to
Localizable.strings
file with respective key (don't forget semicolon at end in this file) - Add key reference to
Strings.swift
file in appropriate section - Reference in your code:
Keys.SomeConstantName.localized
Note:
XCode has built-in localization of the storyboards, etc. that can generate for each locale and get translated. Minimize the hard-coded strings in your storyboards, however, or override them in code when instantiating your views and leverage this technique wherever possible.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There are many architectural design patterns at play here so hopefully I can explain some of them. The expectation is you are familiar with Swift and IOS application development so these are merely suggested best practices for assembling your application with proper encapsulation and error handling, plus simplified changes for future IOS updates.
Constants
Setting up a global constants file allows you to take advantage of the built-in code completion in your software editor (i.e. XCode). In addition, you eliminate spelling errors and allow flexibility if things change (change them in one place and not all over your code).
Helpers
Some of the helpers use optional closures
handler attribute
to allow calling object to execute some code based on user response. This is just a simple example but illustrating how you can centralize common UI events in your app.Why centralize UI events?
A simple example is when Apple deprecated UIAlertView after iOS8 so any apps that had alerts scattered around their code had to recreate them all to UIAlertController with new syntax. If you used a helper class, then you would just update them in one place and entire app would be compliant.
Singleton
A singleton class is one that is invoked only once and re-used throughout the application. This is a popular pattern but must be used prudently.
APIManager
,UserManager
,SettingsManager
are commonly-used singletons that encapsulate the functionality and remote server interaction.Guard syntax
If you study optionals in Swift you use the
if let
syntax to handle potentialnil
return values and prevent exceptions. By using theguard
syntax you can control flow of your application and allow the view layer to handle exceptions by using thedo ... try ... catch
syntax as illustrated in the view controller.