Repeat same statement in all viewDidLoad functions possible extension file - ios

I want this same statement repeated in all of my projects' viewDidLoad functions. I know I can just manually type it in but I am trying to find a way to increase my code speed. I don't know if I can use a extension file in this.
override func viewDidLoad() {
super.viewDidLoad()
let myswitchBoolValuefromFirstVc : Bool = UserDefaults.standard.bool(forKey: "mySwitch")// this is how you retrieve the bool value
// to see the value, just print those with conditions. you can use those for your things.
if myswitchBoolValuefromFirstVc == true {
print("true")
rosaryCounterLabel.isHidden = false
}
else {
print("false")
rosaryCounterLabel.isHidden = true
}

If you are prepared to abuse the Objective-C runtime that UIViewController still uses, you can use method swizzling to do what you ask. https://medium.com/#abhimuralidharan/method-swizzling-in-ios-swift-1f38edaf984f
let aClass: AnyClass! = object_getClass(instance)
let originalMethod = class_getInstanceMethod(aClass, #selector(viewDidLoad))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(newViewDidLoad))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
but I wouldn't recommend it. You trade between discoverability and repetition. In the case where you control all the code, it is going to be easier to maintain using a solution like subclassing that will still require some changes in every view controller.

Create a "master" view controller.
class MasterViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//"Global" code here
}
}
And then inherit in all your other view controllers
class ViewController: MasterViewController{
override func viewDidLoad() {
super.viewDidLoad()
//controller specific code here
}
}

Related

Set global default back button display mode for all UIViewController instances

iOS 14 introduced configurable Back button mode.
Eg. you can have "Back" label text on the button, but in the compact history menu you can still see the proper title of previous controllers.
I'm looking for a easy, pleasant way to configure the default mode, so all UIViewController instances during runtime will have default mode set, like UINavigationItemBackButtonDisplayModeGeneric
I wonder if there is a way to do that without subclassing UIViewController or remember to always configure manually every instance of UIViewController
(via viewController.navigationItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeGeneric).
Any handy method which does not require extensive refactoring of hundreds of UIViewController instances greatly appreciated!
Without subclassing I think is not possible since navigationItem requires an instance to work with and can't be modified directly from extensions
class GenericViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// your code here
viewController.navigationItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeGeneric
}
}
And use that where ever you need
class viewController: GenericViewController
This is a really good approach since you have control over what implements it and what's not considering that it might not be there in all scenes
To solve the same problem, I used the swizzling technique
import UIKit
private let swizzling: (UIViewController.Type, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in
if let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
let didAddMethod = class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
extension UIViewController {
static func swizzle() {
let originalSelector1 = #selector(viewDidLoad)
let swizzledSelector1 = #selector(swizzled_viewDidLoad)
swizzling(UIViewController.self, originalSelector1, swizzledSelector1)
}
#objc open func swizzled_viewDidLoad() {
if let _ = navigationController {
if #available(iOS 14.0, *) {
navigationItem.backButtonDisplayMode = .generic
} else {
// Fallback on earlier versions
navigationItem.backButtonTitle = "Back"
}
}
swizzled_viewDidLoad()
}
}
And in application(_:didFinishLaunchingWithOptions:) call
UIViewController.swizzle()

How to count how many times all classes are called

