[SC]()

iOS. Apple. Indies. Plus Things.

iOS 17: Notable UIKit Additions

// Written by Jordan Morgan // Jun 5th, 2023 // Read it in about 5 minutes // RE: UIKit

I’m really starting to wonder why I write this annual post anymore 😅.

During the entirety of The Platforms State of the Union, I think I heard the term UIKit or UIViewController…twice? Maybe three, if you count the bit where the new #Preview{ } syntax works for UIKit controls now.

But alas - as long as the framework keeps chuggin’ with diffs each W.W.D.C., then I’ll keep writing this annual post. To that end, here are some notable UIKit additions for iOS 17. If you want to catch up on this series first, you can view the iOS 11, iOS 12, iOS 13, iOS 14, iOS 15 and iOS 16 versions of this article.

Unavailable Content
Ah yes, the empty data view. Here’s a snippet of code I currently use to implement it in UIKit, complete with Objective-C and swizzling:

#pragma mark - Swizzle

void swizzleInstanceUpdateMethods(id self) {
    if (!implementationLookupTable) implementationLookupTable = [[NSMutableDictionary alloc] initWithCapacity:2];
    
    if ([self isKindOfClass:[ASCollectionNode class]]) {
        // Swizzle batchUpdates for collection node which is all we'll need
        Method methodBatchUpdates = class_getInstanceMethod([self class], @selector(performBatchUpdates:completion:));
        IMP performBatchUpdates_orig = method_setImplementation(methodBatchUpdates, (IMP)swizzledBatchUpdates);
        [implementationLookupTable setValue:[NSValue valueWithPointer:performBatchUpdates_orig] forKey:[self instanceLookupKeyForSelector:@selector(performBatchUpdates:completion:)]];
    } else if ([self isKindOfClass:[ASTableNode class]]) {
        // Swizzle batchUpdates and reloadData for table node since we'll need both
        Method methodBatchUpdates = class_getInstanceMethod([self class], @selector(performBatchUpdates:completion:));
        IMP performBatchUpdates_orig = method_setImplementation(methodBatchUpdates, (IMP)swizzledBatchUpdates);
        [implementationLookupTable setValue:[NSValue valueWithPointer:performBatchUpdates_orig] forKey:[self instanceLookupKeyForSelector:@selector(performBatchUpdates:completion:)]];
        
        Method methodReloadDataWithCompletion = class_getInstanceMethod([self class], @selector(reloadDataWithCompletion:));
        IMP performReloadDataWithCompletion_orig = method_setImplementation(methodReloadDataWithCompletion, (IMP)swizzledReloadDataWithCompletion);
        [implementationLookupTable setValue:[NSValue valueWithPointer:performReloadDataWithCompletion_orig] forKey:[self instanceLookupKeyForSelector:@selector(reloadDataWithCompletion:)]];
    }
}

// And a whole lot, LOT more...

…it’s…a little easier now. Enter UIContentUnavailableView:

var config = UIContentUnavailableConfiguration.empty()
config.text = "Nothing Here"
config.image = .init(systemName: "plus")

let unavailable = UIContentUnavailableView(configuration: config)

Which yields:

An empty data view

The API is built on top of the content configurations that were introduced back in iOS 14. They have built in configurations for a few things: empty states, loading states and and searching.

Preview Syntax
As I mentioned in the opening, Swift’s slick new macros allow for Xcode Preview support in UIKit. This didn’t used to be straightforward, but it has always been possible. Behold at how laughably simple this is now (please excuse the odd syntax highlighting, my CSS isn’t quite yet ready for language changes):

#Preview("My Controller") {
	MyController()
}

That’s it, now you’ll get a live Xcode preview just like you would for SwiftUI. Amazing:

Xcode Preview using UIKit

Automatic Label Vibrancy
Labels will now, at least on visionOS, automatically support vibrancy. From an API standpoint, it uses UILabelVibrancy:

let lbl = UILabel(frame. zero)
lbl.preferredVibrancy = .automatic

On visionOS, that’s always set to .automatic, but it’s .none everywhere else. Of note, you can only opt in to vibrancy, you can’t use this API to opt out of it. Plus, it’ll only effect situations where vibrancy would apply, which basically means when it’s over system materials.

Symbol Animations in Buttons
There is a whole lot of new API for animating SF Symbols. I haven’t dug into everything yet, but there are developer sessions later on in the week over it. I’ll be interested to learn more.

In the meantime, there is API I’m seeing for UIButton to get symbol animations by setting one property:

