Categories
iOS Swift SwiftUI

Animating with keyframe animator in SwiftUI

In the previous blog post, we discussed how to use the new phase animator in SwiftUI. Today, we’ll dive into KeyframeAnimator which gives us much more control of the animation. The phase animator worked by specifying different states (e.g. scale, offset etc), and the phase animator animated from one state to another. KeyframeAnimator on the other hand, allows specifying different animation tracks, where each of the track animates a property conforming to the Animatable protocol independently. The value of the property is applied to the view with the content closure of the animator. Each of the animated property is stored by a struct passed into the animator’s initialValues argument. Therefore, the very first thing to start with keyframe animations is creating a new struct which holds animated values and defines initial values. Let’s just animate rotation and scale in our example. Therefore, we can create a struct BannerAnimationValues with rotation and scale properties.

struct BannerAnimationValues {
var rotation: Angle = .zero
var scale = 1.0
}

Secondly, we use keyframeAnimator(initialValue:trigger:content:keyframes:) view modifier on the view. The view modifier defines how these values are applied to the view and when and how exactly the values are animated and interpolated.

BannerView()
.keyframeAnimator(initialValue: BannerAnimationValues(),
trigger: counter,
content: { view, value in
view
.scaleEffect(value.scale)
.rotationEffect(value.rotation)
},
keyframes: { value in
KeyframeTrack(\.scale) {
LinearKeyframe(1.0, duration: 0.5)
SpringKeyframe(2.0, duration: 0.4, spring: .snappy)
CubicKeyframe(1.0, duration: 0.6)
}
KeyframeTrack(\.rotation) {
SpringKeyframe(.degrees(-35), duration: 0.4, spring: .smooth)
SpringKeyframe(.degrees(35), duration: 0.8, spring: .smooth)
LinearKeyframe(.degrees(0), duration: 0.3)
}
})

The first argument is initialValue and we pass our struct with initial values to it. SwiftUI will then modify values of the struct based on the keyframes closure’s KeyframeTracks and keeps calling the content closure to apply new values. In the example above, we can see that the content closure applies scale and rotation effect based on the current animation values. The keyframes closure contains two tracks, since we animate two properties. The first keyframe track defines how the scale property is changed of the BannerAnimationValues struct. Here we keep the scale at 1.0 for 0.5 seconds, then animate the scale value to 2.0 with spring timing function, and finally, setting the scale back to 1.0 using cubic curve timing function. SwiftUI will handle the changes of velocities between different timing functions to keep the animation smooth. The rotation is similar, we use a spring function for setting the rotation to -35 degrees, then to 35 degrees and finally a linear timing for animating the rotation back to 0 degrees.

Some things to keep in mind with keyframe animations is that these animations should not be interrupted or changed in the middle of the animation. This is because we set exact values of the animation, and therefore it can’t retarget to a new set of values and interpolate to new values nicely. Also, the view is updated on every frame, therefore we need to keep in mind performance and not doing any expensive work while the animation is running.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Animating with PhaseAnimator in SwiftUI

WWDC’23 introduced us a new view modifiers for animating views in SwiftUI. In this blog post, we are going to have a look at phase animation view modifiers. There are two view modifiers: phaseAnimator(_:content:animation:) and phaseAnimator(_:trigger:content:animation:). The first one creates an animation which runs continuously by cycling through all the animation steps. The latter is triggered once when an observed value has changed.

A multistep or multiphase animation is easiest to model using an enum.

enum BannerAnimationPhase: CaseIterable {
case initial, middle, final
var animation: Animation {
switch self {
case .initial: .easeIn
case .middle: .linear
case .final: .easeOut
}
}
var gradientStartPoint: UnitPoint {
switch self {
case .initial: .bottomLeading
case .middle: .leading
case .final: .topLeading
}
}
var rotation: Angle {
switch self {
case .initial: .degrees(-15)
case .middle: .degrees(15)
case .final: .zero
}
}
var scale: Double {
switch self {
case .initial: 1.5
case .middle: 1.7
case .final: 1.0
}
}
}

Here we have an enum with 3 animation phases. Each of the phase slightly modifies 3 values: scale, rotation and gradient start point. These properties are accessed from the phaseAnimator’s view builder, where we modify the original view based on the phase. In addition, we also configured different animations based on the current phase.

