英文:
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())
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论