toggle to get notified in swiftui - xcode

I want to be able to notify the user of my app every day at a specific time. in this example, the time is noon
import SwiftUI
import UserNotifications
struct Alert: View {
#State var noon = false
func noonNotify() {
let content = UNMutableNotificationContent()
content.title = "Meds"
content.subtitle = "Take your meds"
content.sound = UNNotificationSound.default
var dateComponents = DateComponents()
dateComponents.hour = 14
dateComponents.minute = 38
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// add our notification request
UNUserNotificationCenter.current().add(request)
}
var body: some View {
VStack {
Toggle(isOn: $noon) {
Text("ThirdHour")
}
if noon {
noonNotify()
}
Button("Request Permission") {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("All set!")
} else if let error = error {
print(error.localizedDescription)
}
}
}
}
}
}
I created a func and when the toggle is true, the func will execute, but when it's false, then it won't. however, when I create an if statement, I get an error
Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
can someone explain what I'm doing wrong?

You can't call a function like that. Everything inside var body: some View { has to be a View, and noonNotify() doesn't return a View.
Instead, add an onChange block, which will get triggered whenever noon changes.
Toggle(isOn: $noon) {
Text("ThirdHour")
}
.onChange(of: noon) { newValue in
if newValue {
noonNotify()
}
}

Related

SwiftUI Delete a List Section

