Writing: Tutorials, Blogs, and Cheatsheets
SwiftUI tips and tricks: 2021
I was given a coding challenge recently and I decided to code it in SwiftUI in spite of only having dipped my toe in to that technology as of yet. The challenge was to retrieve NYC high school information into a view from a web API url, then allow users to get details such as SAT scores from another url and display those in a detail view. Even though this would be a good situation to use Combine, I chose not to for simplicity, and also because I don't have a good grasp of publishers and sinks just yet. I also chose to organize the architecture using the MVVM pattern since I've been exploring that recently, and it seems to go very well with SwiftUI, which is pretty strict about keeping View classes pure and simple. The challenge gave extra points for writing tests, so I also did some of that in spite of being relatively inexperienced.
I made two model structs: School, and SATDetails. The best practice is to use camelCase names. The JSON comes in snake_case, but you can tell the decoder to translate.
struct School: Decodable {
let dbn: String
let schoolName: String
let location: String
let boro: String
// this will arrive later, so the Decoder needs it to be an Optional
var satData: SATData?
}
struct SATData: Decodable {
let dbn: String
let schoolName: String
let numOfSatTestTakers: String
let satCriticalReadingAvgScore: String
let satMathAvgScore: String
let satWritingAvgScore: String
}
To start out, I grabbed the school data and put it in a file in the bundle.
Here's one of the decoding tests. The other one is very straightforward.
func testSchoolDecoding() {
let bundle = Bundle(for: type(of: self))
if let url = bundle.url(forResource: "TestData", withExtension: "json") {
if let data = try? Data(contentsOf: url) {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
do {
let schools = try decoder.decode([School].self, from: data)
XCTAssertEqual(schools[0].schoolName, "Clinton School Writers & Artists, M.S. 260")
}
catch let error {
print(error)
XCTFail()
}
}
else {
XCTFail()
}
}
else {
XCTFail()
}
}
I thought it would be cool (and easy) to show a map on the detail page. Even though the latitude and longitude data was spotty, there seemed to be a String with lattitude and longitude appended to the end of the location which started with the address, so I did a little parsing and put the results in a view model.
import SwiftUI
import MapKit
class SchoolDetailsViewModel {
var school: School
var address: String
var longitude: Double
var latitude: Double
@State var region: MKCoordinateRegion
init(school: School) {
self.school = school
let index = school.location.lastIndex(of: "(")
address = String(school.location[.. = ["(", ")", " "]
gpsLocation.removeAll(where: { parensAndSpace.contains($0) })
let coords = gpsLocation.split(separator: ",")
latitude = Double(coords[0])!
longitude = Double(coords[1])!
region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude) , span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
}
}
And here's a test for that:
func testSchoolViewModelPopulation() {
let viewModel = SchoolViewModel(school: School(dbn: "dbn", schoolName: "Prep", location: "London (51.50007773, -0.1246402)", boro: "M"
))
XCTAssertEqual(viewModel.latitude, 51.50007773)
XCTAssertEqual(viewModel.address, "London ")
}
I put networking in it's own class, keeping it out of the view models. I found a sweet URLSession test example on Hacking with Swift, one of my favorite resources.
class URLProtocolStub: URLProtocol {
// this dictionary maps URLs to test data
static var testURLs = [URL?: Data]()
// say we want to handle all types of request
override class func canInit(with request: URLRequest) -> Bool {
return true
}
// ignore this method; just send back what we were given
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
print("startLoading()")
// if we have a valid URL…
if let url = request.url {
// …and if we have test data for that URL…
if let data = URLProtocolStub.testURLs[url] {
// …load it immediately.
self.client?.urlProtocol(self, didLoad: data)
}
}
// mark that we've finished
self.client?.urlProtocolDidFinishLoading(self)
}
// this method is required but doesn't need to do anything
override func stopLoading() { }
}
Here's how I used that:
func testGetSATs() {
let url = URL(string: "https://data.cityofnewyork.us/resource/f9bf-2cp4.json")
let dataString = #"[{"dbn":"01M292","school_name":"HENRY STREET SCHOOL FOR INTERNATIONAL STUDIES","num_of_sat_test_takers":"29","sat_critical_reading_avg_score":"355","sat_math_avg_score":"404","sat_writing_avg_score":"363"}]"#
URLProtocolStub.testURLs = [url: Data(dataString.utf8)]
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [URLProtocolStub.self]
let session = URLSession(configuration: config)
Networking.getSATs(session: session) {
data, response, error in
print("test completion handler called")
if let data = data
{
print("test completion handler has valid data")
let str = String(decoding: data, as: UTF8.self)
XCTAssertEqual(dataString, str)
}
else {
print(error)
XCTFail()
}
}
}
Here's one of two nearly identical calls in my Networing class:
class Networking {
static let nycAppToken = "************************"
static func getSchools(session: URLSession, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
print("getSchools")
if let url = URL(string: "https://data.cityofnewyork.us/resource/s3k6-pzi2.json") {
var request = URLRequest(url: url)
request.addValue(nycAppToken, forHTTPHeaderField: "X-App-Token")
session.dataTask(with: request) {
data, response, error in
print("completionHandler")
completionHandler(data, response, error)
}.resume()
}
else {
print("bad url")
}
}
View Stuff
The top level view in SwiftUI is often just called ContentView, but it doesn't have to be. It gets instantiated in the App (pardon the name, it was a condition of the assignment!)
import SwiftUI
@main
struct _0210407_EricFord_NYCSchoolsApp: App {
var body: some Scene {
WindowGroup {
SchoolListView(viewModel: SchoolListViewModel())
//ContentView()
}
}
}
Inside that view I want a ProgressView if the city data hasn't arrived yet, and a List if it has. But a View can't return more than one View subclass, so the conditional with two Views has to be embedded in something else. Many people do something like a VStack, but I prefer the advice of using a Group:
import SwiftUI
struct SchoolListView: View {
// redraw the View when the viewModel changes, like when the data arrives
@ObservedObject var viewModel: SchoolListViewModel
init(viewModel: SchoolListViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationView {
Group {
if viewModel.schoolViewModels.count == 0 {
ProgressView()
.scaleEffect(x: 10, y: 10, anchor: .center)
}
else {
List(viewModel.schoolViewModels, id: \.school.schoolName) { item in
NavigationLink(destination: SchoolDetailsView(viewModel: item)) {
VStack(alignment: .leading) {
Text(item.school.schoolName)
.font(.headline)
Text(item.address)
}
}
}
.navigationTitle("NYC Schools")
}
}.onAppear(perform: viewModel.loadData)
}.navigationViewStyle(StackNavigationViewStyle())
// the line above stops the iPad from initially hiding the List
}
}
Here's the real workhorse class where it all comes together:
import Foundation
class SchoolListViewModel : ObservableObject // ObservableObject allows the view to receive changes
{
// @Published gets more specific about what will trigger the View to get redrawn
@Published var schoolViewModels = [SchoolDetailsViewModel]()
func loadData() {
Networking.getSchools(session: URLSession.shared, completionHandler: {
data, response, error in
if let data = data {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
if let schools = try? decoder.decode([School].self, from: data) {
DispatchQueue.main.async {
self.schoolViewModels = schools.map {
school in
SchoolDetailsViewModel(school: school)
}
Networking.getSATs(session: URLSession.shared, completionHandler: self.getSATsCompletionHandler)
}
}
}
})
}
func getSATsCompletionHandler(data: Data?, response: URLResponse?, error: Error?) {
if let data = data {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
if let sats = try? decoder.decode([SATData].self, from: data) {
for sat in sats {
if let schoolViewModel = schoolViewModels.first(where: { $0.school.dbn == sat.dbn }) {
schoolViewModel.school.satData = sat
}
}
}
}
}
}
The Map view turned out to be harder than I thought, and my solution for the binding it requires only works because I don't need the binding to change, I just needed it to not be hard coded for a single location.
import SwiftUI
import MapKit
struct AnnotatedItem: Identifiable {
let id = UUID()
var name: String
var coordinate: CLLocationCoordinate2D
}
struct SchoolDetailsView: View {
@State var viewModel: SchoolDetailsViewModel
var body: some View {
let pointsOfInterest = [
AnnotatedItem(name: viewModel.school.schoolName, coordinate: .init(latitude: viewModel.latitude, longitude: viewModel.longitude))
]
VStack {
Map(coordinateRegion: viewModel.$region, annotationItems: pointsOfInterest) { item in
MapMarker(coordinate: item.coordinate, tint: .red)
}
.navigationBarTitle(viewModel.school.schoolName, displayMode: .inline)
SATDetailsView(viewModel: viewModel.school.satData) // just a VStack of text
}
}
}
What I want to learn next
I need to understand bindings better. I need to work more with Combine and understand Publishers and Sinks.
Tutorial
Testable Error Handling with CloudKit
: 2020
I wrote a couple of CloudKit based apps recently. One is a messaging app for very old people called Senior Bulletin Board. The other is Jamulator, a music app where you can share music composition/improvisation in semi-realtime.
The first time I got a CloudKit error, my app didn’t handle it well. So I added some error handling code and then saw that it would be hard to test it the way I had implemented it since these errors were rare. If I disconnected from the network my network connectivity checking code would kick in, and also I couldn’t test specific CloudKit errors.
In my messaging app I have two RecordTypes: Config and Message. My CloudKit code handles subscribing to creation, updates, and deletion of both of these. It also can load all my Messages, load a specific Config, receive changes for a Message or Config, receive a deletion of a Message, receive a new Message, and send a Message or Config to CloudKit.
I had all of this streamlined in a CloudKitHelper class made up of entirely static functions:
Note: Code has been edited for readability, missing some detail like .rawValue and such
static func subscribeToMessages(myEmail: String, emailOfFriend: String,
completionHandler: @escaping (CKSubscription?, Error?) -> Void)
static func subscribeToConfigChanges(myEmail: String, admin: String,
completionHandler: @escaping (CKSubscription?, Error?) -> Void)
static func loadConfig(recipient: String,
recordFetchedBlock: @escaping (CKRecord) -> Void,
completionHandler: @escaping (CKQueryOperation.Cursor?, Error?) -> Void)
static func sendConfigToCloud(config: Config,
completionHandler: @escaping (CKRecord?, Error?) -> Void)
static func sendMessageToCloud(message: Message,
completionHandler: @escaping (CKRecord?, Error?) -> Void)
static func loadMessages(stateController: StateController,
emailOfCurrentFriend: String,
recordFetchedBlock: @escaping (CKRecord) -> Void,
completionHandler: @escaping (CKQueryOperation.Cursor?, Error?) -> Void)
static func receiveMessage(_ notification: Notification,
completionHandler: @escaping (Message?, Error?) -> Void)
static func receiveConfig(_ notification: Notification, stateController: StateController,
completionHandler: @escaping (Config?, Error?) -> Void)
static func deleteMessage(recordID: CKRecord.ID,
completionHandler: @escaping (CKRecord.ID?, Error?) -> Void)
All of these methods execute asynchronously. Often they need to be chained together in a manner similar to promises. Another requirement is that the chain of asynchronous commands needs to stop executing in the event of an error. In order to test error conditions we’d also like to be able to remove CloudKit calls and test the surrounding code by simulating error conditions.
My initial pass at handling CloudKit errors is to just show a screen with a message and buttons to retry or quit. Maybe I should add an option to ignore, or change quit to ignore, since they can quit whenever they want to. My guess is that in my application CloudKit errors will generally be temporary server side conditions that resolve without any action on the part of the user. Thus the retry option. I did find situations where I should silently ignore some errors.
I introduced a wrapper that will either make the actual calls to CloudKit or simulate an error condition and invoke the error handling code. I called it TestableCloudKitWrapper. I need to be able to save a subscription, save a record, add a query operation, add a fetch records operation, and delete a record.
I chose not to make this a static class, but it could be one. It has three instance variables:
var container: CKContainer
var forceErrors = false
var errorToForce: Error?
The query operation needed a couple more lines of code than the others, but the other methods in the wrapper follow this basic blueprint:
func add(_ operation: CKQueryOperation, recordFetchedBlock: @escaping (CKRecord) -> Void,
completionHandler: @escaping (CKQueryOperation.Cursor?, Error?) -> Void)
{
if forceErrors && errorToForce != nil
{
completionHandler(nil, errorToForce)
}
else
{
operation.recordFetchedBlock = recordFetchedBlock
operation.queryCompletionBlock = completionHandler
container.publicCloudDatabase.add(operation)
}
}
Now to implement the promise-like chaining of asynchronous commands that can be interrupted in case of an error. While we’re doing that we can streamline the retry feature as well.
I built a CloudCommandExecutor that reminds me a little bit of when I used to write assembler for the 6502 processor. The executor has a queue of commands, and it has some registers to hold values temporarily, usually only meaningful for the subsequent command:
class CloudCommandExecutor
{
var messageRegister: Message?
var configRegister: Config?
var retryRegister: CloudCommand?
var queue = QueueArray()
func clear()
{
queue.clear()
}
func enqueue(_ command: CloudCommand)
{
queue.enqueue(command)
}
func next() -> CloudCommand?
{
retryRegister = queue.dequeue()
return retryRegister
}
func run()
{
next()?.execute()
}
}
Notice that the next() method caches the command being returned in the retry register. This allows your error UI to implement a retry method like this:
@objc func retry()
{
CloudKitHelper.cloudKitWrapper.forceErrors = false
dismiss(animated: true,
completion: cloudCommandExecutor?.retryRegister?.execute)
}
Other CloudCommands such as RecieveConfigCommand and ReceiveMessageCommand can stash values in the other two registers so that their completion routines can access them and verify that something was received.
CloudCommand is a protocol:
protocol CloudCommand: class
{
// controller is needed to present UI for errors
var controller: UIViewController? { get set }
var cloudCommandExecutor: CloudCommandExecutor? { get set }
var forceError: Bool { get set }
func execute()
}
The completionHandler method can be shared by all of the CloudCommand subclasses. Since completionHandlers come in two flavors, we’ll do a little hack that will conform to both method signatures by using a generic dummy cloudObject parameter, which we don’t use in our completionHandler.
We can add an extension to the CloudCommand protocol like this:
extension CloudCommand
{
// the cloudObject param is only needed to match both method signatures
func completionHandler(cloudObject: T?, error: Error?)
{
DispatchQueue.main.async {
// local func
func executeNext()
{
if let executor = self.cloudCommandExecutor
{
print(executor.queue.description)
if let command = executor.next()
{
if command.forceError
{
CloudKitHelper.cloudKitWrapper.forceErrors = true
}
command.execute()
}
}
else
{
print("no executor")
}
}
if error != nil
{
var ignore = false
if let cloudKitHelperError = error as? CloudKitHelperError
{
if cloudKitHelperError == .notMyConfig
|| cloudKitHelperError == .notFromFriend
{
// skip completion, this message wasn't for us
ignore = true
self.cloudCommandExecutor?.clear() }
}
if !ignore
{
// handle errors here
// use the controller instance variable to present some UI
}
}
else
{
executeNext() // the local function above
}
}
}
}
Now we have a sweet little command language, so we can add a command for each CloudKit situation that can be sequenced, interrupted for errors, and has a built in retry feature:
import UIKit
import CloudKit
class LoadConfigCommand : CloudCommand
{
var recipient: String
weak var controller: UIViewController?
weak var cloudCommandExecutor: CloudCommandExecutor?
var recordFetchedBlock: (CKRecord) -> Void
var forceError: Bool = false
init(recipient: String, controller: UIViewController,
cloudCommandExecutor: CloudCommandExecutor,
recordFetchedBlock: @escaping (CKRecord) -> Void)
{
self.recipient = recipient
self.controller = controller
self.cloudCommandExecutor = cloudCommandExecutor
self.recordFetchedBlock = recordFetchedBlock
}
func execute()
{
CloudKitHelper.loadConfig(recipient: recipient,
recordFetchedBlock: recordFetchedBlock,
completionHandler: completionHandler)
}
}
Each command has instance variables for the params that get passed along to CloudKitHelper methods, but the rest of the code looks the same.
After working with this for a while, I found I needed two non-CloudKit commands to support this design. A CompletionCommand, and a SwiftCodeCommand. The CompletionCommand ends a given sequence, and the SwiftCodeCommand continues executing more CloudCommands by calling the completionHandler. Both of these commands accept a closure and store that in an instance variable.
Here’s the SwiftCodeCommand:
import UIKit
class Dummy
{
}
class SwiftCodeCommand : CloudCommand
{
var controller: UIViewController?
var cloudCommandExecutor: CloudCommandExecutor?
var code: () -> Void
var forceError: Bool = false
init(cloudCommandExecutor: CloudCommandExecutor, code:
@escaping () -> Void)
{
self.cloudCommandExecutor = cloudCommandExecutor
self.code = code
}
func execute()
{
DispatchQueue.main.async {
self.code()
self.completionHandler(cloudObject: Dummy(),
error: nil)
}
}
}
List of CloudCommands I used in the Senior Bulletin Board app:
LoadConfigCommand
SwiftCodeCommand
SendConfigCommand
SubscribeToConfigCommand
SubscribeToMessagesCommand
ReceiveMessageCommand
ReceiveConfigCommand
DeleteMessageCommand
LoadMessagesCommand
SendMessageCommand
CompletionCommand
Here’s an example of CloudCommands usage:
func sendConfigToCloud(completion: @escaping () -> Void)
{
let sendConfigCommand = SendConfigCommand(config: stateController!.config!,
controller: self, cloudCommandExecutor: executor)
executor.enqueue(sendConfigCommand)
let saveSendConfigStatus = SwiftCodeCommand(cloudCommandExecutor: executor)
{
UserDefaults.standard.set(.sendConfigCommand,
forKey: "lastCloudStepCompleted")
}
executor.enqueue(saveSendConfigStatus)
// here's where we could force some CloudKit errors to test error handling
//CloudKitHelper.cloudKitWrapper.errorToForce = CKError(.networkFailure)
//CloudKitHelper.cloudKitWrapper.forceErrors = true
let subscribeToConfigChangesCommand = SubscribeToConfigCommand(
myEmail: self.stateController!.myEmail!, admin: adminEmail!,
controller: self, cloudCommandExecutor: executor)
executor.enqueue(subscribeToConfigChangesCommand)
let saveSubscribeToConfigStatus = SwiftCodeCommand(cloudCommandExecutor: executor)
{
self.lastCloudStepCompleted = .subscribeToConfigChangesCommand
UserDefaults.standard.set(.subscribeToConfigChangesCommand,
forKey: "lastCloudStepCompleted")
}
executor.enqueue(saveSubscribeToConfigStatus)
let subscribeToMessagesCommand = SubscribeToMessagesCommand(
myEmail: self.stateController!.myEmail!, emailOfFriend: adminEmail!,
controller: self, cloudCommandExecutor: executor)
executor.enqueue(subscribeToMessagesCommand)
let saveSubscribeToMessagesStatus =
SwiftCodeCommand(cloudCommandExecutor: executor)
{
self.lastCloudStepCompleted = .subscribeToMessagesCommand
UserDefaults.standard.set(.subscribeToMessagesCommand,
forKey: "lastCloudStepCompleted")
}
executor.enqueue(saveSubscribeToMessagesStatus)
let completionCommand = CompletionCommand(code: completion)
executor.enqueue(completionCommand)
executor.run()
}
Blogs
Overview of Functional Programming: 2017
- Part 1 of a three part blog series about making the transition to functional programming
- Client: Codeship
- Tools: Elm, Ruby, Objective-C, Swift
Overview of Functional Programming
Advantages of Functional Programming: 2017
- Part 2 of a three part blog series about making the transition to functional programming
- Client: Codeship
- Tools: Elm, Ruby, Objective-C, Swift
Advantages of Functional Programming
Cheatsheets
Git commands
Create a remote repo from a local repo
First create it on github with no readme, then
git remote add origin https://github.com/brazford/crazytalk.git
Clone a repo from github, then view all branches, then create a local branch tracking a remote one (a different branch from the one you cloned which is usually master)
git clone git@github.com:OWNER/repo-mame
git branch -a
git checkout branch-name
Throw away uncomitted changes:
git reset --hard HEAD
Delete local and remote branch
git push --delete
git branch -d
Using Google AdMob to put a Banner Ad in an iOS App: 2018
- Create or log into a Google account
- Sign up for AdMob at https://admob.google.com/home/
- Ad an App (you need this to be already registered in iTunes Connect to be sure the name is actually available)
- Add an Ad Unit
- Note the AppID and the AdUnitID for use in your app's code
- Create iOS app in XCode
- Make sure Cocoapods in installed
- Create a Podfile
target 'appname' do
pod 'Google-Mobile-Ads-SDK'
end
on the command line, cd to project, pod install
put import GoogleMobileAds
in AppDelegate
in didFinishLaunching, GADMobileAds.configure(withApplicationID: "YOUR_ADMOB_APP_ID")
Develop using Google's test ad ID: ca-app-pub-3940256099942544/2934735716
import GoogleMobileAds
import UIKit
class ViewController: UIViewController {
var bannerView: GADBannerView!
override func viewDidLoad() {
super.viewDidLoad()
// Note: Landscape is not handled here
bannerView = GADBannerView(adSize: kGADAdSizeSmartBannerPortrait)
bannerView.adUnitID = "ca-app-pub-3940256099942544/2934735716"
bannerView.rootViewController = self
bannerView.load(GADRequest())
addBannerViewToView(bannerView)
}
func addBannerViewToView(_ bannerView: GADBannerView) {
bannerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bannerView)
view.addConstraints(
[NSLayoutConstraint(item: bannerView,
attribute: .bottom,
relatedBy: .equal,
toItem: bottomLayoutGuide,
attribute: .top,
multiplier: 1,
constant: 0),
NSLayoutConstraint(item: bannerView,
attribute: .centerX,
relatedBy: .equal,
toItem: view,
attribute: .centerX,
multiplier: 1,
constant: 0)
])
}
}
Set up payment info. Note that Safari didn't work for me, but Chrome did.
Note: I encoutered a bug where Ads displayed in views controlled by a UITabBarController failed to hide if I set a starting index (other than the default of zero) in viewDidLoad(). Very puzzling! I moved that line to viewDidAppear() and the problem went away.