Building interactive Apple Watch widget
With watchOS 10, we are able to add widgets into Smart Stack or use them in large complications. The Modular watch face offers one large complication. I use it to display my passwords and PIN codes using my own app Pinbook ($4.99). But there’s a problem: this complication is always visible, especially with Always On screens, and you potentially can reveal sensitive information. And I couldn’t make the complication interactive until watchOS 11.
Now in watchOS 11, Apple added interactive widgets, and I began working on updating the app. Finally, I was able to build my original vision of this product. Development took longer than expected because I also wanted to include a bunch of other features. I am using the Modular watch face with Pinbook 1.1 every day, and I find it convenient.
Now let’s dive deep into technical implementation. I am not sure my implementation is right, but it took some time to figure out and is not obvious. It is possible I am not using the most optimal strategy.
At the core, I use the NSUserDefaults
flag to toggle between two states: locked and unlocked. You can only see passwords in the unlocked state. And you see a --
placeholder in the locked state.
@AppStorage("unlocked") private var unlocked = false
The @AppStorage
is used in the widget View
.
The only way to make the widget interactive is to use the App Intents API.
Button(intent: RevelPasswords()) {
// Widget View
}
Here’s the entire app intent.
import Foundation
import AppIntents
import WatchKit
struct RevelPasswords: AppIntent {
static var title: LocalizedStringResource = "Revel passwords"
static var description = IntentDescription("Used for widget")
func perform() async throws -> some IntentResult {
if UserDefaults.standard.bool(forKey: "unlocked") {
UserDefaults.standard.set(false, forKey: "unlocked")
} else {
UserDefaults.standard.set(true, forKey: "unlocked")
}
return .result()
}
}
Finally, my widget view.
struct ColumnView: View {
let pin: Pin
@Binding var unlocked: Bool
var body: some View {
Label {
if unlocked {
PasswordView(password: pin.code)
} else {
PasswordView(password: "--")
.privacySensitive(true)
}
} icon: {
Image(systemName: pin.symbol)
.font(.caption)
.frame(width: 8, height: 8, alignment: .center)
.widgetAccentable()
}
.onDisappear() { // This does not work.
unlocked = false
}
}
}
I use @Binding
linked to @AppStorage
in a parent view. Unfortunately, I have not yet found a way to toggle between the 2 states programmatically. For example, when the Apple Watch screen is not active, I could automatically hide passwords. I have tried to use .onDisappear()
, and it didn’t work. I am sure there’s a way to do it, but I just didn’t have enough time to figure it out yet.