An Introduction to Combine
Replace the Delegate pattern with Combine
Using Combine to Replace the Delegate Pattern
Preface
There are now a lot of articles and books about Combine. You’ve probably tried it yourself. Maybe you thought it was just for SwiftUI and since you haven’t migrated apps to SwiftUI, you’ve ignored it for now. Well, I decided to hop on the Combine bandwagon at the urging of a co-working, and it was quite the revelation. Making me think in a whole new way. SwiftUI makes you think differently about writing UI code, and relies on Combine, but extracting Combine and using it with a UIKit app, can really drive home its power.
So I’ve cooked up a little example to show how you can replace a very well used design pattern, the Delegate Pattern, with Combine. I will skip the Combine intro since you can find that in many places now. Just suffice it to say that you have publishers and subscribers and I’ll show how these are used in place of delegates.
Set Up
Let’s set up a simple project to illustrate how to replace the Delegate Pattern with Combine publishers and subscribers.
I’m calling the project FoodFacts
because I will expand on this in a future article and use the foodfacts.org API. But for now, the concentration will be on an example to show how delegates can be replaced with Combine.
In this example we’ll have a blank screen with a User Profile button that calls up a sheet with fields for a user’s first and last names along with a Save button. The idea is to transfer the data entered into the fields back to the calling UIViewController
.
Your first reaction to doing something like this would be to create a UserProfileViewController
and a UserProfileViewControllerDelegate
. The main UIViewController
would implement the delegate and when the Save button on the UserProfileViewController
sheet was picked, invoke the delete’s function to transfer the values back. Here’s how you would do the same thing with Combine.
The code below shows the ViewController
(the main controller) and the UserProfileViewController
. There is a storyboard that goes with this but that is not particularly germane to this example.
class ViewController: UIViewController { @IBOutlet weak var yourNameHere: UILabel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. title = "Food Facts" } } class UserProfileViewController: UIViewController { @IBOutlet weak var firstName: UITextField! @IBOutlet weak var lastName: UITextField! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func saveProfile(_ sender: Any) { // to do dismiss(animated: true, completion: nil) } }
You can see in the UserProfileViewController
the action for the button as saveProfile(_:Any)
which right now, just dismisses the sheet.
Now Combine
The first thing we’re going to do to UserProfileViewController
is
import Combine
This brings in the Combine framework so you can begin using it. The idea is that when the Save button is picked, rather than calling upon a delegate and using its functions, we’re going to publish the values and the main ViewController
is going to receive them and use them.
Essentially, the class you would normally use to implement a delegate’s method is the subscriber while the user of the delegate is the publisher.
Now we need something to publish. I created a simple UserProfile
struct that contains the first and last name values and this is what will get published.
struct UserProfile { let firstName: String let lastName: String }
Switching back to UserProfileViewController
, add this line below the @IBOutlet
s:
public var userDataPublisher = PassthroughSubject<UserProfile, Never>()
This line creates a PassthroughSubject
publisher that simply passes on whatever values it is given to send. Values that are UserProfile
type and this publisher never produces any errors (ie Never
).
Down in the saveProfile
function, add these lines:
if let firstName = firstName.text, let lastName = lastName.text { let profile = UserProfile(firstName: firstName, lastName: lastName) userDataPublisher.send(profile) // done with this controller dismiss(animated: true, completion: nil) }
Once the text from the first and last name UITextField
s are extracted, a UserProfile
is created from them and then sent through the userDataPublisher
. That’s it. There is no delegate to access, you are just sending out a UserProfile
to whatever is listening for it.
To that end, switch over the ViewController
and import Combine
into this file, too. Once you’ve done that, we need to subscribe to the userDataPublisher
in UserProfileViewController
.
I set the project up to use segues right in the storyboard, so we need to intercept the segue. If you were doing this with a delegate you would probably do that is the same place.
Override a function in ViewController
called prepare(for:UIStoryboardSegue,sender:Any?)
like this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let destination = segue.destination as? UserProfileViewController { destination.userDataPublisher .sink(receiveValue: { (userProfile) in print("Received profile: \(userProfile)") self.yourNameHere.text = "Welcome, \(userProfile.firstName) \(userProfile.lastName)" }) } }
Once you’ve secured the destination UserProfileViewController
, you set up the subscription. That’s done using the .sink
function provided by the publisher interface. The value being received is the UserProfile
created in the saveProfile
function of UserProfileViewController
. In this example I’m printing it out as well as setting the UILabel
.
But wait, there’s a problem: the compiler is flagging the .sink
with a warning that its return value is not being used.
One important thing about Combine publishers is that they don’t work without subscribers (which you created with the .sink
). And subscribers do not exist unless you keep them someplace. The result of the .sink
is something called a Cancellable
which allows you to programmatically cancel a subscription which then means the publisher will no longer publisher.
At the top of the file, below the @IBOutlet
for the UILabel
, add this line:
private var userProfileSubscription: AnyCancellable?
This will hold the result of .sink
. Here is the completed prepare
function code:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let destination = segue.destination as? UserProfileViewController { userProfileSubscription = destination.userDataPublisher .sink(receiveValue: { (userProfile) in print("Received profile: \(userProfile)") self.yourNameHere.text = "Welcome, \(userProfile.firstName) \(userProfile.lastName)" }) } }
So what’s happening?
When the button in the UI is tapped to trigger the segue,
prepare
is called.The
UserProfileViewController
is assigned to thedestination
.A subscription is created to the
userDataPublisher
inUserProfileViewController
.The subscription is stored in the
userProfileSubscription
so it can be cancelled but more important, it will not disappear when theprepare
function ends.
So now you can run the app.
Tap on the User Profile button. The sheet slides up.
Fill in the first and last name fields.
Tap Save.
The first and last name are printed and appear in the
UILabel
.
Instead of using a delegate, you have used Combine. You published the data you wanted to transfer and picked that up by the ViewController
.
The benefits to this are not obvious at first, except maybe that there is a good separation of the two parts (ViewController
and UserProfileViewController
). Any part of the app that has access to the publisher can subscribe to it. If the user’s profile is something that could change at any time from anywhere, you might consider moving the publisher out of UserProfileViewController
to some place more universal and the view controller just becomes the interface to publish the changes and multiple places could pick it up. One place might change a greeting on the main navigation bar. Another place might store the profile in a database locally while another part of the app sends it off to a remote system. That just is not easily achieved using delegates.
Completion
The thing about publishers is that they keep publishing while there are subscribers and while they haven’t completed. Right now, the userDataPublisher
sends out the UserProfile
and then the UserProfileViewController
is dismissed. A better way is to send all the data and when finished, send a completion. Open UserProfileViewController
and go to the saveProfile
function and add this line right below the send
call:
userDataPublisher.send(completion: .finished)
What this does is send a notice of completion to all subscribers (aka, ViewController
) that says “this is all I’m going to send, I’m done.” Now back in ViewController
, change that .sink
expression to this:
.sink(receiveCompletion: { finalResult in print("Final Result is: \(finalResult)") }, receiveValue: { userProfile in print("Received profile: \(userProfile)") self.yourNameHere.text = "Welcome, \(userProfile.firstName) \(userProfile.lastName)" })
The .sink
now has two parts: one to handle the completion event and one to handle the received value. In the receiveValue
part, the UserProfile
is put to use while in the receiveCompletion
the finalResult
is just printed. If you run this now, fill in the first and last names and pick the Save button, not only does the UserProfile
get picked up, but the send(completion: .finished)
is picked up in the receiveCompletion
part and “finished”
is printed.
This is a little more complex, but you’ll see below how useful this can be. In the meantime, remove the dismiss
call from the saveProfile
function of UserProfileViewController
and put it inside the receiveCompletion
handler.
Since the ViewController
is responsible for presenting the User Profile sheet, it should also be responsible for taking it away. A great place to do that is in this completion handler of the .sink
.
.sink(receiveCompletion: { finalResult in print("Final Result is: \(finalResult)") self.dismiss(animated: true, completion: nil) }, receiveValue: { userProfile in print("Received profile: \(userProfile)") self.yourNameHere.text = "Welcome, \(userProfile.firstName) \(userProfile.lastName)" })
If you run the app now, it works just as it did before, but the Save button code won’t dismiss the sheet; it gets dismissed as a result of sending the completion. Think about this for a second: the sink’s receiveValue
is called each time the publisher has something to send. The receiveCompletion
is called when a finish
(or a failure
- more on that below) is sent; this also closes the publisher and prevents any more things from being sent. It gives you an opportunity for clean up: close a dialog (like here), clear memory, save data, etc.
Improvements
While things are working, this is not the best way to implement it. One problem is that any object that gets hold of the userDataPublisher
can publish UserProfile
values. You really do not want that. Instead, for this example, you want only the UserProfileViewController
to publish and ViewController
to subscribe but not have the ability to publish.
Open UserProfileViewController
again. You’re going to make a bunch of small changes which ultimately hide the actual publisher and offer a proxy instead which can only be used to listen for events.
First, rename userDataPublisher
to userDataSubject
and make it private
as in:
private let userDataSubject = PassthroughSubject<UserProfile, Never>()
Ignore the compiler warnings and errors and add a new userDataPublisher
:
public var userDataPublisher: AnyPublisher<UserProfile, Never> {
userDataSubject.eraseToAnyPublisher()
}
From ViewController
’s point of view, nothing has changed. The userDataPublisher
is still a publisher. It’s a ‘generic’ publisher that sends out UserProfile
s and never fails. The benefit here is only UserProfileViewController
can send events due to userDataSubject
being private. The generic AnyPublisher
can only publish.
One more thing: the saveProfile
has to use userDataSubject
and not userDataPublisher
so change that over, too.
When you run the app, it behaves just as it did, but now the publisher is more secure.
Errors
Up until now, the publisher sent out UserProfile
s, but not errors. But what if the stuff you are having the user enter has an error in it? For example, they forget to enter the last name.
When you use delegates, you can handle this a number of ways: allow empty strings or nils to be sent through and let the ViewController
worry about it. That’s not really good. A better way is to make use of the Error portion of Combine publishers.
Go back to UserProfile
and add in this custom Error:
enum UserProfileError: LocalizedError { case firstNameMissing case lastNameMissing var errorDescription: String? { switch self { case .firstNameMissing: return "First Name is missing" case .lastNameMissing: return "Last Name is missing" } } }
Go back into UserProfileViewController
and change both the userDataSubject
and userDataPublisher
lines by replacing the Never
error type with UserProfileError
as the error type:
private let userDataSubject = PassthroughSubject<UserProfile, UserProfileError>() public var userDataPublisher: AnyPublisher<UserProfile, UserProfileError> { userDataSubject.eraseToAnyPublisher() }
You have to tell what you are publishing: the type of data (UserProfile
) and the type of Errors (UserProfileError
). Now make these changes to the saveProfile
function:
@IBAction func saveProfile(_ sender: Any) { // create a UserProfile from the text fields and publish it if let firstName = firstName.text, let lastName = lastName.text { if firstName.isEmpty { userDataSubject.send(completion: .failure(.firstNameMissing)) } else if lastName.isEmpty { userDataSubject.send(completion: .failure(.lastNameMissing)) } else { let profile = UserProfile(firstName: firstName, lastName: lastName) userDataSubject.send(profile) userDataSubject.send(completion: .finished) } } }
Rather than just blindly send out whatever is entered into the first and last name fields, we’re going to check for validity and if either one is missing (blank), a failure is sent rather than a UserProfile
or the .finished
completion.
Think about this for a minute: when the data is good, you
send
it. But if there’s a problem, youfail
it. And you can fail it with a customError
that can be as simple as static cases or it can include more data and information; whatever you need to convey the problem.
This gets handled in the .sink
’s receiveCompletion
handler back in ViewController
:
if case .failure(let error) = finalResult { let alert = UIAlertController(title: "Missing Data", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) }
I think if case
is a new construct for Swift so its syntax is a little strange, but it lets you test enum values which are not Equatable
without a full switch
statement. If the finalResult
is a failure
, this will post an alert with the message from the error. If you run the app again and leave out one of the fields from the sheet, you’ll see the Alert pop up.
Summary
What I’ve done is abolish the Delegate Pattern, at least for simple things. If you have controllers with complex delegates, Combine might not be the best thing, but keep in mind that Combine can do some powerful stuff (like filter
and map
) or perhaps you will need several publishers. Making your app more reactive should also make it less vulnerable to problems and more easily scalable.
This is the complete ViewController.swift
file. You can see that in the .sink
completion, the dismiss
function’s completion handler is used to examine the failure. This makes sure the sheet has been removed before posting the Alert; iOS gets touchy about having multiple view controllers showing up at the same time.
import UIKit import Combine class ViewController: UIViewController { @IBOutlet weak var yourNameHere: UILabel! // must retain the subscription otherwise when `prepare` function exits the // subscription will cancel. private var userProfileSubscription: AnyCancellable? override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. title = "Food Facts" } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let destination = segue.destination as? UserProfileViewController { // when the segue is about to begin, set up the subscription to the publisher // to get the user profile data userProfileSubscription = destination.userDataPublisher .sink(receiveCompletion: { finalResult in self.dismiss(animated: true) { // using a custom Error you can give very specific feeback to the user if case .failure(let error) = finalResult { let alert = UIAlertController(title: "Missing Data", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) } } // get rid of this dependency self.userProfileSubscription = nil }, receiveValue: { userProfile in self.yourNameHere.text = "Welcome, \(userProfile.firstName) \(userProfile.lastName)" }) } } }
This is the complete UserProfileViewController.swift
file.
import UIKit import Combine class UserProfileViewController: UIViewController { @IBOutlet weak var firstName: UITextField! @IBOutlet weak var lastName: UITextField! // keep the publishing subject private so nothing else can publish data to it. private let userDataSubject = PassthroughSubject<UserProfile, UserProfileError>() // make this generic publisher availble instead and it can only be used to // listen for changes public var userDataPublisher: AnyPublisher<UserProfile, UserProfileError> { userDataSubject.eraseToAnyPublisher() } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func saveProfile(_ sender: Any) { // create a UserProfile from the text fields and publish it if let firstName = firstName.text, let lastName = lastName.text { if firstName.isEmpty { userDataSubject.send(completion: .failure(.firstNameMissing)) } else if lastName.isEmpty { userDataSubject.send(completion: .failure(.lastNameMissing)) } else { let profile = UserProfile(firstName: firstName, lastName: lastName) userDataSubject.send(profile) userDataSubject.send(completion: .finished) } } } }
This is the complete UserProfile.swift
file containing both the UserProfileError
and the UserProfile
itself.
struct UserProfile { let firstName: String let lastName: String } enum UserProfileError: LocalizedError { case firstNameMissing case lastNameMissing var errorDescription: String? { switch self { case .firstNameMissing: return "First Name is missing" case .lastNameMissing: return "Last Name is missing" } } }
Using Targets to structure your IOS app
Using targets to separate your concerns.
Just thought I’d pass this concept along. If your app is big (code-wise) or is going to be big or you think it may become big, here is a technique to help you manage all of the parts in a nice, testable, compact way.
When you first start out writing iOS apps, you may have a vague notion about “targets” which is, simply, the thing that gets built. In other words, your app; the target of the build process is your app. You may have asked yourself, why would I want to use multiple targets in a project? One reason may be you have a Watch app to complement your app or you may have a Widget. Here, though, I will show you how you can use targets to organize your code to make your app more manageable.
I don’t have a GitHub repository this time, because the code itself is almost irrelevant. Its the concept here that’s important.
So imagine you have a travel app. The app has four distinct parts: booking a flight, reserving a hotel room, renting a car, and managing your travel account. For the sake of making my point, further imagine each of these parts has its own tab in the app. It is not important to this technique that your app use tabs but it is important that you divide your app into separate functional bits. Using tabs as an example makes it easier to conceptualize.
When you think about this app, it’s pretty clear that there are four distinct UI pieces. But there are a lot of things each piece shares. For example, string, graphic and color assets. Your app might also need to present a login screen at any time on any tab. Your app might have extensions to classes like Date, String, or View that are used on all of the screens involved with each tab.
Your app could be divided as follows (not comprehensive, just a sample):
Common Code
Assets (string, color, graphic)
Services (connection to remote systems)
Class extensions (Date, String, etc.)
Data models
Shared views (calendar picker, price calculator, chat with travel agent, login)
The App Itself
AppDelegate
ContentView
Booking tab
Booking View
Flight schedules
Flight selector
Hotel tab
Hotel View
Reservation systems
Car rental tab
Car Rental View
Car rental company selector
Rates and discounts view
Account tab
Profile View
Preferences and personal settings
Payment information
One more thing for your imagination: can you see developing each of things things almost independently? For example, booking flights by designing the screens to do that, updating the common data models, sending and receiving data between the app and the backend booking systems. That set of views that make up the booking tab is almost in and of itself, an app.
And this is where Xcode targets come in. Each of those bullet points above could be an Xcode target. Doing that separates the parts and allows each part to be independently built and tested. Each time you create something you can ask, “Is this going to be used exclusively here or could it be used in another part of the app?” If the answer is the latter, then you put that into the Common target.
Doing this is easier than you might think. If you already have an Xcode project set up to build an iOS app, follow these steps to add another target:
Select File-> New -> Target from the File menu.
In the dialog that appears, scroll down to the Application section and pick “App”. If you are a seasoned Xcode you, you could actually use something else, but picking an actual App as a new target allows you to run your code on its own. For example, as you build the Account target, your app can log into the backend system, create a user, update it, etc. You do not need to launch the whole (Main) app to test these pieces.
On the next screen, fill in the name (eg BookingModule or BookingTab) and pick Finish.
Your target is now part of your Xcode project. If you look at the schemes menu, you will see each appearing which means each can be built, tested, and run separately.
Go head back to the Common target which you can add in exactly the same way. The purpose of this target is to be a single place for all of the code that’s shared between the other targets. To use any of the code in this target (Common) with another target (say, Booking), you need to make sure it is included in the build for that target.
Pick a file in the Common target and open the File Inspector (options+command+1). There’s a section in the panel called “Target Membership”. The Common target will already be selected. Check the box next to all of the other targets that will use this common file.
You may have heard or read that building your own SDK is a good idea (such as the code that would be in the Common target of this example). And it is - it certainly makes building your app faster as the code in the SDK is built once, on its own, and just linked into the app. That would be the next level beyond this technique and if you want to do that, go for it!
Once all of the files are selected to be included in the right targets you can assemble the app in the App target. This will be the original target that is the app itself. It will be just like building an app normally, except most of the files will reside in other targets.
The advantages to this technique are:
Forces you to think about your app as an assembly of parts.
Each part of the app is nearly independent and can be run and tested, to some degree, without the other parts. Don’t under estimate the value here. While you develop the hotel reservation target, your QA team could be testing the flight booking target.
Organizing common code into its own target makes it easier to update as the changes apply across the app.
Faster on-boarding of new team members and its easier to divide up the work.
How you divide your app up is, of course, up to you. But if this gets you thinking about code organization - even if you just use folders instead of targets - then it will be worth it. New members to the team will have a much easier time figuring out where things are and how the app works.
Happy Coding
Adventures with React Native
Some considerations for you if you are thinking about using React Native.
The promise of React Native
The goal of React Native is to answer a high-tech Holy Grail: Write Once, Run Everywhere. And by and large, that is true, at least for iOS and Android which are the only platforms I used with it.
My exposure to React Native (RN, henceforth) is limited. I am not a professional RN developer and I do not know all of the ins and outs of using it. But the 6 months I spent with it were interesting. I started at a small company as the new (and only) mobile developer. I took over the RN mobile app someone else had written. They wanted some new features and to see if its performance could be improved. I was hired for my iOS and Android experience because the CTO thought the RN application should be scraped and re-written natively for iOS and Android. I just had to make some improvements before I could begin the re-write.
Their app uses a complex data set, real-time data feeds (via WebSocket), maps, lists, barcode scanning, and some other things. The data can be just a few hundred items or it could be thousands.
React Native apps are mostly written in JavaScript (the native parts are written in the platform’s code - Objective C or Java - and then bridged into the JavaScript). When you start a RN project you have RN generate the actual platform-specific application which forms a container to host your “application” (JavaScript) code. Its essentially a JavaScript interpreter that uses the native bridge to get components to appear and input to be received. For all that it does, its actually quite fast. But remember, your application’s code is not being run directly on the hardware, it is being interpreted (well, translated to byte code, then that’s run) within a virtual machine.
In some ways, React Native is like Android itself, which runs on a variety of processors. Your one Android application (in Kotlin or Java) is executed in a Java Virtual Machine which as been built specifically for the hardware of the device.
The company’s app’s performance problem stemmed from the very fact of RN’s existence: the code is not run optimally for the hardware. Imagine downloading 2,000 items and then rendering them onto the map. Then moving the map around, zooming in and out, etc. Very very slow. Plus there were lots of graphics going on. It was just a performance mess.
Getting Started
I mentioned above that when you start a React Native app you have RN generate the application shell - the actual native application which runs on the device and hosts your JavaScript app code. That’s true, but there’s more to the story.
When you write an iOS or an Android app, you start off by getting the respective IDE: Xcode for iOS and Android Studio for Android. That’s it. Download those, make a quick test app, run it, and start building. You may need some more complex software that you can purchase or find for free (like a barcode scanner) if you do not want to take the time to build it yourself (which, in these environments, is always an option).
When you want to start off with React Native you need a small army of new programs: brew
, npm
, node.js
to name a few. That plus React Native itself. If you just recently upgraded your macOS, you may find the versions of these haven’t quite been upgraded to match, but generally these are pretty solid.
Once you get React Native to generate its shell program you’ll find your directory divided into two parts: one for iOS and one for Android. Inside the iOS directory is a real iOS workspace and project. Go ahead and open that. What you’ll find will be a surprise. This is a very large app (after all, its a full blown advanced JavaScript interpreter with code to host and bridge to native modules). But what I found the most surprising, and the first disappointment, were the nearly 900 compiler warnings. Most deprecation warnings, but also syntactic irregularities. And it was all Objective C. It made me wonder if anyone was actually working on this app full time to remove those because, to me, compiler warnings are not show stoppers, but they should be addressed. I like delivering apps without warnings if at all possible.
True or False
When you build a RN application you run into a lot of things you need. For example, we needed a barcode scanner and access to the camera for both the barcode and so the user could take photos. After doing some digging I discovered this entire world of React Native developers. Basically, if you need to do something in your React Native app, someone has probably already written a plugin to do that. For this article, I’ll focus on the barcode scanner module, but what I’ll describe happened with a couple of modules.
I found a couple of barcode scanner modules for React Native. One thing to look for is the platforms they cover; sometimes the author only supports one platform. When I found one that looked promising I created a new separate RN app to test it out. Good modules come with complete instructions: how to incorporate the module into your code (for Android, change the gradle.build
file, for iOS change the project info.plist
for instance or some are even simpler). Some modules leave it up to the interested reader to figure out the installation.
I cannot stress enough how grateful I am for stackoverflow.com
; a troupe of heroes.
Once I got the module running in a test application and understood how to use it, I replicated the installation into the main application and hooked it up. That worked for the most part EXCEPT in this situation: The company’s app already existed which means its React Native directory/codebase was generated months earlier before I started - it was all in git
for me to just clone locally. My test RN app was recently created. I was using a newer version of a RN app in my test than was being used in the company’s app. In other words, version incompatibilities.
I had to tinker with the code from the barcode scanner module to make it compatible with the RN application code for the main app. Most 3rd party apps come with their source code, but it was the wrapper code to fit it into the RN environment that was the cause of the problem.
This brings me to the actual heart of this article: upgrading.
Upgrading
During the course of my involvement with the company’s original RN app - and early on in the involvement - I accidentally let my computer upgrade Xcode (from 9 to 10 at the time). I didn’t think much of that until I tried to run the app. This began a journey into a land of frustration like no other I have experienced in my years of writing software. Were I a better writer, I could craft for you an Orwellian tale to make you lose a few nights of sleep. But I am not, so I will keep this as brief as possible.
When Xcode upgraded it also upgraded the iOS SDK which brought with it some deprecations but also a few out and out changes - you know, when the deprecated code is finally dropped.
The React Native version used to build the app used some of that now defunct code. Which meant it failed to build. After searching on stackoverflow.com
I found a couple of workarounds: update to a newer version of React Native or go into your RN code and change it. Now keep in mind that this is code that is generated and could be replaced at any time should you need to rebuild the directories; you really should not need to edit it.
I was able to make the changes to the RN code and move on. But that only lasted a few minutes because several of the many (many) 3rd Party modules used to build the app also had similar problems. One fix for one of them was to just get the newest version of that module. In my naiveté I did that.
This started a chain reaction of updating modules because module A now didn’t like the dependency on module B which was now incompatible with iOS - I had to upgrade React Native. To do this you change a few control files, erase all of the modules and re-get them (ie, all that work trying to upgrade them by hand goes out the window) and then re-generate the React Native shell application.
Your React Native app may turn out to be highly dependent on community software, written and, hopefully, maintained by people who are often not being paid full time to write these modules. They may have written them to do their own work and donated them or are just hobbyists.
Now as it happens, some of the 3rd party RN plugin modules were no longer being supported and thus, not compatible with the new React Native code. So while some modules were just fine, others were non-functional. For those I had to find newer replacements (recurse to the above section).
The bottom line here: Upgrading your OS or IDE can open a days-long can of worms with React Native. You find 3rd Party modules cannot be upgraded or they are not supported or the authors have intentions to update too, but just don’t have the time yet to do that.
Hello Simulator? It’s Me
Earlier I mentioned you run your app on device simulators (or emulators for Android). You can also run them on real devices. This is all great - when it works. Remember that upgrade process? Well, iOS upgrades also change the simulators. More important - they change the location or name of the simulators. When you go to run your RN app, there are scripts that run which hunt down the simulator you want and launch it. When the Xcode 10 update came through, the names of the iOS simulators changed and even the latest version of RN that I upgraded to had no clue where these simulators were.
So again, back to stackoverflow.com
for the answer: edit the RN execution script that finds the simulators and change it so it detects the simulators. And that was fine for a while until another update to Xcode ended that and I had to go back and change the script again. Keep in mind that should I have had to update React Native again, I would have had to change this script.
Debugging
When you use an IDE on a native application, you just set a break point right in the code and launch the app. The app runs until it hits a break point then you can examine the current state, etc.
You cannot easily do this with React Native. And of course, there are several ways to get debugging to work. The easiest one I found was to open Chrome
and attach it to the JVM running inside the iOS simulator (I could not get it work with an Android emulator). You could then set a break point in the JavaScript code and most of the time it would work. Sometimes (like 40% of the time) it would break inside minified code which is always fun.
There is a React program you can use but I never had much luck with that. I think it was more suited to web-based React than React Native.
The starting up of the debugger (you had to get the incantation just right or it would not work), the attachment to the process, the running of the React Native mobile environment, hoping your break points were still there (often you have to reset them), was just another frustration.
Thoughts
At this point you are probably thinking I am not a fan of React Native. And you’d be mostly correct. Following my experience with RN, I got to re-write the company’s app natively for iOS and Android. For Android I chose Kotlin rather than Java and for iOS I went with SwiftUI since it was just out and looked super cool - PLUS - it is a reactive framework and that is one thing I really liked about React. In fact, I liked the state flow so much I used a Kotlin version - ReKotlin
- in the Android app.
Oh - and I stuck with iOS for this blog post, but I also had similar issues when it came to Android and updates to gradle
.
The promise of React Native: Write Once, Run Everywhere, holds true to a point. And that point is upgrading. Once your app has been written for a while, along comes an OS update and an IDE update. This can send you into a tailspin. You have React Native core code that becomes unusable. You have 3rd Party modules that are incompatible with newer version of RN itself. At one point I thought it was hopeless because it was a vicious cycle of changes.
My final take away for you is this: if you use React Native, understand that for long haul, its an investment. I see the appeal as I wrote the same app twice for different platforms. If there is a problem I may have to fix it twice. But the upgrades are easier and the app is way, way, faster. I like Kotlin and Swift far more than I like JavaScript, which is just a personal preference here. So for me, the tradeoff of a single code base versus multiple code bases is worth the effort. Bonus if your company has both iOS and Android developers!