let actionHandler = UIAction { ac in
    
}
actionHandler.title = "Tap Me"
actionHandler.image = .init(systemName: "plus.circle.fill")

let btn = UIButton(primaryAction: actionHandler)
btn.frame = view.bounds

// This contextually animates symbols
btn.isSymbolAnimationEnabled = true

…which yields a nice little bobble, bounce animation:

A UIButton animating a plus SF Symbol on tap.

This is available for UIBarButtonItem as well.

Symbol Animations in Images
To stay on those symbol animations for a minute, these are too fun to mess around with. There are several built-in symbol animations, and they all appear to use those juicy spring animations. For example, if you want an up arrow that looks like it drank 14 cups of coffee, here you go:

let symbolView = UIImageView(image: .init(systemName: "arrowshape.up.circle.fill"))
symbolView.frame = view.bounds
symbolView.contentMode = .scaleAspectFit
symbolView.addSymbolEffect(.bounce, options: .repeating, animated: true) { context in
	if context.isFinished {
		print("Animation done - but this won't, because it repeats.")
	}
}

Behold bouncy boi:

A UIButton pumping an up button over and over.

Even better - combine it with the variable fill APIs added last year, and you can come up with some truly interesting effects:

Timer.publish(every: 0.35, on: .main, in: .default)
    .autoconnect()
    .sink { [weak self] _ in
        switch self?.count {
        case 0.0:
            self?.count = 0.2
        case 0.2:
            self?.count = 0.6
        case 0.6:
            self?.count = 1.0
        default:
            self?.count = 0.0
        }
        
        guard let fillCount = self?.count else {
            return
        }
        
        self?.symbolView.image = .init(systemName: "ellipsis.rectangle.fill",
                                       variableValue: fillCount)
    }.store(in: &subs)

A UIButton pumping an up button over and over.

I can tell you now, I will be utilizing an insufferable amount of these:

Tons of SF Symbols animating

More Font Styles
We got huge and hugerer now:

let xl: UIFont = .preferredFont(forTextStyle: .extraLargeTitle)
let xl2: UIFont = .preferredFont(forTextStyle: .extraLargeTitle2)

Preview of both of those:

Large titles

New Text Content Types
There are additions to boost autofill, specifically for credit cards and birthdays:

let bdayTextField = UITextField(frame: .zero)
bdayTextField.textContentType = .birthdateDay /* or .birthDate/Month/Year */

// Or credit cards
let creditCardTextField = UITextField(frame: .zero)
creditCardTextField.textContentType = .creditCardExpiration /* or .month/year/code plus several others */

UIKit Nuggets
Of course, I always write this before the developer sessions drop. So, a lot more is coming. Here are things I saw but didn’t cover because I don’t quite understand them fully, or I couldn’t get them to work in beta one:

  • Massive trait collection capabilities: You can observe trait collection changes, make entirely custom ones, fire arbitrary logic when they change and that’s in addition to bringing them to SwiftUI’s environment.
  • Text Sizing Rules: There is a new sizingRule property for labels, text fields and text views which appear to control sizing based off of a font’s ascenders and descenders when using preferred font styles.
  • Scene Activation: I’m seeing a lot of changes around scenes, and how they activate. I assume this is to support the new spaces stuff in visionOS.
  • Menus: Related, but not specifically iOS - you’ll see that context menu interactions are available on tvOS now.
  • visionOS: Of course, we can’t forget this one. It’s not every day we get a new device idiom! Enter UIUserInterfaceIdiom.reality.
  • Efficient Images: There is a new UIImageReader that seems interesting, allowing for tweaks like pixel per inch, thumbnail size and more. It appears to be meant for downsampling or efficient decoding.
  • Smart Text Entry: Support for the inline text prediction enhancements.

Final Thoughts

What else is there to say? The messaging has officially shifted. It started with “UIKit and SwiftUI are both great ways to build an app”, and went to “We’ve got great ways to put the UIKit stuff into SwiftUI, so you can use the SwiftUI-only APIs”, and last year it was straight up “SwiftUI is the best way to build an app.”

This year, UIKit really had the nail in the coffin feel to me, even though that’s hyperbolic. But it’s not where the puck is going, and Apple has made that abundantly clear. Does that bother me? No, not at all. It’s not going away, much like Objective-C is still here - and I’ll always celebrate how efficient, robust and capable the framework has been for my career (and still is).

But that was then, and now? It’s all about visionOS, interactive widgets - the list goes on. And I’m here for it, but I’ll still never forget the framework that got me started in this biz. Long live UIKit 🍻.

