Skip to content

Instantly share code, notes, and snippets.

@mikesparr
Last active March 18, 2018 00:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mikesparr/ef611fee6cc1865ba0b7 to your computer and use it in GitHub Desktop.
Save mikesparr/ef611fee6cc1865ba0b7 to your computer and use it in GitHub Desktop.
Swift IOS Architecture Quick Start
//
// 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
}
//
// 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)
}
}
/*
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";
//
// 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
}
}
}
}
//
// 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
}
}
}
}
//
// 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"
}
//
// 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
...
}
@mikesparr
Copy link
Author

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 potential nil return values and prevent exceptions. By using the guard syntax you can control flow of your application and allow the view layer to handle exceptions by using the do ... try ... catch syntax as illustrated in the view controller.

@mikesparr
Copy link
Author

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