iOS/Swift: How to add one extension to multiple controllers? - swift

In a file called Extensions, I'm trying to add one extension to 2 View Controllers so I don't have to write the whole code twice.
import UIKit
extension TempConvertViewController {
// code
}
I need to use the exact same code for LocationViewController.
Any help would be appreciated!
Edit:
Thank you all for responding.
What I am trying to achieve is reuse the same keyboard/view code in 2 controllers as both contain textFields. The reason I've extended TempConvertViewController is because I have a variable (activeTextField) which won't be recognizable if I were to implement this in UIViewController.
If I put the code in a function, the keyboardDidShow() and keyboardWillHide() functions won't be recognized in the controller itself. I get the error: Use of unresolved identifier 'keyboardDidShow(notification:), Use of unresolved identifier 'keyboardWillHide(notification:)'
See second part of the code below
Here is the extension of TempConvertViewController
extension TempConvertViewController: UITextFieldDelegate {
#objc func keyboardDidShow(notification: Notification) {
let notificationInfo: NSDictionary = notification.userInfo! as NSDictionary
let keyboardSize = (notificationInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let yKeyboard = view.frame.size.height - keyboardSize.height
let yTextFieldEditing: CGFloat! = activeTextField?.frame.origin.y
if view.frame.origin.y >= 0 {
//checking if textField is covered by keyboard
if yTextFieldEditing > yKeyboard - 60 {
UIView.animate(withDuration: 0.25, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations:{self.view.frame = CGRect(x: 0, y: self.view.frame.origin.y -
(yTextFieldEditing! - (yKeyboard - 60)), width: self.view.bounds.width, height: self.view.bounds.height)}, completion: nil)
}
}
}
#objc func keyboardWillHide(notification: Notification) {
UIView.animate(withDuration: 0.25, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: { self.view.frame = CGRect(x: 0, y:0, width: self.view.bounds.width, height: self.view.bounds.height)}, completion: nil)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
activeTextField = textField
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
}
TempConvertViewController:
override func viewDidLoad() {
super.viewDidLoad()
// Subscribe to Notifications
let center: NotificationCenter = NotificationCenter.default;
center.addObserver(self, selector: #selector(keyboardDidShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
center.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
If I manage to include everything in one function, e.g. keyboardViewManagement(), can't I use a new extension for the other View Controller (LocationChangeViewController) like this:
extension LocationChangeViewController: UITextFieldDelegate {
// calling the function
func keyboardViewManagement()
}
Thank you all again!

Use power of protocols, we are in swift after all
protocol Extendable: class {
var editingTextField: UITextField? { get }
func someFunc()
}
extension Extendable where Self: UIViewController {
func someFunc() {
// yor implementation
}
}
class ViewController: UIViewController {
var editingTextField: UITextField? {
return activeTextField
}
}

Just write an extension of UIViewController:
extension UIViewController {
//anything can go here
func some() {
}
}
Then in your view controller:
class VC1: UIViewController {
some()
}

Related

How to change value of label from another UICollectionViewCell

What if the UILabel is in class A and the didTapRightutton that will animate it is in class B?
the percentDiscountLabel is in RandomizeDealsCollectionViewCell. This should animate into fade appear if I tap didTapRightutton which is in a different VC called RandomizeDealsViewController
How do I call the function that is inside RandomizeDealsCollectionViewCell to animate the percentDiscountLabel? Is there other way to do this?
class RandomizeDealsCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var percentDiscountLabel: UILabel!
func animatePercentDiscountLabel(deals: String) {
self.percentDiscountLabel.alpha = 0.6
self.percentDiscountLabel.isHidden = false
UIView.animate(withDuration: 0.6, delay: 0, options: .curveEaseInOut, animations: {
self.percentDiscountLabel.alpha = 1.0
}) { (isCompleted) in
}
percentDiscountLabel.text = deals
}
}
class RandomizeDealsViewController: UIViewController {
private var centeredCollectionViewFlowLayout: CenteredCollectionViewFlowLayout!
#IBAction func didTapRightButton(_ sender: Any) {
guard let indexCard = centeredCollectionViewFlowLayout.currentCenteredPage else { return }
if (indexCard > 0) {
centeredCollectionViewFlowLayout.scrollToPage(index: indexCard + 1, animated: true)
// should call the animation function here
}
}
}
If indexCard is indexPath of collectionViewCell which you want to animate,
You can call your cell like ->
in RandomizeDealsViewController
#IBAction func didTapRightButton(_ sender: Any) {
guard let indexCard = centeredCollectionViewFlowLayout.currentCenteredPage else { return }
if (indexCard > 0) {
centeredCollectionViewFlowLayout.scrollToPage(index: indexCard + 1, animated: true)
// should call the animation function here
let cell = collectionView!.cellForItemAtIndexPath(indexCard) as? RandomizeDealsCollectionViewCell
cell?.animatePercentDiscountLabel(deals: "deals")
}
}