Until next time ✌️

···

WWDC 2023: The Pregame Quiz

// Written by Jordan Morgan // May 22nd, 2023 // Read it in about 4 minutes // RE: Trivia

No sleep til’ dub dub! Our favorite conference is here, which means that the ninth annual edition of the Swiftjective-C W.W.D.C. Pregame Quiz is ready to go! If you want to test your skills first with the quiz backlog, here are the previous trivia posts from:

import xrOS
import QuizKit

Text("Let's get started!")
    .showAsFloatingARTextViewThingy()

Ground Rules

There are three rounds, and the point break down is as follows:

  • Round 1 – 1 point each answer
  • Round 2 - 2 points each answer
  • Round 3 - 3 points each answer

The last question of each round is an optional wildcard question. Get it right, and your team gets 4 points, but miss it and the team will be deducted 2 points.

Round 1 - Symbolic Questions

Question 1:
SF Symbols are based off of Apple’s San Francisco font, Cupertino’s first new typeface in ~20 years which debuted alongside the Apple Watch. Which version of iOS was the first to use it?

A) iOS 7
B) iOS 8
C) iOS 9
D) iOS 10

Question 2:
As of iOS 16 and SF Symbols 4, which of these glyphs don’t exist in the set?

A) eraser.fill
B) fan.ceiling.fill
C) window.casement
D) dehumidifier.fill

Question 3:
SF Symbols have varying rendering styles that you can apply to them. Which of these isn’t an official rendering style?

A) Palette
B) Monochrome
C) Automatic
D) Multihue

Question 4:
Let’s talk raw numbers! AS of SF Symbols 4, the catalog has grown to over how many symbols on offer?

A) 3,400
B) 4,400
C) 5,400
D) 2,400

Wildcard:
The set contains reserved symbols, which are glyphs that can only be used to represent Apple’s given service or device. For example, shared.with.you.slash is exclusively used to represent the “Share with You” framework. Which of the following glyphs is not a reserved symbol?

A) arrow.counterclockwise.icloud
B) ipad.rear.camera
C) platter.bottom.applewatch.case
D) xserve

Round 2 - iOS Interface History Lessons

Question 1:
When Craig Federighi first unveiled SwiftUI during the W.W.D.C. 2019 Keynote, he first showed a tableview built in UIKit, and then the same thing in SwiftUI. Which of the following was the exact code sample he used?

A)

struct ContentView: View {
    @State var model = Themes. listModel

    var body: some View {
        List(model.items, action: model.selectItem) { item in
            Image(item.image)
            VStack(alignment:.leading) {
                Text(item.title)
                Text(item.subtitle).color(.gray)
            }
        }
    }
}

B)

struct ContentView: View {
    @State var model = Themes. listModel

    var body: some View {
        List(model.items, action: model.selectItem) { item in
            HStack {
                Image(item.image)
                VStack(alignment:.leading) {
                    Text(item.title)
                    Text(item.subtitle).color(.gray)
                }
            }
        }
    }
}

C)

struct ContentView: View {
    @State var model = Themes. listModel

    var body: some View {
        ForEach(model.items) { item in
            HStack {
                Image(item.image)
                VStack(alignment:.leading) {
                    Text(item.title)
                    Text(item.subtitle).color(.gray)
                }
            }
            .onTapGesture {
                model.selectItem()
            }
        }
    }
}

D)

struct ContentView: View {
    @State var model = Themes. listModel

    var body: some View {
        ForEach(model.items) { item in
            Button {
                model.selectItem()
            }, label: {
                Image(item.image)
                VStack(alignment:.leading) {
                    Text(item.title)
                    Text(item.subtitle).color(.gray)
                }
            }
        }
    }
}

Question 2:
Let’s chat AppKit. Today AppKit (and Foundation) are core pieces of Cocoa. But, that’s not where its life started. It first served as the interface framework for NeXTSTEP. Which of the following operating systems did it not support?

A) MS-DOS
B) Windows NT
C) Solaris
D) UN*X

Question 3:
Good ol’ UITableView! The powerful control for displaying lists of data has been around since iPhoneOS 2. Which of the top-level protocols does UITableView conform to?

A) NSCoding, UITableViewDatasource and UISpringLoadedInteractionSupporting
B) NSCoding, UITableViewDatasource and UIDragSession
C) NSCoding, UIDataSourceTranslating and UIDragSession
D) NSCoding, UIDataSourceTranslating and UISpringLoadedInteractionSupporting

