Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interface Builder: accessibility identifiers #369

Open
wants to merge 5 commits into
base: stable
Choose a base branch
from

Conversation

IanKeen
Copy link

@IanKeen IanKeen commented Dec 13, 2017

This PR adds a new type of output to SwiftGen, it will parse storyboards and xibs looking for accessibility identifiers.

I have included two templates, enums:

enum AccessibilityIdentifier {
  enum SomeViewController: String {
    case someAccessibilityIdentifier = "someAccessibilityIdentifier"
  }
}

and properties:

enum AccessibilityIdentifier {
  enum SomeViewController {
    static let someAccessibilityIdentifier = "someAccessibilityIdentifier"
  }
}

@AliSoftware
Copy link
Collaborator

AliSoftware commented Dec 13, 2017

Thanks for the PR! We'll try to finish the merge back into monorepo by cleaning a few stuff but will soon look at it right after 👍

Concerning the new command — swiftgen accessibility — that's actually a debate I wanted to start. The problem being that both swiftgen storyboards and swiftgen accessibility will parse the same *.storyboard files. So:

  • maybe that should be the same command to run, to parse the storyboard files only once for both the sceneIdentifiers & your accessibilityIdentifiers instead of parsing them twice (and just generating a Stencil Context that is more rich, with both info for scenes, segues and a11y identifiers)
  • but if we do that, we should probably rename the swiftgen storyboards subcommand to swiftgen ib or something now that it needs to parse not only storyboards but also XIB files

I've open the debate in a dedicated Team Discussion on GitHub: https://github.com/orgs/SwiftGen/teams/corecontributors/discussions/3
As soon as you've accepted the invitation I sent you to be part of the "Core Contributors" SwiftGen team you should be able to read it and participate 😉

@djbe
Copy link
Member

djbe commented Dec 13, 2017

Yeah that's a discussion we've had a few times here on GitHub, and in slack. I'll see if I can dig up some of the points that were made then.

@AliSoftware
Copy link
Collaborator

Another open discussion is: shouldn't those accessibilityIdentifiers be strongly typed to match their XCUIElement.ElementType? Or is it useless?

For now, the templates seem to only generate constants convertible to strings, which means that nothing would prevent you to do app.buttons[AccessibilityIdentifier.CollectionViewCell.textfield] thus using an accessibility identifier originally attached to a textField to try to match a button

That could indeed be a first take / first version to at least start introducing the feature, that we could then improve later by making it more type-safe, but I was wondering if we couldn't imagine a solution more akin to what we do with StoryboardScenes, where each constant is typed (using generics) to tell which type of UIViewController that scene is supposed to be…


But to make it more strongly-typed, maybe instead we can go the full type-safe route, like this?

struct A11yElement {
  var type: XCUIElement.ElementType
  var id: String
}
extension XCUIApplication {
  subscript(id: A11yElement) -> XCUIElement {
    return self.elementMatching(type: id.type, identifier: id.id)
  }
}

// Generated code:
enum A11y {
  enum ChildViewController
    static let myButton = A11yElement(type: .button, id: "MyButton")
    static let myTextField = A11yElement(type: .textfield, id: "MyTextField")
  }
  
}

So we can then use it like this:

let b = app[A11y.ChildViewController.myButton]
let f =  app[A11y.ChildViewController.myTextField]

I'm sure if we brainstorm further, we can even imagine something even more useful when you execute some UI test on a single VC and want to access a lot of identifiers attached to the same VC, something that would allow us to do things like this?

let avc = A11y.ChildViewController(app)
let b = avc(.myButton)
let f = avc(.myTextField)

But maybe that last idea is too complex for a first take…

@IanKeen
Copy link
Author

IanKeen commented Dec 13, 2017

I'm happy to integrate this as part of the existing commands, im worried however that since this command deals with storyboards and xibs that it may be confusing if it also deals with only storyboard based data (segues etcs) ?

As for the typing, generally all you need is the string for a lookup. There are some lookup functions in which you can narrow the search using an element type also - but I am unsure on how problematic it may be to extract those also?

Funny about your last point, the reason I built this is to facilitate a new UI testing feature im building for work which uses the output from this to achieve type safe/compiler checking of UI tests 😄

i.e my framework gives you:

someScreen.element(.loginButton).tap()

and loginButton is associated to someScreen

Copy link
Collaborator

@AliSoftware AliSoftware left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey 😉

Even if the debate about whether to piggy-back on the existing subcommands or create a dedicated one is still open:

  1. You should rename your templates to include the Swift version they are for (swift3 and/or swift4). It would even be nice to have one version of each template for each swift version, so enums-swift3, enums-swift4, properties-swift3, properties-swift4

  2. Maybe even if for now we keep the dedicated subcommand, maybe because the output stencil context has a hierarchy that is a bit too different from what's generated by storyboards, I wonder if the future feature supporting UITableViewCell / UICollectionViewCell's reuseIdentifiers would maybe end up with a very similar structure / hierarchy in the context, and thus will share the same command? In that spirit, naming it identifiers might seem more adequate? Or if not and we consider still keeping a dedicated command, we might want to renaming it to something shorter, like swiftgen a11y?

  3. In your tests, all accessibility identifiers you use in the different storyboards & XIB happen to have always the same name (button or textfield). So that if by mistake your implementation happen to mess up the various identifiers or merge them between storyboards by mistake, we won't catch this kind of bug. Maybe it would be nice to keep one or two common (to still test the case that we properly avoid collisions) but also have others that are different (to also test we don't mix them up)?

