SwiftUI Widget - One Time Code Boxes

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
three two one okay we are alive welcome to the hiking with swift channel and this should be my first tutorial video hopefully the first one of a very long series so today we're going to make a one-time code boxes using swift ui so the reason i made this video is because i was looking for something like this with swift ui but couldn't find one so i had to build one from scratch and it worked out pretty well so i feel it may be worth sharing so what we are going to make today is something like this so this is a two-factor authentication screenshot from appledeveloper.com and you can see there are six boxes and you can paste the code you received from your text and so we want to achieve basically exactly the same thing so we want to make a one-time code widget like this which can be which can have different digits like we sometimes we have six sometimes we have four and in weird cases we may have five so we just want to change a parameter and uh change the number of boxes also we want to support paste code from sms or the space code from anywhere but we want to support this and also we want to make like typing one and then the cursor automatically jump to the second box and if you delete this cursor will jump from second to first [Music] yeah that's basically the idea so it works like this one two three delete delete delete let's try if i can paste something here just paste three paste yeah so you can even paste in three like i can i can type in one two and then i paste yeah and i'm not typing in the last one because once i type in the last one it will try to log me in which this code is not correct so i'm just using this to demonstrate what we're trying to achieve here okay so you will be watching me create this thing step by step with swift ui and let's get started let's begin with me showing you how the completed widget works so we can start typing and we can delete and we can paste in and we can only paste certain digits paste yeah so it works like this and we can easily change the number here to change the box numbers so you can see four and it's five yeah so it's very flexible okay so let's start from scratch i have created a new swift ui app and it's pretty basic so there's the entry point and i didn't even bother to change the content view default name and what's in the content view is a vertical stack with some basic text like a large title and this text field is supposed to be a placeholder later we can replace with our one-time code boxes and a button recent code i just have something to show so the ui doesn't look too boring and also you will notice i've added two string extension methods so nothing too complicated i'll cover them when we actually use them also a folder to contain the things we're going to make so this one this one is a swift ui view so this you realize like the name it's called boxes so it's a collection of the boxes and another one called one time code input this represents one box so this is the swiftview ivu and this is a one box and this one we're actually going to use the ui view representable to wrap a ui kit text field because okay the reason we want to use a ui view representable to wrap a ui kit view is because we are trying to achieve this uh type typing one and jump to next thing and this is not very easy uh in swift ui because this requires you call something become first responder but swift ui doesn't even have a first responder property or anything so we need to use the old ui kit stuff so that's the main reason so after we analysis uh analyze the model and what we're trying to achieve we need to decide uh what approach we're using so my approach is we create a wrapper for ui text field and then use it in this swift ui view okay so uh that's the step one also we can also analyze the [Music] one-time code so let's say we have six code and a good data model to represent this will be a dictionary because if it's an array then this can be empty and this is filled this is filled it's hard to represent using an array but if with a dictionary you always have 6 keys and the value can be a empty string but you will always have six key value pairs so that's why i decided to go with dictionary so let's start creating those data models or we can call them states so let's start with state call it code dict so this should be a integer and a string and to create this we want to have a code digits so let's say it's 6 and we need a beginning violin so i'll use a dictionary initializer integer string and i'll use this one so this initializer takes a unique keys with values it takes a sequence of tuples so how do we create those tables i'll just use mapping method 0 to code digits so this is zero to five and then map map into a tuple and start with empty string okay this looks good uh so we can delete this let's xcode to infer what what the type is so type its dictionary with integer and string and this may be a little bit confusing but what it looks like is actually this one two well you get the idea until five yeah this is this code dictionary and we want to start with this and then every time we modify modify this we change oh this should start with zero yeah zero one so each time we change in this box this value will change and the second one this will change so that's our model and let's also create a state first responder index and start with zero so we will use this to jump forward and backward to represent the first responder index okay well now we have the states and uh we finished the step one we're going to step two and this is the most fun part so because this will compose this one time code input to use six or four of them so we can't start doing this without having this first so i commented this out to make the code compile if i uncomment them immediately you will see this struct doesn't conform to this protocol so i can add protocol stops which just add me this one well that's helpful i'll just say ui text field because i'm wrapping a ui text field inside this and it's still complaining and i notice if you click this again it doesn't help you very much it just adds this thing again this is not what we want but unfortunately i know what are the required methods so i just add some comment required that's it's so first we want to make ui view and second one is update uav and those are two very basic requirements so you can see the warning the error went away so here we want to create a ui text field and return it so we can just do this and but we do want to configure it a little bit so that's why we create first text field and return the text field and then we do some configuration so keyboard type keyboard type this number i only allow entering numbers and content content mode oh content text content type should be one-time code so setting this will allow you to copy code you received from messages sms text so that's this thing magic for this thing and we also want to configure the font let's say large title to make it large and using this the benefits of using this is is dynamic type so if the user makes the font larger your uh your box also becomes larger and what else alignment text alignment let's make it center yeah that's what i i can think of [Music] so we can add more things later but i think this looks good so far and update view this is the method we we use the states we pass in well i haven't added but i'm going to add it now so we're going to pass these down like pass it into here and then pass it down to each box of course it's not no longer stays it's just a binding and so it's binding and it doesn't have default value it should be an integer string and this is binding so it's integer and we use these states to update the ui view so i also need to pass down the index of this box because this box need to know its own index to update its text so here is where we do that when this state change we update the text so we will say uiview which is the ui text field text equals to code dict index and also if index equals to first responder index we become first responder okay that's the basic requirements this method is a swift ui view trying to update the wrapped ui kit view and but we also want to let the wrapped ui kit view talk to the swift ui view so to do that to do two-ways communication we need to if we only need one we communication this is good enough but to do two ways we also need to use a coordinator so just market this internal type and coordinator and that's object and suddenly you see it uh giving me error again because if you want to use a coordinator you also need to provide a way to make coordinator like this okay now the error is gone so the coordinator is where you handle the ui text field stuff so i will make it conform to ui text field delegate and so here i need to set up that delegate s coordinator a context coordinator okay looks good so far so let's keep going uh now our one time code input conforms to uiview representable but our coordinator is an empty class which does nothing that's not what we want uh so i'm going to add some ui text field to get message text field should change like yeah this long one i'll just do something indentation oh to make it look better okay so this message gets called like when the ui text field text is about to change and it asks you to return a full value to indicate if you want to allow this change so this is called when i type or i paste something into the ui text field so here first of all we want to always return false here why because uh like that's that's the point of using swift ui so to you i use states to manage the ui controls and [Music] so if we return true here we will allow the text field to change it text by itself but we don't want to do that we want to change text by modifying the states that's why we always want to return false here so we will have a single source of truth which is the our states so we return false and we can modify this manually to update the text field text so return false that's the very basic requirement so in all passes we always return false and so here we will be handling three scenarios which is this so if you think about it when we're doing things here so we can type and we can delete and then we can paste that's all the possible cases so we're going to handle that inside this uh delegate method so within this method what you 99 time you want to do is to first you got current text from the text field next you can force unwrap but if you don't like it you can use this too and then we also want to get the range of the string about to change like it gives you this range but you also need to uh use this nes range to create a range so let's say range yes range inside string uh so that's in this range and in string is the current text current text uh i also just returned false always return false so then we find the string range string range is about to change and then we can say updated text equals to current text replacing characters replacing characters within this range and with this replacement string like like incoming string if i'm pasting and the string i'm pasting is this and if i'm typing and then the string i'm typing which will only be one character because you can only type one character at one time it will be this so we replace the range should change with the incoming string we get the updated text so this is what you usually would do with the text field but our text field is a little bit different it's a one-time code uh box so it will only have one letter or one number and then it should jump to next um so i just wrote this because 99 you will use them uh i just write them out and if later i don't need them i can delete them so we need to handle three possible cases one is typing typing two is pasting three is deleting okay so let's start with typing so as i just said if string count equals one then this is typing if we're typing in one character then we just want to update the state say update value the key is my index like if i me index and this text box i have index zero and then i only want to update the uh the states [Music] key zero so but now it's giving me warnings say this code dict is inside this struct but not inside this coordinator i cannot use this so i need to pass it down into the coordinator so let's do that let's pass this down i think i need both yeah okay so this has come we have the index we have code dict uh but now it says it has no initializer well classes they don't they're not like struct you don't get free initializer so we'll just create our own and uh index integer and code this would be a binding of a dictionary okay so self index equals index self underscore because it's a binding i use this underscore to initialize okay now this error is gone and now it's complaining here because when we make it we need to pass in those so we just pass it down there and to pass a binding we use a dollar sign okay now all area is gone we can continue here so if i'm typing one character i just update that character in the dictionary which is spring okay that's pretty easy so pasting pasting so if string count is bigger than one so in this case if i'm pasting in three so i can start pasting it any index i can paste from here i can paste from the index one so i need to uh like do a for loop loop through the next three boxes and and then update its value so i need a for loop for in so and this for loops should start with the index because i don't want to loop zero i want loop one two three so starts with index and should be smaller than uh smaller than index plus count so it should be smaller than this index plus count also uh if we paste in like seven eight digits we don't want to go beyond the boxes so we need to calculate the main number here so we want to calculate mean number and so we don't want to go beyond codec count six and that one is index plus spring.count well it's a bit confusing but you can if you think it slowly and carefully uh you will figure out this loop so i can delete this so i start from index one and uh if i'm copying three characters this will be one plus three is four and uh min is four so one two four one two three one two three okay feels right well i could be wrong if i'm wrong and i can come back and fix this later and let's also think about i'm pasting in seven digits case so i'm pasting seven digits then this is seven and uh i i want to use a minimal one so this will be six and uh so from one to six yeah seems correct so for this this this i want to update its value that code dict update value and the key is this index and the value is string something like this but well string doesn't take integer as subscript you have to pass in like string index that's where uh my extension method string at index comes to help so we can pass an integer to get the character at that index so but here we don't want to always get zero well we always want to start from zero and then we go up so from here we get the zero index and here we get the index one of the pasted string so here we need to do this i minus index i believe so let's uh think about this again so i'm here and my index is one and i start with one and one minus one zero string at zero one two okay that's good so this is how we update the state value when we are pasting and i think this pasting this loop also covers this typing like because if string count is one then i just do indexed index plus one so i loop one time and then update the value so yeah these two can be this two scenario is actually one scenario and so we can do this okay looks good okay so now we've handled two scenarios we want to continue to the deleting scenario so how do i detect if i'm deleting a character so i will check if the incoming string is a backspace key string is backspace this is another extension method here so basically i detect if it's a backspace key and i return true or false if it's backspace key then well what should happen is i should delete this character and because we're returning false in all paths i need to do it manually i need to update my state and index and i need to set it to empty string and i think i think that's it that's the deleting scenario uh but we've missed something that is like why typing here and i after i type in one character i should jump to the next box and white delete i should go back one box so uh so i need to also modify the first responder index here uh so but i need to pass it in first so let's pass it in and first responder index it's a binding integer self underscore first responder index and then we need to add it to the initializer and we just pass this down okay uh okay before i add that let's move this up and if it specs space we just return false because if it's backspace then we're not typing or pasting we can just update value and return okay so here we also want to go backward like if i delete uh so the way i go backward it's first responder minus equal one right but then we're facing uh uh index output range issue if this is zero then we go to minus one negative one that will crash that so we want to do a max here first responder index and if you think about it it shouldn't minus one uh of the first responder it should use the current index minus one because if i'm hitting delete here so i always want to go to here so yeah use index instead of first responder index and we use max to get the larger one so if this is negative we always use zero so we go back to the very first box that's okay and then we also handle the going back and forth here so here it it's because it's increasing so we need to do a menu here and the furthest we want to go is it's index 5 so it should be count -1 and then index plus one actually it's not plus one because we can be typing or we can be pasting so this should be string count so let's think about it if this is correct so if i'm pasting in three character string count is three index is zero uh so three and first should become three three so if i paste in three and the cursor jumps here yeah i think that's right i can try paste yeah so this logic here should be correct and uh yeah i think that covers all the scenarios and i think our one-time code input looks completed well at least it appears to be completed but we can try start using it so we go back to this one-time code boxes this is a swift ui view and so now it's just the default we want to embed in a horizontal stack and we want that for each loop [Music] oh we also need to pass the states down here so we passed from content view into this swift ui view and then pass it down to each box so first we need to get states here and so for each we just provide our range psychotic count i and then we just use the box we just created runtime code input so the index should be this i and we just pass these states done first responder okay uh now the preview is screaming let's pass in some default stuff just a constant something like zero one this one comes in zero okay uh let's see if we can preview this okay the x code wasn't cooperating but now i got it work so i can add like four digits so it looks better uh well it's actually rendering but we can't see it because it has nothing it has white background so what we can do here is i overlay it with a we see a rounded rectangle corner radius let's say 10. and we don't want color we just make it stroke okay now looks better we also change it foreground color to secondary looks good but it's taking too much space so let's change this to only use the size it needs also add a little padding okay now we need to fix this size issue so we want if we want the square we can use aspect ratio aspect ratio one fit no much better much better okay saying we can even duplicate this and put it into dark mode looks good now we can change this to three still working pretty good uh so now i think we can use the swift ui view inside our content view so which replace this thing so i just replace it one time code boxes codex i just passed pass it down look look at this looks good uh we also want to add some padding maybe okay let's try if i can type here one two three four five delete delete delete is not working well we can see some interesting fact like why delete why delete in empty box that it does nothing but why delete here it actually can go back so our code our code here is actually working but uh let me demonstrate something if i enter debug preview and i ping it here and i put a breakpoint here can i type okay we got started in a simulator you can stop let's try start a simulator and try to hit this code inside the ace backspace let's see if it's hit no the breakpoint is not hit but if i do this huh interesting so that's why that's why we delete this is working but when we hit backspace here it's not working actually i've mentioned this previously the reason for this is because this message only gets called when the text is about to change well when we are trying to delete this three the text will change from three to an empty string but when we hit delete here there's nothing to delete so actually the text here is not about to change so this method is not even called so that's a reason why this doesn't get called because this delegate method doesn't even get called so that's why and fortunately i've came up with a solution for this so what we are trying to achieve here is hitting delete key here can also go back but how do we detect uh like the backspace is pressed well there is a function inside ui text field called i forgot what it's called but we're going to make a subclass of it so call it backspace text field inherit from ui text field and we can override that method i think it's back delete backward overwrite this yeah so we want to do some custom behavior here so let's call super delete backward first and then we want to do our custom behavior here so how do we do that well we can pass in a closure say on delete and it's an optional closure so by default optional closures are escaping ones so we can create initializer to pass in this closure so i don't need to do escaping here actually if i do this it will give me some error i think so from the delete yeah closure is already escaping if it's optional so i don't need this and it's screaming at me asking me to add this required initializer so i just added that's fine and the superintendent is not called so let's call it server unit okay so our ui text field subclass backspace textfield is created we can initialize with this initializer so what we want to do is instead of a basic ux field we use its subclass a backspace text field on delete and we do whatever we want to do to handle this case here so what do we want to do here we want to modify the first responder index so because this is the go forward case so we always want to make sure it's not negative and we can do like this i think so basically repeat what we've done here let's run the app again okay one delete delete that's weird it's not working okay don't forget to call this undeleted uh passing closure so let's run the app again one two delete okay it's going back okay seems uh we've achieved what we want let me just commit to get rid of these blue lines okay what else so we've composed boxes and yeah we want to add a oncommit callback so what that is it's like once you finish typing the last character it should automatically submit this code like to your server to check this code is right or something well so that's oncommit so to do that we can easily achieve that by passing down a closure on commit closure to this swift ui view on commit so it's just a void closure and because it's a struct i don't need to do initializer or anything and because it's a optional so it has default value no so it doesn't even break this current initializer that's great but we can add it on commit and for now just print but of course we just passed it in doesn't mean it gets called somewhere we need to pass it down one more level like down to the box so we pass it pass it down one level oh no don't delete it and because this is struck two so i don't need to modify anything and when do we call this i think we call that when our state becomes final like when we have all six digits then we can call this on commit so when our states change it this method will gets called update ui view so we can check here what do we check we say codect and we want to filter out filter out the empty ones it's not empty and count equals six well we don't use hard code we use codic count if this uh yeah what i'm doing here is i get all the values and i filter out the empty ones so the remaining is values that not empty and then i check if it's six and then this means all code boxes all six boxes are filled and so here i can call on commit and uh of course i need to pass on commit from this to this like down one more level so commit and i just passed on commit so let's run the app again and see when we finish one two three four five six okay let's get it gets called six times well that makes sense because we're passing it down to each box so every box detects if six stages are filled and they they call on commit [Music] so it's called six times but you don't want to do this in the real world it could means you're sending six http request to your server and where we want to avoid that right but the good thing is so like when we can finish this in any box we don't have to finish the last box last so we can just finish this box and it will detect if we've completed yeah you can see it's printed so that's a good part so now the only thing we need to do is to avoid calling it six times so i think we can do another check here so i'll check if i'll check this so this is means if i am the first responder of this box i'm the first responder and then my state passes then i call uncommit so other boxes because they cannot beat this check so other fives won't call this so only the first responder box we'll call this so let's try again only caught one time okay that's good so we have finished step five and we are approaching to the end so finally i will just do some cleaning up and do some minor change to optimize the appearance so first of all i'll delete the thing i didn't use so i delete this and now this i didn't use and this i didn't use either so i delete these okay and also i want to add some visual feedback like like here bring up the keyboard you see once it's filled it turns green well it's not quite the same now what apple does apple what hippo does is it highlights the first responder box but like we can easily achieve this if we can achieve this so it's super simple to do this with swift ui let's do it so here inside here is where we define the border well it doesn't give me color oh it doesn't matter we don't actually need the color um i can code this out color uh yeah so the foreground color so if i change this to red it it will become red so what i do here is i do a check like i do a optional not optional training i don't remember how it's called but i do a conditional thing here so i checked if this index box s box at in this index is filled so how do i do that well i can do code that value at this index is empty of course this is optional let is empty okay so if the value is empty then oh we get this is empty and use it to decide the color so if it's empty then we use secondary color otherwise we use green color so how do we quickly test this well i can give it a number here well it's screen and yeah we get instant viral feedback i'll just keep it like this uh also like when it's green the border will get bolder so if it's empty then it's one otherwise let's use two so yeah when it's filled the border green border is thicker so that's wrong again one two three and if we paste in something one two three four thanks and paste it in i think i accidentally copied uh thing let's try again delete and paste again that's weird paste okay anyway seems working and uncommit gets called delete this fill in this uncommit gets caught again okay let's do a little more refactoring so first thing of all i've noticed we're not actually using this first responder index state in the content view apart from just passing it down so maybe we don't need to store this state in our content view because the content is not actually using it just pass it down there so maybe it should be just inside our one-time code boxes so i just delete this and i delete this i don't pass it down because it will has its own state i change this to a state okay and the preview is not happy because i'm passing it in there which i no longer need to pass okay delete those see if still can preview build it missing argument okay because uh i didn't provide a initial value so let's do it takes longer but it's actually working okay so now we've moved uh that first responder index into this so make the content view the user unaware of that they don't need to know that detail and the second thing is i think sometimes we want to know the string type of the code so we can extract the code from this dictionary and then send it to our server or do a validation or anything so what we need is a string type of code which can be a computed property computed from the dictionary so here i'll just first sort it because dictionary doesn't have order so it doesn't necessarily to be zero one it can be like five four one zero yeah something like that so i need to sort it first and uh sort by key so key key sorted by key okay now after i sorted it's array of tuple key and value so i just want to have the value so i map the keepass value so after this we get an array of string like we get this this and until five in order so it's in order so we can just join them to get our actual code okay so here instead of printing this we can print out our code we can try running our app but spoiler alert it won't get printed for some weird reason so one two three four five six hmm not working so but if i delete this and i type in something again it gets printed out so it seems like for the first time the code dict is not updated properly but after the first time it's working so i'm not quite sure why this is happening but i've figured out a workaround to do this so it's adding on change modifier and whenever the code takes changes we actually don't need to do anything in this closure just by adding this on change will make it work but we can just print the code dict to make sure every time we type something is changing so let's try again so one two three four five you can see the dictionary is uh actually updating yeah it has the value and my finish the last digit one two three four five six is printed so it's printed from here and the dictionary is updated properly so i'm not quite sure why uh this is happening as i said we don't need anything in this closure but i think maybe because our custom swift ui view it's like it's not updated based on the value of the dictionary so after adding all change [Music] it just updates the code dict every time the inside code exchanges well i can't explain it very well but this is the workaround and as you see we can print out this code so what you should do is instead of printing you can send it to our server to the server or do some validation so if anyone watching this video know why this is happening please leave a comment uh and i'll make sure to read through all the comments and you can also give me some advice on how to improve my future videos also i'll share link of this project of this ripple on github and share the link in the comment area too so you can just grab this and then use it like this in your project okay let's finish this video by doing some summary so during this tutorial we've covered how to wrap a ui kit view using ui view representable struct like each box it's a ui text field and we make this whole widget using that wrapped ui text field and then we pass the states down into into this struct and we've built like two week communication to let the search ui view to update the ui text field and we also use coordinator to let the ui text field talk back to the swift ui view so that's two-way communication and we've also covered how to make your ui react to the state like the field box has green border and empty has secondary color and also passing a closure into your swift ui view or even into your ui kit view it's very simple so as you can see there's nothing fancy with this widget and it's very reusable whenever you want to reuse it this is all you need okay so that's pretty much with this tutorial thanks for watching it became much longer than i expected because this is my first time recording a video it's a lot harder than i thought and i probably need to do a lot of editing after i finished but here we are and if it helped you in any way feel free to like this video and subscribe i probably will keep posting more tutorials like this uh so once again thanks for watching see you next time
Info
Channel: HikingWithSwift
Views: 151
Rating: undefined out of 5
Keywords: SwiftUI, iOS, Swift, One Time Code
Id: Ry_OrXcXepM
Channel Id: undefined
Length: 67min 56sec (4076 seconds)
Published: Sun Apr 25 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.