Is it possible for an instance of a UIView to call a method which executes a closure, and inside that closure referring to the same instance? This is the non-generic version:
import UIKit
public extension UIView {
func layout(from: (UIView) -> ()) {
from(self)
}
}
When I call it with a UILabel for example, I do not have access to e.g. the text aligment. Is it possible that inside the closure I can refer to the UILabel? I would expect something like this would work:
func layout(from: (Self) -> ()) {
from(self)
}
But it doesn't compile. Is there a workaround? This is what I want:
let label = UILabel(frame: .zero)
label.layout { $0.textAlignment = .natural } // Currenly not working, since $0 = UIView.
Different approach: Protocol Extension with associated type.
protocol Layout {
associatedtype View : UIView = Self
func layout(from: (View) -> ())
}
extension Layout where Self : UIView {
func layout(from: (Self) -> ()) {
from(self)
}
}
extension UIView : Layout {}
let label = UILabel(frame: .zero)
label.layout { $0.textAlignment = .natural }
There are different ways to do it.
Firstly, you could use the closures' variable capturing system in order to directly use the variable inside the closure, without passing it as an argument.
public extension UIView {
func layout(from: () -> ()) {
from()
}
}
label.layout { label.textAlignment = .natural }
Otherwise, if you want to pass a generic UIView and change the behaviour accordingly to the specific one - since it looks like you know for sure what type you are working on - you can use a downcast:
public extension UIView {
func layout(from: (UIView) -> ()) {
from(self)
}
}
let label = UILabel(frame: .zero)
label.layout { ($0 as! UILabel).textAlignment = .natural }
Anyway, why are you doing:
label.layout { $0.textAlignment = .natural }
instead of:
label.textAlignment = .natural
Is there any particular reason not to do it? I imagine there's something bigger behind the scenes, I'm just curious.
Related
Is there any way to get the following working in Swift 3?
let button = UIButton().apply {
$0.setImage(UIImage(named: "UserLocation"), for: .normal)
$0.addTarget(self, action: #selector(focusUserLocation),
for: .touchUpInside)
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = UIColor.black.withAlphaComponent(0.5)
$0.layer.cornerRadius = 5
}
The apply<T> function should take a closure of type (T)->Void, run it passing self into it, and then simply return self.
Another option would be to use an operator for this like "=>"
(borrowed the idea from Kotlin and Xtend languages).
Tried to do extension of NSObject like this:
extension NSObject {
func apply<T>(_ block: (T)->Void) -> T
{
block(self as! T)
return self as! T
}
}
But it requires explicit declaration of the parameter type in closure:
let button = UIButton().apply { (it: UIButton) in
it.setImage(UIImage(named: "UserLocation"), for: .normal)
it.addTarget(self, action: #selector(focusUserLocation),
for: .touchUpInside)
...
This is not convenient and makes the whole idea not worth the effort. The type is already specified at object creation and it should be possible not to repeat it explicitly.
Thanks!
The HasApply protocol
First of all lets define the HasApply protocol
protocol HasApply { }
and related extension
extension HasApply {
func apply(closure:(Self) -> ()) -> Self {
closure(self)
return self
}
}
Next let make NSObject conform to HasApply.
extension NSObject: HasApply { }
That's it
Let's test it
let button = UIButton().apply {
$0.titleLabel?.text = "Tap me"
}
print(button.titleLabel?.text) // Optional("Tap me")
Considerations
I wouldn't use NSObject (it's part of the Objective-C way of doing things and I assume it will be removed at some point in the future). I would prefer something like UIView instead.
extension UIView: HasApply { }
I had the same issue and ended up solving it with an operator:
infix operator <-< : AssignmentPrecedence
func <-<<T:AnyObject>(left:T, right:(T)->()) -> T
{
right(left)
return left
}
let myObject = UIButton() <-< { $0.isHidden = false }
There's a very good and simple Cocoapods library available called Then that does exactly that. Only that it uses then instead of apply. Simply import Then and then you can do as the OP asked for:
import Then
myObject.then {
$0.objectMethod()
}
let label = UILabel().then {
$0.color = ...
}
Here's how the protocol is implemented: https://github.com/devxoul/Then/blob/master/Sources/Then/Then.swift
extension Then where Self: Any {
public func then(_ block: (Self) throws -> Void) rethrows -> Self {
try block(self)
return self
}
Alain has a good answer if you're not allergic to custom operators. If you'd rather not use those, the best alternative I could come up with was:
#discardableResult func apply<T>(_ it:T, f:(T)->()) -> T {
f(it)
return it
}
which then allows you to use:
let button = apply(UIButton()) { $0.setTitleText("Button") }
It's not quite the same, but works out pretty well in general and has the advantage that T is completely unrestrained. It's an obviously contrived example, but:
func apply<T,R>(_ it:T, f:(T)->R) -> R {
return f(it)
}
even allows:
print("\(apply(32) { $0 + 4 })")
In case of object which must be created with non optional init:
let button = UIButton()
Optional(button).map {
$0.isEnabled = true
}
In case of object which must be created with optional init:
let button = UIButton(coder: coder)
button.map {
$0.isEnabled = true
}
In case of existing optional object we can use map function:
optionalObject.map {
$0.property1 = true
$0.property2 = true
}
If object must be cast before being use:
(optionalObject as? NewType).mapĀ {
$0.property1 = true
$0.property2 = true
}
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.
I have this enum :
enum WeatherMapDisplayType {
case temperature
case wind
}
Can I declare a variable like this ?
let markerView: mode == .temperature ? WeatherMapItemTemp : WeatherMapItemWind = UIView.fromNib()
Knowing that mode is of type WeatherMapDisplayType
How can I handle this scenario in an elegant way ?
EDIT:
I want to be able to do something like this :
let markerView: WeatherMapItem = UIView.fromNib()
markerView.frame = CGRect(x: 0, y: 0, width: 80, height: 30)
markerView.setupWeatherInformations(mode: self.currentDisplayMode, forecast: forecasts)
marker.iconView = markerView
Previously I only had WeatherMapItem type.
Then I have been asked to add an other weather map item, that is why I have the WeatherMapItemTemp and WeatherMapItemWind now (also corresponding to my enum display type).
func setupWeatherInformations(forecast: Forecast.HourlyForecast)
This is a method in my custom classes in order to configure the outlets.
But I don't have access to this method if I init my custom view from frame, because it's of UIView type.
Add a common protocol to these views:
protocol WeatherMapItem where Self: UIView {
func setupWeatherInformations()
}
class WeatherMapItemTemp: UIView, WeatherMapItem {
func setupWeatherInformations() {
// setup
}
}
class WeatherMapItemWind: UIView, WeatherMapItem {
func setupWeatherInformations() {
// setup
}
}
Add a computed variable to the enum:
enum WeatherMapDisplayType {
case temperature
case wind
var view: (UIView & WeatherMapItem)? {
var nibName: String? = nil
switch self {
case .temperature:
nibName = "WeatherMapItemTemp"
case .wind:
nibName = "WeatherMapItemWind"
}
if let nibName = nibName, let views = Bundle.main.loadNibNamed(nibName, owner: nil), views.count > 0 {
return views[0] as? UIView & WeatherMapItem
}
return nil
}
}
Now you can generate a view like so:
// assuming mode is the variable of type WeatherMapDisplayType
let view = mode.view
view?.setupWeatherInformations()
return view
Updated Answer
If you want to set data in that single ternary statement then declare one function in your custom view class.
Something like following
class WeatherMapItemTemp: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public func setForecast(forecastData: Forecast) -> WeatherMapItemTemp {
let view = WeatherMapItemTemp.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 100))
//Set your appropriate value to your view objects
return view
}
}
Use this as following
let markerView = mode == .temperature ? (WeatherMapItemTemp().setForecast(forecastData: YOUR_FORECAST_OBJ)) : (WeatherMapItemWind().setForecast(forecastData: YOUR_FORECAST_OBJ))
Update 2
Yes you will need to load that from a nib
Update setForecast as following.
class func setForecast(forecastData: Forecast) -> WeatherMapItemTemp {
let view = UINib(nibName: "WeatherMapItemTemp", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! WeatherMapItemTemp
//Do your further stuff here
return view
}
I am working on two views that are subclassing subclass of UITableViewCell. In the base one (subclass of UITableViewCell) I am trying to setup gesture recognizer in a way that each of super class could change the behavior (eventually call didTapped method on it's delegate) of the tap.
I have written following code. I can use #selector(tap), however I think that using a variable instead of overriding a tap method in each super class is a much cleaner way. Is it even possible to use something like #selector(tapFunc)? If no what would be the cleanest and best from engineering point of view solution?
class BaseCell: UITableViewCell {
#objc var tapFunc: () -> () = { () in
print("Tapped")
}
#objc func tap() {
print("TEST")
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tapFunc))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
And then two views that are building on top of this one:
class ViewA: BaseCell {
//don't want to do this
override func tap() {
//do stuff
}
func setup {
//setup everything else
}
class ViewB: BaseCell {
var delegate: ViewBProtocool?
func setup {
tapFunc = { () in
delegate?.didTapped(self)
}
//setup everything else
}
You're not too far off. Make the following changes:
class BaseCell: UITableViewCell {
var tapFunc: (() -> Void)? = nil
// Called by tap gesture
#objc func tap() {
tapFunc?()
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tap))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
class ViewA: BaseCell {
func setup() {
//setup everything else
}
}
class ViewB: BaseCell {
var delegate: ViewBProtocol?
func setup() {
tapFunc = {
delegate?.didTapped(self)
}
//setup everything else
}
}
Now each subclass can optionally provide a closure for the tapFunc property.
I show above that tapFunc is optional with no default functionality in the base class. Feel free to change that to provide some default functionality if desired.
I want to pass self as instancetype to the callback closure of this function:
extension UIView {
public func onTap(_ handler: #escaping (_ gesture: UITapGestureRecognizer, _ view: Self) -> Void) -> UITapGestureRecognizer {
...
}
}
let view = UIView.init()
view.onTap { tap, v in
...
}
But I got an error:
Self' is only available in a protocol or as the result of a method in a class; did you mean 'UIView'?
How can I do this?
that is just the perfect scenario (by book) when you can use protocols and extensions in Swift quite efficiently:
protocol Tappable { }
extension Tappable { // or alternatively: extension Tappable where Self: UIView {
func onTap(_ handler: #escaping (UITapGestureRecognizer, Self) -> Void) -> UITapGestureRecognizer {
return UITapGestureRecognizer() // as default to make this snippet sane
}
}
extension UIView: Tappable { }
then for e.g.:
let button = UIButton.init()
button.onTap { tap, v in
// v is UIButton...
}
while for e.g.:
let label = UILabel.init()
label.onTap { tap, v in
// v is UILabel...
}
etc...
NOTE: you can read more about Extensions or the Protocols in the Swift Programming Language Book from Apple.