Is there a way to delete an entire list section with a function in SwiftUI? I have a list that you can add sections to as you please, but don't know how to delete them when you want to. I know that you can delete a list item itself using the deleteItems function, but I would like to use a button to delete the entire section. When I try to use the standard "deleteItems" function, it asks for "offsets: IndexSet". I am unsure what this IndexSet would be.
I can't find out what should go in the IndexSet field.
Here is my code
struct SampleView: View {
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.presentationMode) var presentationMode
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Category.categoryString, ascending: false)],
animation: .default)
private var listedCategory: FetchedResults<Category>
#State var categoryString: String = ""
var body: some View {
VStack{
TextField("Enter Text Here", text: $categoryString)
Button {
let newlistedCategory = Category(context: viewContext)
newlistedCategory.categoryString = categoryString
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
categoryString = ""
} label: {
Text("Save")
}
List{
ForEach(listedCategory) { listedCategory in
if listedCategory.typeString == "Item" {
Section(header: Text(listedCategory.categoryString!)) {
Button {
//deleteCategories(offsets: <#T##IndexSet#>)
} label: {
Text("Delete")
}
}
}
}
}
Spacer()
}
}
private func deleteCategories(offsets: IndexSet) {
withAnimation {
offsets.map { listedCategory[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}

UNNotificationRequest only triggers once after device reboot

Local Notification only triggers once after device reboot. when i close and reopen the app it won't trigger even though i see it enters the function send() every time i run the app.
i have the trigger set to nil so it will trigger Imm . any ideas what am i getting wrong ?
Thank you so much
O.K
import SwiftUI
import UserNotifications
#main
struct ozApp: App {
var body: some Scene {
WindowGroup {
ContentView().onAppear { send() }
}
}
func send(){
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("All set!")
} else if let error = error {
print(error.localizedDescription)
}
}
let content = UNMutableNotificationContent()
content.title = "Message"
content.subtitle = "999"
content.sound = UNNotificationSound.default
let uuid = UUID().uuidString
let request = UNNotificationRequest(identifier: uuid , content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
}

How do I wait for a Firebase function to complete before continuing my code flow in SwiftUI?

I have a Firebase function in a class (listenToUser) that works fine but I noticed that the next code (the IF ELSE) does not wait for it to complete before continuing. How can I wait for my function to be completed before continuing my code?
Portion of code of my main view :
...
#EnvironmentObject var firebaseSession: FirebaseSession_VM
...
.onAppear {
firebaseSession.listenToUser()
if firebaseSession.firebaseUser == nil {
showSignInView = true
} else {
showSignInStep1View = true
}
}
My function :
import SwiftUI
import Combine
import FirebaseAuth
class FirebaseSession_VM: ObservableObject {
static let instance = FirebaseSession_VM()
var didChange = PassthroughSubject<FirebaseSession_VM, Never>()
#Published var firebaseUser: FirebaseUser_M? {
didSet {
self.didChange.send(self)
}
}
var handle: AuthStateDidChangeListenerHandle?
func listenToUser () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.firebaseUser = FirebaseUser_M(
id: user.uid,
email: user.email
)
} else {
self.firebaseUser = nil
}
}
}
}
Most of Firebase's API calls are asynchronous, which is why you need to either register a state listener or use callbacks.
Two side notes:
You should not implement ObservableObjects as singletons. Use #StateObject instead, to make sure SwiftUI can properly manage its state.
You no longer need to use PassthroughSubject directly. It's easier to use the #Published property wrapper instead.
That being said, here are a couple of code snippets that show how you can implement Email/Password Authentication with SwiftUI:
Main View
The main view shows if you're sign in. If you're not signed in, it will display a button that will open a separate sign in screen.
import SwiftUI
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("👋🏻 Hello!")
.font(.title3)
switch viewModel.isSignedIn {
case true:
VStack {
Text("You're signed in.")
Button("Tap here to sign out") {
viewModel.signOut()
}
}
default:
VStack {
Text("It looks like you're not signed in.")
Button("Tap here to sign in") {
viewModel.signIn()
}
}
}
}
.sheet(isPresented: $viewModel.isShowingLogInView) {
SignInView()
}
}
}
The main view's view model listens for any auth state changes and updates the isSignedIn property accordingly. This drives the ContentView and what it displays.
import Foundation
import Firebase
class ContentViewModel: ObservableObject {
#Published var isSignedIn = false
#Published var isShowingLogInView = false
init() {
// listen for auth state change and set isSignedIn property accordingly
Auth.auth().addStateDidChangeListener { auth, user in
if let user = user {
print("Signed in as user \(user.uid).")
self.isSignedIn = true
}
else {
self.isSignedIn = false
}
}
}
/// Show the sign in screen
func signIn() {
isShowingLogInView = true
}
/// Sign the user out
func signOut() {
do {
try Auth.auth().signOut()
}
catch {
print("Error while trying to sign out: \(error)")
}
}
}
SignIn View
The SignInView shows a simple email/password form with a button. The interesting thing to note here is that it listens for any changes to the viewModel.isSignedIn property, and calls the dismiss action (which it pulls from the environment). Another option would be to implement a callback as a trailing closure on the view model's signIn() method.
struct SignInView: View {
#Environment(\.dismiss) var dismiss
#StateObject var viewModel = SignInViewModel()
var body: some View {
VStack {
Text("Hi!")
.font(.largeTitle)
Text("Please sign in.")
.font(.title3)
Group {
TextField("Email", text: $viewModel.email)
.disableAutocorrection(true)
.autocapitalization(.none)
SecureField("Password", text: $viewModel.password)
}
.padding()
.background(Color(UIColor.systemFill))
.cornerRadius(8.0)
.padding(.bottom, 8)
Button("Sign in") {
viewModel.signIn()
}
.foregroundColor(Color(UIColor.systemGray6))
.padding(.vertical, 16)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.accentColor)
.cornerRadius(8)
}
.padding()
.onChange(of: viewModel.isSignedIn) { signedIn in
dismiss()
}
}
}
The SignInViewModel has a method signIn that performs the actual sign in process by calling Auth.auth().signIn(withEmail:password:). As you can see, it will change the view model's isSignedIn property to true if the user was authenticated.
import Foundation
import FirebaseAuth
class SignInViewModel: ObservableObject {
#Published var email: String = ""
#Published var password: String = ""
#Published var isSignedIn: Bool = false
func signIn() {
Auth.auth().signIn(withEmail: email, password: password) { authDataResult, error in
if let error = error {
print("There was an issue when trying to sign in: \(error)")
return
}
guard let user = authDataResult?.user else {
print("No user")
return
}
print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
self.isSignedIn = true
}
}
}
Alternative: Using Combine
import Foundation
import FirebaseAuth
import FirebaseAuthCombineSwift
class SignInViewModel: ObservableObject {
#Published var email: String = ""
#Published var password: String = ""
#Published var isSignedIn: Bool = false
// ...
func signIn() {
Auth.auth().signIn(withEmail: email, password: password)
.map { $0.user }
.replaceError(with: nil)
.print("User signed in")
.map { $0 != nil }
.assign(to: &$isSignedIn)
}
}
Alternative: Using async/await
import Foundation
import FirebaseAuth
class SignInViewModel: ObservableObject {
#Published var email: String = ""
#Published var password: String = ""
#Published var isSignedIn: Bool = false
#MainActor
func signIn() async {
do {
let authDataResult = try 3 await 1 Auth.auth().signIn(withEmail: email, password: password)
let user = authDataResult.user
print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
self.isSignedIn = true
}
catch {
print("There was an issue when trying to sign in: \(error)")
self.errorMessage = error.localizedDescription
}
}
}
More details
I wrote an article about this in which I explain the individual techniques in more detail: Calling asynchronous Firebase APIs from Swift - Callbacks, Combine, and async/await. If you'd rather watch a video, I've got you covered as well: 3 easy tips for calling async APIs

