SwiftUI: How to Add an Inverted Mask

SwiftUI's mask(alignment:_:) method allows you to mask the view using the alpha channel of a given view. However, sometime you want to add an inverted mask. There are a few ways to achieve this depending on how your mask view is constructed.

Example of inverted mask

Example of inverted mask effect.

Use eoFill

If your mask is a shape, you can create another shape by adding a Rectangle path and applying a FillStyle(eoFill: true):

struct CircleInRectangle: Shape {
    let padding: CGFloat
    func path(in rect: CGRect) -> Path {
        var shape = Rectangle().path(in: rect)
        let paddedRect = rect.insetBy(dx: padding, dy: padding)
        shape.addPath(Circle().path(in: paddedRect))
        return shape
    }
}

struct ContentView: View {
    var body: some View {
        Image(uiImage: #imageLiteral(resourceName: "img.jpeg"))
            .resizable()
            .scaledToFill()
            .clipped()
            .frame(width: 400, height: 225)
            .background(.white)
            .mask(
                CircleInRectangle(padding: 24)
                    .fill(style: .init(eoFill: true))
            )
        )
    }
}

Use .luminanceToAlpha()

SwiftUI's .luminanceToAlpha() modifier creates a mask by making dark colors transparent and bright colors opaque.

struct ContentView: View {
    var body: some View {
        Image(uiImage: #imageLiteral(resourceName: "pattern.jpeg"))
            .resizable()
            .scaledToFill()
            .clipped()
            .frame(width: 400, height: 225)
            .background(.white)
            .mask(
                Circle()
                    .padding()
                    .foregroundColor(.black)
                    .background(.white)
                    .compositingGroup()
                    .luminanceToAlpha()
            )
    }
}

Use .blendMode(.destinationOut)

SwiftUI's .blendMode(.destinationOut) modifier allows you to erase any of the background that is covered by opaque source pixels.

struct ContentView: View {
    var body: some View {
        Image(uiImage: #imageLiteral(resourceName: "pattern.jpeg"))
            .resizable()
            .scaledToFill()
            .clipped()
            .frame(width: 400, height: 225)
            .background(.white)
            .mask(
                Color.black
                    .overlay(
                        Circle()
                            .padding()
                            .blendMode(.destinationOut)
                    )
                    .compositingGroup()
            )
    }
}

Swift: The Direction of Date.distance(to:)

The official document of Date.distance(to:) says it:

Returns the distance from this date to another date, specified as a time interval.

But it has never been clear to me positive distance means whether the left side date is earlier or later.

So this tip is dedicated to myself: Date.distance(to:) is the opposite of minus. The distance positive when the left side date is earlier.

$ swift
  1> let now = Date()
now: Date = {}
  2> let oneHourLater = now.advanced(by: 3600)
oneHourLater: Date = {}
  3> let distance = now.distance(to: oneHourLater)
distance: TimeInterval = 3600

Finally scheduled my long overdue car service appointment, and just realized that I can carry my bike in my car, drop off my car, and bike back home.

Why hasn't this idea occurred to me before? What has US done to me?

I need to change.

DA 5Star v2021.3

DA 5Star v2021.3 for both iOS and macOS is now available:

  • Added a button in Diagnostics to refresh app icons / podcast artwork images.
  • It now clears the local artwork images when an app is deleted.

Happy cheering yourself up with the 5-star reviews ^_^

DA 5Star for macOS Is Now Powered by Catalyst

Here is what’s new in the latest DA 5Star macOS version updates:

  • The macOS version is now powered by Catalyst.
  • Added Big Sur widgets support.
  • Added iCloud sync to the macOS version.

Happy cheering yourself up with the 5-star reviews ^_^

What's New in dA 5Star v2020.4

dA 5Star v2020.4 features

dA 5Star v2020.4

DA 5Star v2020.4 is now available. What's new:

  • Added the ability to filter reviews by apps and podcasts in widget settings.
  • Added smaller fonts options in widget settings. Also slightly improved the layout in the small widget.
  • Fixed a bug where you can not add apps or podcasts not available in the US store.
  • Improved iCloud sync after reviews are re-downloaded.

Happy cheering yourself up with the 5-star reviews ^_^

What's New in dA 5Star v2020.3

Thank you all for the great feedbacks for DA 5Star. I just released v2020.3 including the following most requested features:

  • Added the ability to filter reviews by apps and podcasts in widget settings.
  • Added smaller fonts options in widget settings. Also slightly improved the layout in the small widget.
  • Fixed a bug where you can not add apps or podcasts not available in the US store.
  • Improved iCloud sync after reviews are re-downloaded.

I am also working hard to bring the rest of your feedbacks to you in the future releases. Stay tuned.

Happy cheering yourself up with the 5-star reviews ^_^

DA 5Star for iOS and iPadOS is Launched

DA 5Star for iOS and iPadOS is Launched

DA 5Star is 33% off until 2020-10-21.

To celebrate the launch, DA 5Star is 33% off until 2020-10-21.

App Store link: https://apps.apple.com/app/da-5star/id1473088658. If you already purchased the macOS version, you can get the iOS and iPadOS version for free.

What is DA 5Star?

DA 5Star is an iOS and iPadOS 14 home screen widget app that displays a random 5-star review of your apps and podcasts, and refreshes after however long you choose. It also has a macOS version that displays 5-star reviews as wallpapers.

Features

