Overriding UILabel text getter not working - swift

I am using Storyboards and I'd like to have some labels which always show a given emoji at the start of the label.
I have the following code:
import UIKit
import CocoaLumberjack
class PITLabel: UILabel {
override public var text: String? {
set {
super.text = newValue?.prependedWithGenderEmoji()
DDLogDebug("🖌 set super.text: \(super.text ?? "🔴")")
}
get {
let newVal = super.text?.prependedWithGenderEmoji()
DDLogDebug("🖌 get super.text: \(newVal ?? "🔴")")
return newVal
}
}
override public var attributedText: NSAttributedString? {
set {
super.attributedText = newValue
DDLogDebug("🖌 set super.attributed: \(super.attributedText ?? NSAttributedString(string: "🔴"))")
}
get {
let genderString = super.attributedText?.string.prependedWithGenderEmoji()
let rangePointer = UnsafeMutablePointer<NSRange>.allocate(capacity: 8)
rangePointer.pointee = NSMakeRange(0, 1)
let originalAttributes = super.attributedText?.attributes(at: 0, effectiveRange: rangePointer)
let newAttributedString = NSAttributedString(string: genderString!, attributes: originalAttributes)
DDLogDebug("🖌 get super.attributed: \(newAttributedString)")
return newAttributedString
}
}
}
And I've set the labels to class PITLabel in the Storyboard.
When the app is run the following is present in the console output:
🖌 get super.text: 🙋‍♂️ PiT SETTINGS
🖌 get super.attributed: 🙋‍♂️ PiT SETTINGS{
NSColor = "UIExtendedSRGBColorSpace 0.0352941 0.498039 0.662745 1";
<snip>
}
This is as expected.
But in the UI of the app it does not show the emoji.
What am I missing?

Because you're actually not prepending it to the text. It should be:
override public var text: String? {
set {
let result = newValue.prependedWithGenderEmoji()
super.text = result
}
get {
return super.text
}
}
Setting the text in Interface Builder does not call the text setter that you override. You have to set it in the code.
yourPitLabel.text = "PiT SETTING

Related

How to get the property names for `UIFont` (and other UIKit classes)?

