When does the order of SwiftUI modifiers matter, and why?
An essential part of SwiftUI.
Articles, podcasts and news about Swift development, by John Sundell.
An essential part of SwiftUI.
Different formatting strategies for different kinds of numbers.
APIs and techniques for performing file system operations.
Ellen Shapiro returns to the show to discuss framework and SDK development, and how that often requires a somewhat different process from app development. Also, API design, GraphQL, using the standard library’s protocol-oriented design, and more.
Tips and techniques that can be good to keep in mind when designing various APIs in Swift.
Filtering collections through type-safe predicates built using closures and operators.
SwiftUI ships with a special view called AnyView
, which can be used as a type erased wrapper to enable multiple view types to be returned from a single function or computed property, or to let us reference a view without having to know its underlying type.
However, while there are cases in which we might need to use AnyView
, it’s often best to avoid it as much as possible. That’s because SwiftUI uses a type-based algorithm to determine when a given view should be redrawn on screen, and since two AnyView
-wrapped views will always look completely identical from the type system’s perspective (even if their underlying, wrapped types are different), performing this kind of type erasure significantly reduces SwiftUI’s ability to efficiently update our views.
So, in this article, let’s take a look at two core techniques that can help us avoid AnyView
while still enabling us to work with multiple view types in very dynamic ways.
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
When using SwiftUI to build views, we very often use the some View
opaque return type to avoid having to explicitly define what exact type that we’re actually returning. That’s especially useful since (almost) every time that we apply a modifier to a given view, or change the contents of a container, we’re actually changing the type of view that we’ll return.
However, the compiler is only able to infer the underlying return type when all of the code branches within a given function or computed property return the exact same type. So something like the following won’t compile, since the if
and else
branches within our textView
property return different types of views:
struct FolderInfoView: View {
@Binding var folder: Folder
var isEditable: Bool
var body: some View {
HStack {
Image(systemName: "folder")
textView
}
}
private var textView: some View {
// Error: Function declares an opaque return type, but
// the return statements in its body do not have matching
// underlying types.
if isEditable {
return TextField("Name", text: $folder.name)
} else {
return Text(folder.name)
}
}
}
Initially, it might seem like the above is one of those situations in which AnyView
must be used in order to give all of our code branches the same return type. What’s very interesting, though, is that if we instead place the above conditional expression inline within our body
property, the compiler error goes away:
struct FolderInfoView: View {
@Binding var folder: Folder
var isEditable: Bool
var body: some View {
HStack {
Image(systemName: "folder")
if isEditable {
TextField("Name", text: $folder.name)
} else {
Text(folder.name)
}
}
}
}
That’s because SwiftUI uses a function/result builder to combine all of the views that are defined within a given scope (such as the above HStack
) into a single return type, and the good news is that we can use that same builder type within our own properties and functions as well.
Like we took a look at in “Adding SwiftUI’s ViewBuilder attribute to functions”, all that we have to do to utilize that same powerful view building functionality is to use the @ViewBuilder
attribute — which in turn lets us express multiple types of views within the same scope, like this:
struct FolderInfoView: View {
@Binding var folder: Folder
var isEditable: Bool
var body: some View {
HStack {
Image(systemName: "folder")
textView
}
}
@ViewBuilder
private var textView: some View {
if isEditable {
TextField("Name", text: $folder.name)
} else {
Text(folder.name)
}
}
}
Note how we’re no longer using any return
statements within our new textView
property implementation, since each expression will now be parsed by SwiftUI’s ViewBuilder
, rather than being returned separately.
So the first way that AnyView
can often be avoided is by using the ViewBuilder
attribute whenever we want a given property or function to be able to return multiple view types.
Another really common type of situation in which AnyView
is often used is when we want to store a given view in a property without having to know its exact type. For example, let’s say that we’re working on the following ItemRow
, which currently uses AnyView
to enable us to inject any accessoryView
that we want to display at the trailing edge:
struct ItemRow: View {
var title: String
var description: String
var accessoryView: AnyView
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(title).bold()
Text(description)
}
Spacer()
accessoryView
}
}
}
Since we can’t use the some View
opaque return type for stored properties (they’re not returning anything, after all), and since we’re no longer dealing with a predefined number of views that can be combined using ViewBuilder
, we’ll have to explore another strategy if we’d like to remove our usage of AnyView
in this case.
Just like how we previously took inspiration from SwiftUI itself when using ViewBuilder
, let’s do the same thing here. The way that SwiftUI solves the problem of enabling any view to be injected is by making the host view generic over the type of view that it’ll contain. For example, the built-in HStack
container is defined as a generic that has a Content
type, which in turn is required to conform to the View
protocol:
struct HStack<Content>: View where Content: View {
...
}
Using the same kind of generic type constraint, we can make our ItemRow
adopt the exact same pattern — which will let us directly inject any View
-conforming type as our accessoryView
:
struct ItemRow<Accessory: View>: View {
var title: String
var description: String
var accessoryView: Accessory
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(title).bold()
Text(description)
}
Spacer()
accessoryView
}
}
}
Not only does the above give us better performance during view updates (since all of the types involved are now well-defined and transparent to the type system), it also makes our call sites simpler as well, since each accessoryView
no longer has to be manually wrapped within an AnyView
:
// Before:
ItemRow(
title: title,
description: description,
accessoryView: AnyView(Image(
systemName: "checkmark.circle"
))
)
// After:
ItemRow(
title: title,
description: description,
accessoryView: Image(
systemName: "checkmark.circle"
)
)
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
While SwiftUI makes many aspects of UI development simpler, there’s no denying that it’s an incredibly complicated framework that makes heavy use of some of Swift’s most powerful features. So while it might be easy to get started building views using it, we often have to use quite advanced techniques (like generic programming) in order to make the best use of what SwiftUI has to offer.
Of course, just because it might be a good idea to avoid AnyView
as much as possible doesn’t mean that it should never be used. It’s a part of SwiftUI’s public API for a reason, and the above two techniques won’t work in every single situation — but when they do, they’ll often result in much more elegant and efficient code.
What do you think? Will the above techniques help you remove any usages of AnyView
? Let me know, along with your questions or comments, either via Twitter or email.
Thanks for reading!
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
My thanks to the team behind DetailsPro — a fantastic SwiftUI view editor for Mac and iOS — for sponsoring Swift by Sundell last week. DetailsPro lets both developers and designers quickly build prototypes completely visually, using real SwiftUI components, which also makes it an excellent way to familiarize yourself with SwiftUI’s overall APIs and features.
→ Get DetailsPro for free from the App Store
Not only does DetailsPro run real SwiftUI code under the hood, which ensures that the design of your views will perfectly match how they’ll eventually get rendered within an app, it also lets you easily export that code as high-quality Swift snippets.
That means that you can start designing a new app, widget, or feature in DetailsPro, and then move it over to Xcode whenever you’d like. Plus, DetailsPro also seamlessly syncs your projects across your devices, so that you can start working on your iPad, and then easily pick that same project up on the Mac, or even on your iPhone!
Even though this is a super powerful, pro-caliber tool, it’s completely free for up to five designs — so simply download it from the App Store and give it a try, and by doing so you’ll also directly support Swift by Sundell as well.
These days, it’s incredibly common for apps to have to work with email addresses one way or another, and when doing so, we typically want to perform some kind of client-side validation on any addresses that were entered by the user.
Conceptually, performing that kind of validation should be quite simple, given that email addresses need to conform to a certain format in order to be valid, but in practice, it can be quite difficult to figure out exactly how to implement that kind of logic in Swift.
One very common way to do so is to use a regular expression, which when combined with NSPredicate
lets us validate a given email address String
like this (the actual regular expression has been omitted for brevity):
class SignUpViewController: UIViewController {
private lazy var emailAddressField = UITextField()
@objc private func handleSignUpButtonPress() {
let emailPredicate = NSPredicate(
format: "SELF MATCHES %@", "<regular expression>"
)
guard let email = emailAddressField.text,
emailPredicate.evaluate(with: email) else {
return showErrorView(for: .invalidEmailAddress)
}
...
}
}
However, while the above technique certainly works, it does have a few downsides. First of all, we have to either carefully craft the regular expression that we wish to use ourselves, or figure out which of the many variants that can be found online that’s actually the right one to use. But perhaps more importantly is that when storing and passing email addresses as raw String
values, there’s no way of establishing a guarantee that a given value has actually been validated.
For example, by just looking at the following User
type, we can’t tell whether the emailAddress
that is being stored within it has actually been passed through any kind of validation, since that logic is currently completely detached from that value:
struct User: Codable {
var name: String
var emailAddress: String
...
}
However, it turns out that Swift actually has a built-in pattern that could let us solve the above set of problems quite elegantly.
If we think about it, we very often deal with other kinds of validated raw values in the shape of enums. For example, the following UserGroup
enum can be initialized with a raw String
value, but will only actually return an instance if that raw value matched one of our enum’s cases:
enum UserGroup: String {
case admins
case moderators
case regular
}
At first, it might seem like the init(rawValue:)
initializer that raw value-backed enums get is the result of some kind of hard-coded compiler logic that’s specific for enums. While that’s partially true, that initializer is actually part of a general-purpose protocol called RawRepresentable
, which enums that have raw values automatically conform to.
That means that we can also define our own RawRepresentable
types as well, which lets us encapsulate our email validation logic in a very neat way — like this:
struct EmailAddress: RawRepresentable, Codable {
let rawValue: String
init?(rawValue: String) {
// Validate the passed value and either assign it to our
// rawValue property, or return nil.
...
}
}
Note that our new EmailAddress
type also conforms to Codable
, which we get support for without having to write any additional code. Values will be automatically encoded and decoded to and from our underlying rawValue
property, just like how enums work in that context.
Now, while we could simply copy/paste our regular expression-based validation logic from before into our new EmailAddress
type, let’s also use this opportunity to explore a different (and, if you ask me, better) way of performing our validation — by using Foundation’s NSDataDetector
API.
Under the hood, NSDataDetector
actually uses regular expressions as well, but hides those details behind a series of dedicated APIs that let us identify tokens like links, phone numbers, and email addresses. Here’s how we could use the link
checking type to extract a mailto
link for the email address that we’re validating, which we could then perform a few extra checks on like this:
struct EmailAddress: RawRepresentable, Codable {
let rawValue: String
init?(rawValue: String) {
let detector = try? NSDataDetector(
types: NSTextCheckingResult.CheckingType.link.rawValue
)
let range = NSRange(
rawValue.startIndex..<rawValue.endIndex,
in: rawValue
)
let matches = detector?.matches(
in: rawValue,
options: [],
range: range
)
// We only want our string to contain a single email
// address, so if multiple matches were found, then
// we fail our validation process and return nil:
guard let match = matches?.first, matches?.count == 1 else {
return nil
}
// Verify that the found link points to an email address,
// and that its range covers the whole input string:
guard match.url?.scheme == "mailto", match.range == range else {
return nil
}
self.rawValue = rawValue
}
}
With the above in place, we can now simply use our new EmailAddress
type wherever we’re storing an email address, and we’ll get a compile time guarantee that each of those addresses will always be validated when they’re converted from a raw String
value:
struct User: Codable {
var name: String
var emailAddress: EmailAddress
...
}
To then actually convert raw email address strings into instances of our new type, we can simply use the same init(rawValue:)
initializer that’s used to convert raw values into enums, or in the case of our SignUpViewController
from before, we could use flatMap
on the optional String
that UITextField
gives us, and then pass our new type’s initializer as a first class function — like this:
class SignUpViewController: UIViewController {
private lazy var emailAddressField = UITextField()
@objc private func handleSignUpButtonPress() {
// As an added bonus, we also trim all whitespaces from
// the string that the user entered before validating it:
let rawEmail = emailAddressField.text?.trimmingCharacters(
in: .whitespaces
)
guard let email = rawEmail.flatMap(EmailAddress.init) else {
return showErrorView(for: .invalidEmailAddress)
}
...
}
}
Of course, if we wanted to, we could’ve kept using the regular expression-based approach within our EmailAddress
implementation as well, but I personally think that the combination of a dedicated RawRepresentable
type and NSDataDetector
results in a much simpler solution.
What do you think? Feel free to share this article if you enjoyed it, or contact me via either Twitter or email if you have any questions or comments. Thanks for reading!
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
Over the years, Foundation’s built-in URLSession
API has grown to become a versatile and very powerful networking tool, to the point where third party libraries are often no longer required to perform standard HTTP network calls in a simple and straightforward way.
While many of the convenience APIs that URLSession
ships with are focused on GET
requests used to fetch data, in this article, let’s take a look at how other HTTP methods can be used as well — specifically how different kinds of POST
requests can be performed without any external dependencies.
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
Perhaps the simplest way to use URLSession
to perform a POST
request is to use the URLRequest
-based overloads of its various dataTask
APIs (which support both delegate and closure-based callbacks, as well as Combine). Among many other things, URLRequest
enables us to customize what httpMethod
that a given network call should use, as well as other useful parameters, such as what httpBody
data to send, and what cachePolicy
to use. Here’s an example:
struct Networking {
var urlSession = URLSession.shared
func sendPostRequest(
to url: URL,
body: Data,
then handler: @escaping (Result<Data, Error>) -> Void
) {
// To ensure that our request is always sent, we tell
// the system to ignore all local cache data:
var request = URLRequest(
url: url,
cachePolicy: .reloadIgnoringLocalCacheData
)
request.httpMethod = "POST"
request.httpBody = body
let task = urlSession.dataTask(
with: request,
completionHandler: { data, response, error in
// Validate response and call handler
...
}
)
task.resume()
}
}
Depending on the server that our POST
request is being sent to, we might also want to configure our URLRequest
instance further, for example by giving it a Content-Type
header.
Alternatively, we could instead choose to use the uploadTask
API to create our request task, which both lets us upload data while the app is in the background, and provides built-in support for attaching body data directly to the task itself:
struct Networking {
var urlSession = URLSession.shared
func sendPostRequest(
to url: URL,
body: Data,
then handler: @escaping (Result<Data, Error>) -> Void
) {
var request = URLRequest(
url: url,
cachePolicy: .reloadIgnoringLocalCacheData
)
request.httpMethod = "POST"
let task = urlSession.uploadTask(
with: request,
from: body,
completionHandler: { data, response, error in
// Validate response and call handler
...
}
)
task.resume()
}
}
While either of the above two approaches will work perfectly fine when sending smaller amounts of data as part of a POST
request, sometimes we might want to upload a file that could potentially be quite large (even something simple, like an image, could easily be several Megabytes in size). When doing that, we likely want to give the user some form of real-time progress updates, since otherwise our app’s UI might appear slow or even unresponsive.
Unfortunately, none of the closure-based or Combine-powered URLSession
APIs offer direct support for observing a request’s ongoing progress, but thankfully, that’s something that we can implement quite easily using the good old fashioned delegate pattern.
To demonstrate, let’s create a FileUploader
class (which needs to be a subclass of Objective-C’s NSObject
). We’ll then use a custom URLSession
instance, rather than the shared
one, since that’ll let us become the delegate of that session. We’ll then define an API that’ll let us upload a file from a given local URL, and we’ll let the callers of that API pass in two closures — one for handling progress events, as well as a standard completion handler. Finally, we’ll store all progress event handlers in a dictionary based on each upload task’s ID, so that we’ll later be able to call those closures within our delegate protocol implementation:
class FileUploader: NSObject {
// We'll define a few type aliases to make our code easier to read:
typealias Percentage = Double
typealias ProgressHandler = (Percentage) -> Void
typealias CompletionHandler = (Result<Void, Error>) -> Void
// Creating our custom URLSession instance. We'll do it lazily
// to enable 'self' to be passed as the session's delegate:
private lazy var urlSession = URLSession(
configuration: .default,
delegate: self,
delegateQueue: .main
)
private var progressHandlersByTaskID = [Int : ProgressHandler]()
func uploadFile(
at fileURL: URL,
to targetURL: URL,
progressHandler: @escaping ProgressHandler,
completionHandler: @escaping CompletionHandler
) {
var request = URLRequest(
url: targetURL,
cachePolicy: .reloadIgnoringLocalCacheData
)
request.httpMethod = "POST"
let task = urlSession.uploadTask(
with: request,
fromFile: fileURL,
completionHandler: { data, response, error in
// Validate response and call handler
...
}
)
progressHandlersByTaskID[task.taskIdentifier] = progressHandler
task.resume()
}
}
Next, let’s implement the URLSessionTaskDelegate
protocol, which is a specialized version of the base URLSessionDelegate
protocol that adds a few extra methods to enable us to observe task-specific events. In this case, we only want to be notified when the progress of a given URLSessionTask
was updated, which can be done by implementing the following method:
extension FileUploader: URLSessionTaskDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64
) {
let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
let handler = progressHandlersByTaskID[task.taskIdentifier]
handler?(progress)
}
}
With the above in place, we’ll now be able use the percentage values passed into each progressHandler
closure to drive any UI component that we want to use to visualize the progress of an upload — such as a ProgressView
, UIProgressView
, or NSProgressIndicator
.
Finally, let’s also take a look at how we could convert the above FileUploader
to use Combine instead of multiple closures. After all, Combine’s "values over time”-focused design is quite a perfect fit for modeling progress updates, since we want to send a number of percentage values over time, and to then end with a completion event, which is exactly what a Combine publisher does.
While we could choose to implement this functionality using a custom publisher, let’s use CurrentValueSubject
in this case, which provides a built-in way to send values that then get cached and sent to every new subscriber. That way, we can associate each upload task with a given subject (just like how we previously stored each progressHandler
closure) and then return that subject as a publisher using the eraseToAnyPublisher
API — like this:
class FileUploader: NSObject {
typealias Percentage = Double
typealias Publisher = AnyPublisher<Percentage, Error>
private typealias Subject = CurrentValueSubject<Percentage, Error>
private lazy var urlSession = URLSession(
configuration: .default,
delegate: self,
delegateQueue: .main
)
private var subjectsByTaskID = [Int : Subject]()
func uploadFile(at fileURL: URL,
to targetURL: URL) -> Publisher {
var request = URLRequest(
url: targetURL,
cachePolicy: .reloadIgnoringLocalCacheData
)
request.httpMethod = "POST"
let subject = Subject(0)
var removeSubject: (() -> Void)?
let task = urlSession.uploadTask(
with: request,
fromFile: fileURL,
completionHandler: { data, response, error in
// Validate response and send completion
...
subject.send(completion: .finished)
removeSubject?()
}
)
subjectsByTaskID[task.taskIdentifier] = subject
removeSubject = { [weak self] in
self?.subjectsByTaskID.removeValue(forKey: task.taskIdentifier)
}
task.resume()
return subject.eraseToAnyPublisher()
}
}
Now all that remains is to update our URLSessionTaskDelegate
implementation to send each progress value to the subject associated with the task in question, rather than calling a closure:
extension FileUploader: URLSessionTaskDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64
) {
let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
let subject = subjectsByTaskID[task.taskIdentifier]
subject?.send(progress)
}
}
Just like that, we can now easily perform both simpler POST
requests and file uploads, with progress events, using either Combine or a closure-based API. Really nice!
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
While the above series of implementations are not a complete networking library by any stretch of the imagination, they’ve hopefully demonstrated how the built-in functionality that URLSession
provides is often all that we need to perform many different kinds of requests, including those that involve posting data or uploading files.
I hope that you found this article useful. If you have any questions or comments, then feel free to reach out via either Twitter or email, and don’t forget to check out this week’s sponsor if you’d like to support my work. Thanks for reading!
And when we might want to do that.
Solving real-world problems.
For convenience and type safety.
Being a fairly strict, statically compiled language, it might not initially seem like Swift offers much in terms of syntax customization, but that’s actually quite far from the case. Through features like custom and overloaded operators, key paths, function/result builders, and more, there are a lot of opportunities for us to tweak Swift’s syntax for particular use cases.
Of course, it could also definitely be argued that any kind of syntax customization should be approached with great caution, as non-standard syntax could also easily become a source of confusion if we’re not careful. But, in certain situations, that tradeoff might be worth it, and can sort of let us craft “micro-DSLs” that can actually help us make our code more clear, rather than the opposite.
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
To take a look at one such case, let’s say that we’re working on an app for managing, filtering and sorting articles, which features the following Article
data model:
struct Article {
var title: String
var body: String
var category: Category
var isRead: Bool
...
}
Now let’s say that a very common task within our code base is to filter various collections that each contain instances of the above model. One way to do that would be to use the fact that any Swift key path literal can be automatically converted into a function, which lets us use the following compact syntax when filtering on any boolean property, such as isRead
in this case:
let articles: [Article] = ...
let readArticles = articles.filter(\.isRead)
That’s really nice, however, the above syntax can only be used when we want to compare against true
— meaning that if we wanted to create a similarly filtered array containing all unread articles, then we’d have to use a closure (or pass a function) instead:
let unreadArticles = articles.filter { !$0.isRead }
That’s certainly not a big problem, but if the above kind of operation is something that we’re performing in many different places across our code base, then we might start to ask ourselves: “Wouldn’t it be great if we could also use that same nice key path syntax for negated booleans as well?”
This is where the concept of syntax customization comes in. By implementing the following prefix function, we can actually create a small little tweak that’ll let us use key paths regardless if we’re comparing against true
or false
:
prefix func !<T>(keyPath: KeyPath<T, Bool>) -> (T) -> Bool {
return { !$0[keyPath: keyPath] }
}
The above is essentially an overload of the built-in !
prefix operator, which makes it possible to apply that operator to any Bool
key path in order to turn it into a function that negates (or flips) its value — which in turn now lets us compute our unreadArticles
array like this:
let unreadArticles = articles.filter(!\.isRead)
That’s quite cool, and doesn’t really make our code confusing, given that we’re using the !
operator in a way that’s consistent with how it’s used by default — to negate a boolean expression.
Now, to take things even further, let’s also make it possible to use key paths to form filter queries that compare a given property against any kind of Equatable
value. That’ll become useful if we, for example, wanted to filter our articles
array based on each article’s category
. The type of that property, Category
, is currently defined as an enum that looks like this:
extension Article {
enum Category {
case fullLength
case quickReads
case basics
...
}
}
Just like how we previously overloaded the !
operator with a key path-specific variant, we can do the same thing with the ==
operator as well, and just like before, we’ll return a Bool
-returning closure that can then be directly passed to APIs like filter
:
func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
return { $0[keyPath: lhs] == rhs }
}
With the above in place, we can now easily filter any collection using a key path-based comparison, like this:
let fullLengthArticles = articles.filter(\.category == .fullLength)
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
Depending on who you ask, the fact that Swift lets us easily create the above kind of functionality through a couple of lightweight overloads is either really awesome or incredibly concerning. I tend to fall somewhere in the middle, and think that it is indeed really great that we can make minor domain-specific tweaks to Swift’s syntax, but at the same time, I think those tweaks should always be made with the goal of making our code simpler, not more complex.
Like with all things I write about here on Swift by Sundell, I’ve personally used the above technique in a few projects in which I had to do a lot of collection filtering, and it’s been quite wonderful, but I wouldn’t deploy it unless I had a strong need for that kind of functionality.
For a much more thorough, and also more advanced, variant of the above technique, check out “Predicates in Swift”, and feel free to send me your questions and comments via either Twitter or email.
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
The start of a new year is often an excellent time to revisit old assumptions, to clean up some long-standing technical debt, or to improve the overall suite of workflows and automation that’s used within a given project.
One thing that intersects more or less all of those areas is continuous integration, which — as a quick recap — is the act of continuously merging new changes into a project’s main branch (rather than using long-lived feature branches), and to always run a certain set of automated tests on those changes in order to ensure that the project continues to build and run as expected.
For long-time Swift by Sundell readers, it should come as no surprise that my favorite tool for continuous integration is Bitrise, which have also helped me keep Swift by Sundell up and running all throughout the past two weeks of holidays. So to round off their latest sponsorship, I thought I’d share a few tips on how I like to configure my various projects on Bitrise. Perhaps some of these tips will help you start 2021 by adding some really solid continuous integration to your project.
One of my favorite things about Bitrise is just how easy it is to configure. For many of my projects, their automatic setup process (which detects what kind of project you’re working on and configures a CI pipeline accordingly) turned out to be everything that I actually needed. Because although there’s a nearly endless amount of verification that we could perform as part of our CI process, sometimes all that we really need to do is to compile the project and run all of its tests.
So if you haven’t yet used Bitrise, my suggestion would be to simply try it by setting it up with that automatic configurator, to see how it could help you improve your project’s development workflow.
As a project grows, it might be a good idea to split its CI process up into multiple, separate workflows. Not only can that often improve the overall speed of getting a given Pull Request or commit green-lit by the CI system, it can also make debugging failures easier, as each workflow will end up having a much more narrow scope.
For example, let’s say that we’re working on an app that ships on both iOS and macOS. While we could build and test both of those two variants within the same Bitrise workflow, it’ll most likely be faster to use two separate ones. The good news is that Bitrise supports using the same project for multiple workflows, all that we have to do is to configure each one as a new app (but using the same repository). For example, Publish (which is the static site generator used to build all of Swift by Sundell) has two separate Bitrise workflows, one for macOS and one for Linux, that can both run in parallel.
There are also other ways that CI workflows could be split up as well. You could, for example, run all of a project’s UI tests within a separate workflow (since those tend to take the longest to run), or run the tests for any internal frameworks separately, or use separate workflows to test your app using multiple Xcode/SDK versions.
Of course, starting with just a single workflow is probably the best approach, as parallelization always tends to come with a certain amount of complexity. In this case, we might also need to be careful to keep each of our workflow configurations in sync, which could be done using a Git-hosted workflow YML file.
One of the most commonly faced challenges when adopting a more CI-centric workflow is flakiness, which is when a given set of tests and verifications seem to either succeed or fail at random.
Even though Bitrise ships with a fair amount of built-in functionality that helps reduce the potential for flakiness (such as automatic retries for UI tests), at the end of the day, the overall, long-term stability of our tests will always come down to how they were actually written.
When it comes to unit and UI tests in particular, one of my top tips on how to avoid flakiness is to always use generous timeouts when calling APIs like waitForExpectations
and waitForExistence
, and to avoid using blocking waiting techniques, such as sleep
.
If we use proper waiting APIs when writing asynchronous tests, the test runner will only pause for the whole timeout interval when a given condition or expectation wasn’t fulfilled — so using a larger timeout won’t slow down the overall execution of our tests, but will give our operations some extra time to finish when running in more resource-constrained environments, which CI servers typically are.
To learn more about testing timeouts and stability, check out my guest article “Making Xcode UI tests faster and more stable” on the Bitrise blog and “Unit testing asynchronous Swift code” right here on Swift by Sundell.
Regardless if you’re completely new to the concept of continuous integration, or if you already have an existing setup, now might be a great time to prepare each of your projects for the upcoming year by ensuring that you have some really solid verifications in place — and if you’re looking for a platform to implement your CI workflows on, then I really recommend checking out Bitrise. It’s free to get started, and it’s the CI tool that I personally use for all of my new projects.
One of SwiftUI’s most powerful traits is just how composable its views are by default. Because of that, breaking up a massive view can often simply be a matter of moving pieces of its body
code into new View
types, and then wiring up any bindings or other kinds of actions as needed.
However, there are also multiple other techniques that can be good to keep in mind when we’d like to break up a larger view in order to improve its maintainability and readability. To take a look at one such technique, let’s start with the following ProfileView
, which currently uses a single body
property to form its internal view hierarchy:
struct ProfileView: View {
var user: User
var body: some View {
ZStack {
LinearGradient(
gradient: user.profileGradient,
startPoint: .top,
endPoint: .bottom
)
.edgesIgnoringSafeArea(.all)
ScrollView {
VStack(spacing: 15) {
Text(user.name)
.font(.title)
Text(user.biography)
.multilineTextAlignment(.leading)
}
.padding()
.foregroundColor(.white)
}
}
}
}
While the above is not really a massive view in terms of line count, it could definitely be broken up in order to make it easier to iterate on and expand its functionality over time. One way to do that would be to use the aforementioned approach of splitting its body
property up into separate View
types — which in this case could be implemented as private nested types in order to give our code a neat degree of namespacing:
private extension ProfileView {
struct BackgroundView: View {
var gradient: Gradient
var body: some View {
LinearGradient(
gradient: gradient,
startPoint: .top,
endPoint: .bottom
)
.edgesIgnoringSafeArea(.all)
}
}
struct InfoView: View {
var user: User
var body: some View {
VStack(spacing: 15) {
Text(user.name)
.font(.title)
Text(user.biography)
.multilineTextAlignment(.leading)
}
.padding()
.foregroundColor(.white)
}
}
}
With the above in place, we’ll now be able to drastically simplify our main ProfileView
, since we can now implement its body
just by instantiating our BackgroundView
and InfoView
within their respective ZStack
and ScrollView
wrappers:
struct ProfileView: View {
var user: User
var body: some View {
ZStack {
BackgroundView(gradient: user.profileGradient)
ScrollView {
InfoView(user: user)
}
}
}
}
However, while the above approach is definitely a great option in many cases, it sort of feels a bit “heavy” in this kind of situation — given that we both need to define multiple types just to split our code up, and that we need to pass parts of our model data down into those nested views, which can quickly end up feeling like unnecessary busywork.
So let’s explore another approach, which involves creating additional body
-like computed properties for the different parts of our view’s internal hierarchy. Since we have a quite natural separation between our view’s foreground and background in this case, let’s name our properties just like that, and if we use the same some View
opaque return type as SwiftUI’s built-in body
property does, then we’ll end up with the following implementation:
private extension ProfileView {
var background: some View {
LinearGradient(
gradient: user.profileGradient,
startPoint: .top,
endPoint: .bottom
)
.edgesIgnoringSafeArea(.all)
}
var foreground: some View {
ScrollView {
VStack(spacing: 15) {
Text(user.name)
.font(.title)
Text(user.biography)
.multilineTextAlignment(.leading)
}
.padding()
.foregroundColor(.white)
}
}
}
With that change in place, all that we now have to do to implement our main view is to combine our background
and foreground
using a ZStack
— which gives us a body
implementation that essentially acts as a “handoff point” between our private implementation and SwiftUI as a framework:
struct ProfileView: View {
var user: User
var body: some View {
ZStack {
background
foreground
}
}
}
The beauty of the above approach is that it doesn’t require us to pass any additional data around, since our entire implementation can now actually remain within a single type — while still achieving a very nice separation between our view’s various parts.
Of course, it’s still a good idea to split certain views up into completely separate types (especially if we want to make some of those parts easier to reuse), but the above technique can be great to keep in mind when we want to break up a larger view while still letting its implementation continue to act as a single unit.
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
Get the most out of Apple’s new UI framework.
How we can create our own Domain Specific Language-like APIs in Swift.
Various tips on how to handle a major paradigm shift like the move to SwiftUI.
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
On this special episode of the show, John wraps up the 2020 season by revisiting some of the key themes and topics that were discussed on the show during the year.
The grand finale of the weekly article series that started Swift by Sundell.
Today I’m incredibly excited to launch the next major version of this site, which features a brand new start page, a new article publishing format, simplified navigation, and many other smaller tweaks and improvements.
On December 13th, I published my 200th and final weekly article. While I really enjoyed working on that article series for almost four years straight, going forward, I’m going to use a much more flexible publishing format. Rather than sticking to a strict weekly cadence, you’ll now frequently see new articles posted that’ll both include my signature deep dives into different Swift topics, but also shorter posts, links, news stories, and more. I’m also planning to start remastering older articles with new code samples and updates for later Swift versions and new system APIs.
To match this new publishing frequency, I’ve completely redesigned this site’s start page, which now features a feed-like design that lets you easily browse the latest content, and I’ve updated the article section page to include all of my articles — regardless of series, format, or length.
I think that this new format will be a huge upgrade for Swift by Sundell, and will let me publish new kinds of articles that didn’t really fit into the previous weekly series. Hopefully, that’ll continue to make this site an even richer resource for the Swift community, and for iOS and Mac developers of all skill levels.
So now is a great time to subscribe to this site through RSS and to follow @swiftbysundell on Twitter, to get notified when new articles are posted. I’ll take some time off during the holidays, but I’ll be back in January 2021 with lots of new articles, podcasts, and videos, and just like always, everything on this site will remain 100% free for everyone.
Thanks for reading, and hope you’ll enjoy this new version of Swift by Sundell!
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
Thanks a lot to Instabug for sponsoring Swift by Sundell last week, which really helped me cross the finishing line of 200 weeks of consecutively published articles.
Instabug is not only my favorite crash reporter for iOS apps (I’ve used it in several projects, and it has always been fantastic!), it also includes a wide range of other features that can help you keep track of your app’s overall quality.
For example, two of their recently added features are Application Performance Monitoring and the Apdex score. The former automatically provides you with concrete performance metrics around things like app launch times, UI responsiveness, and networking speed, while the latter gives you a single, unified score that you can use to measure and improve your app’s performance over time.
Using both of those two features, and everything else that Instabug has to offer, makes it so much easier to identify regressions, to fix bugs and errors, and to constantly iterate on an app’s quality and performance.
Check out Instabug today by going to try.instabug.com/swiftbysundell. When using that URL, you also directly help support Swift by Sundell as well.
If I had to name just one thing that I believe to be an overarching theme among the 200 weekly articles that I’ve now written about Swift development, I’d say that it’s the notion that making solid technical decisions is all about researching and then carefully comparing the tradeoffs of each potential solution.
When writing these articles, I’ve always aimed to be informative, rather than convincing. To share my learnings from adopting a given pattern, technology or framework, without necessarily telling you — the reader — that you should adopt them the exact same way that I did. Because at the end of the day, what solutions that will be the best fit for each given project will always depend on a mix between requirements, tradeoffs, and the personal opinions of the people involved in building it.
So, for my 200th and final* weekly article, I thought I’d sum up what I consider to be three of my major overall learnings after close to four years of continuous writing about Swift, its features, and many of the patterns that it’s commonly used with.
*Although this is indeed the final article in the weekly series that I’ve now been doing for 200 weeks, I’ll of course keep publishing many other kinds of articles on this website going forward. Check out this article for more info about my new publishing format.
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
When I first started programming in Swift, I strongly associated value types with data models. That is, I’d make my models, configuration types, options and other kinds of data representations all structs and enums, while keeping the rest of my code strictly object-oriented through classes and protocols.
However, these days, I tend to decide between value and reference types based purely on whether I really need a given type to hold any kind of shared mutable state. If I don’t (which tends to be the case), then I go for a value type, which not only lets me take advantage of all of the powerful language features that Swift’s value types offer — but doing so also encourages me to keep my various types as immutable as possible, even as I keep iterating on their logic.
For example, with my old way of thinking, if I were to write something like a simple Dataloader
that acts as a wrapper around Foundation’s URLSession
API — then I’d make that a class, mostly because it “felt right” for instances of such a type to be actual objects that are passed by reference, rather than values:
class DataLoader {
typealias Handler = (Result<Data, Error>) -> Void
private let urlSession: URLSession
init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func loadData(from url: URL,
then handler: @escaping Handler) {
let task = urlSession.dataTask(with: url) {
data, response, error in
...
}
task.resume()
}
}
However, there’s really nothing about the above type that warrants reference semantics. It doesn’t hold any form of state that needs to be shared across multiple call sites, and for this kind of low-level code, I’d actually prefer things to stay that way.
So, by turning my DataLoader
into a struct instead, I’ll both get a strong protection against accidental mutations (as a struct can only mutate itself within methods that are explicitly marked as mutating
), and I’ll also be able to remove my manually implemented initializer, since the compiler will now generate a memberwise one (like it does for all structs):
struct DataLoader {
typealias Handler = (Result<Data, Error>) -> Void
var urlSession = URLSession.shared
func loadData(from url: URL,
then handler: @escaping Handler) {
let task = urlSession.dataTask(with: url) {
data, response, error in
...
}
task.resume()
}
}
One tradeoff with the above solution, however, is that the underlying URLSession
instance that my DataLoader
is using is now exposed as part of its API. While that could be problematic in certain cases, I’d gladly accept that tradeoff in this case — especially since this particular DataLoader
type is simply a wrapper around URLSession
to begin with.
Since DataLoader
is now a value type, that also means that it can be mutated using value semantics, which in turn will make those mutations local by default. To illustrate, let’s say that we wanted to add support for attaching a series of HTTP headers to each request that a given DataLoader
will make. Because we’re now dealing with a value type, adding those headers either requires us to create a brand new instance, or to copy an existing one — which is great — since that prevents us from accidentally sharing those header values across the entire application:
struct DataLoader {
typealias Handler = (Result<Data, Error>) -> Void
var urlSession = URLSession.shared
var headers = [String : String]()
func loadData(from url: URL,
then handler: @escaping Handler) {
var request = URLRequest(url: url)
request.allHTTPHeaderFields = headers
let task = urlSession.dataTask(with: request) {
data, response, error in
...
}
task.resume()
}
}
So while I still use classes in many types of situations, especially when I really need to share a piece of state among multiple types, structs have gradually become my default tool for implementing new types — especially when working with very value-centric frameworks, such as SwiftUI.
One of my favorite aspects of Swift is its very strong, static type system. Increasingly, it feels like as long as my code compiles, it’ll actually do what I intended, which is quite amazing. But strong typing is not only great for compile time verification, it can also be a fantastic tool for communication.
Take Foundation’s URL
type as an example. At the end of the day, URLs are just strings, but there’s also a certain set of (quite complex) rules that they have to follow. So while every URL is a string, not every string is a URL. To me, that’s exactly the kind of data that warrants its own, dedicated type — even when we’re dealing with custom, app-specific data that could technically be represented using raw values, such as strings or integers.
For example, the following UserSession
type contains two properties that are currently stored as raw String
values:
struct UserSession {
var id: String
var accessToken: String
...
}
While both of the above two properties might be quite self-explanatory when viewed in the context of their enclosing UserSession
type, chances are quite high that we’d also like to pass those values around individually — at which point we’d now be dealing with completely context-less strings.
Compare that to the following UserSession
implementation, which now uses strong types that clearly explain what each piece of data does, and — as an added bonus — will also make any code using those values much more type safe:
struct UserSession {
var id: User.ID
var accessToken: AccessToken
...
}
At first, it might seem like implementing such wrapper types will be a lot of work — but both thanks to Swift’s aforementioned value type features, and to the many built-in protocols that the Swift standard library ships with — it could simply be done like this:
// Marking a type as Identifiable gives us a nested ID type
// based on its 'id' property (UUID in this case):
struct User: Codable, Identifiable {
let id: UUID
var name: String
...
}
// When using RawRepresentable, each value will be automatically
// encoded and decoded using its raw value (String in this case):
struct AccessToken: Codable, RawRepresentable {
var rawValue: String
}
With the above in place, we’ll now be able to freely pass each of our UserSession
values around individually without losing their contextual meaning. For example, here’s how we might extend our DataLoader
from before with an API for authorizing its requests using a given access token — and since that value now has a dedicated type, our resulting code becomes quite self-documenting:
extension DataLoader {
mutating func authorize(with token: AccessToken) {
headers["Authorization"] = "Bearer \(token.rawValue)"
}
}
These types of changes might seem quite minor in the grand scheme of things, but in my experience, they can really have a quite big impact on the overall semantics and readability of a given system. Because after all, code is only written once, but read multiple times, regardless if we work on our own or with other people, so optimizing for readability and ease of understanding is almost always worth the effort.
In the early days of Swift, the term “protocol-oriented programming” became incredibly popular, and many Swift developers (myself included) started to view protocols as the default way of defining all kinds of abstractions. However, as is the problem with most patterns that are called something like “X-oriented” or “X-driven”, it’s easy to get the impression that the goal is to follow the pattern itself as much as possible, which can most often be quite counter-productive.
So while I still consider protocols to be an incredibly powerful tool, the way that I personally use them has certainly changed over the past 200 weeks. Now, I look at protocols as just another tool in my toolbox, rather than as a design goal. For example, protocols are still a really great option in situations such as:
Collection
, Equatable
, and the Identifiable
and RawRepresentable
protocols that we used earlier.On the other hand, if I’m only looking to model a single requirement, then I’d often rather use a single function instead. When doing that, I don’t need to maintain two separate declarations, while still achieving a high degree of separation of concerns and testability.
For example, let’s now say that we’re looking to build a UserLoader
type that’ll use our DataLoader
from before as its underlying networking engine. Rather than creating a protocol that has a 1:1 relationship to that concrete DataLoader
type, let’s instead model the functionality that we’re looking to use — the loadData(from:then:)
method in this case — as a function that looks like this:
typealias DataLoading = (URL, @escaping DataLoader.Handler) -> Void
Then, rather than injecting a DataLoader
instance directly into our new UserLoader
, we’ll instead be able to inject any function that matches the above signature, and our user loader can then simply call that function when loading its data:
struct UserLoader {
typealias Handler = (Result<User, Error>) -> Void
var dataLoading: DataLoading
var decoder = JSONDecoder()
func loadUser(withID id: User.ID,
then handler: @escaping Handler) {
let url = resolveURL(forID: id)
dataLoading(url) { result in
do {
let data = try result.get()
let user = try decoder.decode(User.self, from: data)
handler(.success(user))
} catch {
handler(.failure(error))
}
}
}
}
Thanks to the fact that Swift supports first class functions, we can then pass a reference to the DataLoader
method that we’re looking to use simply by doing this:
let dataLoader = DataLoader()
let userLoader = UserLoader(dataLoading: dataLoader.loadData)
So what’s the benefit of doing the above, and — sticking to that overall theme again — what are the tradeoffs? Perhaps my favorite thing about the above pattern is that it makes testing so much easier.
Even though protocols can heavily improve the testability of a given type, they always require us to implement dedicated mock types for each piece of functionality that we’re looking to simulate within our tests. When using the functional approach, however, mocking a dependency becomes as simple as using a closure — for example like this:
class UserLoaderTests: XCTestCase {
func testSuccessfullyLoadingUser() throws {
let user = User(id: UUID(), name: "John Appleseed")
let data = try JSONEncoder().encode(user)
let loader = UserLoader { _, handler in
handler(.success(data))
}
var result: Result<User, Error>?
loader.loadUser(withID: user.id) { result = $0 }
XCTAssertEqual(try result?.get(), user)
}
}
To learn more about the above approach, check out “Mock-free unit tests in Swift”.
One of the major tradeoffs, though, is that the above technique doesn’t scale very well for types that have a larger number of dependencies, as we’d have to inject each of those dependencies as separate functions — which can quickly get quite messy. Also, when we need to call multiple methods on a single type, using a protocol (or injecting an instance of that type directly) is most likely going to be easier than passing multiple separate functions.
Even so, it’s a great technique to keep in mind, along with a few other protocol alternatives, rather than immediately reaching for a protocol in all kinds of situations.
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
When I started writing these weekly articles, Swift by Sundell didn’t yet exist, Swift 3 was the new cool version of the language, and I had no idea whether anyone would actually want to read what I was about to write.
Now, 200 weeks later, not only have I learned so much about Swift, what it means to be a writer, and all of the things that are involved in running a website like this — I’ve also had the true pleasure of discussing these topics with so many of you — both on Twitter, through email, and at various conferences and meetups around the world.
So whether this was the first weekly article that you’ve ever read, or your 200th one, thank you. All of your combined support, feedback and encouragement is truly what has kept my going during these 200 weeks of continuous writing. Because as long as the Swift community remains interested in what I have to write, I’ll do my very best to keep writing.
Thanks for reading! 🚀
Support Swift by Sundell by checking out this sponsor:
Genius Scan SDK: Add a powerful document scanner to any iOS app. Genius Scan’s SDK features advanced image processing that’s the result of over 10 years of research and development and can be fully customized and integrated into your app with just one line of code. Mention Swift by Sundell when requesting a free demo to get a 20% discount on your license for a whole year.
Thanks a lot to Architecting SwiftUI apps with MVC and MVVM for sponsoring Swift by Sundell.
A look at what it means for SwiftUI to be a value-driven UI framework, and how we might need to break certain assumptions when adopting SwiftUI within our projects.
Lightweight state observations.
Chris Eidhof returns to the show to go on a deep dive into the SwiftUI layout system. What are the different phases involved in determining a given view’s layout, how do concepts like layout priorities and flexibility work, and what makes SwiftUI different from UIKit and AppKit in terms of layout?
A look at the various sorting APIs that the Swift standard library offers, and how we could augment those APIs in order to make more advanced sorting tasks easier to perform.
A neat technique for values that can’t be created with a single line of code.
Thanks a lot to Instabug for sponsoring Swift by Sundell.
How custom Combine operators and convenience APIs can be implemented using extensions, and how doing so can let us eliminate common sources of boilerplate when implementing things like networking and data validation.