简体   繁体   English

通用 UIView 初始化器,在 swift 中有闭包

[英]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.我想编写一个通用的 UIView 初始化程序,这样我就可以通过在初始化程序中传递配置闭包来初始化 UIViews。我想要的是下面的语法适用于所有 UIView 子类。

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. 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.注意:我能够实现上述使用协议初始化 UIView 和带有闭包的子类型的行为,但我想知道这是否可以通过这种方式实现,即在 UIView 扩展中编写便利初始化程序而无需任何附加协议。

Actually the problem is generic T does not resolved as UIButton .实际上问题是通用T没有解决为UIButton You didn't specify what is the parameter type of configuration closure is.您没有指定configuration闭包的参数类型是什么。

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 .现在将看到T泛型UIButton

What you ask about is not trivial using UIKit.使用 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.在 SwiftUI 中,视图样式是声明性的,但使用 Builder 模式,其中每个视图修饰符都返回视图,以便您可以链接自定义。 However, you specifically asked about being able to customize views by passing closure s, see Solution 2 below.但是,您特别询问是否能够通过传递闭包来自定义视图,请参阅下面的解决方案 2 But first I wanted to present a different approach, using ArrayLiterals with enums, see Solution 1.但首先我想提出一种不同的方法,将 ArrayLiterals 与枚举一起使用,请参阅解决方案 1。

Solution 1 - ViewComposer解决方案 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: ViewComposer是一个库(我几年前开发的),用于使用枚举的数组文字声明视图,它允许您像这样声明视图:

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.向下滚动到README 的底部,您将看到支持的视图列表,支持大多数视图类。

How ViewComposer works is a bit too complicated to post here, but have a look at the code! ViewComposer 的工作原理有点复杂,无法在此处发布,但请查看代码!

Solution 2 - Zhip解决方案 2 - 直邮

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.在我的开源 iOS Zilliqa 钱包应用程序中,我创建了另一个用于轻松配置 UIViews 的解决方案,与您的问题非常相似。

Here here is ReceiveView , which looks like this这里是ReceiveView ,看起来像这样

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:让我们 go 通过 config 的一些视图:

We config a UILabel called addressTitleLabel with this code:我们使用以下代码配置一个名为addressTitleLabelUILabel

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" . 只是具有键L10n.Scene.Receive.Label.myPublicAddress的已翻译字符串的本地化上下文的本地类型别名,因此对于具有英语语言设置的 iOS 设备,将转换为字符串"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: .withStyle(.title)是对我在UILabel上声明的名为withStyle的 function 的调用,请参见此处 Github 上的代码,即

@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. 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 ).这有点类似于SwiftUI 的 Font,它有一个名为title的枚举案例(但我在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 .在 SwiftUI 案例中,它是Font的预设,而在我的案例中,它是整个UILabel.Style的预设。 Let's have a look at it!让我们来看看吧!

  2. UILabel.Style , here is title : UILabel.Style这是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.所以它只是用值UIFont.title作为字体调用初始化程序。

  1. Customize block - in the withStyle function, the second argument is a trailing closure, being a closure to customise the preset style.自定义块 - 在withStyle function 中,第二个参数是尾随闭包,是自定义预设样式的闭包。 In the case of addressTitleLabel , here is where we set the text property of the UILabel .对于addressTitleLabel ,我们在这里设置UILabeltext属性。

  2. Multi customizing in closure - in another view - UnlockAppWithPincodeView we perform multiple customizations of a UILabel :闭包中的多重定制 - 在另一个视图中 - UnlockAppWithPincodeView我们执行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). styles 是如何应用的:前面我们看到了withStyle的代码,它在返回视图 (UILabel) 之前调用apply(style: style) )。 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.然后,我将这个模式应用于我想支持的每个 UIKit 视图。 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).这肯定是一些样板复制粘贴(在上面提到的ViewComposer库中,我付出了更多努力来创建桥接协议等,但这也导致了更复杂的代码库)。 Please have a look at the directory in Zhip where all this code is placed - ../Source/Extensions/UIKit .请查看 Zhip 中放置所有这些代码的目录 - ../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.特别感谢 static 对每种样式的预设,这为 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 ).这是一个直接定义(您可以使用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.如果您需要在代码中引用任何其他属性,那么您可能需要将其更改为lazy var赋值,但是lazy var是一个变异声明有一点副作用。

The inout for the block parameter allows this to work not only with reference types, but with value types as well.块参数的inout允许它不仅适用于引用类型,也适用于值类型。

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主要问题是不适用于 UIView 子类,例如

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

It doesn't compile and the error is value of type Self has no member 'image'它无法编译,错误是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.这只是通过使用初始化程序,我认为我们遇到了 Swift 编译器的限制。

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...这不太一样,但我猜它仍然可读......

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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