Recreating the iOS 12.2 Wallet table view

UPDATE: This feature is now built-in starting with iOS 13 via the insetGrouped table view style!


The Wallet app was redesigned in iOS 12.2 with a fresher, more rounded look. This is most prominent in the table views scattered throughout the app — sections are rounded at the edges, similar how a detail table view looks on iPad. It looks really good everywhere, though, so let’s try to recreate it!

(TL;DR: Here’s the code.)

The iOS 12.2 Wallet app has a fresher, more rounded look.

I’ll be using my app SuperHomework for this, but of course the code should work on any iOS app. I’m also using the iOS 12.2 beta (it should work on any iOS 12.x release, though) and Swift 4.2, the latest versions at the time of writing. I should also note that part of this implementation was taken from this answer and part of this answer on Stack Overflow.

Let’s get started! The first thing you’ll notice is how the section insets are increased in the Wallet app. I was experiencing issues setting UITableView.contentInset (which you would most likely want to do if you can get it working), so instead I just applied constraints to the left and right edges of the table view using SnapKit and set the background color of the main view to that of the table view, giving a seamless look.

self.tableView.snp.remakeConstraints { make in
make.top.bottom.equalToSuperview()
make.left.right.equalToSuperview().inset(16)
}
self.view.backgroundColor = self.tableView.backgroundColor
view raw block.swift hosted with ❤ by GitHub

Next, I created a UITableView extension to make the following implementation easier to apply to many different table views.

extension UITableView {
func useRoundedSectionCorners() {
// This will be called in `viewDidLoad()` to set up the table view
}
func display(withRoundedSectionCorners cell: UITableViewCell, at indexPath: IndexPath) {
// This will be called in `tableView(_:willDisplay:forRowAt:)` to render each cell
}
}
view raw block.swift hosted with ❤ by GitHub

Inside the useRoundedSectionCorners method, I disabled the default separator line that runs through all of the cells and adds a section border — we’ll write our own separator in a bit. I also removed all of the excess padding around the edges of the table view that section headers/footers use to indent themselves.

func useRoundedSectionCorners() {
self.separatorStyle = .none
self.separatorInset = UIEdgeInsets(top: 0, left: self.separatorInset.left, bottom: 0, right: 0)
}
view raw block.swift hosted with ❤ by GitHub

Now let’s jump into display(withRoundedSectionCorners:at:). The first thing we need to do is determine which modifications to make on the cell — that is, we don’t want to round the corners of a cell that’s in the middle of a section and whatnot. We can do that by passing the cell’s indexPath to the method and doing some math:

func display(withRoundedSectionCorners cell: UITableViewCell, at indexPath: IndexPath) {
// Determine what modifications to make
let numberOfRowsInSection = self.numberOfRows(inSection: indexPath.section)
var shouldRoundTop = false
var shouldRoundBottom = false
if indexPath.row == 0 && indexPath.row == numberOfRowsInSection – 1 {
// the cell is the only one in the section
shouldRoundTop = true
shouldRoundBottom = true
} else if indexPath.row == 0 {
// the cell is the first in the section
shouldRoundTop = true
} else if indexPath.row == numberOfRowsInSection – 1 {
// the cell is the last in the section
shouldRoundBottom = true
}
}
view raw block.swift hosted with ❤ by GitHub

Next, we’ll round the corners of the cell based on the calculations we just did. This is achieved using a UIBezierPath with our desired corner radius (12pt in this case to match the Wallet app).

func display(withRoundedSectionCorners cell: UITableViewCell, at indexPath: IndexPath) {
// Determine what modifications to make
// …
// Round corners if applicable
if shouldRoundTop && shouldRoundBottom {
cell.layer.cornerRadius = 10
cell.layer.masksToBounds = true
} else if shouldRoundTop || shouldRoundBottom {
let shape = CAShapeLayer()
let rect = CGRect(x: 0, y: 0, width: cell.bounds.width, height: cell.bounds.size.height)
let corners: UIRectCorner = shouldRoundTop ? [.topLeft, .topRight] : [.bottomRight, .bottomLeft]
shape.path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: 12, height: 12)).cgPath
cell.layer.mask = shape
cell.layer.masksToBounds = true
}
}
view raw block.swift hosted with ❤ by GitHub

