Skip to content

Commit

Permalink
First working implementation of a11y identifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
mac-cain13 committed Jul 4, 2019
1 parent 1753dee commit dec6bf0
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 16 deletions.
2 changes: 1 addition & 1 deletion Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: feb2d125d56abaa9101403aedbf488a7d5445eab

COCOAPODS: 1.7.1
COCOAPODS: 1.7.3
10 changes: 7 additions & 3 deletions ResourceApp/ResourceApp/Duplicate.storyboard
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6211" systemVersion="14A298i" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6204"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
</dependencies>
<scenes/>
</document>
2 changes: 1 addition & 1 deletion ResourceApp/ResourceApp/My View.xib
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="The App Icon.png" translatesAutoresizingMaskIntoConstraints="NO" id="mpi-yw-TUg">
<rect key="frame" x="127.5" y="273.5" width="120" height="120"/>
<accessibility key="accessibilityConfiguration" identifier="appIconImageView"/>
<accessibility key="accessibilityConfiguration" identifier="AppIcon ImageView"/>
</imageView>
</subviews>
<color key="backgroundColor" name="Not dupe"/>
Expand Down
2 changes: 1 addition & 1 deletion ResourceApp/ResourceApp/References.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<!--First View Controller-->
<scene sceneID="Ec6-ap-ExA">
<objects>
<viewController id="IfC-nk-yIu" customClass="FirstViewController" customModule="ResourceApp" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="asdf" id="IfC-nk-yIu" customClass="FirstViewController" customModule="ResourceApp" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="gMc-Wp-NdB"/>
<viewControllerLayoutGuide type="bottom" id="fBR-5G-wqf"/>
Expand Down
11 changes: 8 additions & 3 deletions ResourceApp/ResourceApp/SupplementaryElement.xib
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
Expand Down
20 changes: 13 additions & 7 deletions ResourceApp/ResourceApp/Xib with ViewController.xib
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11173.2" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11143.2"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
Expand All @@ -13,24 +17,25 @@
<viewControllerLayoutGuide type="bottom" id="Vlc-QB-kZj"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="6cb-9m-arb">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Another view that refers to FirstViewController" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0Pv-oh-uAh">
<frame key="frameInset" minX="10" minY="28" width="354" height="21"/>
<rect key="frame" x="10" y="28" width="354" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iv9-w2-Jys">
<frame key="frameInset" minX="147" minY="193" width="46" height="30"/>
<rect key="frame" x="147" y="193" width="46" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="title label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4gQ-iR-0Gr">
<frame key="frameInset" minX="10" minY="72" width="70" height="21"/>
<rect key="frame" x="10" y="72" width="70" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<accessibility key="accessibilityConfiguration" identifier="Some Title label"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
Expand All @@ -46,6 +51,7 @@
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="bmk-oJ-WgQ">
<rect key="frame" x="0.0" y="0.0" width="106" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<accessibility key="accessibilityConfiguration" identifier="RandomButton"/>
<state key="normal" title="Random button"/>
<point key="canvasLocation" x="302" y="-136"/>
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// AccessibilityIdentifierStructGenerator.swift
// R.swift
//
// Created by Mathijs Kadijk on 04/06/2019.
// From: https://github.com/mac-cain13/R.swift
// License: MIT License
//

import Foundation

private protocol AccessibilityIdentifierContainer {
var name: String { get }
var usedAccessibilityIdentifiers: [String] { get }
}

extension Nib: AccessibilityIdentifierContainer {}
extension Storyboard: AccessibilityIdentifierContainer {}

struct AccessibilityIdentifierStructGenerator: ExternalOnlyStructGenerator {
private let accessibilityIdentifierContainers: [AccessibilityIdentifierContainer]

init(nibs: [Nib], storyboards: [Storyboard]) {
accessibilityIdentifierContainers = nibs + storyboards
}

func generatedStruct(at externalAccessLevel: AccessLevel, prefix: SwiftIdentifier) -> Struct {
let structName: SwiftIdentifier = "id"
let qualifiedName = prefix + structName
let structsForMergedContainers = accessibilityIdentifierContainers
.grouped(by: { SwiftIdentifier(name: $0.name) })
.mapValues {
$0.flatMap { $0.usedAccessibilityIdentifiers }
}
.filter { $0.value.count > 0 }
.map { self.structFromContainer(identifier: $0.key, accessibilityIdentifiers: $0.value, at: externalAccessLevel) }

return Struct(
availables: [],
comments: ["This `\(qualifiedName)` struct is generated, and contains static references to accessibility identifiers."],
accessModifier: externalAccessLevel,
type: Type(module: .host, name: structName),
implements: [],
typealiasses: [],
properties: [],
functions: [],
structs: structsForMergedContainers,
classes: [],
os: []
)
}

private func structFromContainer(identifier: SwiftIdentifier, accessibilityIdentifiers: [String], at externalAccessLevel: AccessLevel) -> Struct {
let groupedAccessibilityIdentifiers = Set(accessibilityIdentifiers)
.array()
.grouped(bySwiftIdentifier: { $0 })
groupedAccessibilityIdentifiers.printWarningsForDuplicatesAndEmpties(source: "accessibility identifier", result: "accessibility identifier")

let accessibilityIdentifierLets = groupedAccessibilityIdentifiers
.uniques
.map { letFromAccessibilityIdentifier($0, at: externalAccessLevel) }

return Struct(
availables: [],
comments: [],
accessModifier: externalAccessLevel,
type: Type(module: .host, name: identifier),
implements: [],
typealiasses: [],
properties: accessibilityIdentifierLets,
functions: [],
structs: [],
classes: [],
os: []
)
}

private func letFromAccessibilityIdentifier(_ accessibilityIdentifier: String, at externalAccessLevel: AccessLevel) -> Let {
return Let(
comments: ["Accessibility identifier `\(accessibilityIdentifier)`."],
accessModifier: externalAccessLevel,
isStatic: true,
name: SwiftIdentifier(name: accessibilityIdentifier),
typeDefinition: .specified(._String),
value: "\"\(accessibilityIdentifier)\""
)
}
}