struct ContentView: View {
var body: some View {
BannerView()
.phaseAnimator(BannerAnimationPhase.allCases) { content, phase in
content
.background(LinearGradient(colors: [.orange, .yellow],
startPoint: phase.gradientStartPoint,
endPoint: .trailing))
.cornerRadius(8.0)
.scaleEffect(phase.scale)
.rotationEffect(phase.rotation)
} animation: { phase in
phase.animation
}
}
}

When we only want to have 2 phases for our animation, then we can use boolean values for defining the animation. Below is an example of an animation which is triggered when a value changes and the animation just scales up the banner view and then animates to the initial state – false.

BannerView()
.phaseAnimator([false, true],
trigger: counter) { content, isEnabled in
content
.scaleEffect(isEnabled ? 2.0 : 1.0)
} animation: { isEnabled in
isEnabled ? .bouncy : .easeOut(duration: 0.7)
}
Button("Trigger", action: { counter += 1 })

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Examples of animating SF symbols in SwiftUI

WWDC’23 introduced SF symbol animations. There are 8 different animation presets: appear, disappear, bounce, scale, pulse, variable color, and replace. Each of these presets have a dedicated use-cases. Appear animation is used when a symbol is shown in the UI for the first time. Disappear when a symbol is removed from the UI. Bounce is suitable for communicating to a user that an action was triggered, or it was completed successfully. Scale animation is suitable for highlighting elements in the UI, like when hovering on the element. It could also be used to let the user know that an action has taken a place (think about a button which has pressed down state). Pulse animation is an excellent way to show that some action is ongoing. While recording a video, the record button’s symbol pulses. Variable color animation communicates a state which changes over time (Wi-Fi signal strength). Replace animation is useful for communicating that the function of a symbol has changed. Think about a play button changing to a pause button.

Symbol animations are applied with symbolEffect(_:options:value:) for discrete effects, symbolEffect(_:options:isActive:) for indefinite effects and contentTransition(_:) and transition() view modifiers with a new symbolEffect. Discrete effects are on-off effects, whereas indefinite effects change the symbol indefinitely and need to be explicitly removed. Bounce, pulse, and variable color support discrete effects and pulse, variable color, scale, appear, disappear support indefinite effects. Appear and disappear also support transition effects, and replace supports a content transition effect. Therefore, we need to keep in mind this information when picking the view modifier.

Next, let’s look at some of the examples and how to apply these view modifiers.

Appear and Disappear

struct AppearDisappearView: View {
@State private var isDrizzleHidden = true
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "cloud.fill")
Image(systemName: "cloud.drizzle.fill")
.symbolEffect(.disappear, isActive: isDrizzleHidden)
Image(systemName: "cloud.heavyrain.fill")
}
.imageScale(.large)
HStack {
Image(systemName: "cloud.fill")
if !isDrizzleHidden {
Image(systemName: "cloud.drizzle.fill")
.transition(.symbolEffect(.automatic))
}
Image(systemName: "cloud.heavyrain.fill")
}
.imageScale(.large)
Button("Appear/Disapper") {
isDrizzleHidden.toggle()
}
}
}
}

Bounce

struct BounceView: View {
@State private var value = 0
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "snowflake")
.symbolEffect(.bounce, value: value)
Image(systemName: "snowflake")
.symbolEffect(.bounce, options: .speed(0.1), value: value)
Image(systemName: "snowflake")
.symbolEffect(.bounce, options: .repeat(2), value: value)
}
.imageScale(.large)
Button("Bounce") { value += 1 }
}
}
}

Scale

struct ScaleView: View {
@State private var isActive = false
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "drop.fill")
.symbolEffect(.scale.up, isActive: isActive)
Image(systemName: "drop.fill")
.symbolEffect(.scale.down, options: .speed(0.1), isActive: isActive)
Image(systemName: "drop.fill")
.symbolEffect(.scale.down, options: .speed(5), isActive: isActive)
}
.imageScale(.large)
Button("Scale") { isActive.toggle() }
}
}
}
view raw ScaleView.swift hosted with ❤ by GitHub

Pulse