Instagram Tutorial SwiftUI Firebase not loading Timeline

I'm new to coding and have been trying to go through instagram tutorials to understand some concepts. Since updating to Xcode 12, my Firebase has seemed to not work anymore and is not showing on the home feed.
I placed a rectangle in to see if it was the if !homeViewModel.isLoading was the cause it appears to be so.
Here is my current code:
import SwiftUI
import URLImage
import Firebase
struct HomeView: View {
#ObservedObject var homeViewModel = HomeViewModel()
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
Story()
Rectangle().frame(width: 200, height: 200).foregroundColor(.red)
if !homeViewModel.isLoading {
ForEach(self.homeViewModel.posts, id: \.postId) { post in
VStack(alignment: .center) {
HeaderCell(post: post)
FooterCell(post: post)
}.background(Color.white).cornerRadius(10)
.padding(.leading, 10).padding(.trailing, 10)
}
}
}.background(Color.gray)
HomeViewModel:
import Foundation
import SwiftUI
import Firebase
class HomeViewModel: ObservableObject {
#Published var posts: [Post] = []
#Published var isLoading = false
var listener: ListenerRegistration!
// init() {
// loadTimeline()
// }
func loadTimeline() {
self.posts = []
isLoading = true
Api.Post.loadTimeline(onSuccess: { (posts) in
self.isLoading = false
if self.posts.isEmpty {
self.posts = posts
}
}, newPost: { (post) in
if !self.posts.isEmpty {
self.posts.insert(post, at: 0)
}
}) { (listener) in
self.listener = listener
}
}
}
LoadTimeline func:
func loadTimeline(onSuccess: #escaping(_ posts: [Post]) -> Void, newPost: #escaping(Post) -> Void, listener: #escaping(_ listenerHandle: ListenerRegistration) -> Void) {
guard let userId = Auth.auth().currentUser?.uid else {
return
}
let listenerFirestore = Ref.FIRESTORE_TIMELINE_DOCUMENT_USERID(userId: userId).collection("timelinePosts").order(by: "date", descending: true).addSnapshotListener({ (querySnapshot, error) in
guard let snapshot = querySnapshot else {
return
}
var posts = [Post]()
snapshot.documentChanges.forEach { (documentChange) in
switch documentChange.type {
case .added:
print("type: added")
let dict = documentChange.document.data()
guard let decoderPost = try? Post.init(fromDictionary: dict) else {return}
newPost(decoderPost)
posts.append(decoderPost)
case .modified:
print("type: modified")
case .removed:
print("type: removed")
}
}
onSuccess(posts)
})
listener(listenerFirestore)
}
For some reason it seems as though the function isn't being triggered and timeline isn't loading. unsure why though... Prior to the update it was working fine?
Any help would be much appreciated!