Finally, we need to draw our own separator line for cells that are in the middle of a section, if the section has multiple rows.

func display(withRoundedSectionCorners cell: UITableViewCell, at indexPath: IndexPath) {
// Determine what modifications to make
// …
// Round corners if applicable
// …
// Show separator if applicable
if numberOfRowsInSection > 1 && indexPath.row < numberOfRowsInSection – 1 {
let bottomBorder = CALayer()
bottomBorder.frame = CGRect(x: self.separatorInset.left, y: cell.bounds.maxY – 0.3, width: cell.contentView.frame.size.width, height: 0.3)
bottomBorder.backgroundColor = self.separatorColor?.cgColor
cell.contentView.layer.addSublayer(bottomBorder)
}
}
view raw block.swift hosted with ❤ by GitHub

Now we’re all set to implement this in our own table view! We can do so by calling useRoundedSectionCorners()on our table view in our view controller’s viewDidLoad() and by calling display(withRoundedSectionCorners:at:) inside our table view delegate’s tableView(_:willDisplay:forRowAt:) method, like so:

override func viewDidLoad() {
super.viewDidLoad()
// Add padding to the left and right sides
self.tableView.snp.remakeConstraints { make in
make.top.bottom.equalToSuperview()
make.left.right.equalToSuperview().inset(16)
}
self.view.backgroundColor = self.tableView.backgroundColor
// Enable rounded section corners
self.tableView.useRoundedSectionCorners()
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
tableView.display(withRoundedSectionCorners: cell, at: indexPath)
}
view raw block.swift hosted with ❤ by GitHub

…and here’s our final result!

Our recreation of the Wallet app’s table view as shown in SuperHomework.

Thanks for reading! If you want to try this out in your app, download the full code here.

Throwable initializers in Swift

Intended for students, my app SuperHomework deals a lot with assignments in the form of JSON. Because Swift is a type-safe programming language, though, I had to convert every form of JSON object sent from the backend into its own type. My initial implementation was something along the lines of this:

class Assignment {
init(json: [String: Any]) {
self.title = json["title"] as! String
self.icon = AssignmentIcon(rawValue: json["icon"] as! String)!
if let dueDateString = json["due_date"] as? String {
self.dueDate = try Date(isoString: dueDateString)
}
}
}
view raw block.swift hosted with ❤ by GitHub

So how can I make this better? A first approach could be the following: https://gist.github.com/Wilsonator5000/ee2ed9659fd635f42e69b5f6f82f4b5b

…but this is repetitive, and it means that I have to make my instance types implicitly unwrapped optionals for the initializer to work — yuck. (And the app could still crash later on, too!) In addition, there’s no simple way to alert the user what exactly went wrong.

What can I do to fix this? The answer: throwable initializers.



Swift 2 introduced the try/catch error handling method we all know and love from other popular programming languages like C++, Java, Python, and so on. It’s easy to set up — wrap the throwing code inside a do block, bolt on try to the front, and catch the error. https://gist.github.com/Wilsonator5000/95157d251d1f789dbc779fb728bd7375

You’ll notice that if randomNumber returns an even number, the do block exits and "Error: EvenNumberError" will be printed to the console. Otherwise, the program will continue inside the do block and eventually you’ll receive "Hooray, no errors!".

So how can we apply this principle to initializers that could fail? Unfortunately Swift’s builtin types use failable initializers (initializers that return nil when they cannot create the object) which can be cumbersome to use at times and kind of undermine the point of try/catch, so let’s start by improving some built-in types using throwable initializers.

Creating our own throwable initializers

In order to make things a bit easier, I created a generic InitializableFromInput protocol that will take care of the heavy lifting for me, particularly for handling optional inputs.

