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) {
}
}
Related
This is my code at the moment I instead of using progressView as
self.progressView = showProgressView()
I want to use it as self.showProgress() as a one liner
this is my current protocol
protocol Loadable {
var progressView: ProgressView? { get }
func showProgressView() -> ProgressView
func hideProgressView()
}
extension Loadable where Self: UIViewController {
func showProgressView() -> ProgressView {
let pv = ProgressView(frame: CGRect.zero)
view.addSubview(pv)
pv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(pv.setConstraints(boundsOf: view))
UIView.animate(withDuration: 0.3) {
pv.alpha = 1.0
}
return pv
}
func hideProgressView() {
guard let progressView = progressView else { return }
UIView.animate(withDuration: 0.6) {
progressView.alpha = 0.0
} completion: { finished in
progressView.removeFromSuperview()
}
}
}```
//this is my current usage on the code
#objc private func loadData() {
print("Calling API")
self.progressView = showProgressView()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0){
self.civilizationData()
self.hideProgressView()
}
}
//tried using this method
protocol Loadable {
var progressView: ProgressView? { get }
func showProgressView()
func hideProgressView()
}
extension Loadable where Self: UIViewController {
func showProgressView() {
let progressView = ProgressView(frame: CGRect.zero)
view.addSubview(progressView)
progressView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(progressView.setConstraints(boundsOf: view))
UIView.animate(withDuration: 0.3) {
progressView.alpha = 1.0
}
}
//but the added subview of my progressview doesn't disappear
protocol Loadable {
var progressView: ProgressView? { get }
func showProgressView()
func hideProgressView()
}
extension Loadable where Self: UIViewController {
func showProgressView() {
let progressView = ProgressView(frame: CGRect.zero)
view.addSubview(progressView)
progressView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(progressView.setConstraints(boundsOf: view))
UIView.animate(withDuration: 0.3) {
progressView.alpha = 1.0
}
}
I have a problem with IGListKit sections deallocating. Trying to debug the issue with Xcode memory graph.
My setup is AuthController -> AuthViewModel -> AuthSocialSectionController -> AuthSocialViewModel and some other sections.
AuthController gets presented from several parts of the app if user is not logged in. When I tap close, AuthViewModel and AuthController gets deallocated, but it's underlying sections does not. Memory graph shows nothing leaked in this case, but deinit methods doesn't get called.
But when I'm trying to authorize with social account (successfully) and then look at the memory graph, it shows that sections, that doesn't get deallocated like this:
In this case AuthViewModel doesn't get deallocated either, but after some time it does, but it can happen or not.
I checked every closure and delegate for weak reference, but still no luck.
My code, that I think makes most sense:
class AuthViewController: UIViewController {
fileprivate let collectionView: UICollectionView = UICollectionView(frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout())
lazy var adapter: ListAdapter
= ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
fileprivate lazy var previewProxy: SJListPreviewProxy = {
SJListPreviewProxy(adapter: adapter)
}()
fileprivate let viewModel: AuthViewModel
fileprivate let disposeBag = DisposeBag()
init(with viewModel: AuthViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
hidesBottomBarWhenPushed = true
setupObservers()
}
private func setupObservers() {
NotificationCenter.default.rx.notification(.SJAProfileDidAutoLogin)
.subscribe(
onNext: { [weak self] _ in
self?.viewModel.didSuccessConfirmationEmail()
self?.viewModel.recoverPasswordSuccess()
})
.disposed(by: disposeBag)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
// MARK: - Private
#objc private func close() {
dismiss(animated: true, completion: nil)
}
/// Метод настройки экрана
private func setup() {
if isForceTouchEnabled() {
registerForPreviewing(with: previewProxy, sourceView: collectionView)
}
view.backgroundColor = AppColor.instance.gray
title = viewModel.screenName
let item = UIBarButtonItem(image: #imageLiteral(resourceName: "close.pdf"), style: .plain, target: self, action: #selector(AuthViewController.close))
item.accessibilityIdentifier = "auth_close_btn"
asViewController.navigationItem.leftBarButtonItem = item
navigationItem.titleView = UIImageView(image: #imageLiteral(resourceName: "logo_superjob.pdf"))
collectionViewSetup()
}
// Настройка collectionView
private func collectionViewSetup() {
collectionView.keyboardDismissMode = .onDrag
collectionView.backgroundColor = AppColor.instance.gray
view.addSubview(collectionView)
adapter.collectionView = collectionView
adapter.dataSource = self
collectionView.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
}
}
// MARK: - DataSource CollectionView
extension AuthViewController: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return viewModel.sections(for: listAdapter)
}
func listAdapter(_: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
return viewModel.createListSectionController(for: object)
}
func emptyView(for _: ListAdapter) -> UIView? {
return nil
}
}
// MARK: - AuthViewModelDelegate
extension AuthViewController: AuthViewModelDelegate {
func hideAuth(authSuccessBlock: AuthSuccessAction?) {
dismiss(animated: true, completion: {
authSuccessBlock?()
})
}
func reload(animated: Bool, completion: ((Bool) -> Void)? = nil) {
adapter.performUpdates(animated: animated, completion: completion)
}
func showErrorPopover(with item: CommonAlertPopoverController.Item,
and anchors: (sourceView: UIView, sourceRect: CGRect)) {
let popover = CommonAlertPopoverController(with: item,
preferredContentWidth: view.size.width - 32.0,
sourceView: anchors.sourceView,
sourceRect: anchors.sourceRect,
arrowDirection: .up)
present(popover, animated: true, completion: nil)
}
}
class AuthViewModel {
fileprivate let assembler: AuthSectionsAssembler
fileprivate let router: AuthRouter
fileprivate let profileFacade: SJAProfileFacade
fileprivate let api3ProfileFacade: API3ProfileFacade
fileprivate let analytics: AnalyticsProtocol
fileprivate var sections: [Section] = []
weak var authDelegate: AuthDelegate?
weak var vmDelegate: AuthViewModelDelegate?
var authSuccessBlock: AuthSuccessAction?
private lazy var socialSection: AuthSocialSectionViewModel = { [unowned self] in
self.assembler.socialSection(delegate: self)
}()
init(assembler: AuthSectionsAssembler,
router: AuthRouter,
profileFacade: SJAProfileFacade,
api3ProfileFacade: API3ProfileFacade,
analytics: AnalyticsProtocol,
delegate: AuthDelegate? = nil,
purpose: Purpose) {
self.purpose = purpose
authDelegate = delegate
self.assembler = assembler
self.router = router
self.profileFacade = profileFacade
self.api3ProfileFacade = api3ProfileFacade
self.analytics = analytics
sections = displaySections()
}
private func authDisplaySections() -> [Section] {
let sections: [Section?] = [vacancySection,
authHeaderSection,
socialSection,
authLoginPasswordSection,
signInButtonSection,
switchToSignUpButtonSection,
recoverPasswordSection]
return sections.compactMap { $0 }
}
}
class AuthSocialSectionController: SJListSectionController, SJUpdateCellsLayoutProtocol {
fileprivate let viewModel: AuthSocialSectionViewModel
init(viewModel: AuthSocialSectionViewModel) {
self.viewModel = viewModel
super.init()
minimumInteritemSpacing = 4
viewModel.vmDelegate = self
}
override func cellType(at _: Int) -> UICollectionViewCell.Type {
return AuthSocialCell.self
}
override func cellInitializationType(at _: Int) -> SJListSectionCellInitializationType {
return .code
}
override func configureCell(_ cell: UICollectionViewCell, at index: Int) {
guard let itemCell = cell as? AuthSocialCell else {
return
}
let item = viewModel.item(at: index)
itemCell.imageView.image = item.image
}
override func separationStyle(at _: Int) -> SJCollectionViewCellSeparatorStyle {
return .none
}
}
extension AuthSocialSectionController {
override func numberOfItems() -> Int {
return viewModel.numberOfItems
}
override func didSelectItem(at index: Int) {
viewModel.didSelectItem(at: index)
}
}
// MARK: - AuthSocialSectionViewModelDelegate
extension AuthSocialSectionController: AuthSocialSectionViewModelDelegate {
func sourceViewController() -> UIViewController {
return viewController ?? UIViewController()
}
}
protocol AuthSocialSectionDelegate: class {
func successfullyAuthorized(type: SJASocialAuthorizationType)
func showError(with error: Error)
}
protocol AuthSocialSectionViewModelDelegate: SJListSectionControllerOperationsProtocol, ViewControllerProtocol {
func sourceViewController() -> UIViewController
}
class AuthSocialSectionViewModel: NSObject {
struct Item {
let image: UIImage
let type: SJASocialAuthorizationType
}
weak var delegate: AuthSocialSectionDelegate?
weak var vmDelegate: AuthSocialSectionViewModelDelegate?
fileprivate var items: [Item]
fileprivate let api3ProfileFacade: API3ProfileFacade
fileprivate let analyticsFacade: SJAAnalyticsFacade
fileprivate var socialButtonsDisposeBag = DisposeBag()
init(api3ProfileFacade: API3ProfileFacade,
analyticsFacade: SJAAnalyticsFacade) {
self.api3ProfileFacade = api3ProfileFacade
self.analyticsFacade = analyticsFacade
items = [
Item(image: #imageLiteral(resourceName: "ok_icon.pdf"), type: .OK),
Item(image: #imageLiteral(resourceName: "vk_icon.pdf"), type: .VK),
Item(image: #imageLiteral(resourceName: "facebook_icon.pdf"), type: .facebook),
Item(image: #imageLiteral(resourceName: "mail_icon.pdf"), type: .mail),
Item(image: #imageLiteral(resourceName: "google_icon.pdf"), type: .google),
Item(image: #imageLiteral(resourceName: "yandex_icon.pdf"), type: .yandex)
]
if analyticsFacade.isHHAuthAvailable() {
items.append(Item(image: #imageLiteral(resourceName: "hh_icon"), type: .HH))
}
}
// MARK: - actions
func didSelectItem(at index: Int) {
guard let vc = vmDelegate?.sourceViewController() else {
return
}
let itemType: SJASocialAuthorizationType = items[index].type
socialButtonsDisposeBag = DisposeBag()
api3ProfileFacade.authorize(with: itemType, sourceViewController: vc)
.subscribe(
onNext: { [weak self] _ in
self?.delegate?.successfullyAuthorized(type: itemType)
},
onError: { [weak self] error in
if case let .detailed(errorModel)? = error as? ApplicantError {
self?.vmDelegate?.asViewController.showError(with: errorModel.errors.first?.detail ?? "")
} else {
self?.vmDelegate?.asViewController.showError(with: "Неизвестная ошибка")
}
})
.disposed(by: socialButtonsDisposeBag)
}
}
// MARK: - DataSource
extension AuthSocialSectionViewModel {
var numberOfItems: Int {
return items.count
}
func item(at index: Int) -> Item {
return items[index]
}
}
// MARK: - ListDiffable
extension AuthSocialSectionViewModel: ListDiffable {
func diffIdentifier() -> NSObjectProtocol {
return ObjectIdentifier(self).hashValue as NSObjectProtocol
}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
return object is AuthSocialSectionViewModel
}
}
Where assembler is responsible for creating everyting, for example AuthSocialSection:
func socialSection(delegate: AuthSocialSectionDelegate?) -> AuthSocialSectionViewModel {
let vm = AuthSocialSectionViewModel(api3ProfileFacade: api3ProfileFacade,
analyticsFacade: analyticsFacade)
vm.delegate = delegate
return vm
}
How can I properly debug this issue? Any advice or help is really appreciated
Found an issue in AuthSocialSectionController. Somehow passing viewController from IGList context through delegates caused memory issues. When I commented out the viewModel.vmDelegate = self the issue was gone.
That explains why the AuthViewModel was deallocating properly when I hit close button without attempting to authorize. Only when I hit authorize, that viewController property was called.
Thanks for help #vpoltave
This lines from your AuthViewController can this cause leaks?
// adapter has viewController: self
lazy var adapter: ListAdapter
= ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
fileprivate lazy var previewProxy: SJListPreviewProxy = {
// capture self.adapter ?
SJListPreviewProxy(adapter: adapter)
}()
I'm not sure, but at least you can try :)
UPDATE
I was wondering about this lazy closures and self inside, it won't create retain cycle because lazy initialization are #nonescaping.
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
})
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()
}
I have tried a number of times and the best i get is there would be an animation which never cancels or stop regardless of the command i use.
After following #Mattias example, i updated my code and looks something like this:
// DESIGN ANIMATION... TKTRANSITIONSUBMITBUTTON
#IBOutlet weak var btnFromNib: TKTransitionSubmitButton!
#IBAction func onTapButton(sender: AnyObject) {
btnFromNib.startLoadingAnimation()
if let email = self.emailField.text where email != "", let password = self.passwordField.text where password != "" {
DataService.ds.REF_BASE.authUser(email, password: password, withCompletionBlock: { error, authData in
if error != nil {
self.btnFromNib.returnToOriginalState()
if error.code == STATUS_ACCOUNT_NONEXIST {
self.showErrorAlert("This User does not exist", msg: "Please Sign Up")
} else {
}
} else {
self.btnFromNib.startFinishAnimation(1, completion: {
let myTabbarController = self.storyboard?.instantiateViewControllerWithIdentifier("myTabbarController") as! UITabBarController
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = myTabbarController
myTabbarController.transitioningDelegate = self
})
}
})
}
}
The button yet keeps spinning / animating without stopping. After checking the custom animation class the function inherits from :
public func startLoadingAnimation() {
self.cachedTitle = titleForState(.Normal)
self.setTitle("", forState: .Normal)
self.shrink()
NSTimer.schedule(delay: shrinkDuration - 0.25) { timer in
self.spiner.animation()
}
}
public func startFinishAnimation(delay: NSTimeInterval, completion:(()->())?) {
NSTimer.schedule(delay: delay) { timer in
self.didEndFinishAnimation = completion
self.expand()
self.spiner.stopAnimation()
}
}
public func animate(duration: NSTimeInterval, completion:(()->())?) {
startLoadingAnimation()
startFinishAnimation(duration, completion: completion)
}
public override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
let a = anim as! CABasicAnimation
if a.keyPath == "transform.scale" {
didEndFinishAnimation?()
NSTimer.schedule(delay: 1) { timer in
self.returnToOriginalState()
}
}
}
func returnToOriginalState() {
self.layer.removeAllAnimations()
self.setTitle(self.cachedTitle, forState: .Normal)
}
I noticed it had a public overide func animationDidStop(anim: CAAnimation, finished: Bool) to be the function to stop the animation. But when i use it, i get this error!
How do i rightfully get this to work? ...
Thanks in Advance
** UPDATED QUESTION **
I checked the code of TKTransitionSubmitButton and there are public methods called startLoadingAnimation(), returnToOriginalState() and startFinishAnimation().
I suggest:
Button tapped
startLoadingAnimation()
Check credentials
If wrong/error: returnToOriginalState()
If correct: startFinishAnimation()
Transition, from TKTransitionSubmitButton documentation:
btn.startFinishAnimation {
//Your Transition
let secondVC = SecondViewController()
secondVC.transitioningDelegate = self
self.presentViewController(secondVC, animated: true, completion: nil)
}
Edit: As far as I can see .animate() of the class calls both the start and finish animation, and that's why you can't cancel it.
EDIT2 This one works as intended for me (however I'm not sure about the static cornerRadius)
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var submitButton: TKTransitionSubmitButton!
override func viewDidLoad() {
super.viewDidLoad()
submitButton.layer.cornerRadius = 15
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonPressed(sender: AnyObject) {
submitButton.startLoadingAnimation()
delay(2, closure: {
self.checkCredentials()
})
}
func checkCredentials()
{
//FAKING WRONG CREDENTIALS
let userAndPasswordCorrect = false
if !userAndPasswordCorrect
{
submitButton.returnToOriginalState()
//Alert or whatever
}
}
//Faking network delay
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
}
I checked the code of TKTransitionSubmitButton.
I resolve this issue. please try to stop animation under DispatchQueue.main.async and working well.
'func apicallforLogin() {
let urlString = "http://"
guard let requestUrl = URL(string:urlString) else { return }
let request = URLRequest(url:requestUrl)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
if error == nil,let usableData = data {
DispatchQueue.main.async {
self.btnLogin.startFinishAnimation(0.1, completion: {
let HomeVC = self.storyboard?.instantiateViewController(withIdentifier: "HomeViewController") as! HomeViewController
HomeVC.transitioningDelegate = self
self.navigationController?.pushViewController(HomeVC, animated: false)
})
}
print(usableData)
}else{
DispatchQueue.main.async {
self.btnLogin.returnToOriginalState()
}
}
}
task.resume()'
Well you might have got the answer by now but just I would like to give my answer. You can just copy below code and everything will work fine. I have made the change and had created the pull request for this issue.
import Foundation
import UIKit
#IBDesignable
open class TKTransitionSubmitButton : UIButton, UIViewControllerTransitioningDelegate, CAAnimationDelegate {
lazy var spiner: SpinerLayer! = {
let s = SpinerLayer(frame: self.frame)
return s
}()
#IBInspectable open var spinnerColor: UIColor = UIColor.white {
didSet {
spiner.spinnerColor = spinnerColor
}
}
open var didEndFinishAnimation : (()->())? = nil
let springGoEase = CAMediaTimingFunction(controlPoints: 0.45, -0.36, 0.44, 0.92)
let shrinkCurve = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
let expandCurve = CAMediaTimingFunction(controlPoints: 0.95, 0.02, 1, 0.05)
let shrinkDuration: CFTimeInterval = 0.1
#IBInspectable open var normalCornerRadius:CGFloat = 0.0 {
didSet {
self.layer.cornerRadius = normalCornerRadius
}
}
var cachedTitle: String?
var isAnimating = false
public override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
public required init!(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.setup()
}
func setup() {
self.clipsToBounds = true
spiner.spinnerColor = spinnerColor
}
open func startLoadingAnimation() {
self.isAnimating = true
self.cachedTitle = title(for: UIControlState())
self.setTitle("", for: UIControlState())
self.layer.addSublayer(spiner)
// Animate
self.cornerRadius()
self.shrink()
_ = Timer.schedule(delay: self.shrinkDuration - 0.25) { timer in
self.spiner.animation()
}
}
open func startFinishAnimation(_ delay: TimeInterval,_ animation: CAMediaTimingFunction, completion:(()->())?) {
self.isAnimating = true
_ = Timer.schedule(delay: delay) { timer in
self.didEndFinishAnimation = completion
self.expand(animation)
self.spiner.stopAnimation()
}
}
open func animate(_ duration: TimeInterval,_ animation: CAMediaTimingFunction, completion:(()->())?) {
startLoadingAnimation()
startFinishAnimation(duration, animation, completion: completion)
}
open func setOriginalState() {
self.returnToOriginalState()
self.spiner.stopAnimation()
}
public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
let a = anim as! CABasicAnimation
if a.keyPath == "transform.scale" {
didEndFinishAnimation?()
_ = Timer.schedule(delay: 1) { timer in
self.returnToOriginalState()
}
}
}
open func returnToOriginalState() {
self.spiner.removeFromSuperlayer()
self.layer.removeAllAnimations()
self.setTitle(self.cachedTitle, for: UIControlState())
self.spiner.stopAnimation()
self.isAnimating = false
}
func cornerRadius() {
let cornerRadiusAnim = CABasicAnimation(keyPath: "cornerRadius")
// cornerRadiusAnim.fromValue = frame.width
cornerRadiusAnim.toValue = frame.height/2
cornerRadiusAnim.duration = shrinkDuration
cornerRadiusAnim.timingFunction = shrinkCurve
cornerRadiusAnim.fillMode = kCAFillModeForwards
cornerRadiusAnim.isRemovedOnCompletion = false
layer.add(cornerRadiusAnim, forKey: cornerRadiusAnim.keyPath)
}
func shrink() {
let shrinkAnim = CABasicAnimation(keyPath: "bounds.size.width")
shrinkAnim.beginTime = CACurrentMediaTime() + 0.1
shrinkAnim.fromValue = frame.width
shrinkAnim.toValue = frame.height
shrinkAnim.duration = shrinkDuration
shrinkAnim.timingFunction = shrinkCurve
shrinkAnim.fillMode = kCAFillModeForwards
shrinkAnim.isRemovedOnCompletion = false
layer.add(shrinkAnim, forKey: shrinkAnim.keyPath)
}
func expand(_ animation: CAMediaTimingFunction) {
let expandAnim = CABasicAnimation(keyPath: "transform.scale")
expandAnim.fromValue = 1.0
expandAnim.toValue = 26.0
expandAnim.timingFunction = animation
expandAnim.duration = 0.3
expandAnim.delegate = self
expandAnim.fillMode = kCAFillModeForwards
expandAnim.isRemovedOnCompletion = false
layer.add(expandAnim, forKey: expandAnim.keyPath)
}
}