struct PulseView: View {
@State private var isActive = false
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "moonphase.first.quarter")
.symbolEffect(.pulse, isActive: isActive)
Image(systemName: "moonphase.first.quarter")
.symbolEffect(.pulse, options: .speed(0.1), isActive: isActive)
Image(systemName: "moonphase.first.quarter")
.symbolEffect(.pulse, options: .speed(5), isActive: isActive)
}
.imageScale(.large)
Button("Pulse") { isActive.toggle() }
}
}
}
view raw PulseView.swift hosted with ❤ by GitHub

Variable Color

struct VariableColorView: View {
@State private var isActive = false
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "rainbow")
.symbolEffect(.variableColor, options: .speed(0.1), isActive: isActive)
Image(systemName: "rainbow")
.symbolEffect(.variableColor.iterative, options: .speed(0.1), isActive: isActive)
Image(systemName: "rainbow")
.symbolEffect(.variableColor.iterative.reversing, options: .speed(0.1), isActive: isActive)
}
.symbolRenderingMode(.multicolor)
.imageScale(.large)
Button("Variable Color") { isActive.toggle() }
}
}
}

Replace

struct ReplaceView: View {
@State private var isActive = false
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: isActive ? "pause.circle.fill" : "play.circle.fill")
.contentTransition(.symbolEffect(.replace))
Image(systemName: isActive ? "pause.circle.fill" : "play.circle.fill")
.contentTransition(.symbolEffect(.replace.offUp))
Image(systemName: isActive ? "pause.circle.fill" : "play.circle.fill")
.contentTransition(.symbolEffect(.replace.upUp))
}
.imageScale(.large)
Button("Replace") { isActive.toggle() }
}
}
}

Example Project

SymbolsAnimationExample (Xcode 15 beta 6)

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Metal Swift SwiftUI

Applying metal shader to text in SwiftUI

WWDC ’23 introduced view modifiers for applying shaders to SwiftUI views. The new view modifiers are called colorEffect, distortionEffect, and layerEffect. In addition, the Shader also conforms to the ShapeStyle protocol which means that we can directly pass the shader to the foregroundStyle view modifier and then SwiftUI uses the shader to compute pixel color values for the view. In this post, we are going to have a look at what it takes to apply a shader to text. What we will not cover is how to use each of the new view modifiers, what were mentioned before.

Metal shaders are defined in .metal files. Therefore, the first step is to add a new .metal file to the project. As an example, we’ll create a shader which applies a stripe effect to the text. The .metal file’s implementation is shown below:

#include <metal_stdlib>
using namespace metal;

[[ stitchable ]] half4 stripes(float2 position, float stripeWidth) {
    bool isAlternativeColor = uint(position.x / stripeWidth) & 1;
    return isAlternativeColor ? half4(0, 0, 1, 1) : half4(1, 0, 0, 1);
}

The shader function returns color of the pixel for the current pixel position. The stripeWidth argument is going to be handled by us for configuring the width of the stripe. When the shader function is defined, then the next step is using the new ShaderLibrary type for accessing the shader function. Since the ShaderLibrary type implements @dynamicMemberLookup then the metal shader function in the .metal file can be accessed directly though the name stripes.

var shader: Shader {
  ShaderLibrary.stripes(.float(15))
}

The last step is applying the shader to the Text value using the foregroundStyle view modifier.

Text("Hello, world!")
  .font(.largeTitle)
  .foregroundStyle(shader)

The final result can be seen here:

Sample code: SwiftUIMetalTextShaderExample (Xcode 15 beta 5)

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI Xcode

Discovering #Preview macro in Swift

Previews are snippets of codes for creating a live preview of a view what we can see in Xcode. This enables to quickly iterate your views because we can see right away how the view looks after each of the code change. Previews are compiled alongside with the app, and therefore we can access all the other code and resources like images. If we want to use preview only resource, we can use Xcode’s development assets feature for making the resource only part of the app when rendering previews. Now when we know what previews are in general, let’s move on to creating a preview.

So far we have created a new struct which conforms to a PreviewProvider protocol if we wanted Xcode to render the view for us. Xcode 15 with Swift 5.9’s macro support introduces a #Preview macro, which replaces the old way of creating live previews. The benefit of the new macro is having to write less code to get going with live previews. Let’s compare the both approaches and have a look at an accessory view’s preview.