This simple code:
func getProperties(object: AnyObject) -> [String] {
var result = [String]()
let mirror = Mirror(reflecting: object)
mirror.children.forEach { property in
guard let label = property.label else {
return
}
result.append(label)
}
return result
}
works for custom classes, e.g.:
class A {
var one: String
var two: Int
init() {
one = "hello"
two = 1
}
}
var a = A()
print(getProperties(object: a))
// prints ["one", "two"]
However the same code for UIFont (and other UIKit classes), returns nothing:
var font = UIFont.systemFont(ofSize: 10)
print(getProperties(object: font))
// prints []
I also tried an old-school extension for NSObject:
extension NSObject {
var propertyNames: [String] {
var result = [String]()
let clazz = type(of: self)
var count = UInt32()
guard let properties = class_copyPropertyList(clazz, &count) else {
return result
}
for i in 0..<Int(count) {
let property: objc_property_t = properties[i]
guard let name = NSString(utf8String: property_getName(property)) else {
continue
}
result.append(name as String)
}
return result
}
}
And again, it works for my custom classes (if they extend NSobject and the properties are marked as #objc):
class B: NSObject {
#objc var one: String
#objc var two: Int
override init() {
one = "hello"
two = 1
}
}
var b = B()
print(b.propertyNames)
// prints ["one", "two"]
but doesn't find any properties for UIFont.
If I look at the UIFont definition, the properties are seems defined like regular properties (or at least this is what Xcode shows:
open class UIFont : NSObject, NSCopying, NSSecureCoding {
//...
open var familyName: String { get }
open var fontName: String { get }
// etc
The type of the class is UICTFont though, not sure if it matters.
I would like to understand why this is not working, but my main question is more generic: is there any way to obtain the properties of the UIKit class (e.g. UIFont)?

Using getter/setter for property makes it not appear in reflection (mirror)

I've been trying to implement a theme logic for my custom components. I'll use ZFButton as example.
When the app starts I instantiate ZFbutton and set whatever characteristics I want this one theme to have:
let theme = ZFButton()
theme.backgroundColor = .red //UIButton property
theme.cornerRadius = 8 //ZFButton property
theme.borderColor = .green //ZFButton property
theme.borderWidth = 1 //ZFButton property
Then add it to the theme array:
ZFButton.themes.append(theme)
which resides in the ZFButton as follows:
public static var themes = [ZFButton]()
In my ZFButton I have the following property, which allows me to choose from the IB Attributes Inspector which theme I want to use for that particular ZFButton
#IBInspectable public var theme: Int = 0 { didSet { self.setupTheme() } }
And finally, once the theme property is set, setupTheme() is called, in which I attempt to copy the value set from all properties of a given theme to this particular instance of ZFButton. For that I use reflection:
private func setupTheme() {
if ZFButton.themes.count > self.theme {
let theme = ZFButton.themes[self.theme]
let mirror = Mirror(reflecting: theme)
for child in mirror.children {
if let label = child.label,
label != "theme", //check to prevent recursive calls
self.responds(to: Selector(label)) {
self.setValue(child.value, forKey: label)
print("Property name:", child.label)
print("Property value:", child.value)
}
}
}
}
Now I have two problems:
1 - Properties that have setter/getter do not show up in the reflection, example:
#IBInspectable public var borderColor: UIColor {
set { layer.borderColor = newValue.cgColor }
get { return UIColor(cgColor: layer.borderColor!) }
}
Whereas properties that use didSet do, such as:
#IBInspectable public var iconText: String = "" { didSet { self.setupIcon() } }
However, I do need a getter here to return the borderColor in layer.
2 - When using Mirror to reflect all ZFButton properties, besides the issue described in (1), I don't get UIButton properties either, is there a way to get ZFButton's superclass (UIButton) properties as well?
For the first problem I ended up using the following extension, which does see properties with getters/setters unlike Mirror:
extension NSObject {
func propertiesNames() -> [String] {
var count : UInt32 = 0
let classToInspect = type(of: self)
guard let properties : UnsafeMutablePointer <objc_property_t> = class_copyPropertyList(classToInspect, &count) else { return [] }
var propertyNames : [String] = []
let intCount = Int(count)
for i in 0..<intCount {
let property : objc_property_t = properties[i]
let propertyName = NSString(utf8String: property_getName(property))!
propertyNames.append(propertyName as String)
}
free(properties)
return propertyNames
}
}
As for the second issue I ended up copying each property over from the theme to the button as they are always the same. The goal was to avoid having to maintain a Theme class to bridge values every time something new is implemented in ZFButton.

Swift #IBInspectables with priority choose the order are they executed in

I am playing a bit with #IBInspectables. I have created a reusable custom View which has some #IBInspectables.
Is there no way to give priority to the #IBInspectables to get executed?
In the following case to modify the color or the font of the placeholder Its needed to do through an attributed text. So I need that some #IBInspectables like Font, Color, get executed before the #IBInspectable which sets the placeholder text.
In this case I have done the workaround to get always the placeholder Color. However, I want to add more attributes to the placeholder like the Font but if I don't know which order are they going to get executed I would have to set the "attributedPlaceholder" from every IBInspectable that modifies the placeholder)
#IBInspectable
var placeholder: String? {
didSet {
guard let placeholder = placeholder else { return }
textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSAttributedStringKey.foregroundColor: placeholderColor ?? UIColor.red])
}
}
#IBInspectable
var placeholderColor: UIColor? {
didSet {
guard let placeholderColor = placeholderColor else { return }
textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder != nil ? textField.placeholder! : "", attributes: [NSAttributedStringKey.foregroundColor: placeholderColor])
}
}
You should write the setters in a way that the order of calls won't matter. This is not only about the order of calls in Interface Builder, this is also about the order when called programatically.
It shouldn't matter whether you call:
view.placeholder =
view.placeholderColor =
or
view.placeholderColor =
view.placeholder =
A sample implementation:
#IBInspectable
var placeholder: String? {
didSet {
updatePlaceholder()
}
}
#IBInspectable
var placeholderColor: UIColor? {
didSet {
updatePlaceholder()
}
}
private func updatePlaceholder() {
textField.attributedPlaceholder = NSAttributedString(
string: placeholder ?? "",
attributes: [.foregroundColor: placeholderColor ?? UIColor.red]
)
}

