Files
2025-10-04 08:46:46 +01:00

267 lines
9.2 KiB
Swift
Executable File

// Copyright © 2017 Schibsted. All rights reserved.
import Layout
import ObjectiveC
import UIKit
private let validClasses: [String] = {
var classCount: UInt32 = 0
let classes = objc_copyClassList(&classCount)
var names = [String]()
for cls in UnsafeBufferPointer(start: classes, count: Int(classCount)) {
if class_getSuperclass(cls) != nil,
class_conformsToProtocol(cls, NSObjectProtocol.self),
cls.isSubclass(of: UIView.self) || cls.isSubclass(of: UIViewController.self)
{
let name = "\(cls)"
if !name.hasPrefix("_") {
names.append(name)
}
}
}
names = names.sorted()
return names
}()
protocol EditViewControllerDelegate: class {
func didUpdateClass(_ viewOrControllerClass: NSObject.Type, for node: LayoutNode)
func didUpdateExpression(_ expression: String, for name: String, in node: LayoutNode)
}
final class EditViewController: UIViewController, UITextFieldDelegate {
weak var delegate: EditViewControllerDelegate?
var node: LayoutNode! {
didSet {
guard rootNode != nil else { return }
if oldValue.view.classForCoder != node.view.classForCoder ||
oldValue.viewController?.classForCoder != node.viewController?.classForCoder
{
updateUI()
} else {
updateFieldValues()
}
}
}
var rootNode: LayoutNode!
@objc var classField: UITextField?
var expressionFields = [String: UITextField]()
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
preferredContentSize = CGSize(width: 320, height: 400)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
nil
}
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
override func viewDidLayoutSubviews() {
rootNode.update()
}
private func updateFieldValues() {
for (name, field) in expressionFields {
field.text = node.expressions[name]
}
}
private func updateUI() {
let cls: AnyClass = node.viewController?.classForCoder ?? node.view.classForCoder
var children = [
LayoutNode(
view: UITextField(),
outlet: #keyPath(classField),
expressions: [
"top": "10",
"left": "10",
"width": "100% - 20",
"height": "auto",
"borderStyle": "roundedRect",
"autocorrectionType": "no",
"autocapitalizationType": "none",
"placeholder": "Class",
"editingChanged": "didUpdateText",
"editingDidEnd": "didUpdateClass",
"text": "\(cls)",
]
),
]
expressionFields.removeAll()
func filterType(_ key: String, _ type: RuntimeType) -> String? {
if !type.isAvailable {
return nil
}
switch type.swiftType {
case is CGFloat.Type,
is Double.Type,
is Float.Type,
is Int.Type,
is NSNumber.Type,
is Bool.Type,
is String.Type,
is NSString.Type,
is NSAttributedString.Type,
is UIColor.Type,
is UIImage.Type,
is UIFont.Type,
is CGImage.Type,
is CGColor.Type,
is [String].Type:
return key
default:
return type.values.isEmpty ? nil : key
}
}
var fieldNames = ["top", "left", "width", "height", "bottom", "right"]
fieldNames.append(contentsOf: node.viewControllerExpressionTypes.compactMap(filterType).sorted())
fieldNames.append(contentsOf: node.viewExpressionTypes.compactMap(filterType).sorted {
switch ($0.hasPrefix("layer."), $1.hasPrefix("layer.")) {
case (true, true),
(false, false):
return $0 < $1
case (true, false):
return false
case (false, true):
return true
}
})
let start = CACurrentMediaTime()
for name in fieldNames {
children.append(
LayoutNode(
view: UILabel(),
expressions: [
"top": "previous.bottom + 5",
"left": "10",
"width": "100% - 20",
"height": "auto",
"text": name,
"font": "10",
]
)
)
let field = UITextField()
field.borderStyle = .roundedRect
field.autocorrectionType = .no
field.autocapitalizationType = .none
field.text = node.expressions[name]
expressionFields[name] = field
children.append(
LayoutNode(
view: field,
expressions: [
"top": "previous.bottom",
"left": "10",
"width": "100% - 20",
"height": "auto",
"borderStyle": "roundedRect",
"placeholder": name,
"editingDidEnd": "didUpdateField:",
]
)
)
}
rootNode = LayoutNode(
view: UIScrollView(),
expressions: [
"width": "100%",
"height": "100%",
],
children: [
LayoutNode(
view: UIView(),
expressions: [
"width": "100%",
"height": "auto + 10",
],
children: children
),
]
)
print("nodes:", children.count)
print("creation:", round((CACurrentMediaTime() - start) * 1000))
for view in view.subviews {
view.removeFromSuperview()
}
do {
try rootNode.mount(in: self)
} catch {
print("\nError: \(error)\n")
}
print("creation + mount:", round((CACurrentMediaTime() - start) * 1000))
}
@objc func didUpdateText() {
classField?.backgroundColor = .white
guard let classField = classField,
let textRange = classField.textRange(from: classField.beginningOfDocument, to: classField.selectedTextRange?.start ?? classField.endOfDocument),
let text = classField.text(in: textRange)
else {
return
}
var match = ""
for name in validClasses {
if match.isEmpty || name.count < match.count,
name.lowercased().hasPrefix(text.lowercased())
{
match = name
}
}
if !match.isEmpty {
let string = NSMutableAttributedString(string: String(match[...text.endIndex]),
attributes: [NSAttributedStringKey.foregroundColor: UIColor.black])
string.append(NSMutableAttributedString(string: String(match[text.endIndex...]),
attributes: [NSAttributedStringKey.foregroundColor: UIColor.lightGray]))
classField.attributedText = string
classField.selectedTextRange = classField.textRange(from: textRange.end, to: textRange.end)
return
}
let string = NSAttributedString(string: text,
attributes: [NSAttributedStringKey.foregroundColor: UIColor.black])
classField.attributedText = string
classField.selectedTextRange = classField.textRange(from: textRange.end, to: textRange.end)
}
@objc func didUpdateClass() {
guard let text = classField?.text else {
classField?.text = "UIView"
delegate?.didUpdateClass(UIView.self, for: node)
return
}
classField?.attributedText = NSAttributedString(string: text, attributes: [NSAttributedStringKey.foregroundColor: UIColor.black])
guard let cls = NSClassFromString(text) as? NSObject.Type,
cls is UIView.Type || cls is UIViewController.Type
else {
classField?.backgroundColor = UIColor(red: 1, green: 0.5, blue: 0.5, alpha: 1)
return
}
delegate?.didUpdateClass(cls, for: node)
}
@objc func didUpdateField(_ textField: UITextField) {
for (name, field) in expressionFields where field === textField {
if (node.expressions[name] ?? "") != (field.text ?? "") {
delegate?.didUpdateExpression(field.text ?? "", for: name, in: node)
}
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return false
}
}