private extension Sequence {
func groupedWithExactDuplicatesAllowed(bySwiftIdentifier identifierSelector: @escaping (Iterator.Element) -> String) -> SwiftNameGroups<Iterator.Element> {
var groupedBy = grouped { SwiftIdentifier(name: identifierSelector($0)) }
let empty = SwiftIdentifier(name: "")
let empties = groupedBy[empty]?.map { "'\(identifierSelector($0))'" }.sorted()
groupedBy[empty] = nil

let uniques = Array(groupedBy.values.filter { $0.count == 1 }.joined())
.sorted { identifierSelector($0) < identifierSelector($1) }
let duplicates = groupedBy
.filter { $0.1.count > 1 }
.map { ($0.0, $0.1.map(identifierSelector).sorted()) }
.sorted { $0.0.description < $1.0.description }

return SwiftNameGroups(uniques: uniques, duplicates: duplicates, empties: empties ?? [])
}
}
8 changes: 8 additions & 0 deletions Sources/RswiftCore/ResourceTypes/Nib.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct Nib: WhiteListedExtensionsResourceType, ReusableContainer {
let reusables: [Reusable]
let usedImageIdentifiers: [String]
let usedColorResources: [String]
let usedAccessibilityIdentifiers: [String]

init(url: URL) throws {
try Nib.throwIfUnsupportedExtension(url.pathExtension)
Expand All @@ -49,6 +50,7 @@ struct Nib: WhiteListedExtensionsResourceType, ReusableContainer {
reusables = parserDelegate.reusables
usedImageIdentifiers = parserDelegate.usedImageIdentifiers
usedColorResources = parserDelegate.usedColorReferences
usedAccessibilityIdentifiers = parserDelegate.usedAccessibilityIdentifiers
}
}

Expand All @@ -58,6 +60,7 @@ internal class NibParserDelegate: NSObject, XMLParserDelegate {
var reusables: [Reusable] = []
var usedImageIdentifiers: [String] = []
var usedColorReferences: [String] = []
var usedAccessibilityIdentifiers: [String] = []

// State
var isObjectsTagOpened = false;
Expand All @@ -82,6 +85,11 @@ internal class NibParserDelegate: NSObject, XMLParserDelegate {
usedColorReferences.append(colorName)
}

case "accessibility":
if let accessibilityIdentifier = attributeDict["identifier"] {
usedAccessibilityIdentifiers.append(accessibilityIdentifier)
}

default:
if let rootView = viewWithAttributes(attributeDict, elementName: elementName),
levelSinceObjectsTagOpened == 1 && ignoredRootViewElements.allSatisfy({ $0 != elementName }) {
Expand Down
8 changes: 8 additions & 0 deletions Sources/RswiftCore/ResourceTypes/Storyboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct Storyboard: WhiteListedExtensionsResourceType, ReusableContainer {
private let initialViewControllerIdentifier: String?
let viewControllers: [ViewController]
let viewControllerPlaceholders: [ViewControllerPlaceholder]
let usedAccessibilityIdentifiers: [String]
let usedImageIdentifiers: [String]
let usedColorResources: [String]
let reusables: [Reusable]
Expand Down Expand Up @@ -61,6 +62,7 @@ struct Storyboard: WhiteListedExtensionsResourceType, ReusableContainer {
initialViewControllerIdentifier = parserDelegate.initialViewControllerIdentifier
viewControllers = parserDelegate.viewControllers
viewControllerPlaceholders = parserDelegate.viewControllerPlaceholders
usedAccessibilityIdentifiers = parserDelegate.usedAccessibilityIdentifiers
usedImageIdentifiers = parserDelegate.usedImageIdentifiers
usedColorResources = parserDelegate.usedColorReferences
reusables = parserDelegate.reusables
Expand Down Expand Up @@ -128,6 +130,7 @@ private class StoryboardParserDelegate: NSObject, XMLParserDelegate {
var viewControllerPlaceholders: [Storyboard.ViewControllerPlaceholder] = []
var usedImageIdentifiers: [String] = []
var usedColorReferences: [String] = []
var usedAccessibilityIdentifiers: [String] = []
var reusables: [Reusable] = []

// State
Expand Down Expand Up @@ -172,6 +175,11 @@ private class StoryboardParserDelegate: NSObject, XMLParserDelegate {
usedColorReferences.append(colorName)
}

case "accessibility":
if let accessibilityIdentifier = attributeDict["identifier"] {
usedAccessibilityIdentifiers.append(accessibilityIdentifier)
}

case "viewControllerPlaceholder":
if let id = attributeDict["id"] , attributeDict["sceneMemberID"] == "viewController" {
let placeholder = Storyboard.ViewControllerPlaceholder(
Expand Down
1 change: 1 addition & 0 deletions Sources/RswiftCore/RswiftCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public struct RswiftCore {
ReuseIdentifierStructGenerator(reusables: resources.reusables),
ResourceFileStructGenerator(resourceFiles: resources.resourceFiles),
StringsStructGenerator(localizableStrings: resources.localizableStrings),
AccessibilityIdentifierStructGenerator(nibs: resources.nibs, storyboards: resources.storyboards)
]

let aggregatedResult = AggregatedStructGenerator(subgenerators: generators)
Expand Down

0 comments on commit dec6bf0

Please sign in to comment.