Accessibility Voice One for Date in a String - ios

I have below code.
let text = "as of 07/29/2020"
textLabel.text = text
Voice Over read it - "as of 07 slash 29 slash 2 Thousand 20"
How can i make it to pronounce like "as of July, 29 2 Thousand 20"

Answer
Not sure exactly what you are trying to solve. But hopefully this helps.
Step 1: You need to use the accessibility label property label.accessibilityLabel = "some string"
Step 2: Convert your date into a more friendly string for voice over. You can do this by using a Date() object and format it differently for the label and the voice-over label. Here is a link for creating dates in swift.
Below is the link to the documentation and the section that mentions accessibility labels.
Playground Example: Your code will then look something like this.
import UIKit
import PlaygroundSupport
let label = UILabel()
let dateForLabel = formatDate(date: Date(), style: .short)
let dateStringForLabel = "as of \(dateForLabel)"
label.text = dateStringForLabel
let dateForVoiceOverLabel = formatDate(date: Date(), style: .long)
let dateStringForVoiceOver = "as of \(dateForVoiceOverLabel)"
label.accessibilityLabel = dateStringForVoiceOver
func formatDate(date: Date, style: DateFormatter.Style) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = style
dateFormatter.timeStyle = .none
let date = date
// US English Locale (en_US)
dateFormatter.locale = Locale(identifier: "en_US")
return dateFormatter.string(from: date) // Jan 2, 2001
}
Link to create a Date() object
How do you create a Swift Date object?
Link to apple documentation on Accessibility labels
https://developer.apple.com/documentation/uikit/accessibility_for_ios_and_tvos/supporting_voiceover_in_your_app
"Update Your App’s Accessibility
For elements that were not accessible to VoiceOver, start by improving their accessibility labels and hints:
The accessibilityLabel property provides descriptive text that VoiceOver reads when the user selects an element.
The accessibilityHint property provides additional context (or actions) for the selected element.
Accessibility labels are very important, because they provide the text that VoiceOver reads. A good accessibility label should be short and informative. It is important to note that a UILabel and an accessibilityLabel are different things. By default, VoiceOver reads the text associated with standard UIKit controls such as UILabel and UIButton. However these controls can also have corresponding accessibilityLabel properties to add more detail about the label or button..
Depending on the context, hints aren’t always necessary. In some cases, the label provides enough context. If you feel like you’re saying too much in an accessibility label, consider moving that text into a hint.
To ensure that users understand the intent of your interface, you might need to set some accessibility labels manually. Accessibility labels and hints can either be set in Xcode’s Identity Inspector or programmatically."

Related

Instantly format a UITextView using AttributedString

