Button Click Twice - swift

As we all know, to avoid clicking twice, we can set the code bellow on the tap method and add a HUD such as SVProgress.show().
isUserInteractionEnabled = false
After the network request, set it to true and SVProgress.dismiss().
I wonder if there is a method to extract the function for those button which needs to send a request. I have thought to use method swizzling. Add this to the button extension, the codes is bellow. It seems not good. Do you guys have some good ways to extract the function? Using inheritance, protocol or something else?
extension UIButton {
private struct AssociatedKeys {
static var cp_submitComplete = "cp_submitComplete"
static var cp_defaultMessage:String = NSLocalizedString("Loading", comment: "prompt")
static var cp_customMessage = "cp_customMessage"
}
var submitNotComplete: Bool {
get {
let objc_Get = objc_getAssociatedObject(self, &AssociatedKeys.cp_submitComplete)
if objc_Get != nil {
if let objc_Get = objc_Get as? Bool, objc_Get == true {
return true
}
return false
} else {
return false
}
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.cp_submitComplete, newValue as Bool, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
if !newValue {
isUserInteractionEnabled = true
SVProgressHUD.dismiss()
}
}
}
var customMessage: String {
get {
let cp_customMessage = objc_getAssociatedObject(self, &AssociatedKeys.cp_customMessage)
if let message = cp_customMessage {
return message as! String
} else {
return AssociatedKeys.cp_defaultMessage
}
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.cp_customMessage, newValue as String, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
override open class func initialize() {
if self == UIButton.self {
DispatchQueue.once(NSUUID().uuidString, block: {
let systemSel = #selector(UIButton.sendAction(_:to:for:))
let swizzSel = #selector(UIButton.cpSendAction(_:to:for:))
let systemMethod = class_getInstanceMethod(self, systemSel)
let swizzMethod = class_getInstanceMethod(self, swizzSel)
let isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod))
if isAdd {
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
} else {
method_exchangeImplementations(systemMethod, swizzMethod);
}
})
}
}
private dynamic func cpSendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
cpSendAction(action, to: target, for: event)
if submitNotComplete {
//begin submit
isUserInteractionEnabled = false
SVProgressHUD.show(withStatus: customMessage)
}
}
}

I think it's a bad idea to handle this kind of logic in UIButton. I would rather make the view controller responsible for enabling/disabling the button.
func handleTap(_ sender: UIButton) {
sender.isEnabled = false
SVProgressHUD.show(withStatus: customMessage)
doSomeTaskAsync(withCompletion: {
sender.isEnabled = true
SVProgressHUD.dismiss()
})
}

Related

How do I modify function to be used in MVP architecture?