Update SwiftUI List after network request using RxSwift

I need to update a SwiftUI List after making a network request. For requests, I use Moya approach with combination of triggers(Input&Output - "Kickstarter").
I cant use Combine framework due to the structure of the project, while they have a lot of helpful advises(not sure about my case).
Simple ContactList:
struct ContactList: View {
var viewModel: UserViewModel
var body: some View {
NavigationView {
List(viewModel.users) { contact in
NavigationLink(destination: ContactDetail(user: contact)) {
ContactRow(user: contact)
}
}
.navigationBarTitle(Text("Team Members"))
}
}
}
Then ViewModel
class UserViewModel {
let disposeBag = DisposeBag()
var users: [TeamMember] = []
init(users: [TeamMember] = []) {
let networkModel = UserNetworkModel()
networkModel.output.teamMembers.subscribe { (event) in
self.users.append(contentsOf: event.element.orEmpty)
}.disposed(by: disposeBag)
networkModel.output.error.subscribe(onNext: { error in
print(error.localizedDescription)
}).disposed(by: disposeBag)
networkModel.input.loadTrigger.onNext(Void())
self.users = users
}
}
And NetworkModel
class UserNetworkModel {
let disposeBag = DisposeBag()
let input: Input
let output: Output
struct Input {
let loadTrigger: AnyObserver<Void>
let searchTrigger: AnyObserver<String>
}
struct Output {
let teamMembers: Observable<[TeamMember]>
let error: Observable<Error>
}
internal let loadSubject = PublishSubject<Void>()
internal let searchSubject = BehaviorSubject<String>(value: "")
internal let errorSubject = PublishSubject<Error>()
internal let teamMembersSubject = BehaviorSubject<[TeamMember]>(value: [])
init() {
let service = MoyaProvider<TeamTarget>()
self.input = Input(loadTrigger: loadSubject.asObserver(), searchTrigger: searchSubject.asObserver())
self.output = Output(teamMembers: teamMembersSubject.asObservable(), error: errorSubject.asObservable())
let result = loadSubject.flatMapLatest { _ -> Observable<[TeamMember]> in
service.rx.request(.get).debug().mapArray(TeamMember.self).asObservable()
}.share(replay: 1)
Observable.combineLatest(result, searchSubject).map { (arg) in
let (members, filter) = arg
if filter.isEmpty {
return members
} else {
let searchText = try! self.searchSubject.value()
return members.filter({
return [$0.firstName, $0.lastName]
.compactMap({ $0 })
.first(where: { $0.hasPrefix(searchText) }) != nil
})
}
}.bind(to: teamMembersSubject).disposed(by: disposeBag)
result.subscribe(onError: { error in
self.errorSubject.onNext(error)
}).disposed(by: self.disposeBag)
}
}
Is it possible to update users array in this way? Or only Combine can do it for me easily?
Thanks for your time.
While I've found some Combine solution. Still waiting for Rx solution.
So, I fixed it just added #ObservedObject for ContactList property and #Published for users' array in Network model. Much less code, natively but not what I was looking for.
Full answer:
struct ContactList: View {
#ObservedObject var networkModel: NetworkModel
var body: some View {
NavigationView {
List(networkModel.users) { contact in
NavigationLink(destination: ContactDetail(user: contact)) {
ContactRow(user: contact)
}
}
.navigationBarTitle(Text("Team Members"))
}
}
}
#if DEBUG
struct ContactList_Previews: PreviewProvider {
static var previews: some View {
ContactList(networkModel: .init(users: contactData))
}
}
#endif
class NetworkModel: ObservableObject {
#Published var users = [User]()
init(users: [User] = []) {
getMembers()
}
private func getMembers() {
let provider = MoyaProvider<TeamTarget>()
provider.request(.get) { [weak self] (result) in
switch result {
case .success(let response):
do {
let result = try JSONDecoder().decode([User].self, from: response.data)
self?.users.append(contentsOf: result)
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.errorDescription.orEmpty)
}
}
}
}

Resources