I'm developing a macOS rich-text editor that applies pre-defined style for each line of the text view.
To format the lines, I'm using NSAttributedString, then, I'm inserting that string into my UITextView. To make things easier, I'm using a tool called SwiftRichString.
My code looks like below. It's straight-forward and works fine.
import Cocoa
import SwiftRichString
import AppKit
class ViewController: NSViewController {
#IBOutlet var textView: NSTextView!
override func viewDidLoad() {
super.viewDidLoad()
// Format the string
let style = Style {
$0.font = NSFont(name: "Arial", size: 20)
$0.color = NSColor.black
$0.alignment = .center
}
let attributedText = "Hello World!".set(style: style)
// Add formatted string to the text view
textView.textStorage?.append(attributedText)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
Current situation:
User is typing a formatted line. Then when user hits Return and types something, format of the new line returns back to the default style of the UITextView.
What I want:
User is typing a certain formatted line, then he hits Return. The next line should be formatted to another pre-defined style on-the-go.
Example:
User is typing the Title line. Current style is (Arial, bold, 20pt).
He hits Return.
Next line should be styled as Normal Text using a pre-defined style (Arial, 12pt).
Important Note:
In my above code, I was able to format the line easily because it's hard-coded. My real issue is, how can I instantly format the next line, because the next line will be entered by the user. The style should be applied to the next line before user begins writing it.
Okay, I just figured out how to use typingAttributtes to solve this question (thanks to #Larme for the hint).
// Define next attributes
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: NSColor.red,
.font: NSFont(name: "Arial", size: 12)!,
]
// Assign attributes to the text view typing attributes
textView.typingAttributes = attributes
Very easy!
Pardon off topic. If you're making a text editor, you may consider using a table view, where each text line is a cell - this is extra work for you as a programmer but it'll boost the performance significantly. That's how Xcode editor is built.
Maybe you might use optional func textViewDidChange(_ textView: UITextView)
https://developer.apple.com/documentation/uikit/uitextviewdelegate/1618599-textviewdidchange
and after the change, just parse the text, find new lines, split them and apply all styling you want

How to access the iOS 14 time picker in XCUITest

I want to make use of the inline date picker style that was recently introduced in iOS 14. However, I need to make adjustments to a couple of XCUITests that include the previous picker wheel style.
The problem is I can't change the value of the time picker at all. I noticed that the time picker is not a picker nor a pickerWheel element in XCUITest; Instead, it's a textField element.
E.g.
TextField, {{178.3, 401.7}, {74.7, 36.3}}, label: 'Time', value: 12:01
Changing its value like a typical textfield doesn't work at all (typeText doesn't do anything). I tried to access the picker wheels that seem to be inside the textfield, but checking for its descendants returns empty.
po timeInput.descendants(matching: .any).count
t = 799.62s Get number of matches for: Descendants matching type Any
0 // no descendants found
So how do I change the value of the time picker text field in XCUITest?
EDIT:
The date picker mode for the UIDatePicker is set to time, so I'm not really seeing the calendar view, just the time input field.
I put the date picker inside the contentView of a UITableViewCell, which is then added when another table view cell is tapped (i.e. it's dynamically added).
The date picker is configured like this:
picker.datePickerMode = .time
picker.date = Date(timeInterval: time, since: date)
picker.minuteInterval = intervals
if #available(iOS 14, *) {
picker.preferredDatePickerStyle = .inline
}
Previously, the date picker is displayed as a picker wheel and I had no problem accessing it in the XCUITest. I could simply call this to adjust the value of the picker wheel:
pickerWheels.element(boundBy: index).adjust(toPickerWheelValue: value)
However, with the date picker set to inline, the previous query no longer works. I also checked for picker and datePicker elements, but they also return empty results. I can see a textfield element that has a "Time" label and the value contains whatever value is set in the time picker.
(lldb) po app.pickers.count
t = 2270.22s Get number of matches for: Descendants matching type Picker
0 // No results
(lldb) po app.pickerWheels.count
t = 2277.07s Get number of matches for: Descendants matching type PickerWheel
0 // No results
(lldb) po app.datePickers.count
t = 2286.58s Get number of matches for: Descendants matching type DatePicker
0 // No results
(lldb) po app.textFields.count
t = 2302.55s Get number of matches for: Descendants matching type TextField
1 // 1 element matched
(lldb) po app.textFields
t = 2308.08s Requesting snapshot of accessibility hierarchy for app with pid 55829
t = 2308.46s Find: Descendants matching type TextField
t = 2308.47s Requesting snapshot of accessibility hierarchy for app with pid 55829
t = 2308.61s Find: Descendants matching type TextField
t = 2308.61s Requesting snapshot of accessibility hierarchy for app with pid 55829
Find: Target Application
Output: {
Application, pid: 55829, label: 'Projects'
}
↪︎Find: Descendants matching type TextField
Output: {
TextField, {{178.3, 401.7}, {74.7, 36.3}}, label: 'Time', value: 01:00
}
So I can't see any pickers, but I have textfield whose value is set to the date picker's time input value. I tried changing the textfield's value by using typeText but it doesn't seem to do anything at all.
I ended up creating a sample view controller which contains just the UIDatePicker to see if I can access it in XCUITest.
Interestingly, I was able to detect a datePicker element in the XCUITest which is something I wasn't able to do when dynamically adding the UIDatePicker in a table view.
The descendants of the datePicker looks like this:
Find: Descendants matching type DatePicker
Output: {
DatePicker, {{89.3, 423.3}, {196.7, 53.3}}
}
↪︎Find: Descendants matching type Any
Output: {
Other, {{89.3, 423.3}, {196.7, 53.3}}
Other, {{89.3, 431.3}, {196.7, 37.3}}
Other, {{89.3, 431.3}, {196.7, 37.3}}, label: 'Time Picker'
TextField, {{97.3, 431.3}, {74.7, 36.3}}, label: 'Time', value: 05:54
SegmentedControl, {{178.0, 431.3}, {100.0, 36.3}}
Button, {{178.0, 431.3}, {49.0, 36.3}}, label: 'AM'
Button, {{228.0, 431.3}, {50.0, 36.3}}, label: 'PM', Selected
}
The time input field is still a textfield element but I was able to change its value using typeText. I suspect that there's something in our codebase that handles the valueChanged delegate that causes the typeText to not work.
Bottomline, using typeText to change the value of the time input picker should work just fine.
Before using typeText to change its value, make sure it has focus. Try to tap the textfield first, and then the value can be changed via typeText.

iOS UIDatePicker display without time

I would like to have a Date Picker with only one wheel, filled with days as on the Date and Time mode.
If I choose the Date mode, I have 3 wheels and I loose the name of the day.
In fact I would like something like the date and time mode, but only with the date.
I could use a UIPickerView, but I would have to fill it by myself with a very big array of data, as I want to be able go back far in time. With the Date picker, it's filled automatically, which is cool.
Is there a solution ?
You can also use UIDatePicker's pickerMode property to do that.
datePicker.pickerMode = .Date
Look into header file, you will see that there are few other Options that you can play with. Other values are;
Time
Date
DateAndTime
CountDownTimer
You should use the UIPickerView instead of the UIDatePicker. There is no possibility to customise UIDatePicker in the way you want.
try displayedComponents with .date
DatePicker( selection: .constant(futureDate), displayedComponents: [.date],
label: { Text("Due date") })
As you only need to show the contents that you have marked in the green box. You can use this https://github.com/attias/AADatePicker and modify it a bit and you can get it as per your requirement.
The changes you will need to make are within the AADatePickerView Class
1) In viewForRow method comment the following things
2) Second change
datePicker.datePickerMode = UIDatePicker.Mode.date
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd MMMM yyyy"
let selectedDate = dateFormatter.string(from: datePicker.date)
print(selectedDate)

