Skip to content
ECE564-iOS-DukePersonApp

ECE564-iOS-DukePersonApp

Project ID: 9863

iOS course project for tc233

DukePerson App Logo
Swift Xcode macOS version
DukePerson app is a course/homework project follows the rubrics from Prof. Ric Telford, which helps students in ECE564 iOS Mobile Development course to get started with Swift and iOS programming.

DukePerson

The starting line of my iOS developer journey. Never forget where I started.

Features

  • Local data persistence with Codable protocol and JSONEncoder/JSONDecoder
  • Camera/Album support for user avatar
  • Lazyload server entries with animation and background threads
  • Fuzzy search of names
  • CRUD operations for Duke affiliates locally and remotely
  • More to find in the app!

Example

View person demo

Above demonstrates the basic feature of viewing a Duke Person in the app. More features can be found in the section Usage down below.

The example application is the best way to see DukePerson in action. Simply open the omnimojiHW6/omnimojiHW6.xcodeproj and run the omnimojiHW6 scheme.

Please note: since the server for fetchAPI and postAPI is shut down, the remote features cannot be accessed anymore. You can still check out the fallback behaviors without internet connection.

Usage

Create Read Update Delete
Create Read Update Delete
The app allows user to create new entries, and pick corresponding avatar for the person added. The basic info of a Duke Person can be displayed. Also, the entries are grouped by their categories, such as Professor, TA and Student (Further grouped by custom user input group names). User can change all the fields of a Duke Person and make the update in local database instantly. According to the requirements, if the user want to post to the database, he will need to tap on the upload button for the specific Duke Person on the Detail Information Page. The app also allows user to delete data entry from local database, by swiping on the main table view.
Search Data Persistence Animations
Search Persistence Animations
With fuzzy search support, you could type in any part of a Duke Person's name and find him in the table easily. Data will be stored locally within a json file, and will be persisted between sessions. According to the requirements, several custom animations were added to the app to corresponding entries. For Omnimoji group, we chose one from each guy's favorite activities, and made customized animations with CoreAnimation framework.

Videos are created with Screenshot macOS app and compressed with FFmpeg.

ffmpeg -i <input>.mov -vf scale=640:-1 <output>.mov

Designs and highlights

FuzzySearch search bar

Fuzzy Search result with T as input Fuzzy Search result with Tel as input
fuzzy-1 fuzzy-2

The search bar above the main table supports fuzzy and full text search for names of Duke Person. When user typing in characters, the search result list will be instantly updated to match his input. As the examples shown above, if the user type T into the search bar, it will show two corresponding entries whose name contains a T. When a more specific input is given, as it is shown on the right (e.g. Tel), the search result will be more accurate. With this search bar, user can locate an entry more efficiently.

The search bar is powered by a query method inplemented within the database object. The method will first check if the input string contains a whitespace. If it does, then it will search with a first name + last name fashion. Else it will make a best guess by trying to find any matches in the name field.

AvatarController

permission control

The app embeded in a UIImagePickerController to handle user's avatar choices. It provides two features: select from photo album, or take a picture with the camera.

For the first time that user invoke the image picker, it will request camera and photo album permissions. If the user does not allow those permissions, everytime the image picker presents, it will show a prompt to let user change the permissions in settings.

For taking pictures, in order to avoid accessing camera in a Simulator, which would cause it to crash, the following snippet is used to check if camera is available.

let cameraType = UIImagePickerController.SourceType.camera
let cameraAvailable = UIImagePickerController.isSourceTypeAvailable(cameraType)
if (!cameraAvailable) {
    let alert = UIAlertController(title: "No camera", message: "Maybe you are testing with Simulator. Try with a real device!", preferredStyle: UIAlertController.Style.alert)
    let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
    alert.addAction(okAction)
    self.present(alert, animated: true, completion: nil)
    return
}

Also, user can pinch zoom to select a square region of their preferred avatar. The avatar will then be compressed and encoded in to base64 format, so that it can be persisted together with other fields for a Duke Person in the JSON local database. More details will be covered later in sections below.

FetchAPI/PostAPI with URLSession

For the homework requirements, this app supports both uploading a single entry to the remote server and downloading the full database from server. Since the test server is shut down, the app provides default fallback results when there is no server connection.

The main snippet of fetching the database from server is shown below. Refer to doc here.

