繁体   English   中英

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

[英]Generic UIView Initializer with closure in swift

我想编写一个通用的 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.

这是我的初始化程序。

extension UIView {

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

        configurations(self as! T)
    }
}

任何建议将不胜感激。

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

实际上问题是通用T没有解决为UIButton 您没有指定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)
}

现在将看到T泛型UIButton

使用 UIKit,您提出的问题并非易事。 在 SwiftUI 中,视图样式是声明性的,但使用 Builder 模式,其中每个视图修饰符都返回视图,以便您可以链接自定义。 但是,您特别询问是否能够通过传递闭包来自定义视图,请参阅下面的解决方案 2 但首先我想提出一种不同的方法,将 ArrayLiterals 与枚举一起使用,请参阅解决方案 1。

解决方案 1 - ViewComposer

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)]

向下滚动到README 的底部,您将看到支持的视图列表,支持大多数视图类。

ViewComposer 的工作原理有点复杂,无法在此处发布,但请查看代码!

解决方案 2 - 直邮

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

这里是ReceiveView ,看起来像这样

zhip_receive

有这个代码:

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()
    }
}

和视图配置:


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)
        }
    }
}

让我们 go 通过 config 的一些视图:

我们使用以下代码配置一个名为addressTitleLabelUILabel

addressTitleLabel.withStyle(.title) {
    $0.text(€.Label.myPublicAddress)
}
  1. 只是具有键L10n.Scene.Receive.Label.myPublicAddress的已翻译字符串的本地化上下文的本地类型别名,因此对于具有英语语言设置的 iOS 设备,将转换为字符串"My public address"

  2. .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. 这有点类似于SwiftUI 的 Font,它有一个名为title的枚举案例(但我在SwiftUI发布之前很久就创建了这个)。 在 SwiftUI 案例中,它是Font的预设,而在我的案例中,它是整个UILabel.Style的预设。 让我们来看看吧!

  2. UILabel.Style这是title

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

所以它只是用值UIFont.title作为字体调用初始化程序。

  1. 自定义块 - 在withStyle function 中,第二个参数是尾随闭包,是自定义预设样式的闭包。 对于addressTitleLabel ,我们在这里设置UILabeltext属性。

  2. 闭包中的多重定制 - 在另一个视图中 - UnlockAppWithPincodeView我们执行UILabel的多重定制:

func setupSubviews() {
    descriptionLabel.withStyle(.body) {
        $0
            .text(€.label)
            .textAlignment(.center)
    }
}
  1. styles 是如何应用的:前面我们看到了withStyle的代码,它在返回视图 (UILabel) 之前调用apply(style: style) )。 这是我们的视图样式化的地方。 这正是您所期望的,它适用于每个配置:
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. 然后,我将这个模式应用于我想支持的每个 UIKit 视图。 这肯定是一些样板复制粘贴(在上面提到的ViewComposer库中,我付出了更多努力来创建桥接协议等,但这也导致了更复杂的代码库)。 请查看 Zhip 中放置所有这些代码的目录 - ../Source/Extensions/UIKit

  2. 特别感谢 static 对每种样式的预设,这为 UIViews 的创建和样式提供了非常简洁和简短的语法。

像这样简单的事情怎么样?

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

像这样使用它:

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

它看起来像一个惰性初始化器,但事实并非如此。 这是一个直接定义(您可以使用let )。 如果您需要在代码中引用任何其他属性,那么您可能需要将其更改为lazy var赋值,但是lazy var是一个变异声明有一点副作用。

块参数的inout允许它不仅适用于引用类型,也适用于值类型。

我一直在我的代码中使用它,我很惊讶没有人用这样的解决方案来回答这个问题。

我越接近你想要做的就是:

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

简单地与

UIView { $0.backgroundColor = .red }

主要问题是不适用于 UIView 子类,例如

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

它无法编译,错误是value of type Self has no member 'image'

这只是通过使用初始化程序,我认为我们遇到了 Swift 编译器的限制。

但是,您可以尝试以下解决方法:

public protocol WithPropertyAssignment {}

public extension WithPropertyAssignment where Self: AnyObject {

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

extension NSObject: WithPropertyAssignment {}

然后你可以做

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

这不太一样,但我猜它仍然可读......

暂无
暂无

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

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