2D-Arrays and the Power of Programmatic Constraints
For my latest project I am building an interactive synthesizer interface. It is based off of a classic design you see often in MIDI interfaces for studios or live performances (and subsequently may be used as such), but for now the design is based on an 8 note scale and an 8 beat measure. As such the screen will display 64 buttons and look more or less like this:
The first problem I came across was 2-fold: how does one go about creating a layout for 64 individual UIButtons, and how will each button retain a unique identity so that the program will know which note to play on a specific beat? The solution I found merges two extremely useful concepts: the power of programmatic views and the versatility of 2D arrays.
If you are someone who only uses Storyboard for layout, consider for a second how you would go about initializing and constraining 64 unique buttons? (If you do have a method, please share in the comments as new techniques are always appreciated!) In my understanding, you would have to copy/paste 64 UIButtons, drag them to a corresponding IBOutlet, and then make sure the constraints were consistent enough across multiple devices. NO WAY! Furthermore, this method doesn’t even begin to address the problem of giving each button a way to know its note and position.
This particular situation highlights the power of programmatic layout over Storyboard, as initializing 64 buttons is actually as easy as running a for-loop 64 times! But how to create a system that would give each button a note in the musical scale as well as a beat in the measure? 🤔
The answer came after studying the shape and dimensions of the layout- it was a simple grid system with each button belonging to a column and a row. If I could assign a column and a row during initialization, each button would be wholly unique! With this in mind I created a custom button which inherited from UIButton, but contained a
row, and a convenience initializer
init(column: Int, row: Int). I then created a for-loop within a for-loop, with the outer one corresponding to the column (beat in a measure) and the inner representing each 8 note of the musical scale.
Unbeknownst to me at the time, this concept is known as a 2D-Array, the name of which is a bit of an intimidating misnomer…
2D-Arrays are essentially the naming equivalent of what “3D Touch” is to “super hard pressing”
To the computer a 2D array is simply arrays within an array, but to us humans it is more easily visualized as a grid system. It is also a highly effective layout system for games such as Tic-Tac-Toe and Battleship. In my experience this concept also pops up on coding challenges from time to time, but once you realize that initializing a coordinate is as simple as a for-loop within a for-loop, you’re in the clear 😎
Great! So now we have 64 UIButtons each containing a unique coordinate relative to its place in the grid. How the hell do we give them all constraints? There is absolutely no way I’m about to manually enter in constraints for that many objects; even with SnapKit that would be a nightmare! Fortunately UIKit comes prepared with a versatile and easy-to-use class- UIStackView
In my opinion StackView is the sleeper of the UIKit arsenal, with many people opting for the sexier and complex (read: complicated) UICollectionView. But while CollectionViews come with a relatively steep learning curve, StackViews do much of the heavy lifting on their own. They can automatically set the alignment and spacing between Subviews, and even handle each Subview’s width constraints as more are added or removed. In short they made constraining 64 buttons a complete breeze, and I’ll show you how:
Before my initializer I created nine empty StackViews- eight vertical ones and one horizontal (yes, you can even set the stack direction in one line of code). In the initializer I created an array of
[UIStackView] and set it equal to my eight vertical StackViews. I then began the column for-loop as usual
for (column, verticalStackView) in StackViewArray.enumerated, and within that set another for-loop
for (row, _) in 0...7. Within that inner for-loop I finally initialized each button as
let synthButton = CustomButton(column: column, row: row) and added it to the
verticalStackView that was being enumerated through. At the end of the outer for-loop I added that verticalStackView to the horizontalStackView! And at the end of my
configure() function, I can add the single horizontalStackView to the original view, and allow it to handle all the constraints for each sub-StackView. Deceivingly simple!
I use this technique for any time I need to initialize a grid, and have come to love how much time StackViews save in terms of layout. Additionally the coordinate system has been create for creating Enums of different musical scales, using rawValues in relation to each Button’s row/musical note. I’m looking forward to pushing the app to the store as soon as I have some fun and weird sounds programmed, but in the meantime would love to share this learning experience with the community! Feel free to check out the view layout file here, as well as the rest of the project which utilizes AudioKit and may be a good example for fellow developer musicians. Rock on and happy coding! 🤘🏼👾