protocol InitializableFromInput {
associatedtype InputType
static func fromInput(_ input: InputType) throws -> Self
}
extension InitializableFromInput {
static func fromInput(_ input: InputType?) throws -> Self {
guard input != nil else {
throw InvalidInputError<()>(for: nil)
}
return try Self.fromInput(input!)
}
}
view raw block.swift hosted with ❤ by GitHub
…and here’s the code for the InvalidInputError type that I used above — it’s pretty self-explanatory, and we’ll use it a lot when implementing the non-optional-input initializer:
struct InvalidInputError<InputType>: Error {
let input: InputType?
init(for input: InputType?) {
self.input = input
}
var message: String? {
if let input = self.input {
return "Invalid input \(input)"
} else {
return "Input was nil"
}
}
}
view raw block.swift hosted with ❤ by GitHub
Now we can begin to implement throwable initializers for a few built-in types. Let’s start with Int. The Int type has an initializer that accepts a string, but returns nil when the string does not contain a valid integer. Instead of returning an optional, let’s create a throwable initializer by adopting the InitializableFromInput type.
extension Int: InitializableFromInput {
typealias InputType = String
static func fromInput(_ input: InputType) throws -> Int {
guard let result = self.init(input) else {
throw InvalidInputError(for: input)
}
return result
}
}
view raw block.swift hosted with ❤ by GitHub
We can extend this idea to various other types, like URL, Date, and so on:
extension URL: InitializableFromInput {
typealias InputType = String
static func fromInput(_ input: InputType) throws -> URL {
guard let result = self.init(string: input) else {
throw InvalidInputError(for: input)
}
return result
}
}
extension Date: InitializableFromInput {
typealias InputType = String
static func fromInput(_ input: InputType) throws -> Date {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = .current
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let date = dateFormatter.date(from: input) else {
throw InvalidInputError(for: input)
}
return self.init(timeInterval: 0, since: date)
}
}
view raw block.swift hosted with ❤ by GitHub

Dealing with JSON

To make it easier to deal with JSON like I mentioned with Assignment at the beginning, I created a quick little Dictionary extension that adopts throwable initializers. If the key does not exist in the dictionary or is not of the correct type, the initializer throws a KeyError.
extension Dictionary {
struct KeyError<Key, DesiredMetatype>: Error {
let key: Key
private let desiredMetatype: DesiredMetatype
fileprivate init(key: Key, desiredMetatype: DesiredMetatype) {
self.key = key
self.desiredMetatype = desiredMetatype
}
var message: String {
return "Key \"\(self.key)\" not found in JSON object or not of type \(self.desiredMetatype)"
}
}
/// Gets the value from the specified key and throws `KeyError` if it
/// doesn't exist or is not of the correct type.
func get<T>(_ key: Key) throws -> T {
guard let value = self[key] as? T else {
throw KeyError(key: key, desiredMetatype: T.self)
}
return value
}
/// Gets an optional value while still ensuring it is of the right type
/// if it exists.
func getOptional<T>(_ key: Key) throws -> T? {
if let untypedValue = self[key] {
guard let typedValue = untypedValue as? T else {
throw KeyError(key: key, desiredMetatype: T.self)
}
return typedValue
}
return nil
}
}
view raw block.swift hosted with ❤ by GitHub

Continuing on

Now that we have implemented throwable initializers throughout our lower-level types, we can extend their use to our larger data types. Finally we can get rid of our force-wraps or implicitly-unwrapped optionals and replace it with nice, clean, type-inferenced code!
class Assignment {
var title: String
var icon: AssignmentIcon
var dueDate: Date?
init(json: [String: Any]) throws {
self.title = try json.get("title")
self.icon = try AssignmentIcon.fromInput(try json.get("icon"))
if let dueDateString: String = try json.getOptional("due_date") {
self.dueDate = try Date(isoString: dueDateString)
}
}
}
view raw block.swift hosted with ❤ by GitHub

Wrapping up

In this guide, we learned how to apply the idea of throwable initializers to our code and make it more concise and type-inferenced. We created a generic protocol called InitializableFromInput so our implementation works with any type, and extended Swift’s built-in types like URL, Int and any RawRepresentable enum in addition to our own higher-level data types to make our lives easier.

Extending knowledge: throwable casting