Simple clickable link in Cocoa and Swift

Im writing a desktop app in swift for mac 10.11 and I would like to add a link to the about page.
Very much like this: https://developer.apple.com/library/mac/qa/qa1487/_index.html
I haven't been able to find a good tutorial or reference.
Any help would be much appreciated
Swift 4, xCode 9
#IBDesignable
class HyperlinkTextField: NSTextField {
#IBInspectable var href: String = ""
override func resetCursorRects() {
discardCursorRects()
addCursorRect(self.bounds, cursor: NSCursor.pointingHand)
}
override func awakeFromNib() {
super.awakeFromNib()
// TODO: Fix this and get the hover click to work.
let attributes: [NSAttributedStringKey: Any] = [
NSAttributedStringKey.foregroundColor: NSColor.linkColor,
NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue as AnyObject
]
attributedStringValue = NSAttributedString(string: self.stringValue, attributes: attributes)
}
override func mouseDown(with theEvent: NSEvent) {
if let localHref = URL(string: href) {
NSWorkspace.shared.open(localHref)
}
}
}
Modified the existing answers to allow for a substring of the label's text to become underlined and blue, so you can do something like: This is the answer
// A text field that can contain a hyperlink within a range of characters in the text.
#IBDesignable
public class SubstringLinkedTextField: NSTextField {
// the URL that will be opened when the link is clicked.
public var link: String = ""
#available(*, unavailable, message: "This property is reserved for Interface Builder. Use 'link' instead.")
#IBInspectable public var HREF: String {
get {
return self.link
}
set {
self.link = newValue
self.needsDisplay = true
}
}
// the substring within the field's text that will become an underlined link. if empty or no match found, the entire text will become the link.
public var linkText: String = ""
#available(*, unavailable, message: "This property is reserved for Interface Builder. Use 'linkText' instead.")
#IBInspectable public var LinkText: String {
get {
return self.linkText
}
set {
self.linkText = newValue
self.needsDisplay = true
}
}
override public func awakeFromNib() {
super.awakeFromNib()
self.allowsEditingTextAttributes = true
self.isSelectable = true
let url = URL(string: self.link)
let attributes: [NSAttributedStringKey: AnyObject] = [
NSAttributedStringKey(rawValue: NSAttributedStringKey.link.rawValue): url as AnyObject
]
let attributedStr = NSMutableAttributedString(string: self.stringValue)
if self.linkText.count > 0 {
if let range = self.stringValue.indexOf(substring: self.linkText) {
attributedStr.setAttributes(attributes, range: range)
} else {
attributedStr.setAttributes(attributes, range: NSMakeRange(0, self.stringValue.count))
}
} else {
attributedStr.setAttributes(attributes, range: NSMakeRange(0, self.stringValue.count))
}
self.attributedStringValue = attributedStr
}
}
The easiest way is to subclass NSTextField to create a HyperlinkTextField. Below is an example:
First, let's add a HyperlinkTextField class to your project:
// HyperlinkTextField.swift
import Cocoa
#IBDesignable
class HyperlinkTextField: NSTextField {
#IBInspectable var href: String = ""
override func awakeFromNib() {
super.awakeFromNib()
let attributes: [String: AnyObject] = [
NSForegroundColorAttributeName: NSColor.blueColor(),
NSUnderlineStyleAttributeName: NSUnderlineStyle.StyleSingle.rawValue
]
self.attributedStringValue = NSAttributedString(string: self.stringValue, attributes: attributes)
}
override func mouseDown(theEvent: NSEvent) {
NSWorkspace.sharedWorkspace().openURL(NSURL(string: self.href)!)
}
}
Next, in Interface Builder, drag a label from the Object library to your window.
Select that label, go to the menu View > Utilities > Show Identity Inspector (or press Cmd + Opt + 3) and change the class to HyperlinkTextField
Go to the Attributes Inspector (Cmd + Opt + 4) and set Href to the URL you want to visit.
The label shows black text in Interface Builder but everything will be fine when you run your app. Clicking on the label will open the link in your default browser.
One thing I couldn't achieve was to make the HyperlinkTextField shows up as blue and underlined in Interface Builder. Comments on how to do that are welcome.
Exactly what I needed. Here is the Swift3 version:
import Cocoa
#IBDesignable
class HyperTextField: NSTextField {
#IBInspectable var href: String = ""
override func awakeFromNib() {
super.awakeFromNib()
let attributes: [String: AnyObject] = [
NSForegroundColorAttributeName: NSColor.blue,
NSUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue as AnyObject
]
self.attributedStringValue = NSAttributedString(string: self.stringValue, attributes: attributes)
}
override func mouseDown(with event: NSEvent) {
NSWorkspace.shared().open(URL(string: self.href)!)
}
}
let link = NSTextField()
link.isBezeled = false
link.drawsBackground = false
link.isEditable = false
link.isSelectable = true
link.allowsEditingTextAttributes = true
let url = URL(string: "http://www.google.com")
let linkTextAttributes: [NSAttributedStringKey: Any] = [
NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue,
NSAttributedStringKey.foregroundColor: NSColor.blue,
NSAttributedStringKey.link: url as Any
]
let string = "whatever"
link.attributedStringValue = NSAttributedString(string: string, attributes: linkTextAttributes)
window.contentView?.addSubview(link)
swift 4.2
class HyperlinkTextField: NSTextField {
private var detector: NSDataDetector!
override func awakeFromNib() {
super.awakeFromNib()
detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
}
func setText(text: String ){
let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
for match in matches {
guard let range = Range(match.range, in: text) else { continue }
let url = text[range]
let attributedString = NSMutableAttributedString(string: text)
attributedString.addAttribute(.link, value: url, range: match.range)
self.attributedStringValue = attributedString
}
}
}