@AliSoftware
Copy link
Collaborator

As for the typing, generally all you need is the string for a lookup. There are some lookup functions in which you can narrow the search using an element type also - but I am unsure on how problematic it may be to extract those also?

Funny that you mention that, as I originally replied to your previous comment by saying that we could make the constants not String but struct A11yElement(name: String) and then allow extension XCUIApplication { subscript(e: A11yElement) { return self.element(e.name) } }

But then as I was writing that I realised that it was not safe enough (in the sense that, if we're gonna generate type-safe constants, the goal is to make it way safer than using just a string, so as we're at it better make it extra-safe even), because one might have multiple elements with the same accessibilityIdentifier and just be different by their XCUIElement.ElementType, so if it were possible to parse the element type in the process too maybe it would be awesome to include that in the wrapper type we generate as well to add that extra safety? That was my idea when I finally replied with my comment above in the end 😉

@IanKeen
Copy link
Author

IanKeen commented Dec 17, 2017

@AliSoftware thanks for spending some time on this 🤘

  1. happy to produce some more templates, they will be duplicates though as there is no variation in the syntax between swift 3 & 4 - do you still what me to do this anyway?

  2. I think identifiers is a good option for now 👍

  3. No probs, I'll make sure there are unique identifiers in there too

I'm not sure then how you'd like me to proceed re: XCUIElement.ElementType ??

@AliSoftware
Copy link
Collaborator

About XCUIElement.ElementType I think we could imagine a hard-coded map in the parser which looks at the XML element's tag name (<button …> or<textfield …> etc) while parsing and knows (via an internal hard-coded map in SwiftGenKit's code) what is the corresponding XCUIElement.ElementType to include in the Stencil context, so that templates can then use that to generate safer code.

@AliSoftware
Copy link
Collaborator

Mmmmh but now that I think about it, if the template use the accessibilityIdentifier's String to determine the name of the constant to generate… we might still have conflicts indeed…

like:

// Static code in the generated output, not depending on the files parsed
struct A11yElement {
  let name: String
  let type: XCUIElement.ElementType
  private init(name: String, type: XCUIElement.ElementType) {
    self.name = name
    self.type = type
  }
}

// Code generated according to files parsed
enum A11y {
  enum SomeViewController {
    static let foo = A11yElement(name: "foo", type: .button)
    static let foo = A11yElement(name: "foo", type: .textfield)
  }
}

So indeed in that case as the generated constant's name is likely to be determined by the accessibilityIdentifier, if multiple have the same name we're out of luck anyway. Except if we find a clever scheme or name those generated constants differently (including the type in the name) but that will generate ugly and longer-than-necessary constants then, so…

Still, if it's not too complicated, at least maybe it's still worth it including the XCUIElement.ElementType inside the Stencil Context, even if we don't use it in the templates we'll bundle in SwiftGen, because other people might want to generate their own custom templates and use them and come up with clever ideas in the future anyway?

@IanKeen
Copy link
Author

IanKeen commented Dec 18, 2017

Yeah I'll look into adding the element type, finding them isn't hard - it seems the <accessibility tag is always 1 level deep so just grabbing the parent tag name gives the internal name - I'll just need to create a big map to the ElementType from that.

Also re: uniqueness - I think its a happy side effect that collision would fail.. this will enforce uniqueness which you could argue is just another reason SwiftGen adds safety (imo this is better than potential collisions)

@IanKeen
Copy link
Author

IanKeen commented Dec 18, 2017

also, you're thoughts on my q1 above ?

@AliSoftware
Copy link
Collaborator

AliSoftware commented Dec 18, 2017

