Withable

📐 仅用 10 行代码即可实现声明式 UIKit。

请参阅相关文章:仅用 10 行代码即可实现声明式 UIKit:一个简单的扩展,而不是库 以了解更多信息。

如何使用

通过在 AnyObject 上的一个简单的扩展,您可以执行以下操作。

class ContentViewController: UIViewController {
    
    ...
    
    lazy var titleLabel = UILabel()
        .with {
            $0.text = viewModel.title
            $0.textColor = .label
            $0.font = .preferredFont(forTextStyle: .largeTitle)
        }
    
    ...
}

任何类型的对象都适用。

lazy var submitButton = UIButton()
    .with {
        $0.setTitle("Submit", for: .normal)
        $0.addTarget(self, action: #selector(didTapSubmitButton), for: .touchUpInside)
    }
present(
    DetailViewController()
        .with {
            $0.modalTransitionStyle = .crossDissolve
            $0.modalPresentationStyle = .overCurrentContext
        },
    animated: true
)
present(
    UIAlertController(title: title, message: message, preferredStyle: .alert)
        .with {
            $0.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
        },
    animated: true
)
let today = DateFormatter()
    .with {
        $0.dateStyle = .medium
        $0.locale = Locale(identifier: "en_US")
    }
    .string(from: Date())
lazy var displayLink = CADisplayLink(target: self, selector: #selector(update))
    .with {
        $0.isPaused = true
        $0.preferredFramesPerSecond = 120
        $0.add(to: RunLoop.main, forMode: .common)
    }

甚至包括值类型(在遵循 Withable 协议之后)。

extension PersonNameComponents: Withable { }

let name = PersonNameComponents()
    .with {
        $0.givenName = "Geri"
        $0.familyName = "Borbás"
    }

更不用说 3D 内容(ARKitRealityKitSceneKit)。

view.scene.addAnchor(
    AnchorEntity(plane: .horizontal)
        .with {
            $0.addChild(
                ModelEntity(
                    mesh: MeshResource.generateBox(size: 0.3),
                    materials: [
                        SimpleMaterial(color: .green, isMetallic: true)
                    ]
                )
            )
        }
)

工作原理

它通过这个 with 方法实现。💎

public extension Withable {
    
    func with(_ closure: (Self) -> Void) -> Self {
        closure(self)
        return self
    }
}

该方法实现了相当经典的模式。您可以将其视为介于非特定/参数化构建器,或具有可定制/可插拔装饰行为的装饰器之间。 有关所有详细信息(泛型、值类型),请参阅 Withable.swift

UIKit 的好处

该包包含一些我使用的 UIKit 类的方便的扩展(可能会随着它们的增长而移动到它们自己的包中)。 我故意将它们留在这里,因为它们可以作为示例,说明如何创建针对您代码库的需求量身定制的扩展

例如,您可以为 UILabel 创建一个方便的 text 装饰器

extension UILabel {
    
    func with(text: String?) -> Self {
        with {
            $0.text = text
        }
    }
}

此外,您可以将您的样式精简为简单的扩展,如下所示。

extension UILabel {
    
    var withTitleStyle: Self {
        with {
            $0.textColor = .label
            $0.font = .preferredFont(forTextStyle: .largeTitle)
        }
    }
    
    var withPropertyStyle: Self {
        with {
            $0.textColor = .systemBackground
            $0.font = .preferredFont(forTextStyle: .headline)
            $0.setContentCompressionResistancePriority(.required, for: .vertical)
        }
    }
    
    var withPropertyValueStyle: Self {
        with {
            $0.textColor = .systemGray
            $0.font = .preferredFont(forTextStyle: .body)
        }
    }
    
    var withParagraphStyle: Self {
        with {
            $0.textColor = .label
            $0.numberOfLines = 0
            $0.font = .preferredFont(forTextStyle: .footnote)
        }
    }
}

通过这样的扩展,您可以清理视图控制器。

class ContentViewController: UIViewController {
    
    let viewModel = Planets().earth
    
    private lazy var body = UIStackView().vertical(spacing: 10).views(
        UILabel()
            .with(text: viewModel.title)
            .withTitleStyle,
        UIStackView().vertical(spacing: 5).views(
            UIStackView().horizontal(spacing: 5).views(
                UILabel()
                    .with(text: "size")
                    .withPropertyStyle
                    .withBox,
                UILabel()
                    .with(text: viewModel.properties.size)
                    .withPropertyValueStyle,
                UIView.spacer
            ),
            UIStackView().horizontal(spacing: 5).views(
                UILabel()
                    .with(text: "distance")
                    .withPropertyStyle
                    .withBox,
                UILabel()
                    .with(text: viewModel.properties.distance)
                    .withPropertyValueStyle,
                UIView.spacer
            ),
            UIStackView().horizontal(spacing: 5).views(
                UILabel()
                    .with(text: "mass")
                    .withPropertyStyle
                    .withBox,
                UILabel()
                    .with(text: viewModel.properties.mass)
                    .withPropertyValueStyle,
                UIView.spacer
            )
        ),
        UIImageView()
            .with(image: UIImage(named: viewModel.imageAssetName)),
        UILabel()
            .with(text: viewModel.paragraphs.first)
            .withParagraphStyle,
        UILabel()
            .with(text: viewModel.paragraphs.last)
            .withParagraphStyle,
        UIView.spacer
    )
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(body)
        view.backgroundColor = .systemBackground
        body.pin(
            to: view.safeAreaLayoutGuide,
            insets: UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 30)
        )
    }
}

我建议阅读相关文章 仅用 10 行代码即可实现声明式 UIKit:一个简单的扩展,而不是库 以了解更多关于背景和更多示例。

Apple 的使用

后来,我发现 Apple 有时会使用完全相同的模式来启用对象的内联装饰。 这些装饰器函数甚至使用相同的 with 命名约定。

以下示例位于原生 UIKit 中。 🍦

let arrow = UIImage(named: "Arrow").withTintColor(.blue)
let mail = UIImage(systemName: "envelope").withRenderingMode(.alwaysTemplate)
let color = UIColor.label.withAlphaComponent(0.5)

扩展中的存储属性

此外,该包包含一个 NSObject 扩展,可帮助在扩展中创建存储属性。 我最终将其包含在内,因为我发现使用存储属性扩展 UIKit 类是一个非常常见的用例。 有关更多信息,请参见 NSObject+Extensions.swiftUIButton+Extensions.swift

您可以执行以下操作。

extension UITextField {
    
    var nextTextField: UITextField? {
        get {
            associatedObject(for: "nextTextField") as? UITextField
        }
        set {
            set(associatedObject: newValue, for: "nextTextField")
        }
    }
}

内联声明约束

另一个秘密武器是 UIView.onMoveToSuperview 扩展,它只是一个在 view 添加到 superview 时调用的闭包(一次)。 这样,您可以使用此闭包在初始化时提前声明约束,然后在运行时在视图具有 superview 时添加/激活它们。 有关用法示例,请参见 Keyboard Avoidance 存储库。

许可证

MIT 许可证 下获得许可。