Data Model Provider
TLDR; Will be adopting an architecture inspired by MVVM (temporarily named Data Model Provider) when writing portable Swift that can be ran everywhere.
Back to the business of getting Swift to run everywhere, today's article focuses on building upon the previously released portable architecture article.
We will be discussing a general architectural pattern when working with cross platform Swift. The goal is to standardize the way we write Swift so we can avoid (to the best of our abilities) writing un-reusable spaghetti code
Hopefully this exercise will allow us to increase our chances of being able to leverage the Swift code we write in as many platforms as possible, and being able to focus on platform specific implementations at the lowest level.
Without further ado, lets begin.
Starting From MVVM
In our previous portable architecture article, we went over some existing patterns commonly used in contemporary Swift app development. I'm usually drawn to one particular architecture I talked about, MVVM (Model, View, View Model).
MVVM (Model, View, View Model)
As a quick recap, using the example of an app that shows info about cars, I'll discuss how MVVM proposes how you separate your code.
It's done in 3 core parts.
- The Model that you're working with (or the the thing that you're representing. IE: A car)
- The View that you're presenting to the end user (IE: UI's like screens with buttons and lists)
- The View Model the general state and reactions to user input from the view, that's fed back to the view. (IE: how many cars am I looking at, and which car is being represented on the screen at any given time.)
This works great in iOS development because it gives you just enough room to represent anything within the confines of your app screens.
Want to show the user other data other than a car to your app? Make a simple Model that represents what you want to show, and store its context inside your View Model
Then show it off in what ever View you want in your app. Reuse an existing view and add new labels or what ever to it, or make an entirely new view and feed it your previous View Model.
If we were to try to adopt this pattern now for cross platform Swift however, we'd quickly run into the most glaring limitations around your View layer. It's easy to integrate this pattern in Apple's ecosystem because they provide all the frameworks that draw Views, screens and manage the in between.
With Apple's ecosystem, this architectural pattern acts as a thin layer over a bunch of abstractions they handle for you in their white glove manner. Outside of it falls on us to figure out how we'd present stuff to the user.
Addressing The Elephant in the Room
I know I'm a brokenr record at this point talking about what we have and don't have with Swift, but now I'm going to try to address it. A View layer, there technically can't be a MVVM pattern. It becomes more like 'M ???? ???? Model'. So why don't we abstract away from the ???? and handle it differently.
Lets assume we have some sort of alternate View framework we can use instead of Apple's UIKit. Lets call it 'UIKit2'. How would that look in an app? In the most brutish case it could be something like this:
#if canImport(UIKit)
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("Hi! I loaded and you can probably see me on the screen soon!")
}
}
#elseif canImport(UIKit2)
import UIKit2
class ViewController: MySpecialViewControllerSubstitute {
func myPersonalViewDidLoad() {
print("Hi! I loaded and you can probably see me on the screen soon!")
}
}
#endif
Assuming you have this magical hypothetical UIKit2
framework somewhere, you should be able to run this Swift code on Macs and outside of it. This is one technique of being able to leverage compiler directives
to write code that can be conditional depending on whats available to you.
Maybe I'll go a little more in depth to compiler directives
in a different article but for now, trust that you can use them condtionally create rules when your code is being built.
This comes in handy when you want to branch out certain behavior like checking for a platform you're on to decide if the code you want to write will wrok there.
If we wanted to leverage the MVVM car app example from above, we could do it sort of like this:
The code below is a mock up example to represent a Car and a View Model that is responsible for providing cars to the user
struct Car {
var color: String
var name: String
var year: Int
}
class CarViewModel {
private var cars = [Car]()
// returns all cars being kept track of
public func getCars() -> [Car] {
return cars
}
// returns only red cars being kept track of
public func getRedCars() -> [Car] {
return cars.allSatisfy { $0.color == "red" }
}
}
The code above represents the Model part of MVVM (the car) and the View Model. To represent this code being used in our View we mocked up above (using the hypothetical UIKit2 framework), it would look soomething like this:
#if canImport(UIKit)
import UIKit
class ViewController: UIViewController {
private var viewModel = CarViewModel()
override func viewDidLoad() {
super.viewDidLoad()
print("Hi! We have this many red cars in stock: \(viewModel.getRedCars().count)")
}
}
#elseif canImport(UIKit2)
import UIKit2
class ViewController: MySpecialViewControllerSubstitute {
private var viewModel = CarViewModel()
func myPersonalViewDidLoad() {
print("Hi! We have this many red cars in stock: \(viewModel.getRedCars().count)")
}
}
#endif
In this example, you'll notice that we have 2 different kinds of frameworks drawing stuff on screen for the user here, but the same View Model and Model being used with them. Hopefully the path before us should become clearer now.
Final Checks Before Jumping
So we can check whether or not we can draw stuff on the screen with Apple and a still non existant hypothetical alternative, when writing Swift. This should cover most cases. But Apple also has a bunch of other proprietary frameworks that we rely on.
They have AVFoundation
for working with video, CoreAudioKit
for sound. MapKit
for maps. Etc. Their tendrills have a wide grasp on the whole developer experience, so even though drawing UI on screens is a big step to overcome, there will be other challenge's we'll face when trying to use Swift outside of Apple's ecosystem.
With that in mind I'm going to try to abstract a little further with this pattern to try to future proof myself. Instead of always expecting to have a View and View Model to work with, I'll accept the fact that maybe we're not working with a View.
Maybe we have to work on a piece of code for playing Audio. And we need to let Swift know that we can play sound outside of Apple's hardware. We could create a Model that keeps track of the data needed to be held when playing sound. We can even keep track of the state of this in a View Model
But at this point, we're not working with a View Model per say, just a piece of code that keeps track of the state of this audio framework and lets user's communicate with it.
We know we want to represent data, keep track of it's state, but also not necessarily have it coupled to the idea of being tied to a View. Lets keep that thought to ourselves while we think about another case.
What if we have a program that draws virtual 3D scenes. We could technically represent this as a screen, but it would be messy because 3D scenes could have a lot going in on them throughout their life cycle, as opposed to a simple UI screen which (traditionally) has a limited scope and life cycle.
We can still represent what goes on in the scene with Models (like the car Model above for a 3D scene drawing cars), and a state like View Model object, that keeps track of how things currently are at any given moment in the scene. But it wouldn't be apt to call this a View so much more anymore.
In this case I'll decide that like above, we can do stuff we normally would in a View, but not call it that. Instead I'll call this abstracted 'View' concept, a Presenter
, since a View can present stuff to the user in the form of UI, and 3D scene can do so in the form of 3D objects.
Present The Data Models We Work With
Lets represent the magic cross platform UICode I wrote above using this train of thought:
class Presenter {
#if canImport(UIKit)
var appleViewController: UIViewController
#elseif canImport(UIKit2)
var crossPlatformViewController: MySpecialViewControllerSubstitute
#endif
func myPersonalViewDidLoad() {
#if canImport(UIKit)
appleViewController.viewDidLoad()
#elseif canImport(UIKit2)
crossPlatformViewController.myPersonalViewDidLoad()
#endif
}
}
And now lets use this new representation of a Presenter
to show off a ViewController with a lot less duplicated code compared to the first time I showed this concept:
class ViewController: Presenter {
private var viewModel = CarViewModel()
override func myPersonalViewDidLoad() {
print("Hi! We have this many red cars in stock: \(viewModel.getRedCars().count)")
}
}
What I did was, I made a new class that took into account apple's view controller from UIKit, and my hypothetical magical UIKit2
based viewcontroller, and called out to each of them depending on what platform I was using, but only 1 time.
The end result being a pretty compact Presenter
subclass that looks like Apple's original UIKit
style ViewController
, but also plays nice on Apple platforms as well as outside ones. Notice that we also leverage the benefits of reusing the same car Model
and ViewModel
I think if we clean up the terminology a little, we'll see that we're still doing the same things we normally do when making iOS apps, but with the added advantage of taking it outside of iOS and Apple. The tradeoff being the burden for implementing Framework X
that doesn't exist outside of Apple's ecosystem, not withstanding.
But we have a clear path forward on how to do it. Henceforth, I'll be representing this pattern as I polish and refine it, as the Data Model Provider
pattern, or DMP for short.
- Our data stays the same (light weight structs and classes)
- Our Models are now more general from
View Model
toData Models
- Our Views now become
Presenter
's, because they can be anything, including a View, ViewController, or some new thing we'll build one day.
On the last bullet point, Im choosing to call the Presenter part of theDMP
acronym aProvider
just in case we have to provide stuff other than a UI to the user (IE: like the audio framework example).
In my train of thought, this style puts just enough distance between us and the underlying implementation by giving us final say in how we present or represent data who's state is being managed by an underlyingViewModel
like entity.
In the audio example, maybe we have a type of Provider that knows how to actually do an #if canImport(CoreAudioKit)
check to play Audio, that a Model
and DataModel
can work with. Then you can pass that Provider everywhere and have it do its thing, like the MVVM relationship without the View
Closing Thoughts
We have some almost tangible code that shows how we can take Swift outside of Apple's Ecosystem. It's still a concept but not a pipe dream anymore (hopefully).
The road ahead is still long because implementing these abstractions over Apple's ecosystem is easier said than done.
But at least its not impossible. Besides, the best things in life don't come easy.
The next time we revisit this matter, we'll try to get even closer to taking this concept further. Cheers! 🎉