I've been able to get a rudimentary version of Face / Touch ID working inside my app. However, I want to add better fallbacks and error handling.
So I've been researching how to do it. There are fantastic resources like this:
Face ID evaluation process not working properly
However, I can't find anything that works inside a SwiftUI view. At the moment my project won't run with:
'unowned' may only be applied to class and class-bound protocol types, not 'AuthenticateView'
and
Value of type 'AuthenticateView' has no member 'present'
any help would be much appreciated. Thank you!
Here's my code inside AuthenticateView.swift
func Authenticate(completion: #escaping ((Bool) -> ())){
//Create a context
let authenticationContext = LAContext()
var error:NSError?
//Check if device have Biometric sensor
let isValidSensor : Bool = authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if isValidSensor {
//Device have BiometricSensor
//It Supports TouchID
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Touch / Face ID authentication",
reply: { [unowned self] (success, error) -> Void in
if(success) {
// Touch / Face ID recognized success here
completion(true)
} else {
//If not recognized then
if let error = error {
let strMessage = self.errorMessage(errorCode: error._code)
if strMessage != ""{
self.showAlertWithTitle(title: "Error", message: strMessage)
}
}
completion(false)
}
})
} else {
let strMessage = self.errorMessage(errorCode: (error?._code)!)
if strMessage != ""{
self.showAlertWithTitle(title: "Error", message: strMessage)
}
}
}
func errorMessage(errorCode:Int) -> String{
var strMessage = ""
switch errorCode {
case LAError.Code.authenticationFailed.rawValue:
strMessage = "Authentication Failed"
case LAError.Code.userCancel.rawValue:
strMessage = "User Cancel"
case LAError.Code.systemCancel.rawValue:
strMessage = "System Cancel"
case LAError.Code.passcodeNotSet.rawValue:
strMessage = "Please goto the Settings & Turn On Passcode"
case LAError.Code.touchIDNotAvailable.rawValue:
strMessage = "TouchI or FaceID DNot Available"
case LAError.Code.touchIDNotEnrolled.rawValue:
strMessage = "TouchID or FaceID Not Enrolled"
case LAError.Code.touchIDLockout.rawValue:
strMessage = "TouchID or FaceID Lockout Please goto the Settings & Turn On Passcode"
case LAError.Code.appCancel.rawValue:
strMessage = "App Cancel"
case LAError.Code.invalidContext.rawValue:
strMessage = "Invalid Context"
default:
strMessage = ""
}
return strMessage
}
func showAlertWithTitle( title:String, message:String ) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let actionOk = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(actionOk)
self.present(alert, animated: true, completion: nil)
}
Explanation:
'unowned' may only be applied to class and class-bound protocol types, not 'AuthenticateView'
First of all, you have AuthenticateView which is a struct. You can't do it class because whole Apple's SwiftUI idea is about structures. And because Struct is value type and not a Reference type, so no pointer as such. So you may not include code parts containing unowned self and weak self modifiers into struct AuthenticateView: View {}
Value of type 'AuthenticateView' has no member 'present'
present is a UIViewController's method. Here in SwiftUI you have no access to it. The alerts are being presented using the next style:
struct ContentView: View {
#State private var show = false
var body: some View {
Button(action: { self.show = true }) { Text("Click") }
.alert(isPresented: $showingAlert) {
Alert(title: Text("Title"),
message: Text("Message"),
dismissButton: .default(Text("Close")))
}
}
}
Solution:
For your case, I would create a class Handler subclass of ObservableObject for your logic and use the power of #ObservedObject, #Published and #State.
Rough example for understanding the concept:
import SwiftUI
struct ContentView: View {
#ObservedObject var handler = Handler()
var body: some View {
Button(action: { self.handler.toggleShowAlert() }) { Text("Click") }
.alert(isPresented: $handler.shouldShowAlert) {
Alert(title: Text(handler.someTitle),
message: Text(handler.someMessage),
dismissButton: .default(Text("Close")))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Handler: ObservableObject {
#Published var shouldShowAlert: Bool = false
#Published var someTitle = ""
#Published var someMessage = ""
func toggleShowAlert() {
shouldShowAlert.toggle()
someTitle = "ErrorTitle"
someMessage = "ErrorMessage"
}
}
Related
In this app, there is a main screen (WorkoutScreen) that displays the contents of a list one at a time as it iterates through the list (current workout in a list of many). In a popOver, a list that contains all of the workouts appears and has the ability to add, delete or move items in that list.
When I delete the bottom most item, there is no error. When I delete any other item in the list I get this NSRangeException error that crashes the app:
/*
2022-04-24 15:41:21.874306-0400 Trellis
beta[9560:3067012] *** Terminating app due to
uncaught exception 'NSRangeException', reason:
'*** __boundsFail: index 3 beyond bounds [0 ..
2]'
*** First throw call stack:
(0x1809150fc 0x19914fd64 0x180a1e564 0x180a2588c
0x1808c0444 0x1852dcce4 0x1852e1400 0x185424670
0x185423df0 0x185428a40 0x18843e4a0 0x188510458
0x188fd83ec 0x10102f3bc 0x1010500a4 0x188494f4c
0x10102c664 0x10103e0d4 0x18841a944 0x10102be18
0x10103122c 0x18837b8ac 0x188363484 0x18834bb64
0x188371d20 0x1883b88e4 0x1b28fe910 0x1b28fe318
0x1b28fd160 0x18831e780 0x18832f3cc 0x1883f5e34
0x18834206c 0x188345f00 0x182eb0798 0x184613138
0x184605958 0x184619f80 0x184622874 0x1846050b0
0x183266cc0 0x1835015fc 0x183b7d5b0 0x183b7cba0
0x1809370d0 0x180947d90 0x180882098 0x1808878a4
0x18089b468 0x19c42638c 0x18323d088 0x182fbb958
0x1885547a4 0x188483928 0x1884650c0 0x10109a630
0x10109a700 0x1015b9aa4)
libc++abi: terminating with uncaught exception
of type NSException
dyld4 config:
DYLD_LIBRARY_PATH=/usr/lib/system/introspection
DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktrac
eRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/Private
Frameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
*** Terminating app due to uncaught exception 'NSRangeException', reason: '***
__boundsFail: index 3 beyond bounds [0 .. 2]'
terminating with uncaught exception of type NSException
(lldb)
*/
struct WorkoutScreen: View {
#EnvironmentObject var workoutList: CoreDataViewModel //calls it from environment
#StateObject var vm = CoreDataViewModel() //access on the page
#Environment(\.scenePhase) var scenePhase
var body: some View{
//displays the current item in the list
}
}
When I add an item to the list I get the error:
'''
CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'FruitEntity' so +entity is unable to disambiguate."
'''
Moving Items without adding or deleting any prior gives me this error upon closing the pop over:
'''
Error saving: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x2804f1480) for NSManagedObject (0x28327d900) with objectID '0x9ede5774e26501a4...
'''
Here is the core data and related functions:
class CoreDataViewModel: NSObject, ObservableObject {
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
// Whenever you put your Core Data fetch in a view model, you should use an NSFetchedResultsController.
// This allows you to automatically update your #Published var when your Core Data store changes.
// You must inherit from NSObject to use it.
private let fetchResultsController: NSFetchedResultsController<FruitEntity>
#Published var savedEntities: [FruitEntity] = []
override init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("ERROR LOADING CORE DATA: \(error)")
}
else {
print("Successfully loaded core data")
}
}
context = container.viewContext
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
let sort = NSSortDescriptor(keyPath: \FruitEntity.order, ascending: true)
request.sortDescriptors = [sort]
// This initializes the fetchResultsController
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
// Because you inherit from NSObject, you must call super.init() to properly init the parent class. The order of when
// this is to be called has changed.
super.init()
// Because this is a delegate action, you must set the delegate. Since the view model will respond, we set the delegate to self.
fetchResultsController.delegate = self
fetchFruits()
}
func fetchFruits() {
do {
// Instead of calling container.viewContext.fetch(request) which is static, use fetchResultsController.performFetch()
try fetchResultsController.performFetch()
// Make sure the fetch result is not nil
guard let fruitRequest = fetchResultsController.fetchedObjects else { return }
savedEntities = fruitRequest
// You do not need to let error. error is automatically captured in a do catch.
} catch {
print("Error fetching \(error)")
}
}
func addFruit(text: String, nummSets: Int16, nummWeights: Int16, nummReps: Int16, secOrRepz: Bool, orderNumz: Int64, multilimbz: Bool, countDownz: Int16, repTimez: Int16, restTimez: Int16, circuitz: Bool) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
newFruit.numOFSets = nummSets
newFruit.numOFWeight = nummWeights
newFruit.numOFReps = nummReps
newFruit.measure = secOrRepz
newFruit.order = orderNumz
newFruit.multiLimb = multilimbz
newFruit.countDownSec = countDownz
newFruit.timePerRep = repTimez
newFruit.restTime = restTimez
newFruit.circuit = circuitz
saveData()
}
func deleteFunction(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = savedEntities[index]
container.viewContext.delete(entity)
saveData()
}
func saveData() {
do {
try context.save()
fetchFruits()
} catch let error {
print("Error saving: \(error)")
}
}
}
// This is your delegate extension that handles the updating when your Core Data Store changes.
extension CoreDataViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller:
NSFetchedResultsController<NSFetchRequestResult>) {
// Essentially, you are redoing just the fetch as the NSFetchedResultsController knows how to fetch from above
guard let fruits = controller.fetchedObjects as? [FruitEntity] else { return }
self.savedEntities = fruits
}
}
Here is the list struct:
struct WorkoutListPopUp: View {
#ObservedObject var vm = CoreDataViewModel()
#EnvironmentObject var listViewModel: ListViewModel
#EnvironmentObject var workoutList: CoreDataViewModel
//Too many #State var to list here
var body: some View {
Button (action: {
//this triggers the bug>
vm.addFruit(text: "Workout name", nummSets: Int16(addSets) ?? 3, nummWeights: Int16(addWeights) ?? 0, nummReps: Int16(addReps) ?? 8, secOrRepz: addSecOrReps, orderNumz: Int64((vm.savedEntities.count)), multilimbz: dualLimbs, countDownz: 10, repTimez: 3, restTimez: 60, circuitz: false)
loadNums()
}, label: {
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:20, height: 20)
.foregroundColor(Color.pink.opacity(1.0))
.padding(.top, 0)
})
List(){
ForEach(vm.savedEntities) {entity in
VStack{
EditWorkouts(entity: entity, prescribeMeasure: $prescribeMeasure, addReps: $addReps, measurePrescribed: $measurePrescribed, repTimePicker: $repTimePicker, repz: $repz, restPicker: $restPicker, setz: $setz, ready2Press: $ready2Press, workoutz: $workoutz, weightz: $weightz, setsRemaining: $setsRemaining, workoutNum: $workoutNum, workoutInstructions: $workoutInstructions, multiplelimbs: $multiplelimbs, showAllInfo: $showAllInfo)
//are these onChanges needed if "EditWorkouts" file is saving?
.onChange(of: entity.name) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFSets) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFReps) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFWeight) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.measure) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.order) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.circuit) { text in
vm.saveData()
loadNums()
}
}
}
.onDelete(perform: vm.deleteFunction)
.onMove(perform: moveItem)
}
}
func loadNums(){
if vm.savedEntities.count > 0 {
workoutz = vm.savedEntities[workoutNum].name ?? "NO Name"
setz = String(vm.savedEntities[workoutNum].numOFSets)
weightz = String(vm.savedEntities[workoutNum].numOFWeight)
repz = String(vm.savedEntities[workoutNum].numOFReps)
multiplelimbs = vm.savedEntities[workoutNum].multiLimb
prescribeMeasure = vm.savedEntities[workoutNum].measure
if setsRemaining == 0 && ((workoutNum + 1) - (Int(vm.savedEntities.count)) == 0) {
workoutInstructions = "Goal: \(repz) \(measurePrescribed)"
}
else {
workoutInstructions = "Goal: \(repz) \(measurePrescribed)"
}
}
else {
workoutz = "Add a Workout 👉"
workoutInstructions = " "
}
}
func moveItem(indexSet: IndexSet, destination: Int) {
let source = indexSet.first!
if destination > source {
var startIndex = source + 1
let endIndex = destination - 1
var startOrder = vm.savedEntities[source].order
while startIndex <= endIndex {
vm.savedEntities[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
vm.savedEntities[source].order = startOrder
}
else if destination < source {
var startIndex = destination
let endIndex = source - 1
var startOrder = vm.savedEntities[destination].order + 1
let newOrder = vm.savedEntities[destination].order
while startIndex <= endIndex {
vm.savedEntities[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
vm.savedEntities[source].order = newOrder
}
vm.savedEntities[source].circuit = false
vm.saveData()
loadNums()
}
}
This is the EditWorkouts file that the WorkoutPopUp file connects to:
struct EditWorkouts: View {
#EnvironmentObject var workoutList: CoreDataViewModel
#StateObject var vm = CoreDataViewModel()
#EnvironmentObject var listViewModel: ListViewModel
let entity: FruitEntity
//too many #State vars to post
var body: some View {
VStack{
HStack{
//many lines of code for options that alter the respective workout on the list. All are followed by their version of:
//.onChange(of:
//vm.savedEntities[Int(entity.order)].multiLimb) { _ in
//vm.saveData()
//loadNums()"
//}
//-or-
//.onChange(of:vm.savedEntities[Int(entity.order)].circuit) { _ in
//entity.circuit = entity.circuit
//vm.saveData()
//}
}
}
}
}
Picture of CoreData FruitEntity:
Image
Thank you again for your time!!
There are a couple of issues with your code. I suspect one is the sole contributor to the crash, but the other may be contributing as well. First, the most likely culprit. If you use .onDelete(), you can't use id: \.self. The reason is pretty simple: the ForEach can get pretty confused as to which entity is which. .self is often not unique, and it really needs to be if you are deleting and rearranging things in the ForEach(), i.e. .onDelete() and .onMove().
The solution is simple. Whatever you are using in the ForEach should conform to Identifiable. Core Data managed objects all conform to Identifiable, so the fix is easy; remove the `id: .self``:
struct ListView: View {
#StateObject var vm = CoreDataViewModel()
var body: some View {
List {
ForEach(vm.savedEntities) {entity in
Text(entity.name ?? "")
}
.onDelete(perform: vm.deleteFunction)
}
// This just adds a button to create entities.
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
vm.addFruit()
} label: {
Image(systemName: "plus")
}
}
}
}
}
That fix alone will most likely stop the crash. However, I also noticed that you were having issues with your updates in your view. That is because you did not implement an NSFetchedResultsController and NSFetchedResultsControllerDelegate which updates your array when your Core Data store changes. Your view model should look like this:
import SwiftUI
import CoreData
class CoreDataViewModel: NSObject, ObservableObject {
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
// Whenever you put your Core Data fetch in a view model, you should use an NSFetchedResultsController.
// This allows you to automatically update your #Published var when your Core Data store changes.
// You must inherit from NSObject to use it.
private let fetchResultsController: NSFetchedResultsController<FruitEntity>
#Published var savedEntities: [FruitEntity] = []
override init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("ERROR LOADING CORE DATA: \(error)")
}
else {
print("Successfully loaded core data")
}
}
context = container.viewContext
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
let sort = NSSortDescriptor(keyPath: \FruitEntity.order, ascending: true)
request.sortDescriptors = [sort]
// This initializes the fetchResultsController
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
// Because you inherit from NSObject, you must call super.init() to properly init the parent class. The order of when
// this is to be called has changed.
super.init()
// Because this is a delegate action, you must set the delegate. Since the view model will respond, we set the delegate to self.
fetchResultsController.delegate = self
// Renamed function to conform to naming conventions. You should use an active verb like fetch to start the name.
fetchFruits()
}
func fetchFruits() {
do {
// Instead of calling container.viewContext.fetch(request) which is static, use fetchResultsController.performFetch()
try fetchResultsController.performFetch()
// Make sure the fetch result is not nil
guard let fruitRequest = fetchResultsController.fetchedObjects else { return }
savedEntities = fruitRequest
// You do not need to let error. error is automatically captured in a do catch.
} catch {
print("Error fetching \(error)")
}
}
// This is just to be able to add some data to test.
func addFruit() {
var dateFormatter: DateFormatter {
let df = DateFormatter()
df.dateStyle = .short
return df
}
let fruit = FruitEntity(context: context)
fruit.name = dateFormatter.string(from: Date())
fruit.measure = false
fruit.numOfReps = 0
fruit.numOfSets = 0
fruit.numOfWeight = 0
fruit.order = 0
saveData()
}
func deleteFunction(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = savedEntities[index]
container.viewContext.delete(entity)
saveData()
}
func saveData() {
do {
try context.save()
} catch let error {
print("Error saving: \(error)")
}
}
}
// This is your delegate extension that handles the updating when your Core Data Store changes.
extension CoreDataViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
// Essentially, you are redoing just the fetch as the NSFetchedResultsController knows how to fetch from above
guard let fruits = controller.fetchedObjects as? [FruitEntity] else { return }
self.savedEntities = fruits
}
}
You will notice refreshID no longer exists in the view. It updates without it. Also, please note that by incorporating the data store init into your view model, you can't expand it to have other entities with other views. Each will have a different context and they will crash the app. You are better off having a controller class that creates a singleton for the Core Data store, such as what Apple gives you in the default set up.
In the end, I think you issue was a combination of using id: .self which is known to crash with .onDelete() AND the fact that you were using refreshID not NSFetchedResultsController to update the List.
I going learn swift and swiftUI.
I make application for organize notes by category. you can find my project in my GitHub if you need. https://github.com/yoan8306/List-Notes
I have problem. I think it's simple. I would like make 2 alerts messages. The first it's when save is success and the second is when they are problem like one field is empty or category is empty.
private func checkNoteIsOk() -> Bool{
if !noteTitleField.isEmpty && !noteField.isEmpty && categorySelected != nil {
return true
} else {
return false
}
}
.
Button(action: {
guard checkNoteIsOk() else {
presentAlert = true
return
}
coreDM.saveNote(noteData: noteField, noteTitle: noteTitleField,
noteDate: Date(), noteCategory: categorySelected!)
emptyField()
saveSuccess = true
},
label: {
Text("Save")
}
)
}
//end Vstak
.navigationTitle("Create new note")
.alert(isPresented: $presentAlert) {
Alert(title: Text("Error !"), message: Text("Not saved"),
dismissButton: .default(Text("OK"))) }
.alert(isPresented: $saveSuccess) {
Alert(title: Text("Success !"), message: Text("Insert with success !"),
dismissButton: .default(Text("OK"))) }
I think it's because they are two alerts messages. And only the last message alert can display. Thank you for your answer and your help.
For multiple alerts in a single view, you can use an enum.
First, you need to create an enum like this and define all the alert message
enum AlertType: Identifiable {
var id: UUID {
return UUID()
}
case success
case error
var title: String {
switch self {
case .success:
return "Success !"
case .error:
return "Error !"
}
}
var message: String {
switch self {
case .success:
return "Insert with success !"
case .error:
return "This category already exist !!"
}
}
}
now create one state var in the view.
struct NewCategoryView: View {
#State private var alertType: AlertType?
// Other code
}
and add the alert at the end
//end Vstak
.navigationTitle("New Category")
.onAppear(perform: { updateCategoryList()} )
.alert(item: self.$alertType, content: { (type) -> Alert in
Alert(title: Text(type.title), message: Text(type.message),
dismissButton: .default(Text("OK")))
})
now show the alert by assigning the value. Like this
if condition_true {
alertType = AlertType.success //<-- Here
} else {
alertType = AlertType.error //<-- Here
}
While [Raja]'s answer is working. I don't think it is ideal because
it generates random UUID's which Apple discourages when it's not needed.
it does requires multiple switch statements where only one is needed.
A more simple solution might be to define the enum like this
enum ResultAlert: Int8, Identifiable {
case success, error
var id: some Hashable { rawValue }
var content: Alert {
switch self {
case .success: return Alert(title: Text("Success!"))
case .error: return Alert(title: Text("Oy, error..."))
}
}
}
Then the rest is the same as Raja's answer:
Add it as a #State variable to your view
#State var resultAlert: ResultAlert?
Activate it using resultAlert = .success or resultAlert = .error. Deactivate it using resultAlert = .none
And present it like this:
.alert(item: $resultAlert, content: \.content)
I have an imessage extension app that works fine except on the first send to a group.
iOS 14.4 multiple devices 8, 8plus, 10...
Xcode 12.4
The code goes straight from the send closure (success) to didResignActive. The app is supposed to stay active. There is no dismiss called anywhere. I've debugged w/ attached device stepped through the code, and it goes straight from the log line to didResignActive.
If I launch the app again on the same group it works fine. The app only fails if I start a new group conversation, then click on the app in tray, and send is called.
note: this only happens to group sends, and only on the first time a group is created.
thisConversation.send(message) { error in
if let error = error {
os_log("submitMessage(%#): initial send error: %#", log: .default, type: .debug, type, error.localizedDescription)
} else {
os_log("submitMessage(%#): initial send success!", log: .default, type: .debug, type)
}
}
Edit:
Just an update...
I was told to file a bug report for this (which I did).
I also supplied a video which shows how to reproduce the bug.
This is the message sent back from support...
Hi,
Thank you contacting Apple Developer Technical Support (DTS).
There is no workaround DTS can provide for Feedback ID #FB9049862; it
is still under investigation. Please continue to track the problem via
the bug report.
...
I was able to create a test app that reproduces this problem, with what I think is the minimal amount of code to demonstrate. Hope this invites a solution!
//
// MessagesViewController.swift
// testGroupSend MessagesExtension
//
import UIKit
import Messages
import os
class MessagesViewController: MSMessagesAppViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
// MARK: - Conversation Handling
func composeSelectionMsg(on conversation: MSConversation, in
session: MSSession) -> MSMessage {
let layout = MSMessageTemplateLayout()
layout.caption = "caption..."
let message = MSMessage(session: session)
message.layout = layout
message.summaryText = "summary text..."
var components = URLComponents()
var queryItems = [URLQueryItem]()
queryItems.append(URLQueryItem(name: "MessageType1", value: "msgType"))
queryItems.append(URLQueryItem(name: "Encode-Name", value: "Encode-Value"))
components.queryItems = queryItems
message.url = components.url!
return message
}
func submitMessage() {
guard let conversation = activeConversation else {
os_log("submitMessage(): guard on conversation falied!", log: .default, type: .debug)
return
}
var session : MSSession
if let tSess = conversation.selectedMessage?.session {
session = tSess
os_log("submitMessage() got a session!...", log: .default, type: .debug)
} else {
os_log("###### submitMessage() did NOT get a session, creating new MSSession() #####", log: .default, type: .debug)
session = MSSession()
}
var message: MSMessage
message = composeSelectionMsg(on: conversation, in: session)
// conversation.insert(message) { error in
conversation.send(message) { error in
if let error = error {
os_log("submitMessage(): initial send error: %#", log: .default, type: .debug, error.localizedDescription)
} else {
os_log("submitMessage(): initial send success!", log: .default, type: .debug)
}
}
}
fileprivate func loadContentView() {
os_log("loadContentView()...", log: .default, type: .debug)
let childViewCtrl = ContentViewHostController()
childViewCtrl.delegate = self
childViewCtrl.view.layoutIfNeeded() // avoids snapshot warning?
if let window = self.view.window {
childViewCtrl.myWindow = window
window.rootViewController = childViewCtrl
}
}
override func willBecomeActive(with conversation: MSConversation) {
loadContentView()
}
override func didResignActive(with conversation: MSConversation) {
os_log("didResignActive()...", log: .default, type: .debug)
}
}
//
//
import SwiftUI
import Messages
import os
final class ContentViewHostController: UIHostingController<ContentView> {
weak var delegate: ContentViewHostControllerDelegate?
weak var myWindow: UIWindow?
init() {
super.init(rootView: ContentView())
rootView.submitMessage = submitMessage
}
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: ContentView())
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func submitMessage() {
os_log("ContentViewHostController::submitMessage(): submit message...", log: .default, type: .debug)
delegate?.contentViewHostControllerSubmitMessage(self)
}
}
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var submitMessage: (() -> Void)?
var body: some View {
VStack {
VStack {
HStack {
Button(action: { self.cancel() } ) { Image(systemName: "chevron.left") }
.padding()
Spacer()
Button(action: { self.submit() } ) {
Text("Send...")
}
.padding()
} // HStack
} // VStack
} // outside most VStack
} // body
private func cancel() {
presentationMode.wrappedValue.dismiss()
}
private func submit() {
submitMessage!()
presentationMode.wrappedValue.dismiss()
}
} // ContentView
//
//
import SwiftUI
import Messages
import os
extension MessagesViewController: ContentViewHostControllerDelegate {
// MARK: - ContenHost delegate
func contentViewHostControllerSubmitMessage(_ controller: ContentViewHostController) {
os_log("delegateSubmitMessage:...")
submitMessage()
}
}
//
//
import SwiftUI
import Messages
protocol ContentViewHostControllerDelegate: class {
func contentViewHostControllerSubmitMessage( _ controller: ContentViewHostController )
}
I've done a ton of searching and read a bunch of articles but I cannot get SwiftUI to dynamically update the view based on changing variables in the model, at least the kind of thing I'm doing. Basically I want to update the view based on the app's UNNotificationSettings.UNAuthorizationStatus. I have the app check the status on launch and display the status. If the status is not determined, then tapping on the text will trigger the request notifications dialog. However, the view doesn't update after the user either permits or denies the notifications. I'm sure I'm missing something fundamental because I've tried it a dozen ways, including with #Published ObservableObject, #ObservedObject, #EnvironmentObject, etc.
struct ContentView: View {
#EnvironmentObject var theViewModel : TestViewModel
var body: some View {
VStack {
Text(verbatim: "Notifications are: \(theViewModel.notificationSettings.authorizationStatus)")
.padding()
}
.onTapGesture {
if theViewModel.notificationSettings.authorizationStatus == .notDetermined {
theViewModel.requestNotificationPermissions()
}
}
}
}
class TestViewModel : ObservableObject {
#Published var notificationSettings : UNNotificationSettings
init() {
notificationSettings = type(of:self).getNotificationSettings()!
}
func requestNotificationPermissions() {
let permissionsToRequest : UNAuthorizationOptions = [.alert, .sound, .carPlay, .announcement, .badge]
UNUserNotificationCenter.current().requestAuthorization(options: permissionsToRequest) { granted, error in
if granted {
print("notification request GRANTED")
}
else {
print("notification request DENIED")
}
if let error = error {
print("Error requesting notifications:\n\(error)")
}
else {
DispatchQueue.main.sync {
self.notificationSettings = type(of:self).getNotificationSettings()!
}
}
}
}
static func getNotificationSettings() -> UNNotificationSettings? {
var settings : UNNotificationSettings?
let start = Date()
let semaphore = DispatchSemaphore(value: 0)
UNUserNotificationCenter.current().getNotificationSettings { notificationSettings in
settings = notificationSettings
semaphore.signal()
}
semaphore.wait()
while settings == nil {
let elapsed = start.distance(to: Date())
Thread.sleep(forTimeInterval: TimeInterval(0.001))
if elapsed > TimeInterval(1) {
print("ERROR: did not get notification settings in less than a second, giving up!")
break
}
}
if settings != nil {
print("\(Date()) Notifications are: \(settings!.authorizationStatus)")
}
return settings
}
}
func getUNAuthorizationStatusString(_ authStatus : UNAuthorizationStatus) -> String {
switch authStatus {
case .notDetermined: return "not determined"
case .denied: return "denied"
case .authorized: return "authorized"
case .provisional: return "provisional"
case .ephemeral: return "ephemeral"
#unknown default: return "unknown case with rawValue \(authStatus.rawValue)"
}
}
extension UNAuthorizationStatus : CustomStringConvertible {
public var description: String {
return getUNAuthorizationStatusString(self)
}
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ authStatus: UNAuthorizationStatus) {
appendLiteral(getUNAuthorizationStatusString(authStatus))
}
}
EDIT: I tried adding objectWillChange but the view still isn't updating.
class TestViewModel : ObservableObject {
let objectWillChange = ObservableObjectPublisher()
#Published var notificationSettings : UNNotificationSettings {
willSet {
objectWillChange.send()
}
}
init() {
notificationSettings = type(of:self).getNotificationSettings()!
}
Per the apple docs the properties wrappers like #Published should hold values. UNNotificationSettings is a reference type. Since the class gets mutated and the pointer never changes, #Publushed has no idea that you changed anything. Either publish a value (it make a struct and init it from he class) or manually send the objectwillChange message manually.
While I was not able to get it to work with manually using objectWillChange, I did create a basic working system as follows. Some functions are not repeated from the question above.
struct TestModel {
var notificationAuthorizationStatus : UNAuthorizationStatus
init() {
notificationAuthorizationStatus = getNotificationSettings()!.authorizationStatus
}
}
class TestViewModel : ObservableObject {
#Published var theModel = TestModel()
func requestAndUpdateNotificationStatus() {
requestNotificationPermissions()
theModel.notificationAuthorizationStatus = getNotificationSettings()!.authorizationStatus
}
}
struct ContentView: View {
#ObservedObject var theViewModel : TestViewModel
var body: some View {
VStack {
Button("Tap to update") {
theViewModel.requestAndUpdateNotificationStatus()
}
.padding()
switch theViewModel.theModel.notificationAuthorizationStatus {
case .notDetermined: Text("Notifications have not been requested yet.")
case .denied: Text("Notifications are denied.")
case .authorized: Text("Notifications are authorized.")
case .provisional: Text("Notifications are provisional.")
case .ephemeral: Text("Notifications are ephemeral.")
#unknown default: Text("Notifications status is an unexpected state.")
}
}
}
}
I am wanting to display the price of the SKProduct item inside my label, rather than it being an alertView, as presented by SwiftyStoreKit.
In the viewDidLoad, I tried
coralsAppLabel.text = getInfo(PurchaseCorals)
but this results in the error that I cannot covert a type () to a UILabel.
This is based on the SwiftyStoreKit code below.
enum RegisteredPurchase : String {
case reefLifeCorals = "ReefLife4Corals"
}
#IBOutlet weak var coralsAppLabel: UILabel!
func getInfo(_ purchase: RegisteredPurchase) {
NetworkActivityIndicatorManager.networkOperationStarted()
SwiftyStoreKit.retrieveProductsInfo([purchase.rawValue]) { result in
NetworkActivityIndicatorManager.networkOperationFinished()
self.showAlert(self.alertForProductRetrievalInfo(result))
}
}
func alertForProductRetrievalInfo(_ result: RetrieveResults) -> UIAlertController {
if let product = result.retrievedProducts.first {
let priceString = product.localizedPrice!
return alertWithTitle(product.localizedTitle, message: "\(product.localizedDescription) - \(priceString)")
}
else if let invalidProductId = result.invalidProductIDs.first {
return alertWithTitle("Could not retrieve product info", message: "Invalid product identifier: \(invalidProductId)")
}
else {
let errorString = result.error?.localizedDescription ?? "Unknown error. Please contact support"
return alertWithTitle("Could not retrieve product info", message: errorString)
}
}
Any help is appreciated
The main problem here is that you're trying to assign Void (aka ()) value that your function getInfo implicitly returns to a String? property of UILabel. That's not going to work.
You can't easily return needed info from getInfo function either because it does asynchronous call. One way to accomplish what you need is to re-factor the code a bit to something like following (didn't check for syntax errors, so be wary):
override func viewDidLoad() {
super.viewDidLoad()
getProductInfoFor(PurchaseCorals, completion: { [weak self] (product, errorMessage) in
guard let product = product else {
self?.coralsAppLabel.text = errorMessage
return
}
let priceString = product.localizedPrice!
self?.coralsAppLabel.text = "\(product.localizedDescription) - \(priceString)"
})
}
func getProductInfoFor(_ purchase: RegisteredPurchase, completion: (product: SKProduct?, errorMessage: String?) -> Void) {
NetworkActivityIndicatorManager.networkOperationStarted()
SwiftyStoreKit.retrieveProductsInfo([purchase.rawValue]) { result in
NetworkActivityIndicatorManager.networkOperationFinished()
let extractedProduct = self.extractProductFromResults(result)
completion(product: extractedProduct.product, errorMessage: extractedProduct.errorMessage)
}
}
func extractProductFromResults(_ result: RetrieveResults) -> (product: SKProduct?, errorMessage: String?) {
if let product = result.retrievedProducts.first {
return (product: product, errorMessage: nil)
}
else if let invalidProductId = result.invalidProductIDs.first {
return (product: nil, errorMessage: "Invalid product identifier: \(invalidProductId)")
}
else {
let errorString = result.error?.localizedDescription ?? "Unknown error. Please contact support"
return (product: nil, errorMessage: errorString)
}
}
Here you have your SKProduct or errorMessage in viewDidLoad in the completion closure and you are free to do whatever you want with it: show alert, update label, etc. And overall this code should be a little bit more flexible and decoupled which is usually a good thing ;)