How to Reuse Paging Interface Controllers in watchOS
watchOS is currently very non-dynamic. You have a storyboard and you have to put all your Interface Controllers in it, even when they’re all pretty much the same, like in this Italian Food Apple Watch app:
Each Interface Controller simply has an image and a label:
Even though these are exactly the same and have the exact same logic in the Interface Controllers, for the past year, I haven’t been able to figure out how to re-use one Interface Controller for each of these!
So I had three different controllers where I copied and pasted all the code and only changed the model-layer details for the image and label data. But alas, after searching once again, I finally stumbled on a horrible solution that actually works.
The Model
First, here is the simple FoodItem model that I’m using to populate the data for the Interface Controllers:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct FoodItem { let title: String let imageName: String } extension FoodItem { static let foodItems = [ FoodItem(title: "Camogliese al Rum", imageName: "comogli"), FoodItem(title: "Pesto alla Genovese", imageName: "pasta"), FoodItem(title: "Focaccia di Recco", imageName: "recco"), ] } |
The Storyboard
The next step is to create the reusable Interface Controller, let’s name it FoodItemInterfaceController, and to assign it as the class for every single Interface Controller in the storyboard:
Next, create and connect the IBOutlets for the image and label in the FoodItemInterfaceController:
Finally, you have to add a unique identifier for each of your Interface Controllers in the Storyboard:
The Interface Controller
Now comes the ugly part… When the first interface controller loads, you have to trick it into loading all the others instead…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import WatchKit class FoodItemInterfaceController: WKInterfaceController { @IBOutlet var image: WKInterfaceImage! @IBOutlet var label: WKInterfaceLabel! // you have to keep track of whether this is the first load... static var first = true override func awake(withContext context: Any?) { super.awake(withContext: context) // if first load... if FoodItemInterfaceController.first { // then reload with the data for all 3 controllers... // the Names are the storyboard identifiers // the Context is the data if FoodItemInterfaceController.first { WKInterfaceController.reloadRootControllers( withNames: ["FoodItem1", "FoodItem2", "FoodItem3"], contexts: FoodItem.foodItems) FoodItemInterfaceController.first = false } // the data is in the context that's passed into this method if let foodItem = context as? FoodItem { // set the proper data into the image and label image.setImage(UIImage(named: foodItem.imageName)) label.setText(foodItem.title) } } } |
Conclusion
First, this is slower than just hardcoding all the Interface Controllers, since the first time the Interface Controller loads, it has to reload everything. But at least the code is in one place, right?
Also, there is no way to my knowledge to have a dynamic data set (e.g. you get the variable food item array data from the server and want to display it in more than 3 pages. Although in that case, you can use a table instead of paging interface.
Oh, and of course, you still have to duplicate the Interface Controllers in the Storyboard, so even though these all have the same-sized images and labels with the same layout and fonts, if you make a change to one, you have to remember to make it to all so they all look the same at the end. I forgot to do this a few times even for this demo…
You can view the full source code on Github here.