Yeah, I happen to agree (it's an identifier after all, they generally are meant to be unique)…

… but we also have to remember that to each their own and that some other people might have different habits and PoVs (we had that experience/feedback in the past especially with the way some people organise Localizable.strings keys differently than other people for example).

After all the whole power of SwiftGen is its template system and ability for people to customize the output according to how they work, so it's always good to consider that some users might not have the same rules (eg about uniqueness) as we both do 😉

@IanKeen
Copy link
Author

IanKeen commented Dec 21, 2017

@AliSoftware so im getting close to having the element type as part of the output however another potential issue is going to be the XCUIElement.Type requiring import XCTest - which obviously isn't going to only be available in a test target.

depending on the use case that'll mean more templates, do you have any thoughts on this ??

@AliSoftware
Copy link
Collaborator

I'm not sure I get how that would be an issue… I mean:

  1. I thought the first draft of the templates were not gonna use the XCUIElement.Type in the generated output, and that the only feature we wanted for now is to make the XCUIElement.ElementType available in the context — in case some people wanted to create their own template using it — but not really use it in the templates we're gonna bundle in SwiftGen yet — because duplicate identifiers in those template would lead to duplicate constant names anyway? So in that context, the templates you wrote won't use that XCUIElement.Type yet and won't require import XCTest, right?
  2. For people writing their own custom template which would use XCUIElement.ElementType in the generated code, they will indeed need to make the template also generate import XCTest too… but will also likely only use those custom templates in their test targets then. I don't see any issue in having templates that would generate code only compatible with test targets and not app targets, that's gonna be a use case

So yeah, that would mean more templates (one template only compatible with test targets — that I'm not sure we're gonna provide just yet, but might exist in the future or in custom templates — and one not using XCUIElement.ElementType but compatible with app targets — the one you already wrote in this PR) but ability to customize and choose what's needed for one's use case is what makes SwiftGen powerful (especially compared to other alternatives not supporting templates) 👍

@IanKeen
Copy link
Author

IanKeen commented Dec 21, 2017

ok sounds good, in that case I won't worry about creating the XCUIElement.Type mapping (since that would require XCTest - I'll have it simply extract the element the identifier is from (button etc..)

@AliSoftware
Copy link
Collaborator

AliSoftware commented Dec 22, 2017

I still don't understand why you'd need import XCTest to just create that mapping and generate the values in the Stencil context. The Stencil context is just a dictionary of arrays and strings…

So in the parser you'll just need to create a [String: String] dictionary to map the XML tag name (e.g. <button>, <textfield>, …) to the name (string) of the XCUIElement.ElementType, like "button", "textField", etc). No need to import XCTest for that mapping, as it's all Strings.

Then when you parse your XIBs/Storyboards and generate the Stencil context entries for each accessibility identifier, you can build that entry using the accessibilityID and the string corresponding to the elementType (so that people will be able to use it in their own templates and generate text which happen to be valid Swift referencing the element type)

Sure, if we could import XCTest in the Parser code that would allow us to type-safely build an intermediate [String: XCUIElement.ElementType] dictionary/mapping while parsing the IB files… but in the end we'd have to call .mapValues { String(describing: $0) } on that dictionary to transform it to a [String: String] in order to insert it into the Stencil context, as Stencil contexts are just PLIST-compatible structures, so…

@IanKeen
Copy link
Author

IanKeen commented Feb 9, 2018

@AliSoftware Finally got a chance to get back to this.., I've:

  • Updated all the identifiers in the test fixtures to be unique
  • Added the element type to the context (though it isn't used by the included templates)

Lemme know what you think 🤘


{{accessModifier}} enum AccessibilityIdentifier {
{% for container in containers %}
{{accessModifier}} enum {{container.name}}: String {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that enum here is declaring String rawValue is causing a compiler error, where it says enum with no cases shouldn't declare a rawValue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Mazyod @IanKeen Would the compiler error go away if we add a conditional statement that checks the size of the array before actually iterating through them?

{{accessModifier}} enum AccessibilityIdentifier {
  {% for container in containers %}
    {% if container.accessibility.count > 0 %} 
    {{accessModifier}} enum {{container.name}}: String {
      {% for accessibility in container.accessibility %}
      {{accessModifier}} static let {{accessibility.identifier}} = "{{accessibility.rawValue}}"
      {% endfor %}
    }
    {% endif %}
    {% endfor %}
  }
}

I imagine this would avoid an instance where you get an enum that has no cases.

@djbe djbe added this to To do in SwiftGen 6.0 May 6, 2018
@djbe djbe changed the title Feature/accessibility Interface Builder: accessibility identifiers Sep 4, 2018
@djbe djbe added this to the SwiftGen 6.1 milestone Sep 26, 2018
@djbe djbe modified the milestones: 6.1.0, 6.2.0 Jan 26, 2019
@AliSoftware AliSoftware changed the base branch from master to stable June 10, 2020 23:16
@djbe djbe modified the milestones: 6.3.0, 6.4.0 Jul 6, 2020
@djbe djbe modified the milestones: 6.5.0, Next minor (new features) Oct 7, 2020
@brunomunizaf
Copy link

2021, what's the status of this? 😬

@acosmicflamingo
Copy link

Oh wow this would be great to have working indeed :)

@acosmicflamingo
Copy link

acosmicflamingo commented Jul 3, 2022

Rather than just poke people and say "When are you going to work on this", I figured that I could try and help! I ran 'git rebase develop' on this branch, resolved conflicts, and pushed my own commit that I believe addresses what @Mazyod had found: #949. Hope this helps!

@brunomunizaf
Copy link

@acosmicflamingo I apologize if that's what it sounded like. You are right

@acosmicflamingo
Copy link

@acosmicflamingo I apologize if that's what it sounded like. You are right

Oh I wasn't accusing you of sounding like that; I was totally talking about general cases! I'm sorry for making you think that 🙂

@acosmicflamingo
Copy link

Maybe 2024 will be the year :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
SwiftGen 6.0
  
To do
Development

Successfully merging this pull request may close these issues.

None yet

6 participants