Observing Firebase Authenfication with ObservableObject - firebase-authentication

I am trying to observe firebase authentification and update my View accordingly.
I have an SessionStore object:
class SessionStore: ObservableObject {
#Published var session: Account?
var handle: AuthStateDidChangeListenerHandle?
deinit {
stopListen()
}
func listen() {
if handle == nil {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
print("User logged in: \(user)")
self.session = Account.preData
} else {
self.session = nil
}
}
}
}
func stopListen() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
}
I use it in a view like this:
struct TabBarView: View {
#EnvironmentObject var sessionStore: SessionStore
#State var selectedTab = Tab.swiping
enum Tab: Int {
case swiping, matches, profil
}
func getUser() {
sessionStore.listen()
}
var body: some View {
Group {
if (sessionStore.session != nil) {
TabView(selection: $selectedTab) {
SwipingView().tabItem {
TabBarItem(text: "Text", image: "pause.circle")
}.tag(Tab.swiping)
}
} else {
LoginView()
}
}.onAppear(perform: getUser).onDisappear(perform: sessionStore.stopListen)
}
}
And call it like this:
sessionStore = SessionStore()
TabBarView().environmentObject(sessionStore!)
But it is only showing the LoginView even when the session is not nil. I made some code changes this is actually the solution.

I think this is the way to do that
class SessionStore: ObservableObject {
#Published var session: Account?
Also you referenced self inside the state closure meaning your object will never deinit. Add unowned or weak like so:
handle = Auth.auth().addStateDidChangeListener { [unowned self] (auth, user) in
https://www.avanderlee.com/swift/weak-self/

The correct implementation is:
class SessionStore: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var session: Account? {
didSet {
objectWillChange.send()
}
}
var handle: AuthStateDidChangeListenerHandle?
deinit {
stopListen()
}
func listen() {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
print("User logged in: \(user)")
self.session = Account.preData
} else {
self.session = nil
}
}
}
func stopListen() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
}
More info about it:
https://www.pointfree.co/blog/posts/30-swiftui-and-state-management-corrections

Related

UICollectionViewCell button callback

How to implement callback for example of button tap inside UICollectionViewCell's UIContentView or better how to do it in Combine way?
Cell registration inside UICollectionView:
let cellRegistration = UICollectionView.CellRegistration<MyCell, Item> { (cell, indexPath, item) in
cell.item = item
}
Cell:
class MyCell: UICollectionViewCell {
var item: Item?
override func updateConfiguration(using state: UICellConfigurationState) {
var newConfiguration = MyContentConfiguration().updated(for: state)
newConfiguration.name = item?.title
contentConfiguration = newConfiguration
}
}
Content configuration:
struct MyContentConfiguration: UIContentConfiguration, Hashable {
var name: String?
func makeContentView() -> UIView & UIContentView {
return MyContentView(configuration: self)
}
}
Content view:
class MyContentView: UIView, UIContentView {
let title = UILabel()
private var currentConfiguration: MyContentConfiguration!
var configuration: UIContentConfiguration {
get {
currentConfiguration
}
set {
guard let newConfiguration = newValue as? MyContentConfiguration else {
return
}
apply(configuration: newConfiguration)
}
}
init(configuration: MyContentConfiguration) {
super.init(frame: .zero)
// Create the content view UI
setupUI()
apply(configuration: configuration)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension MyContentView {
private func setupUI() {
// UI stuff
}
private func apply(configuration: MyContentConfiguration) {
guard currentConfiguration != configuration else {
return
}
currentConfiguration = configuration
// Set data to UI elements
title.text = title
//etc..
}
}
Inside MyContentView there'll be a button for example. This button should be bound with item, so when it's tapped - some callback or Combine publisher should emit value, which I've got to catch inside my collection view.
How I've implemented this previously:
let cellRegistration = UICollectionView.CellRegistration<MyCell, Item> { (cell, indexPath, item) in
cell.item = item
//Catch value which is bound with associated item
cell.somePublisher
.sink { [weak self] in
guard let self = self else { return }
self.subscribePublisher.send($0)
}
.store(in: &self.subscriptions)
}
Cell:
class MyCell: UICollectionViewCell {
var item: Item?
public private(set) var somePublisher = CurrentValueSubject<Bool?, Never>(nil)
//UI setup, etc..
//Emit value for a button tap gesture
#objc func handleTap() {
somePublisher.send(true)
}
}
Is it possible to implement this behavior using UIContentView & UIContentConfiguration?

Swift: Can't change class attribute from #objc function

I'm trying to capture ProximitySensor activity on SwiftUI.
So I've created a class ProximityOberver and trying to update the attribute 'state' in the notification:
import SwiftUI
import UIKit
class ProximityObserver {
#State var state = false;
#objc func didChange(notification: NSNotification) {
print("MyView::ProximityObserver.didChange")
if let device = notification.object as? UIDevice {
print(device.proximityState)
state = device.proximityState
print(state)
}
}
}
struct ContentView: View {
#State var proximityObserver = ProximityObserver()
func activateProximitySensor() {
print("MyView::activateProximitySensor")
if !UIDevice.current.isProximityMonitoringEnabled {
UIDevice.current.isProximityMonitoringEnabled = true
if UIDevice.current.isProximityMonitoringEnabled {
NotificationCenter.default.addObserver(proximityObserver, selector: #selector(proximityObserver.didChange), name: UIDevice.proximityStateDidChangeNotification, object: UIDevice.current)
}
}
}
func deactivateProximitySensor() {
print("MyView::deactivateProximitySensor")
UIDevice.current.isProximityMonitoringEnabled = false
NotificationCenter.default.removeObserver(proximityObserver, name: UIDevice.proximityStateDidChangeNotification, object: UIDevice.current)
}
var body: some View {
Text(proximityObserver.state ? "true" : "false" )
.animation(.linear(duration: 20).delay(20), value: proximityObserver.state)
.onAppear() {
self.activateProximitySensor()
}
.onDisappear() {
self.deactivateProximitySensor()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
But even 'state = device.proximityState' code executed, the following print(state) shows the attribute never changed.
MyView::ProximityObserver.didChange
true
false
Can someone explain why this happens, and how to fix this?
Thank you for the comment.
I could fix this as suggested.
class ProximityObserver: ObservableObject {
#Published var state = false;
#objc func didChange(notification: NSNotification) {
print("MyView::ProximityObserver.didChange")
if let device = notification.object as? UIDevice {
print(device.proximityState)
self.state = device.proximityState
print(state, device.proximityState)
}
}
}
struct ContentView: View {
#ObservedObject var proximityObserver = ProximityObserver()
...

How to make API call in swiftUI

I am trying to use PokeApi to make a Pokedex app. I just started swift a couple of days ago so I'm following a tutorial here: https://www.youtube.com/watch?v=UsO-84Xnhww. The tutorial doesn't seem to work, and I don't know how to access the PokeAPI in order to make this app. My code is posted below:
ContentView:
import SwiftUI
struct ContentView: View {
#State var searchText = ""
var pokemon = [Pokemon]()
var body: some View {
NavigationView {
List{
ForEach(searchText == "" ? pokemon : pokemon.filter({
$0.name.contains(searchText.lowercased())
})) { entry in
HStack {
Circle() //Pokemon Image
NavigationLink("\(entry.name)".capitalized, destination: Text("Detail view for \(entry.name)"))
}
}
}
.onAppear {
PokemonManager().getData() { pokemon in self.pokemon = pokemon
for pokemon in pokemon {
print(pokemon.name)
}
}
}
.searchable(text: $searchText)
.navigationTitle("PokePass")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PokemonAPI:
import Foundation
struct CurrentPokemon: Codable {
var results: [Pokemon]
}
struct Pokemon: Codable, Identifiable {
var id = UUID()
var name: String
var url: String
}
class PokemonManager {
func getData(completion: #escaping ([Pokemon]) -> ()) {
guard let url = URL(string: "https://pokeapi.co/api/v2/pokemon?limit=151") else {
return
}
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
let pokemonList = try! JSONDecoder().decode(CurrentPokemon.self, from: data)
DispatchQueue.main.async {
completion(pokemonList.results)
}
}
.resume()
}
}
Use let id = UUID() in Pokemon, this will avoid decoding it, and that is what you want, since id is not part of the data.
You can also use this approach:
struct Pokemon: Codable, Identifiable {
var id = UUID()
var name: String
var url: String
enum CodingKeys: String, CodingKey {
case name, url
}
}
EDIT-1
and use #State var pokemon = [Pokemon]() in ContentView

Is it possible to disable "funk" error sound on global keyboard events in SwiftUI (macOS)?

I've started a SwiftUI project (it is a macOS tray application) that relies on global keyboard events (even when my application is minimized). Specifically i care about the F3 and F4 keys. While the keyboard events are registered correctly and my application is fully functional it is always playing that error "funk" sound when a key is pressed. Does anyone know how to fix this?
MyApp.swift
import SwiftUI
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var delegate;
var body: some Scene {
Settings {
ContentView()
}
}
}
class AppDelegate: NSObject,NSApplicationDelegate {
var statusItem: NSStatusItem!
var popOver: NSPopover!
func applicationDidFinishLaunching(_ notification: Notification){
let contentView = ContentView()
let popOver = NSPopover();
popOver.behavior = .transient
popOver.animates = true
popOver.contentViewController = NSHostingController(rootView: contentView)
popOver.setValue(true, forKeyPath: "shouldHideAnchor")
self.popOver = popOver
self.statusItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String : true]
let accessEnabled = AXIsProcessTrustedWithOptions(options)
if !accessEnabled {
print("Access Not Enabled")
}
// Here is where the global keypress event is registered
NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { (event) in
if (event.keyCode == 99) {
// do smth
}else if (event.keyCode == 118) {
// do smth else
}
}
}
if let MenuButton = self.statusItem.button {
MenuButton.image = NSImage(systemSymbolName: "display.2", accessibilityDescription: nil)
MenuButton.action = #selector(MenuButtonToggle)
}
}
#objc func MenuButtonToggle(_ sender: AnyObject){
if let button = self.statusItem.button {
if self.popOver.isShown{
self.popOver.performClose(sender)
}else {
self.popOver.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
self.popOver.contentViewController?.view.window?.makeKey()
}
}
}
}
It looks like you can achieve this by assigning the keys through the view directly at least, perhaps this can work on your AppDelegate also but you need to override.
Here is a working example:
struct KeyEventHandling: NSViewRepresentable {
class KeyView: NSView {
func isManagedByThisView(_ event: NSEvent) -> Bool {
//...
return true
}
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
if isManagedByThisView(event) {
print(">> key \(event.keyCode)")
} else {
super.keyDown(with: event)
}
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
DispatchQueue.main.async { // wait till next event cycle
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
KeyEventHandling()
}
}
According to Documentation: "When you call super.keyDown(with: event), the event goes up through the responder chain and if no other responders process it, causes beep sound." Good Luck!
struct DisableBeepsView: NSViewRepresentable {
class KeyView: NSView {
func isManagedByThisView(_ event: NSEvent) -> Bool {
return true
}
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
if isManagedByThisView(event) {
// print(">> key \(event.keyCode)")
} else {
super.keyDown(with: event)
}
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
DispatchQueue.main.async { // wait till next event cycle
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
just locate DisableBeepsView() inside your View where you need to disable beep sound

How to check if #EnvironmentObject is set? (SwiftUI)

What I have: View and ViewModel (as extension to View struct).
Both of them are use #EnvironmentObject of type AppState.
The problem is that my preview crashes due to this error:
Fatal error: No ObservableObject of type AppState found.
Commenting out lines in loadUser func saves from crash.
struct ProfileView: View {
#EnvironmentObject var appState: AppState
#ObservedObject var viewModel = ViewModel()
...
}
extension ProfileView {
class ViewModel: ObservableObject {
#EnvironmentObject var appState: AppState
#Published var userVM = UserVM(.example)
init() {
loadUser()
}
func loadUser() {
User.WebService.getSelf { user, errorMsg in
DispatchQueue.main.async {
guard let user = user else {
/*self.appState.showingAlert = true
self.appState.alert = Alert(
title: Text("An error occured!"),
message: Text(errorMsg ?? "unknown error"))*/
return
}
self.userVM = UserVM(user)
}
}
}
}
}
struct ProfileView_Previews: PreviewProvider {
static let viewModel = ProfileView.ViewModel()
static var previews: some View {
let appState = AppState()
appState.activeScreen = .profile
return ProfileView()
.environmentObject(appState)
}
}