I have the function below. It works properly.
When a user types any character it validates the user input and hides some imageView based on the input.
#IBAction func onEmailValueChanged(_ sender: UITextField) {
let hasMinimumLength = TextValidationHelper.validateHasMinimumLength(password: sender.text!)
passLengthCheckmarkImageView.isHidden = hasMinimumLength ? false : true
let hasCapitalLetter = TextValidationHelper.validateHasCapitalLetter(password: sender.text!)
passHasUppercaseCheckmarkImageView.isHidden = hasCapitalLetter ? false : true
let hasNumber = TextValidationHelper.validateHasNumber(password: sender.text!)
passHasNumberCheckmarkImageView.isHidden = hasNumber ? false : true
let hasSpecialCharacter = TextValidationHelper.validateHasSpecialCharacter(password: sender.text!)
passHasSymbolCheckmarkImageView.isHidden = hasSpecialCharacter ? false : true
resetButton.isHidden = hasMinimumLength && hasCapitalLetter && hasNumber && hasSpecialCharacter ? false : true
}
But now I want to apply an MVP model on this function to remove the function from the ViewController file.
How can I do that?
Do I need to publish more code to make it possible to create an answer for this question?
It is not a good practice to use any architectural pattern only for method. So assuming you are having a complete app with many classes or files.
An important thing is that it is not fixed/compulsory to use any specific pattern. It actually depends on the code, sometimes you end up writing much code just to handle a method. So try to think the optimal approach to make the code more testable and scalable.
But for your reference, you can check the following code:
On ViewController:
lazy var presenter:Presenter = Presenter(view:self)
#IBAction func onEmailValueChanged(_ sender: UITextField) {
presenter.validateHasMinimumLength(password: sender.text!)
presenter.validateHasCapitalLetter(password: sender.text!)
presenter.validateHasNumber(password: sender.text!)
presenter.validateHasSpecialCharacter(password: sender.text!)
}
//Adopting ViewController:PrensenterViewProtocol on ViewController
extension ViewController:PrensenterViewProtocol {
func updateLengthCheckmarkImageView(isHidden:Bool) {
passLengthCheckmarkImageView.isHidden = isHidden
}
func updateUpperCaseCheckmarkImageView(isHidden:Bool) {
passHasUppercaseCheckmarkImageView.isHidden = isHidden
}
func updateNumberCheckmarkImageView(isHidden:Bool) {
passHasNumberCheckmarkImageView.isHidden = isHidden
}
func updateSymbolCheckmarkImageView(isHidden:Bool) {
passHasSymbolCheckmarkImageView.isHidden = isHidden
}
func updateResetButton(isHidden:Bool) {
resetButton.isHidden = isHidden
}
}
PresenterView protocol as:
protocol PrensenterViewProtocol:NSObjectProtocol {
func updateLengthCheckmarkImageView(isHidden:Bool)
func updateUpperCaseCheckmarkImageView(isHidden:Bool)
func updateNumberCheckmarkImageView(isHidden:Bool)
func updateSymbolCheckmarkImageView(isHidden:Bool)
func updateResetButton(isHidden:Bool)
}
Presenter as:
class Presenter {
weak var view:PrensenterViewProtocol!
private var hasMinimumLength:Bool = false
private var hasCapitalLetter:Bool = false
private var hasNumber:Bool = false
private var hasSpecialCharacter:Bool = false
init(view:PrensenterViewProtocol) {
self.view = view
}
func validateHasMinimumLength(password:String?) {
hasMinimumLength = TextValidationHelper.validateHasMinimumLength(password: password)
self.view.updateLengthCheckmarkImageView(isHidden: hasMinimumLength)
checkAllValidations()
}
func validateHasCapitalLetter(password:String?) {
hasCapitalLetter = TextValidationHelper.validateHasCapitalLetter(password: password)
self.view.updateUpperCaseCheckmarkImageView(isHidden:hasCapitalLetter )
checkAllValidations()
}
func validateHasNumber(password:String?) {
hasNumber = TextValidationHelper.validateHasNumber(password: password)
self.view.updateNumberCheckmarkImageView(isHidden: hasNumber)
checkAllValidations()
}
func validateHasSpecialCharacter(password:String?) {
hasSpecialCharacter = TextValidationHelper.validateHasSpecialCharacter(password: password)
self.view.updateSymbolCheckmarkImageView(isHidden: hasSpecialCharacter)
checkAllValidations()
}
func checkAllValidations() {
let areAllValid:Bool = hasMinimumLength && hasCapitalLetter && hasNumber && hasSpecialCharacter ? false : true
self.view.updateResetButton(isHidden: areAllValid)
}
}

init class from another class swift

