Fix vCard contact list

This commit is contained in:
benoit.martins 2025-04-08 18:13:35 +02:00
parent 5989887723
commit 7219731c0e
5 changed files with 266 additions and 189 deletions

View file

@ -46,11 +46,12 @@ final class ContactsManager: ObservableObject {
private var coreDelegate: CoreDelegate?
private var friendListDelegate: FriendListDelegate?
private var magicSearchDelegate: MagicSearchDelegate?
private init() {}
func fetchContacts() {
coreContext.doOnCoreQueue { core in
self.coreContext.doOnCoreQueue { core in
if core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off {
print("\(#function) - Core is being stopped or already destroyed, abort")
} else {
@ -91,12 +92,12 @@ final class ContactsManager: ObservableObject {
}
}
MagicSearchSingleton.shared.searchForContactsWithoutCoreThread(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, error) in
if let error = error {
print("\(#function) - failed to request access", error)
self.addFriendListDelegate()
self.addCoreDelegate(core: core)
return
}
if granted {
@ -105,10 +106,16 @@ final class ContactsManager: ObservableObject {
CNContactPostalAddressesKey, CNContactIdentifierKey,
CNInstantMessageAddressUsernameKey, CNContactInstantMessageAddressesKey,
CNContactOrganizationNameKey, CNContactImageDataAvailableKey, CNContactImageDataKey, CNContactThumbnailImageDataKey]
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
let dispatchGroup = DispatchGroup()
do {
var contactCounter = 0
try store.enumerateContacts(with: request, usingBlock: { (contact, _) in
dispatchGroup.enter()
let newContact = Contact(
identifier: contact.identifier,
firstName: contact.givenName,
@ -129,51 +136,7 @@ final class ContactsManager: ObservableObject {
name: contact.givenName + contact.familyName,
prefix: "",
contact: newContact, linphoneFriend: false, existingFriend: nil) {
if (self.friendList?.friends.count ?? 0) == contactCounter {
// Every contact properly added, proceed
self.linphoneFriendList?.updateSubscriptions()
self.friendList?.updateSubscriptions()
if let friendListDelegate = self.friendListDelegate {
self.friendList?.removeDelegate(delegate: friendListDelegate)
}
self.friendListDelegate = FriendListDelegateStub(onNewSipAddressDiscovered: { (_: FriendList, linphoneFriend: Friend, sipUri: String) in
var addedAvatarListModel: [ContactAvatarModel] = []
linphoneFriend.phoneNumbers.forEach { _ in
let address = try? Factory.Instance.createAddress(addr: sipUri)
if address != nil {
linphoneFriend.edit()
linphoneFriend.addAddress(address: address!)
linphoneFriend.done()
let addressTmp = linphoneFriend.address?.clone()?.asStringUriOnly() ?? ""
addedAvatarListModel.append(
ContactAvatarModel(
friend: linphoneFriend,
name: linphoneFriend.name ?? "",
address: addressTmp,
withPresence: true
)
)
DispatchQueue.main.async {
NotificationCenter.default.post(
name: NSNotification.Name("ContactAdded"),
object: nil,
userInfo: ["address": addressTmp]
)
}
}
}
DispatchQueue.main.async {
self.avatarListModel += addedAvatarListModel
self.avatarListModel = self.avatarListModel.sorted { $0.name < $1.name }
}
})
self.friendList?.addDelegate(delegate: self.friendListDelegate!)
}
dispatchGroup.leave()
}
}
} else {
@ -183,70 +146,30 @@ final class ContactsManager: ObservableObject {
name: contact.givenName + contact.familyName,
prefix: "-default",
contact: newContact, linphoneFriend: false, existingFriend: nil) {
if (self.friendList?.friends.count ?? 0) == contactCounter {
// Every contact properly added, proceed
self.linphoneFriendList?.updateSubscriptions()
self.friendList?.updateSubscriptions()
if let friendListDelegate = self.friendListDelegate {
self.friendList?.removeDelegate(delegate: friendListDelegate)
}
self.friendListDelegate = FriendListDelegateStub(onNewSipAddressDiscovered: { (_: FriendList, linphoneFriend: Friend, sipUri: String) in
var addedAvatarListModel: [ContactAvatarModel] = []
linphoneFriend.phoneNumbers.forEach { _ in
let address = try? Factory.Instance.createAddress(addr: sipUri)
if address != nil {
linphoneFriend.edit()
linphoneFriend.addAddress(address: address!)
linphoneFriend.done()
let addressTmp = linphoneFriend.address?.clone()?.asStringUriOnly() ?? ""
addedAvatarListModel.append(
ContactAvatarModel(
friend: linphoneFriend,
name: linphoneFriend.name ?? "",
address: addressTmp,
withPresence: true
)
)
DispatchQueue.main.async {
NotificationCenter.default.post(
name: NSNotification.Name("ContactAdded"),
object: nil,
userInfo: ["address": addressTmp]
)
}
}
}
DispatchQueue.main.async {
self.avatarListModel += addedAvatarListModel
self.avatarListModel = self.avatarListModel.sorted { $0.name < $1.name }
}
})
self.friendList?.addDelegate(delegate: self.friendListDelegate!)
}
dispatchGroup.leave()
}
}
}
if !(contact.givenName.isEmpty && contact.familyName.isEmpty) {
contactCounter += 1
}
})
dispatchGroup.notify(queue: .main) {
self.addFriendListDelegate()
self.addCoreDelegate(core: core)
MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
}
} catch let error {
print("\(#function) - Failed to enumerate contact", error)
self.addFriendListDelegate()
self.addCoreDelegate(core: core)
MagicSearchSingleton.shared.searchForContactsWithoutCoreThread(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
}
} else {
print("\(#function) - access denied")
self.addFriendListDelegate()
self.addCoreDelegate(core: core)
MagicSearchSingleton.shared.searchForContactsWithoutCoreThread(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
}
}
self.addCoreDelegate(core: core)
}
}
@ -459,6 +382,106 @@ final class ContactsManager: ObservableObject {
}
}
func addFriendListDelegate() {
self.linphoneFriendList?.updateSubscriptions()
self.friendList?.updateSubscriptions()
CoreContext.shared.mCore.friendsLists.forEach { friendList in
friendList.updateSubscriptions()
}
if let friendListDelegate = self.friendListDelegate {
CoreContext.shared.mCore.friendsLists.forEach { friendList in
friendList.removeDelegate(delegate: friendListDelegate)
}
}
self.friendListDelegate = FriendListDelegateStub(
onContactCreated: { (friendList: FriendList, linphoneFriend: Friend) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactCreated")
},
onContactDeleted: { (friendList: FriendList, linphoneFriend: Friend) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactDeleted")
},
onContactUpdated: { (friendList: FriendList, newFriend: Friend, oldFriend: Friend) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactUpdated")
},
onSyncStatusChanged: { (friendList: FriendList, status: FriendList.SyncStatus?, message: String?) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onSyncStatusChanged")
if status == .Successful {
friendList.friends.forEach { friend in
let addressTmp = friend.address?.clone()?.asStringUriOnly() ?? ""
let newContact = Contact(
identifier: UUID().uuidString,
firstName: friend.name ?? addressTmp,
lastName: "",
organizationName: "",
jobTitle: "",
displayName: friend.address?.displayName ?? "",
sipAddresses: friend.addresses.map { $0.asStringUriOnly() },
phoneNumbers: [],
imageData: ""
)
self.textToImageInMainThread(firstName: friend.name ?? addressTmp, lastName: "") { image in
self.saveImage(
image: image,
name: friend.name ?? addressTmp,
prefix: "-default",
contact: newContact, linphoneFriend: false, existingFriend: friend) {
}
}
}
}
MagicSearchSingleton.shared.searchForContactsWithoutCoreThread(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
},
onPresenceReceived: { (friendList: FriendList, friends: [Friend?]) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onPresenceReceived \(friends.count)")
},
onNewSipAddressDiscovered: { (_: FriendList, linphoneFriend: Friend, sipUri: String) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onNewSipAddressDiscovered \(linphoneFriend.name ?? "")")
var addedAvatarListModel: [ContactAvatarModel] = []
linphoneFriend.phoneNumbers.forEach { _ in
let address = try? Factory.Instance.createAddress(addr: sipUri)
if address != nil {
linphoneFriend.edit()
linphoneFriend.addAddress(address: address!)
linphoneFriend.done()
let addressTmp = linphoneFriend.address?.clone()?.asStringUriOnly() ?? ""
addedAvatarListModel.append(
ContactAvatarModel(
friend: linphoneFriend,
name: linphoneFriend.name ?? "",
address: addressTmp,
withPresence: true
)
)
DispatchQueue.main.async {
NotificationCenter.default.post(
name: NSNotification.Name("ContactAdded"),
object: nil,
userInfo: ["address": addressTmp]
)
}
}
}
DispatchQueue.main.async {
self.avatarListModel += addedAvatarListModel
self.avatarListModel = self.avatarListModel.sorted { $0.name < $1.name }
}
}
)
CoreContext.shared.mCore.friendsLists.forEach { friendList in
friendList.addDelegate(delegate: self.friendListDelegate!)
}
}
func addCoreDelegate(core: Core) {
self.coreDelegate = CoreDelegateStub(
onFriendListCreated: { (_: Core, friendList: FriendList) in

View file

@ -282,6 +282,9 @@ final class CoreContext: ObservableObject {
switch state {
case .Ok:
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name("CoreStarted"), object: nil)
}
ContactsManager.shared.fetchContacts()
if self.mCore.consolidatedPresence != ConsolidatedPresence.Online {
self.updatePresence(core: self.mCore, presence: ConsolidatedPresence.Online)

View file

@ -82,15 +82,18 @@ struct ContentView: View {
private let avatarSize = 45.0
@State private var imagePath: URL?
@State private var imageTmp: Image?
var body: some View {
let pub = NotificationCenter.default
let contactLoaded = NotificationCenter.default
.publisher(for: NSNotification.Name("ContactLoaded"))
let pub2 = NotificationCenter.default
let contactAdded = NotificationCenter.default
.publisher(for: NSNotification.Name("ContactAdded"))
.compactMap { $0.userInfo?["address"] as? String }
let imageChanged = NotificationCenter.default
.publisher(for: NSNotification.Name("ImageChanged"))
let coreStarted = NotificationCenter.default
.publisher(for: NSNotification.Name("CoreStarted"))
GeometryReader { geometry in
VStack(spacing: 0) {
if (telecomManager.callInProgress && !fullscreenVideo && ((!telecomManager.callDisplayed && callViewModel.callsCounter == 1) || callViewModel.callsCounter > 1)) || isShowConversationFragment {
@ -316,7 +319,8 @@ struct ContentView: View {
VStack(spacing: 0) {
if searchIsActive == false {
HStack {
if (accountProfileViewModel.accountModelIndex ?? 0) < CoreContext.shared.accounts.count {
if let accountModelIndex = accountProfileViewModel.accountModelIndex,
accountModelIndex < CoreContext.shared.accounts.count {
AsyncImage(url: imagePath) { image in
switch image {
case .empty:
@ -328,13 +332,31 @@ struct ContentView: View {
.aspectRatio(contentMode: .fill)
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
.onAppear {
imageTmp = image
}
case .failure:
Image(uiImage: contactsManager.textToImage(
firstName: CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex ?? 0].avatarModel?.name ?? "",
lastName: ""))
.resizable()
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
if CoreContext.shared.accounts[accountModelIndex].avatarModel != nil {
let tmpImage = contactsManager.textToImage(
firstName: CoreContext.shared.accounts[accountModelIndex].avatarModel!.name,
lastName: "")
Image(uiImage: tmpImage)
.resizable()
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
.onAppear {
accountProfileViewModel.saveImage(image: tmpImage, name: CoreContext.shared.accounts[accountModelIndex].avatarModel!.name, prefix: "-default")
}
} else if let cachedImage = imageTmp {
cachedImage
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
} else {
ProgressView()
.frame(width: avatarSize, height: avatarSize)
}
@unknown default:
EmptyView()
}
@ -343,17 +365,13 @@ struct ContentView: View {
openMenu()
}
.onAppear {
if let accountModelIndex = accountProfileViewModel.accountModelIndex,
accountModelIndex < CoreContext.shared.accounts.count {
let imagePathTmp = CoreContext.shared.accounts[accountModelIndex].getImagePath()
if !(imagePathTmp.lastPathComponent.isEmpty || imagePathTmp.lastPathComponent == "Error" || imagePathTmp.lastPathComponent == "ImageError.png") {
imagePath = imagePathTmp
}
let imagePathTmp = CoreContext.shared.accounts[accountModelIndex].getImagePath()
if !(imagePathTmp.lastPathComponent.isEmpty || imagePathTmp.lastPathComponent == "Error" || imagePathTmp.lastPathComponent == "ImageError.png") {
imagePath = imagePathTmp
}
}
.onChange(of: CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex ?? 0].usernaneAvatar) { _ in
if let accountModelIndex = accountProfileViewModel.accountModelIndex,
accountModelIndex < CoreContext.shared.accounts.count {
.onChange(of: CoreContext.shared.accounts[accountModelIndex].usernaneAvatar) { username in
if !username.isEmpty {
let imagePathTmp = CoreContext.shared.accounts[accountModelIndex].getImagePath()
if !(imagePathTmp.lastPathComponent.isEmpty || imagePathTmp.lastPathComponent == "Error" || imagePathTmp.lastPathComponent == "ImageError.png") {
sharedMainViewModel.changeDefaultAvatar(defaultAvatarURL: imagePathTmp)
@ -362,8 +380,7 @@ struct ContentView: View {
}
}
.onReceive(imageChanged) { _ in
if let accountModelIndex = accountProfileViewModel.accountModelIndex,
accountModelIndex < CoreContext.shared.accounts.count {
if !CoreContext.shared.accounts[accountModelIndex].usernaneAvatar.isEmpty {
let imagePathTmp = CoreContext.shared.accounts[accountModelIndex].getImagePath()
sharedMainViewModel.changeDefaultAvatar(defaultAvatarURL: imagePathTmp)
imagePath = imagePathTmp
@ -1366,13 +1383,16 @@ struct ContentView: View {
self.index = 2
}
}
.onReceive(pub) { _ in
.onReceive(contactLoaded) { _ in
conversationsListViewModel.updateChatRoomsList()
historyListViewModel.refreshHistoryAvatarModel()
}
.onReceive(pub2) { address in
.onReceive(contactAdded) { address in
conversationsListViewModel.updateChatRoom(address: address)
}
.onReceive(coreStarted) { _ in
accountProfileViewModel.setAvatarModel()
}
}
.overlay {
if isMenuOpen {
@ -1396,7 +1416,6 @@ struct ContentView: View {
orientation = UIDevice.current.orientation
if newPhase == .active {
conversationsListViewModel.computeChatRoomsList(filter: "")
accountProfileViewModel.setAvatarModel()
}
}
}

View file

@ -34,36 +34,38 @@ class AccountProfileViewModel: ObservableObject {
func saveChangesWhenLeaving() {
if accountModelIndex != nil {
CoreContext.shared.doOnCoreQueue { _ in
let displayNameAccountModel = CoreContext.shared.accounts[self.accountModelIndex!].displayNameAvatar
let newParams = CoreContext.shared.accounts[self.accountModelIndex!].account.params?.clone()
if (displayNameAccountModel != newParams?.identityAddress?.displayName)
&& (newParams?.identityAddress?.displayName != nil || !displayNameAccountModel.isEmpty) {
if let newIdentityAddress = newParams?.identityAddress?.clone() {
try? newIdentityAddress.setDisplayname(newValue: displayNameAccountModel)
try? newParams?.setIdentityaddress(newValue: newIdentityAddress)
}
if self.getImagePath().lastPathComponent.contains("-default") || self.getImagePath().lastPathComponent == "Documents" {
let usernameTmp = CoreContext.shared.accounts[self.accountModelIndex!].usernaneAvatar
if self.accountModelIndex! < CoreContext.shared.accounts.count {
let displayNameAccountModel = CoreContext.shared.accounts[self.accountModelIndex!].displayNameAvatar
let newParams = CoreContext.shared.accounts[self.accountModelIndex!].account.params?.clone()
if (displayNameAccountModel != newParams?.identityAddress?.displayName)
&& (newParams?.identityAddress?.displayName != nil || !displayNameAccountModel.isEmpty) {
if let newIdentityAddress = newParams?.identityAddress?.clone() {
try? newIdentityAddress.setDisplayname(newValue: displayNameAccountModel)
try? newParams?.setIdentityaddress(newValue: newIdentityAddress)
}
DispatchQueue.main.async {
self.saveImage(
image: ContactsManager.shared.textToImage(
firstName: displayNameAccountModel.isEmpty ? usernameTmp : displayNameAccountModel, lastName: ""),
name: usernameTmp,
prefix: "-default")
if self.getImagePath().lastPathComponent.contains("-default") || self.getImagePath().lastPathComponent == "Documents" {
let usernameTmp = CoreContext.shared.accounts[self.accountModelIndex!].usernaneAvatar
DispatchQueue.main.async {
self.saveImage(
image: ContactsManager.shared.textToImage(
firstName: displayNameAccountModel.isEmpty ? usernameTmp : displayNameAccountModel, lastName: ""),
name: usernameTmp,
prefix: "-default")
}
}
}
if self.dialPlanSelected != nil
&& (self.dialPlanSelected!.countryCallingCode != newParams?.internationalPrefix || self.dialPlanSelected!.isoCountryCode != newParams?.internationalPrefixIsoCountryCode) {
newParams?.internationalPrefix = self.dialPlanSelected?.countryCallingCode
newParams?.internationalPrefixIsoCountryCode = self.dialPlanSelected?.isoCountryCode
newParams?.useInternationalPrefixForCallsAndChats = true
}
CoreContext.shared.accounts[self.accountModelIndex!].account.params = newParams
}
if self.dialPlanSelected != nil
&& (self.dialPlanSelected!.countryCallingCode != newParams?.internationalPrefix || self.dialPlanSelected!.isoCountryCode != newParams?.internationalPrefixIsoCountryCode) {
newParams?.internationalPrefix = self.dialPlanSelected?.countryCallingCode
newParams?.internationalPrefixIsoCountryCode = self.dialPlanSelected?.isoCountryCode
newParams?.useInternationalPrefixForCallsAndChats = true
}
CoreContext.shared.accounts[self.accountModelIndex!].account.params = newParams
}
}
}
@ -118,7 +120,7 @@ class AccountProfileViewModel: ObservableObject {
return
}
let photoAvatarModelKey = "photo_avatar_model" + CoreContext.shared.accounts[self.accountModelIndex!].usernaneAvatar
let photoAvatarModelKey = CoreContext.shared.accounts[self.accountModelIndex!].usernaneAvatar
ContactsManager.shared.awaitDataWrite(data: data, name: name, prefix: prefix) { _, result in
UserDefaults.standard.set(result, forKey: photoAvatarModelKey)

View file

@ -98,47 +98,55 @@ class AccountModel: ObservableObject {
let preferences = UserDefaults.standard
let photoAvatarModelKey = "photo_avatar_model" + usernaneAvatarTmp
let photoAvatarModelKey = usernaneAvatarTmp
if preferences.object(forKey: photoAvatarModelKey) == nil {
preferences.set(photoAvatarModelKey, forKey: photoAvatarModelKey)
} else {
photoAvatarModelTmp = preferences.string(forKey: photoAvatarModelKey)!
}
DispatchQueue.main.async { [self] in
switch state {
case .Cleared, .None:
humanReadableRegistrationState = "drawer_menu_account_connection_status_cleared".localized()
summary = "manage_account_status_cleared_summary".localized()
registrationStateAssociatedUIColor = .orangeWarning600
case .Progress:
humanReadableRegistrationState = "drawer_menu_account_connection_status_progress".localized()
summary = "manage_account_status_progress_summary".localized()
registrationStateAssociatedUIColor = .greenSuccess500
case .Failed:
humanReadableRegistrationState = "drawer_menu_account_connection_status_failed".localized()
summary = "manage_account_status_failed_summary".localized()
registrationStateAssociatedUIColor = .redDanger500
case .Ok:
humanReadableRegistrationState = "drawer_menu_account_connection_status_connected".localized()
summary = "manage_account_status_connected_summary".localized()
registrationStateAssociatedUIColor = .greenSuccess500
case .Refreshing:
humanReadableRegistrationState = "drawer_menu_account_connection_status_refreshing".localized()
summary = "manage_account_status_progress_summary".localized()
registrationStateAssociatedUIColor = .grayMain2c500
if !photoAvatarModelKey.isEmpty {
if preferences.object(forKey: photoAvatarModelKey) == nil {
DispatchQueue.main.async {
self.saveImage(
image: ContactsManager.shared.textToImage(
firstName: usernaneAvatarTmp, lastName: ""),
name: usernaneAvatarTmp,
prefix: "-default")
}
} else {
photoAvatarModelTmp = preferences.string(forKey: photoAvatarModelKey)!
}
isRegistrered = state == .Ok
isDefaultAccount = isDefault
self.displayName = displayName
address.map {self.address = $0}
photoAvatarModel = photoAvatarModelTmp
displayNameAvatar = displayNameTmp
usernaneAvatar = usernaneAvatarTmp
imagePathAvatar = getImagePath()
DispatchQueue.main.async { [self] in
switch state {
case .Cleared, .None:
humanReadableRegistrationState = "drawer_menu_account_connection_status_cleared".localized()
summary = "manage_account_status_cleared_summary".localized()
registrationStateAssociatedUIColor = .orangeWarning600
case .Progress:
humanReadableRegistrationState = "drawer_menu_account_connection_status_progress".localized()
summary = "manage_account_status_progress_summary".localized()
registrationStateAssociatedUIColor = .greenSuccess500
case .Failed:
humanReadableRegistrationState = "drawer_menu_account_connection_status_failed".localized()
summary = "manage_account_status_failed_summary".localized()
registrationStateAssociatedUIColor = .redDanger500
case .Ok:
humanReadableRegistrationState = "drawer_menu_account_connection_status_connected".localized()
summary = "manage_account_status_connected_summary".localized()
registrationStateAssociatedUIColor = .greenSuccess500
case .Refreshing:
humanReadableRegistrationState = "drawer_menu_account_connection_status_refreshing".localized()
summary = "manage_account_status_progress_summary".localized()
registrationStateAssociatedUIColor = .grayMain2c500
}
isRegistrered = state == .Ok
isDefaultAccount = isDefault
self.displayName = displayName
address.map {self.address = $0}
photoAvatarModel = photoAvatarModelTmp
displayNameAvatar = displayNameTmp
usernaneAvatar = usernaneAvatarTmp
imagePathAvatar = getImagePath()
}
}
}
@ -248,6 +256,28 @@ class AccountModel: ObservableObject {
core.removeAccount(account: self.account)
}
}
func saveImage(image: UIImage, name: String, prefix: String) {
guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else {
return
}
let photoAvatarModelKey = name
ContactsManager.shared.awaitDataWrite(data: data, name: name, prefix: prefix) { _, result in
UserDefaults.standard.set(result, forKey: photoAvatarModelKey)
self.photoAvatarModel = ""
self.imagePathAvatar = nil
NotificationCenter.default.post(name: NSNotification.Name("ImageChanged"), object: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.photoAvatarModel = result
self.imagePathAvatar = self.getImagePath()
NotificationCenter.default.post(name: NSNotification.Name("ImageChanged"), object: nil)
}
}
}
}
class AccountDeviceModel: ObservableObject {