Zouhair Mahieddine

Migrating Your Legacy Code to Local SPM Packages - An Iterative Approach

Swift PMArchitecture
Transcript

I hope you're doing okay, I'm a bit stressed but I'm really happy to be here and I want to start by thanking the people at FrenchKit for having me here and letting me talk to you a bit about how you can use local packages to modernize your code base and try and modernize it. And the second thing I want to say before I start is I'm going to show you some weird stuff, as in I'm going to show you some Objective-C code and I'm really sorry about it. I'm going to show you a project that looks like what we had at Medium, I'd say like two years ago or something and I'm also going to show you some ways to make it better. That might not be the way you would do it. But I'd like you to keep an open mind and look more at the approach and the strategies and see how maybe this could help you and maybe some hints and tips that I'm gonna unveil might be of some help.

Alright, cool. We're gonna start so why would you modernize the code base? That's a simple one. The why is easy: you want to make it more robust, you want to make future changes faster and easier to implement and one thing that's really important when you work on a team is you want to make it easier for collaboration, you want to facilitate that and modularising it as we'll see really help this because you're gonna put things into little modules are going to be independent; and maybe one of you is going to use the module and maybe another one is going to work on the innards of the module, but you won't conflict all the time. And then the other question is, when do you modernize the code base? And I don't know, maybe you thought about going to the bigwigs at your company and telling them, okay, you want me to implement all of these new features and solve all of these things, but the code base is crap. So I'm gonna need six months to rewrite everything and then we're good. We're gonna ship new stuff. Well you're gonna probably get told off and if you're not told off, two years from then you're going to still be into this rewriting. So my opinion on this is every chance you get, you just clean your code base, you should try and modernize it; maybe add some tests, modularize it, etcetera. And you want to do it in as small iterations as possible because you're continuously releasing to production... or at least that's my life. And I guess it's a lot of yours also.

And so that's when local packages come in and can be really useful because they're independent modules, which means there's an enforced separation of concerns, you're gonna only make public what you want to make public and the rest of it, the innards are going to be just like in there and none of the clients will see it, and that makes them easier to set up, easier to maintain. And it's also a really small initial footprint. That's what I'm going to try to show at the beginning of this: it 's that you don't need much to start modularising and hopefully it's gonna help you, like, get into it easily.

Alright, so a bit of an acknowledgement, I'm going to show you something, something that we've been doing at Medium for two years and obviously it's not just me, it's a whole team of people working at the company right now and people who have been working at the company in the past and yeah, I'm just thankful that I'm the one who's showing you all of this because I definitely didn't build all of this. So thanks Thomas, thanks Alain, thanks to the former team, Logan, everyone. Thank you.

Alright, cool. So what would you expect? So I'm gonna have some slides talking about some of the concepts is going to be really simple. Nothing really advanced. I wanted to keep it like day to day stuff and then because I'm a bit stupid, I'm going to do some live coding, so be be nice and would be nice. Also, we'll see how it goes if you want to follow along and if the gods of WiFi are nice, you might want to clone this repo and there are some tags that I'm going to highlight during the presentation that you can follow, but everything will be available at the end anyway. And the idea for this is I wanted it to be less and less familiar. So the first thing I'm going to show up probably like day to day stuff that you know already and hopefully till we get to the end, you're going to learn a few stuff. We'll see. So basically my goal was each of us will leave this room having learned something from a presentation and that's a lot of pressure.

So, okay, did you know that Tim Burton's movie Mars Attacks was inspired by a set of unsettling trading cars from the 60s. They're horrible. And I put in just the nice ones because I want to show that presentation to my kid at some point; they're horrible. I didn't know about them. We've all learned something. Pressure off.

Okay, let's go. So the starting point is, in my opinion, the one that you always get, you just get handed a project, you need to maintain it. You need to release stuff. Product features, bug fixes, et cetera, but you also want to reduce the tech debt, you want to make your life easier. You want to modernize the code base. Let's take a look at this project.