To take it to another level, we can even go so far as to replace as? and as! with our own throwable casting function. This can be useful in a number of cases; try to think of your own!
public struct CastError<T, U>: Error {
public var obj: T
public var type: U.Type
public init(from obj: T, to type: U.Type) {
self.obj = obj
self.type = type
}
public var message: String {
return "Error casting \(self.obj) to type \(self.type)"
}
}
/// Usage:
///
/// let bar: Bar = …
/// let foo: Foo = cast(bar)
///
/// let fooTypeInference = cast(bar, to: Foo.self)
public func cast<T, U>(_ obj: T, to type: U.Type = U.self) throws -> U {
guard let castedObj = obj as? U else {
throw CastError(from: obj, to: type)
}
return castedObj
}
view raw block.swift hosted with ❤ by GitHub

Footnote: Limitations of approach

There are a few current limitations of my implementation of throwable initializers; if you have a better way, please let me know!
  • You can only specify one InputType for the protocol and implement only one fromInput(_:) initializer.
  • Technically fromInput(_:) isn’t actually an initializer — rather, a static method that returns an instance of its member type. This is due to the fact that Swift doesn’t allow the delegation of failable initializers (ie. ones that return an optional) to non-failable initializers (in our case, the ones that throw InvalidInputError).

Thanks for reading! If you enjoyed the article, make sure to share it. If you want to try out my latest app SuperHomework, you can do so by following the link here or learning more at the official website.

Delegation in Swift, explained yet again

Here’s my take at delegation in Swift!


When you’re writing an iOS app, you have to deal with view controllers that manage the app’s user interface and respond to events (such as tapping a UIButton or entering text in a UITextField). But sometimes, you need to respond to events outside of the current view controller — for example, in a to-do list app, you want the table view that displays the list to update said list when the user adds a new item. If you want to easily allow options or details the be added to an item during its creation, you’ll probably have two separate view controllers: one for the entire list, and one for item creation.

But how would you pass the information between the two view controllers? Well, you could use a global or shared variable:


var _newItem: ToDoItem?
class ItemsTableViewController: UITableViewController {
@IBAction func addButtonTapped(_ sender: Any) {
let vc = CreateItemViewController()
self.present(vc, animated: true, completion: nil)
}
}
class CreateItemViewController: UIViewController {
// … Setup for item creation … //
@IBAction func confirmItemButtonTapped(_ sender: Any) {
// … Pull all the values from the fields and create the ToDoItem … //
_newItem = createdItem
self.dismiss(animated: true)
}
}

view raw

block1.swift

hosted with ❤ by GitHub

… but this is messy, and the ItemsTableViewController would still have to check for the value of the variable every so often manually!

There’s a better way. Let’s dive in.

Think of delegates like “subscribers” or “slaves”

When you create a delegate, you typically use it in one of two ways: to have a class respond to the events of another class (usually a view controller responding to the events of another view controller or an asynchronous helper class) or perform an action for another class (like a view controller showing an alert when the Internet connection goes down). We will discuss both ways here.

Delegates as Subscribers

If you want a class to be notified when something happens, you can use delegation. In our example, the “delegate” will be our ItemsTableViewController.

The first thing we need to do is define a set of functions that the delegate must follow. This is to ensure that the delegator (the class sending the events) is able to call certain defined functions from that delegate. We can do this with a protocol:


protocol ToDoItemEventsDelegate {
func toDoItemAdded(_ item: ToDoItem)
}

view raw

block2.swift

hosted with ❤ by GitHub

Notice that we do not add code to the function — that task is up to the class that uses the protocol.

Now, any class that conforms to that protocol must also include its methods:


class ItemsTableViewController: UITableViewController, ToDoItemEventsDelegate {
func toDoItemAdded(_ item: ToDoItem) {
// This is where we add our code.
}
}

view raw

block3.swift

hosted with ❤ by GitHub

Now that our view controller conforms to the protocol, the delegator knows that the view controller is a valid class to send actions to.

We’ll fill out the above function in a minute, but first, let’s create our delegator (in this case, the CreateItemViewController):