Save data of a Label in swift

I have a TextField in my settingsController that i can modify selecting a name from a pickerView, the problem is this : when i change the text of my textField i also change the text of a label in another controller, it work but when i close and reopen my app the label is empty, i can't find a way to save the text that i give it with the pickerView.
My code in the settingsController
override func viewDidLoad() {
super.viewDidLoad()
var defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
if let firstNameIsNotNill = defaults.objectForKey("firstName") as? String {
self.currencyLabel.text = defaults.objectForKey("firstName") as String
}
currencyLabel.delegate = self
}
func currencyDoneClicked(sender: UIBarButtonItem) {
var myRow = picker.selectedRowInComponent(0)
currencyLabel.text = pickerData.objectAtIndex(myRow) as NSString
DataManager.sharedInstance.contenitore = currencyLabel.text
var defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(self.currencyLabel.text, forKey: "firstName")
defaults.synchronize()
}
DataManager :
import UIKit
class DataManager: NSObject {
class var sharedInstance:DataManager {
get {
struct Static {
static var instance : DataManager? = nil
static var token : dispatch_once_t = 0
}
dispatch_once(&Static.token) { Static.instance = DataManager() }
return Static.instance!
}
}
var contenitore : String!
}
And in the other controller :
override func viewDidLoad() {
super.viewDidLoad()
labelCurrency.text = DataManager.sharedInstance.contenitore
}
Just use NSUserDefaults.
NSUserDefaults.standardUserDefaults().setValue(textField.text, forKey: "savedTextField")
Then, when you start up again, populate the field in viewDidLoad or viewWillAppear.
if let text = NSUserDefaults.standardUserDefaults().stringForKey("savedTextField") {
textField.text = text
}