iOS Swift - Add target(selector) to string

Is there a way to add a target to a specified word from an UITextView? For example.
I want to add a target to a hash-tagged word from a UITextView.
I'm using a function to get an array with the hash-tagged words from the textview, but I don't know how to add them a target, or a tap-gesture.
You can use an attributed string to set the text in the text view. That attributed string uses link attributes (NSLinkAttributeName with a URL value of your choice) in the range of your target hash-tagged words. You need to search the text for your hash-tagged words and add the appropriate link attributes. You probably want to create the link URLs to have a custom scheme and include information about the hash-tagged word. When one of the links is tapped you get a delegate callback from the text view.
I had the same question recently and I found a solution that fit my needs. It's not perfect but maybe it will help:
I decided to go for a whole text button with specific style around my target element. In a user experience way, user may be inclined to touch the styled word.
#IBOutlet weak var stringBtn: UIButton!
override func viewWillAppear(animated: Bool) {
let string = "string with #tag" as NSString
var attributedString = NSMutableAttributedString(string: string as String)
let firstAttributes = [NSUnderlineStyleAttributeName: 1, NSForegroundColorAttributeName: UIColor.whiteColor()]
attributedString.addAttributes(firstAttributes, range: string.rangeOfString("#tag"))
stringBtn.setAttributedTitle(attributedString, forState: .Normal)
//Add target to your button
}

How do I set the accessibility label for a particular segment of a UISegmentedControl?