Question 4:
Now, let’s talk tableview’s big brother — UICollectionView. Which version of iOS did the control for displaying data in more flexible formats debut?

A) iOS 5
B) iOS 6
C) iOS 7
D) iOS 8

Wildcard:
In what continues to be the greatest travesty to ever grace the iOS platform, 3D Touch was phased out eventually. Along with it - its API deprecated. Which of the following APIs is the only one which is not deprecated?

A) UIAccelerometerDelegate
B) UIViewControllerPreviewing
C) UILocalNotification
D) UIStatusBarAnimation

Round 3 - Early iPhone Days

Question 1:
The original iPhone actually never received iOS, instead it lived its life on iPhoneOS. What was the last version it received?

A) iPhone OS 3.1.3
B) iPhone OS 1.1.3
C) iPhone OS 2.1.3
D) iPhone OS 4.1.3

Question 2:
Basically all cell phone carriers are the worst. But alas, one was chosen to initially support iPhone in the United States. Which service provider was it?

A) Alltel
B) Verizon
C) AT&T
D) Sprint

Question 3:
[Redacted] were not initially released with the iPhone, but instead were held off until they were perfected for the form factor. Which one of the following were not supported until iPhoneOS 3.0?

A) Printing
B) Cut, copy and paste
C) SMS Image Support
D) Video Editing

Question 4:
A prominent technology company claimed that the iPhone’s design was stolen from them, based on a model they produced in 2006. Which company was it?

A) Samsung
B) L.G.
C) Google
D) Nokia

Wildcard:
Storage comes pretty plentiful these days, but for the original iPhone - what were the original storage sizes on offer?

A) 4GB and 8GB
B) 6GB and 8GB
C) 8GB and 10GB
D) 4GB and 6GB

Answer Key

Round 1:

  1. C (iOS 9)
  2. A - there is no eraser, come on!
  3. D - there is multicolor, no multihue though.
  4. B - 4,400!
  5. Wildcard: B! Apparently, it’s all good to represent the rear iPad camera however you want.

Round 2:

  1. A - note some of the early concepts of SwiftUI that were not persisted, such as action within the List.
  2. A - As far as my research shows, all of the others were explicitly supported.
  3. D. Springloading allows for an arbitrary UIControl to allow for drops to occur when hovering over them for a few seconds.
  4. B, it was iOS 6.
  5. Wildcard: D, literally all of those are toast aside from that.

Round 3:

  1. B - A! That was the last of it, as iPhoneOS 3.2 was meant to kickoff another Apple device, the iPad.
  2. A - C. Formerly known as Cingular, it was an 18-month negotiation before it got done.
  3. C - B. It took a bit, but the tent pole editing features finally landed in iPhoneOS 3.0.
  4. D - B - The LG Prada.
  5. Wildcard: A, so, you know, don’t take any photos in RAW.
···

Creating a Swift Package: Quick Start Gotchas

// Written by Jordan Morgan // May 11th, 2023 // Read it in about 3 minutes // RE: Swift Package Manager

Swift Package Manager has been out for a few years now, but I’ve only recently authored my first one. As I continue to move things over to Swift at my job at Buffer, I decided to centralize some of our logic into a Swift package.

The process is fairly no-nonsense, but there are three quick things to note which I ran into. Also, I should note these apply to those of you distributing your package with Git - which I assume most developers are. Also, I just can’t even with package managers, so I’m probably holding things wrong somewhere along the way.

Gotcha 1: The Default Package Structure Won’t Work

When you go to Xcode and choose File -> New Project -> Multiplatform -> Swift Package, the default folder structure won’t mesh well with Git. To demonstrate, let’s say you work up your package and then go into an existing Xcode project to import it. Mercifully, that’s all easy - simply add your new package like you would any other and slap in the URL where it lives in Github.

What happens next is that you’ll like get an error like this: "the package manifest at '/Package.swift' cannot be accessed (/Package.swift doesn't exist in file system)", or similar. What this is actually trying to say is The Package.swift file has to be at the root of the project., and that’s not what happens by default on the file system.

let concatenatedThoughts = """

I was relieved at how easy this was to do with private repos as well. There are no extra configuration steps, which other package managers have conditioned me to think about. Rather, Xcode will pop up a modal to enter in credentials if need be, and that's that.

"""

