Thumbnail for So You Think You Know Swift? - Nick Lockwood by SwiftServerConf

So You Think You Know Swift? - Nick Lockwood

SwiftServerConf

33m 46s6,964 words~35 min read
YouTube auto captions
Transcript source

YouTube auto captions

This transcript was extracted from YouTube's auto-generated caption track. The transcript below is server-rendered so it can be read, searched, cited, and shared without opening the original YouTube player.

Use this transcript
Related transcript hubs

[0:04]okay uh yeah so my name is Nick and my talk is entitled so you think you know Swift um this was originally a 45 minute talk and after much agonizing about what to cut out I ended up adding another three slides so um I'm sorry if I go a bit fast so Swift is built on the principle of progressive disclosure and basically what this means is that it's easy to learn but hard to master and a lot of people know some Swift but almost nobody knows all Swift and that includes myself but hopefully today I'm going to show you at least something that you haven't seen before so I'm going to start with the The Humble for Loop um you often have a situation with for Loops where you want to apply some logic only to some of the elements in a list for example and you'd probably do that by nesting an if statement but there's actually a nice Clause a wear Clause that you can add to your for Loop so you avoid that extra level of nesting but let's say you have a scenario where maybe you have a list of optionals and you want to unwrap the the the non-nil ones and you can't use a wear clause for that because you can't bind new variables inside a wear what you can do instead is you can use uh case let uh so this basically lets you uh bind your non-nil string variable inside your Loop and then use it directly inside the loop without any uh further conditionals um casel isn't just for unwrapping optionals you can actually use it for other things like type casting or I guess in this case it's more like type filtering so we have a list of any value and we're only interested in pulling out the strings so you can use this uh sequence of keywords to to do that I'll leave it up to you to decide whether that's preferable to the Alternatives um another cool thing you can do with uh Loops so you often run into this situation where you've got like nested loops and you want to break out of the outer loop from inside an inner loop and you can't easily do that because the brake statement binds to whatever its kind of local scope is but in this situation you can actually label your Loop you can give it a name and then you can apply your brake to the you know the specific uh loop that you're in interested in and this also works with continue and so on and and this also can be quite useful if say you have a loop containing a switch statement and you want to break out of the loop from within the switch statement but again your break is going to bind to the switch by default and actually labels aren't limited to Loops so you can also label an if statement for example and break out of that and this is particularly interesting because normally break doesn't bind to if at all like you can't break out of an if statement normally but if you've given it a name then you can Target it for breaking and this also works with other things like a do condition for example so let's talk about strings so strings are kind of I mean everybody knows strings right they're sort of the hell World type um of of most languages and uh Swift strings are pretty similar to strings in other languages at first blush so you know if you've used strings in python or C or objective c like you kind of know what you're going to get but Swift strings have a reputation for being difficult to work with if you're trying to do anything kind of complicated so um let's say you want to get the nth character of a string uh you know in C that's very very easy you just give the index and you get the character you asked for objective c is a little bit more verose cuz you know it wouldn't be objective c if it wasn't but it's still basically the same idea and then you've got Swift and uh yeah what what on Earth is going on here so you've got this very long complicated expression to do what should be a very simple operation you've got to mention your string variable multiple times you've got to get an index and offset it and so on so you know why have they made it this difficult to do well the answer predictably is Unicode um this slide originally was titled Unicode ruined everything but I was asked to tone it down um so Unicode is great like we love Unicode um but it does cause some problems for uh some languages shall we say um so I've got this Unicode string and appropriately it's a clown car and in um in C I'm trying to pull out the second character you know I'm asking for index one and it's just giving me garbage back so I'll tried in Objective C Objective C is kind of Unicode friendly right that ought to work absolutely not it's actually only Swift that does this right very few languages get this right I think rust is another one but it's a very short list of languages that actually handle this correctly so why is that well if you look at what's actually going on under the hood with a string um strings have an encoding so in Swift and C that encoding is typically utf8 C doesn't actually really have encodings it's just kind of a bag of btes but the bag of btes are normally utf8 bytes um and so your clown is not actually one character in in you know this encoding it's it's four four separate um characters or four four separate bites and so what happens in C which doesn't really understand about encodings is that when you're asking for the second bite you're just getting the second bite of the clown rather than the next character along and even in um UTF 16 which Objective C uses it's using two 16bit um pieces for your your clown so when you ask for index one you're getting the second half of the clown rather than the car so I mean fine you know Swift has its reasons for making this complicated but why why not just build a nicer API around that it's it's pretty trivial to wrap the horrible API in a nice simple API and give it the same you know ergonomics as as C gives you and so you know you can do this yourself it's kind of a oneliner but why doesn't Swift just give you that out of the box well the answer is actually performance so the problem is that in languages like C you expect to be able to get the nth character as a a big O of one operation basically an instantaneous thing um and if Swift offered the similar API but which actually had very different performance then you'd have the problem that people would be porting C code to Swift and then saying why is Swift so slow it's a terrible language blah blah blah rather than having this kind of um attractive nuisance of an API that looks like it ought to work nicely but is actually super slow it's better just not to give you the option at all and you know make it clear that because the API is hard to use maybe you shouldn't be using it that way okay so let's talk a bit about optionals so there's two syntax is for optionals in Swift um the top one is kind of syntax sugar for the bottom one but you might be sort of forgiven for thinking these are essentially equivalent but there is actually one case in which they're different so when you have um when you're using a a synthesized Constructor for a struct basically you know this nice feature of Swift where you don't have to write the initializer yourself as long as it's internal um if you have an optional property that's using the question mark syntax then it turns into an optional parameter in your initializer so basically you don't have to specify it at all which is often very convenient the problem is that this can cause bugs so if you have a situation where you you know you have an existing struct and you later add a new property and you're expecting the compiler to tell you all the places that you were supposed to set it but it doesn't because it's optional so you accidentally end up not setting it somewhere and maybe you forget to inject a dependency somewhere that you meant to do it so to avoid this problem you can actually use the the longer form optional syntax and this has the subtle distinction that then your initializer it becomes a mandatory property it's still an optional but you have to explicitly say that it's nil if you want it to be nil but what actually is an optional well an optional is a monad and what is a monad yes so but it's kind of complicated thing to explain better people than me have tried but I like to think of a monad as basically being a box containing one or more things um perhaps a more technical description is that a type that can be mapped or flat mapped so yeah an optional is kind of a collection um you can think of an optional as being sort of a collection of one or zero items it's like an array that can only have one or nothing in it um and in fact this was actually how optionals were originally implemented in a very early version of Swift before um the the more sort of sophisticated modern implementation came along so um why is it useful to be able to map um something which can only have one element in it well you can actually use map for unwrapping optionals so often when you have a situation where you want to do something only if the optional is non- nil and you want to do something to the non-nil value you'd use the IFL syntax but you can use map in this case to um sort of apply a function that only is only executed on the values inside which in this case is either something or nothing um it's kind of uh sometimes a little bit controversial to do something like this where it's it's not it's not technically returning a value so maybe it's like a violation of functional principles but um I like okay but what is an optional really I mean you could be forgiven for thinking that an optional was maybe a pointer something like that and in Objective C that's basically what it is it's just some like sort of attribute that you apply to a pointer to say that this can be nil but that wouldn't make sense in Swift where you know uh even something primitive like an integer can be an optional in Swift an optional is actually an enum and it's specifically it's a generic enum so so it's a a an enum which can either be nothing or t where T is the type that you provide so let's talk about enums so I mean we've all seen C style enums which can basically they basically just syntax sugar for a collection of integer constants Swift extends that with string enums which can be super useful as well and Swift enums really come into their own when we're talking about enums that have Associated values these are sometimes called tagged unions in other languages so enums where each case can you know wrap a value maybe of a different type in each situation when you're comparing enums when you're checking whether you know a particular value of an enum is a particular value um they're equatable by default which is quite handy so you can just use equals but when you've got an enum with Associated values this doesn't really work because in the case of say like an HTTP response where you want to check whether the response was okay you don't want have to pass the body that you were expecting to see if that's what you got you just want to check if it's okay and then get it to give you the body so in order to do that you need to use patent matching so let's talk about patent matching so we we mentioned the if Case let syntax earlier or the case let syntax earlier and I mean this this works um it's it's hard to love as a syntax um this is sort of like so so well known there's actually a website dedicated this fact and that the website comes in two flaves work safe and not so work safe uh it it borrows from the grand tradition of the effing block syntax from The Objective C days which was another particularly unlovable API um so instead of case let you're probably going to want to use um switch which is the kind of the the maybe more familiar way of doing this and there's actually um there's some other good reasons for using switch rather than if Case let which I'll get to in a second but case let um so sorry there's these syntaxes are all effectively equivalent so you can use switch you use Ife let you can use guard case let they all do the same thing um where case Le is quite useful is that you can use it in context outside of if so the for case let I mentioned earlier is quite handy you can also use while case let and there's actually um a version of this for do catch statement so it doesn't use case it just uses catch let but it's basically the same thing it lets you do pattern matching on uh on errors that are thrown inside to do catch so let's talk about uh switch case so switch case isn't only limited to um matching optionals or unwrapping enums it can also be used for other kinds of pattern matching so for example you can match whether an integer value is inside a particular range or for that matter a floating point value you can also match if um you can match the type of a thing that you're passing in so you can say is it this type or you can unwrap it as that type and in fact you can um you can add a wear Clause to your cases as well uh so that if you want to check both um you know your your patent matching and also maybe some other condition as well you can do that and if you use a wild card for your case matching you can effectively build a sort of fancy if else statement this way by just you know ignoring the value that's passed in and then just applying some logic in your wear Clauses um it's also possible to create your own patents so there's the uh the tier equals operator basically lets you define instead of a equality comparison it's a kind of patent matches comparison and if you define this for your own types it lets you use them in switch cases to match let's say if you have some kind of custom sort of collection type you can match whether a value is logically inside that type for whatever that means for your your particular um logic um something to bear in mind with switch uh case statements is I've often seen this pattern where you you match a particular case and then like in this case we only have two cases so you're like well I'll match if it's portrait and if it's not then I'll just assume that it's landscape because what else could it be right and it's very it's very common to just use a sort of a lazy default like the problem is that if somebody then comes along later and adds maybe another case to your enum your logic breaks but the compiler won't warn you it has no way of knowing that actually your default is no longer correct in this situation and for this reason it's better to use um exhaustive switching so this is a really nice feature of Swift where basically if you explicitly handle all cases and don't have a default then if somebody comes along and adds another case the compiler is going to warn you that that case hasn't been handled um another situation where it's really handy to use uh switch is if you have a situation with maybe some complicated Boolean logic where you're checking multiple different Boolean variables or indeed any kind of variable really um it's very easy to sort of accidentally Miss branches in your in your logic you know there's a case here where we we checked if you're logged in and if you're an admin but we forgot to check for the case where you're logged in and you're not an admin and then maybe we've introduced a bug because of this and the compiler again isn't really empowered to warn us that we've missed that case but if you replace this with switch over a tuple of Boolean values then it lets you create an exhaustive switch over essentially what's a truth table so you can basically make sure that you handle every possible scenario in this case and if there are cases you don't care about you're still free to ignore them using an underscore as a wild card so rather than using a default here where if I'd forgotten a case it wouldn't be able to warn me by explicitly just saying well I don't care you know if you're not logged in I don't care if you're an admin I can just Target that specific case that I want to ignore but it will still warn me if I for for en about anything else okay so in Swift There are value types and there are reference types value types uh typically they're stored in line maybe on the stack um they're copied every time that they're used and they tend to have a fixed size reference Types on the other hand they'll normally live on the Heap um they're shared between call sites so if you make copies of then they're basically all references to the same value and they're dynamically allocated so they can change size at at runtime for example so as we all know in Swift um classes are reference types whereas structs enums and tups are all value types right it's not exactly uh the case so um I'll give you an example um I've created here A A struct it's um it's a node maybe it's a node in a link list for example and I want to use a struct for this but I can't unfortunately because it's not really logically meaningful for a structor to be able to contain a copy of itself it would basically have to be infinitely large in order to be able to do that and even if um in this case my my parent node is optional that doesn't really help because as we saw before um optionals are enums and enums are also value types so it would still require infinite storage in order to work this way and yet if I was to make this something like a tree node which has multiple children and contains an array of other nodes this is absolutely fine the Swift compiler is quite happy to let me do this and the thing is arrays are struct they're value types in Swift so like how is this possible well the answer is that if you look at the implementation of how array Works yes it is a struct on the surface but that's that's really just a facade what's actually happening internally is that it's a class so it's a value type but it's got a lot of the semantics of a reference type in terms of dynamic allocation ability to grow at run time and so on and it's actually stored on the Heap so if it's following like if it's got a reference-based implementation how is it still managing to maintain value semantics well the answer is that it uses a paradigm called copy on write um or cow for short so copy on WR is a mechanism that Swift uses to basically allow classes to behave like value types where when they're copied um you know changing one of them doesn't change all of the other ones and the way this works is using a function called is known uniquely referent so basically whenever you try and mutate an array the uh the the subscript implementation of the array checks whether its internal storage is being used by any other copies of that array and if there are any other um references to it it will make a copy of the entire array mutate that and then store that copy internally so that in future um it's going to be mutating that one instead hence copy on right when you're copying it normally it doesn't need to do that and so you have a lower overhead now there's quite a lot of myths about copy on right in in Swift I think there's a perception maybe that this is kind of one of those magic things the compiler just takes care of for you the reality is that Swift Never implements copy on right for you it's it's not something where it's just like oh my struts kind of big like maybe the compiler will sort of turn it into a class under the hood um you have to do all of that work yourself and it is quite a lot of overhead um it's overhead both in terms of implementation like difficulty and complexity but also actually in terms of performance as well um because um because of all all the automatic reference counting and heat promotion and everything that has to happen it's a lot more expensive to have a structure that's backed by a class than just a regular struct it can however still be the Lesser evil in terms of performance in some cases um something that happens when you have a struct containing multiple um copy on right type so for example if you have a struct with multiple arrays inside it is that every time you make a copy of that struct it's got to increase the retain count of all of the internal storages of all of the properties inside it and that can actually have a significant impact if you're doing something like this inside in like a hot Loop in your code and so you might actually find that with a case like this it's more efficient if you wrap all of those properties inside a class and manage the um the copy and right logic yourself so enums like struts also cannot contain themselves by default for exactly the same reason they'd have to be infinitely large but with enums there's a really nice um solution to this which is the indirect keyword so the indirect keyword lets you either the the whole enum or just a particular case as being stored like out of line of the enum itself which allows for it to have self references um there has been talk about adding an indirect keyword that works with structs as well but it's not it's not currently gone anywhere but yeah this is a much nicer way of implementing the kind of uh copy on write pattern for example without having to do all of that logic yourself so we know that um strs and enums are values and classes are references but what are Protocols are they values or are they references well the answer is it depends so a regular protocol is actually a value type um you know it can represent a value type or it can represent a class um but there are also class-bound protocols so protocols that um conform to any object and those are reference types um so let's talk a little bit about class bound protocols so they um they they get some like extra capabilities versus an um protocol you can use the um weak reference uh keyword with them because it knows that it's a reference so it can be a weak reference and you can also use the the triple equals identity operator to basically ask if two instances are the same which isn't possible with a normal protocol um they also have a lower overhead um basically they require less storage I'm going to talk about that in a minute um but if you can use a class bound protocol you're generally better off doing so basically if you know that everything that's going to implement Meed is going to be a class um and they actually have this nice feature that they can be restricted to a specific based class so rather than any object you can say well this protocol only makes sense in the context of say a view I did try and find a more server side relevant example of this but I had to look at Vaper and all of its classes are are final so that doesn't really make sense um but you know imagine that this is a class that you care about okay so uh here's an example of a class bound protocol so I've got my protocol Fu any object and I've got um my concrete classes bar and Bas which both Implement my Fu protocol and I've got this variable uh Fu which is sorry I used Fu too much but insert your own name here um fu is of type any Fu um and the question is like what actually is that like what how is that represented and the answer is that it's it's just a pointer it's a it's a pointer to some class so that's fine and I can assign bar to it and then I can change it BS and that's okay because pointers are Pointers um it's interchangeable I can swap them around at runtime and that requires you know eight bytes of storage is actually sometimes 16 because of witness tables but we don't need to go into that um but then let's talk about what happens with the regular protocol so this isn't a class bound protocol it could be anything I've got a struct bar that implements it I've got a class B that implements it basically anything that conforms to Fu can go here and so if I've got a variable of type any Fu like what is that how does that work how can I how can I alloc at storage for something that could be literally any size um how big actually is that you know variable in memory well to answer that you have to understand how something called existentials work so what is an existential um existentials get their name from the axium that there exists a type T there exists you see a type t such that t conforms to the protocol you can read about them in the um Swift book so what is an existential well it's basically the the actual type that exists when you're not sure what type you have and existentials are represented using a existential container this is the C++ code that is the implementation of an existential container in the Swift runtime um and uh this is a struct and it's 40 bytes in size so the answer to the question how big is something when you don't know how big it is is it's 40 bytes so this um this struct has a few things inside it it's got some metadata we mentioned witness tables before um the interesting part here though is this fix sized buffer so there's a buffer inside the existential container it's 24 bytes and this is where your actual value gets put so let's say you've got a stru that conforms to your protocol if it's 24 byes or under it just gets put in line inside there and it's all good you're done if it's more than 24 bytes it has to be stored somewhere on the Heap and a poter to it gets put inside this buffer instead and so you have this interesting performance Cliff where if you're using Proto with struct if your struct suddenly goes from being 24 bytes to 25 bytes it can have a sudden performance impact where a bunch of stuff that was previously living on the stack and like having nice cash locality is all suddenly spread all over the Heap and you know your performance falls through the floor and you're not really sure why and this is one of those like interesting little performance details where unless you actually understand what's going on under the hood there's no possible way that you could expect or or or predict that that would happen so I mentioned for um any Fu but there's also this syntax some Fu and some any relatively new keywords to the Swift language what is the difference between Su and any well any is basically it means that this is a value where it can it can change at runtime it could be anything that conforms to this protocol some is a little bit different because although you don't know what type it's going to be you know that the compiler will know what type it is when when it's actually building your app so any is for when you don't know at runtime what type it's going to be and some is for when you don't know at a compil time what type it's going to be and because of this um some is potentially quite a lot more efficient to use than any um there's only going to be uh like one specific type it's not going to change and so the compiler can maybe optimize it maybe if there's only one type in your whole um application that conforms to this protocol it can just be in lined and it doesn't need to do any of this messing with existential containers at all so we talked about uh value versus reference types but another way of dividing types in Swift is nominal versus structural nominal types are ones which are unique because of their name structural types are ones that are unique because of their structure um classes struct and enums are all examples of nominal types so if they have different names then they're different types tuples and closures are examples of structural types where it doesn't matter what they're called as long as they have the same structure then they're interchangeable I'll give you an example so phenomenal types I might have a user ID and a service ID these have the same structure they're both basically just string wrappers but because they have different names they can't be used interchangeably here I've got an example I've created Bob which is a user and prod which is a service and I'm not allowed to compare them because they're they're not the same thing they're apples and oranges and yet if I was to use uh tuples to implement these and i' I've created type aliases here just sort of readability but it doesn't actually make any difference to the type my person um is obviously you know it's a person it has a name and an age my address has a street and a house number but as far as Swift's concerned these are both just topples of string and int and so even though I can look at this code and see that logically it makes no sense at all to compare these things for equality as far as Swift's concerned it's perfectly okay and they can be exactly the same thing um I mentioned that closures are not example of a structural type let's talk about closures for a bit so you've all seen closures before they're kind of sort of backwards functions where the parameters go after the the curly brace this is the typical syntax you might use to declare a closure what you may not realize is that all of these things are also closures regular functions are closures methods are closures initializers are closures and even enum cases are also closures they're all closures and because closures are structural types different types of closure can be used interchangeably as long as their signatures match in this case I've got a function print type interface that takes a callback which is a closure and it it accepts a Boolean parameter and it returns a type face and I have an enum type face which has a case which has a Boolean payload and in actual fact that can conform to this protocol uh sorry to this um closure signature so I can pass my uh case directly to something which is expecting a closure and because the signatures match it's per L happy to to accept it and and the code will work fine um it's not immediately obvious like why this would be useful but a slightly more uh useful case is when you're dealing with something like a method instead so here's an example with a method where I've got a four each and I'm passing at a closure to add a sub view again sorry about the views thing I'm basically an IRS developer at heart um but you can simplify this you can just pass the ad subview function directly which is you know it saves you some typing which is always good right um there is a danger to to doing this um so you're all aware of the problem with uh retain Cycles in Swift and um in a situation like this where you've got an update Handler function and you're using a closure the compiler is going to warn you here that oops you're accidentally capturing self and in order to suppress this warning you would need to explicitly mention self in the capture list maybe' Market his weak self to avoid the retain cycle here but um if you try to be clever and just call the the default up Handler method directly from you know passing it as your closure uh Swift isn't going to warn you anymore this works perfectly well it's just you've created a a retain cycle here and the compiler isn't going to let you know that you've done it so generally speaking it's probably maybe not a good idea to do this where it is um a much better idea to to use this Paradigm is for initializers where you don't have this risk of a um retain cycle because you can't create a retain cycle if you're passing initializer because the object doesn't exist yet um so this is quite nice from a sort of syntax sugar point of view but it does actually have a a pretty handy real world use case um I'm going to talk about that in a second but first a little uh discussion about factories so um factories are a solution to a problem uh that we all encounter when it comes to testing so you've got a class like a tool user and internally it's creating a hammer and it's doing some stuff with that and that's all great and then you get to the point where you want to test it and you realize um this is really very untestable code because you don't know what dependencies it's using internally you can't get at them you can't mock them you can't um see what's happening inside these classes you don't even know it's using these classes and so the sort of the standard solution to solve this is that we use the factory pattern so you have a hammer Factory um or rather you have a hammer Factory protocol um the hammer Factory creates the hammer you can replace your Hammer factory with a mock Hammer Factory and so on and um this does involve quite a lot of boilerplate code but you know it's it's the only solution like you know this is well established this is what we have to do right I'm not a huge fan of this pattern I would say um it it it has a bit of a sort of you know Java design patterns from the '90s uh enterpris feel to it I feel like we can do better than this Swift is a modern language uh it has closures and uh all of this boiler plate can just go away and just be replaced with a closure I mean in a sense what is a function if not a factory it's you know a a thing for making something and returning it to you so we don't yeah we don't need uh we don't need a protocol we don't need concrete implementations we can just have a closure the usage for this is pretty much the same um as it would be uh for the the protocol based version we we have a function which accepts a factory function um we pass in our Factory function and it creat the hammer internally but this is where the uh the fact that initializers arejust closures is quite nice because here instead of having to create a closure and a factory function and everything that actually creates our object we can just pass the Hammer's initializer as the factory function because that's all an initializer is it's it's a function that produces the object so we can just pass it indirectly and if we want to mock it we just pass the mock Hammer's initializer instead so I did have one more thing that's a huge Li many many more things um I didn't actually have time uh to cover everything I wanted to um I crowdsourced a bunch of like cool things from Mastered on and I've cut down that list to to just the ones that I thought were most interesting for time reasons but I'm going to try and rush through this as quickly as I can so here's some things I like um we talked about enums before um Swift doesn't have name spaces which is kind of annoying but you uh mte nums actually make great name spaces because um the thing about an mte num is you can't instantiate it there's there's no real like object there at all you can't make an empty enum it just exists as a type into which you can put other things and so it's great for using if you want a name space um by default everything in Swift as you probably know is module scope but sometimes uh you have like a bunch of things that you want to make public and instead of having to mark all of them public you can just put them in a public extension and then they all become public by Magic and it's super handy because you know maybe you can uh mark them as private initially and then if you want to change them to public later you just have to change one line instead of you know hundreds um there's a nice new thing in Swift 10.10 protocols can now finally be nested inside other types which is a great relief it's like the one thing that you couldn't do it was so annoying um another really annoying thing is if if you have a string and you want to convert it to data because you need data um but it it's an optional why does it return optional it can never fail so annoying but there's this alternative syntax you can use which which is uh I only discovered this recently but it's changed my life um if you have long numeric literals I don't know if you knew this but you can use underscores to break them up which is a really nice feature and this last one um I included mostly as a cautionary tail so when you're writing unit tests um to test your Json outputs you've probably run into the situation where you you paste in your Json and then the unit test fails because the order of the Json and when it's generated isn't deterministic so you can Mark um on your Json encoder you can mark the output formatting sorted Keys property which will mean that it always generates the keys in a persistent order which solved this problem and it solved this problem for many years is and then Apple broke it in iOS 18 so they changed the order so I guess it's going to be consistent from now on again probably um hopefully but yeah maybe don't rely on that after all anyway that was all I had for today thank you

Need another transcript?

Paste any YouTube URL to get a clean transcript in seconds.

Get a Transcript