Refactoring SwiftUI Views, Generics and Is This Really Better?
I’m currently building a mapping app for MacOS using SwiftUI, codenamed Cartographer for now. There have been some struggles but currently it’s going well. I’m enjoying really digging into my tasks and taking the time to make things right.
One of the things I’ve been doing is refactoring any views down to manageable chunks. Not only does this avoid the compiler errors where Xcode complains it’s taking too long but more importantly it helps me reason with my own code.
My app allows users to create locations and paths on the map. The locations are MapKit custom markers and the paths are Polyline overlays. A user can also arrange these annotations into groups to create some order if they desire.
A recent task was creating a view to display all the annotations in the map and allow the user to select any or all of them to be exported to a GPX file.
I wanted a view to show all the exportable items and allow the user to pick the items to be exported. I settled on a TabView with each annotation type being a distinct tab. It looks like this right now:
Each tab has a list for its content where the list displays all the exportable locations, paths or groups. A user can click on any item to select it for export. List selection is handled with a Set<some exportable> to allow for multiple selection. It looks something like this:
The key thing here in starting to reduce the code repetition was the updateSelections method. This generic method takes some sort of element and a Set of the same element types and adds or removes the item as needed. By making this method generic I’m able to use it whether the Tab is selecting Locations, Paths, Groups or Widgets. It doesn’t matter.
Eventually this dialog grew to be many lines of code and I wanted to refactor each tab out to its own view. I started cutting and pasting and things were great but my first attempt had me recreating the generic method to updateSelections to a more specific version for each annotation type. So each tab had its own version of the updateSelections method. This felt wrong-ish.
I made a few attempts to get the code working so that one of my custom TabViews could accept the generic closure as a parameter and I could continue to use this generic version of the updateSelections method. My first few attempts were wrong but I did get this working and it looks something like this:
Notice the LocationsTab takes a closure parameter defined (Location) -> Void. This seems counterintuitive but this is correct and will accept the generic updateSelections method when passed in. Where is the <T>? It’s not necessary. The method being passed in is still a generic but it meets the criteria for this closure.
This partial code listing shows the Sets that track the selected items and how the LocationsTab is called in the TabView. This still calls the original generic updateSelection() method. Note that the location selections set has to be passed as &self.locationsToExport due to the way mutable properties are handle in the generic method.
Is this actually better than just having different methods in each custom TabView? I’m not sure. I think it’s legible enough and easy enough to reason with or I would just write the three separate methods.
I have looked at making each custom TabView more generic so you could pass in all the parameters for the different annotation types but I think that may just be too much. Some times the code gets too general purpose and it’s difficult to reason with what it’s actually doing. Sure it would be clear it’s displaying a list of things to select but I think in this case it’s worth having all the context that comes from being a little less than a general view type. Maybe I’m wrong but I think there is a point of over optimization where you can’t read/reason with your code and I’d like to avoid that.
I thought this was interesting enough to share. This is all a work in progress and likely to change many more times but as of today this is where I am. If you have any thoughts or suggestions feel free to hit me up on Mastodon at the link below.