I am lacking some basic understandings.
Today I wanted to subclass some UIView and I looked in to the UIButton definition, but I can not figure out how it works.
In the UIButton definition are properties like:
open var adjustsImageWhenHighlighted: Bool
open var adjustsImageWhenDisabled: Bool
When using an UIButton it does not matter when the values of the UIButton get set, it always gets configured the correct way same with tableView or any other UIKit classes.
I made a example:
class customView: UIView {
var shouldSetupConstraints = true
var addAdditionalSpacing = true
var elements: [String]? {
didSet{
if addAdditionalSpacing == false {
doSomething()
}
}
}
func doSomething() {
}
override init(frame: CGRect) {
super.init(frame: frame)
setUpLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func updateConstraints() {
if(shouldSetupConstraints) {
// AutoLayout constraints
shouldSetupConstraints = false
}
super.updateConstraints()
}
func setUpLayout() {
}
}
Using CustomView:
lazy var customV: CustomView = {
let v = CustomView()
v.addAdditionalSpacing = true
v.elements = ["One","Two"]
return v
}()
lazy var customV2: CustomView = {
let v = CustomView()
v.elements = ["One","Two"]
v.addAdditionalSpacing = true
return v
}()
So if I am using CustomView it makes a difference in which order I set it up, I understand why but I do not understand in which way I have to design my classes so I can set the values whenever I want, except with different if didSet configurations. Why do the properties in UIButton do not have any setters, how do the values get set in that case?
Any links to documentations are appreciated as well.
First of all, in a UIButton you can take control of the didSet property observer for existing properties like this:
override var adjustsImageWhenDisabled: Bool {
didSet {
doStuff()
}
}
Secondly, in your class scenario you might consider passing the parameters in the constructor:
init(shouldSetupConstraints: Bool = true, var addAdditionalSpacing = true)
This way the properties will be available for you whenever you need them.
Let me know if this helps.
Related
I have a custom view and the VC for it. The custom view has all kind of things in it, one of these are the labels for the available sizes for the given product. :
class CustomView: UIView {
var productSize: Array<String>
// all other labels and stuff
required init(size: Array<String>) {
self.productSizes = size
super.init(frame: .zero)
setupView()
setupConstraints()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And the other view:
class CustomViewViewController: UIViewController {
private let productDetailView = ProductDetailsView(size: [])
let product: Product
}
I get the product without any problem, but how should I connect the available sizes with the view? Obviously that empty array there is not correct, how should i connect the data from the remote file with the view init?
I was thinking about making an array and pushing it with the product itself from the original table view like this:
if let productsArray = self.productsArray {
let allSizes: Array<String> = productsArray[indexPath.row].sizes?.components(separatedBy: ",") ?? []
let customVC = CustomViewViewController(product: productsArray[indexPath.row], allSizes: allSizes)
navigationController?.pushViewController(customVC, animated: true)
}
But I could't really figure out how this one should work either...
You can make it a lazy var
lazy var productDetailView:ProductDetailsView = {
let res = ProductDetailsView(size:self.product.sizes?.components(separatedBy: ",") ?? [])
return res
}()
OR
1- var productDetailView:ProductDetailsView!
2- Inside viewDidLoad
productDetailView = ProductDetailsView(size:self.product.sizes?.components(separatedBy: ",") ?? [])
I wanted to implement my own HUD for a UIViewCntroller and a UIView, so I did this:
protocol ViewHudProtocol {
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
Now I can easily adopt ViewHudProtocol on any UIView to call showLoadingView and hideLoadingView. The problem is I want to use the same protocol for UIViewController, so I did this:
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.view.showLoadingView() //Error: UIView has no member showLoadingView
}
func hideLoadingView() {
self.view.hideLoadingView() //Error: UIView has no member hideLoadingView
}
}
I agree to the error that UIView has not adopted the protocol yet. So I did this:
extension UIView: ViewHudProtocol {}
And it works. Is there a better way to do this? I mean it feels wrong to extend every view with ViewHudProtocol, where not all of them will use it. If I could do something like, "only adopt ViewHudProtocol implicitly for a UIView, if its UIViewController demands for it. Else you could adopt ViewHUDProtocol manually on any UIView when required."
I would solve this with the following approach, using associatedtype, defined only for needed views and/or controllers (tested in Xcode 11.2 / swift 5.1):
protocol ViewHudProtocol {
associatedtype Content : ViewHudProtocol
var content: Self.Content { get }
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
var content: some ViewHudProtocol {
return self
}
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.content.showLoadingView() //NO Error
}
func hideLoadingView() {
self.content.hideLoadingView() //NO Error
}
}
//Usage
extension UITableView: ViewHudProtocol { // only for specific view
}
extension UITableViewController: ViewHudProtocol { // only for specific controller
var content: some ViewHudProtocol {
return self.tableView
}
}
The problem
So you want to constraint the conformance of a UIViewController to the protocol ViewHudProtocol only when the UIViewController.view property conforms to ViewHudProtocol.
I am afraid this is not possible.
Understanding the problem
Let's have a better look at your problem
You have 2 types (UIView and UIViewController) and you want to add to both the same functionalities
func showLoadingView()
func hideLoadingView()
What Mick West teaches us
This kind of scenario is somehow similar to what Mick West faced during the development of the Tony Hawks series Mick West and an elegant solution is described in its article Evolve your hierarchy.
Solution
We can apply that approach to your problem and here's the solution
struct HudViewComponent {
let view: UIView
private let hud: UIView
init(view: UIView) {
self.view = view
self.hud = UIView(frame: view.frame)
self.hud.isHidden = true
self.view.addSubview(hud)
}
func showLoadingView() {
self.hud.isHidden = false
}
func hideLoadingView() {
self.hud.isHidden = true
}
}
protocol HasHudViewComponent {
var hidViewComponent: HudViewComponent { get }
}
extension HasHudViewComponent {
func showLoadingView() {
hidViewComponent.showLoadingView()
}
func hideLoadingView() {
hidViewComponent.hideLoadingView()
}
}
That's it, now you can add the hud functionalities to any Type conforming to HasHudViewComponent.
class SomeView: UIView, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self) }()
}
or
class MyViewController: UIViewController, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self.view) }()
}
Considerations
As you can see the idea is to thinking in terms of components.
You build a component (HudViewComponent) with your hud functionalities. The component only asks for the minimum requirements: it needs a UIView.
Next you define the HasHudViewComponent which states that the current type has a HudViewComponent property.
Finally you can add your hud functionalities to any Type which has a view (UIView, UIViewController, ...) simply conforming your type to HasHudViewComponent.
Notes
You asked an interesting question and I know this does not answers 100% what you were looking for, but by a practical point of view it should provides you with a tool to achieve what you need.
I would have taken this approach:
Create a UIView Class,
setup the view
Declare a shared object.
A function to show the view
A function to remove the view. and then call it in view controllers as IndicatorView.shared.show() , IndicatorView.shared.hide()
import Foundation
import UIKit
import Lottie
class IndicatorView : UIView {
static let shared = IndicatorView()
var loadingAnimation : AnimationView = {
let lottieView = AnimationView()
lottieView.translatesAutoresizingMaskIntoConstraints = false
lottieView.layer.masksToBounds = true
return lottieView
}()
var loadingLabel : UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont(name: "SegoeUI", size: 12)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func show() {
setupLoadingView()
self.alpha = 0
UIView.animate(withDuration: 0.5, animations: {
self.isHidden = false
self.alpha = 1
}, completion: nil)
applyLottieAnimation()
}
public func hide() {
self.alpha = 1
UIView.animate(withDuration: 0.5, animations: {
self.alpha = 0
}, completion: { _ in
self.isHidden = true
self.removeFromSuperview()
}) }
private func setupLoadingView() {
let controller = UIApplication.shared.keyWindow!.rootViewController!
controller.view.addSubview(self)
//setup your views here
self.setNeedsLayout()
self.reloadInputViews()
}
}
For this particular scenario, a Decorator would work better, and result in a better design:
final class HUDDecorator {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
func showLoadingView() {
// add the spinner
}
func hideLoadingView() {
// remove the spinner
}
}
Using the Decorator would then be as easy as declaring a property for it:
class MyViewController: UIViewController {
lazy var hudDecorator = HUDDecorator(view)
}
This will allow any controller to decide if it wants support for showing/hiding a loading view by simply exposing this property.
Protocols are too invasive for simple tasks like enhancing the looks on a UI component, and they have the disadvantage of forcing all views of a certain class to expose the protocol functionality, while the Decorator approach allows you to decide which view instances to receive the functionality.
We have a requirement in our project. We need to set accessibility identifier for all the components in approximately 40 view controllers. I was thinking how to achieve these basic work by getting each view controller name and iboutlet names in run time and generate ids by combining these values as accessibility id. For these, I need to get IBOutlet's names. How can I do that ? Or do you have any alternative idea for automating this process another way ?
Thanks.
You can try Sourcery
It able to parse all your source files and provide you information about IBOutlets of all controllers:
You interested in classes -> variables -> attributes
You can generate inline for all such variables didSet block in which you will setup proper accessibility identifier
you can check this link and you find a great solution to automate setting accessibilityIdentifier with the same name of the variable
https://medium.com/getpulse/https-medium-com-dinarajas-fixing-developer-inconveniences-ios-automation-e4832108051f
import UIKit
protocol AccessibilityIdentifierInjector {
func injectAccessibilityIdentifiers()
}
extension AccessibilityIdentifierInjector {
func injectAccessibilityIdentifiers() {
var mirror: Mirror? = Mirror(reflecting: self)
repeat {
if let mirror = mirror {
injectOn(mirror: mirror)
}
mirror = mirror?.superclassMirror
} while (mirror != nil)
}
private func injectOn(mirror: Mirror) {
for (name, value) in mirror.children {
if var value = value as? UIView {
UnsafeMutablePointer(&value)
.pointee
.accessibilityIdentifier = name
}
}
}
}
extension UIViewController: AccessibilityIdentifierInjector {}
extension UIView: AccessibilityIdentifierInjector {}
class BaseView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
injectAccessibilityIdentifiers()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
injectAccessibilityIdentifiers()
}
}
How to use it
class ViewController: BaseViewController {
let asd1 = BaseView()
let asd2 = BaseView()
let asd3 = BaseView()
let asd4 = BaseView()
let asd5 = BaseView()
let asd6 = BaseView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
[asd1, asd2, asd3, asd4, asd5, asd6].forEach {
print($0.accessibilityIdentifier)
}
}
}
I'm afraid I'm relatively new to Swift, but have looked around as best I could and haven't been able to figure out how to do this relatively simple task!
I would like to add a new property called "angle" to the class UIImageView, such that you could use "image.angle". Here's what I've got, having attempted to follow the method of a tutorial I used (note that the required init? part was suggested by Xcode and I am not too sure of what it does):
class selection_image: UIImageView {
var angle = Double()
init(angle: Double) {
self.angle = angle
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Thank you very much for any help!!
class selection_image: UIImageView {
var angle = Double()
// your new Init
required convenience init(angle: Double) {
self.init(frame: CGRect.zero)
self.angle = angle
}
override init(frame: CGRect) {
super.init(frame: CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Using Swift 4.2
#jasperthedog. You can add a property to a given class using AssociatedObjects of the Runtime using an extension as follows:
In this example, I add an optional viewAlreadyAppeared: Bool? property to UIViewController. And with this, I avoid creating subclasses of UIViewController
extension UIViewController {
private struct CustomProperties {
static var viewAlreadyAppeared: Bool? = nil
}
var viewAlreadyAppeared: Bool? {
get {
return objc_getAssociatedObject(self, &CustomProperties.viewAlreadyAppeared) as? Bool
}
set {
if let unwrappedValue = newValue {
objc_setAssociatedObject(self, &CustomProperties.viewAlreadyAppeared, unwrappedValue as Bool?, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}
I've subclassed an NSSlider that behaves differently whether the option key is pressed. In order to do that, I overrode the mouseDown func. It seems to do the job.
The thing is, I've noticed the connected #IBAction in my ViewController is only triggered when the option key is unpressed (i.e. when the mouseDown method is passed to super). What am I missing in order to allow the #IBAction to perform?
Many thanks
Besides the issue, improvement advices on the code are welcome... :-)
Josh
class ViewController: NSViewController {
#IBOutlet weak var theSlider: MySlider!
#IBAction func moveSlider(sender: NSSlider) {
print(sender.floatValue) //works only with optionKey unpressed
}
}
class MySlider: NSSlider { //Implemented in another source file
#IBInspectable var multiplier: Float = 0.5
private var modifierKeys = NSEventModifierFlags.AlternateKeyMask
private var optionKeyPressed = false
private var previousSliderPosition: Float = 0.0
//MARK: Init with NSCoder
required init?(coder: NSCoder) {
super.init(coder: coder)
Swift.print("init Coder called")
self.continuous = true
NSEvent.addLocalMonitorForEventsMatchingMask(.FlagsChangedMask) { (theEvent) -> NSEvent? in
self.flagsChanged(theEvent)
return theEvent
}
}
//MARK: Mouse tracking
override func mouseDown(theEvent: NSEvent) {
if optionKeyPressed {
var keepOn = true
previousSliderPosition = self.floatValue * Float(self.bounds.width) / Float(self.maxValue)
while keepOn {
if let nextEvent = self.window?.nextEventMatchingMask(Int(NSEventMask.LeftMouseUpMask.rawValue) | Int(NSEventMask.LeftMouseDraggedMask.rawValue))
{
switch nextEvent.type
{
case .LeftMouseDragged:
let mouseInnerLocationX = Float(self.convertPoint(nextEvent.locationInWindow, fromView: self.superview).x)
let mouseDelta = mouseInnerLocationX - previousSliderPosition
let newSliderPosition = previousSliderPosition + (mouseDelta) * multiplier
self.floatValue = newSliderPosition * Float(self.maxValue) / Float(self.bounds.width)
break
case .LeftMouseUp:
keepOn = false
break
default:
break
}
}
}
} else {
super.mouseDown(theEvent)
}
}
//MARK: Option key handling
override func flagsChanged(theEvent: NSEvent) {
if (theEvent.modifierFlags.rawValue & NSEventModifierFlags.DeviceIndependentModifierFlagsMask.rawValue) == NSEventModifierFlags.AlternateKeyMask.rawValue {
optionKeyPressed = true
} else {
optionKeyPressed = false
}
}
}
If you're not calling super.mouseDown, you need to send the action yourself:
sendAction(action, to: target)
sendAction(_:to:), action and target are existing members of NSControl.