delegate protocol for two viewcontrollers - ios

i am fairly new to ios programming and i am not sure what the best way to do this is. i want to Create a delegate protocol that can be used to notify interested observers (two viewcontrollers to be specific) of an image array's change in state. i want each view controller to conform to it also. for example, if it is in state 1 then it only appears in the first viewcontroller and if it is in state 2 then it only appears in the second viewcontroller.
this is what i have so far:
protocol imgState {
static var state1 : Int {get set}
static var state2 : Int {get set}
func stateChange(){
if state1 == 1 {
set {
state1 == 0
state2 == 1
}
}
if state2 == 1 {
set {
state1 == 1
state2 == 0
}
}
}
}
my thinking is to call the function in each viewcontroller when a button is pressed to change the state.
is there any good tutorials someone could direct me towards or any guidance on how to start at all? anything should help.

I've used delegates in the past but I much prefer posting a notification.
In your example, when the button is pressed, you can do something like this,
NSNotificationCenter.defaultCenter().postNotificationName("buttonPressed", object: nil)
The other views would listen for this notification with:
// Usually added to viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateState", name: "buttonPressed", object: nil)
You would then have a func named "updateState" in the relevant view controllers which would be called whenever the 'buttonPressed' notification is posted.
Hope this helps.

You can achieve this in a much simpler way using NSNotifications... The way you would do it, since you're trying to listen to 2 different inputs/actions, is to post a notification for each... these will respond right away!
So as an example, in an action method (but could be used anywhere really) we could be expecting 2 different values...
var var1:Bool = false // setting a false value as a default
#IBAction func expectAction(sender:AnyObject) {
if var1 == false {
NSNotificationCenter.defaultCenter().postNotificationName("key1", object: nil)
} else {
NSNotificationCenter.defaultCenter().postNotificatioinName("key2", object: nil)
}
}
Then on each of your classes you would add observers to respond to such notifications as follows:
(assuming ViewController1 is listener 1 and ViewController2 is listener 2)
At your viewDidLoad() method in ViewController1 add the following:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"actonKey1:", name:"key1", object: nil)
Then add a new method in ViewController1
#obj private func actonKey1(notification: NSNotification) {
// do your work here
}
Finally (for this ViewController1) add a deinitializing method like this:
deinit{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
You repeat this for ViewController2 changing the appropriate names of the key1 to key2
At your viewDidLoad() method in ViewController2 add the following:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"actonKey2:", name:"key2", object: nil)
Then add a new method in ViewController2
#obj private func actonKey2(notification: NSNotification) {
// do your work here
}
Finally (for this ViewController2) add a deinitializing method like this:
deinit{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
And that's pretty much it... The response time is amazingly fast... and it is (at least to my belief) less convoluted than using protocols and delegates...
Cheers

One idea is to use an Observable/Subject class (like this one that I wrote: https://gist.github.com/dtartaglia/0a71423c578f2bf3b15c but there are lots of libraries that provide this, and other, services.)
With this service, you would wrap the image in a Subject object and the two view controllers would attach a block of code to the subject. Then whenever the contained image is changed (by assigning to mySubject.value) the attached blocks will be called.
Just be sure to dispose of the observers when done.

Related

How to reloadData after an in app purchase through modalView

I've looked at different sources to find an answer to this and possibly tried several ways to reloadData() but no luck so far.
Here is my case:
I have a tabBar where I present the content in a tableView. When you go into that tab, I present a modal view if you are not purchased the content yet.
Let's say you have purchased the content. The modal view dismisses itself but tableview doesn't show anything unless you kill the app and re-run again. When you this, tableView reloads itself with the content. No problem there. But this is definitely problematic and I do lose users just because of this.
I do keep track of the purchase with a key like this:
private var hiddenStatus: Bool = UserDefaults.standard.bool(forKey:"purchaseStatus")
I set it to true on a successful purchase as shown below:
self.setNonConsumablePurchase(true)
And finally the code where I check if the key is set to "true" on purchase. I call this on the viewDidLoad but it doesn't make sense because it's been already called when user touched on this tabBar item.
if hiddenStatus == true {
DispatchQueue.main.async(execute: {
self.segmentioView.valueDidChange = { [weak self] _, segmentIndex in
switch self?.segmentioView.selectedSegmentioIndex {
case 0:
self?.observeAugustProducts()
self?.tableView.reloadData()
case 1:
self?.observeSeptemberProducts()
self?.tableView.reloadData()
default:
self?.tableView.reloadData()
}
}
})
}
So I'm looking for two things:
1- To keep everything as it is and tweak the code to make this work.
2- Alternative ideas to give users a better in-app purchase experience (I once used UIAlertViewController for in-app purchase but decided not to go with it)
I recommend using this extension:
extension NotificationCenter {
func setUniqueObserver(_ observer: AnyObject, selector: Selector, name: NSNotification.Name, object: AnyObject?) {
NotificationCenter.default.removeObserver(observer, name: name, object: object)
NotificationCenter.default.addObserver(observer, selector: selector, name: name, object: object)
}
}
Before dismissing your Modal View, have your Modal View's ViewController post a Notification.
Something like:
NotificationCenter.default.post(name: "Purchased", object: nil)
Then have your TableView's ViewController register to listen for that Notification (in viewWillAppear) - and handle it.
Something like:
NotificationCenter.default.setUniqueObserver(self, selector: #selector(handlePurchased), name: "Purchased", object: nil)
and
#objc func handlePurchased() {
// Reload your TableView, etc.
}
Hope this helps someone!

Call method on all the existing instances of UIViewController

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().

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(){}

How to subscribe to delegate events globally?

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.

Go back to previous controller from class swift

I have an app that use http calls to stream video from external storage.
When the user's device isn't connected to a network service, I need the app to go back to the previous controller.
the flow is the following: the user access a list of elements (table view cell), select one, then the app goes to the player controller. On this controller, the call is made to stream the file.
I use an api call handler in a class outside of the controller and I don't know how to proceed to make it go back to the previous controller from here (the element list).
Connectivity issues errors are all catched within the api class.
I don't show any code as Im not sure it would be relevant. If you need to see anything, let me know and I will update the question. What should be the way to do that? (of course I use a navigation controller)
Thanks
If you want go back to the previous view controller you should use:
navigationController?.popViewControllerAnimated(true)
If you need to use this function not in the view-controller but in another class you can use NSNotificationCenter for notify the view-controller when it's needed to show the previous controller, just like this:
YourViewController
override func viewDidLoad()
{
...
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "goBack:",
name: "goBackNotification",
object: nil)
...
}
func goBack(notification: NSNotification)
{
navigationController?.popViewControllerAnimated(true)
}
AnotherClass
NSNotificationCenter.defaultCenter().postNotificationName("goBackNotification", object: nil)
Don't forget to remove the observer in your YourViewController:
deinit
{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
EDIT 1: you can use obviously a delegate instead of a NSNotification method. If you don't know the differences between NSNotification and delegate I recommend to you this answer.
A common approach besides NSNotificationCenter is to utilize closures or delegates to inform your ViewController that the streaming attempt failed. Using closures, the API of the class responsible for the streaming could be extended to take a completion closure as parameter, and call it with an NSError, if one occurred, or nil if it didn't.
func streamFile(completion: NSError? -> Void) {
// Try to start the streaming and call the closure with an error or nil
completion(errorOrNil)
}
When the call to the API in the ViewController is made you can then pass a closure to the method and check for errors. In case something went wrong an error should be present and the ViewController should be dismissed
func startStream() {
StreamingAPIClient.streamFile(completion: { [weak self] error in
if error != nil {
// Handle error
self.dismissViewControllerAnimated(true, completion: nil)
} else {
// Proceed with the streaming
}
})
}

Resources