Files

309 lines
11 KiB
Swift
Executable File

// Copyright © 2017 Schibsted. All rights reserved.
import AVKit
import GLKit
import MapKit
import SceneKit
import SpriteKit
import UIKit
import WebKit
import XCTest
@testable import Layout
final class UIKitSymbols: XCTestCase {
func getProperties() -> [String: [String: RuntimeType]] {
// Force classes to load
_ = AVPlayerViewController()
_ = WKWebView()
// Get class names
var classCount: UInt32 = 0
let classes = objc_copyClassList(&classCount)
var names = ["SKView"] // Doesn't load otherwise for some reason
for cls in UnsafeBufferPointer(start: classes, count: Int(classCount)) {
if class_getSuperclass(cls) != nil,
cls.isSubclass(of: UIView.self) || cls.isSubclass(of: UIViewController.self)
{
let name = NSStringFromClass(cls)
if !name.contains("_"), !name.contains(".") {
names.append(name)
}
}
}
// Filter views
// These lists are longer than the actual list of view we support
// but the advantage of this approach is that new classes added in
// future iOS releases will be picked up automatically
let whitelist = [
"AVPlayerViewController",
"AVPictureInPictureViewController",
"MKOverlay",
"MKMapView",
"GLK",
"SCNView",
"SKView",
"UI",
"WKWebView",
]
let blacklist = [
"AVPlayerViewControllerContentView",
"AVPlayerViewControllerCustomControlsView",
"AVPlayerViewControllerCustomControlsViewLayoutMarginsGuideProvidingView",
"MKOverlayContainer",
"UIAccessibility",
"UIActionSheet",
"UIActivityGroupViewController",
"UIActivityViewPopoverBackgroundView",
"UIAlertView",
"UIApplication",
"UIAutocorrect",
"UIAutoRotatingWindow",
"UIButtonLabel",
"UICallout",
"UICheckeredPatternView",
"UIClassic",
"UICollectionViewControllerWrapperView",
"UICollectionViewTable",
"UICompatibilityInputViewController",
"UICoverSheetButton",
"UIDateLabel",
"UIDatePickerContentView",
"UIDatePickerWeekMonthDayView",
"UIDebugging",
"UIDefaultKeyboardInput",
"UIDictation",
"UIDimmingView",
"UIDocument",
"UIDOM",
"UIDropShadowView",
"UIDynamicCaret",
"UIFieldEditor",
"UIGroupTable",
"UIIndexBar",
"UIInputS",
"UIInputWindow",
"UIInsertControl",
"UIInterface",
"UIKB",
"UIKeyCommand",
"UIKeyboard",
"UILayoutContainerView",
"UIMainPrinterUtilityCell",
"UIMore",
"UIMorphingLabel",
"UIMovieScrubber",
"UIMultiColumnViewController",
"UINavigationBarBackIndicatorView",
"UINavigationButton",
"UINavigationTransitionView",
"UINotesTableView",
"UIPageController",
"UIPasscodeField",
"UIPageControllerScrollView",
"UIPanel",
"UIPDF",
"UIPeripheral",
"UIPickerContentView",
"UIPickerColumnView",
"UIPickerTableView",
"UIPopoverB",
"UIPrint",
"UIProgressHUD",
"UIProgressIndicator",
"UIRecent",
"UIReferenceLibraryViewController",
"UIRemoteKeyboardWindow",
"UIRemoveControl",
"UIRoundedRect",
"UISegment",
"UISearch",
"UISelection",
"UIShadow",
"UISnapshot",
"UISplitAndMaskView",
"UISpringBoard",
"UISoftware",
"UIStatusBar",
"UISegment",
"UISwappableImageView",
"UISwipe",
"UISwitch",
"UISystemInputViewController",
"UITabBar",
"UITableViewBackground",
"UITableViewCell",
"UITableViewCollectionCell",
"UITableViewCountView",
"UITableViewIndex",
"UITableViewLabel",
"UITableViewWrapperView",
"UIText",
"UIToolbar",
"UITransitionView",
"UIURLDragPreviewView",
"UIVideoEditorController",
"UIViewControllerWrapperView",
"UIWeb",
"UIWindow",
"UIWK",
"UIZoomViewController",
]
names = names.filter { name in
whitelist.contains { name.hasPrefix($0) } &&
!blacklist.contains { name.hasPrefix($0) }
}
names += [
"UISearchBar",
"UISearchController",
"UISegmentedControl",
"UISwitch",
"UITabBar",
"UITabBarController",
"UITableViewCell",
"UITextField",
"UITextView",
"UIToolbar",
"UIWebView",
]
// Get properties
var result = [String: [String: RuntimeType]]()
for name in names {
var props: [String: RuntimeType]
guard let cls = NSClassFromString(name), let superclass = class_getSuperclass(cls) else {
continue
}
switch cls {
case let viewClass as UIView.Type:
props = viewClass.cachedExpressionTypes
if viewClass == UIView.self {
break
}
for (key, type) in (superclass as! UIView.Type).cachedExpressionTypes where props[key] == type {
props.removeValue(forKey: key)
}
case let controllerClass as UIViewController.Type:
props = controllerClass.cachedExpressionTypes
if controllerClass == UIViewController.self {
break
}
for (key, type) in (superclass as! UIViewController.Type).cachedExpressionTypes where props[key] == type {
props.removeValue(forKey: key)
}
default:
props = [:]
}
for (key, type) in props where !type.isAvailable {
props.removeValue(forKey: key)
}
result[name] = props
}
return result
}
func testBuildLayoutToolSymbols() throws {
if #available(iOS 11.0, *) {} else {
XCTFail("Must be run with latest iOS SDK to ensure all symbols are supported")
return
}
// Build output
var output = ""
let properties = getProperties()
for name in properties.keys.sorted() {
let props = try XCTUnwrap(properties[name])
var superclassName: String?
if let cls = NSClassFromString(name), let superclass = class_getSuperclass(cls),
superclass is UIView.Type || superclass is UIViewController.Type
{
superclassName = NSStringFromClass(superclass)
}
output += " symbols[\"\(name)\"] = ["
if let superclassName = superclassName {
output += "\n \"superclass\": \"\(superclassName)\","
}
if props.isEmpty, superclassName == nil {
output += ":]\n"
} else {
output += "\n"
for prop in props.keys.sorted() {
let type = try XCTUnwrap(props[prop])
output += " \"\(prop)\": \"\(type)\",\n"
}
output += " ]\n"
}
}
output = "// Copyright © 2017 Schibsted. All rights reserved.\n\n" +
"import Foundation\n\n" +
"// NOTE: This is a machine-generated file. Run the UIKitSymbols scheme to regenerate\n\n" +
"let UIKitSymbols: [String: [String: String]] = {\n" +
" var symbols = [String: [String: String]]()\n" + output +
" return symbols\n" +
"}()\n"
// Write output
let url = URL(fileURLWithPath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("LayoutTool/Symbols.swift")
guard FileManager.default.fileExists(atPath: url.path) else {
XCTFail("\(url) does not exist")
return
}
XCTAssertNoThrow(try output.write(to: url, atomically: true, encoding: .utf8))
}
func testBuildSublimeCompletions() throws {
if #available(iOS 11.0, *) {} else {
XCTFail("Must be run with latest iOS SDK to ensure all symbols are supported")
return
}
// Build output
var rows = [
"{ \"trigger\": \"outlet\tString\", \"contents\": \"outlet=\\\"$0\\\"\" }",
"{ \"trigger\": \"template\tURL\", \"contents\": \"template=\\\"$0\\\"\" }",
"{ \"trigger\": \"xml\tURL\", \"contents\": \"xml=\\\"$0\\\"\" }",
]
for name in Array(layoutSymbols).sorted() {
let type = (name == "center") ? "CGPoint" : "CGFloat"
rows.append("{ \"trigger\": \"\(name)\t\(type)\", \"contents\": \"\(name)\" }")
}
let properties = getProperties()
for name in properties.keys.sorted() {
let props = try XCTUnwrap(properties[name])
rows.append("{ \"trigger\": \"\(name)\", \"contents\": \"\(name) $0/>\" }")
for prop in props.keys.sorted() {
let type = try XCTUnwrap(props[prop])
let row = "{ \"trigger\": \"\(prop)\t\(type)\", \"contents\": \"\(prop)\" }"
if !rows.contains(row) {
rows.append(row)
}
}
}
let output = "{\n" +
" \"scope\": \"text.xml\",\n" +
" \"completions\": [\n " + rows.sorted().joined(separator: ",\n ") + "\n" +
" ]\n" +
"}\n"
// Write output
let url = URL(fileURLWithPath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("layout.sublime-completions")
guard FileManager.default.fileExists(atPath: url.path) else {
XCTFail("\(url) does not exist")
return
}
XCTAssertNoThrow(try output.write(to: url, atomically: true, encoding: .utf8))
}
}