Generically setting accessibility identifier for automation tools? - swift

We have a requirement in our project. We need to set accessibility identifier for all the components in approximately 40 view controllers. I was thinking how to achieve these basic work by getting each view controller name and iboutlet names in run time and generate ids by combining these values as accessibility id. For these, I need to get IBOutlet's names. How can I do that ? Or do you have any alternative idea for automating this process another way ?
Thanks.

You can try Sourcery
It able to parse all your source files and provide you information about IBOutlets of all controllers:
You interested in classes -> variables -> attributes
You can generate inline for all such variables didSet block in which you will setup proper accessibility identifier

you can check this link and you find a great solution to automate setting accessibilityIdentifier with the same name of the variable
https://medium.com/getpulse/https-medium-com-dinarajas-fixing-developer-inconveniences-ios-automation-e4832108051f
import UIKit
protocol AccessibilityIdentifierInjector {
func injectAccessibilityIdentifiers()
}
extension AccessibilityIdentifierInjector {
func injectAccessibilityIdentifiers() {
var mirror: Mirror? = Mirror(reflecting: self)
repeat {
if let mirror = mirror {
injectOn(mirror: mirror)
}
mirror = mirror?.superclassMirror
} while (mirror != nil)
}
private func injectOn(mirror: Mirror) {
for (name, value) in mirror.children {
if var value = value as? UIView {
UnsafeMutablePointer(&value)
.pointee
.accessibilityIdentifier = name
}
}
}
}
extension UIViewController: AccessibilityIdentifierInjector {}
extension UIView: AccessibilityIdentifierInjector {}
class BaseView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
injectAccessibilityIdentifiers()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
injectAccessibilityIdentifiers()
}
}
How to use it
class ViewController: BaseViewController {
let asd1 = BaseView()
let asd2 = BaseView()
let asd3 = BaseView()
let asd4 = BaseView()
let asd5 = BaseView()
let asd6 = BaseView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
[asd1, asd2, asd3, asd4, asd5, asd6].forEach {
print($0.accessibilityIdentifier)
}
}
}

Related

How to layout a view with translatesAutoresizingMaskIntoConstraints enabled

