Accessibility identifier for UI subviews based on parent view id - ios

I have a custom view that has some subviews like labels and text fields. When I use multiple custom views into one controller I want to know the subview Accessibility Identifier. What I want to achieve is that set parent identifier(parent_accessibility_identifier) and than subview identifier can be an extension of it (eg parent_accessibility_identifier_label, parent_accessibility_identifier_text_field). Can we do this by setting the parent identifier accessibly to false and adding labels and text into the child's view but is there any better way to do it? this code doesn't work in the subview class.
public override var accessibilityIdentifier: String? {
didSet {
if let accessibilityIdentifier = self.accessibilityIdentifier {
self.accessibilityIdentifier = accessibilityIdentifier + "_label"
}
}
}
this work in the custom view class
public override var accessibilityIdentifier: String? {
didSet {
guard let accessibilityIdentifier = accessibilityIdentifier else { return }
textLabel.accessibilityIdentifier = accessibilityIdentifier + "_text_label"
}
}

I would suggest using swizzling for that. It enables you to override the behavior of opened properties and functions of system-level frameworks.
First, put the swizzling code somewhere:
///
extension UIView {
/**
*/
class func swizzle() {
let orginalSelectors = [
#selector(getter: accessibilityIdentifier),
]
let swizzledSelectors = [
#selector(getter: swizzledAccessibilityIdentifier)
]
let orginalMethods = orginalSelectors.map { class_getInstanceMethod(UIView.self, $0) }
let swizzledMethods = swizzledSelectors.map { class_getInstanceMethod(UIView.self, $0) }
orginalSelectors.enumerated().forEach { item in
let didAddMethod = class_addMethod(
UIView.self,
item.element,
method_getImplementation(swizzledMethods[item.offset]!),
method_getTypeEncoding(swizzledMethods[item.offset]!))
if didAddMethod {
class_replaceMethod(
UIView.self,
swizzledSelectors[item.offset],
method_getImplementation(orginalMethods[item.offset]!),
method_getTypeEncoding(orginalMethods[item.offset]!))
} else {
method_exchangeImplementations(orginalMethods[item.offset]!, swizzledMethods[item.offset]!)
}
}
}
/// that's where you override `accessibilityIdentifier` getter for
#objc var swizzledAccessibilityIdentifier: String {
if let parentIdentifier = superview?.accessibilityIdentifier {
return "\(parentIdentifier)_\(self.swizzledAccessibilityIdentifier)"
} else {
return self.swizzledAccessibilityIdentifier
}
}
}
Call UIView.swizzle(). Usually, you do it at the app launch. Somewhere in the AppDelegate or similar.
Setup view hierarchy, assign identifiers and test:
class ParentView: UIView {}
class SubclassedChildView: UIView {}
let parentView = ParentView()
let child1 = SubclassedChildView()
let child2 = UIView()
parentView.accessibilityIdentifier = "parent"
child1.accessibilityIdentifier = "child1"
child2.accessibilityIdentifier = "child2"
parentView.addSubview(child1)
child1.addSubview(child2)
print(parentView.accessibilityIdentifier) // "parent"
print(child1.accessibilityIdentifier) // "parent_child1"
print(child2.accessibilityIdentifier) // "parent_child1_child2"

Related

Check initialized Struct value inside UIView

I have a UIView with a custom-init so I can change its mode:
var wishlistMode: Constants.WishlistMode.Type?
init(wishlistMode: Constants.WishlistMode.Type) {
self.wishlistMode = wishlistMode
super.init(frame: CGRect.zero)
setupViews()
}
For that I have created this struct:
struct Constants: Equatable {
struct WishlistMode: Equatable {
static let isCreating = WishlistMode.self
static let isChanging = WishlistMode.self
}
/*...*/
}
Inside my UIView I have this function to check the WishListMode but somehow it is always printing out isChanging even if I initialize the view with let v = CreateNewListView(wishlistMode: Constants.WishlistMode.isCreating) :
func checkWishlistMode(){
if self.wishlistMode == Constants.WishlistMode.isChanging {
print("isChanging")
} else if self.wishlistMode == Constants.WishlistMode.isCreating {
print("isCreating")
}
}
I have no clue what I am doing wrong. Can anyone help me out here?
Change your struct with constants to an enum instead
enum WishlistMode {
case isCreating
case isChanging
}
and in the UIView code change from Constants.WishlistMode.Type to Constants.WishlistMode