class CreateItemViewController: UIViewController {
var delegate: ToDoItemEventsDelegate? // 1
@IBAction func confirmItemButtonTapped(_ sender: Any) {
// … Pull all the values from the fields and create the ToDoItem … //
self.delegate?.toDoItemAdded(createdItem) // 2
}
}

view raw

block4.swift

hosted with ❤ by GitHub

  1. Any class that wants to become this view controller’s delegate must assign itself to the delegate object. Notice that the object is of type ToDoItemEventsDelegate — this means that only classes that conform to ToDoItemEventsDelegate are allowed to assign themselves to the object. Also, the object is optional because having a delegate is not required, only if it is assigned.
  2. To send an event to the delegate, you can just call the function from the delegate object (if there is one — notice the optional binding). How do we know that the function exists within the delegate object? Because we know that the delegate object must conform to the protocol we made, which includes that function.

Now when a new item is added, the ItemsTableViewController’s toDoItemAdded(_:) function will be called and any code within it will be executed:


class ItemsTableViewController: UITableViewController, ToDoItemEventsDelegate {
func toDoItemAdded(_ item: ToDoItem) {
self.items.append(item)
self.tableView.reloadData()
}
}

view raw

block5.swift

hosted with ❤ by GitHub

In Practice

You already have a delegate in your iOS project right now — the AppDelegate. If you open it up (under AppDelegate.swift), you can see that it conforms to UIApplicationDelegate and contains a few functions that respond to events from iOS, like your app opening, closing, going in the background, or receiving a notification.

Delegates as Slaves

You could also think of delegates as slaves, performing the action for another class that is unable to do so. For example, you could have a helper class that detects the status of the Internet connection:


class InternetConnectionHelper { }

view raw

block6.swift

hosted with ❤ by GitHub

If you wanted to display an alert when the Internet connection was lost, you could use a delegate to show the alert on behalf of the helper class:


protocol InternetConnectionStatusResponder {
func statusChanged(_ isConnected: Bool)
}
class InternetConnectionHelper {
// We want to make this a singleton class, so that there is only ever one delegate
static var shared = InternetConnectionHelper()
private init() { }
var delegate: InternetConnectionStatusResponder?
// I did not include code for detecting
//the connection status for brevity,
// but here you would implement this
}
class ViewController: UIViewController, InternetConnectionStatusResponder {
override func viewDidLoad() {
super.viewDidLoad()
InternetConnectionHelper.shared.delegate = self // Assign the delegate to the view controller
}
// Delegate function
func statusChanged(_ isConnected: Bool) {
// Display alert with new status, or update your views accordingly
}
}

view raw

block7.swift

hosted with ❤ by GitHub

Now whenever your InternetConnectionHelper detects a change in the status, it will call the view controller’s statusChanged(_:) function and the view controller can display an alert or update its views accordingly.

But Wait! What if I want more than one delegate at a time?

Delegates are assigned per instance, which means that every instance of the delegator can have its own delegate (that is, this code is OK):


let delegator1 = MyDelegator()
delegator1.delegate = self
let delegator2 = MyDelegator()
delegator2.delegate = self
let delegator3 = MyDelegator()
delegator3.delegate = self

view raw

block8.swift

hosted with ❤ by GitHub

In a singleton class, you could have an array of delegates.


// Creates an empty array of delegates
var delegates = [MyDelegate]()

view raw

block9.swift

hosted with ❤ by GitHub

When a class wants to become a delegate for the singleton, it can just append itself to the array:


MySingletonDelegator.shared.delegates.append(self)

view raw

block10.swift

hosted with ❤ by GitHub

And if you want the delegator to send an event to its delegates, just iterate over the array:


func sendEventToDelegates() {
for delegate in self.delegates {
delegate.doSomething(with: something)
}
}

view raw

block11.swift

hosted with ❤ by GitHub

Conclusion