  • Widgets: This is the sole purpose of DA 5Star. It displays a random 5-star review of your apps and podcasts in the widget, and refreshes after however long you choose.
  • Apps and podcasts: It supports 5-star reviews of apps and podcasts on Apple platforms.
  • Custom themes: You can customize the widget themes so they fit on your home screen.
  • Review list: You can view the list of 5-star reviews of your apps and podcasts.
  • iCloud sync: Your added apps and podcasts are synced with iCloud (macOS version is not yet supported).
  • Custom app icons: It provides 5 alternative app icons.
  • Dark mode: Yes.

The story behind DA 5Star

On July 6, 2019, I was listening to EP 56: The happiest Customer of the Independence podcast. Curtis pitched this app idea:

A mac app, that will sit in your menu bar. You can feed it your iTunes connect information. And it will go to your ratings, pull random 5-star rating every however many hours. And have like kind that those motivational quote things, but have in your wallpaper.

After hearing it, I just couldn't stop thinking about it. As developers, we all experienced those 1-star ratings that ruin our day or week. We humans are bad at ignoring them. We fail to remind ourselves that there are more important 5-star customer ratings out there. So I immediately created the Xcode project and started implemented. On the next day I sent the hosts @eataduckimust, @parrots, @jellybeansoup a demo. I got great feedbacks and launched the first version in two weeks. I later then released 5 updates to improve it.

I didn't expect other huge updates to the app. Until one day after 2020's WWDC, I realized that this app is now viable after the introduction of iOS 14 home screen widgets. So the iOS version was born.

NOTE: The initial macOS version was a $1.99 (USD) paid upfront app. I decided to raise the price to $2.99 (USD) with the launch of the iOS and iPadOS version (it's a universal purchase). But I'm keeping the original price for another three days, so it's effectively a 33% off launch sale.

Again, you can purchase DA 5Star here: https://apps.apple.com/app/da-5star/id1473088658.

SwiftUI: Set a Max Width on Spacer()

SwiftUI's Spacer() has a minLength parameter, but some times we also want a maxLength. There isn't one, but you can use .frame(maxWidth:) to achieve the same effect:

Spacer()
    .frame(maxWidth: 16)

SwiftUI: How To Programmatically Make a TextField First Responder

It's a common scenario to make a text field first responder when it first appears. A typical example is the login screen. As soon as it appears, you want the account field to be in focus and the keyboard to appear. Then the user can start typing right way.

SwiftUI in iOS 15 introduced a new property wrapper called @FocusState. It allows you to control which input has focus. It can be bound to a Bool or an enum:

struct ContentView: View {
    @FocusState private var focused: Bool
    @State private var name = "Mango Umbrella"

    var body: some View {
        VStack {
            TextField("Name", text: self.$name)
                .focused(self.$focused)
            Button("Focus on name") {
                self.focused = true
            }
        }
    }

But how can you make the text field focus as soon as the view appears? You can't assign an initial value to the property. Another thought is to change its value in .onAppear like this:

struct ContentView: View {
    @FocusState private var focused: Bool

    var body: some View {
        VStack {
            // ...
        }
        .onAppear {
            self.focused = true
        }
    }
}

This won't work unfortunately. It appears that @FocusState only works when the view has been rendered after some time. A not-so-good workaround is to add a delay:

struct ContentView: View {
    @FocusState private var focused: Bool

    var body: some View {
        VStack {
            // ...
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                self.focused = true
            }
        }
    }
}

I don't recommend this approach though. The delay is arbitrary, and slower devices might need a longer delay. It isn't a great user experience anyway.

For this task, it's best falling back to UIKit's becomeFirstResponder(). It's easy to wrap a UITextField in a UIViewRepresentable like this:

struct MyTextField: UIViewRepresentable {
    typealias UIViewType = UITextField

    @Binding var becomeFirstResponder: Bool

    func makeUIView(context: Context) -> UITextField {
        return UITextField()
    }
    
    func updateUIView(_ textField: UITextField, context: Context) {
        if self.becomeFirstResponder {
            DispatchQueue.main.async {
                textField.becomeFirstResponder()
                self.becomeFirstResponder = false
            }
        }
    }
}

Notice the async call. It is necessary because it's modifying the state and it isn't allowed in updateUIView. If you forget, Xcode will warn you Modifying state during view update, this will cause undefined behavior.

To make it first responder when the view appears, just call it in .onAppear. And it works without a delay:

struct ContentView: View {
    @State private var becomeFirstResponder = false

    var body: some View {
        MyTextField(becomeFirstResponder: self.$becomeFirstResponder)
            .onAppear {
                self.becomeFirstResponder = true
            }
    }
}

TIP: If you use SwiftUI Instrospect, you can directly inspect the backing UITextField without your own wrapper:

struct ContentView: View {
    @State private var name = "Mango Umbrella"
    @State private var becomeFirstResponder = true

    var body: some View {
        TextField("Name", text: self.$name)
            .introspectTextField { textField in
                if self.becomeFirstResponder {
                    textField.becomeFirstResponder()
                    self.becomeFirstResponder = false
                }
            }
        }
    }
}