NSPopoverTouchBarItems in NSScrollView (NSTouchBar)

Is there a way to add an array of NSPopoverTouchBarItems into a NSScrollView?
Currently, my view hierarchy resembles the below list.
NSTouchBar
NSCustomTouchBarItem
NSScrollView
NSStackView
Array of NSButtons
The above hierarchy outputs the following screenshot.
In sum, the end goal is to replace the array of NSButtons with NSPopoverTouchBarItems.
I believe what you need is the use of NSScrubber to be able to scroll or have fixed position of multiple buttons including NSPopoverTouchBarItem
https://developer.apple.com/documentation/appkit/nsscrubber
Check out this repository for more information and sample codes that might help you:
https://github.com/loretoparisi/touchbar
import Cocoa
fileprivate extension NSTouchBar.CustomizationIdentifier {
static let popoverBar = NSTouchBar.CustomizationIdentifier("com.TouchBarCatalog.popoverBar")
}
fileprivate extension NSTouchBarItem.Identifier {
static let scrubberPopover = NSTouchBarItem.Identifier("com.TouchBarCatalog.TouchBarItem.scrubberPopover")
}
class PopoverScrubber: NSScrubber {
var presentingItem: NSPopoverTouchBarItem?
}
class PopoverScrubberViewController: NSViewController {
// MARK: NSTouchBar
override func makeTouchBar() -> NSTouchBar? {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = .popoverBar
touchBar.defaultItemIdentifiers = [.scrubberPopover]
touchBar.customizationAllowedItemIdentifiers = [.scrubberPopover]
return touchBar
}
}
// MARK: NSTouchBarDelegate
extension PopoverScrubberViewController: NSTouchBarDelegate {
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
guard identifier == NSTouchBarItem.Identifier.scrubberPopover else { return nil }
let popoverItem = NSPopoverTouchBarItem(identifier: identifier)
popoverItem.collapsedRepresentationLabel = "Scrubber Popover"
popoverItem.customizationLabel = "Scrubber Popover"
let scrubber = PopoverScrubber()
scrubber.register(NSScrubberTextItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: "TextScrubberItemIdentifier"))
scrubber.mode = .free
scrubber.selectionBackgroundStyle = .roundedBackground
scrubber.delegate = self
scrubber.dataSource = self
scrubber.presentingItem = popoverItem
popoverItem.collapsedRepresentation = scrubber
popoverItem.popoverTouchBar = PopoverTouchBarSample(presentingItem: popoverItem)
return popoverItem
}
}
// MARK: NSScrubber Data Source and delegate
extension PopoverScrubberViewController: NSScrubberDataSource, NSScrubberDelegate {
func numberOfItems(for scrubber: NSScrubber) -> Int {
return 20
}
func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
let itemView = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "TextScrubberItemIdentifier"), owner: nil) as! NSScrubberTextItemView
itemView.textField.stringValue = String(index)
return itemView
}
func scrubber(_ scrubber: NSScrubber, didSelectItemAt index: Int) {
print("\(#function) at index \(index)")
if let popoverScrubber = scrubber as? PopoverScrubber,
let popoverItem = popoverScrubber.presentingItem {
popoverItem.showPopover(nil)
}
}
}

Listen for updates in the data model array when one of its properties changes