let decoder = JSONDecoder()
var serverPeople = codableDB()
let url = URL(string: "http://ece564.colab.duke.edu:5000/get")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
        self.handleClientError(error)
        return
    }
    guard let httpResponse = response as? HTTPURLResponse,
        (200...299).contains(httpResponse.statusCode) else {
        self.handleServerError(response)
        return
    }
    if let mimeType = httpResponse.mimeType, mimeType == "text/html",
        let data = data {
        DispatchQueue.main.async {
            do {
                // failable decode
                let decoded = try decoder.decode([FailableDecodable<codableDukePerson>].self, from: data).compactMap { $0.base }
                // convert object
                serverPeople = serverDBToCodableDB(myServerDB: decoded)
                // persist data
                appDB.loadCodableDataToDB(codableDBInstance: serverPeople)
                let tempCodableDB = appDB.convertDBToCodable()
                if !saveDBToDisk(codableDBInstance: tempCodableDB) {
                    print("Error: save to disk error in TableViewController.swift when trying to save fetched data")
                }
                UIView.transition(with: self.tableView, duration: 0.5, options: .transitionCrossDissolve, animations: {self.tableView.reloadData()}, completion: nil)
            }
            catch let jsonError {
                print("Error: decode error!\n\(jsonError)")
            }
        }
    }
}
task.resume()

For postAPI, the process is similar, and you can check the

func postPersonDataToServer(person: codableDukePerson) -> Bool

in utils.swift.

Both get and post requests are implemented within Swift native APIs, which get rid of the overload of 3rd-party libraries such as Alamofire.

Default content

When the app is launched, rather than using a spinner/progress indicator to let users to wait for fetching data from server, the app provides default data to display in the table.

If the user has never connected to the server, then a dummy local fallback database will be loaded, with a few placeholder data. If the user has connected to the server, then the local database should have been updated, hence the app will load the database from last session.

After the database is downloaded, the table is updated from a background thread, which causes minimum disturbance of user experience.

Sanity check of input

sanity check

For both Create and Update of the database, essential sanity check of user input should be enforced.

In the Add/Update ViewController, the sanity check for each field is taken into consideration. When any of the required fields are missing, the background color of that text field will be fade into light yellow, which informs the user to fill in corresponding fields.

Furthermore, when the input strings are too long, or mal-formatted, or not in the proper format (e.g. wrong delimiter, whitespace, etc.), the Controller layer in MVC model will first check if it can handle it by removing whitespaces, change to lower case, auto-completion, etc. If it cannot, the View will have the same yellow highlighted fields, to let user fix the problem.

Finally, a debug output is shown in the page for easier debugging. This is due to my unfamiliarity of Swift programming at that time. I want to check the output in Model layer to ensure my backend manipulation is correct.

Data persistence

By default, an application does not persist data between sessions(i.e. user settings and data will be lost the next time when the app launches.). In order to persist useful data, there are many methods, including UserDefaults key-value pairs, CoreData framework and many other options such as Realm or fmdb.

In this app, I used JSONEncoder to persist data and stored it to document directory of the app.

Codable protocol

In Swift 4, Apple introduced the Codable protocol for built-in objects. By conforming to this protocol, a Struct or Class object can be easily encoded into a JSON file, which latter can be saved to documents directory of the app.

A simple example of a Codable object is shown below. As you can see, most of the basic data types are supported by this protocol.

class codableDukePerson : NSObject, Codable {
    var uid: Int?
    var firstname: String = "First"
    var lastname: String = "Last"
    var wherefrom: String = "Anywhere"
    var gender: Bool = true  // enum converted
    var degree: String = "NA"
    var hobbies: [String] = []
    var languages: [String] = []
    var role: String = "Student"  // enum converted
    var pic: String? = ""  // base64 converted
    var team: String?

    // ... methods are omitted
}

By integrating this protocol with JSONEncoder, one can serialize an object and persist it on local disk with minimum effort. Instead of using NSKeyedArchiver, this protocol has a better readability for the serialized JSON file, and is easier to collaborate with other APIs and programs, as JSON file is a universal plain-text exchangeable format. Modern languages including C#, Python, MATLAB and JavaScript all have mature support for JSON files.

Failable decoder

Another highlight is the failable JSONDecoder, which allows the app to decode mal-formatted server database, and recover usable entries with best efforts.

When testing the app in class, one group uploaded several mal-formatted entries, which does not abide by the JSON format below mentioned @169.