Alright, so simple app. I'm really happy because I get a job at a company that makes that jokes and I love that jokes. So this is just an app listing dad jokes, you have a nice pool to refresh to get some new dad jokes in and you can have some of them (and they should all be safe for work by the way) and then you can remove some of your favorites when you're tired of reading them. That's it. That's a simple app. But because it's a project that's been on for a few years, the code is like a bit of everything. I'm not going to show you everything that's in there, but basically you have some Objective-C you have Swift, you have UIKit, you have SwiftUI, you have a set of unit tests that are alright but like for example, this is the API service, I'm loading everything from the bundle because again, WiFi, but it's just a fetch of the top rated, you get them and then there's a bit of Objective-C prefix method in there, because I can't use the Swift errors. So people who built this said, okay, we're going to keep some of the clean stuff and we're gonna expose to Objective-C something that works to Objective-C. That's how it's done. You have a small design system here which is just colors, exposing Objective-C that's used in Swift and Objective-C, it's a bit of everything.

Okay, so that's the project,

You get handed this one, as I said, it's a mix of everything and you want to put on some sort of strategy to go at it, you just don't want to go into Objective-C code when you have to modify it, you don't want to just use what's in there: you want to clean it along the way. So the idea for this, or at least what we try to do at Medium, is we wanted to set up the minimal stuff that we need to have to start modularizing it. And I'm going to try and show you this live in a minute and then you want to do the like the smallest iteration as possible because you want to see them through including unit tests.

You want to take some piece of code module, arise it and leave it in a state where people after you, your colleagues or even yourself in a few weeks are going to be able to use it and you're not going to constrain future iterations on this module, you're gonna leave it in a state that's both usable and manageable. And we have a few optional rules, that's actually rules that we use at Medium, is: we don't want to reduce the test coverage which is always the easiest past just to remove the tests because you change everything. We want to always be writing Swift. So that means if you have Objective-C code that works and you want to leave it there, that's okay; if you want to interact with the Objective-C code, what you write to interact with should be isolated. You should keep it on the side and you shouldn't pollute your new Swift code with Objective-C stuff or, like, not to the extent that is going to be everywhere, basically.

