QLPreviewController detect when QLPreviewItem Changed - ios

I have application that use QLPreviewController to show document from MS. Office. I use this controller to multiple files. I need to log which files that already viewed. But I can't find event that triggered when PreviewItem changed.
I try to trigger event when GetPreviewItem called, But this method just called 1 time each preview item. So I can't use this.
Anyone can help me to find how to detect QLPreviewItem changed?

You can do this with Key-Value Observing. Subclass the QLPreviewController and add something like this:
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
observation = observe(\.currentPreviewItemIndex, options: [.new] ) { _, change in
// You get the new index value in change.newValue
}
}

Related

is there a shortcut to know, when I'm running my app, which view controller am I in?

I'm working on an application that already exists, and I want to run the app and know in the output which view controller am I in, without going to every class and viewController and printing its name. Is there a shortcut or something that can help me ??
I've tried printing in view did load in every ViewController
like
print("view did load : MainViewController")
but I found this wasting of time because I have so many viewControllers.
I think about debugging but I really don't know what it does or how.
You could swizzle (exchange the implementation of) UIViewController.viewDidAppear so it prints the class name. First define your swizzled implementation:
extension UIViewController {
#objc func printingViewDidAppear(_ animated: Bool) {
print(String(describing: type(of: self)))
// Call the original implementation. This looks like recursion but isn't
printingViewDidAppear(animated)
}
}
Then exchange the default implementation with your implementation like this:
let originalViewDidAppear: Method = class_getInstanceMethod(UIViewController.self, #selector(UIViewController.viewDidAppear(_:)))!
let swizzledViewDidAppear: Method = class_getInstanceMethod(UIViewController.self, #selector(UIViewController.printingViewDidAppear(_:)))!
method_exchangeImplementations(originalViewDidAppear, swizzledViewDidAppear)
That way you wouldn't need to change any of your existing view controllers.
You must make sure to call the original implementation at the end of the swizzled implementation. It looks like a recursive call but it isn't. Hope that helps.
I'm not sure this is possible. What you can do, however, to make it a little simpler, is adding this extension to UIViewController:
extension UIViewController {
var className: String {
return String(describing: type(of: self))
}
}
Then, in viewDidLoad, print it:
print("loaded \(self.className)")

how to call a method in a view controller from Appdelegate in Swift?

this Main Menu VC will be opened when the app launched for the first time or after the user back to the app (the app become active after enter the background state).
every time this main menu VC is opened, ideally I need to update the time that the date time data comes from the server. in this main menu vc class I call getDateTimeFromServer() after that I updateUI().
but to update the data after the app enter the background and back to the foreground, the getDateTimeFromServer() and updateUI() shall be activated from Appdelegate using function.
func applicationWillEnterForeground(application: UIApplication) {
}
so how do I activate a method that are exist in Main Menu VC from AppDelegate
You don’t need to call the view controller method in app delegate. Observe foreground event in your controller and call your method from there itself.
Observe for the UIApplicationWillEnterForeground notification in your viewController viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.yourMethod), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
Implement this to receive callback when user enters foreground
#objc func yourMethod() {
// Call getDateTimeFromServer()
}
These types of messaging are in most cases done with static context. As it was already mentioned you could alternatively use notification center within the within the view controller to be notified of your application entering foreground. I discourage you creating custom notifications for this though (but is a possible solution as well).
Anyway for your specific case I suggest you have a model that contains your data. Then create a shared instance of it.
class MyDataModel {
static var shared: MyDataModel = {
let model = MyDataModel()
model.reloadData()
return model
}()
var myObjects: [MyObject]?
func reloadData() {
// load data asynchronously
}
}
Now when your view controller needs to reload it simply uses MyDataModel.shared.myObjects as data source.
In app delegate all you do is reload it when app comes back to foreground using MyDataModel.shared.reloadData().
So now a delegate is still missing so we add
protocol MyDataModelDelegate: class {
func myDataModel(_ sender: MyDataModel, updatedObjects objects: [MyObject]?)
}
class MyDataModel {
weak var delegate: MyDataModelDelegate?
static var shared: MyDataModel = {
Now when your view controller appears it needs to assign itself as a delegate MyDataModel.shared.delegate = self. And implement the protocol in which a reload on the view must be made.
A callout to the delegate can simply be done in a model setter:
}()
var myObjects: [MyObject]? {
didSet {
delegate.myDataModel(self, updatedObjects: myObjects)
}
}
func reloadData() {
You can do something like that, using a technique called Key-Value Observation:
class CommonObservableData: NSObject {
// Use #objc and dynamic to ensure enabling Key-Value Observation
#objc dynamic var dateTime: Date?
static let shared = CommonObservableData()
func updateFromWeb() {
// callWebThen is a function you will define that calls your Web API, then
// calls a completion handler you define, passing new value to your handler
callWeb(then: { self.dateTime = $0 })
}
}
Then you observe on it using Swift 4 's new NSKeyValueObservation.
class SomeViewController: UIViewController {
var kvo: NSKeyValueObservation?
func viewDidLoad() {
...
kvo = CommonObservableData.shared.observe(
\CommonObservableData.dateTime, { model, change in
self.label.text = "\(model.dateTime)"
})
}
}
Key-Value Observation is originally an Objective-C technique that is "somewhat revived" by Swift 4, this technique allows you to observe changes on a property (called a Key in Objective-C) of any object.
So, in the previous code snippets, we made a class, and made it a singleton, this singleton has an observable property called dateTime, where we could observe on change of this property, and make any change in this property automatically calls a method where we could update the UI.
Read about KVO here:
Key-Value Observation Apple Programming Guide
Key-Value Observation using Swift 4
Also, if you like Rx and RFP (Reactive Functional Programming), you can use RxSwift and do the observation in a cleaner way using it.
In swift 4 and 5, the notification name is changed the below code working for both.
notifyCenter.addObserver(self, selector: #selector(new), name:UIApplication.willEnterForegroundNotification, object: nil)
#objc func new(){}

Clearing Firebase observations from a UITableViewCell

In all iOS classes that use Firebase you will have code like this,
private func clearObservations() {
// your method for clearing observations, probably something like
blah blah. removeAllObservers()
}
In view controllers, it's essential that you call this in viewWillDisappear (or viewDidDisappear)
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
clearObservations()
}
That's fine.
Assume that you have created an observation in a UITableViewCell.
What is the best place in a cell to "clear observations" ?
Note that prepareForReuse is useless, try it.
The only approach we've found is
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
clearObservations()
}
super.willMove(toSuperview: newSuperview)
}
Seems flakey/bizarre though.
What's the deal on this?
Update
Note while "XY Answers" are interesting and informative, if anyone knows the answer to the question that would be great also!
Preface
This was an attempt to answer the question but the question was misunderstood. I'll leave it here as it does have some relevance regarding observers, handles and tableView cell interaction.
While you can go through those gyrations, it's not really needed in most use cases.
For example, if you add and observer to a node, there wouldn't necessarily be a someRef? variable hanging around. So here we are watching the Posts node for new posts
let postsRef = self.ref.child("Posts")
postsRef.observe(.childAdded, with: { snapshot in
print(snapshot) //add the post to the dataSource and reloadTableview/cell
})
Here's another example of watching for any posts that are changed by uid_2
let postsRef = self.ref.child("Posts")
let queryRef = postsRef.queryOrdered(byChild: "poster_id").queryEqual(toValue: "uid_2")
queryRef.observe(.childChanged) { (snapshot) in
print(snapshot) //change the post in the dataSource and reloadTableview/cell
}
No class vars are needed for this functionality and nothing needs be nil'd. The point here being that you do not have to have class vars to get observing functionality and you do not need to keep a handle for every observer (keep reading)
In view controllers, it's essential that you call this
(someRef?.removeAllObservers()) in viewWillDisappear (or Did)..
will use Firebase in the cells of tables.
To clarify; I wouldn't want to put Firebase observers in the cells of tables. The observers should be in whichever viewController controls the tableView that has cells. Cells should pull data from the dataSource array (which is backed by Firebase)
There are some circumstances where you may want to remove all observers, again no need to have a class var or a need to nil a var.
let postsRef = self.ref.child("Posts")
postsRef.removeAllObservers()
There are times when a specific observer needs to be removed (in the case where a node has observers on it's child nodes for example), and in those cases, we store a handle to that observer as say, a class var (keeping them in an array is a tidy way to do it)
class ViewController: UIViewController {
var myPostHandle : DatabaseHandle?
func addObserver() {
let postsRef = self.ref.child("Posts")
self.myPostHandle = postsRef.observe(.childAdded, with: { snapshot in
print(snapshot)
})
func stopObserving() {
if self.myPostHandle != nil {
let postsRef = self.ref.child("Posts")
postsRef.removeObserver(withHandle: self.myPostHandle) //remove only the .childAdded observer
}
}
}
Again though, once the observer is removed, the handle would go out of scope once the class closes.
Tableviews that contain cells are backed by a dataSource and that dataSource get's it's data from firebase. When something is added, changed or removed from Firebase, your app is notified and the array is updated and then the cell refreshed. No need for an observer in the cell itself.
There's no need to add dozens of observers (in the cells) - add one central observer and let it keep the array current. Refresh tableView only when something changes.
EDIT
To Address a comment regarding the use of removeAllObservers: code is worth 1000 words:
Create a new Firebase project with two button actions. Here's the code for button0 which adds an observer to a node:
func button0() {
let testRef = self.ref.child("test_node")
testRef.observe( .value) { snapshot in
print(snapshot)
}
}
when this button0 is clicked, from there on, any adds, changes, or deletes to the test node will print it's contents to the log.
func button1() {
let testRef = self.ref.child("test_node")
testRef.removeAllObservers()
}
This will remove all observers for the node specified. Once clicked, no events will print to the console.
Try it!
It is not right to clear observations in cell and therefore there is not a best place to do it in cell, because, firstly, this approach contradicts MVC pattern. Views only responsible for displaying content and they should only contain code that describes how they must be draw. And in the view controller you give the content for showing by views. Usually content has provided by your model. So controller connects views and model. In your case, when you place clearObservations() in cell class, you also have someRef as a class property, so you have a model in your view class and this is incorrect.
Secondly, if you try to clear observations in table cell you definitely make logic of showing some content in table in wrong way. Cell only show data that has to be generated by some object that conforms to UITableViewDataSource protocol and implements protocol methods. For instance, in cellForRow method you generate cell and setup it with some content from array. This array is generated from model (Firebase service). Your view controller may be this data source object. You have to include array property to controller class and someRef, than you fill array and reload table data. If controller's view disappeared you clear observations, but you do it only inside view controller (in viewWillDisappear()).
Overall, all manipulations with someRef you should do in view controller and therefore "clear observations" also inside controller.

Firebase database detach listener

In my app I fetch live data like this:
//Firebase
var ref: FIRDatabaseReference?
var handle: FIRDatabaseHandle?
override func viewDidLoad() {
ref = FIRDatabase.database().reference()
handle = ref?.child("posts").child(String(itemId)).observe(.childChanged, with: { (snapShot) in
if let item = snapShot.value as? String {
print(item)
}
})
.....
Now reading the firebase docs I see this:
Observers don't automatically stop syncing data when you leave a ViewController. If an observer isn't properly removed, it continues to sync data to local memory.
So I added this function that gets fired when I exit the VC:
#IBAction func backButtonDidTouch(_ sender: AnyObject) {
if let handle = handle {
ref?.removeObserver(withHandle: handle)
}
showNavBar = true
_ = navigationController?.popViewController(animated: true)
}
But I can also call removeAllObservers() insetad of removeObserver() and the docs also says:
Calling removeObserverWithHandle or removeAllObservers on a listener does not automatically remove listeners registered on its child nodes; you must also keep track of those references or handles to remove them.
So looking at my code am I doing it right? I dont want to keep data syncing between my app and firebase when I exit my VC
You seem to be calling an observer on the specific post, but you are removing the observer from the parent reference. As the documentation states, removing a listener from a reference does not clear the observers from the children, hence I believe you have not removed the observer as you intended.
I have run into this issue myself. Particularly when I log a user out, for a brief moment, the presenting view controller is trying to read from firebase and crashes. What I have done is define a set of type DatabaseReference among the singleton I am using. And where I call
ref.observe(.value) {(snapshot) in
singleton.refsUsed.insert(snapshot.ref)
...
}
Then upon logging out, before I dismiss the current view controller, I am iterating over all the items in the reference set and removing all observers.

UIView doesn't change at runtime

I've had this working in other variations but something seems to elude me in the change from objective-c to swift as well as moving some of the setup into it's own class.
So i have:
class ViewController: UIViewController, interfaceDelegate, scrollChangeDelegate{
let scrollControl = scrollMethods()
let userinterface = interface()
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
}
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
}
}
This sets everything up correctly but the problem occurs when I change loadMenu() at runtime. So if the user calls loadMenu("AnotherMenu") it won't change the UIView. It will call the right functions but it won't update the view. Although if I call loadMenu("AnotherMenu") at the start, the correct menu will display. Or if I call loadMenu("Start") and then loadMenu("AnotherMenu") then the menu displayed will be "AnotherMenu". As in:
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
loadMenu("AnotherMenu")
}
When I list all the subviews each time loadMenu() is called, they look correct. Even during runtime. But the display is not updated. So something isn't getting the word. I've tried disabling Auto Layout after searching for similar issues but didn't see a difference.
Try adding setNeedsDisplay() to loadMenu
Eg
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
view.setNeedsDisplay()
}
setNeedsDisplay() forces the view to reload the user interface.
I didn't want to post the whole UIView class as it is long and I thought unrelated. But Dan was right that he would need to know what was going on in those to figure out the answer. So I created a dummy UIView class to stand in and intended to update the question with that. I then just put a button on the ViewController's UIView. That button was able to act on the view created by the dummy. So the problem was in the other class. Yet it was calling the methods of the ViewController and seemingly worked otherwise. So then the issue must be that its acting on an instanced version? The way the uiview class worked, it uses performSelector(). But in making these methods into their own class, I had just lazily wrote
(ViewController() as NSObjectProtocol).performSelector(selector)
when it should have been
(delegate as! NSObjectProtocol).performSelector(selector)
so that was annoying and I wasted the better part of a day on that. But thanks again for the help.

Resources