{
    uid: Int?,
    firstname: String,
    lastname: String,
    wherefrom: String,
    gender: Bool,
    role: String,
    degree: String,
    team: String,
    hobbies: [String],
    languages: [String],
    pic: String
}

Whereas people can always treat the JSON file as a plain-text file and parse it, it would be better to just parse those valid entries and omit the invalid's.

By adapting this tutorial, I implemented a failable decoder to handle such case.

struct FailableDecodable<Base : Decodable> : Decodable {
    
    let base: Base?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.base = try? container.decode(Base.self)
        if self.base == nil {
            print("Warning: sanity check failed for certain entry. Please check if it conform to our data model @169.")
        }
    }
}

// decode data from `URLSession.shared.dataTask`
let decoder = JSONDecoder()
let decoded = try decoder.decode([FailableDecodable<codableDukePerson>].self, from: data).compactMap { $0.base }

In this way, each received entry will first go through the failable decoder, where those invalid entries will be nullified and discarded in the mapping step.

JSON

func loadDBFromDisk() -> codableDB? {
    let decoder = JSONDecoder()
    var people = codableDB()
    let tempData: Data
    
    do {
        tempData = try Data(contentsOf: peopleDB.ArchiveURL)
    }
    catch let error as NSError {
        print(error)  // no such file error
        return nil
    }
    catch {
        print("Error: load DB from disk. Unknown type.")
        return nil
    }
    if let decoded = try? decoder.decode(codableDB.self, from: tempData) {
        people = decoded
    }
    return people
}

With a couple of lines of code, the app can deal with JSON decode and encode processes.

Animations

animation

In the requirements, Prof. Telford asked us to create animations with CoreAnimation framework. I created a scene with multiple image layers and rasterized graphs and made them animated.

Below are a few helper functions to animate the CALayers and UIViews. Please refer to CyclingViewController.swift for the full code.

// rotate a layer infinitely
func layerRotate360(layer: CALayer, duration: Double) {
    let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
    rotateAnimation.fromValue = 0.0
    rotateAnimation.toValue = CGFloat(Double.pi * 2)
    rotateAnimation.isRemovedOnCompletion = false
    rotateAnimation.duration = duration
    rotateAnimation.repeatCount = Float.infinity
    layer.add(rotateAnimation, forKey: "rotateAnimation")
}

// loop scaling a layer infinitely
func layerZoomInOut(layer: CALayer, inScale: Float, outScale: Float, duration: Double) {
    let zoomAnimation1 = CABasicAnimation(keyPath: "transform.scale.x")
    zoomAnimation1.fromValue = inScale
    zoomAnimation1.toValue = outScale
    zoomAnimation1.autoreverses = true
    zoomAnimation1.repeatCount = Float.infinity
    zoomAnimation1.duration = duration
    let zoomAnimation2 = CABasicAnimation(keyPath: "transform.scale.y")
    zoomAnimation2.fromValue = inScale
    zoomAnimation2.toValue = outScale
    zoomAnimation2.autoreverses = true
    zoomAnimation2.repeatCount = Float.infinity
    zoomAnimation2.duration = duration
    layer.add(zoomAnimation1, forKey: "scaleXAnimation")
    layer.add(zoomAnimation2, forKey: "scaleYAnimation")
}

With all the default animations and a few calculated position coordinates, the animations can generate a pleasing visual effect. Please check the video above in Usage section for a better notion.

Detail Info Page layout

According to the requirements, the Add/Update page should use manual layout, rather than .xib autolayout or autoresizing. In order to avoid repetitive work, I used a loop to layout all the labels and text fields on the page, and hardcoded the other views' positions according to UIScreen dimensions.

Progress notes

Please refer to ๐Ÿ‘‰this document๐Ÿ‘ˆ to see the evolution of the app.

Contributing

Contributions are very welcome ๐Ÿ™Œ.

According to the discussion with Prof. Telford, a weekly based template project might be created, so that students could refer to the template project after they submit their homework, to ensure they can obtain credits even if they cannot fulfill the requirements of previous step. Also, the progress can be ensured with everyone in class with same context.

Installation

Manual

You can download and compile DukePerson app manually. Please make sure your Xcode versions >= 9.4.1 and build tool >= Swift 4.1.

Credits

The template for this description is taken from SwiftKit.

License

DukePerson app
Copyright ยฉ Ting Chen 2018
All rights reserved. 

last revision: 190613