So that’s how delegation works in Swift! All you really need is:

  1. A protocol that describes what functions the delegate class should follow. For a class to even have a chance of becoming a certain delegate, it must conform to that protocol.
  2. A delegator class that other classes can assign themselves as delegates to. The delegator is also responsible for sending the events to its delegate(s).
  3. A delegate class(es) that assigns themselves as the delegator’s delegate. As long as it conforms to the delegate protocol specified, it can receive events and perform actions on behalf of the delegator.

Now that you know how delegates work, you’ll also understand why, for example, you can have a view controller conform to UITableViewDelegate, implement the methods, and have them “magically called” in the background — in fact, they are called by iOS UIKit delegators.

Happy coding!

Introducing TPSHomeworkBot

 

TPSHomeworkBot, a solution that unifies where students get their homework, will be launching at Tyngsborough Middle School on Monday, October 16. Here’s what you need to know.

How it works

TPSHomeworkBot started as an idea over the last few days counting down to the start of Eighth Grade, while I was trying to find a better way to keep track of my homework. Agenda books were big and clunky and never worked for me, and I had tried using services like Trello or a calendar, but those didn’t work either. I had always had an idea of using my own, custom-built service in the back of my mind, and early versions of a “homework app” had never been automatic or efficient and slowly died away. I needed something that would just work every single day, but to do that I needed to build it from scratch. So I sat down at the Linux terminal on my Raspberry Pi and got to work.

TPSHomeworkBot runs off a Raspberry Pi Zero that's on 24/7.
TPSHomeworkBot runs off a Raspberry Pi Zero that’s on 24/7.

The program is written in Python, a flexible and easy-to-use programming language that comes preinstalled on the Pi. The Pi’s default OS (“Raspbian”) is configured to run the program every day at around 2 PM.

My plan for TPSHomeworkBot was to have it automatically create a PDF of the teacher’s blogs (so that one could print out their homework agenda when they got home) and additionally create grade-based summaries for the assignments (for example, “8th grade has homework in Math and History tonight” or “6th grade has no homework tonight”). All of this information would be posted on Twitter under the account @TPSHomeworkBot for the PDFs, and @TPSHomeworkBot6, @TPSHomeworkBot7, and @TPSHomeworkBot8 for the summary.

So I’m all set up, and on the first day of school I talk to my principal, Mr. Pollet, about the idea. He was happy to help me get it out to students, and we set off to find information about the TMS teachers and their blogs for use in TPSHomeworkBot.

My further plans for TPSHomeworkBot, drafted out on my whiteboard.

Mr. Pollet's and my initial spreadsheet for various teacher's blogs and information.
Mr. Pollet’s and my initial spreadsheet for various teacher’s blogs and information.

Once I had this information I was able to add it into my program and create both the PDF and the summary accounts.

Now it was off to testing! Here are two examples of a PDF post and a summary post:

https://twitter.com/TPSHomeworkBot7/status/918914830890950657

What this means for students

TPSHomeworkBot should be happily welcomed by students, as it means that they don’t have to keep track of homework by writing it down in a $4 agenda book they carry around everywhere — instead they get a notification on a device they already own. They don’t have to install anything, either, as TPSHomeworkBot is entirely Twitter-based, and setup is as easy as following the account(s)!

The only thing students have to do is scan the Twitter QR code with their phone to follow the account. Here's a poster that will be hung around the school.
The only thing students have to do is scan the Twitter QR code with their phone to follow the account. Here’s a poster that will be hung around the school.

What this means for parents

While some parents already opt-in to some teachers’ Remind101 service, this does not cover all the assignments that their child is given. TPSHomeworkBot pulls directly from teachers’ blogs, which most post daily on, which means that any assignment the teacher uploads will be posted on the Twitter feed. Now parents will be able to keep track of their child’s work using the same system as their child. And since every teacher at TMS uses a blog, the service is unified and only requires one platform — one that most parents and students already use.

Stay tuned for Monday!

TPSHomeworkBot will be launched on Monday and should have a great impact on our school and the way students stay informed of their assignments. In a few weeks, I will post again about the progress and usage of TPSHomeworkBot. If you have any suggestions, feel free to contact me on Twitter at @wgramer03. Until then, stay tuned!

Design a site like this with WordPress.com
Get started