I have this ScreenSecurityWorker
final class ScreenSecurityWorker {
weak var delegate: ScreenSecurityWorkerDelegate?
private var isPassPhraseOn: Bool { return SettingsManager.shared.load(.passPhrase) }
private var isPassPhraseForced: Bool { return environment.forcePassPhraseEnabled }
var screenSecurityEnabled = false
inside my Mainrouter I am changing screenSecurityEnabled bool value to true at activateScreenSecurity
func activateScreenSecurity() {
guard !screenSecurityWorker.passwordSecurityInProgress else { return }
screenSecurityVC = ScreenSecurityBuiler.make()
app.dismissKeyboardOnTopVC()
app.window.rootViewController = screenSecurityVC
screenSecurityWorker.passPhareSuccess = false
screenSecurityWorker.screenSecurityEnabled = true
}
I am trying to reach to screenSecurityEnabled from ChatVC
but if I init it as
let screenSecurityWorker = ScreenSecurityWorker()
it has no values inside at all.
if I try like this it gives error
let screenSecurityWorker : ScreenSecurityWorker?
init(screenSecurityWorker: ScreenSecurityWorker){
self.screenSecurityWorker = screenSecurityWorker
}
I need this screenSecurityWorker.screenSecurityEnabled to change subviews in this function inside ChatVC
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if screenSecurityWorker!.screenSecurityEnabled {
let place = chatTextAreaContainerView.bounds.height + CGFloat(lastContentOffset ?? collectionView.contentOffset.y - chatTextAreaContainerView.bounds.height)
self.collectionView.contentOffset.y = place
screenSecurityWorker!.screenSecurityEnabled = false
}
}

Swift: array is set but is then empty when accessed