So the plan for what we're going to look at today is we're gonna set up an initial local SPM package, we're gonna see how it interacts with schemes and test targets. We're gonna see how the linking is done. Actually these days with Xcode things are pretty pretty okay. What I'm going to show you runs on Xcode 14 and supports iOS 14 and higher, so it should be okay if you're using old versions of Xcode, don't come to me, I don't know how it works. And the first package we're gonna do is just take this design system that I showed you, put it in isolation and then start using it in Swift and Objective-C in the main project. Then we're going to do it with the API services and this time we're gonna try and modernize it a bit, maybe use some async await and I'm going to try and show you some cool stuff about using the async await with Objective-C (don't throw anything at me please!).

Alright, so initial setup: basically these days it's really easy to almost one click away to just set some local packages into your project. Xcode takes care of most of it. And the bonus part, if you don't know about it, is that packages, even when you're using them within the project, is just based on the file system structure and the Package.swift file. So if you're tired of Xcode profiles, xcuserdata or whatever, you don't have any of this. Well you have a bit of this, but anyway.

So let's try and do this. I have my project in there and I just want to start with an initial setup so I can modularize some of the stuff. I'm going to go there and just select "new package"; I'm gonna call it local packages because as you'll see we'll add different packages in there and then I just need to find where this moved and put it in the same repository. I don't want to have to play with repositories or anything. Link it to the project, put it at the root of the project and that's it. That's almost it. But that's it.

I have a first package that was created automatically that just has struct with a "Hello World". I even have some tests for it. And the nice thing is, Xcode created a scheme for me so I can basically just run the tests. So that's it, really in two clicks. I get a local package set up if I want to start using it, I'm going to take Swift class because it's easier and I'm going to just try and print the content of this local package: all seems to work. But obviously if I try and build this on the right scheme it won't work. So I'm going to import this package and obviously it still won't work, but for a different reason, now there's an undefined symbol. That's the only step that you still need to do: that's old school xcproj stuff. Is that in these frameworks and libraries and embedded content, you just need to link it and you'll have to do this for all of the modules that you add. But if I do this, I'm good, I'm not gonna show you yet, but trust me, it works.

Alright, so that's the first step, that was simple enough. And now we have our setup, we have a package that does basically nothing but we can start adding stuff to this setup. So we're gonna start with the design system and basically it's super easy to add new independent modules these days, they're gonna be directly accessible from Swift code in the main project in modules and you'll be left with a choice if you have Objective-C code in your project either, you write your module to be available from Objective-C you'll have a lot of the Objective-C requirements, you will have like something that you can do, like structure items but you'll be able to do it.

So how do you add a new independent module in your package set up? Well, it's very simple. You're just gonna go add a new folder because as I said, this is just file system. I'm going to call it "Design System". I'm going to put a new file in it and again, this is a really stupid design system, it's just a set of colors, making my life easier for the presentation. Yeah. Okay. I'm having a design system here and because it's just file system and packages of Swift, I'm gonna add my new design system as a product of my package set up. I'm gonna call it "Design System" and I'm going to declare a target for it and this target here will have the name of the folder that I had before and that's it with that and a bit of resolving done by X code. I get a design system package If I had added a test folder with the same name suffix by test. I would also get for free test set up in the scheme and I would be able to run some tests. I'm not going to do it here and let's move on to taking some of these colors. So I want to migrate this one. For example, I want to do it in a really simple way. There's multiple ways to do it, but I want to do it in a really simple way with Swift. So I'm gonna just create an enum, I'm gonna call it "colors" and I'm gonna put a static var in this in this enum called accentTint, which is a UIColor. And if I remember where well this is .systemTeal and that's it. I have a set of colors.

Now obviously I want to make this public because everything in your package is internal by default. And so it's not going to be visible in the outside world. And then an easy way to do this is just to say, okay, I'm just going to remove this and let the compiler tell me what I need to do. And obviously if I'm not in the right scheme, the compiler won't tell me anything. I'm gonna import my package and here same thing in there and get nice auto complete could get nicer to complete an extension on your eye color, but I wanted to show something a bit different and I get this identifying symbol again, obviously I forgot this one while on stage. Easy enough. I'm going to add it here. Good thing is these days, the compiler tells you about these things at some point in the past, you didn't know, but okay, this builds and I have my first module, it's quite easy because this design system can now evolve and leave, and there can be new stuff in there, it's always going to be there, it's always going to be usable and contained. Now, let's do something a bit more complicated, which is taking one of these colors that is actually used in Objective-C. I know this one is, and I know if I remove it right now it's gonna scream a bit, yes, it does in Swift and in Objective-C. And so if I add this here and do exactly the same thing, and I don't remember tertiary system background and I start using it in the Swift part of the code, obviously it's going to work and I need to do this because I linked the design system already, but in the Objective-C part, it doesn't and it doesn't because this is a Swift enum and none of this is marked Objective-C. So I could do something like this and add this everywhere, and to make it a class, and because its Objective-C I need to make it in a NSObject and yeah, okay, cool, that's gonna work. I can go in there now, I can import my design system this way, it's visible.

Alright, this thing is that's a little bit different. It's this, it's what we call a bridge, kind of like the bridges that you have for Objective-C and Swift and built-in in Xcode, but it's something that we built ourselves and the idea behind this is that I want my design system to stay clean. I wanted to stay in in like this, but I want it to be available in Objective-C when I need it because I don't want to touch this Objective-C code as long as it works, but I want it to work. So basically what happens is: you can remove all of the Objective-C. That you have as long as you're in there and you're exposing only what you need to Objective-C. The good thing about this is that as soon as you get rid of the Objective-C. Code you can get rid of this bridge and you're fine. That's just a strategy that we've been using and it's been working pretty well because then you're left with the Swift code. That's nice and clean. Alright, cool.

Next step is we're gonna try and do the same thing with the API service. It's going to be a bit different because the API service obviously exposes model objects. So we're gonna need to have our model objects as modules and we're going to need to be able to link them. That's simple enough. That's the same package that we saw. Except there's two new libraries in there, the model and an API, and if you want your API to see your model you just do that. That's a good opportunity to clean all of the things that I showed you before and maybe move to async await as we saw in some of the talks during the conference. And so we're gonna take this with all of the Objective-C specific code, et cetera and we want it to look like this. A simple async await: I won't dwell on this, but basically we're going to go from that amount of code to that amount of code and something that's a little easier to read.

But to do that, because I told you I'm not using any real API in there and I only have resources: I need to have resources in my package. And again, the way you do it, just to make it clear, it's really simple is: you declare your resources this way and then you can call Bundle.module, which is pretty cool; Bundle.main, Bundle.module: easy enough to understand. (If you want to talk later about how to use module resources into your main project, that's something else). But anyway, that's the idea.

The last thing is, well, I removed everything that made it available to Objective-C. I put it in the module, I put the resources in the module, how can I get this to be available in Objective-C code? And there's one neat trick for it that we're gonna look at right now and the idea for this is that if I go to there, I have everything that I showed you, I have my API service in there, that's an async function. I have my throw, I can use it as a try await somewhere else. But the actual Objective-C code using it is here and was using the old stuff with a completion block. I'd like to get access to this, how do I do it? I'm just gonna move to the next step and now I have it. It's named the same way as before, but has a completion handler. And how do I get this? How do I get all of the benefits of kuwait but still can use it in my Objective-C code if I don't want to touch it. Well, I built a bridge for the API and there's nothing in there. Basically, if you're using async await in Swift and you want to use it in Objective-C code, you're going to get a completion handler for free, and that's pretty cool if you still have to deal with the Objective-C code because that's just the normal auto completion: it will take your async await code in Swift and... [Zouhair struggling with Xcode]. But trust me, this is just like this is just this signature turned into Objective-C, which is pretty cool.

Okay, one last thing I want to talk about. Um if you looked at this and if you've played a bit with interoperability, you like probably had some issues like this, cycling thinking issues or cyclic dependency issues. Right now in my project, I have this for one small reason, which is the code that's using the Swift. Api is an Objective-C. But the tests are in Swift. So all of this won't work if I just use it like this, that's some quirks, like there are still some quirks in there. But basically what happens is that my Swift code is in a module that gets imported into Objective-C. And then this Objective-C code gets imported into Swift in the test to be able to use it and so the compiler doesn't know what do I build first to able to Swift, Objective-C. Who needs whom? And that's a cycling dependency. There's one way to fix it in Objective-C. That's a bit ugly, but in my opinion and and my team's opinion, it's alright because again, you don't want to make your Objective-C code better. At this point, either get rid of the Objective-C or like tell yourself that your tests are doing the work and you shouldn't take care of it. So basically what you can do, and that's when you don't throw anything at me please, is I get some type checking in my Swift now; I shouldn't, but that's because I removed all information about this Swift code in there and at least it works.

I get to the end of it without most of the thing crashing, so I'm happy about it.

What did we cover? We covered how to get a minimal setup. If you if you let anything, just one thing after this talk is it's really easy to start, it's really one click away from starting and then choose what you move and as soon as you've moved it, you're going to see that it makes things a lot simpler for you and your team. We've seen how to isolate the code. We've also seen how to bridge to Objective-C. Because trust me, when you get to a big project, you don't want to have all of these, that Objective-C stuff everywhere.

You want your new code to be clean and just some bridges because you're still not completely there, you still have Objective-C code. Don't lie: you still have it. And I think the way bridging to Objective-C was pretty cool. Also when we discovered this, we were just like to the moon because it was a pain before that and hopefully some tips and hints, and a strategy for iterative code based modernization that maybe you're gonna be able to use.

Thank you for listening.

Greg: We so you talked a lot about those packages. Do you use also a lot of packages in non legacy code situations?

Zouhair: What do you mean by that? Regular? Yeah, we actually use a lot of even external packages, um because it's like, it's really easy to set up if it's external or not external. And one thing we've been trying to do and we're not there yet is completely removed CocoaPods in favor of these, as long as they are available, it's all right. Some of some things we can't do yet, but yeah, we're trying to use packages for basically anything.

Simone: how can you manage a project in which you have lots of packages? How do you basically decide who has the right to create new packages in your team and how can you organize all this?

Zouhair: So the good thing at Medium is we're not a really big team. So like we don't have a lot of process around these decisions and we basically try and use common sense when we're reusing something a lot and when also it would benefit from being isolated; because then, as soon as you start isolating things, you're starting to think differently about what it can access, what it cannot access and what it means. And so when we feel like some piece of code has a meaning by itself, we're going to throw it into a module and we've been doing this for like a year now or something and never did we tell ourselves this was a bad idea to modularize it. Then, another strategy that we're doing is when we're having a hierarchy of modules and we're doing something that's going to have multiple things in there: at some point we're gonna realize that one part of it is getting really big and we're going to extract it into another module as well; that worked pretty well for us for now.

Simone: How many modules have you got?

Zouhair: I don't know. It's split between two of these local packages that I showed. We have a model one with like 10 packages in there. We have like API networking, we have some model stuff and then there's the features and this one tends to get really big because we have one feature package, let's say around 20.

Simone: And does it have an impact on performance and on the startup time?

Zouhair: I don't think we saw any real impact on startup time. It has great impact on performance. It's pretty amazing how it works. And as long as you don't like start importing all of your packages into each other and getting really complex with the with the hierarchy hierarchy tree. I think it's it's just it makes things a lot better.

Greg: Do you have a dedicated repository for all of those modules?

Zouhair: Again, where the philosophy is doing things when we really need them, we never needed this. It's fine for this to live in the repository. And actually modules kind of by that definition tend to be pretty small. And so it would be like it would be a hassle to just isolate them into repositories. It all lives in the same repository. For the fact that the iOS Repository is the only repository that lives by itself at Medium: everything else is in one giant repository, the back-end, the front-end... I hated it at first. But I'm telling it on stage today because I think it's a great idea.

Greg: Android developers are used to use packages. It's kind of inheritance. Do you think the iOS Community should use more packages or should split more code?

Zouhair: To be honest, I don't know how much this is used or not. What we found out when we started doing this at Medium is that there was not a lot of like real life experience or return on experience on these things. So that's why I also wanted to talk about it today. So, I guess, we could do more but it's also been a hassle for years. Like when it started just like Samuel said before it was really complicated. You couldn't really do whatever you wanted with it. It's getting a lot better. So yeah I think like if you're not on the packages train you should get on it.

Simone: A couple of questions from the audience. First one being what's the best strategy to start with? How do you split into different packages when you have a spaghetti code?

Zouhair: So you you need to like use your use your good sense to find a piece of the code. Even a really small one that can be extracted. Like even if it's that's why I started with the colors because, it's really stupid but, as soon as you start to work on a package, you're gonna want to put more things in there. That's because when you start working on a package and testing a package, for example, you refactoring the API service, you have to imagine that you put all of the code in the package and then you only work on the package. That means your your app doesn't work anymore. But you don't care because what you're gonna do in there is going to compile, you're gonna be able to test it, you're gonna be able to iterate on it. And so like start with something really simple and don't try and think too much about the architecture of things, just like do a first module and you'll see the benefits of it pretty quickly and it's gonna become a habit.

Simone: And another one is: are there any downsides to do to what you've shown today?

Zouhair: There are because I'm praising a lot the separation of concerns, the relation of the code, et cetera. But that's one thing you might want to think about when you get to more and more packages is that those cyclic dependencies. I show the stupid one, but you can get like pretty heavy ones and you can get yourself into a situation where one of your modules is using another module but this other module will use another module and at some point it's gonna circle back to the first one and obviously that's an issue that you'll never have if you have everything in your main project. But usually when you get to this issue is because one of the things along the way should be more isolated or it's not in the right place. So it's also a good reminder that these things are isolated for a reason.

Simone: And last question: when you have lots of sub-projects and lots of modules, how do you make sure that all the modules have the same configuration?, For instance, when you have to change the configuration for all modules at the same time. And how you apply those modifications to all modules project-wise?

Zouhair: That's that's the good thing about about the modules made with Swift Package Manager is that SPM doesn't really care about conflicts. At some point it yelled at me because the platform wasn't set correctly. So I was using something that was us 13 without telling Swift package manager that I was only I was 13 or above. But apart from that, if you're used to having multiple conflicts for multiple schemes or things like this, it's it's not a concern anymore. And if you've used the CocoaPods in the past or if you're still using it, you always tend to have these conflict files that are getting bigger and bigger for all of the quirks and bitcode... and I want this in debug, I want that in debug... The only thing that is a bit complicated these days is setup. If you have an existing project and start adding modules is what if I have test files that I want to get exposed outside of this? That's the only thing that's a bit weird in terms of configuration. But apart from that it's all in the Package.swift and the structure of your folders. So it makes your life a lot easier actually.

Edit on GitHub