如何在SwiftUI中在Face ID匹配后更新所选行的列表?

huangapple go评论69阅读模式
英文:

How to update selected row from List after Face ID matched in SwiftUI?

问题

我有一个 List,我想在点击 contextMenu 按钮时更新项目的文本。

当按钮被点击时,一个 @Published 的值会被更新。我使用 onReceive 监听值的变化,如果该值为 true,则应该更新长按以显示 contextMenu 并点击按钮的列表项的文本。

问题是列表中的所有项目都被更新了。所以 onReceive 对于列表中的每个元素都会触发。从某种程度上我能理解,因为元素是在 ForEach 中生成的,尽管我的期望是只更新一个项目。

我尝试捕获所选索引,但再次触发了列表中的每个项目的 onReceive

如何定义一个自定义修饰符,类似于 onDelete,可以在正确的 IndexSet 上删除,或者定义一个函数,可以接受 IndexSet 并应用我需要的更改到该索引上?

以下是我尝试解决的代码:

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.biometricsAvailable = false
            case .notAvailable, .unknown:
                self.biometricsAvailable = false
            }
        }
    }

    init() {
        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?

        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            let reason = "We need to unlock your data"

            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
                if success {
                    Task { @MainActor in
                        self.isUnlocked = true
                    }
                } else {
                    // Handle authentication failure
                }
            }
        } else {
            // Biometrics not available
        }
    }
}

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())
    }
}

这只是我应用程序的一个复制品。我想了解这个 onReceive 是如何工作的,或者是否将其应用于 ForEach 是一个好主意。我尝试将其移到 List 级别,但我不再能够访问从循环中获取的索引。

还要提到,在真实的应用程序中,数据被持久化在 CoreData 中,但为了简化,我在这个示例中创建了一个数组。

任何帮助都将不胜感激。

英文:

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.

答案1

得分: 0

我成功完成了它。我将onReceive移至List级别,并从列表中获取了所选项目,即用于显示上下文菜单的项目。在身份验证调用之后设置所选项目。

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
        }
    }

    func passcodeAuthenticate() {
        let context = LAContext()
        var error: NSError?

        // check wether biometric authentication is possible
        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
            // it's possible, so go ahead and use it
            let reason = "Authenticate to access your data"

            context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authenticationError in
                // authentication has now completed
                if success {
                    // authenticated successfully
                    DispatchQueue.main.async {
                        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")
    ]
    @State var selectedOcean: Ocean?
    @State var selectedIndex: Int?
    @State var biometricsCalls: Int = 0

    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 biometricsHandler.isUnlocked {
                                biometricsHandler.passcodeAuthenticate()
                            }
                            selectedOcean = ocean
                        }) {
                            if ocean.hasPhoto {
                                Label("Remove lock", systemImage: "lock.slash")
                            } else {
                                Label("Lock", systemImage: "lock")
                            }
                        }
                    }
                }
                .onDelete(perform: removeRows)
            }
            .onReceive(biometricsHandler.$isUnlocked) { isUnlocked in
                if isUnlocked {
                    if let index = oceans.firstIndex(where: { $0 == selectedOcean }) {
                        oceans[index].hasPhoto.toggle()
                    }
                }
            }
        }
    }

    func removeRows(at offsets: IndexSet) {
        withAnimation {
            oceans.remove(atOffsets: offsets)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(BiometricsHandler())
    }
}
英文:

I managed to do it. I moved onReceive on List level 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.

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
}
}
func passcodeAuthenticate() {
let context = LAContext()
var error: NSError?
// check wether biometric authentication is possible
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
// it's possible, so go ahead and use it
let reason = "Authenticate to access your data"
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authenticationError in
// authentication has now completed
if success {
// authenticated successfully
DispatchQueue.main.async {
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")
]
@State var selectedOcean: Ocean?
@State var selectedIndex: Int?
@State var biometricsCalls: Int = 0
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 biometricsHandler.isUnlocked {
biometricsHandler.passcodeAuthenticate()
}
selectedOcean = ocean
}) {
if ocean.hasPhoto {
Label("Remove lock", systemImage: "lock.slash")
} else {
Label("Lock", systemImage: "lock")
}
}
}
}
.onDelete(perform: removeRows)
}
.onReceive(biometricsHandler.$isUnlocked) { isUnlocked in
if isUnlocked {
if let index = oceans.firstIndex(where: {$0 == selectedOcean}) {
oceans[index].hasPhoto.toggle()
}
}
}
}
}
func removeRows(at offsets: IndexSet) {
withAnimation {
oceans.remove(atOffsets: offsets)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(BiometricsHandler())
}
}

huangapple
  • 本文由 发表于 2023年7月14日 03:57:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/76682837.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定