// Before
struct RatingsView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ForEach(0…5, id: \.self) { value in
RatingsView(value: .constant(value))
}
}
.previewLayout(.sizeThatFits)
}
}
// After
#Preview(traits: .sizeThatFitsLayout) {
VStack {
ForEach(0…5, id: \.self) { value in
RatingsView(value: .constant(value))
}
}
}
view raw Preview.swift hosted with ❤ by GitHub

In the example above, we wanted to apply a trait since this view is a tiny accessory view, and therefore we would like to see it rendered as small as possible. A list of traits what we can use are listed here:

extension PreviewTrait where T == Preview.ViewTraits {
/// Preview with `.device` layout (the default).
public static var defaultLayout: PreviewTrait<Preview.ViewTraits> { get }
public static var sizeThatFitsLayout: PreviewTrait<Preview.ViewTraits> { get }
public static func fixedLayout(width: CGFloat, height: CGFloat) -> PreviewTrait<T>
public static var portrait: PreviewTrait<Preview.ViewTraits> { get }
public static var landscapeLeft: PreviewTrait<Preview.ViewTraits> { get }
public static var landscapeRight: PreviewTrait<Preview.ViewTraits> { get }
public static var portraitUpsideDown: PreviewTrait<Preview.ViewTraits> { get }
}

When working with full screen views, the preview macro can be as simple as this:

#Preview {
OnboardingView()
}

In addition to traits, we can also give a name to the preview which is displayed in the Xcode’s preview canvas. This can be useful if we create multiple previews for the same view.

A view with multiple previews with different names.

Another thing to note is that the preview canvas in Xcode also lists a pin button next to the name of the preview. Pinning previews is useful if we want to navigate to another file to make some changes and keeping the preview running. Maybe we want to change some constants which affects the layout of the view, but these constants are defined somewhere else. Then it is useful to keep the preview running and seeing how changing a constant in another view is reflected by the view using it.

There is another tip to keep in mind. We can run previews on physical devices as well. We just need to pick the physical device instead of a simulator from the device picker.

Preview canvas, which lists a physical device for preview.

Finally, let’s not forget about the great way to see all the colour scheme and dynamic type variants at the same time. There is a separate variants button for that next to the live preview and selectable preview buttons.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

If-else and switch statements as expressions in Swift

Swift 5.9 arrived with Xcode 15 and one of the new additions to the language is if-else and switch statements as expressions. SE-0380 proposed the new language feature, and we can find in depth information about it there. Let’s go through some of the examples where it comes handy.

Firstly, using if-else or switch for returning values in functions, closures or properties. I write quite a lot of code which just turns an enum value into some other type. Now we can omit the return statement which, in my mind, makes the code much more readable. Here we have an example of returning a different background colour based on the current coffee brewing method.

enum BrewMethod {
case espresso, frenchPress, chemex
}
// Before
func backgroundColor(for method: BrewMethod) -> UIColor {
switch method {
case .espresso: return .systemBrown
case .frenchPress: return .systemCyan
case .chemex: return .systemMint
}
}
// After
func backgroundColor(for method: BrewMethod) -> UIColor {
switch method {
case .espresso: .systemBrown
case .frenchPress: .systemCyan
case .chemex: .systemMint
}
}

Secondly, we can use this new feature for initializing variables with more complex logic without needing to create an immediately executing closure. Personally, I tend to do this quite a lot in my projects. Here is an example of assigning a value to the title variable based on the current brewing method. Note that we can omit return and the closure declaration now.

let method: BrewMethod = .espresso
// Before
let title: String = {
switch method {
case .espresso: return "Espresso"
case .frenchPress: return "French Press"
case .chemex: return "Chemex"
}
}()
// After
let title2: String =
switch method {
case .espresso: "Espresso"
case .frenchPress: "French Press"
case .chemex: "Chemex"
}

Here is another example where we use if-else statement for the same purpose. Especially handy to contain some more complex logic.