I want the user to be able to know how many times they have visited each class. Then add together the totals from each page together to form a group sum. I want to print the total sum in the log file in each of the two view controllers. So just one string should be printed.
class oneV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults.standard.set(true, forKey: "VC1")
}
}
class twoV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults.standard.set(true, forKey: "VC2")
}
}
If you mean visited each view controller, when you say visited each class. Then i'd recommend you do it viewDidAppear.
class YourViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let key = String(describing: type(of: self))
let count = UserDefaults.standard.value(forKey: key) as? Int ?? 0
UserDefaults.standard.set(value + 1, forKey: key)
}
}
To make it simpler, you could use an extension on UIViewController.
extension UIViewController {
func updateVisitCount() {
let key = String(describing: type(of: self))
let count = UserDefaults.standard.value(forKey: key) as? Int ?? 0
UserDefaults.standard.set(count + 1, forKey: key)
}
}
Or, if you need this for every view controller that you create, then you can create a base view controller which you would use everywhere instead of UIViewController.
class BaseViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateVisitCount()
}
}
The most automatic solution would be inject the accounting call in viewDidLoad without replacing the original viewDidLoad.
Here demo purpose i've created a sample Playground
import UIKit
import PlaygroundSupport
extension UIViewController {
#objc dynamic func substitutedViewDidAppear() {
print("This is injected code in view did appear")
substitutedViewDidAppear() // it may look like recursive, but it isn't, actually it calls the original `viewDidAppear` method.
}
class func swizzle() {
let originalMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidAppear(_:)))
let substitutedMethod = class_getInstanceMethod(UIViewController.self, #selector(substitutedViewDidAppear))
if let originalMethod = originalMethod,
let substitutedMethod = substitutedMethod {
print("swizzled")
method_exchangeImplementations(originalMethod, substitutedMethod)
} else {
print("not swizzled")
}
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
view.addSubview(label)
self.view = view
print("view loaded")
}
}
// Swizzle
UIViewController.swizzle() // call this in #top of didFinishLaunchingWithOptions
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Output:
swizzled
view loaded
This is injected code in view did appear
Now in the substitutedViewDidAppear upper portion inject your counting code as #Rakesha Shastri Suggested, call the updateVisitCount method inside of substitutedViewDidAppear & place the UIViewController.swizzle() in applicationDidFinishLaunchingWithOptions before creating the root window.
Create a static variable. A static variable is a type of class, not object therefore throughout all objects a variable maybe maintained. I think this example may better explain how this works. Click here
In ViewDidLoad method call this function :
func updateVisitingCounter() {
var counter = UserDefaults.standard.integer(forKey: "firstPageCounter")
counter += 1
UserDefaults.standard.set(counter, forKey: "firstPageCounter")
}
You may set declare variables at project scope "outside of classes"
var vc1Count = 0
class oneV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
vc1Count = vc1Count+1
}
}
var vc2Count = 0
class twoV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
vc2Count = vc2Count+1
}
}
you can also declare these variables at a common place.
As per your requirements its kind of Analytics on app usage. You can implement in 2 ways
By storing data with screen visit in local DB and show it on Analysis Page or on summery page.
Sample code for storing Screen details in DB:
==> Create your Entity for Screen capture.
ScreenVisit.
==> Store Data with screen name.
let entity = NSEntityDescription.entity(forEntityName: "ScreenVisit", in: context)
let newVisit = NSManagedObject(entity: entity!, insertInto: context)
newVisit.setValue("HomeScreen", forKey: "screenname")
newVisit.setValue("1", forKey: "visited")
do {
try context.save()
} catch {
print("Failed saving")
}
==> Fetch data where you required.
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "ScreenVisit")
//request.predicate = NSPredicate(format: <Your Filter Logic>)
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject] {
print(data.value(forKey: "screenname") as! String)
print(data.value(forKey: "visited") as! String)
}
} catch {
print("Failed")
}
You can use any 3rd party library like Google analytics, Crashlytics for tracking your user actions.
Ref Links :
Firebase iOS analytics
Crashlytics
but as per my experience 2nd way is more convenient and powerful.
All depends on your requirements.
Hope this will helps you to get your user action captured.

Swizzling CocoaTouch class in Swift 3.1