This makes sense, because you typically (if you’re like yours truly) create the Xcode project and then create a Github repo for it. When you clone the repo, it makes another folder on your file system (i.e. Documents/Projects/Thing_I_Just_Cloned) but your project already lives at Documents/Projects/CoolThing. I usually end up dragging the project I already had into the new, freshly created folder from Github.

Therein lies the issue.

When your project is open in Xcode, is appears as though the Pacakge.swift file is at the root (and it is, as far as Xcode can see) but you need to make it appear that way in git, too. To fix this:

  1. In the folder that was just created from cloning the new repo in Github, I create a new folder called something like “ProjectCore”.
  2. Then, I copied all of the source files in the existing Xcode folder created from my package there.
  3. Lastly, I put Package.swift and Package.resolved at the root level of the repo folder from step 1.

A picture is easier to demonstrate, but this is what it looks liked for me:

A folder structure for SPM.

The last step, and this is critical - is to update your Package.swift file target entry to point to where things are at now:

targets: [
    // Before, there was no `path` parameter used.
    .target(
        name: "SwiftUIMedia",
        path: "SwiftUIMediaCore/Sources/SwiftUIMedia")
]

With this setup, the package pulled into my other Xcode projects just fine without issue. Note that you’ll have to close and reopen the local package within Xcode again since its location on the file system will have changed. For more on this little issue, you can check out this Stack Overflow answer, which is what led me to this solution.

If you know of a better way to get around this little dance, please give me a shout. For now, moving on.

Gotcha 2: Adding Target Dependencies

Look around the internet and Apple’s docs, and you’ll find a lot of useful information on how to add package dependencies. But, what I couldn’t find was an obvious way to associate those dependencies to your specific targets. Here’s how:

let package = Package(
    name: "SwiftUIMedia",
    platforms: [
        .iOS(.v16) 
    ],
    products: [
        .library(
            name: "SwiftUIMedia",
            targets: ["SwiftUIMedia"]),
    ],
    dependencies: [
        .package(url: "some_github_url", .upToNextMajor(from: "2.2.4"))
    ],
    targets: [
        .target(
            name: "SwiftUIMedia",
            dependencies: [
                .product(name: "SPM_Product_Name", package: "SPM_Package_Name")
            ],
            path: "SwiftUIMediaCore/Sources/SwiftUIMedia")
    ]
)

I could have this wrong, and I could be glossing right over the relevant documentation for this, but it seems this is how to get it all hooked up. Take Nuke’s Package.swift file, for example. You’d add it like this for a package wide dependency:

dependencies: [
    .package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "12.1.0")),
]

And, for each target which used it - it’d look like so:

targets: [
    .target(
        name: "SwiftUIMedia",
        dependencies: [
            // Check out the link above to see where `name` and `package` came from
            .product(name: "Nuke", package: "Nuke") 
        ],
        path: "SwiftUIMediaCore/Sources/SwiftUIMedia")
]

After that, I was Nukin’. To me, how to make those associations weren’t immediately obvious. But again, package managers have never been my forté - so if there is a more “correct” way, teach me.

Gotcha 3: Accessing .xcasset Resources

This last one is elementary to fix, but I didn’t catch it up until a nice runtime exception hit me in testing. In short, you’ll need to specify that you’re accessing asset catalog resources within the bundle’s module.

So, instead of that…

Image("Test")
Color("BrandBlue")

…you’d do this:

Image("Test", bundle: .module))
Color("BrandBlue", bundle: .module))

We need to use Bundle.module when accessing resources in our source code for the package, which makes sense because they should not assume they know where the specific location of a resource might actually be. Luckily, Xcode recognizes several Apple-proprietary file formats and asset catalogs are one of them. So, there’s no other steps you need to do to get them to pull in with your package other than adding them under /Sources for the relevant target.

Bonus: Share the Love

And hey - why not share your package with the world since you’ve put all of that hard work into it? Consider popping over to the Swift Package Index and adding your work here, it’s quick and easy - plus everyone can benefit from your code. Personally, I’ve picked up several tips from browsing other packages not only for code, but to see how they setup their own packages to work through some of the things I’ve listed here.

Final Thoughts

Swift Package Manager has been my preferred way to handle dependencies for some time now. I simply enjoy that I can interact with it directly within Xcode, which makes the whole experience feel a little more integrated to me. It has issues when it comes to larger teams, sure, and in fact - CocoaPods continues to grow. Regardless, if you want to get started with creating packages yourself, hopefully these three little obstacles won’t be in your path now.

Until next time ✌️

···

Using TabularData to Dump Model Data

// Written by Jordan Morgan // May 4th, 2023 // Read it in about 6 minutes // RE: TabularData