let method: BrewMethod = …
let hasSelection = …
let bannerText =
if !hasSelection {
"Please select a brewing method"
}
else if method == .chemex {
"Yada yada"
}
else {
"Other yada yada"
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Collapsible wheel picker for forms in SwiftUI

While working on an app, I needed a way for showing a picker with a wheel style in a form. If we add a wheel picker to a form, it is always shown. Better would be to show a row which describes the current selection and if I tap on the row, it reveals the picker with wheel style. This is exactly what we are going to build in this blog post. Moreover, we will first create a general purpose collapsible view which can be used in other use cases as well. In the end, we will have a collapsible wheel picker which behaves like this:

We start with creating a general purpose collapsible view which has two closures: one for title view and the other for collapsible view. The view implementation is fairly simple. A button controls the collapsed state and then optionally we add the collapsible secondary view. We are using view builders here since we want to construct views with closures which support creating multiple child views. The view is optimized for forms, and therefore we use the Group view which automatically adds a divider between the button and the secondary view. Button uses a plain style which removes the tint colour of it in forms.

struct CollapsibleView<Label, Content>: View where Label: View, Content: View {
@State private var isSecondaryViewVisible = false
@ViewBuilder let label: () -> Label
@ViewBuilder let content: () -> Content
var body: some View {
Group {
Button(action: { isSecondaryViewVisible.toggle() }, label: label)
.buttonStyle(.plain)
if isSecondaryViewVisible {
content()
}
}
}
}

Now we can use this view to create a new CollapsibleWheelPicker. This view just adds a picker with wheel style as the secondary view.

struct CollapsibleWheelPicker<SelectionValue, Content, Label>: View where SelectionValue: Hashable, Content: View, Label: View {
@Binding var selection: SelectionValue
@ViewBuilder let content: () -> Content
@ViewBuilder let label: () -> Label
var body: some View {
CollapsibleView(label: label) {
Picker(selection: $selection, content: content) {
EmptyView()
}
.pickerStyle(.wheel)
}
}
}

A full example looks like this:

struct ContentView: View {
@State private var selection = 1
let items = [0, 1, 2, 3, 4, 5, 6, 7, 8]
var body: some View {
NavigationStack {
Form {
CollapsibleWheelPicker(selection: $selection) {
ForEach(items, id: \.self) { item in
Text("\(item)")
}
} label: {
Text("Cups of Water")
Spacer()
Text("\(selection)")
}
}
}
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

@Observable macro in SwiftUI

@Observable macro was announced in WWDC23 with the aim of simplifying observation related code and improving the performance of the app. The performance boost comes from the fact that SwiftUI starts tracking which properties are used in SwiftUI view’s body and only then view rendering is triggered when tracked properties change. If any of the other properties change in the same model object, no new re-rendering happens – excellent.

It has always felt a bit odd to use the @Published property wrapper for each of the property, which should trigger view updates. I feel overwhelmingly happy that I can clean up all of that noise from the code now. Let’s compare two model objects: one which uses ObservableObject and the other one using the new @Observable macro. Note that we need to import the new Observation framework for being able to use @Observable.

struct ContentView: View {
@StateObject private var viewModel = ContentViewModel()
var body: some View {
VStack {
Text(viewModel.title)
TextField("Username", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
}
.padding()
}
}
final class ContentViewModel: ObservableObject {
@Published var username: String = ""
var title: String {
if username.isEmpty {
return "Hello, world!"
}
else {
return "Hello \(username)!"
}
}
}
view raw Old.swift hosted with ❤ by GitHub
A simple view with a view model conforming to ObservableObject.
import Observation
import SwiftUI
struct ContentView2: View {
@Bindable private var viewModel = Content2ViewModel()
var body: some View {
VStack {
Text(viewModel.title)
TextField("Username", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
}
.padding()
}
}
@Observable class Content2ViewModel {
var username: String = ""
var title: String {
if username.isEmpty {
return "Hello, world!"
}
else {
return "Hello \(username)!"
}
}
}
view raw New.swift hosted with ❤ by GitHub
A simple view with a view model using the new @Observable macro.

When we compare these two views then the main differences are that we can drop using @Published property wrappers, instead of conforming to ObservableObject protocol we instead add the @Observable macro in front of the view model’s definition. Instead of @StateObject we can just use @State since we still want the view to own and keep one instance of the view model.

Here we have a tiny example, but if we are dealing with larger models then having a chance to skip all the @Published property wrappers is a bliss.

Note: In Xcode 15 beta 1 the Swift compiler crashes if the Content2ViewModel is part of the ContentView2 extension.

Time to get back to consuming WWDC23 information, thank you for reading!

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Ratings view in SwiftUI

I am currently building a new app where I needed to show ratings for items. Ratings are from 0 to 5 with 0.5 step. In this blog post, we’ll implement a SwiftUI view which uses SF symbols to display ratings. Since we need to make sure it works properly with each of the step, we are going to leverage the usefulness of SwiftUI previews and render each state one by one vertically. Swift has a function called stride what we can use to easily create an array of double values with a step of 0.5 and feed it in to ForEach.

struct RatingsView_Previews: PreviewProvider {
static var previews: some View {
VStack {
let steps = Array(stride(from: 0.0, through: 5.0, by: 0.5))
ForEach(steps, id: \.self) { value in
RatingsView(value: value)
}
}
.previewLayout(.sizeThatFits)
}
}
view raw Preview.swift hosted with ❤ by GitHub

The view needs to render 5 stars next to each other and figure out which stars are filled, half filled or empty. Rendering 5 images is straight-forward in SwiftUI. We can use HStack and ForEach with an integer range from 0 to 5. Since we use SF symbols, then the colour of the filled star can be applied with the foregroundColor view modifier.

/// A view displaying a star rating with a step of 0.5.
struct RatingsView: View {
/// A value in range of 0.0 to 5.0.
let value: Double
var body: some View {
HStack(spacing: 0) {
ForEach(0..<5) { index in
Image(systemName: imageName(for: index, value: value))
}
}
.foregroundColor(.yellow)
}
func imageName(for starIndex: Int, value: Double) -> String {
// redacted
}
}

The most complicated part is implementing a function what returns the name of the SF symbol for the current star index and the double value passed into the view. When I thought about it then I ended up with two solutions: one with if clauses and the other one with switch statement.

func imageName(for starIndex: Int, value: Double) -> String {
// Version A
if value >= Double(starIndex + 1) {
return "star.fill"
}
else if value >= Double(starIndex) + 0.5 {
return "star.leadinghalf.filled"
}
else {
return "star"
}
// Version B
switch value – Double(starIndex) {
case ..<0.5: return "star"
case 0.5..<1.0: return "star.leadinghalf.filled"
default: return "star.fill"
}
}

The solution A is checking if the value is larger than the star index + 1 which means that in case of value 1.0 the zero indexed star image is rendered as filled star. Half-filled cases are handled by checking if the zero indexed star index + 0.5 is less than the current value. The solution B uses open-ended ranges and a switch statement. When we subtract the zero indexed star index from the current value, then if it is less than 0.5 then the star should be not filled, if 0.5 to 1.0 then half-filled and in other cases filled. When comparing these implementations, then the B is more concise, but on the other hand A is more readable. Years ago I read a book “Masters of Doom” and John Romero’s programming principles. Don’t know why, but his programming principle “simplify” always pops on my mind when I need to decide on multiple implementations. Like here, only because of this, I am going for solution A.

SwiftUI preview showing each state of the ratings view.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

Implicit self for weak self captures

Since this week has kept me extremely busy with packing our things, selling furniture, and wrapping up any loose ends before relocating back to Estonia from Japan after 4.5 years, I am going to use this week’s blog post for highlighting a new Swift 5.8 language feature. I welcome this change since how hard I try there still will be cases where I need to capture self in closures.

The language feature we are talking about is of course SE-0365: Allow implicit self for weak self captures, after self is unwrapped. Which means that if we capture self weakly in a closure, use guard let self then there is no need to write self. inside the closure any more.

Let’s take a look at a concrete example from my SignalPath app. It is a tiny change, but I feel like it makes a lot of sense. Especially because I already have explicitly handled weak self with guard let.

// Old
iqDataSource.didChangeSampleCount
.sink { [weak self] sampleCount in
guard let self else { return }
let seriesDelta = self.layout.adjust(to: sampleCount)
guard seriesDelta < 0 else { return }
self.resetTiles(priority: .immediately)
}
.store(in: &cancellables)
// New
iqDataSource.didChangeSampleCount
.sink { [weak self] sampleCount in
guard let self else { return }
let seriesDelta = layout.adjust(to: sampleCount)
guard seriesDelta < 0 else { return }
resetTiles(priority: .immediately)
}
.store(in: &cancellables)
view raw Snippet.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.