About a clean common method

It is a process to slide the screen when the keyboard appears, but this process is described by many controllers.
The processing in if and else changes according to each controller.
Is there a way to cleanly share it?
Also, where and how should we commonize?
func keyboardWillChangeFrame(_ notification: Notification) {
if let endFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
var keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
if #available(iOS 11, *) {
if keyboardHeight > 0 {
view.addGestureRecognizer(ui.viewTapGesture)
ui.isHiddenSubmitBtn(false)
ui.isHiddenTextCount(false)
keyboardHeight = keyboardHeight - view.safeAreaInsets.bottom + ui.submitBtn.frame.height + 8
} else {
view.removeGestureRecognizer(ui.viewTapGesture)
ui.isHiddenSubmitBtn(true)
ui.isHiddenTextCount(true)
}
}
ui.textViewBottomConstraint.constant = -keyboardHeight - 8
view.layoutIfNeeded()
}
}
You can use class and protocols, example code below
// Firstly Create Notifier Keyboard
class KeyboardNotifier: NSObject {
// We notify this closures
var keyboardPresent: ((_ height: CGFloat) -> Void)?
var keyboardDismiss: ((_ height: CGFloat) -> Void)?
// Add Notification
func listenKeyboard() {
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillChangeFramex(_:)),
name: UIApplication.keyboardWillChangeFrameNotification,
object: nil)
}
// Handle Notification and call closures
#objc func keyboardWillChangeFramex(_ notification: Notification) {
if let endFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
if #available(iOS 11, *) {
DispatchQueue.main.async { [weak self] in
if keyboardHeight > 0 {
self?.keyboardPresent?(keyboardHeight)
} else {
self?.keyboardDismiss?(keyboardHeight)
}
}
}
}
}
}
// Create a listener protocol
protocol KeyboardListener {
func keyboardPresent(_ height: CGFloat)
func keyboardDismiss(_ height: CGFloat)
var keyboardNotifier: KeyboardNotifier! { get set }
}
// We need a extension for we don't want all viewController
extension KeyboardListener where Self: UIViewController {
// We need call this function viewDidLoad later
func listenKeyboard(keyboardNotifier: KeyboardNotifier) {
keyboardNotifier.keyboardDismiss = { [weak self] height in
self?.keyboardDismiss(height)
}
keyboardNotifier.keyboardPresent = { [weak self] height in
self?.keyboardPresent(height)
}
}
}
// A XViewController want's to listen keyboard
class XViewController: UIViewController, KeyboardListener {
var keyboardNotifier: KeyboardNotifier!
override func viewDidLoad() {
super.viewDidLoad()
// We need instance for life cycle
keyboardNotifier = KeyboardNotifier()
listenKeyboard(keyboardNotifier: keyboardNotifier)
}
func keyboardPresent(_ height: CGFloat) {
// TODO UI
}
func keyboardDismiss(_ height: CGFloat) {
// TODO UI
}
}
// If you want all viewController listen keyboard
class BaseViewController: UIViewController, KeyboardListener {
var keyboardNotifier: KeyboardNotifier!
override func viewDidLoad() {
super.viewDidLoad()
// We need instance for life cycle
keyboardNotifier = KeyboardNotifier()
listenKeyboard(keyboardNotifier: keyboardNotifier)
}
func keyboardPresent(_ height: CGFloat) {
}
func keyboardDismiss(_ height: CGFloat) {
}
}
// This Y ViewController from BaseViewController
class YViewController: BaseViewController {
override func keyboardPresent(_ height: CGFloat) {
}
override func keyboardDismiss(_ height: CGFloat) {
}
}