We use KIF for our functional testing, and it uses the accessibility label of elements to determine where to send events. I'm currently trying to test the behaviour of a UISegmentedControl, but in order to do so I need to set different accessibility labels for the different segments of the control. How do I set the accessibility label for a particular segment?
As Vertex said,
obj-c
[[[self.segmentOutlet subviews] objectAtIndex:3] setAccessibilityLabel:#"GENERAL_SEGMENT"];
swift
self.segmentOutlet.subviews[3].accessibilityLabel = "GENERAL_SEGMENT"
some advice so you don't go crazy like I did:
To scroll in accessibility mode swipe three fingers
The indexes of the segments are backwards than you would expect, i.e. the furthest segment to the right is the 0th index and the furthest to the left is the n'th index where n is the number of elements in the UISegmentControl
I'm just getting started with KIF myself, so I haven't tested this, but it may be worth a try. I'm sure I'll have the same issue soon, so I'd be interested to hear if it works.
First, UIAccessibility Protocol Reference has a note under accessibilityLabel that says:
"If you supply UIImage objects to display in a UISegmentedControl, you can set this property on each image to ensure that the segments are properly accessible."
So, I'm wondering if you could set the accessibilityLabel on each NSString object as well and be able to use that to access each segment with KIF. As a start, you could try creating a couple of strings, setting their accessibility labels, and using [[UISegmentedControl alloc] initWithItems:myStringArray]; to populate it.
Please update us on your progress. I'd like to hear how this goes
Each segment of UISegmentedControl is UISegment class instance which subclass from UIImageView. You can access those instances by subviews property of UISegmentedControl and try to add accessibility for them programmatically.
You can't rely on the index in the subviewsarray for the position. For customisation of the individual subviews I sort the subviews on their X Position before setting any propery.What would also be valid for accesibilityLbel.
let sortedViews = self.subviews.sorted( by: { $0.frame.origin.x < $1.frame.origin.x } )
sortedViews[0].accessibilityLabel = "segment_full"
sortedViews[1].accessibilityLabel = "segment_not_full"
This is an old question but just in case anyone else runs up against this I found that the segments automatically had an accessibility label specified as their text. So if two options were added of Option 1 and Option 2. A call to
[tester tapViewWithAccessibilityLabel:#"Option 2"];
successfully selected the segment.
The solutions with using an indexed subview is not working since you cannot rely on a correct order and it will be difficult to change the number of segments. And sorting by origin does not work, since the frame (at least for current versions) seems to be always at x: 0.
My solution:
(segmentedControl.accessibilityElement(at: 0) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 1"
(segmentedControl.accessibilityElement(at: 1) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 2"
(segmentedControl.accessibilityElement(at: 2) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 3"
Seems to work for me and has the correct order. You also do not rely on an image. Not that pretty either but maybe more reliable than other solutions.
This is an old question but just in case anyone else runs up against this I found that the segments automatically had an accessibility label specified as their text.
Further to Stuart's answer, I found it really useful when writing test cases to turn on 'Accessibility Inspector' on the Simulator (Settings -> General -> Accessibility -> Accessibility Inspector). You'd be surprised how many elements already have accessibility labels included, like in the standard iOS UI elements or even third party frameworks.
Note: Gestures will now be different - Tap to view accessibility information, double tap to select. Minimizing the Accessibility Inspector window (by tapping the X button) will return the gestures back to normal.
You guys want to see how Apple recommends it be done?
It's FUGLY.
This is from this example:
func configureCustomSegmentsSegmentedControl() {
let imageToAccessibilityLabelMappings = [
"checkmark_icon": NSLocalizedString("Done", comment: ""),
"search_icon": NSLocalizedString("Search", comment: ""),
"tools_icon": NSLocalizedString("Settings", comment: "")
]
// Guarantee that the segments show up in the same order.
var sortedSegmentImageNames = Array(imageToAccessibilityLabelMappings.keys)
sortedSegmentImageNames.sort { lhs, rhs in
return lhs.localizedStandardCompare(rhs) == ComparisonResult.orderedAscending
}
for (idx, segmentImageName) in sortedSegmentImageNames.enumerated() {
let image = UIImage(named: segmentImageName)!
image.accessibilityLabel = imageToAccessibilityLabelMappings[segmentImageName]
customSegmentsSegmentedControl.setImage(image, forSegmentAt: idx)
}
customSegmentsSegmentedControl.selectedSegmentIndex = 0
customSegmentsSegmentedControl.addTarget(self,
action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)),
for: .valueChanged)
}
They apply the accessibility labels to images, and then attach the images. Not too different from the above answer.
another option if not willing to set accesibility label might be calculating the poistion of each segment part and use
[tester tapScreenAtPoint:segementPosition];
to trigger the actions
If you look at the segmented control thru the accessibility inspector, you find that the segments are UISegment objects. Moreover, they turn out to be direct subviews of the UISegmentedControl. That fact suggests the following insanely crazy but perfectly safe Swift 4 code to set the accessibility labels of the segments of a UISegmentedControl:
let seg = // the UISegmentedControl
if let segclass = NSClassFromString("UISegment") {
let segments = seg.subviews.filter {type(of:$0) == segclass}
.sorted {$0.frame.minX < $1.frame.minX}
let labels = ["Previous article", "Next article"] // or whatever
for pair in zip(segments,labels) {
pair.0.accessibilityLabel = pair.1
}
}
As mentioned in the accepted answer, adding accessibilityLabel to the text should do the trick:
let title0 = "Button1" as NSString
title0.accessibilityLabel = "MyButtonIdentifier1"
segmentedControl.setTitle("\(title0)", forSegmentAt: 0)
let title1 ="Button2" as NSString
title1.accessibilityLabel = "MyButtonIdentifier2"
segmentedControl.setTitle("\(title1)", forSegmentAt: 1)
XCode 12 / iOS 14.3 / Swift 5
This is an old post but I encountered the same problem trying to set accessibility hints for individual segments in a UISegmentedControl. I also had problems with some of the older solutions. The code that's currently working for my app borrows from replies such as those from matt and Ilker Baltaci and then mixes in my own hack using UIView.description.
First, some comments:
For my UISegmentedControl with 3 segments, the subview count is 3 in the viewDidLoad() and viewWillAppear() of the parent UIVIewController. But the subview count is 7 in viewDidAppear().
In viewDidLoad() or viewWillAppear() the subview frames aren't set, so ordering the subviews didn't work for me. Apparently Benjamin B encountered the same problem with frame origins.
In viewDidAppear(), the 7 subviews include 4 views of type UIImageView and 3 views of type UISegment.
UISegment is a private type. Working directly with the private API might flag your app for rejection. (see comment below)
type(of:) didn't yield anything useful for the UISegment subviews
(HACK!) UIView.description can be used to check the type without accessing the private API.
Setting accessibility hints based on X order tightly couples UI segment titles and hints to their current positions. If user testing suggests a change in segment order, then changes must be made both in the UI and in the code to set accessibility hints. It's easy to miss that.
Using an enum to set segment titles is an alternative to relying on X ordering set manually in the UI. If your enum inherits from String and adopts the protocols CaseIterable and RawRepresentable, then it's straightforward to create titles from the enum cases, and to determine the enum case from a segment title.
There's no guarantee the following will work in a future release of the framework, given the reliance on description.contains("UISegment") but it's working for me. Gotta move on.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// get only the UISegment items; ignore UIImageView
let segments = mySegmentedControl.subviews.compactMap(
{ $0.description.contains("UISegment") ? $0 : nil }
)
let sortedSegments = segments.sorted(
by: { $0.frame.origin.x < $1.frame.origin.x }
)
for i in 0 ..< sortedSegments.count {
let segment = sortedSegments[i]
// set .accessibilityHint or .accessibilityLabel by index
// or check for a segment title matching an enum case
// ...
}
}
On Private APIs and Rejection
I'm referring to the April 2016 comment from #dan in Test if object is an instance of class UISegment:
It's a private class. You can check it with [...
isKindOfClass:NSClassFromString(#"UISegment")] but that may get your
app rejected for using private api or stop working in the future if
apple changes the internal class name or structure.
Also:
What exactly is a Private API, and why will Apple reject an iOS App if one is used?
"App rejected due to non-public api's": https://discussions.apple.com/thread/3838251
As Vortex said, the array is right to left with [0] starting on the right. You can set every single accessibility option by accessing the subviews. Since the subviews are optional, it is good to pull out the subview first, and then assign the accessibility traits that you want. Swift 4 example for a simple two option segment control:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let rightSegment = segmentControl.subviews.first, let leftSegment = segmentControl.subviews.last else { return }
rightSegment.accessibilityLabel = "A label for the right segment."
rightSegment.accessibilityHint = "A hint for the right segment."
leftSegment.accessibilityLabel = "A label for the left segment."
leftSegment.accessibilityHint = "A hint for the left segment."
}

Resources