The TabularData framework gets its bones by wrangling tables of data to train machine learning models. But don’t let the description on the tin force you to leave it alone, this little guy has a lot of power hiding underneath it.

For example, the framework can be used to:

  1. Parse .csv files
  2. Parse .json files
  3. Import, or export, either of those, and
  4. Dump amazing logs in your console from your own models.

Its utility expands beyond that, but I want to show you a little trick I use it for - specifically dealing with point #4 above. Say you’ve got some .json like this:

{
    "people":[
        {
            "name": "David",
            "mobile": 33333333,
            "hasPets": true,
            "pets": ["Dog", "Fish", "Bird"],
            "address": {
                "permanent": "France",
                "current": "UK"
            }
        },
        {
            "name": "Jordan",
            "mobile": 33333333,
            "hasPets": false,
            "pets": [],
            "address": {
                "permanent": "Austrailia",
                "current": "US"
            }
        }
    ]
}

But imagine it’s bigger, like….a lot bigger. Dare I say massive? Say 1,000 or so entries. If you were to decode such a response using pedestrian models like so:

import Foundation

struct Tennants: Codable {
    let renters: [Renter]
    
    enum CodingKeys: String, CodingKey {
        case renters = "people"
    }
}

struct Renter: Codable {
    let name: String
    let mobile: Int
    let hasPets: Bool
    let pets: [String]
    let address: Address
}

struct Address: Codable {
    let permanent, current: String
}

// Later on...
do {
    let decoder = JSONDecoder()
    let people: Tennants = try decoder.decode(Tennants.Type, 
                                              from: data)
    print(people)
} catch {
    Swift.debugPrint("Unable to decode response: \(error.localizedDescription)")
}

and simply print (or dump, Swift.debugPrint, Mirror(reflecting:)) in lldb, you’d see something like this:

Tennants(renters: [SwiftUI_Playgrounds.Renter(name: "David", mobile: 33333333, hasPets: true, pets: ["Dog", "Fish", "Bird"], address: SwiftUI_Playgrounds.Address(permanent: "France", current: "UK")), SwiftUI_Playgrounds.Renter(name: "Jordan", mobile: 33333333, hasPets: false, pets: [], address: SwiftUI_Playgrounds.Address(permanent: "Austrailia", current: "US")), SwiftUI_Playgrounds.Renter(name: "Peter", mobile: 33333333, hasPets: true, pets: ["Lizard"], address: SwiftUI_Playgrounds.Address(permanent: "India", current: "FR")), SwiftUI_Playgrounds.Renter(name: "Sarah", mobile: 33333333, hasPets: false, pets: [], address: SwiftUI_Playgrounds.Address(permanent: "Egypt", current: "US")), SwiftUI_Playgrounds.Renter(name: "Rory", mobile: 33333333, hasPets: true, pets: ["Snakes"], address: SwiftUI_Playgrounds.Address(permanent: "United Kingdom", current: "US"))])

It’s not bad in a pinch, certainly serviceable. But, if you put that data into TabularData’s DataFrame and print that? Well, let’s just venture to say it’s a smidge nicer. Check it out:

A Hacker News comment mentioning that Supabase offers an iOS SDK.

…is….is this love 😍?

I’ve started using this technique when any of these are true:

  • I’m dealing with large amounts of data
  • Regardless of size, if I want to sort or filter data
  • I need a better, easy way to visualize the data I’m getting from my own model layer for scannability
  • Or generally, I simply want to read things easier

…and I adore it. The code to spin it up is trivial:

do {
    let people = try await loadPeople()
    let data = try JSONEncoder().encode(people.renters)

    // Create the DataFrame from .json data
    let dataFrame = try DataFrame(jsonData: data)

    // Beautiful print
    print(dataFrame.description(options: .init(maximumLineWidth: 250)))
} catch {
    Swift.debugPrint("Unable to create DataFrame: \(error.localizedDescription)")
}

Notice the options: parameter. There, you can control cell width, date formatting options, line widths and more. If you want a mountain of data to be translated into a digestible format quickly, creating throw away DataFrame instances is a good option.

If you want, you can even slice the data up a bit to get at certain properties. For example, what if I only wanted to know which renters had pets, and what they were?

That’s no issue, as you can get another DataFrame from an existing one - but specify that it should only include a few of the columns you’re interested in:

