I am working with JSQMessagesViewController and have implemented three bubble colors. The extra color is designed to indicate an unapproved message in a moderated chat room.
I am running a Firebase backend and updating an approved flag when the chat message entries changes.
All is going well and the data is being changed real time. The problem is with the chat bubble colors, no matter what I do they will not change.
I've tried invalidating the layout, reloaddata, accessing the cell directly (comes up read only) and nothing seems to change the color other than leaving the chat view and coming back.
messageRef.observe(.childChanged, with: { (snapshot) in
let key = snapshot.key
if let dict = snapshot.value as? [String: AnyObject] {
let approved = (dict["approved"]?.boolValue ?? true)
let indexOfMesage = self.messages.index(where:{$0.key == key})
var message = self.messages[indexOfMesage!]
message.approved = approved
print(message)
self.collectionView.performBatchUpdates({ () -> Void in
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.reloadData()
}, completion:nil)
}
Any help would be appreciated. The code above is just one of many attempts.
Adding my "messageBubbleImageDataForItemAt" call for additional info after response below.
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.messageItem.senderId == senderId { // 2
if (message.approved == true){
return outgoingBubbleImageView
}else{
return outgoingUnnaprovedBubbleImageView
}
}else if (self.superUsers.contains(message.messageItem.senderId)){
return incomingAdminBubbleImageView
}else { // 3
if (message.approved == true){
return incomingBubbleImageView
}else{
return incomingUnnapprovedBubbleImageView
}
}
}
Invalidating the layout will not do anything for the color of the bubble. I would suggest that you modify you messageDataObject all you have to do is conform to the JSQMessageData protocol and I would add a property for a messages approval status.
I am going to assume that you have three different message bubbles defined
something like this
var incomingBubble: JSQMessagesBubbleImage!
var outgoingBubble: JSQMessagesBubbleImage!
var approvedBubble: JSQMessagesBubbleImage!
then in your view did load you actually define them.
incomingBubble = JSQMessagesBubbleImageFactory().incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
outgoingBubble = JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(with: UIColor.lightGray)
approvedBubble = JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(with: UIColor.red)
Then in the overridden function messageBubbleImageDataForItemAt you should provide logic for which bubble to use.
override func collectionView(_ collectionView: JSQMessagesCollectionView, messageBubbleImageDataForItemAt indexPath: IndexPath) -> JSQMessageBubbleImageDataSource {
//Sent by the current user
if messages[indexPath.item].senderId == self.senderId(){
return outgoingBubble
}
//Check if the message is approved from that property earlier.
if messages[indexPath.item].approved {
return approvedBubble
}
// its just a normal message return incoming message.
return incomingBubble
}
Other wise you need to fetch from firebase at this point if a message is approved or not.
Then calling self.collectionView.reloadData() should update your colors as long as you have pulled the latest from firebase.
I hope this helps you out. Let me know if you have more questions and keep on keeping on. ππ½
Related
My app requests JSON data (latitude, longitude, and other information about a place) and then displays them on a map in a form of clickable annotations. I'm receiving around 30,000 of those, so as you can imagine, the app can get a little "laggy".
The solution I think would fit the app best is to show those annotations only on a certain zoom level (for example when the user zooms so only one city is visible at once, the annotations will show up). Since there's a lot of them, showing all 30,000 would probably crash the app, that's why I also aim at showing just those that are close to where the user zoomed in.
The code below shows immediately all annotations at once at all zoom levels. Is there a way to adapt it to do the things I described above?
struct Map: UIViewRepresentable {
#EnvironmentObject var model: ContentModel
#ObservedObject var data = FetchData()
var locations:[MKPointAnnotation] {
var annotations = [MKPointAnnotation]()
// Loop through all places
for place in data.dataList {
// If the place does have lat and long, create an annotation
if let lat = place.latitude, let long = place.longitude {
// Create an annotation
let a = MKPointAnnotation()
a.coordinate = CLLocationCoordinate2D(latitude: Double(lat)!, longitude: Double(long)!)
a.title = place.address ?? ""
annotations.append(a)
}
}
return annotations
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
// Show user on the map
mapView.showsUserLocation = true
mapView.userTrackingMode = .followWithHeading
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// Remove all annotations
uiView.removeAnnotations(uiView.annotations)
// HERE'S WHERE I SHOW THE ANNOTATIONS
uiView.showAnnotations(self.locations, animated: true)
}
static func dismantleUIView(_ uiView: MKMapView, coordinator: ()) {
uiView.removeAnnotations(uiView.annotations)
}
// MARK: Coordinator Class
func makeCoordinator() -> Coordinator {
return Coordinator(map: self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var map: Map
init(map: Map) {
self.map = map
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// Don't treat user as an annotation
if annotation is MKUserLocation {
return nil
}
// Check for reusable annotations
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: Constants.annotationReusedId)
// If none found, create a new one
if annotationView == nil {
annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: Constants.annotationReusedId)
annotationView!.canShowCallout = true
annotationView!.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
} else {
// Carry on with reusable annotation
annotationView!.annotation = annotation
}
return annotationView
}
}
}
Been searching for an answer for a while now and found nothing that worked well. I imagine there's a way to get visible map rect and then condition that in Map struct, but don't know how to do that. Thanks for reading this far!
Your delegate can implement mapView(_:regionDidChangeAnimated:) to be notified when the user finishes a gesture that changes the map's visible region. It can implement mapViewDidChangeVisibleRegion(_:) to be notified while the gesture is happening.
You can get the map's visible region by asking it for its region property. Regarding zoom levels, the region documentation says this:
The region encompasses both the latitude and longitude point on which the map is centered and the span of coordinates to display. The span values provide an implicit zoom value for the map. The larger the displayed area, the lower the amount of zoom. Similarly, the smaller the displayed area, the greater the amount of zoom.
Your updateUIView method recalculates the locations array every time SwiftUI calls it (because locations is a computed property). You should check how often SwiftUI is calling updateUIView and decide whether you need to cache the locations array.
If you want to efficiently find the locations in the visible region, try storing the locations in a quadtree.
Finally figured that out...
The Coordinator class can implement mapView(_:regionDidChangeAnimated:) (as #rob mayoff said) that gets called after the user finishes a gesture that changes the map's visible region. When that happens, annotations on the map and their array are updated. Looks something like this...
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if mapView.region.span.latitudeDelta < <Double that represents zoom> && mapView.region.span.longitudeDelta < <Double that represents zoom> {
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(map.getLocations(center: mapView.region.center))
}
}
... phrases (doubles missing from the if statement) in < > are to be replaced with your own code (the greater the double, the smaller zoom is needed to view the annotations). The array of annotations is updated by a function defined in Map struct and looks like this...
func getLocations(center: CLLocationCoordinate2D) -> [MKPointAnnotation] {
var annotations = [MKPointAnnotation]()
let annotationSpanIndex: Double = model.latlongDelta * 10 * 0.035
// Loop through all places
for place in data.dataList {
// If the place does have lat and long, create an annotation
if let lat = place.latitude, let long = place.longitude {
// Create annotations only for places within a certain region
if Double(lat)! >= center.latitude - annotationSpanIndex && Double(lat)! <= center.latitude + annotationSpanIndex && Double(long)! >= center.longitude - annotationSpanIndex && Double(long)! <= center.longitude + annotationSpanIndex {
// Create an annotation
let a = MKPointAnnotation()
a.coordinate = CLLocationCoordinate2D(latitude: Double(lat)!, longitude: Double(long)!)
a.title = place.adresa ?? ""
annotations.append(a)
}
}
}
return annotations
}
... where annotationSpanIndex determines in how big of a region around the center point will the annotations be shown (greater the index, bigger the region). This region should be ideally slightly larger than the zoom on which the annotations are shown.
I have a fairly complex document type to work with. It is basically a bundle containing a set of independent documents of the same type, with various pieces of metadata about the documents. The data structure that represents the bundle is an array of structs, similar to this (there are several more fields, but these are representative):
struct DocumentData: Equatable, Identifiable, Hashable {
let id = UUID()
var docData: DocumentDataClass
var docName: String
var docFileWrapper: FileWrapper?
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
static func ==(lhs: KeyboardLayoutData, rhs: KeyboardLayoutData) -> Bool {
return lhs.id == rhs.id
}
}
The window for the bundle is a master-detail, with a list on the left and, when one is selected, there is an edit pane for the document on the right. The FileWrapper is used to keep track of which files need to be written for saving, so it gets initialised on reading the relevant file, and reset when an undoable change is made. That is largely the only way that the DocumentData structure gets changed (ignoring explicit things like changing the name).
I've reached a point where a lot of things are working, but I'm stuck on one. There's a view inside the edit pane, several levels deep, and when I double-click it, I want a sheet to appear. It does so, but then disappears by itself.
Searching for ways to work this out, I discovered by using print(Self._printChanges()) at various points that the edit pane was being refreshed after showing the sheet, which meant that the parent disappeared. What I found was that the dependency that changed was the DocumentData instance. But, I then added a print of the DocumentData instance before the _printChanges call, and it is identical. I have also put in didSet for each field of DocumentData to print when they get set, and nothing gets printed, so I'm not sure where the change is happening.
So the question comes down to how I can work out what is actually driving the refresh, since what is claimed to be different is identical in every field.
There are some other weird things happening, such as dragging and dropping text into the view causing the whole top-level document array of DocumentData items to change before the drop gets processed and the data structures get updated, so there are things I am not understanding as clearly as I might like. Any guidance is much appreciated.
ADDED:
The view that triggers the sheet is fairly straightforward, especially compared to its enclosing view, which is where most of the interface code is. This is a slightly simplified version of it:
struct MyView: View, DropDelegate {
#EnvironmentObject var keyboardStatus: KeyboardStatus
#Environment(\.displayFont) var displayFont
#Environment(\.undoManager) var undoManager
var keyCode: Int
#State var modifiers: NSEvent.ModifierFlags = []
#State private var dragHighlight = false
#State private var activeSheet: ActiveSheet?
#State private var editPopoverIsPresented = false
// State variables for double click and drop handling
...
static let dropTypes = [UTType.utf8PlainText]
var body: some View {
ZStack {
BackgroundView(...)
Text(...)
}
.onAppear {
modifiers = keyboardStatus.currentModifiers
}
.focusable(false)
.allowsHitTesting(true)
.contentShape(geometry.contentPath)
.onHover { entered in
// updates an inspector view
}
.onTapGesture(count: 2) {
interactionType = .doubleClick
activeSheet = .doubleClick
}
.onTapGesture(count: 1) {
handleItemClick()
}
.sheet(item: $activeSheet, onDismiss: handleSheetReturn) { item in
switch item {
case .doubleClick:
DoubleClickItem(...) ) {
activeSheet = nil
}
case .drop:
DropItem(...) {
activeSheet = nil
}
}
}
.popover(isPresented: $editPopoverIsPresented) {
EditPopup(...)
}
.onDrop(of: KeyCap.dropTypes, delegate: self)
.contextMenu {
ItemContextMenu(...)
}
}
func handleItemClick() {
NotificationCenter.default.post(name: .itemClick, object: nil, userInfo: [...])
}
func handleEvent(event: KeyEvent) {
if event.eventKind == .dropText {
interactionType = .drop
activeSheet = .drop
}
else if event.eventKind == .replaceText {
...
handleItemDoubleClick()
}
}
func handleSheetReturn() {
switch interactionType {
case .doubleClick:
handleItemDoubleClick()
case .drop:
handleItemDrop()
case .none:
break
}
}
func handleItemDoubleClick() {
switch itemAction {
case .state1:
...
case .state2:
...
case .none:
// User cancelled
break
}
interactionType = nil
}
func handleItemDrop() {
switch itemDropAction {
case .action1:
...
case .action2:
...
case .none:
// User cancelled
break
}
interactionType = nil
}
// Drop delegate
func dropEntered(info: DropInfo) {
dragHighlight = true
}
func dropExited(info: DropInfo) {
dragHighlight = false
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: MyView.dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { (textData, error) in
if let textData = String(data: textData as! Data, encoding: .utf8) {
let event = ...
handleEvent(event: event)
}
}
return true
}
return false
}
}
Further edit:
I ended up rewiring the code so that the sheet belongs to the higher level view, which makes everything work without solving the question. I still don't understand why I get a notification that a dependency has changed when it is identical to what it was before, and none of the struct's didSet blocks are called.
Try removing the class from the DocumentData. The use of objects in SwiftUI can cause these kind of bugs since itβs all designed for value types.
Try using ReferenceFileDocument to work with your model object instead of FileDocument which is designed for a model of value types.
Try using sheet(item:onDismiss:content:) for editing. I've seen people have the problem you describe when they try to hack the boolean sheet to work with editing an item.
i have a behavior i cant resolve.
I have a controller (controller 1) where i check some defaults value. if not present (first time use of app: check login and pwd) i present (modal) a settings vc:
IN CONTROLLER 1
override func viewDidAppear(_ animated: Bool) {
if(!isKeyPresentInUserDefaults(key: "username")) {
NSLog("username not present")
let vc = self.storyboard?.instantiateViewController(withIdentifier: "settings") as! SettingsViewController
self.present(vc, animated: true, completion: nil)
}
}
in this vc (controller 2) (also in the main storyboard) i have a button done. When pressed, it is associated to the:
IN CONTROLLER 2: SettingsVc -> ID : settings
#IBAction func doneSettings(sender: AnyObject) {
if(isInternetAvailable()) {
NSLog("internet available")
login { (result) in
switch result
{
case .Success(let result):
//print(result)
self.dismissSelf()
break
case .Failure(let error):
print(error)
break
}
}
}
else {
NSLog("internet not available")
}
}
the dismissSelf func is defined in the Settingsvc as:
func dismissSelf() {
NSLog("dismissSettingsVC")
self.dismiss(animated: false, completion: nil)
}
the login func is defined in another class, dealing with networking stuff and is as is:
func login(completion: #escaping (AsyncResult<[CustomUserObject]>)->())
{
let myUrl = URL(string: "http://www.xxxx.com/api/api.php");
var request = URLRequest(url:myUrl!)
request.httpMethod = "POST"// Compose a query string
let postString = "u=login&s=password&cmd=login";
request.httpBody = postString.data(using: String.Encoding.utf8);
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest){
(data, response, error) -> Void in
if let error = error
{
completion(AsyncResult.Failure(error as NSError?))
} else {
let result: [CustomUserObject] = []//deserialization json data into array of [CustomUserObject]
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("*********response data = \(responseString)")
completion(AsyncResult.Success(result))
}
}
task.resume()
}
So, I launch the app for the first time (no login, no pwd in defaults), then the Settingvc is presented. I press the done button with param hardcoded. The login is correctly called, I receive the answer correctly, and on completion i should dismiss the Settingvc.
thing is, i see the NSLOG for dismiss, but the dismiss appears seconds after the network func completion (from 10 secs to up to a minute), which I can't understand. Why such a delay? any idea?
2017-11-27 21:54:33.262892 app[998:211517] username not present
2017-11-27 21:54:36.119754 app[998:211517] internet available
*********response data =
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-27 21:54:38.472306 app[998:211542] dismissSettingsVC
2017-11-27 21:54:48.048095 app[998:211517] username not present
in this case, it took 10 sec to dismiss the Settingsvc after receiving the login results.
another one:
2017-11-27 22:04:20.364097 app[998:211517] internet available
*********response data =
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-27 22:04:22.495642 app[998:212974] dismissSettingsVC
2017-11-27 22:05:00.049177 app[998:211517] username not present
in this other case, it took 38 sec to dismiss the Settingsvc after receiving the login results.
EDITED
I tried not using a vc presented. Instead in controller 1, i added a view that i first set as visible if username in defaults does not exist and then that I will hide after the login completion. in this view, i added a button to call the loginAction.
#IBAction func loginAction(sender: AnyObject) {
if(isInternetAvailable()) {
NSLog("internet available")
login { (result) in
switch result
{
case .Success(let users):
print(users)
self.loginView.isHidden = true
NSLog("login ok: hiding view")
break
case .Failure(let error):
print(error ?? "ERROR")
break
}
}
}
else {
NSLog("internet not available")
}
}
Same result:
I see the completion and the received data:
2017-11-28 18:17:34.314706 cellar[1270:311710] username not present
2017-11-28 18:17:35.066333 cellar[1270:311710] internet available
2017-11-28 18:17:35.076930 cellar[1270:311710] done login
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-28 18:17:37.655829 cellar[1270:311763] login ok: hiding view
the view should be hidden before the NSLOG "login ok: hiding view". Instead, the UI is updated seconds after (about a min, but variable)
What would avoid the UI to be updated for so long as I wait the completion of the network stuff to perform the UI update?
UPDATE:
weird situation:
as soon as I get the network completion result, by changing the orientation, the dismiss appears right away:
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-28 22:28:30.620408 cellar[1461:360470] dismiss
2017-11-28 22:28:31.537588 cellar[1461:360413] username not present
2017-11-28 22:28:32.126759 cellar[1461:360413] [App] if we're in the
real pre-commit handler we can't actually add any new fences due to CA
restriction
your help is much that appreciated. Thanks
Not sure if this is the reason for your problem but it looks like you aren't running the dismiss() call on the main thread. You should call all UI code on the main thread. Wrap it as follows
case .Success(let result):
//print(result)
DispatchQueue.main.async {
self.dismissSelf()
}
break
currently my output is displaying selected values from pickerview inside textfields.
now my question is i want to access all these values on submit button and want to display in another view controller how to do this?. let me explain my scenario my first vc is set as collectionview from one of the collectionviewcell m redirecting to this page.
Note: i already know that how to pass data between two view controller. but its not working in my case.Please Help.
Code
#IBAction func StaffAtten_Action(_ sender: Any) {
// let secondVC = self.storyboard?.instantiateViewController(withIdentifier: "StaffAttendence_SecondPage") as! StaffAttendence_SecondPage
//
// secondVC.a = active_text.text!
// secondVC.b = active_text.text!
// secondVC.c = active_text.text!
// secondVC.savedata.append(year.text!)
// secondVC.savedata.append(month.text!)
// secondVC.savedata.append(institute.text!)
}
}
The problem that you are having is that you are instantiating a brand new VC and passing data to it. The VC that is actually presented is not the one you created.
Since you have a segue connecting the two VCs, override prepare(for:sender:)
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? StaffAttendence_SecondPage {
vc.a = ...
vc.b = ...
// pass the rest of the data here...
}
}
I'm building an app (in swift). One of the options should be to allow the user to either select multiple photos and upload them or take a photo and create a post with it.
My idea is based the following hierarchy:
New Photo post button pressed -> User is presented with UICollection view controller with all photos from their library and an option to select multiple photos (I'm reusing a cell to populate those collection view with the images from the photo library) and above that should be the Camera cell (the user needs to click on the button to allow access to the camera in order to take a photo). Here's a representation from the Simulator.
I've written the code that gets the photos from library and add them in an array here (I'll show the code for these below).
The problem is when you initially load the app for the first time and try to post, you're being asked to grant access to the photos. Despite of the user's choice, (either they agree or don't) they collection view isn't update.
What it needs to happen - When the user is asked to grant access to Photos and they click "OK", it should reload the data in the collection view, but it doesn't. And when they click on "Don't allow" it should dismiss the entire View controller. Here's my code
class CVController: UICollectionViewController, UINavigationControllerDelegate, UICollectionViewDelegateFlowLayout {
var photosLibraryArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
grabAllPhotosFromPhoneLibrary()
}
// Grab All Photos from Library
func grabAllPhotosFromPhoneLibrary () {
let imageManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true // synchronous works better when grabbing all images
requestOptions.deliveryMode = .opportunistic
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] // if false = last image we took would show first
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
// 1. Are there photos in the library
if fetchResult.count > 0 {
// 2. if fetch.count > 0 it means there's at least 1 photo in the library
for i in 0..<fetchResult.count {
// 3. Cycling through the photos
imageManager.requestImage(for: fetchResult.object(at: i), targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: requestOptions, resultHandler:
{ image, error in
// 4. Append each image in the array
self.photosLibraryArray.append(image!)
})
}
} else {
print("You've got no photos")
self.collectionView?.reloadData()
}
I tried calling collectionView.reloadData() in the viewWillApear(), viewDidApear(), nothing worked.
class CVController: UICollectionViewController, UINavigationControllerDelegate, UICollectionViewDelegateFlowLayout {
var photosLibraryArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
checkPhotoLibraryPermission()
}
Add this below mentioned function
func checkPhotoLibraryPermission() {
let status = PHPhotoLibrary.authorizationStatus()
switch status {
case .authorized:
//handle authorized status - // call this function here
grabAllPhotosFromPhoneLibrary()
case .denied, .restricted :
//handle denied status
case .notDetermined:
// ask for permissions
PHPhotoLibrary.requestAuthorization() { status in
switch status {
case .authorized:
// call this function here
grabAllPhotosFromPhoneLibrary()
case .denied, .restricted:
// as above
_ = navigationController?.popViewController(animated: true)
case .notDetermined:
// won't happen but still, if you want to handle.
}
}
}
}
You may accordingly make changes in the following function below. As per the need.
// Grab All Photos from Library
func grabAllPhotosFromPhoneLibrary () {
let imageManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true // synchronous works better when grabbing all images
requestOptions.deliveryMode = .opportunistic
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] // if false = last image we took would show first
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
// 1. Are there photos in the library
if fetchResult.count > 0 {
// 2. if fetch.count > 0 it means there's at least 1 photo in the library
for i in 0..<fetchResult.count {
// 3. Cycling through the photos
imageManager.requestImage(for: fetchResult.object(at: i), targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: requestOptions, resultHandler:
{ image, error in
// 4. Append each image in the array
self.photosLibraryArray.append(image!)
})
}
//Use this reload data here as - you want the data loaded in the array first and then show the data.
self.collectionView?.reloadData()
} else {
print("You've got no photos")
//self.collectionView?.reloadData()
// if you want to hide the view controller when there is no photo. pop the view controller from your navigation controller like this.
_ = navigationController?.popViewController(animated: true)
}