As I understand it, there is a new method to configure UITableView cells for iOS 14. I am using this method, but when I implemented it, there was an error.
Exception NSException * "The content view returned from the content configuration must have translatesAutoresizingMaskIntoConstraints enabled"
However, when I set translatesAutoresizingMaskIntoConstraints to true, I could no longer layout the view for the cell. That is, even though I was setting the view's width, nothing was displayed.
Below is the method I am using (without my code that configures the views). My question is whether there is a way to avoid the error while still being able to layout the view.
#available(iOS 14, *)
extension GoalCell {
struct Configuration: UIContentConfiguration {
func makeContentView() -> UIView & UIContentView {
return MyContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> GoalCell.Configuration {
return self
}
}
class MyContentView: UIView, UIContentView {
var configuration: UIContentConfiguration {
didSet {
self.configure()
}
}
}()
init(configuration: UIContentConfiguration) {
self.configuration = configuration
super.init(frame: .zero)
self.configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
guard let config = self.configuration as? Configuration else { return }
self.translatesAutoresizingMaskIntoConstraints = true
}
}

Unit testing a viewController with constructor dependency

I want to write a simple unit test for the viewController described below that has constructor dependency. Unfortunately one of the parameters is of type NSCoder. How do I create the NSCoder argument while testing? As I get this error message.
caught "NSInvalidArgumentException", "*** -decodeObjectForKey: only defined for abstract class. Define -[NSKeyedArchiver decodeObjectForKey:]!"
online research: deprecated solution stackoverflow
ViewModel
class ViewModel {
private let response: DataResponse
private var name = "Character name"
init(reponse: Result) {
self.reponse = response
}
var fullName: String {
return name
}
fund getData() {
if self.result.name.count > 0 {
self.name = self.result.name
}
}
}
ViewController
class ViewController: UIViewController {
#IBOutlet weak var name: UILabel!
private var viewModel: ViewModel
init?(viewModel: ViewModel, coder: NSCoder) {
self.viewModel = viewModel
super.init(coder: coder)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.getData()
characterNameLabel.text = viewModel.fullName
}
}
The method that shows viewController
#IBSegueAction func showViewController(coder: NSCoder) ->
ViewController? {
guard let selectedRow = tableView.indexPathForSelectedRow?.row else {
return nil
}
let singleResult = specialViewModel.results(at: selectedRow)
let viewModel = ViewModel(result: singleResult)
return ViewController(viewModel: viewModel, coder: coder)
}
Testcase snippet
class ViewControllerTest: XCTestCase {
var viewControllerUnderTest: ViewController!
var viewModel: ViewModel!
override func setUp() {
super.setUp()
let data = PagedResponse.stub().data.results[0]
self.viewModel = ViewModel(result: data)
self.viewModel.getData()
let coder = "??" // how do I create coder
self.viewControllerUnderTest = ViewControllerUnderTest(viewModel: viewModel, coder: coder)
self.viewControllerUnderTest.loadView()
self.viewControllerUnderTest.viewDidLoad()
}
override func tearDown() {
super.tearDown()
}
}

Swift constrained Protocol for a subclass and superclass

I wanted to implement my own HUD for a UIViewCntroller and a UIView, so I did this:
protocol ViewHudProtocol {
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
Now I can easily adopt ViewHudProtocol on any UIView to call showLoadingView and hideLoadingView. The problem is I want to use the same protocol for UIViewController, so I did this:
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.view.showLoadingView() //Error: UIView has no member showLoadingView
}
func hideLoadingView() {
self.view.hideLoadingView() //Error: UIView has no member hideLoadingView
}
}
I agree to the error that UIView has not adopted the protocol yet. So I did this:
extension UIView: ViewHudProtocol {}
And it works. Is there a better way to do this? I mean it feels wrong to extend every view with ViewHudProtocol, where not all of them will use it. If I could do something like, "only adopt ViewHudProtocol implicitly for a UIView, if its UIViewController demands for it. Else you could adopt ViewHUDProtocol manually on any UIView when required."
I would solve this with the following approach, using associatedtype, defined only for needed views and/or controllers (tested in Xcode 11.2 / swift 5.1):
protocol ViewHudProtocol {
associatedtype Content : ViewHudProtocol
var content: Self.Content { get }
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
var content: some ViewHudProtocol {
return self
}
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.content.showLoadingView() //NO Error
}
func hideLoadingView() {
self.content.hideLoadingView() //NO Error
}
}
//Usage
extension UITableView: ViewHudProtocol { // only for specific view
}
extension UITableViewController: ViewHudProtocol { // only for specific controller
var content: some ViewHudProtocol {
return self.tableView
}
}
The problem
So you want to constraint the conformance of a UIViewController to the protocol ViewHudProtocol only when the UIViewController.view property conforms to ViewHudProtocol.
I am afraid this is not possible.
Understanding the problem
Let's have a better look at your problem
You have 2 types (UIView and UIViewController) and you want to add to both the same functionalities
func showLoadingView()
func hideLoadingView()
What Mick West teaches us
This kind of scenario is somehow similar to what Mick West faced during the development of the Tony Hawks series Mick West and an elegant solution is described in its article Evolve your hierarchy.
Solution
We can apply that approach to your problem and here's the solution
struct HudViewComponent {
let view: UIView
private let hud: UIView
init(view: UIView) {
self.view = view
self.hud = UIView(frame: view.frame)
self.hud.isHidden = true
self.view.addSubview(hud)
}
func showLoadingView() {
self.hud.isHidden = false
}
func hideLoadingView() {
self.hud.isHidden = true
}
}
protocol HasHudViewComponent {
var hidViewComponent: HudViewComponent { get }
}
extension HasHudViewComponent {
func showLoadingView() {
hidViewComponent.showLoadingView()
}
func hideLoadingView() {
hidViewComponent.hideLoadingView()
}
}
That's it, now you can add the hud functionalities to any Type conforming to HasHudViewComponent.
class SomeView: UIView, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self) }()
}
or
class MyViewController: UIViewController, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self.view) }()
}
Considerations
As you can see the idea is to thinking in terms of components.
You build a component (HudViewComponent) with your hud functionalities. The component only asks for the minimum requirements: it needs a UIView.
Next you define the HasHudViewComponent which states that the current type has a HudViewComponent property.
Finally you can add your hud functionalities to any Type which has a view (UIView, UIViewController, ...) simply conforming your type to HasHudViewComponent.
Notes
You asked an interesting question and I know this does not answers 100% what you were looking for, but by a practical point of view it should provides you with a tool to achieve what you need.
I would have taken this approach:
Create a UIView Class,
setup the view
Declare a shared object.
A function to show the view
A function to remove the view. and then call it in view controllers as IndicatorView.shared.show() , IndicatorView.shared.hide()
import Foundation
import UIKit
import Lottie
class IndicatorView : UIView {
static let shared = IndicatorView()
var loadingAnimation : AnimationView = {
let lottieView = AnimationView()
lottieView.translatesAutoresizingMaskIntoConstraints = false
lottieView.layer.masksToBounds = true
return lottieView
}()
var loadingLabel : UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont(name: "SegoeUI", size: 12)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func show() {
setupLoadingView()
self.alpha = 0
UIView.animate(withDuration: 0.5, animations: {
self.isHidden = false
self.alpha = 1
}, completion: nil)
applyLottieAnimation()
}
public func hide() {
self.alpha = 1
UIView.animate(withDuration: 0.5, animations: {
self.alpha = 0
}, completion: { _ in
self.isHidden = true
self.removeFromSuperview()
}) }
private func setupLoadingView() {
let controller = UIApplication.shared.keyWindow!.rootViewController!
controller.view.addSubview(self)
//setup your views here
self.setNeedsLayout()
self.reloadInputViews()
}
}
For this particular scenario, a Decorator would work better, and result in a better design:
final class HUDDecorator {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
func showLoadingView() {
// add the spinner
}
func hideLoadingView() {
// remove the spinner
}
}
Using the Decorator would then be as easy as declaring a property for it:
class MyViewController: UIViewController {
lazy var hudDecorator = HUDDecorator(view)
}
This will allow any controller to decide if it wants support for showing/hiding a loading view by simply exposing this property.
Protocols are too invasive for simple tasks like enhancing the looks on a UI component, and they have the disadvantage of forcing all views of a certain class to expose the protocol functionality, while the Decorator approach allows you to decide which view instances to receive the functionality.