do {
    let people = try await loadPeople()
    let data = try JSONEncoder().encode(people.renters)

    // Create the DataFrame from .json data
    let dataFrame = try DataFrame(jsonData: data)

    // Just get names and pets
    let partialFrame = dataFrame.selecting(columnNames: "name", "pets")
    print(partialFrame)
} catch {
    Swift.debugPrint("Unable to create DataFrame: \(error.localizedDescription)")
}

// Results in...
┏━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
    name      pets                                            
    <String>  <Array<Optional<Any>>>                          
┡━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
 0  David     [Optional(Dog), Optional(Fish), Optional(Bird)] 
 1  Jordan    []                                              
 2  Peter     [Optional(Lizard)]                              
 3  Sarah     []                                              
 4  Rory      [Optional(Snakes)]                              
└───┴──────────┴─────────────────────────────────────────────────┘
5 rows, 2 columns

There are so many flavors of Swift’s functional stuff in there, too - built just for the framework. You get the idea, but real quick - now suppose you wanted only the names, and sorted:

do {
    let people = try await loadPeople()
    let data = try JSONEncoder().encode(people.renters)

    // Create the DataFrame from .json data
    let dataFrame = try DataFrame(jsonData: data)

    // Select only names, and sort them
    let sortedNames = dataFrame.sorted(on: .init("name", String.self), by: { lhs, rhs in
        lhs < rhs
    }).selecting(columnNames: "name")
    print(sortedNames.description)
} catch {
    Swift.debugPrint("Unable to create DataFrame: \(error.localizedDescription)")
}

// Results in...
┏━━━┳━━━━━━━━━━┓
    name     
    <String> 
┡━━━╇━━━━━━━━━━┩
 0  David    
 1  Jordan   
 2  Peter    
 3  Rory     
 4  Sarah    
└───┴──────────┘
5 rows, 1 column

Again, a lot you can do here. You might provide default values, completely transform or combine columns to decode your own models or even print out statistical data using numericSummary.

I’m belaboring the point now, but there’s virtually nothing you can’t do with data augmentation or formatting. This specific post shows how I personally use DataFrame with the console, but you can use it for far more practical uses. In fact, that’s what it’s meant for.

Suppose you had two columns representing longitude and latitude coming back from some data source. You could smash them together in a combineColumns(into:transform) to create CLLocation instances to use while someone is searching for a location in a search bar. Or, you could do SQL-esque joins of two different DataFrame instances using joined(on:).

Snazzy.

let concatenatedThoughts = """

There are several ways to spin up a DataFrame, too. You can use local .csv or .json files, or their raw data and more. Be sure to check out the docs.

"""

The framework deserves its own proper post, quite honestly. If you’ve got any sort of of tabular data (i.e. data structured, or unstructured, in rows and columns) you can sort, change, filter or sift through it using its powerful (cliché, I know) APIs.

If you want to mess around with it right now to get your hands dirty, just create a DataFrame using a dictionary literal - and then you can toy around with dumping to the console as we’ve done here or feel out its API:

let testFrame: DataFrame = [
    "id": [0, 1, 2, 3, 4],
    "job": ["developer", "designer", "pm", "em", "staff SWE"],
    "fillBy": ["jordan", "jansyn", "bennett", "remy", "baylor"]
]
        
print(testFrame.description)

// Results in...
┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┓
    id     job        fillBy   
    <Int>  <String>   <String> 
┡━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━┩
 0      0  developer  jordan   
 1      1  designer   jansyn   
 2      2  pm         bennett  
 3      3  em         remy     
 4      4  staff SWE  baylor   
└───┴───────┴───────────┴──────────┘
5 rows, 3 columns

This whole post is kinda funny, though, because it goes to show you that if you hold something a little differently than what its instructions say - sometimes you get useful results.

Perhaps Cupertino & Friends™️ didn’t intend for developers to use the TabularData framework for…debug logging? It’s a bit of a lingua franca situation, as a framework built for maximum efficiency in training machine learning models crosses over and talks to the folks who simply want to log out “It works” within LLDB.

But here we are, and it’s incredibly useful for the job.

Until next time ✌️

···

Spinning Up a Feature Request Screen with Supabase

// Written by Jordan Morgan // Apr 27th, 2023 // Read it in about 3 minutes // RE: Tech Notes

It all started with this Hacker News comment:

A Hacker News comment mentioning that Supabase offers an iOS SDK.

As I near the finish line for the M.V.P. of my next app, I’m at the “10%” phase. The settings screen is the sole remaining, seemingly vast, chasm which lay between “developing” and “finished”, and as most of you can likely attest too - these things tend to drag on.

