SwiftUI Property Wrappers Version 2

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
data flow in swift ui is controlled by property wrappers and a property wrapper is a simple generic class or struct that has some logic that gets triggered whenever the value of the property gets modified swift ui offers 17 property wrappers for our applications each of which provides different functionality in the first version of my video on this topic we covered these ones and we'll repeat them again in this video but we'll also add in two more that were introduced in 2020 we'll take a very simple starter project and work our way through each of these different property wrappers so that by the end you'll hopefully have a clear understanding of when and how to use each of them in your own swift ui projects if this is something you want to learn then keep watching now before we start i should mention that there are many good written resources on this subject the references i noted in the first version of this video have also been updated and i recommend that you check them out as well there are some new ones too and i'll leave the links in the description below there's the quick reference and flow chart from donnie walls there's one from majid that covers some key differences between state object environment object and observed object and i also hope to make that clear here in this video and of course there's the guide from john sundell from swift by sundell if you learn better from reading blog posts then these might be useful for you and i encourage you to check them out even if you do stay with me through this tutorial so what we're going to do is work through the sample project and a link to the download is in the description below this starter project has all of the ui in place and you might take issue with my design layout but that's not the purpose of this video you may note that i have updated the code to use the new toolbar api for navigation toolbars and that was introduced in ios 14. so you'll have to be using xcode version 12 or later currently none of the objects with actions are wired up we have two buttons and a future navigation link in this content view where we will start when we tap the new number button we want to generate a new number and update the lucky number field and as we move through the process we'll learn how to essentially pass values onto different views whether that be in a modal sheet presentation or a navigation stack and be able to update the information on any view and see that it is updated in all other views we'll start slowly by using the state and binding property wrappers and pass only a single value the lucky number integer then we'll introduce an object and pass it around using a state object and an observed object and publish property wrappers and finally explore the use of the environment object property wrapper and the new app storage property wrapper for storing information in user defaults so let's get started this will currently display the value of the variable number using text interpolation in the text view if i change the value of the number the view updates when i force to refresh i want swift ui to be able to do that by choosing a random number from 1 to 10 by the click of a button and then have it change the value of the number seems easy right well we get the error cannot assign to property self is immutable swift ui views are structs which means they are immutable by default their value types if this were our own code we could mark the methods using mutating to tell swift that they will change values but we can't do that in a swift ui struct because it uses a computed property what we will have to do is wrap our variable in a property wrapper that will store the value in a special internal memory outside of the view struct and this is a at state property only wrapper related view can access it and as soon as the value of a state property changes swift ui rebuilds the view to respect the change in the value for this reason whenever we use the add state property wrapper we should also designate it as private since no other view should be able to access it if we update our canvas and go into preview mode we see that the button now generates the number and forces an update of our view to display it in our text field in swift ui we often use add state variables to trigger some kind of action like showing a modal view and in swift ui this is done by using the structs sheet method so let's add this function to our button and we see from the code help that this version will present a sheet when a given condition is true and the is presented argument is a binding which is a boolean this means then that the is presented parameter must be bound to some variable that can change its state so we need a state variable for that let's create a private state variable called present modal and initialize it as false with that state variable defined we can complete our dot sheet method by binding is presented to the present modal and we do that by prefacing the variable with a dollar sign the last parameter of this function is a trail enclosure that will present our view so we can swiftify it by calling it directly after the function like this and then just pretend a view with text hello world now all we need is some way to make our variable true well we have a button for that so all we need to do is either set it to true or toggle it and since it's decorated with an at state property wrapper we know that we can change it we'll refresh our canvas and try this out and we see that our modal sheet is presented as expected now something funny is going on here the modal presents itself when present modal is set to true but when do we ever set it back to false well we don't specifically but the modal view does that in the background anytime you dismiss a sheet by pulling down the boolean value that caused the presentation gets set back to false and since this is being done outside of the original view itself you might be confused because didn't i say that no other view can access it well remember when we first implemented the sheet method the completion didn't say state bool it said binding bool and this means that we can basically create a two-way connection to our state variable by binding it to something else somewhere else in the case of our.sheet method that is handled for us but let's see how we might handle the use of a state variable that's bound to a variable on another view so a binding is a two-way data connection without ownership let's take a look at this modal view now it's similar to our content view in that there's a button to create a new lucky number and we'll get to that and there is a dismiss button on the navigation bar first let's return our content view and change the text view to modal sheet view and when we test out our button we see that our modal sheet view is now presented now what i want to be able to do is dismiss the modal by tapping on this dismiss button and for those of you experienced in swift ui development you'll know about an environment variable called presentation mode that can be used to dismiss the modal view and this is great but i'm not going to cover environment variables here let's assume that the environment variables don't exist so we have to roll our own method here using the concept of binding what i'd like to do is dismiss this modal by setting the value of our present modal variable in content view to false thus triggering a state change that would refresh that view and since present modal would now be false the modal wouldn't be shown in ui kit this would be similar to a delegate pattern of passing and running a closure these are difficult concepts to understand let's just watch how easy it is in swift ui what we need to do is pass the variable into our modal sheet view but when we do that we need to wrap it in a new property wrapper called a binding instead of a state variable so in modal view let's define that property as at binding and we'll specify it as a boolean variable called is showing now we can't assign a default value because its original value will be coming from our content view when we tap the button and we also can't make it private otherwise our content view wouldn't be able to access it this presents an error in our preview provider but we can solve that by using a constant value for the binding like this now that we have our bound value we can use our action in the dismiss button to toggle that value to false and since it's bound to the original value that we have in our content view it will be refreshed and the sheet will disappear back in content view we get an error because the modal sheet view is now expecting an argument for a value is showing well we can just solve that by using our present modal value from this view notice that it requires the dollar sign that indicates that it's going to be bound we can refresh our canvas and we find it's working as designed you may have noticed that during the code completion for our modal sheet view there were two options for arguments one that had is showing and the other that would also allow us to pass a number and that's because our modal sheet view has a property that is used to display the lucky number and the default here is 11. so let's see if we can pass the number from our first view to our modal view and then have it change in the modal view if we test this we can first generate a new number in our first view and when we present the modal we'll see if it does show the same number in that modal view indeed it does but can we change this number and have it update the one in our original view within our modal view now let's then update our function to change the number by generating a new random number between 11 and 20. of course we get an error because the number is not mutable same problem we had in the first view well let's make it a state variable well this is fine for our modal view but it generates an error in our content view it's claiming that there's an extra argument number in fact if we return to our code completion there's now only one option you can't update a state variable from outside the view that it's on because it was private what you need to do is bind it to a variable from another view so let's undo and we'll fix that in our modal view instead of specifying our number view as a state variable we need to specify it as a binding which can't be private and we can't give it a default value this creates another error in our preview but we can fix that again by adding another constant for that number back in our content view we can now pass both of our state variables as bindings so we need to make sure that we preface them both with the dollar sign testing the preview we can update the lucky number present the modal view and change it there and see that the number is changed on our content view we have a two-way binding via property wrappers well we're off to a good start but the state property wrapper only works if we're binding single property values to views what if we wanted to pass an object instead this object that we create needs to observe any changes as we might want to update properties of an instance of that object this means we need to use a different property wrapper let's create a new swift file and we'll call this file user within this file we're going to create a class called user and it'll have two properties name as string and lucky number as an int now i'm going to let xcode help me out here to create my memberwise initializer by command clicking on the class and letting it create the memberwise initializer what we want to do is to change our two views to use an instance of this class and when we create the instance in content view and then bind it to a second view where we will update only the number and see if that update will be reflected back in that first view just like we did before we would like the lucky number property to be updated by our random number generator and we'd also like to use the name to be visible on both screens in the navigation bar so in order to do this we have to make our user conform to the observable object protocol anything that conforms to observable object can be used inside swift ui and publish announcements when its values are changed so that the views can be refreshed just like when we change the state variable so all we have to do is conform user to observable object and then specify which of our properties we want to observe being changed and we do this by assigning the at published property wrapper to those properties so in our case the only thing that we're going to change is lucky number so when we create the instance of our user we'll assign values to both properties but only the number will change so that's the only property that we'll need to decorate with the at published property wrapper let's return to content view and replace our state private var number with an instance of our user object we can't use state however for this we are creating an object so we'll use at state object instead and we'll assign the name stuart and a lucky number of 0. now an at state object works very similarly to the at state property wrapper but the main difference is that we can share it between multiple independent views which can subscribe and observe changes on that object and as soon as changes appear swift ui rebuilds all views bound to this object this breaks our code so let's get it working again our text view gets updated to use the user.lucky number similarly the button needs user.lucky number and in the presentation of our modal sheet view our binding becomes the binding of our lucky number property notice that the dollar prefaces our observed object and our lucky number is the published property let's also just change our navigation bar title and use string interpolation here to use the user.name let's preview this and we see our name is displayed in the navigation bar title testing our new lucky number we see that it still works our lucky number property is the int that is bound to the binding number property in our modal view now i want the title to be used on our modal view as well so instead of passing in and binding just a lucky number we can bind the entire object only oneness thing is being published but that doesn't prevent us from binding the entire object a binding is no longer an int it's going to be a user so we can change our binding to reflect that but we also need to change at binding to at observed object because it's no longer a value type but rather a reference type remember user is a class instance the text view needs to be updated to change to user.lucky number and similarly the action needs to change as well and now that we have our user we can change the title of our navigation bar title here to use that username as well the preview provider needs to be updated so we'll give it an instance of our user we'll need to return to content view now and change our argument for the modal sheet view to a user and pass the entire user instance note this time it's not asking for a binding so we don't preface our user with a dollar sign the dollar sign is used for a binding only testing again we see that the username is showing as the title let's get a lucky number and go to the modal view we see that both the number and the name made it through to the modal sheet view when we update the lucky number and because it's published it's reflected on our content view when we return but we have no way of updating our name because it's not published i'll leave that up to you to sort that one out i hope that you see that you can use this state object and observed objects to pass instances of your objects throughout your app we've seen how at state is used to work with a state that is local to a single view and how at state object lets us pass one object from one view to another so that we can share it as an observed object well environment object takes that one step further we can place an object into the environment so that any child view can automatically have access to it swift ui's environment object property wrapper allows us to create views that rely on shared data often across an entire swift ui app think of environment object as a smarter simpler way of using state objects and observed objects on lots of views rather than creating some data in a view and then passing it down or up the chain to other views you can create it in a view and put it into the environment so that all other descendant views have access to it let's change this text view in the navigation bar to a navigation link with a destination that will present our navigation destination view i'll just bring up the library and drop in a navigation link and move the text in as the placeholder and change the destination to nav destination view which is our navigation destinations view let's test this out yes the link pushes the nav destination view onto the stack but our name is not entered into the title and if we update our lucky number it doesn't get passed through this is not surprising because we're not passing it along as we did with the modal view the nav destination view is very similar to other views it's not been fully fleshed out yet because we want to demonstrate the use of this new environment object as i mentioned an environment object gets supplied by its ancestor view so if we set up an environment object for this view then it will have to get it from another ancestor which is the content view because this one has been pushed onto the navigation stack by that navigation link so let's assume they're going to do that and let's proceed we can treat environment object just like an observed object so in nav destination view we'll create a user variable and give it the environment object property wrapper as with our modal view we can now use this in our text view similarly for the button action let's assign user.luckynumber a random integer from 21 to 30. then for the toolbar title we'll replace anonymous with user.name notice that the navigation bar isn't showing in our preview and this is because this is a dumb preview it doesn't know that this view has been pushed onto a navigation stack however it has been and i am providing a toolbar for the navigation with the username text to be displayed now our view requires a navigation object being present it's important that you also update your preview code to provide some example settings to use so we can do this by modifying our preview using an instance of our user as the environment object that is bound to an instance of user and we do it this way it builds find but let's go back to our content view and preview and if you've been following along you will probably realize that there's something wrong indeed there is when i tap the navigation link the preview crashes let me run it in the simulator to see if it sheds some light we get a fatal error there's no observed object of type user found the environment objects missing from the ancestor view and this is what you'd expect environment object gets supplied by its ancestor and we've not yet done that when we use the modal we pass the value on using an observed object we need to change at state object to add environment object but we're not going to initialize our user instance here anymore the user environment object must be provided for from somewhere outside of this entity rather than being created by the current view or specifically passed in so how do we do this well first let's fix our preview by modifying it with the environment object and bound to a sample instance just as we did in our navigation view now notice how i added that environment object in the preview well we can do the same thing one level up and this is the starting point of our app and that's where our user is going to get instantiated and that one level up is the file that's decorated with at main so we'll create our user instance here and then we'll attach that to the window group it's content view if i run the app now all seems to work just fine our content view gets our instance of user and changes the navigation title and sets the initial value tapping go next we see the values are there too if we update the lucky number here and return it works there's one issue however our modal view that we are using is using an observed object when we should actually be using the environment object so let's go back and change it in our modal view to use an environment object and we'll update our preview provider now because it's an environment object we no longer need to pass our user instance in the modal sheet view from our content view it's going to be coming from the environment so we'll update the sheet method by removing the user argument i'm going to run this in the simulator because i know something's wrong when we try to go to the modal sheet view it crashes but why i get this error again it's missing an ancestor view remember environment objects must be supplied by an ancestor view and if swiftly can't find an environment object of the correct type you get a crash and this applies for previews too so be careful well the reason is that a modal view is not in the content views hierarchy it wasn't pushed onto a stack it has no idea what the environment object is so for modals or sheets we need to add the environment object modifier to the view as it gets presented just as we did for our previews this should look pretty familiar it's exactly the same way that we added it to content view when we initialized our instance of user and how we did it for all of our previews testing now we see it's working as designed the last thing i want to cover is another view property wrapper that was introduced this year and that's the app storage with app storage you can read and write values from user defaults and this wrapper watches a key in user defaults and updates it when the value changes for example let's say we want to keep track of our last lucky number so that when our app starts up it'll always use that last number as you see right now every time my app starts the lucky number always starts at 3 even though i change it over time what i need to do is to go back into my user model and create a new variable and i'm going to call that variable last number and i'll declare it as an int and i'll assign it that initial value which was 3. before i can use app storage i have to change our import from foundation to swift ui and with that change i can now decorate it with the app storage property wrapper assigning it a key of any name but i'll use the same name just for convenience now every time my lucky number changes i want it to be saved to that key and user defaults all i have to do then in the lucky number published variable is add a did set that assigns its value to lucky number whenever it changes and the app storage property will take care of writing it back to user defaults and because we're using did set we do need to assign an initial value to lucky number and that can be any integer it doesn't really matter because our initializer changes it to the last number when it's initialized now as soon as that last number changes the app storage property wrapper will store it to user defaults using the last number key now finally since we want our lucky number to be the last number in our initializer we assign lucky number to the last number and again the app storage wrapper will read that value from user defaults and assign it to our lucky number let's test it out running the app it defaults first to that initial value of three but let's change it now let's stop the app and run it again and remember that the last number was 24. indeed when it launches that value of 24 is placed into our number field well there you have it a complete walkthrough of the different ways to pass data back and forth between views causing state changes and a refresh of the ui to reflect those changes this is the declarative way of doing things in swift ui now if you're paying attention at the beginning you may have noticed that i didn't cover or include an example of scene storage scene storage works a bit like app storage in that you provide it with the name to save things plus the default value but rather than working with user defaults it instead gets used for state restoration and it even works great with the kinds of complex multi-scene setups that we often see in ipad os where you have multiple scenes and i'll leave it up to you to explore that usage i have lots of other videos available and in the queue as well so please check out the rest of my channel you can also visit my website to see the apps that i have available on the app store and visit my github page to see what i have available as public repositories if you like what you've seen give it a thumbs up and subscribe to my channel and ring the bell to get notified when i post new videos i'm most active on twitter so please follow me there as well to find out what else i'm up to thanks for watching
Info
Channel: Stewart Lynch
Views: 2,929
Rating: undefined out of 5
Keywords: SwiftUI, Property Wrappers, StateObject, AppStorage, Xcode, iOS
Id: u3RIfxSk1As
Channel Id: undefined
Length: 31min 27sec (1887 seconds)
Published: Sun Sep 27 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.