簡體   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