Among them? A feature request screen. Here were my rough requirements:

  • I don’t want to just open up Mail, or another email client, and fire off an email.
  • I want to store requests somewhere such that later, I can filter and mangle the data into another client app (i.e. an internal feedback tool).
  • I want to own the data so that I could also display it inside the app later, if so desired.
  • I wanted an incredibly, super super suuuuuper light dependency for cloud storage.

All told, the heart of the matter is this: I need a cloud based database, and an API to access it via iOS. You’d be surprised how barren the landscape is for something that, to me, seems to be an insanely common problem. As a UIKit, SwiftUI, Cocoa-lovin’ fool - I don’t want to write my own API to connect to some bespoke cloud database.

Which brings me back to that Hacker News comment, Supabase seemed perfect for the task. Turns out it works great, and the whole setup to API call was about five or ten minutes.

Here is the end result:

Filling out a feature request form in Elite Hoops.

let concatenatedThoughts = """

The art of making an effective feedback form is also an interesting topic to me. There are so many different ways to ask a user about how things are going, and as many U.I. patterns to match. For me? I stole a trick we use at Buffer, and simply asked them what they were trying to do that they could not do. This gets to the heart of the matter, for me at least, as to what feature is missing.

"""

Supabase Setup

So, it turns out that Supabase has an unofficial, official client. It seems that’s the case with three of their five client libraries, with Flutter (what?) and Javascript being their official offerings:

A list of Supabase's client libraries.

That’s fine - I’m no stranger to open source work, I use a lot of it everyday. Yet, this is a service, so using a community driven solution had previously turned me away. No matter, with the Swift library being emblazoned with the ‘community’ badge on their official website, it seems likely they’re wisely capitalizing on an existing solution.

So long as they’ve given it an unambiguous, certified “If you want Supabase on platform X, then use client library Y” vote of confidence, I’m down to dance. And dance I did.

Supabase setup is a breeze. Simply create an account, and that’s it - there is no hand holding, tool tip laden forced entry through the door. The whole thing took me back to the sign up flows of yore, where I only provided an email - and I was rewarded with getting back to the problem I was trying to solve.

Post signup, I created a new table in the Postgres database you’re given out of the box. Using their WYSIWYG editor, I created a table just for feature requests. After setting up a few basic fields, I mirrored what data I needed in Swift:

import Foundation

typealias FeatureRequests = [FeatureRequest]

struct FeatureRequest: Codable {
    var id: Int8?
    var createdAt: Date?
    var text: String
    var email: String
    
    enum CodingKeys: String, CodingKey {
        case id, text, email
        case createdAt = "created_at"
    }
}

From there, the process of installing the Supabase client from Swift Package Manager had me up and going. In fact, this is the entirety of the code - all that’s left to do is remove the hardcoded API keys:

struct Outreach {
    private static let url: String = "your_supabase_api_url"
    private static let apiKey: String = "api_key"
    private let featuresTable: String = "FeatureRequests"
    private let client = SupabaseClient(supabaseURL: URL(string: Outreach.url)!, supabaseKey: Outreach.apiKey)
}

extension Outreach {
    func sendFeatureRequest(_ text: String, email: String = "") async throws {
        let req = FeatureRequest(text: text, email: email)
        try await client.database
            .from(featuresTable)
            .insert(values: req)
            .execute()
    }
    
    func getFeatureRequests() async throws -> FeatureRequests {
        let features: FeatureRequests = try await client.database.from(featuresTable)
            .execute().value
        return features
    }
}

And that’s it, from the SwiftUI view I simply call that with the feedback they’ve left. All landing in my own little cloud database that I am free to tweak any which way:

Supabase showing data.

Perf - now I get the feedback, I can feed it into my custom feedback app, and I can later extend the data model. All of my boxes? Checked.

A Note on Row Level Security

Of course, you may notice the giant yellow “YOU’RE PLAYING WITH FIRE” banner.

That’s expected, as I couldn’t wrangle how to get inserts working with the Supabase client with any sort of RLS enabled. So, I guess, please don’t spam my feature requests. If anyone knows how to get this properly working, give me a shout. The friction I kinda hit was that I have no concept of a logged in user, and a lot of the RLS (as I see it) depends on some sort of notion of one.

No matter, I’m stoked with how easy all of this was. A lightweight library, a no-fuss database with an accessible API - that’s really all I wanted. I’ll be keeping on eye on what the folks at Supabase get up to, and it may become more core to my current project.

Until next time ✌️

···