I have a List that I want to update item's text when contextMenu button is tapped.
When the button is tapped a @Published value is updated. I listen to value changes with onReceive and if that value is true the list item where I long pressed to bring the contextMenu and tap the button should update its text.
The issue is that all the items from the list are updated. So onReceive is hit for every element from the list. In one way I understand because elements are populated in ForEach although my expectation was to update only one item.
The behaviour I'm trying to replicate is from Notes app when you long press a Note and tap Lock Note. On that action the lock is applied only for the selected Note.
I tried to capture the selected index but again the onReceive is triggered for every item from the list.
How to define a custom modifier like onDelete that deletes at the right IndexSet or a function that can take the IndexSet and apply the changes I need to that index?
Here is the code I'm trying to solve.
import SwiftUI
import LocalAuthentication
enum BiometricStates {
case available
case lockedOut
case notAvailable
case unknown
}
class BiometricsHandler: ObservableObject {
@Published var biometricsAvailable = false
@Published var isUnlocked = false
private var context = LAContext()
private var biometryState = BiometricStates.unknown {
didSet {
switch biometryState {
case .available:
self.biometricsAvailable = true
case .lockedOut:
// self.loginState = .biometryLockout
self.biometricsAvailable = false
case .notAvailable, .unknown:
self.biometricsAvailable = false
}
}
}
init() {
// self.loginState = .loggedOut
checkBiometrics()
}
private func checkBiometrics() {
var evaluationError: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &evaluationError) {
switch context.biometryType {
case .faceID, .touchID:
biometryState = .available
default:
biometryState = .unknown
}
} else {
guard let error = evaluationError else {
biometryState = .unknown
return
}
let errorCode = LAError(_nsError: error).code
switch(errorCode) {
case .biometryNotEnrolled, .biometryNotAvailable:
biometryState = .notAvailable
case .biometryLockout:
biometryState = .lockedOut
default:
biometryState = .unknown
}
}
}
func authenticate() {
let context = LAContext()
var error: NSError?
// check wether biometric authentication is possible
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
// it's possible, so go ahead and use it
let reason = "We need to unlock your data"
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
// authentication has now completed
if success {
// authenticated successfully
Task { @MainActor in
self.isUnlocked = true
}
}
else {
// there was a problem
}
}
}
else {
// no biometrics
}
}
}
struct Ocean: Identifiable, Equatable {
let name: String
let id = UUID()
var hasPhoto: Bool = false
}
struct OceanDetails: View {
var ocean: Ocean
var body: some View {
Text("\(ocean.name)")
}
}
struct ContentView: View {
@EnvironmentObject var biometricsHandler: BiometricsHandler
@State private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
var body: some View {
NavigationView {
List {
ForEach(Array(oceans.enumerated()), id: \.element.id) { (index,ocean) in
NavigationLink(destination: OceanDetails(ocean: ocean)) {
ocean.hasPhoto ? Text(ocean.name) + Text(Image(systemName: "lock")) : Text("\(ocean.name)")
}
.contextMenu() {
Button(action: {
biometricsHandler.authenticate()
}) {
if ocean.hasPhoto {
Label("Remove lock", systemImage: "lock.slash")
} else {
Label("Lock", systemImage: "lock")
}
}
}
.onReceive(biometricsHandler.$isUnlocked) { isUnlocked in
if isUnlocked {
oceans[index].hasPhoto.toggle()
biometricsHandler.isUnlocked = false
}
}
}
.onDelete(perform: removeRows)
}
}
}
func removeRows(at offsets: IndexSet) {
withAnimation {
oceans.remove(atOffsets: offsets)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(BiometricsHandler())
}
}
This is just a replication from my app. I want to understand how this onReceive is working or if it is a good idea to apply it on ForEach. I tried to move it on List level but I don't have access anymore to index that I get from the loop.
Also would like to mention that in real app the data is being persisted in CoreData but for simplicity I created an array in this exmple.
Any help would be much appreciated.
I managed to do it. I moved
onReceiveonListlevel and got the selected item from the list, the one that is tapped for the context menu to show. Set the selected item after the call to authenticate.