简体   繁体   中英

Generic UIView Initializer with closure in swift

I want to write a generic UIView initializer so I can initialize UIViews by passing the configuration closure in initializer.What I want is the below syntax to work for all UIView subclasses.

let button = UIButton() {
    $0.backgroundColor = UIColor.red
    $0.frame = CGRect(x: 220, y: 30, width: 100, height: 100)
    $0.setTitle("Test", for: .normal)
}

I have written the convenience initializer in UIView extension but with this I am not able to set UIView subclasses properties like the setTitle(_:for) property for UIButton because in closure it is always sending UIView type parameter instead of the specific subclass type.

Here is my initializer.

extension UIView {

    convenience init<T: UIView>(_ configurations: (T) -> Void) {
        self.init()

        configurations(self as! T)
    }
}

Any suggestions will be appreciated.

NOTE: I was able to achieve the above behaviour of initializing UIView and subtypes with closure using protocol but I wonder if this can be achieved this way ie Writing convenience initializer in UIView extension without any additional protocol.

Actually the problem is generic T does not resolved as UIButton . You didn't specify what is the parameter type of configuration closure is.

let button = UIButton() { (b: UIButton) in
    b.backgroundColor = UIColor.red
    b.frame = CGRect(x: 220, y: 30, width: 100, height: 100)
    b.setTitle("Test", for: .normal)
}

Now the T generic will be seen UIButton .

What you ask about is not trivial using UIKit. In SwiftUI styling of views is declarative, but using a Builder pattern, where each view modifier returns the view so that you can chain customization. However, you specifically asked about being able to customize views by passing closure s, see Solution 2 below. But first I wanted to present a different approach, using ArrayLiterals with enums, see Solution 1.

Solution 1 - ViewComposer

ViewComposer is a library (that I developed some years ago) for declaring views using array literals of enums, which allows you to declare views like so:

let button: UIButton = [.color(.red), .text("Red"), .textColor(.blue)]
let label: UILabel = [.text("Hello World"), .textColor(.red)]
lazy var emailField: UITextField = [.font(.big), .height(50), .placeholder("Email"), .delegate(self)]

Scroll down to the bottom of the README and you will see a list of supported views , most view classes are supported.

How ViewComposer works is a bit too complicated to post here, but have a look at the code!

Solution 2 - Zhip

In my open source iOS Zilliqa wallet app called Zhip I've created yet another solution for easily configuring UIViews, much similar to your question.

Here here is ReceiveView , which looks like this

zhip_receive

Having this code:

final class ReceiveView: ScrollableStackViewOwner {

    private lazy var qrImageView            = UIImageView()
    private lazy var addressTitleLabel      = UILabel()
    private lazy var addressValueTextView   = UITextView()
    private lazy var copyMyAddressButton    = UIButton()
    private lazy var addressAndCopyButton   = UIStackView(arrangedSubviews: [addressValueTextView, copyMyAddressButton])
    private lazy var addressViews           = UIStackView(arrangedSubviews: [addressTitleLabel, addressAndCopyButton])
    private lazy var requestingAmountField  = FloatingLabelTextField()
    private lazy var requestPaymentButton   = UIButton()

    // MARK: - StackViewStyling
    lazy var stackViewStyle = UIStackView.Style([
        qrImageView,
        addressViews,
        requestingAmountField,
        .spacer,
        requestPaymentButton
        ])

    override func setup() {
        setupSubviews()
    }
}

and config of views:


private typealias € = L10n.Scene.Receive
private extension ReceiveView {

    // swiftlint:disable:next function_body_length
    func setupSubviews() {
        qrImageView.withStyle(.default)

        addressTitleLabel.withStyle(.title) {
            $0.text(€.Label.myPublicAddress)
        }

        addressValueTextView.withStyle(.init(
            font: UIFont.Label.body,
            isEditable: false,
            isScrollEnabled: false,
            // UILabel and UITextView horizontal alignment differs, change inset: stackoverflow.com/a/45113744/1311272
            contentInset: UIEdgeInsets(top: 0, left: -5, bottom: 0, right: -5)
            )
        )

        copyMyAddressButton.withStyle(.title(€.Button.copyMyAddress))
        copyMyAddressButton.setHugging(.required, for: .horizontal)

        addressAndCopyButton.withStyle(.horizontal)
        addressViews.withStyle(.default) {
            $0.layoutMargins(.zero)
        }

        requestingAmountField.withStyle(.decimal)

        requestPaymentButton.withStyle(.primary) {
            $0.title(€.Button.requestPayment)
        }
    }
}

Let's go through config of some of the views:

We config a UILabel called addressTitleLabel with this code:

addressTitleLabel.withStyle(.title) {
    $0.text(€.Label.myPublicAddress)
}
  1. is just a local typealias to a localization context for translated strings with the key L10n.Scene.Receive.Label.myPublicAddress , so for an iOS device with English language setting that will translate to the string "My public address" .

  2. .withStyle(.title) is a call to a function called withStyle that I have declared on UILabel , see code on Github here , being:

@discardableResult
func withStyle(
    _ style: UILabel.Style,
    customize: ((UILabel.Style) -> UILabel.Style)? = nil
) -> UILabel {
    translatesAutoresizingMaskIntoConstraints = false
    let style = customize?(style) ?? style
    apply(style: style)
    return self
}
  1. We pass .title as an argument to the function, and in the function declaration above you can see that the type is UILabel.Style , meaning we have declared a static variable called title as an extension on UILabel.Style , being some default style. This is somewhat similar to SwiftUI's Font, which has an enum case called title (but I created this long before the release of SwiftUI ). Where in the SwiftUI case it is a preset of the Font , where as in my case it is a preset of a whole UILabel.Style . Let's have a look at it!

  2. UILabel.Style , here is title :

static var title: UILabel.Style {
    return UILabel.Style(
        font: UIFont.title
    )
}

So it is just calling the initializer with the value UIFont.title as font.

  1. Customize block - in the withStyle function, the second argument is a trailing closure, being a closure to customise the preset style. In the case of addressTitleLabel , here is where we set the text property of the UILabel .

  2. Multi customizing in closure - in another view - UnlockAppWithPincodeView we perform multiple customizations of a UILabel :

func setupSubviews() {
    descriptionLabel.withStyle(.body) {
        $0
            .text(€.label)
            .textAlignment(.center)
    }
}
  1. How styles are applied: Earlier above we saw the code for withStyle , which calls apply(style: style) before returning the view (UILabel). This is where our view gets styled. Which just does exactly what you would expect, it applies every config:
extension UILabel {
    func apply(style: Style) {
        text = style.text
        font = style.font ?? UIFont.Label.body
        textColor = style.textColor ?? .defaultText
        numberOfLines = style.numberOfLines ?? 1
        textAlignment = style.textAlignment ?? .left
        backgroundColor = style.backgroundColor ?? .clear
        if let minimumScaleFactor = style.adjustsFontSizeMinimumScaleFactor {
            adjustsFontSizeToFitWidth = true
            self.minimumScaleFactor = minimumScaleFactor
        }
    }
}
  1. I've then applied this pattern for every UIKit view I would like to support. Which is some boilerplate copy paste for sure (In above-mentioned ViewComposer lib I made some more effort of creating bridging protocols etc, but that also resulted in a much more complicated code base). Please have a look at the directory in Zhip where all this code is placed - ../Source/Extensions/UIKit .

  2. Especially thanks to static presets on each style this results in a pretty neat and short syntax for creating and styling of UIViews.

How about something as simple as this?

@discardableResult
func config<T>(_ object: T,
               _ block (inout T) throws -> Void) rethrows -> T {
    var object = object
    try block(&object)
    return object
}

Use it like this:

let view = config(UIView()) {
    $0.backgroundColor = .white
    $0.frame.size = .init(width: 50, height: 50)
    // ... other configuration code ...
}

It looks like a lazy initializer, but it isn't. It's a direct definition (you can use let ). If you need to refer to any other property in your code, then you might need to change it to a lazy var assignment, but there's a bit of a side effect that lazy var is a mutating declaration.

The inout for the block parameter allows this to work not only with reference types, but with value types as well.

I use this all the time in my code, and I'm surprised that nobody has responded to this question with such a solution.

The closer I've got to what you're trying to do is this:

public extension UIView {
    convenience init(_ closure: (Self) -> Void) {
        self.init()
        closure(self)
    }
}

Which is used simply with

UIView { $0.backgroundColor = .red }

Main issue is that is not working with UIView subclasses like

UIImageView { $0.image = UIImage(named: "whatever") }

It doesn't compile and the error is value of type Self has no member 'image'

This is just by using initializers, and I think we're hitting a limitation of Swift Compiler.

However, you can try this workaround:

public protocol WithPropertyAssignment {}

public extension WithPropertyAssignment where Self: AnyObject {

func with(_ closure: @escaping (Self) -> Void) -> Self {
        closure(self)
        return self
    }
}

extension NSObject: WithPropertyAssignment {}

and then you can do

UILabel().with { $0.text = "Ciao" }
UIImage().with { $0.image = UIImage() }

Which is not quite the same, but it's still readable I guess...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM