How to call a method when a Binding changes using UIKit objects wrapped un UIViewRepresentable? - data-binding

Here's the top of the view hierarchy. There are two objects SUIScrollHistory which is a UIViewControllerRepresentable wrapper around a UIKit PageViewController. And there is a DateField which is a UIViewRepresentable wrapper around a UIKit TextField that has a custom inputView. Both those objects have #Binding var date: Date. The binding is only updating inside the UITextField code and does not propagate outside of the UIViewRepresentable and UIViewController representable. I need to call a method on both objects when the Bindind<Date> changes. Any suggestions?
import SwiftUI
struct TestUI: View
{
#State var date: Date = Date() {
didSet {
print("Tabs date didSet")
}
}
var body: some View {
NavigationView {
SUIScrollHistory(date: $date)
.edgesIgnoringSafeArea(.top)
.navigationBarTitle("History")
.navigationBarItems(trailing: DateField(date: $date))
}
}
}
Here's the ScrollView wrapped in UIViewControllerRepresentable.
import UIKit
import SwiftUI
import CoreData
final class SUIScrollHistory: UIViewControllerRepresentable
{
#Binding var date: Date {
willSet {
print("SUI scroll history willSet:")
print("scroll to appropriate pg")
print("NOT CALLED")
}
}
init(date: Binding<Date>) {
self._date = date
}
func makeUIViewController(context: Context) -> ScrollHistory {
return ScrollHistory(date: _date)
}
func updateUIViewController(_ uiViewController: ScrollHistory, context: Context) {
//test
}
}
import UIKit
import SwiftUI
import CoreData
class ScrollHistory: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource
{
#Binding var date: Date {
willSet {
scrollTo(newDate: newValue)
print("scroll history willSet")
print("scroll to appropriate pg")
print("NOT CALLED")
}
}
init(date: Binding<Date>) {
self._date = date
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
self.setViewControllers( [ UIHostingController(rootView: Rectangle().overlay(Color.blue)) ], direction: .forward, animated: true, completion: nil)
self.delegate = self
self.dataSource = self
self.view.isUserInteractionEnabled = true
self.title = "Mon, Aug 17"
}
required init?(coder: NSCoder) { fatalError() }
func scrollTo(newDate: Date) {
if newDate > date { //Slide the PageVC left if date > currentDay
//let nextDay = date.nextDay()
let edibleJournalTVC = UIHostingController(rootView: Text("inject date"))
self.setViewControllers([edibleJournalTVC], direction: .forward, animated: true, completion: nil)
}
if newDate < date { //Slide the PageVC right if date < currentDay
//let previousDay = date.previousDay()
let edibleJournalTVC = UIHostingController(rootView: Text("inject date")) //self.fdPg(date: date) //FIXME: = FoodPage(date: previousDay)
self.setViewControllers([edibleJournalTVC], direction: .reverse, animated: true, completion: nil)
}
self.date = newDate //TODO: Does this loop infinitely?
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
//update DateField
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let foodPage = UIHostingController(rootView: Text("inject date")) //FIXME: Binding<Date>(previousDay)))
return foodPage
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let foodPage = UIHostingController(rootView: Text("inject date")) //FIXME nextDay))
return foodPage
}
}
Here's the DateField which is wrapped in UIViewRepresentable.
import SwiftUI
import UIKit
struct DateField: UIViewRepresentable
{
#Binding var date: Date {
willSet {
print("DateField willSet")
print("NOT CALLED")
}
didSet {
print("DateField didSet")
print("NOT CALLED")
}
}
private let format: DateFormatter
init(date: Binding<Date>, format: DateFormatter = YMDFormat()) {
self._date = date
self.format = format
}
func makeUIView(context: UIViewRepresentableContext<DateField>) -> UITextField {
let tf = DateTextField(date: $date) //FIXME: not injected bcs it's a wrapper
print("makeUIView")
return tf
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<DateField>) {
print("updateUIView")
uiView.text = format.string(from: date)
}
}
import SwiftUI
import UIKit
class DateTextField: UITextField
{
#Binding var date: Date {
willSet {
print("DateTextField Date")
print("Is Called")
}
}
private lazy var pickDate: UIDatePicker = { return UIDatePicker() }()
init(date: Binding<Date>) {
self._date = date
super.init(frame: .zero)
self.pickDate.date = date.wrappedValue
self.pickDate.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged)
self.inputView = pickDate
self.textColor = .black
}
required init?(coder: NSCoder) { fatalError() }
#objc func dateChanged(_ sender: UIDatePicker) {
self.date = sender.date
}
}
import Foundation
class YMDFormat: DateFormatter
{
override init() {
super.init()
dateFormat = "yyyy-MM-dd"
}
required init?(coder: NSCoder) { fatalError() }
}

Related

Every time view is seen, the name keeps on adding to itself

I know that this sounds a bit dumb, but how do you call a function only once. I have a tab bar at the bottom of my app, and every time that it is called, the name that I got from my firebase database, keeps on being added. For example, the name in firebase is Bob. The app for the first time will display Bob. Then you would click on the settings, and go back to the home view. Then the app will say BobBob, and over and over again. How do I make this stop.
Code:
import SwiftUI
import Firebase
struct HomeView: View {
#State var name = ""
var body: some View {
NavigationView {
ZStack {
VStack{
Text("Welcome \(name)")
.font(.title)
Text("Upcoming Lessions/Reservations:")
.bold()
.padding()
Divider()
}
}
}
.navigationTitle("Home")
.onAppear(perform: {
downloadNameServerData()
})
}
private func downloadNameServerData() {
let db = Firestore.firestore()
db.collection("users").addSnapshotListener {(snap, err) in
if err != nil{
print("\(String(describing: err))")
return
}
for i in snap!.documentChanges {
_ = i.document.documentID
if let Name = i.document.get("Name") as? String {
DispatchQueue.main.async {
name.append(Name)
print("\(name)")
}
}
}
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
import SwiftUI
import Firebase
struct HomeView: View {
#State var name = ""
var body: some View {
NavigationView {
ZStack {
VStack{
Text("Welcome \(name)")
.font(.title)
Text("Upcoming Lessions/Reservations:")
.bold()
.padding()
Divider()
}
}
}
.navigationTitle("Home")
.onAppear(perform: {
downloadNameServerData()
})
}
private func downloadNameServerData() {
if !name.isEmpty { return }
let db = Firestore.firestore()
db.collection("users").addSnapshotListener {(snap, err) in
if err != nil{
print("\(String(describing: err))")
return
}
for i in snap!.documentChanges {
_ = i.document.documentID
if let Name = i.document.get("Name") as? String {
DispatchQueue.main.async {
name = Name
print("\(name)")
}
}
}
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
Did you consider only loading the name if you don't have one yet?
.onAppear(perform: {
if (name == null) downloadNameServerData()
})

Update toggle SwiftUI toggle switch with Firebase

Despite the simplicity of the question I have not been able to find a satisfactory answer yet. I want to update toggle switches based on value in Firebase. I have added listeners but run into problems converting a Bool to Binding Bool, any help is appreciated.
struct oneSeqeuncer : Identifiable{
var id: String
var status: Bool
}
struct Sequencers: View {
#ObservedObject var seqModel = SequencerModel()
#State private var novaseq404A :Bool = true
#State private var novaseq404B :Bool = true
#State private var novaseq297A :Bool = true
#State private var novaseq297B :Bool = true
var body: some View {
ZStack{
VStack{
Text("Sequencers")
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
.padding()
List{
HStack{
Text("404")
.font(.title)
.padding()
Toggle("", isOn: $novaseq404A)
.onChange(of: novaseq404A) { newValue in
updateStatus(name: "404A", status: novaseq404A)
}
Toggle("", isOn: $novaseq404B)
.padding()
.onChange(of: novaseq404B) { newValue in
updateStatus(name: "404B", status: novaseq404B)
}
}
HStack{
Text("297")
.font(.title)
.padding()
Toggle("", isOn: $novaseq297A)
.onChange(of: novaseq297A) { newValue in
updateStatus(name: "297A", status: novaseq297A)
}
Toggle("", isOn: $novaseq297B)
.padding()
.onChange(of: novaseq297B) { newValue in
updateStatus(name: "297B", status: novaseq297B)
}
}
}
}
}.onAppear(){
self.seqModel.fetchData()
for seq in seqModel.seqs{
if seq.id == "404A"{
novaseq404A = seq.status
}
if seq.id == "404B"{
novaseq404A = seq.status
}
if seq.id == "297A"{
novaseq297A = seq.status
}
if seq.id == "297B"{
novaseq297B = seq.status
}
}
func updateStatus(name: String, status: Bool){
let timeInterval = NSDate().timeIntervalSince1970
let myInt = Int(timeInterval)
let db = Firestore.firestore()
if status == false{
db.collection("Sequencers").document(name).updateData([
"status": false,
"lastChange" : myInt
]){ error in
if error != nil{
print(error!)
}
}
}
else{
let docRef = db.collection("Sequencers").document(name)
docRef.getDocument {(document, error) in
if error != nil {
print(error!)
}
if let document = document, document.exists{
let data = document.data()
if let lastChange = data!["lastChange"]! as? Int{
let timeOff = myInt - lastChange
if let timeOffTotal = data!["timeOff"]! as? Int{
let newTimeOff = timeOffTotal + timeOff
db.collection("Sequencers").document(name).updateData([
"timeOff" : newTimeOff
])
}
}
db.collection("Sequencers").document(name).updateData([
"previousChange": data!["lastChange"]!,
"status": true ,
"lastChange" : myInt
])
}
}
}
}
}
struct Sequencers_Previews: PreviewProvider {
static var previews: some View {
Sequencers()
}
}
Below is my model for storing 'sequencers'
import Foundation
import FirebaseFirestore
import Firebase
class SequencerModel : ObservableObject {
#Published var seqs = [oneSeqeuncer]()
private var db = Firestore.firestore()
func fetchData(){
db.collection("Sequencers").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.seqs = documents.map { queryDocumentSnapshot -> oneSeqeuncer in
let data = queryDocumentSnapshot.data()
let id = queryDocumentSnapshot.documentID
let status = data["status"] as? Bool
print(id)
print(status as Any)
return oneSeqeuncer(id: id, status: status!)
}
}
}
}
My solution was not ideal but solved, I realized the function .fetchData() that I was calling to was taking too long to respond. Ideally I should use some completion handler... However I simply changed my TabView on ContentView to display another page first, to allow time for my call to Firebase to finish, which allowed my for loop in .onAppear to have a non empty iterable. Again not sure this really belongs as an "Answer" but just wanted to share my temp solution as an option.

finding a nil value in session when accessing value in view

So i have a sessionStore:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userInSession: User?
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({(auth, user) in
if let user = user {
let firestoreGetUser = Firestore.firestore().collection("users").document(user.uid)
firestoreGetUser.getDocument{(document, error) in
if let dict = document?.data() {
guard let decodedUser = try? User.init(fromDictionary: dict) else { return }
self.userInSession = decodedUser
print("decoded user = \(decodedUser)")
}
}
self.isLoggedIn = true
print("user logged in")
} else {
self.isLoggedIn = false
self.userInSession = nil
print("no one logged in")
}
})
}
func logout() {
do {
try Auth.auth().signOut()
} catch {
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
unbind()
}
}
Its working as expected, I am able to sign in etc.
I have the following to pull the current user data:
import Foundation
import Firebase
import FirebaseAuth
import FirebaseFirestore
class ProfileViewModel: ObservableObject {
var uid: String = ""
var email: String = ""
var username: String = ""
var profileURL: String = ""
var bio: String = ""
var occupation: String = ""
var city: String = ""
func LoadAUser(userId: String) {
Firestore.firestore().collection("users").document(userId).getDocument{(snapshot, error) in
guard let snap = snapshot else {
print("error fetching data")
return
}
let dict = snap.data()
guard let decodedUser = try? User.init(fromDictionary: dict!) else { return }
print("decoded user - load user - \(decodedUser)")
}
}
}
In my view im trying to call it like:
import SwiftUI
struct ProfileView: View {
#EnvironmentObject var session: SessionStore
#ObservedObject var profileViewModel = ProfileViewModel()
func loadUserData() {
profileViewModel.LoadAUser(userId: session.userInSession!.uid)
}
var body: some View {
VStack {
Text("Edit Profile")
.fontWeight(.semibold)
.font(.system(.title, design: .rounded))
.foregroundColor(Color("startColor"))
Spacer()
VStack(alignment: .leading) {
Text("view")
}.padding()
.onAppear(perform: loadUserData)
}
}
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView()
}
}
Im using .onAppear(perform: loadUserData) which is causing an issue - Thread1: Fatal error: Unexpectedly found nil while unwrapping
I also tried:
init() {
profileViewModel.LoadAUser(userId: session.userInSession!.uid)
}
But this also causes the same error.
The thing is I should only be able to get to this view if I'm logged in as this already works:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
Group {
if session.isLoggedIn {
MainView()
} else {
NavigationView {
SignUpView()
}
}
}.onAppear(perform: listen)
}
}
I have an initialView()
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
Group {
if session.isLoggedIn {
MainView()
} else {
NavigationView {
SignUpView()
}
}
}.onAppear(perform: listen)
}
}
which takes you to the MainView() which has tabs to control which screen you can navigate to, then from here i can go to ProfileView()
Anyway by the logic of provided code it is more correct to activate isLoggedIn in
let firestoreGetUser = Firestore.firestore().collection("users").document(user.uid)
firestoreGetUser.getDocument{(document, error) in
if let dict = document?.data() {
guard let decodedUser = try? User.init(fromDictionary: dict) else { return }
self.userInSession = decodedUser
print("decoded user = \(decodedUser)")
self.isLoggedIn = true // << here !!
print("user logged in")
}
}
So whats worked for me is passing in Auth instead of session data:
func loadUserData() {
profileViewModel.LoadAUser(userId: Auth.auth().currentUser!.uid)
}

SWIFTUI Call Key Dictionary not work with the error: 'Subscript index of type '() -> Bool' in a key path must be Hashable'

I have this view:
import SwiftUI
struct SectionView1: View {
let dateStr:String
#Binding var isSectionView:Bool
var body: some View {
HStack {
Button(action: {
self.isSectionView.toggle()
}) {
Image(systemName: isSectionView ? "chevron.down.circle" : "chevron.right.circle")
}
Text("Media del \(dateStr)")
}
}
}
which will be called from view:
import SwiftUI
import Photos
struct MediaView: View {
let geoFolder:GeoFolderCD
#State private var assetsForDate = [String :[PHAsset]]()
#State private var isSectionViewArray:[String:Bool] = [:]
var body: some View {
List {
ForEach(assetsForDate.keys.sorted(by: > ), id: \.self) { dateStr in
Section {
SectionView1(dateStr: dateStr,
isSectionView: self.$isSectionViewArray[dateStr, default: true])
}
}
}
.onAppear {
self.assetsForDate = FetchMediaUtility().fetchGeoFolderAssetsForDate(geoFolder: geoFolderStruct, numAssets: numMediaToFetch)
for dateStr in self.assetsForDate.keys.sorted() {
self.isSectionViewArray[dateStr] = true
}
}
}
}
but I have the error: Subscript index of type '() -> Bool' in a key path must be Hashable in isSectionView: self.$isSectionViewArray[dateStr, default: true]
Why isSectionViewArray:[String:Bool] = [:] is not Hasbable?
How can modify the code for work?
If I remove, in SectionView, #Binding var isSectionView:Bool, the code work fine, or if I set, from SectionView, #Binding var isSectionViewArray:[String:Bool] = [:], the code work fine.
You can write your own binding with the below code and it should work
var body: some View {
List {
ForEach(assetsForDate.keys.sorted(by: > ), id: \.self) { dateStr in
let value = Binding<Bool>(get: { () -> Bool in
return self.isSectionViewArray[dateStr, default: true]
}) { (value) in
}
Section {
SectionView1(dateStr: dateStr,
isSectionView: value)
}
}
}
.onAppear {
self.assetsForDate = FetchMediaUtility().fetchGeoFolderAssetsForDate(geoFolder: geoFolderStruct, numAssets: numMediaToFetch)
for dateStr in self.assetsForDate.keys.sorted() {
self.isSectionViewArray[dateStr] = true
}
}
}

If Firebase has a pdf attribute, i want to display it within the pdfView in my IOS app

I am trying to create an app which will have a list of announcements, connected to the Firebase server for testing and if the Firebase has a pdf attribute i want to display it in the App.
The code for this is below:
import UIKit
import FirebaseDatabaseUI
import Firebase
import Down
import FontAwesomeIconFactory
import PDFKit
extension AnnouncementDetailViewController: PDFViewDelegate {
func pdfViewWillClick(onLink sender: PDFView, with url: URL){
print(url)
}
}
class AnnouncementDetailViewController: UIViewController {
#IBOutlet var authorLabel: UILabel!
#IBOutlet var titleLabel: UILabel!
#IBOutlet var authorImage: UIImageView!
var announcementKey = ""
let announcement: Announcement = Announcement()
lazy var ref: DatabaseReference = Database.database().reference()
var announcementRef: DatabaseReference!
var refHandle: DatabaseHandle?
var contentView: DownView?
var pdfView: PDFView!
override func viewDidLoad() {
super.viewDidLoad()
initialiseContentView()
initialseDatabaseChild()
initialiseNavbarTitle()
//Method calls for Pdf
configureUI()
loadPDF()
playWithPDF()
}
private func configureUI(){
pdfView = PDFView ()
pdfView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pdfView)
pdfView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
pdfView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
pdfView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
pdfView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
pdfView.delegate = self
}
private func addObservers(){
NotificationCenter.default.addObserver(self, selector: #selector(handlePageChange(notification:)), name: Notification.Name.PDFViewPageChanged, object: nil)
}
#objc private func handlePageChange(notification: Notification){
print("Current page is changed")
}
private func loadPDF() {
guard
let url = URL(string:"http://www.pdf995.com/samples/pdf.pdf"),
let document = PDFDocument(url: url)
else { return }
if document.isLocked && document.unlock(withPassword: "test"){
pdfView.document = document
} else {
print("We have a problem")
}
}
private func playWithPDF(){
pdfView.displayMode = .singlePageContinuous
pdfView.autoScales = true
}
private func initialiseNavbarTitle() {
self.navigationItem.title = "Announcement Detail"
}
private func initialseDatabaseChild() {
announcementRef = ref.child("announcements").child(announcementKey)
}
private func generateDownViewHeight() -> CGFloat {
return UIScreen.main.bounds.height - authorLabel.frame.height - authorImage.frame.height - (navigationController?.navigationBar.frame.height ?? 0) - view.safeAreaInsets.top - 70
}
private func initialiseContentView() {
guard let contentView = try? DownView(frame: CGRect(x: 0, y: 0, width: 0, height: 0), markdownString: "") else { return }
view.addSubview(contentView)
contentView.scrollView.bounces = false
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5).isActive = true
contentView.heightAnchor.constraint(equalToConstant: generateDownViewHeight()).isActive = true
contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
self.contentView = contentView
}
#objc override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refHandle = announcementRef.observe(DataEventType.value, with: { (snapshot) in
let announcementDict = snapshot.value as? [String : AnyObject] ?? [:]
self.announcement.setValuesForKeys(announcementDict)
do {
try self.contentView?.update(markdownString: self.announcement.content)
let factory = FontAwesomeIconFactory.button()
self.authorImage.image = factory.createImage(NIKFontAwesomeIcon.male)
self.authorLabel.text = self.announcement.author
self.titleLabel.text = self.announcement.title
}
catch {
}
})
}

Resources