I have a custom delegate that triggers certain events. For context, it's a bluetooth device that fires events arbitrarily. I'd like my view controllers to optionally subscribe to these events that get triggered by the device delegate.
It doesn't make sense that each view controller conforms to the custom delegate because that means the device variable would be local and would only fire in that view controller. Other view controllers wouldn't be aware of the change. Another concrete example would be CLLocationManagerDelegate - for example what if I wanted all view controllers to listen to the GPS coordinate changes?
Instead, I was thinking more of a global delegate that all view controllers can subscribe to. So if one view controller triggers a request, the device would call the delegate function for all subscribed view controllers that are listening.
How can I achieve this architectural design? Are delegates not the right approach? I thought maybe NotificationCenter can help here, but seems too loosely typed, perhaps throwing protocols would help makes things more manageable/elegant? Any help would be greatly appreciated!
You could have an array of subscribers that would get notified.
class CustomNotifier {
private var targets : [AnyObject] = [AnyObject]()
private var actions : [Selector] = [Selector]()
func addGlobalEventTarget(target: AnyObject, action: Selector) {
targets.append(target)
actions.append(action)
}
private func notifyEveryone () {
for index in 0 ..< targets.count {
if targets[index].respondsToSelector(actions[index]) {
targets[index].performSelector(actions[index])
}
}
}
}
Naturally, you'd have to plan further to maintain the lifecycle of targets and actions, and provide a way to unsubscribe etc.
Note: Also ideal would be for the array of targets and actions to be an of weak objects. This SO question, for instance, deals with the subject.
• NotificationCenter is first solution that comes in mind. Yes, it is loosely typed. But you can improve it. For example like this:
extension NSNotificationCenter {
private let myCustomNotification = "MyCustomNotification"
func postMyCustomNotification() {
postNotification(myCustomNotification)
}
func addMyCustomNotificationObserverUsingBlock(block: () -> ()) -> NSObjectProtocol {
return addObserverForName(myCustomNotification, object: nil, queue: nil) { _ in
block()
}
}
}
• Second solution would be to create some shared object, which will store all delegates or blocks/closures and will trigger them when needed. Such object basically will be the same as using NotificationCenter, but gives you more control.
Related
In my view model, I have two properties:
private val databaseDao = QuestionDatabase.getDatabase(context).questionDao()
val allQuestions: LiveData<List<Question>> = databaseDao.getAllQuestions()
I have observers set on "allQuestions" in my fragment and I'm noticing the observer is being called when I rotate the device. Even though the View Model is only being created once (can tell via a log statement in init()), the observer methods are still being called.
Why is this? I would think the point is to have persistency in the View Model. Ideally, I want the database questions to be only loaded once, regardless of rotation.
This happens because LiveData is lifecycle aware.
And When you rotate the screen you UI Controller [Activity/Fragment] goes through various lifecycle states and lifecycle callbacks.
And since LiveData is lifecycle aware, it updates the detail accordingly.
I have tried to explain this with following points:
When the UI Controller is offscreen, Live Data performs no updates.
When the UI Controller is back on screen, it gets current data.
(Because of this property you are getting above behavior)
When UI controller is destroyed, it performs cleanup on its own.
When new UI Controller starts observing live data, it gets current data.
add this check inside observer
if(lifecycle.currentState == Lifecycle.State.RESUMED){
//code
}
I have the same issue, after reading the jetpack guideline doc, I solve it. Just like what #SVK mentioned, after the rotation of the screen, activity/fragment were re-created.
Base on the solution https://stackoverflow.com/a/64062616,
class SingleLiveEvent<T> : MutableLiveData<T>() {
val TAG: String = "SingleLiveEvent"
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer<T> { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun observeForever(observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observeForever { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
#MainThread
override fun setValue(#Nullable t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}
I want to be able to allow the user to change some properties of the graphical user interface immediately through all the app. To achieve that, I thought about creating a protocol like
protocol MyProtocol {
func changeProperties()
}
so each UIViewController can change those properties in its own way and then call this method in all the current instantiated controllers.
However, I don't know if this is possible. My first idea was to access the most root controller of the app, and then iterate through all the child recursively calling the method. Something like
func updatePropertiesFrom(_ vc: UIViewController) {
for child in vc.childViewControllers {
if let target = child as? MyProtocol {
target.changeProperties()
}
updatePropertiesFrom(child)
}
}
let appRootController = ...
updatePropertiesFrom(appRootController)
I don't know how to get that appRootController and I would like to know if there's any more elegant way for doing this. Thanks.
You can use NotificationCenter. E.g. define a notification name:
extension Notification.Name {
static let changeViewProperties = Notification.Name(Bundle.main.bundleIdentifier! + ".changeViewProperties")
}
And your various view controllers can then register to be notified when this notification is posted:
NotificationCenter.default.addObserver(forName: .changeViewProperties, object: nil, queue: .main) { _ in
// do something here
}
(If you're supporting iOS versions prior to iOS 9, make sure to remove your observer in deinit.)
And to post the notification when you want to initiate the change:
NotificationCenter.default.post(name: .changeViewProperties, object: nil)
You can access the app's root controller with:
UIApplication.shared.keyWindow.rootViewController
Of course this assumes the current key window is the one with your app's root controller.
You could also access the window property of your app delegate and get the rootViewController from that window.
Your general approach is valid. There is no other "more elegant" way to walk your app's current view controllers. Though you should also traverse presented view controllers as well as the child view controllers.
However, a simpler solution might be to post a notification using NotificationCenter and have all of your view controllers respond accordingly.
You could do this with a central dispatcher and having each UIViewController register with it.
Example:
class MyDispatcher{
static let sharedInstance = MyDispatcher()
var listeners: [MyProtocol]()
private init() {}
public func dispatch(){
for listener in listeners{
listener.changeProperties()
}
}
}
From each UIViewController you want to update the properties, you call MyDispatcher.sharedInstance.listeners.append(self). When you want to update the properties, you call MyDispatcher.sharedInstance.dispatch().
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(){}
I am reading about VIPER and my understanding is- generally a viewController is related to one presenter and one presenter talks to one Interactor.
But, what if we have master-details pages or list-detail pages. To display list of items, I would have one controller/Presenter to display list and another controller/presenter to display details. And FetchList and FetchDetail should belong to same interactor.
If these two presenters communicate to this interactor, they would have to implement both the methods FetchList and FetchDetail. And one of these two method's implementation would be empty.
You should have two separate VIPER modules: MainItems and DetailedItems.
Read this post (https://www.ckl.io/blog/best-practices-viper-architecture) and see how to use delegates to send data between VIPER modules. Note that FetchList and FetchDetail should belong to different interactor:
// 1. Declare which messages can be sent to the delegate
// ProductScreenDelegate.swift
protocol ProductScreenDelegate {
//Add arguments if you need to send some information
func onProductScreenDismissed()
func onProductSelected(_ product: Product?)
}
// 2. Call the delegate when you need to send him a message
// ProductPresenter.swift
class ProductPresenter {
// MARK: Properties
weak var view: ProductView?
var router: ProductWireframe?
var interactor: ProductUseCase?
var delegate: ProductScreenDelegate?
}
extension ProductPresenter: ProductPresentation {
//View tells Presenter that view disappeared
func onViewDidDisappear() {
//Presenter tells its delegate that the screen was dismissed
delegate?.onProductScreenDismissed()
}
}
// 3. Implement the delegate protocol to do something when you receive the message
// ScannerPresenter.swift
class ScannerPresenter: ProductScreenDelegate {
//Presenter receives the message from the sender
func onProductScreenDismissed() {
//Presenter tells view what to do once product screen was dismissed
view?.startScanning()
}
...
}
// 4. Link the delegate from the Product presenter in order to proper initialize it
// File ScannerRouter.swift
class ProductRouter {
static func setupModule(delegate: ProductScreenDelegate?) -> ProductViewController {
...
let presenter = ScannerPresenter()
presenter.view = view
presenter.interactor = interactor
presenter.router = router
presenter.delegate = delegate // Add this line to link the delegate
...
}
}
My understanding is that you have one view/view-controller and presenter per screen and then one interactor per use case, which probably means more than one per screen. This is good practice from the point of view of Single Responsibility Principle and therefore aids testing. But sometimes a concession is made and an interactor handles multiple uses cases.
I'm having a bit of trouble getting this right. What i have is a NavigationController with several View Controllers. In the first View Controller i instantiate a new class, GPSTrackingManager:
var tracking = GPSTrackingManager()
Because i need GPS data and functions available in all view controllers after this UINavController. I'm calling this already in the first VC so the GPS can get a early fix. How can i pass this tracking object to the new view controllers? I tried several things in the prepareForSegue, but i'm out of ideas currently. If i put the above code in the new VC then i have the startUpdatingLocation() running 2 times. If you can point me in the right direction that would be greatly appreciated!
I think the most appropriate solution (without going too overboard, because there are certainly more thorough ways to go about this problem) is to separate the gps management into its own singleton class, and to subscribe to a trackerUpdated protocol for updates.
import Foundation
import placeWhereGPSTrackingManagerIs
protocol GPSManagerDelegate {
func didUpdateTracker(tracker: yourTrackerObject)
}
let _GPSManagerSharedInstance = GPSManager()
class GPSManager {
weak var delegate: GPSManagerDelegate?
let tracker: yourTrackerObject?
class var sharedInstance:GPSManager {
return _GPSManagerSharedInstance
}
init() {
tracker = GPSTrackingManager()
tracker.startUpdatingLocation()
}
func updateTracker(newInformation) // you'll want to pass whatever information your tracker needs here
{
tracker.trackerProperty = newInformation
self.delegate?.didUpdateTracker(tracker)
}
}
Swift singleton explanation
Using a dispatch_once singleton model in Swift
Now, whatever startUpdatingLocation does could call updateTracker in this class with the new information it needs to update the tracker. You could also migrate that logic to live inside of here (which I recommend doing in this case) so everything is contained in one class.
Any view controller interested in the information can subscribe to the didUpdateTracker protocol method, which will pass the tracker through to that view controller
import UIKit
class controllerOne: UIViewController, GPSManagerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
GPSManager.sharedInstance.delegate = self // set the GPSManager delegate
if let tracker = GPSManager.sharedInstance.tracker {
// if the tracker exists, we assign it to the "tracker" variable, and use as needed
// setup page with tracker
}
}
func didUpdateTracker(updatedTracker: yourTrackerObject) {
// protocol method that will be called from the gpsmanager everytime it updates
// do whatever updates to the page with the
}
}
As you can only have one GPSManager and its tracker property cannot be changed, it is always safe to ask the GPS manager for this tracker, which only calls startUpdatingLocation() on its initialization
You could also post notifications as #shesh nath mentioned, which is a perfectly valid way to broadcast changes if you prefer it to the delegation method. Either way you approach it, I would recommend separating the tracker from the view controller so that the view controllers are not the ones in charge of managing the trackers state.
There are several ways to achieve it like
1) using Key-Value observer
2) Notification
Example of Notification is on your first viewController post notification and pass your data to this notification.
NSDictionary *data;
[[NSNotificationCenter defaultCenter] postNotificationName:#"updateBeaconStateOnDetailScreen" object:data];
and on left-over all viewcontroller register listener for this notification on viewdidload example.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateBeaconState:)name:#"updateBeaconStateOnDetailScreen" object:nil];
and your method which will be called by this notification is
-(void) updateBeaconState:(NSNotification *) notification
{ //notification raised from BeaconList with updated Beacon State
NSDictionary *dictUpdatedBeacon=[notification valueForKey:#"object"];
}