Swift MVVM - use protocol to handle viewModel events

I'm trying to use MVVM with delegate protocols. When something changes in the view model I want to trigger it in the view controller.
When I want to use protocols to handle the view model's event on a view controller, I can not set the protocol to the view controller for my view model class.
It gives me the error:
Argument type (SecondViewController) -> () -> SecondViewController does not conform to expected type SecondViewModelEvents
How can I do this the right way?
Here is the code for my view model:
protocol SecondViewModelEvents {
func changeBackground()
}
class SecondViewModel:NSObject {
var events:SecondViewModelEvents?
init(del:SecondViewModelEvents) {
self.events = del
}
func loadDataFromServer() {
self.events?.changeBackground()
}
}
And for my view controller class:
class SecondViewController: UIViewController,SecondViewModelEvents {
let viewModel = SecondViewModel(del: self) //Argument type '(SecondViewController) -> () -> SecondViewController' does not conform to expected type 'SecondViewModelEvents'
#IBAction func buttonPressed(_ sender: Any) {
self.viewModel.loadDataFromServer()
}
func changeBackground() {
self.view.backgroundColor = UIColor.red
}
}
You're trying to initialize the view model variable and pass the view controller as a delegate which at this point is not fully initialized.
Try checking out the very informative and very detailed Initialization page in the official Swift language guide.
Since this is a protocol used for this specific purpose, we can safely constrain it to classes (notice the : class addition to your code.
protocol SecondViewModelEvents: class {
func changeBackground()
}
It's good practice to use more descriptive naming, and also using weak references for delegate objects in order to avoid strong reference cycles.
class SecondViewModel {
weak var delegate: SecondViewModelEvents?
init(delegate: SecondViewModelEvents) {
self.delegate = delegate
}
func loadDataFromServer() {
delegate?.changeBackground()
}
}
You can try to use an optional view model, which will get initialized in an appropriate place, like awakeFromNib():
class SecondViewController: UIViewController, SecondViewModelEvents {
var viewModel: SecondViewModel?
override func awakeFromNib() {
super.awakeFromNib()
viewModel = SecondViewModel(delegate: self)
}
#IBAction func buttonPressed(_ sender: Any) {
viewModel?.loadDataFromServer()
}
func changeBackground() {
view.backgroundColor = UIColor.red
}
}
Or an alternative approach would be to initialize a non-optional view model in the UIViewController required initializer:
// ...
var viewModel: SecondViewModel
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.viewModel = SecondViewModel(delegate: self)
}
// ...
You need to use lazy initialization as,
lazy var viewModel = SecondViewModel(del: self)
OR
lazy var viewModel = { [unowned self] in SecondViewModel(del: self) }()

Setting values of a class for example UIButton

I am lacking some basic understandings.
Today I wanted to subclass some UIView and I looked in to the UIButton definition, but I can not figure out how it works.
In the UIButton definition are properties like:
open var adjustsImageWhenHighlighted: Bool
open var adjustsImageWhenDisabled: Bool
When using an UIButton it does not matter when the values of the UIButton get set, it always gets configured the correct way same with tableView or any other UIKit classes.
I made a example:
class customView: UIView {
var shouldSetupConstraints = true
var addAdditionalSpacing = true
var elements: [String]? {
didSet{
if addAdditionalSpacing == false {
doSomething()
}
}
}
func doSomething() {
}
override init(frame: CGRect) {
super.init(frame: frame)
setUpLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func updateConstraints() {
if(shouldSetupConstraints) {
// AutoLayout constraints
shouldSetupConstraints = false
}
super.updateConstraints()
}
func setUpLayout() {
}
}
Using CustomView:
lazy var customV: CustomView = {
let v = CustomView()
v.addAdditionalSpacing = true
v.elements = ["One","Two"]
return v
}()
lazy var customV2: CustomView = {
let v = CustomView()
v.elements = ["One","Two"]
v.addAdditionalSpacing = true
return v
}()
So if I am using CustomView it makes a difference in which order I set it up, I understand why but I do not understand in which way I have to design my classes so I can set the values whenever I want, except with different if didSet configurations. Why do the properties in UIButton do not have any setters, how do the values get set in that case?
Any links to documentations are appreciated as well.
First of all, in a UIButton you can take control of the didSet property observer for existing properties like this:
override var adjustsImageWhenDisabled: Bool {
didSet {
doStuff()
}
}
Secondly, in your class scenario you might consider passing the parameters in the constructor:
init(shouldSetupConstraints: Bool = true, var addAdditionalSpacing = true)
This way the properties will be available for you whenever you need them.
Let me know if this helps.