I have an array which is set once a specific API is called. However when I then try to access this array at a later point it is empty again.
The class in question looks like this:
class SearchableLoginFormField: LoginFormField {
weak var delegate: PopoverPresentableDelegate?
var selectedObject: Selectable?
let popoverType: PopoverType
var sourceList = [Selectable]() {
didSet {
// Field set
}
}
private lazy var selectionPopover: ContainerPopover = {
let popover = LoginFormPopover(objectSelected: { object in
self.selectedObject = object
self.text = object.selectionName
self.selectionPopover.dismiss(animated: true)
}, popoverType: self.popoverType)
return popover
}()
init(popoverType: PopoverType, fieldTitle: String, fieldIcon: UIImage?,
colorScheme: UIColor?, returnAction: (() -> Void)?) {
self.popoverType = popoverType
super.init(fieldTitle: fieldTitle, fieldIcon: fieldIcon, colorScheme: colorScheme, returnAction: returnAction)
configureFormField()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureFormField() {
textDidChangeAction = { [weak self] in
/// We clear the selectedAirport each time user searches in field so that we know whether we need to trigger shouldEndEditingAction or not
/// (which we only trigger if user ends action and selectedAirport is nil)
self?.selectedObject = nil
self?.configureField(with: I6TextInputKeyboardSettings(
capitalization: .allCharacters,
spellCheck: .no
))
guard let self = self else { return }
self.searchObjects(
field: self,
popover: self.selectionPopover,
sourceList: self.sourceList,
dataPopover: self.selectionPopover)
}
self.shouldEndEditingAction = { [weak self] in
if self?.selectedObject == nil {
let filteredText = self?.text.replacingOccurrences(of: "-", with: "") // We remove the "-" if user has entered it so that reg can still be validated
self?.text = filteredText ?? ""
self?.verifyFieldInput()
}
}
}
private func searchObjects<T>(field: I6TextField, popover: ContainerPopover, sourceList: [T], dataPopover: ContainerPopover) {
if field.text.isEmpty {
dataPopover.dismiss(animated: true, completion: nil)
} else {
let filterCompletion: (Int) -> () = { count in
dataPopover.sourceView = field
// Present if needed
let isPopoverPresented = dataPopover.isVisiblyPresented
if (!dataPopover.isBeingPresented && !isPopoverPresented && count > 0) {
self.delegate?.presentPopover(popover: dataPopover)
}
if (isPopoverPresented || dataPopover.isBeingPresented) && count == 0 {
dataPopover.dismiss(animated: false, completion: nil)
}
}
dataPopover.filterToSearchTerm(field.text.replacingOccurrences(of: "-", with: ""), objects: sourceList, completion:filterCompletion)
}
}
private func verifyFieldInput() {
let matchingList = sourceList.filter {
$0.selectionName.lowercased() == self.text.lowercased()
}
if matchingList.count == 1 {
self.text = matchingList[0].selectionName
self.selectedObject = matchingList[0]
self.hasError = false
} else if matchingList.count > 1 {
self.errorAlert(errorText: Strings.EHandshake.FlightLookupCustomisable.mulitpleMatches.localizedFormat(""))
} else if matchingList.isEmpty {
self.errorAlert(errorText: Strings.EHandshake.FlightLookupCustomisable.noSelectionMatches.localizedFormat(""))
}
delegate?.textInputValidated(popover: selectionPopover)
}
}
The variable I am focussed on is the sourceList var.
Now, in the class where this object is created we declare the SearchableLoginFormField as follows:
lazy var iata: SearchableLoginFormField = {
let field = SearchableLoginFormField(
popoverType: .airport,
fieldTitle: FlightLookupStrings.originAiportCode.localized,
fieldIcon: UIImage.Login.origin,
colorScheme: fieldColor,
returnAction: nil)
field.delegate = self
validator.registerField(field, rules: [RequiredRule(message: ValidationStrings.aircraftRegistrationRequired.localized)])
return field
}()
And then we set the sourceList with the following delegate method:
func airportsSet() {
iata.sourceList = CoreDataObjectsManager.shared.airportsList
}
This is called when the airports list is retrieved from an API call.
Break points prove that the sourceList is being set correctly:
airportsSet() delegate method is hit and contains the correct airport list
didSet method on the SearchableLoginFormField is fired and the sourceList is successfully set to the Airports list
when we hit the textField delegate method upon editing the field, the sourceList is empty
I tried moving the configureFormField() method into the didSet on the sourceList but I have the same result.
I'm really confused how this seems to be set but then empty again. I'm keeping the breakpoint on the didSet and at no point is it being set to empty by anything else.
The superclass contains the following var:
public var textDidChangeAction: (() -> Void)?
and the following textFieldDelegate method:
open func textFieldDidChangeSelection(_ textField: UITextField) {
didChangeSelectionAction?()
}
So in the configureFormField method we are setting the action of this delegate method accordingly which gets triggered. It is at this point that the sourceList is empty.
The field itself is added in viewDidLoad of the main display viewController as follows:
stackView.add(arrangedSubviews: [number, reg, iata, submitButton, errorLabel])

closure error when connecting UI with logic Swift

I need to connect UI with the module.
Error: Cannot convert value of type '()' to closure result type 'Bool'
Controller Code:
private func connectLogicView() {
screenView.handlePlayPauseButton(sender: screenView.playPauseButton) { () -> Bool in
obj.playPause(queue: .global()) { //Error here
if self.obj.getPlayer().timeControlStatus != .paused {
return true
} else if self.obj.getPlayer().timeControlStatus == .paused {
return false
}
}
}
}
Module code:
public func playPause(queue: DispatchQueue, completion: #escaping ()->Void){
if player.timeControlStatus == .playing {
player.pause()
} else {
player.play()
}
queue.async {
completion()
}
}
UI code:
func handlePlayPauseButton(sender: UIButton, completion: () -> Bool) {
if completion() == true {
sender.setTitle("pause", for: .normal)
} else {
sender.setTitle("play", for: .normal)
}
}
If you can show a better way, it will be greatly appreciated.
playPause method don't have anything in completion block but
obj.playPause(queue: .global()) {
if self.obj.getPlayer().timeControlStatus != .paused {
return true // NO NEED FOR THIS LINE
} else if self.obj.getPlayer().timeControlStatus == .paused {
return false // NO NEED FOR THIS LINE
}
}
You are returning true or false remove that and your code will run fine
As #Prashant Tukadiya said the code seems to be little bit tricky/complex.
Easy way would be something similar to below
func playPause(queue: DispatchQueue, completion: #escaping (Bool) -> Void){
let someOtherCondition: Bool = false
var returnAnswer: Bool = false
if someOtherCondition {
print("player paused")
returnAnswer = true
} else {
print("player play")
}
queue.async {
completion(returnAnswer)
}
}
func connectLogicView() {
let aSampleButton: UIButton = UIButton.init()
playPause(queue: .global()) { (isPlaying) in
if isPlaying {
aSampleButton.setTitle("pause", for: .normal)
} else {
aSampleButton.setTitle("play", for: .normal)
}
}
}

Access to value of the variable in if statement

I need to access the value of a variable in a if statement, this is my code:
var codeError : Int?
#IBAction func mySwitch(_ sender: UISwitch) {
if sender.isOn {
codeError = 1
} else {
codeError = 2
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if codeError == 1 {
front()
} else if codeError == 2 {
back()
}
}
My two functions, this is my front() function:
func front() {
let deviceDiscoverySession = AVCaptureDeviceDiscoverySession(deviceTypes: [AVCaptureDeviceType.builtInDualCamera, AVCaptureDeviceType.builtInTelephotoCamera,AVCaptureDeviceType.builtInWideAngleCamera], mediaType: AVMediaTypeVideo, position: AVCaptureDevicePosition.unspecified)
for device in (deviceDiscoverySession?.devices)! {
if device.position == AVCaptureDevicePosition.front {
do {
let input = try AVCaptureDeviceInput(device: device)
if captureSession.canAddInput(input) {
captureSession.addInput(input)
if captureSession.canAddOutput(photoOutput) {
captureSession.addOutput(photoOutput)
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
previewLayer?.connection.videoOrientation = AVCaptureVideoOrientation.portrait
cameraView.layer.addSublayer(previewLayer!)
}
}
} catch {
print("Error")
}
}
}
captureSession.startRunning()
print("😇")
}
And back() function:
func back() {
let deviceDiscoverySession = AVCaptureDeviceDiscoverySession(deviceTypes: [AVCaptureDeviceType.builtInDualCamera, AVCaptureDeviceType.builtInTelephotoCamera,AVCaptureDeviceType.builtInWideAngleCamera], mediaType: AVMediaTypeVideo, position: AVCaptureDevicePosition.unspecified)
for device in (deviceDiscoverySession?.devices)! {
if device.position == AVCaptureDevicePosition.back {
do {
let input = try AVCaptureDeviceInput(device: device)
if cS.canAddInput(input) {
cS.addInput(input)
if cS.canAddOutput(photoOutput) {
cS.addOutput(photoOutput)
previewLayer = AVCaptureVideoPreviewLayer(session: cS)
previewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
previewLayer?.connection.videoOrientation = AVCaptureVideoOrientation.portrait
cameraView.layer.addSublayer(previewLayer!)
}
}
} catch {
print("Error")
}
}
}
cS.startRunning()
print("🙃")
}
The problem is when I run the app. I get a nil value of the codeError when I should get 1 o 2.
How do I solve this?
I'm not sure why you're accessing a variable set by user when interacting with the view on viewWillAppear. I suggest spending some time and studying UIViewController life cycles.
But if you just want to print error codes on switch changing values, this should do it.
var codeError : Int?
#IBAction func mySwitch(_ sender: UISwitch) {
if sender.isOn {
codeError = 1
} else {
codeError = 2
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// will print nothing since errorCode is not set
printCodeError()
}
func printCodeError() {
if let codeError = self.codeError {
print("\(codeError)")
}
}
it is working like you original code. but you print it inside viewWillAppear. but at this time it is not initialized from you uiswitch.
one solution: give it default value:
var codeError: Int = 1
or shorter
var codeError = 1
Note:
viewWillAppear(_:)
Notifies the view controller that its view is about to be added to a view hierarchy.
see https://developer.apple.com/reference/uikit/uiviewcontroller/1621510-viewwillappear
but it is called on the view where your UISwitch is on. for your main goal (hidden in comments on the other answer) it is the wrong place to print out the variable