
ECE564-iOS-DukePersonApp
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
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 |
---|---|
![]() |
![]() |
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
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.
URLSession
FetchAPI/PostAPI with 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
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
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 CALayer
s and UIView
s. 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
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