I have a custom UITableViewCell which has a data model array, and a UILabel as this:
class ItemCustomizationCollectionViewCell: UITableViewCell {
var customizationData: CustomizationData?
let priceLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 18)
label.textAlignment = .left
label.textColor = UIColor.gray
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
//other init and constraints
}
Now, the CustomizationData looks like this:
class CustomizationData {
let title: String
var customizationOptions: [PickerOption]
var choosenOption: PickerOption?
init(title: String, customizationOptions: [PickerOption], choosenOption: PickerOption?) {
self.title = title
self.customizationOptions = customizationOptions
self.choosenOption = choosenOption
}
}
and the PickerOption is:
class PickerOption {
let title: String
let price: String
init(title: String, price: String) {
self.title = title
self.price = price
}
}
My Question is:
I want to listen to customizationData's choosenOption gets set, and get the title to change the UILabel's text to the choosenOption's price.
I tried to add a didSet to the customizationData but it's not being called as one of its properties is changing. Not the customizationData itself.
What you can do is possible, but to trigger a didSet customizationOptions of you need to change its value, basically in 2 ways:
if customizationOptions is a reference type, you need to change it with another reference
if customizationOptions is a value type such as an Array, the didSet should be called also if you change a value inside it, because the array change the struct completely.
To understand better here is an example.
Using a value type:
class Mine {
var list: [String] = ["Hello"] {
didSet {
print("Array changed \(list)")
}
}
}
var mine = Mine()
mine.list.append("World") //Array changed ["Hello", "World"]
Using a reference type:
class Mine {
var list: NSArray = ["Hello"] {
didSet {
print("Array changed \(list)")
}
}
}
var mine = Mine()
mine.list.adding("World")
//Doesn't print nothing
mine.list = ["World"]
//print World
I think you can do this using delegation. Create a CustomizationDataDelegate protocol:
protocol CustomizationDataDelegate {
func choosenOptionDidChange(to newValue: PickerOption)
}
Then create a delegate property in the CustomizationData class, for instance:
internal var delegate : CustomizationDataDelegate?
And add a didSet to choosenOption:
var choosenOption : PickerOption? {
didSet{
if let _ = choosenOption {
delegate?.choosenOptionDidChange(to choosenOption!)
}
}
}
Then register the ItemCustomizationCollectionViewCell to the CustomizationDataDelegate protocol, and then when you have some customization data, make sure the delegate is set to the cell's value:
class ItemCustomizationCollectionViewCell: UITableViewCell, CustomizationDataDelegate {
var customizationData: CustomizationData? {
didSet {
if let _ = customizationData {
if let _ = customizationData!.choosenOption {
// You need this in case the choosenOption has already been set
choosenOptionDidChange(to: customizationData!.choosenOption)
}
// You need this to listen for future changes
customizationData!.delegate = self
}
}
}
let priceLabel: UILabel = {
// ...
// In your class' function declarations...
//# MARK:- CustomizationDataDelegate methods
func choosenOptionDidChange(to newValue: PickerOption) -> Void {
// Update the label's value here based on newValue
}
}
Hope that helps.

Using generic function in base class

What I'm trying to do...
In my app I have a lot of form fields that look alike with a bunch of custom functionality (change color on highlight e.c.t.).
I want to create a sort of wrapper class, that abstracts all of this code, then inherit from that to implement my different input types such as date input and text input.
The inherited classes will just need to setup the correct input control for it's type.
What i've tried
This is more like pseudo-code. I have been trying for hours but I just don't understand how to achieve what I need
I think i start with a base class, this needs to define a reference to the input control, and a few methods that each one will override such as being able to set or get the current value
class BaseInput<T>: UIView {
let label = UILabel()
let control: T
... A bunch of methods for layout and stuff ...
func setControlValue(_ value: U) {
print("I'm not a real input yet, so i can't do that")
}
}
I then create an inherited class for a date input. This uses a basic label for the control, and internally will use a UIDatePicker to set the value
class DateInput: BaseInput<UILabel> {
override init() {
self.control = UILabel()
}
override func setControlValue(_ value: Date) {
MyGlobalDateFormatter.string(format:value)
}
}
and another for a text input field
class TextInput: BaseInput<UITextField> {
override init() {
self.control = UITextField()
}
override func setControlValue(_ value: String) {
control.textLabel!.text = value
}
}
What i'm ultimately looking for is the ability to initialise a input component, and for the MyInput.control property to be of the correct class for that specific input, and for the setControlValue method to accept the correct kind of Data (i.e. a String, Int or Date depending on the type of control)
I believe this can be solved using generic's but i'm really struggling to understand how. If anyone can point me in the right direction that would be great.
Note: I don't expect or want anyone to write all of the code for me. Pseudo-code would be enough to allow me to work it all out.
Attempt 1:
protocol CustomControl {
associatedtype Control
associatedtype Value
var control : Control { get }
var label : UILabel { get }
func setControlValue(_ value: Value)
}
class AbstractInputField: UIView {
// This class contains all the setup
// for the label and wrapping UI View
}
class TextInputField: AbstractInputField, CustomControl {
typealias Control = UITextField
typealias Value = String
let control = UITextField()
func setControlValue(_ value: String) {
control.text = value
}
}
class DateInputField: AbstractInputField, CustomControl {
typealias Control = UILabel
typealias Value = String
let control = UILabel()
private let picker = UIDatePicker()
func setControlValue(_ value: Date) {
control.text = GlobalDateFormatter.string(from: value)
}
.. Also in this class it's a bunch of date picker methods ..
}
Elsewhere if I do:
override func viewDidLoad() {
let firstInput = makeControl("text")
firstInput.label.text = "First name"
firstInput.setControlValue(myUser.first_name)
let dobInput = makeControl("date")
dobInput.label.text = "Date of birth"
dobInput.setControlValue(myUser.dob)
}
func makeControl(controlType: String) -> CustomControl {
// Im using strings just for testing, i'd probably make this an enum or something
if controlType == "text" {
return TextInputField()
} else {
return DateInputField()
}
}
I get the error: `Protocol 'CustomControl' can only be used as a generic constraint because it has Self or associated type requirements
What i'm ultimately trying to achieve, is a very simple API to my inputs where i can set the label text, and set the input value. The rest of my app doesn't care if it's a textfield, textview or complete custom input type. My app wants to work with the protocol (or a base class of some kind) that says it has these methods & properties.
Maybe i'm being stupid.
I would suggest to use protocols.
protocol CustomControl {
associatedtype Control
associatedtype Value
var control: Control { get }
func setControlValue(_ value: Value)
}
and then create custom classes conform to this protcol
class CustomTextField: CustomControl {
typealias Control = UITextField
typealias Value = String
let control: UITextField
init() {
self.control = UITextField()
}
func setControlValue(_ value: String) {
control.text = value
}
}
Edited:
class CustomLabel: CustomControl {
typealias Control = UILabel
typealias Value = String
let control: UILabel
init() {
self.control = UILabel()
}
func setControlValue(_ value: String) {
control.text = value
}
}
Edit 2: Alternative approach
protocol HasSettableValue: class {
associatedtype Value
var customValue: Value { get set }
}
protocol IsInitializable {
init()
}
extension UITextField: IsInitializable {}
extension UITextField: HasSettableValue {
typealias Value = String?
var customValue: String? {
get {
return text
}
set {
text = newValue
}
}
}
class BaseClass<T> where T: HasSettableValue, T: IsInitializable {
let control: T
init() {
self.control = T()
}
func setControlValue(_ value: T.Value) {
control.customValue = value
}
}
class CustomTextField: BaseClass<UITextField> {
}
let customTextField = CustomTextField()
customTextField.setControlValue("foo")
print(customTextField.control) // prints <UITextField: 0x...; frame = (0 0; 0 0); text = 'foo'; opaque = NO; layer = <CALayer: 0x...>>
Final update:
The problem with protocols having associated types is you can't use them for declaration of variables. You always need to specify the concrete implementation.
I guess I found a solution fitting your needs:
enum ControlType {
case textField, datePicker
}
enum ControlValueType {
case text(text: String)
case date(date: Date)
var string: String {
switch self {
case .text(text: let text):
return text
case .date(date: let date):
// apply custom format
return "\(date)"
}
}
var date: Date {
switch self {
case .date(date: let date):
return date
default:
preconditionFailure("`date` can be only used with `.date` value type")
}
}
}
protocol Control: class {
var controlValue: ControlValueType { get set }
}
class TextInputField: Control {
private let textField = UITextField()
var controlValue: ControlValueType {
get {
return .text(text: textField.text ?? "")
}
set {
textField.text = newValue.string
}
}
}
class DateInputField: Control {
private let picker = UIDatePicker()
var controlValue: ControlValueType {
get {
return .date(date: picker.date)
}
set {
picker.date = newValue.date
}
}
}
func createControl(ofType type: ControlType) -> Control {
switch type {
case .textField:
return TextInputField()
case .datePicker:
return DateInputField()
}
}
let customDatePicker = createControl(ofType: .datePicker)
customDatePicker.controlValue = .date(date: Date())
print(customDatePicker.controlValue.string) // prints 2017-09-06 10:47:22 +0000
let customTextFiled = createControl(ofType: .textField)
customTextFiled.controlValue = .text(text: "Awesome text")
print(customTextFiled.controlValue.string) // prints Awesome text
I hope this helps.
PS: What you are trying to achieve is not very common pattern in iOS so far I know.

UIBindingObserver for UINavigationBar does not work as expected

I bind to UIBindingObserver for barTintColor new UIColor and title, but it does not showing. Wired thing is that when I drag UIViewController back and release everything appear
Code
extension Reactive where Base: UINavigationBar {
var barTintColor: UIBindingObserver<Base, UIColor> {
return UIBindingObserver(UIElement: self.base) { navigationBar, barTintColor in
navigationBar.barTintColor = barTintColor
}
}
}
class BaseViewController: UIViewController {
private(set) var tintColor: Variable<UIColor> = Variable(ColorConstants.tintColor)
override func viewDidLoad() {
super.viewDidLoad()
if let navigationBar = self.navigationController?.navigationBar {
self.tintColor.asObservable()
.bind(to: navigationBar.rx.barTintColor)
.addDisposableTo(self.disposeBag)
}
}
}
class TasksListViewController: BaseViewController {
var viewModel: TasksListViewModel!
...
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.colorObsrvable
.unwrap()
.bind(to: self.tintColor)
.addDisposableTo(self.disposeBag)
self.viewModel.titleObsrvable
.unwrap()
.bind(to: self.rx.title)
.addDisposableTo(self.disposeBag)
}
}
class TasksListViewModel {
private(set) var titleObsrvable: Observable<String?>!
private(set) var colorObsrvable: Observable<UIColor?>!
init(from goal: Goal) {
let goalPredicate = NSPredicate(format: "(SELF = %#)", self.goal.objectID)
self.goalObservable = CoreDataModel.shared.rx.fetchObject(predicate: goalPredicate)
self.titleObsrvable = goalObservable.map { $0?.name }
self.colorObsrvable = goalObservable.map { $0?.color }
}
}
extension Reactive where Base: CoreDataModel {
func fetchObjects<T: NSManagedObject>(predicate: NSPredicate? = nil) -> Observable<[T]> {
let entityName = String(describing: T.self)
let fetchRequest = NSFetchRequest<T>(entityName: entityName)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
fetchRequest.predicate = predicate
return base.managedObjectContext.rx.entities(fetchRequest: fetchRequest)
}
}
class Goal is just class Entitiy
What I have tried
Yes it's MainThread, I tested. And also run this task on GCD MainThread ❌
added observeOn(MainScheduler.sharedInstance) does not help ❌
put breakpoints inside the UIBindingObserver when drag and release it does not call ❌
added title in storyboard to NavBar and it works, but with wired delay. First it shows "Ti..." and then "Title" ✅
Replaced(And its works✅ idk why):
self.titleObsrvable = goalObservable.map { $0?.name }
self.colorObsrvable = goalObservable.map { $0?.color }
with:
self.titleObsrvable = Observable.of("1234")
self.colorObsrvable = Observable.of(UIColor.red)
Any idea what I'm doing wrong?
If you want to check whole project you can see it here:
https://github.com/Yerkenabildin/my-everest
It seems to be some sort of race condition. You are setting the barTintColor at the same moment that the navigation controller is starting its animation. If you make sure you don't change the tin color until after the animation is complete, it will work the way you want.
Also, you have two different binders to rx.barTintColor of the same navigation bar and they are feeding the bar conflicting information. You should only have a single binder if at all possible.

Resources