I use Swift 3.1 in XCode 8.3 and see that warning:
Method 'initialize()' defines Objective-C class method 'initialize',
which is not guaranteed to be invoked by Swift and will be disallowed
in future versions
I use Swizzling CocoaTouch class and have an issue with that part:
extension UIViewController {
open override class func initialize() {
// make sure this isn't a subclass
guard self === UIViewController.self else { return }
swizzling(self)
}
// MARK: - Method Swizzling
func proj_viewWillAppear(animated: Bool) {
self.proj_viewWillAppear(animated: animated)
let viewControllerName = NSStringFromClass(type(of: self))
print("viewWillAppear: \(viewControllerName)")
}
}
How to rewrite that part of the code?
open override class func initialize()
for fix new warning?
I saw that link, but I don't understand how to use info in my code.
I had same problem and fix it.
My Solution
1. Change swizzling() method to public from private
public let swizzling: (AnyClass, Selector, Selector) -> () = { forClass, originalSelector, swizzledSelector in
let originalMethod = class_getInstanceMethod(forClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
2. Remove initialize() method at extension UIViewController
// open override class func initialize() {
// // make sure this isn't a subclass
// guard self === UIViewController.self else { return }
// swizzling(self)
// }
3. Add swizzling() at AppDelegate::didFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// some code
swizzling(UIViewController.self)
return true
}
This is simply/easy solution. (this is not Elegant)
If you want to get more information, please see this:
Swift 3.1 deprecates initialize(). How can I achieve the same thing?

Function Overriding in iOS

I have created BaseClassviewController and all my controllers are derived from this controller. I am doing the following steps:
Set custom delegate in BaseClassViewController.
Implement all function of protocol in BaseClassViewController.
Then I am pushing HomeController derived from BaseClassViewController.
Again I am pushing DetailController also derived from BaseClassViewController.
Now when delegate function is called I should get control in DetailController but I am getting control in HomeController.
So my question is why its not calling top controller at navigation i.e DetailController and is it possible to call delegate functions in both controllers?
P.S I am overriding delegate functions in all child controllers.
EDIT: After reading answers and comments I think I have not been clear that much so adding following code snippet.
In Helper Class:
#objc protocol SampleDelegate: class
{
#objc optional func shouldCallDelegateMethod()
}
class SampleHelper: NSObject
{
var sampleDelegate:SampleDelegate!
static var sharedInstance = SampleHelper()
//It is triggered
func triggerDelegateMethod()
{
sampleDelegate!.shouldCallDelegateMethod()
}
func apiCall()
{
let urlString = URL(string: "https://google.com")
if let url = urlString {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error)
} else {
if let usableData = data {
self. triggerDelegateMethod()
}
}
}
task.resume()
}
}
}
In BaseClass
class BaseClassViewController: UIViewController,SampleDelegate{
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(true)
SampleHelper.sharedInstance.delegate = self;
}
func shouldCallDelegateMethod()
{
//Override
}
}
In HomeController i.e 1st controller to be pushed
class HomeViewController: BaseClassViewController{
override func shouldCallDelegateMethod()
{
//
}
}
In DetailController i.e 2nd controller is pushed after HomeController from HomeController.
class DetailViewController: BaseClassViewController{
override func viewDidLoad()
{
super.viewDidLoad()
SampleHelper.sharedInstance.apiCall()
}
override func shouldCallDelegateMethod()
{
//
}
}
Now my question is when delegate is triggered from helper class it calls shouldCallDelegateMethod in HomeViewController but not in DetailViewController. But DetailViewController is at top of navigation array.
Also is there any possibility I can trigger same function in both controller at a time with delegate only?
In BaseClassviewController you should have a delegate variable/property.
In HomeController and DetailController you need to set that delegate variable/property to self if you want that class to be listening to the delegate callbacks.
The basic problem is that you are using delegate with a singleton.
Setting the delegate in viewWillAppear is not a good solution either. In short, when view controllers are being shown and hidden, the delegate on your singleton will changed all the time.
Don't use delegates with singletons. Use a completion callback. Otherwise you will keep running into problems.
func apiCall(onCompletion: (() -> Void)?) {
let urlString = URL(string: "https://google.com")
if let url = urlString {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error)
} else if let usableData = data {
onCompletion?()
}
}
task.resume()
}
}
called as
SampleHelper.apiCall {
// do something
}
Edit 2
After you posted your code, i realize that you have used the singleton class for delegation.
Delegates allows an object to send a message to another object.
Answer for your query is "No". You can not trigger same function in both controller at a time with delegate.
If you really want to listen an event in both class at a time, i would suggest you to use NSNotificationCenter instead of delegate.
Thats not the correct way to achieve this. I think proper way to set delegate only in respective UIViewController rather than implementing that protocol on BaseViewController and then overriding in child classes. So your implementation should be like.
In HomeViewController
class HomeViewController: BaseClassViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
SampleHelper.sharedInstance.delegate = self;
}
func shouldCallDelegateMethod() {
// Provide implementation
}
}
In DetailViewController
class DetailViewController: BaseClassViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewDidLoad()
SampleHelper.sharedInstance.delegate = self;
}
func shouldCallDelegateMethod() {
// Provide implementation
}
}
Using this imeplementation you will be having only one-to-one communication design pattern, ensuring right UIViewController to be called.

Swift override function in extension

If I have a class:
class Spaceship<FuelType> {
function prepareForLiftoff() throws {
//Start the countdown!
}
}
I originally assumed that I would be able to override prepareForLiftoff without subclassing by adding an extension:
extension Spaceship where FuelType: CollectionType {
func prepareForLiftoff() throws {}
}
This code doesn't compile though, the error says invalid redeclaration of the function, which makes sense.
My question is: Is there anyway to override a function of a particular class? In other words can I replace the functionality under certain conditions like the example above where FuelType: CollectionType. If not, is there any other workaround or way to achieve that behavior (maybe declaring another protocol, idk)
Now that I think about it more, I would have to say that's not possible because what's to stop someone from overriding any of the standard library functions?
From the documentation:
Extensions can add new functionality to a type, but they cannot override existing functionality.
The documentation lists carefully and precisely what an extension is allowed to do.
As to your question:
Is there anyway to override a function of a particular class
Yes, it's called subclassing.
Instead of overriding, you may like to try swizzling. For example the following code allows you to run your own viewWillAppear. Swift 3:
extension UIViewController {
open override class func initialize() {
// make sure this isn't a subclass
guard self === UIViewController.self else { return }
DispatchQueue.once(token: "viewWillAppear") {
let originalSelector = #selector(self.viewWillAppear(_:))
let swizzledSelector = #selector(self.proj_viewWillAppear1(animated:))
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
func proj_viewWillAppear1(animated: Bool) {
self.proj_viewWillAppear1(animated: animated)
let viewControllerName = NSStringFromClass(type(of: self))
print("viewWillAppear: \(viewControllerName)")
}
}
Update 20170621
public extension DispatchQueue {
private static var _onceTracker = [String]()
public class func once(file: String = #file, function: String = #function, line: Int = #line, block:(Void)->Void) {
let token = file + ":" + function + ":" + String(line)
once(token: token, block: block)
}
public class func once(token: String, block:(Void)->Void) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if _onceTracker.contains(token) {
return
}
_onceTracker.append(token)
block()
}
}

Resources