Mon 03 December 2018
If you've done any iOS development, you're likely familiar with UICollectionView, a data-driven way of building complex views that offer more flexibility than UITableView. If you've dabbled in Cocoa (macOS) development, you've probably seen NSCollectionView and NSTableView... and promptly wondered what era you stepped in to.
I kid, but only somewhat. Over the past few macOS releases, Apple's been silently patching these classes to be more modern. NSTableView is actually super similar to UITableView; auto-sizing rows work fine, and you can get free row-swiping on trackpads. You'll be stuck working around a strange data source discrepancy (a flat list vs a 2-dimensional list), but it can work. NSCollectionView was mired by an old strange API for awhile, but it's now mostly compatible with UICollectionView, provided you do the proper ritual to force it into the new API.
In iOS-land, there's a pretty great project by the name of SwipeCellKit which brings swipe-to-reveal functionality to UICollectionView. It's nice, as one of the more annoying things about moving from UITableView to UICollectionView has been the lack of swipeable actions. In an older project I wound up looking into how difficult it'd be to bring this same API to NSCollectionViewItem; I didn't finish it as the design for the project wound up being easier to implement with NSTableView, but I figured I'd share my work here in case anyone out there wants to extend it further. A good chunk of this has been from digging through various disconnected docs and example projects from Apple, coupled with poking and prodding through Xcode, so I'd hate for it to fade back into obscurity.
Just Give Me the Code...
If you're the type who'd rather dig around in code, feel free to jump directly to the project on GitHub. It's a complete macOS Xcode project that you can run and mess around with, in the style of a holiday calendar. It's December, sue me.
Swiping on Mac
Getting this working for Cocoa was a bit cumbersome, as there's a few different ways you can attempt it, all with their own pitfalls.
- Like UIKit, Cocoa and AppKit have the concept of Gesture Recognizers... but they're more limited in general, as they seemingly require a full click before you can act on mouse or gesture movement. I spent a bit of time testing this, and it seems impossible to disable. This means they ultimately don't work, as a trackpad on Mac can be configured to not be click-on-tap. In addition, a few things here seem specific to the Touch Bar found in newer MacBook Pros, which don't particularly help here.
- We could try the old school Mouse tracking NSEvent APIs, but they feel very cumbersome in 2018 (not that they don't have their place). Documentation also feels very spotty on them.
Ultimately, the best approach I found was going with simple touchesBegan(), touchesMoved(), and touchesEnded() methods on the view controller item. The bulk of the logic happens in touchesMoved(), with the rest existing mostly as flow-control for everything.
Before implementing those methods, setting up an NSCollectionViewItem so that it'll report touch events requires a couple of lines of code. I don't use Interface Builder, so this is included in overriding loadView() below; if you're an Interface Builder user, you might opt for this to happen in viewDidLoad() instead.
Of note here:
- We need to make sure that
allowedTouchTypes supports direct and indirect touch types. Some of the docs allude to these being used more for the Touch Bar, but in my testing not having them resulted in the swipes not registering sometimes. Go figure.
- We add in a
contentView property here; UICollectionViewCell already has this property, but NSCollectionViewItem is a View Controller and lacks it. Since we need two layers for swiping to reveal something, we'll just follow the UICollectionView API for comfort.
postsBoundsChangedNotifications are something I disable, as they can make resizing and animating complex NSCollectionViews choppy. I learned of this from some Cocoa developer who threw it on Twitter, where it likely fell into the ether and doesn't surface much anymore. Helped me in early 2018, so I'm not inclined to believe it's changed. Friends don't let friends post this stuff on Twitter.
- We keep a
leftAnchor reference to do swipe animations later, and rather than pin the right anchor to the item right anchor, we just map the width.
Capturing the Swipe
With the above in place, touches should properly register. We're primarily interested in mimicing the two-finger swipe-to-reveal that NSTableView has, so our
touchesBegan should block anything other than that.
When the first two-finger swipe begins, we grab the initial points for comparing to later movements.
As a drag occurs, this event will continually fire. We grab the newest ("current") touches, and compare where they are in relation to the initial touches. There's a bit of math involved here to get this right, as the Trackpad on Mac isn't quite like a touch screen (
normalizedPosition doesn't map to a pixel coordinate). Once we've calculated everything, we can begin animating the top (content) view to reveal the contents underneath.
The last necessary pieces are just handling when a drag event ends or is cancelled. We'll forward both of those events into
endTracking, which determines the final resting state of the drag animation: if we've dragged far enough in either direction, it'll "snap" to the threshold and hang there until a new swipe gesture begins.
Taking It Further
While the above implements swiping, it's not... great yet. As I noted over on the GitHub repository, this could definitely be tied into a SwipeCellKit-esque API (or just into SwipeCellKit entirely). It also doesn't take drag velocity into account, as calculating it on macOS isn't as simple as iOS, and I ended up scrapping this before using it in a shipping product. Feel free to crib whatever code or assets as necessary! If you end up building on this or taking it further, a line of credit would be cool.
Fri 29 June 2018
Ah, a two year writing gap. Life can be distracting sometimes. Get ready for a rust-y blog entry!
Yeah that was pretty bad, even by my standards.
Two Years and GUIs
One of the big (and kind of annoying) discussions that's been bantered about in the tech world over those two years has been whether Electron, a web-browser-masquerading-as-native-app project, is a plague on society or not. Resource wise, it's probably got some argument there, but in terms of productivity it's hands down the king of the castle - effectively trading memory pressure and less focus on platform conventions in favor of just shipping something out the door. You can open and scan any thread on Hacker News or /r/programming and find people bemoaning this repeatedly.
I wouldn't keep those threads open, for what it's worth - there's generally little worth reading in them, beyond people on both sides completely misunderstanding one another. The tl;dr for pretty much every thread is: more traditional native developers tend not to understand web development, or they look down upon it as easier and less worthy of respect. Web developers tend to favor shipping faster to a wider audience, and being able to implement things across (just about) any platform. You'll see some native developers go on about Qt/GTK and co as ideal native approaches (they're not), or advocating tripling your development efforts and re-implementing a UI across n platforms (why would you bother to do this for most commerical projects? Do you enjoy wasting time and money?).
With that all said, I had reason to build some stuff in Rust recently, and wound up wanting to throw together a basic UI for it. Rust is a wonderful language, and I genuinely enjoy using it, but it's very clear that the community doesn't have a GUI focus. I figured I'd end up basically packaging together some web view (Electron or [insert your choice here]), but wanted to see how easily I could finagle things together at a native level. Since I haven't written anything in some time, I figured this would be a good fit for a super short post.
Going Down a GUI Rabbit Hole
I use a Mac pretty much exclusively, and am pretty familiar with Cocoa, so jumping to that from Rust seemed pretty reasonable. It's also ridiculously simple, thanks to Objective-C - the C interop in Rust makes this fairly transparent, provided you're willing to dabble in the unsafe side of things. What I came up with was a mix of rust-objc and rust-cocoa, from core-foundation-rs, along with some usage of Serde for a quickly hacked together CSS framework.
A basic app would look like this:
The declarative approach to the UI is inspired by React (and co). I threw the resulting package on Github as Shinekit, in case anyone out there finds it interesting and would want to hack on it themselves. Since it's basically hacking through Objective-C, I've got a theory that it could be wired up to Microsoft's port of Objective-C for Windows, which ultimately creates native UWP apps.
Of note, some things for GUI programming are a pain in the ass in Rust - e.g, parent and child relationships. Shinekit pushes that stuff over to Cocoa/Objective-C where possible, rather than tackling it head-on. In a sense, it's cheating - but it creates a slightly nicer API, which (in my experience) is important in UI development.
Well, if you're a native fan, you're gonna hate to hear that yes, I did wind up just throwing a web browser into the mix to ship. Goal wasn't to build a full GUI framework.
With that said... time permitting I'll probably hack on this some more, as the existing GUI options in Rust don't really fit my idea of what a GUI framework should look and function like. As Rust becomes more popular, a decent GUI approach (even for small toolkit apps and the like) would be great to have. If or when this becomes more mature, I'd throw it up on crates.io as something to be used more widely.
Tue 22 March 2016
Back in August,
PL/v8 project, which straps Google's rocket
the CommonJS API.
Enabling PL/v8 on Your Deployment
First thing's first: you'll need to enable it by creating the extension. The quickest and easiest way to do
this is likely using psql from a terminal (below), but if you prefer using another tool it shouldn't pose any problems.
and LiveScript. CoffeeScript offers a more Ruby-esque syntax, and
languages you'll need to create their extensions below as well.
Creating a function using PL/v8 looks like any other PostgreSQL function, with the exception of a language specifier change. Take the (basic) example
below: we're simply incrementing each int in an Array by 2, and returning it as pure JSON.
is incredibly expressive and easy to work with. While you can't get all crazy with any ES6 code yet (the version of V8 currently used is a bit older),
pretty much all the good parts of ES5 are up for grabs. Couple that with the native JSON support PostgreSQL offers, and you can get many of the
features (e.g, Schema-less storage) from Document-oriented databases like MongoDB or RethinkDB.
including polymorphic types (anyelement, anyarray, anyenum and anynonarray) and bytea
(unless you're returning JSON, in which case go for it), and you can't create your own Typed Arrays for use in functions; you can, however,
modify and return the ones passed to your function.
this value, and it can be confusing at first. SQL functions do share context though, as far as accessing globally declared variables and
functions. As long as you pay attention to scoping issues, you can avoid context binding issues and write reusable code.
Hacking Together a Module System
V8 is synonymous with Node.js for many developers, and inevitably the question of importing modules comes up. There is no baked-in module system, but
we can simulate one using some of the features of PL/v8. It's important to note that while this works, we're in a sandboxed environment - modules
involving network calls or browser-related functionality won't work. We'll be simulating the CommonJS module.exports API though, so many modules
should "just work" right off npm.
The first thing we'll need is a table to store our module source(s) in. We really only need two columns to start: the module name (module),
and the source code (source). To sweeten the deal we'll add an autoload column (autoload) that we'll use to dictate whether a module
should be transparently available at all times.
We'll need a function to handle wrapping the require() API, and ideally we'll want a cache for module loading so we're not pulling from
a database table every time we require a module. The global plv8 object has a few things we'll make use of here - it brings important
functionality like executing statements, subtransactions, logging and more to the table. We'll be eval()ing the source for each module, but
we wrap it in a function to ensure nothing leaks into the global scope. Our autoloading of certain modules also takes place in this function, just to
prime the module cache for later use.
Now in terms of using this, we have that dangling context problem to consider - how do we make sure that require() is available to each
PL/v8 function that needs it? Well, it just so happens that PL/v8 supports setting a specific function to run before any other functions run.
We can use this hook to bootstrap our environment - while ordinarily you could set it in your config files, you don't have access to those on
Compose. We can, however, SET this value every time we open a connection. As long as you do this prior to any function call
(including CREATE FUNCTION itself) you'll have access to require().
Let's try it out by throwing together a module that implements the Fisher-Yates shuffle algorithm - we'll name the module "shuffle", to keep things
simple, and go ahead and set it to autoload.
Now we should be able to require() this! We can try it immediately - a simple table of people and a super readable random_person()
function works well.
See how clean that becomes? A shuffle algorithm is only one application - modules like
lodash are a prime candidate to check out for using here.
What Are You Waiting For?
when dealing with your database. You can reap the benefits of Document-oriented, Schema-less databases while leaving yourself the option to
for organizing code that makes sense - you'll feel right at home.
Even more in-depth documentation on PL/v8 can be found over on the
official docs. Try it out today!
Thu 04 June 2015
I've developed with Django for a number of years - out of all the
frameworks I've ever used, it strikes the best balance between "let me
get stuff done" and "don't try to provide me too much, get out of my way
when I say so". I was ecstatic when
released, as it solved a very annoying part of the development process
with Django - static files. Rather than uploading things to S3 (ala the
old Storages route) it's now just easier to toss CloudFront in front of
your files and let uwsgi serve it up. Since WhiteNoise debuted, I've
used it on a few different projects, and over time I found myself
wanting a few things that it lacked - django-rednoise provides them.
Feel free to read on for details, or go find it on
GitHub! I published
it as a separate module as the goals differ from the established
WhiteNoise goals, but I'd be absolutely open to it being merged or
WhiteNoise has a Django-specific module that you can use, but it's
essentially geared towards production use-cases. I tend to prefer having
a development setup that mimics my production setup; patching Django
urls to load static or media files in development just feels clunky to
RedNoise respects whether Django is in DEBUG mode or not. If DEBUG is
set to True, RedNoise will mimic Django's default static-file loading
pattern, so you don't need to reload the entire server just to debug
some frontend issues.
Should You Use It?
I've run it for a bit with no real issues; I would say that whatever you
do, tossing a CDN in front of it all is pretty efficient. Provided you
do that, the CDN should absorb most of the requests, leaving your server
to do its thing.
If you use WhiteNoise, and wish you had the above features, give
django-rednoise a shot. It's just a pip install away.
Looking for More?
I've been writing for quite some time! You may want to check out the
Essays section for a full list.