App Environment, Dependency Injection and SwiftUI Previews

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[Music] in this video i wanted to lay down some groundwork for future videos in which i'll build entire apps today i wanted to cover the use of an app environment this is a a class that basically allows you to via dependency injection define different versions of the external resource providers that your app might use for example doing file io network io and a host of other things for today's video i'll show a simple file client that does reading and writing of files but through the idea of an app environment um if you're interested in learning a little more about that i've learned a lot about it through both um this great video how to control the world by steven selles and as well as the videos on point free in which steven's a part of and have a great set of videos on both that this concept and many many other concepts so let's get started by first creating a very simple file client which will then facilitate the need and use of an app environment so i'll create a new file and we'll just call it file client okay so basically we'll make this file client fairly straightforward as long as i spell it right and um the idea behind these dependency injection structures are that you essentially provide um pointers to functions basically so in our case we want to be able to get the documents directory but we don't want to hard code this we want to provide that implementation after the fact so in this case well we don't have to pass anything in but we'd like to return the url of the documents directory and you'll see later what's very nice about that is you can substitute what is returned by this function and a great example of that might be in previews you don't want to necessarily use the preview or the simulators documents directory which might be hidden in a far away temporary directory you may want to expose that somewhere that you can get to and we'll do that here okay um the other thing is obviously the ability to load a file so we'll say to load a file you have to pass in a url and we'll return the data and in our case we want to use combine and so we don't have to be specific to what sort of publisher will type erase that to in any publisher returning the data or an error for some reason that doesn't work and then finally we'll offer the ability to save a file and in that case we need the url of where to save and then of course the data itself to save in this case the return there's no data saving local files doesn't return anything only returns an error so in this case so there's never any data but there is potentially an error and that's really it for setting this very simple client up now it's a matter of having an implementation so before we get to implementation now we have enough to set up a basic application environment so let's start with that i'll create a new file a new file and i'll just call this [Music] app app environment and uh just in case i don't think we necessarily need here but we'll do that and for this app environment we're going to want to place this inside of swifty swifty-wise environments in order to that it then has to be a class and the reason has to be a class because it has to conform to observable object and observable objects can only be classes they can't be value types it can't be struck so in this case we'll just have for now a single item is our file client like that and because it's a class and because we'll we'll uh as you can see here we need an initializer we can use the convenience function of command clicking you can say generate memorized initializer and that looks at the properties and generates this for you we won't make it internal and i like moving this down just run the key commands there we go there we go so we'll move okay there we go so it's that simple we've got a class and our environment right now only have a single client you'll see in future videos that we'll have a whole host of clients here now it's a matter of kind of connecting this generic basically client i'm sorry environment at this point to our main app and in order to do that what we'll do here is we'll create it so we'll do a state object and we'll save our app environment [Music] say create a new app environment with a new file client and right now we'll just create a blank file client this isn't how ultimately end up but for now that's what we'll do and then we'll just pass it along as an environment object to our uh to our view so let's see if this compiles um okay so what we're seeing here is that we haven't defined file client file client is more here of an interface almost like a protocol that tells that tells us what we need to implement but we can't new up one of these because we would need to pass all of these in now we could we could do that here and that's one of the benefits of this approach is we could actually define all of these functions inline but there's a much nicer cleaner cleaner way of doing that and we'll do it here right now inside of the file client all right so we'll create an extension on file client and what we'll do here is we'll create the um what we'll call the live client in other words the client that actually reads and writes files and does so through the documents directory of the running application so what we'll do is we'll create a static instance of our file client right so we'll call that live and it's of type self it's one of us if you will and what we'll do here is return a new one of a new file client and we'll define it this way so we have three functions to provide here so we have to provide the documents directory directory so we'll implement that right we then have to provide the load file function right and that takes a url in and then we'll have to provide that and then finally we have to implement the save file function and that takes the url in and some data and then we have to save that so that's the implementation of the live version of the file client so now we'll quickly um do that and i'll just paste some some code in for this one basically we're getting the default file manager and we're asking it for the documents directory and then just taking the first value there for load file and for save file we have to return publishers so these are you know slightly more complicated but not too bad let's start by first grabbing the data right so to load a file we'll say let the data be try because data might throw an error the contents of this url okay well this is going to one throw an exception so we have to we have to do try this we have to catch this um this allows us to try this error throwing exception throwing call and if it does throw an exception we can catch it and do something about it but that's still not enough because this has to return because this is how we've defined it we have to return a publisher so the way to do that is we can just wrap this in a future publisher that's that provides data or an error and what futures do is they give us a callback called a promise call it whatever you want but it's effectively a promise that says that when you're finished you're going to call that promise and say that you've either succeeded or failed so in this case we've got our data so then we'll call the promise and with a success of our data but if we haven't succeeded for some reason we'll go ahead and call our promise but this time we'll pass in a failure of our error that's passed in okay um now we could be done the thing here those features get executed immediately even before the subscriber connects to this so what we want to do is we want to defer this loading until someone is actually subscribed and that's very straightforward you just say deferred and that just means that it won't execute that future until someone has subscribed right okay and almost done and that's the whole thing the issue here is as you can see the errors you can't return you could but you can't necessarily return a deferred publisher here because we have type erased that to any publisher we don't want to know whether it's deferred or not or future or not that's really irrelevant to us all we want is the data so in this case we just say erased any publisher and what that does it says don't worry about the type just return it as in any publisher but we know that internally it's actually deferred running a future loading our data okay okay now we'll do the same thing for saving saving we can implement a little differently because we want to know when the saving is done and if we were to use a future the problem here is there is no data so if you have a future with a never of data you can't call it success promise which is no big deal if you want to just save the file and not know whether you've finished saving that's fine we'll just take a different approach here is we'll write the data and then we'll basically return something in both the cases of success and failure so much like the loading we'll start by trying to write our data now the difference here is there's nothing returned by right other than a failure itself so what we'll do is we'll we'll go ahead and we'll go ahead and do a do catch on this and if we fail that's straightforward we'll return a failure failure publisher with the error that was given to us and again because we're being generic here saying it's in any publisher coming back we can't return a failing publisher directly we just have to say erased any publisher okay so now we've returned a generic essential generic failure to anyone who tries to save if the right failed but what do we do if it succeeds again there's no return value so this one is slightly tricky right what do we return here you can't return nil um because in a way that is uh a type of value right and it's expecting a never so one way to do is you can return an empty um an empty object which essentially is a void but there's more to it right we still can't even do that because again a never is the return type here so we can say ignore the output and that essentially erases the output but then we have to also say well it is expecting an error if there is an error and in this case there is not so we'll say well here's the failure type if there were a failure again in this case there is no failure and then finally we have to say and also make this generic erase it to any publisher type so all of this is really to say we have no value and we have no error it's a little verbose to do that now we we could do it like we did here with the future and the difference would be we would just never call promise dot success we would just let it do its saving and that's it again i like this approach in that it allows us to know when the success has happened uh with no errors or we'll get the failure so now what we have here is we have a live file client right we have an implementation of this of this structure think of it again as an interface it's almost like a protocol this interface two file clients says if you implement these three if you provide these three functions i'll now have a full-fledged working file client and we've done that through a static implementation of that that we're calling live okay so all that's to say how do we use that now well now we can come here and say well we have a file client now that's a live file client right so now we can say live and now we know that that is a a client that talks to the file system via the documents directory and what's nice is because app environment takes file client as a parameter type we can just say dot live and that makes it really nice and and clean and straightforward at this point um you might say okay so what's what's the value here well there's lots of benefits there's a whole series of benefits when it comes to testing we'll cover those in another video but for now there's one really nice benefit in that when we're doing previews in swift ui as i mentioned earlier you don't necessarily want to write to the actual documents directory you may want to write to somewhere else that's just for previews so for that why don't we create a very simple preview only file client and then now you can start to see the differences and how you can provide alternate implementations right so let's create another one called info client and we'll call this one what we call a preview so this is a file client that's specific to being used inside of previews so again we'll take the very same approach we'll do that and for this documents directory we don't want to return the system document directory we want to return our own specific path i'll show you that in a minute what is interesting though is you know load file uh works fine here because we pass in the url i don't need to re-implement load file that we've already done before so what's kind of neat here is that you can say we'll just take the live implementation a load file we'll use that same function we don't need to re-implement it and then same for for save file i can say you know what i've already got a good implementation in this case so i'll just use that what's nice about this is i don't have to implement these for previews they work just great i just want to change where the document directory comes from so in a way it's a it's a technique for overriding some or all of an implementation that's already there okay for this one i'll go a little quickly this is just one approach i've taken to how to get to this preview content i'll show that in a minute but i want that to be my documents directory not the actual documents directory so what i'll do is i'll start by saying what's the path to this file right here and this is a nice little shortcut that's built in the swift compiler that will give you the full path name to the file you're in right now so in this case it would be to my file client so you just do that right and then what i'll do is i'll i'll try to find a path to that preview directory so what do we just call that output directory and we'll take that my path and we'll say let's break down the components of that so that separates all the components of that path with a slash and then what we'll do is we'll drop the last part because that's the file client itself i don't need that i just need to find its directory and then what we'll add to it is we'll add the preview content file name which is right there preview content we'll add that as uh at the end of the past so what we should have is the same location as file client but we're gonna get rid of the word file client and append to the end the preview content directory name um okay so now we'll get the path string and that'll be that output directory we just did and now we'll rejoin it by a separator of slashes so we we broke it apart by slashes here we added preview content and now we're reassembling it with slashes and then finally we'll let the output directory u url be url here and we'll give it a file path and in this case it will be the path string there and we'll give it a hint and let it know that yes this indeed is a directory okay so now we'll return that so what we've done here is we've we've grabbed our own path to the file we're in right now we've stripped off our file name and added the name of the directory that we want to be in um rejoined those parts as a real string path and then to take in that string path told it that it's a directory and convert it to an actual url and i'll put that url okay so that's it so now we have two versions of file client and that's really easy to see if you come back here we could actually say well you know even during testing and development rather i don't want to use the live client i would like to use the preview client so that you can see it's a very very easy way to switch between those and again there's lots more uses of doing this but for today's video i'm just going to show you how to use it for previews so in this case i'll go ahead and leave it preview i don't think it matters right now because what's more important is of course in the previews itself that's how we want to use it so let's start by creating a very simple um view that does something with uh you know with our new found file client and application environment so if you recall here in the um application itself we passed in the app environment as an environment object so how to get that back out we'll just say environment environment object app environment of course you can call this whatever you want it has to be of type app environment so that'll fetch it back out and what we'll do here is we'll create a super simple text editor text editor that lets us just edit some text right and then we'll load and save that text so let's create that text it'll say state bar text as a string and we'll say enter some text okay so now we've got that we can start the preview oops we can start the preview now resume that give it a second okay there it is not very pretty so let's print it up a bit let's um put this in a v-stack let's add some padding and let's uh just so we can see it better let's add some background color color white let's do maybe 0.9 there we go um that's some corner radius why just because we can make it make it even bigger um okay so there it is so now we have a text editor it's got some text that that can be edited if we hit play here we should there we go should be able to edit that text okay but notice that every time this preview updates it's resetting our text because again this view every time things change is being recreated and we have no easy way of kind of populating that with some initial text so one thing that's interesting is so for our file client in the preview mode in the preview version of it we go looking inside preview content for our documents not in the documents directory so one way we can do that we can utilize that is you know pass in an environment object and we'll pass in a new app environment but with the file client of preview the same one we built what's really neat about that is that now for example we can go and create a file here let's create a new text file um i don't text no we'll just call it empty file there we just create an empty file and we'll call this text.txt does not need to be part of the target it doesn't really matter for now so there it is so this is my sample text okay so we've put a text file in here and now because we're using a preview client that's its document directory so in theory we ought to be able to load that file right in our preview and save to it during development and then in the live app right you could switch this to live and that would load from the actual documents directory so let's get that working and uh perhaps we can start by saying how about when this view appears we'll load whatever is in there so i'm gonna go unappear we'll do a uh we'll create a function called load text so we can just do it this way okay all right there we go so we have to use a little bit of combine here because that's the um that's the interface that we've defined that when we load something we get a publisher so let's do that so now we have access to um the app environments file client and then we'll say load file and then for recall let's go back so we can see the content this is so for load file the parameter is given a url it'll give you back the publisher with data or an error and it wants the url to our text file so text file url and why don't we create a computed property for this so no need to keep doing it so let's just say var text file url is the url and to get this value you get the app environment file client documents directory we have to execute that because it's a it's effectively a closure or a function pointer however you want to think about it right here so we execute this with no parameter in to get the url out so get the documents directory now we can appending the path component of the file we want to load in this case test i mean text dot text and that's the file we want so then now this is our text file url so there we go okay now that we've done this now we have to take the data that's coming out of this publisher and assign it to our view so to do that we'll start by first saying what if there's an error we have to deal with that so we'll say if there's an error let's just replace the value with a message say error loading the file right and because the the type of the data coming back is an actual data type so we'll say convert it to data using utf-8 okay like that all right there's no error we just want to sync the value now that means we want to take the value that was successfully received and we want to assign it to our text field so in that case we'll just say that the text equals a new string decoding there we go we want to decoding our our data that came in and we want to use utf-8 as the means of decoding so that's how you create a new string from a data object that itself contains uh string data okay that's not quite enough we've done that we also need to store store this publisher in a collection so we'll just create one called um cancelables now you'd normally want to have all this in a view model but in this case for the sake of this example i'm just doing it all in line so cancelables here would be a set of any cancelables and we need to import combine okay and we're going to create a new one of those there we go so now we're storing our um our load file publisher and subscribing to it getting its data and storing it and cancelable so it doesn't get it doesn't get garbage collected basically okay let's see what's wrong here our [Music] dot data oh it's uh it could fail but in our case we know this can't fail because we're providing the string so i'm just doing a force unwrap on that okay so um yeah meant to set equals there you go okay so now if we resume this preview okay so a lot happened there so let's explain basically in our preview provider here we just pass an environment object of the app environment that our app that our view in this case needs but we passed that the preview one that we talked about a few minutes ago that resets in effect the pre the document directory to be our preview content not the actual document directory what's really nice about that is when the app is run on device from the simulator you can use the live app environment but when it's run inside xcode in a preview you can say don't go there that's too hard to find let me define where the documents directory is and in our case it's right here in preview content okay and the last step now would be to implement the save that's really just an exercise in um doing something very very similar to what we did for load but we'll do it for the sake of completeness so for example what if we just added a very uh basic save button to the top of this v stack let me turn this off okay i don't want it actually was save text which we haven't defined yet i'll just put it here okay so it doesn't give us an error now we'll just clean this button up let's give it a frame [Music] i'll just have it spam the whole top and we'll want the alignment to be trailing at the end and some padding let's see we should have our button up there try again okay there we go there's a save button and if we click it it'll do the save so let's very similar to how we did load text um let's implement the save and again because we're using publishers we're going to have to use combine to facilitate that save so so app environment get access to our file client we're going to save the file and we have the text file url already and now we need the actual data to save in this case we'll just take the text that's currently in the view and we'll say we need to get its data using [Music] utf-8 okay so that does the save now the question is you know do we want to see whether successful or not you know why not sure and this is how you do it so for this one we get a completion in this completion we can actually switch on that and then we'll let um i will force unwrap this just like before but for this one we'll let xcode fill in the two cases so we'll just keep it simple now here successfully save and in the case of an error we could print the saving like that so we can see what happens and um there is no received value you can see never so in this case we'll just we'll just ignore uh the receiving since that that doesn't happen basically oh yeah and lastly just like loading you need to store the publisher in a cancelable so it stays around okay so so that's that so now if we try again and we hit run we now should have a functioning loading and saving and what's a little interesting is you can even um if you pin this view and then open our text file what's really neat is that even though we're in a preview we should be able to see changes if you click save and then you can see that our preview is loading and saving directly into our xcode text.txt file right here and we can watch it and that makes it really nice for um again if you're developing and you need to essentially mock your in this case file client or any other sort of external data data sources whether it be network or otherwise or anything else database anything like that you can provide an implementation into the app environment which is not necessarily a live one and as i mentioned before it also works great for testing because now you can completely control what's happening in with those clients okay i know this was a little long i hope it was helpful in the next video we're going to talk about how to further expand upon this and also how to set up your projects to be a lot more modular using swift package manager thanks
Info
Channel: Paul Colton
Views: 944
Rating: undefined out of 5
Keywords:
Id: YwQT5j0qE8g
Channel Id: undefined
Length: 31min 45sec (1905 seconds)
Published: Wed Apr 28 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.