How to add Gecture for UIView Extension method swift

Hi I want to give the PanGecture Swipe action for UIView on the top of UIViewControllers.for that one I write one common method in view controller extension but its make the entire viewcontroller is swipe.
I want to give the swipe action only for UIView please help to me any idea to change the following code to UIView Extension.
extension UIViewController{
#objc func pangectureRecognizerDismissoneScreen(_ sender: UIPanGestureRecognizer){
var initialTouchPoint: CGPoint = CGPoint(x: 0,y: 0)
let touchPoint = sender.location(in: self.view?.window)
if sender.state == UIGestureRecognizerState.began {
initialTouchPoint = touchPoint
} else if sender.state == UIGestureRecognizerState.changed {
if touchPoint.y - initialTouchPoint.y > 0 {
self.view.frame = CGRect(x: 0, y: touchPoint.y - initialTouchPoint.y, width: self.view.frame.size.width, height: self.view.frame.size.height)
}
} else if sender.state == UIGestureRecognizerState.ended || sender.state == UIGestureRecognizerState.cancelled {
if touchPoint.y - initialTouchPoint.y > 100 {
self.view.backgroundColor = UIColor.clear
self.dismiss(animated: true, completion: nil)
} else {
UIView.animate(withDuration: 0.3, animations: {
self.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
})
}
}
}
}
You can add this UIView extension
import UIKit
import RxSwift
struct AssociatedKeys {
static var actionState: UInt8 = 0
}
typealias ActionTap = () -> Void
extension UIView {
var addAction: ActionTap? {
get {
guard let value = objc_getAssociatedObject(self, &AssociatedKeys.actionState) as? ActionTap else {
return nil
}
return value
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.actionState, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
self.tapWithAnimation()
}
}
func tapWithAnimation() {
self.gestureRecognizers?.removeAll()
let longTap = UILongPressGestureRecognizer(target: self, action: #selector(viewLongTap(_:)))
longTap.minimumPressDuration = 0.035
longTap.delaysTouchesBegan = true
self.addGestureRecognizer(longTap)
}
#objc
func viewLongTap(_ gesture: UILongPressGestureRecognizer) {
if gesture.state != .ended {
animateView(alpha: 0.3)
return
} else if gesture.state == .ended {
let touchLocation = gesture.location(in: self)
if self.bounds.contains(touchLocation) {
animateView(alpha: 1)
addAction?()
return
}
}
animateView(alpha: 1)
}
fileprivate func animateView(alpha: CGFloat) {
UIView.transition(with: self, duration: 0.3,
options: .transitionCrossDissolve, animations: {
self.subviews.forEach { subView in
subView.alpha = alpha
}
})
}
}
Example:
myView.addAction = {
print("click")
}
Use this class extension.
//Gesture extention
extension UIGestureRecognizer {
#discardableResult convenience init(addToView targetView: UIView,
closure: #escaping () -> Void) {
self.init()
GestureTarget.add(gesture: self,
closure: closure,
toView: targetView)
}
}
fileprivate class GestureTarget: UIView {
class ClosureContainer {
weak var gesture: UIGestureRecognizer?
let closure: (() -> Void)
init(closure: #escaping () -> Void) {
self.closure = closure
}
}
var containers = [ClosureContainer]()
convenience init() {
self.init(frame: .zero)
isHidden = true
}
class func add(gesture: UIGestureRecognizer, closure: #escaping () -> Void,
toView targetView: UIView) {
let target: GestureTarget
if let existingTarget = existingTarget(inTargetView: targetView) {
target = existingTarget
} else {
target = GestureTarget()
targetView.addSubview(target)
}
let container = ClosureContainer(closure: closure)
container.gesture = gesture
target.containers.append(container)
gesture.addTarget(target, action: #selector(GestureTarget.target(gesture:)))
targetView.addGestureRecognizer(gesture)
}
class func existingTarget(inTargetView targetView: UIView) -> GestureTarget? {
for subview in targetView.subviews {
if let target = subview as? GestureTarget {
return target
}
}
return nil
}
func cleanUpContainers() {
containers = containers.filter({ $0.gesture != nil })
}
#objc func target(gesture: UIGestureRecognizer) {
cleanUpContainers()
for container in containers {
guard let containerGesture = container.gesture else {
continue
}
if gesture === containerGesture {
container.closure()
}
}
}
}
EDIT 1 :-
Use like this
UIGestureRecognizer.init(addToView: yourView, closure: {
// your code
})

Protocol extension on an ObjC protocol

I have an Objective-C protocol which is used by mostly objective-C objects and one or two Swift objects.
I would like to extend the protocol in Swift and add 2 functions. One to register for a notification and another to handle the notification.
If I add these
func registerForPresetLoadedNotification() {
NSNotificationCenter.defaultCenter().addObserver(self as AnyObject,
selector: #selector(presetLoaded(_:)),
name: kPresetLoadedNotificationName,
object: nil)
}
func presetLoaded(notification: NSNotification) {
}
I get an error on the #selector which says:
Argument of '#selector' refers to a method that is not exposed to Objective-C
If I then mark presetLoaded as #objc I get an error which says:
#objc can only be used with members of classes, #objc protocols, and concrete extensions of classes
I also cannot mark the protocol extension as #objc
When I create the Objective-C protocol as a Swift protocol I get the same error.
Is there a way to achieve this that will work for Objective-C and Swift classes that use the protocol?
Indeed, you can't really mark a function of a protocol extension as #objc (or dynamic, which is equivalent by the way). Only methods of a class are allowed to be dispatched by Objective-C runtime.
In your particular case, if you really want to make it through protocol extension, I can propose the following solution (assuming your original protocol is named ObjcProtocol).
Let's make a wrapper for our notification handler:
final class InternalNotificationHandler {
private let source: ObjcProtocol
init(source: ObjcProtocol) {
// We require source object in case we need access some properties etc.
self.source = source
}
#objc func presetLoaded(notification: NSNotification) {
// Your notification logic here
}
}
Now we need extend our ObjcProtocol to introduce required logic
import Foundation
import ObjectiveC
internal var NotificationAssociatedObjectHandle: UInt8 = 0
extension ObjcProtocol {
// This stored variable represent a "singleton" concept
// But since protocol extension can only have stored properties we save it via Objective-C runtime
private var notificationHandler: InternalNotificationHandler {
// Try to an get associated instance of our handler
guard let associatedObj = objc_getAssociatedObject(self, &NotificationAssociatedObjectHandle)
as? InternalNotificationHandler else {
// If we do not have any associated create and store it
let newAssociatedObj = InternalNotificationHandler(source: self)
objc_setAssociatedObject(self,
&NotificationAssociatedObjectHandle,
newAssociatedObj,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return newAssociatedObj
}
return associatedObj
}
func registerForPresetLoadedNotification() {
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(notificationHandler.presetLoaded(_:)),
name: kPresetLoadedNotificationName,
object: self)
}
func unregisterForPresetLoadedNotification() {
// Clear notification observer and associated objects
NSNotificationCenter.defaultCenter().removeObserver(self,
name: kPresetLoadedNotificationName,
object: self)
objc_removeAssociatedObjects(self)
}
}
I know this might look not so elegant, so I'd really consider changing a core approach.
One note: You do might want to restrict your protocol extension
extension ObjcProtocol where Self: SomeProtocolOrClass
I found a way to do it :) Just avoid #objc all together :D
//Adjusts UITableView content height when keyboard show/hide
public protocol KeyboardObservable: NSObjectProtocol {
func registerForKeyboardEvents()
func unregisterForKeyboardEvents()
}
extension KeyboardObservable where Self: UITableView {
public func registerForKeyboardEvents() {
let defaultCenter = NotificationCenter.default
var tokenShow: NSObjectProtocol!
tokenShow = defaultCenter.addObserver(forName: .UIKeyboardDidShow, object: nil, queue: nil) { [weak self] (notification) in
guard self != nil else {
defaultCenter.removeObserver(tokenShow)
return
}
self!.keyboardWilShow(notification as NSNotification)
}
var tokenHide: NSObjectProtocol!
tokenHide = defaultCenter.addObserver(forName: .UIKeyboardWillHide, object: nil, queue: nil) { [weak self] (notification) in
guard self != nil else {
defaultCenter.removeObserver(tokenHide)
return
}
self!.keyboardWilHide(notification as NSNotification)
}
}
private func keyboardDidShow(_ notification: Notification) {
let rect = ((notification as NSNotification).userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let height = rect.height
var insets = UIEdgeInsetsMake(0, 0, height, 0)
insets.top = contentInset.top
contentInset = insets
scrollIndicatorInsets = insets
}
private func keyboardWillHide(_ notification: Notification) {
var insets = UIEdgeInsetsMake(0, 0, 0, 0)
insets.top = contentInset.top
UIView.animate(withDuration: 0.3) {
self.contentInset = insets
self.scrollIndicatorInsets = insets
}
}
public func unregisterForKeyboardEvents() {
NotificationCenter.default.removeObserver(self)
}
}
Example
class CreateStudentTableView: UITableView, KeyboardObservable {
init(frame: CGRect, style: UITableViewStyle) {
super.init(frame: frame, style: style)
registerForKeyboardEvents()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How using class func to call a method from another class in SWIFT

I would to call from the GameViewController my timer method (written in my GameScene) in order to pause the game with an UIButton initialized in GameViewController class.
I'm trying to use a class func like this :
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
GameScene.startTimer()
}
class func startTimer(){
timerCount = NSTimer.scheduledTimerWithTimeInterval(1.0
, target: self, selector: Selector("updateTimer:"), userInfo: nil, repeats: true)
}
func updateTimer(dt:NSTimer){
counter--
counterGame++
if counter<0{
timerCount.invalidate()
removeCountDownTimerView()
} else{
labelCounter.text = "\(counter)"
}
if counterGame>20{
balloon.size = CGSizeMake(50, 50)
}
if counterGame>40{
self.physicsWorld.gravity = CGVectorMake(0, -0.8)
}
if counterGame>60{
self.physicsWorld.gravity = CGVectorMake(0, -1)
}
}
func removeCountDownTimerView(){
defaults.setInteger(balloonDestroyed, forKey: "score")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let settingController: UIViewController = storyboard.instantiateViewControllerWithIdentifier("GameOverViewController") as UIViewController
let vc = self.view?.window?.rootViewController
vc?.presentViewController(settingController, animated: true, completion: nil)
}
}
But this code return an error :
[Funfair_balloon.GameScene updateTimer:]: unrecognized selector sent to class 0x10b13d940
When I don't use the class func the app works perfectly but I can't stop the timer with my UIButton.
What did I do wrong?
Please note that when using class func, you cannot use the variables that were initialized in that class.
So for example, you cannot use the variable timerCount if it was initialized in the class GameScene.
I'm not sure why you want to use class functions. The following should work using just the current instance of GameScene. Note that the var timerCount is an optional (since you can't easily override the init) until such times as you create it in startTimer(), and so you will have to unwrap it when you eventually invalidate it.
class GameScene: SKScene, SKPhysicsContactDelegate {
var timerCount: NSTimer? = nil
var counter = 100 // or whatever
override func didMoveToView(view: SKView) {
self.startTimer()
}
func startTimer() {
self.timerCount = NSTimer.scheduledTimerWithTimeInterval(1.0
, target: self, selector: Selector("updateTimer:"), userInfo: nil, repeats: true)
}
func updateTimer(dt: NSTimer) {
// Do stuff
counter--
if counter < 0 {
self.timerCount!.invalidate() // or dt.invalidate()
self.removeCountDownTimerView()
} else {
// update counter label
}
// Do more stuff
}
func removeCountDownTimerView() {
// Do stuff
}
}