├── .build ├── manifest.db └── workspace-state.json ├── Host App ├── ViewController.swift ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── README.md ├── SceneDelegate.swift ├── AppDelegate.swift ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.storyboard └── Info.plist ├── Sources └── Ruka │ ├── Ruka.h │ ├── UISwitch.swift │ ├── Animation.swift │ ├── UISlider.swift │ ├── UITextField.swift │ ├── UIView+SubviewFinder.swift │ ├── UITableViewCell.swift │ ├── UIStepper.swift │ ├── UIView+Identifiable.swift │ ├── Identifiable.swift │ ├── UIAlertController+ActionInvoker.swift │ ├── Info.plist │ └── App.swift ├── Ruka.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Ruka_Info.plist ├── RukaTests_Info.plist ├── xcshareddata │ └── xcschemes │ │ └── Ruka.xcscheme └── project.pbxproj ├── Support ├── TabBarViewController.swift ├── NestedModalViewController.swift ├── SecondTabViewController.swift ├── ModalViewController.swift ├── TableViewController.swift ├── Main.storyboard ├── RootViewController.swift └── FormViewController.swift ├── Package.swift ├── .gitignore ├── Tests └── RukaTests │ ├── Info.plist │ └── Tests.swift ├── LICENSE └── README.md /.build/manifest.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joemasilotti/Ruka/HEAD/.build/manifest.db -------------------------------------------------------------------------------- /.build/workspace-state.json: -------------------------------------------------------------------------------- 1 | {"object": {"artifacts": [], "dependencies": []}, "version": 4} -------------------------------------------------------------------------------- /Host App/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ViewController: UIViewController {} 4 | -------------------------------------------------------------------------------- /Host App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Host App/README.md: -------------------------------------------------------------------------------- 1 | # Host App 2 | 3 | You can safely ignore this target. It is only added so the tests can have a "Host Application" set in the build configuration. 4 | -------------------------------------------------------------------------------- /Sources/Ruka/Ruka.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | FOUNDATION_EXPORT double RukaVersionNumber; 4 | 5 | FOUNDATION_EXPORT const unsigned char RukaVersionString[]; 6 | -------------------------------------------------------------------------------- /Ruka.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /Sources/Ruka/UISwitch.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UISwitch { 4 | func toggle() { 5 | guard isEnabled else { return } 6 | 7 | sendActions(for: .valueChanged) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Host App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Ruka/Animation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Animation { 4 | enum Length: TimeInterval { 5 | case short = 0.05 6 | case dismissAlert = 0.25 7 | case popController = 0.4 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Ruka/UISlider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UISlider { 4 | func set(value: Float) { 5 | guard isEnabled else { return } 6 | 7 | setValue(value, animated: false) 8 | sendActions(for: .valueChanged) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Ruka.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/Ruka/UITextField.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UITextField { 4 | func type(text: String) { 5 | guard isEnabled else { return } 6 | 7 | self.text = text 8 | _ = delegate?.textField?(self, shouldChangeCharactersIn: NSMakeRange(0, text.count), replacementString: text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Host App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | var window: UIWindow? 5 | 6 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 7 | guard let _ = (scene as? UIWindowScene) else { return } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Ruka.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Ruka/UIView+SubviewFinder.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | func findViews(subclassOf: T.Type) -> [T] { 5 | return recursiveSubviews.compactMap { $0 as? T } 6 | } 7 | 8 | private var recursiveSubviews: [UIView] { 9 | return subviews + subviews.flatMap { $0.recursiveSubviews } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Ruka/UITableViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UITableViewCell { 4 | func tap() { 5 | guard 6 | let tableView = superview as? UITableView, 7 | let indexPath = tableView.indexPath(for: self) 8 | else { return } 9 | 10 | tableView.delegate?.tableView?(tableView, didSelectRowAt: indexPath) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Support/TabBarViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class TabBarViewController: UITabBarController { 4 | 5 | let secondTabViewController = SecondTabViewController() 6 | 7 | override func viewDidLoad() { 8 | super.viewDidLoad() 9 | 10 | viewControllers = [RootViewController(), UINavigationController(rootViewController: secondTabViewController)] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Ruka/UIStepper.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIStepper { 4 | func increment() { 5 | guard isEnabled else { return } 6 | 7 | value += stepValue 8 | sendActions(for: .valueChanged) 9 | } 10 | 11 | func decrement() { 12 | guard isEnabled else { return } 13 | 14 | value -= stepValue 15 | sendActions(for: .valueChanged) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Ruka/UIView+Identifiable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | func isIdentifiable(by identifier: String, in controller: UIViewController) -> Bool { 5 | let identifiable = 6 | (self as? Identifiable)?.isIdentifiable(by: identifier) ?? false || 7 | accessibilityLabel == identifier || 8 | accessibilityIdentifier == identifier 9 | 10 | return identifiable && !isHidden && frame.intersects(controller.view.bounds) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Ruka", 7 | products: [ 8 | .library( 9 | name: "Ruka", 10 | targets: ["Ruka"] 11 | ), 12 | ], 13 | dependencies: [], 14 | targets: [ 15 | .target( 16 | name: "Ruka", 17 | dependencies: [] 18 | ), 19 | .testTarget( 20 | name: "RukaTests", 21 | dependencies: ["Ruka"] 22 | ), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X temp files 2 | .DS_Store 3 | 4 | # Build temp files 5 | DerivedData/ 6 | Build/ 7 | .build/ 8 | 9 | # Xcode config settings 10 | *.pbxuser 11 | *.perspective 12 | *.mode1v3 13 | *.mode2v3 14 | 15 | # Whitelist default settings 16 | # http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects 17 | !default.pbxuser 18 | !default.mode1v3 19 | !default.mode2v3 20 | !default.perspectivev3 21 | 22 | # Xcode noise 23 | UserInterfaceState.xcuserstate 24 | Breakpoints.xcbkptlist 25 | IDEWorkspaceChecks.plist 26 | xcuserdata 27 | *.xccheckout 28 | *.xcscmblueprint 29 | -------------------------------------------------------------------------------- /Sources/Ruka/Identifiable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol Identifiable { 4 | func isIdentifiable(by identifier: String) -> Bool 5 | } 6 | 7 | extension UILabel: Identifiable { 8 | func isIdentifiable(by identifier: String) -> Bool { 9 | text == identifier 10 | } 11 | } 12 | 13 | extension UIButton: Identifiable { 14 | func isIdentifiable(by identifier: String) -> Bool { 15 | title(for: .normal) == identifier 16 | } 17 | } 18 | 19 | extension UITextField: Identifiable { 20 | func isIdentifiable(by identifier: String) -> Bool { 21 | placeholder == identifier 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Host App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 6 | return true 7 | } 8 | 9 | // MARK: UISceneSession Lifecycle 10 | 11 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 12 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Ruka/UIAlertController+ActionInvoker.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIAlertController { 4 | private typealias AlertHandler = @convention(block) (UIAlertAction) -> Void 5 | 6 | func tapButton(title: String) { 7 | let action = actions.first(where: { $0.title == title }) 8 | if let action = action, let block = action.value(forKey: "handler") { 9 | let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self) 10 | handler(action) 11 | 12 | dismiss(animated: false) 13 | let animationLength = Animation.Length.dismissAlert.rawValue 14 | RunLoop.current.run(until: Date().addingTimeInterval(animationLength)) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/RukaTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Ruka.xcodeproj/Ruka_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Ruka.xcodeproj/RukaTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sources/Ruka/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Support/NestedModalViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class NestedModalViewController: UIViewController { 4 | override func viewDidLoad() { 5 | super.viewDidLoad() 6 | 7 | let button = UIButton(type: .system) 8 | button.setTitle("Dismiss view controller", for: .normal) 9 | button.addTarget(self, action: #selector(dismissViewController), for: .touchUpInside) 10 | button.translatesAutoresizingMaskIntoConstraints = false 11 | 12 | view.addSubview(button) 13 | NSLayoutConstraint.activate([ 14 | button.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), 15 | button.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), 16 | ]) 17 | } 18 | 19 | @objc private func dismissViewController() { 20 | dismiss(animated: true) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Joe Masilotti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Support/SecondTabViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by dasdom on 16.12.20. 2 | // 3 | // 4 | 5 | import UIKit 6 | 7 | class SecondTabViewController: UIViewController { 8 | 9 | override func viewDidLoad() { 10 | super.viewDidLoad() 11 | 12 | view.backgroundColor = .white 13 | installStackView() 14 | } 15 | 16 | private func installStackView() { 17 | let label = UILabel() 18 | label.text = "Second tab" 19 | 20 | let button = UIButton(type: .system) 21 | button.setTitle("Present view controller from second tab", for: .normal) 22 | button.addTarget(self, action: #selector(presentViewController), for: .touchUpInside) 23 | 24 | let stackView = UIStackView(arrangedSubviews: [label, button]) 25 | stackView.translatesAutoresizingMaskIntoConstraints = false 26 | stackView.axis = .vertical 27 | 28 | view.addSubview(stackView) 29 | 30 | NSLayoutConstraint.activate([ 31 | stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 32 | stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor) 33 | ]) 34 | } 35 | 36 | @objc private func presentViewController() { 37 | present(ModalViewController(), animated: true) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Support/ModalViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ModalViewController: UIViewController { 4 | override func viewDidLoad() { 5 | super.viewDidLoad() 6 | 7 | let dismissButton = UIButton(type: .system) 8 | dismissButton.setTitle("Dismiss view controller", for: .normal) 9 | dismissButton.addTarget(self, action: #selector(dismissViewController), for: .touchUpInside) 10 | 11 | let presentModalButton = UIButton(type: .system) 12 | presentModalButton.setTitle("Present view controller", for: .normal) 13 | presentModalButton.addTarget(self, action: #selector(presentViewController), for: .touchUpInside) 14 | 15 | let stackView = UIStackView(arrangedSubviews: [dismissButton, presentModalButton]) 16 | stackView.translatesAutoresizingMaskIntoConstraints = false 17 | 18 | view.addSubview(stackView) 19 | NSLayoutConstraint.activate([ 20 | stackView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), 21 | stackView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), 22 | ]) 23 | } 24 | 25 | @objc private func dismissViewController() { 26 | dismiss(animated: true) 27 | } 28 | 29 | @objc private func presentViewController() { 30 | present(NestedModalViewController(), animated: true) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Host App/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Host App/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Host App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Host App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Support/TableViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class TableViewController: UIViewController { 4 | private let label = UILabel() 5 | private let tableView = UITableView() 6 | 7 | private let strings = [ 8 | "One", 9 | "Two", 10 | "Three", 11 | ] 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | view.backgroundColor = .white 16 | installLabel() 17 | installTableView() 18 | } 19 | 20 | private func installLabel() { 21 | label.text = "Label text" 22 | label.translatesAutoresizingMaskIntoConstraints = false 23 | view.addSubview(label) 24 | } 25 | 26 | private func installTableView() { 27 | tableView.register(TableViewCell.self, forCellReuseIdentifier: "CellIdentifier") 28 | tableView.dataSource = self 29 | tableView.delegate = self 30 | 31 | view.addSubview(tableView) 32 | tableView.translatesAutoresizingMaskIntoConstraints = false 33 | NSLayoutConstraint.activate([ 34 | label.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), 35 | label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 36 | 37 | tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), 38 | tableView.topAnchor.constraint(equalTo: label.bottomAnchor), 39 | tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), 40 | tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), 41 | ]) 42 | } 43 | } 44 | 45 | extension TableViewController: UITableViewDataSource { 46 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return strings.count 48 | } 49 | 50 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 51 | let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath) as! TableViewCell 52 | cell.label.text = strings[indexPath.row] 53 | return cell 54 | } 55 | } 56 | 57 | extension TableViewController: UITableViewDelegate { 58 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 59 | label.text = "Changed label text" 60 | } 61 | } 62 | 63 | private class TableViewCell: UITableViewCell { 64 | let label = UILabel() 65 | 66 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 67 | super.init(style: style, reuseIdentifier: reuseIdentifier) 68 | 69 | label.translatesAutoresizingMaskIntoConstraints = false 70 | addSubview(label) 71 | 72 | NSLayoutConstraint.activate([ 73 | label.bottomAnchor.constraint(equalTo: bottomAnchor), 74 | label.leadingAnchor.constraint(equalTo: leadingAnchor), 75 | label.topAnchor.constraint(equalTo: bottomAnchor), 76 | label.trailingAnchor.constraint(equalTo: trailingAnchor), 77 | ]) 78 | } 79 | 80 | required init?(coder: NSCoder) { 81 | fatalError() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Support/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Ruka.xcodeproj/xcshareddata/xcschemes/Ruka.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI tests without UI Testing experiment 2 | 3 | This repo is a small experiment to see if there's an "in-between" for testing iOS applications. More feature-level than XCTest but not as heavy handed (or slow and brittle) as UI Testing. 4 | 5 | The general idea is to provide an API similar to UI Testing but be able to spin up any controller on the fly. By putting the controller inside of a window the test behaves a bit more like the real app. 6 | 7 | This extends on my thoughts in a recent blog, [Testing the UI without UI Testing in Swift](https://masilotti.com/testing-ui-without-ui-testing/). 8 | 9 | ## Is this crazy? 10 | 11 | Probably! But maybe this is something worth exploring. 12 | 13 | Have you tried anything like this? Is this obviously a maintenance nightmare? I would love to hear what you think. 14 | 15 | ## Setup 16 | 17 | In your `XCTestCase` tests... 18 | 19 | ```swift 20 | continueAfterFailure = false 21 | 22 | // Code-powered views 23 | let controller = SomeViewController() 24 | let app = App(controller: controller) 25 | 26 | // Storyboard-powered views 27 | let controller = SomeViewController() 28 | let app = App(storyboard: "Main", identifier: "Some identifier") 29 | 30 | // ... 31 | ``` 32 | 33 | See the unit tests for more examples. 34 | 35 | ## API 36 | 37 | First, create a reference to an `app` instance with your controller, as shown above. 38 | 39 | The "top" view controller's view will be searched. For example, a controller pushed onto a navigation stack or presented modally. 40 | 41 | The finders ignore views that are disabled or not in the controller's view's frame. For example, a view rendered off the screen cannot be found. 42 | 43 | Interaction with elements is ignored if the element is disabled. 44 | 45 | ### Label 46 | 47 | `let label = try app.label()` - find a label via `text` or accessibility label/identifier 48 | 49 | ### Button 50 | 51 | `try app.button(title:)` - find a non-hidden button with the given `title`, recursively, in the view 52 | 53 | `app.tapButton(title:)` - triggers the target-action for the non-hidden button with the given `title`, if not disabled 54 | 55 | ### Switch 56 | 57 | `let aSwitch = try app.switch()` - find a switch via accessibility label/identifier 58 | 59 | `aSwitch?.toggle()` - triggers the value changed action on the switch 60 | 61 | ### Table cell 62 | 63 | `let cell = try app.cell(containingText:)` - find the first `UITableViewCell` (or subclass) containing a label matching the text 64 | 65 | `cell?.tap()` - taps the cell via its index path and delegate 66 | 67 | ### Stepper 68 | 69 | `let stepper = try app.stepper()` - find a stepper via accessibility label/identifier 70 | 71 | `stepper?.increment()` - increments the stepper by the step value and triggers the value changed action 72 | 73 | `stepper?.decrement()` - decrements the stepper by the step value and triggers the value changed action 74 | 75 | ### Slider 76 | 77 | `let slider = try app.slider()` - find a slider via accessibility label/identifier 78 | 79 | `slider?.set(value:)` - sets the slider to the value and triggers the value changed action 80 | 81 | ### Text fields 82 | 83 | `let textField = try app.textField()` - find a text field via `placeholder` or accessibility label/identifier 84 | 85 | `textField?.type(text:)` - sets the text field's value and calls `textField(_:, shouldChangeCharactersIn:, replacementString:)` on the delegate 86 | 87 | ### Alerts 88 | 89 | `app.alertViewController?.tapButton(title:)` - triggers the attached action and dismisses the alert 90 | 91 | ## To-do 92 | 93 | 1. Gesture - swiping and scrolling 94 | 1. Collection view 95 | 1. Map view 96 | 1. ... 97 | 98 | ## Out of scope (for now) 99 | 100 | 1. System alert - this probably isn't be possible 101 | 1. SwiftUI - [ViewInspector](https://github.com/nalexn/ViewInspector) is probably a better choice 102 | -------------------------------------------------------------------------------- /Support/RootViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class RootViewController: UIViewController { 4 | private let label = UILabel() 5 | private let stackView = UIStackView() 6 | 7 | override func viewDidLoad() { 8 | super.viewDidLoad() 9 | view.backgroundColor = .white 10 | installStackView() 11 | installSubviews() 12 | } 13 | 14 | private func installStackView() { 15 | stackView.axis = .vertical 16 | stackView.alignment = .center 17 | stackView.translatesAutoresizingMaskIntoConstraints = false 18 | view.addSubview(stackView) 19 | 20 | NSLayoutConstraint.activate([ 21 | view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), 22 | view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), 23 | view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: stackView.topAnchor), 24 | ]) 25 | } 26 | 27 | private func installSubviews() { 28 | installLabels() 29 | installButtons() 30 | } 31 | 32 | private func installLabels() { 33 | label.text = "Label text" 34 | stackView.addArrangedSubview(label) 35 | _ = addLabel(text: "Hidden label text", isHidden: true) 36 | 37 | let a11yLabeledLabel = addLabel(text: "") 38 | a11yLabeledLabel.accessibilityLabel = "a11y labeled label" 39 | 40 | let a11yIdentifiedLabel = addLabel(text: "") 41 | a11yIdentifiedLabel.accessibilityIdentifier = "a11y identified label" 42 | 43 | let offScreenLabel = UILabel() 44 | offScreenLabel.text = "Off screen label text" 45 | view.addSubview(offScreenLabel) 46 | offScreenLabel.frame.origin.y = -100 47 | } 48 | 49 | private func installButtons() { 50 | _ = addButton(title: "Button title") 51 | _ = addButton(title: "Hidden button title", isHidden: true) 52 | _ = addButton(title: "Disabled button title", isEnabled: false) 53 | 54 | _ = addButton(title: "Push view controller", action: #selector(pushViewController)) 55 | _ = addButton(title: "Pop view controller", action: #selector(popViewController)) 56 | _ = addButton(title: "Present view controller", action: #selector(presentViewController)) 57 | 58 | _ = addButton(title: "Show alert", action: #selector(showAlert)) 59 | 60 | let a11yLabeledButton = addButton(title: "") 61 | a11yLabeledButton.accessibilityLabel = "a11y labeled button" 62 | 63 | let a11yIdentifiedButton = addButton(title: "") 64 | a11yIdentifiedButton.accessibilityIdentifier = "a11y identified button" 65 | 66 | let offScreenButton = UIButton() 67 | offScreenButton.setTitle("Off screen button title", for: .normal) 68 | view.addSubview(offScreenButton) 69 | offScreenButton.frame.origin.y = -100 70 | } 71 | 72 | private func addLabel(text: String, isHidden: Bool = false) -> UILabel { 73 | let label = UILabel() 74 | label.text = text 75 | label.isHidden = isHidden 76 | stackView.addArrangedSubview(label) 77 | return label 78 | } 79 | 80 | private func addButton(title: String, isHidden: Bool = false, isEnabled: Bool = true, action: Selector = #selector(changeLabelText)) -> UIButton { 81 | let button = UIButton(type: .system) 82 | button.isHidden = isHidden 83 | button.isEnabled = isEnabled 84 | button.setTitle(title, for: .normal) 85 | button.addTarget(self, action: action, for: .touchUpInside) 86 | stackView.addArrangedSubview(button) 87 | return button 88 | } 89 | 90 | @objc private func changeLabelText() { 91 | label.text = "Changed label text" 92 | } 93 | 94 | @objc private func pushViewController() { 95 | navigationController?.pushViewController(RootViewController(), animated: true) 96 | } 97 | 98 | @objc private func popViewController() { 99 | navigationController?.popViewController(animated: true) 100 | } 101 | 102 | @objc private func presentViewController() { 103 | present(ModalViewController(), animated: true) 104 | } 105 | 106 | @objc private func showAlert() { 107 | let alert = UIAlertController(title: "Alert title", message: "Alert message.", preferredStyle: .alert) 108 | alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: { [weak self] _ in 109 | self?.label.text = "Changed label text" 110 | })) 111 | present(alert, animated: true) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/Ruka/App.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | public struct App { 4 | public enum FailureBehavior { 5 | case failTest 6 | case raiseException 7 | case doNothing 8 | } 9 | 10 | public init(controller: UIViewController, failureBehavior: FailureBehavior = .failTest) { 11 | self.failureBehavior = failureBehavior 12 | 13 | load(controller: controller) 14 | } 15 | 16 | public init(storyboard: String, identifier: String, failureBehavior: FailureBehavior = .failTest) { 17 | self.failureBehavior = failureBehavior 18 | 19 | let storyboard = UIStoryboard(name: storyboard, bundle: nil) 20 | let controller = storyboard.instantiateViewController(withIdentifier: identifier) 21 | load(controller: controller) 22 | } 23 | 24 | // MARK: UILabel 25 | 26 | public func label(_ identifier: String, file: StaticString = #filePath, line: UInt = #line) throws -> UILabel? { 27 | let labels = controller.view.findViews(subclassOf: UILabel.self) 28 | let label = labels.first(where: { $0.isIdentifiable(by: identifier, in: controller) }) 29 | 30 | if label == nil, failureBehavior != .doNothing { 31 | try failOrRaise("Could not find label with text '\(identifier)'.", file: file, line: line) 32 | } 33 | return label 34 | } 35 | 36 | // MARK: UIButton 37 | 38 | public func button(_ identifier: String, file: StaticString = #filePath, line: UInt = #line) throws -> UIButton? { 39 | let buttons = controller.view.findViews(subclassOf: UIButton.self) 40 | let button = buttons.first(where: { $0.isIdentifiable(by: identifier, in: controller) }) 41 | 42 | if button == nil, failureBehavior != .doNothing { 43 | try failOrRaise("Could not find button with text '\(identifier)'.", file: file, line: line) 44 | } 45 | return button 46 | } 47 | 48 | public func tapButton(title: String, file: StaticString = #filePath, line: UInt = #line) throws { 49 | guard let button = try button(title), button.isEnabled else { return } 50 | 51 | let windowBeforeTap = window 52 | button.sendActions(for: .touchUpInside) 53 | 54 | // Controller containing button is being popped off of navigation stack, wait for animation. 55 | let timeInterval: Animation.Length = windowBeforeTap != button.window ? .popController : .short 56 | RunLoop.main.run(until: Date().addingTimeInterval(timeInterval.rawValue)) 57 | } 58 | 59 | // MARK: UITableView 60 | 61 | public var tableView: UITableView? { 62 | controller.view.findViews(subclassOf: UITableView.self).first 63 | } 64 | 65 | public func cell(containingText text: String, file: StaticString = #filePath, line: UInt = #line) throws -> UITableViewCell? { 66 | let tableViewCell = tableView?.visibleCells.first(where: { cell -> Bool in 67 | cell.findViews(subclassOf: UILabel.self).contains { $0.text == text } 68 | }) 69 | 70 | if tableViewCell == nil, failureBehavior != .doNothing { 71 | try failOrRaise("Could not find cell containing text '\(text)'.", file: file, line: line) 72 | } 73 | return tableViewCell 74 | } 75 | 76 | // MARK: UISwitch 77 | 78 | public func `switch`(_ identifier: String, file: StaticString = #filePath, line: UInt = #line) throws -> UISwitch? { 79 | let switches = controller.view.findViews(subclassOf: UISwitch.self) 80 | let `switch` = switches.first(where: { $0.isIdentifiable(by: identifier, in: controller) }) 81 | 82 | if `switch` == nil, failureBehavior != .doNothing { 83 | try failOrRaise("Could not find switch with accessibility label '\(identifier)'.", file: file, line: line) 84 | } 85 | return `switch` 86 | } 87 | 88 | // MARK: UIStepper 89 | 90 | public func stepper(_ identifier: String, file: StaticString = #filePath, line: UInt = #line) throws -> UIStepper? { 91 | let steppers = controller.view.findViews(subclassOf: UIStepper.self) 92 | let stepper = steppers.first(where: { $0.isIdentifiable(by: identifier, in: controller) }) 93 | 94 | if stepper == nil, failureBehavior != .doNothing { 95 | try failOrRaise("Could not find stepper with accessibility label '\(identifier)'.", file: file, line: line) 96 | } 97 | return stepper 98 | } 99 | 100 | // MARK: UISlider 101 | 102 | public func slider(_ identifier: String, file: StaticString = #filePath, line: UInt = #line) throws -> UISlider? { 103 | let sliders = controller.view.findViews(subclassOf: UISlider.self) 104 | let slider = sliders.first(where: { $0.isIdentifiable(by: identifier, in: controller) }) 105 | 106 | if slider == nil, failureBehavior != .doNothing { 107 | try failOrRaise("Could not find slider with accessibility label '\(identifier)'.", file: file, line: line) 108 | } 109 | return slider 110 | } 111 | 112 | // MARK: UITextField 113 | 114 | public func textField(_ identifier: String, file: StaticString = #filePath, line: UInt = #line) throws -> UITextField? { 115 | let textFields = controller.view.findViews(subclassOf: UITextField.self) 116 | let textField = textFields.first(where: { $0.isIdentifiable(by: identifier, in: controller) }) 117 | 118 | if textField == nil, failureBehavior != .doNothing { 119 | try failOrRaise("Could not find text field with placeholder '\(identifier)'.", file: file, line: line) 120 | } 121 | return textField 122 | } 123 | 124 | // MARK: UIAlertController 125 | 126 | public var alertViewController: UIAlertController? { 127 | controller as? UIAlertController 128 | } 129 | 130 | // MARK: Private 131 | 132 | private enum RukaError: Error { 133 | case unfoundElement 134 | } 135 | 136 | private let failureBehavior: FailureBehavior 137 | private let window = UIWindow() 138 | private var controller: UIViewController! { visibleViewController(from: window.rootViewController) } 139 | 140 | private func load(controller: UIViewController) { 141 | window.rootViewController = controller 142 | window.makeKeyAndVisible() 143 | controller.loadViewIfNeeded() 144 | controller.view.layoutIfNeeded() 145 | } 146 | 147 | private func visibleViewController(from viewController: UIViewController?) -> UIViewController? { 148 | if let navigationController = viewController as? UINavigationController { 149 | return visibleViewController(from: navigationController.topViewController) 150 | } 151 | 152 | if let tabBarController = viewController as? UITabBarController { 153 | return visibleViewController(from: tabBarController.selectedViewController) 154 | } 155 | 156 | if let presentedViewController = viewController?.presentedViewController { 157 | return visibleViewController(from: presentedViewController) 158 | } 159 | 160 | return viewController 161 | } 162 | 163 | private func viewIsVisibleInController(_ view: UIView) -> Bool { 164 | view.frame.intersects(controller.view.bounds) 165 | } 166 | 167 | private func failOrRaise(_ message: String, file: StaticString, line: UInt) throws { 168 | switch failureBehavior { 169 | case .failTest: 170 | XCTFail(message, file: file, line: line) 171 | case .raiseException: 172 | throw RukaError.unfoundElement 173 | case .doNothing: 174 | break 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Support/FormViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class FormViewController: UIViewController { 4 | private let stackView = UIStackView() 5 | private let switchlabel = UILabel() 6 | private let stepperLabel = UILabel() 7 | private let sliderLabel = UILabel() 8 | private let textFieldLabel = UILabel() 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | view.backgroundColor = .white 13 | installStackView() 14 | installSubviews() 15 | } 16 | 17 | private func installStackView() { 18 | stackView.axis = .vertical 19 | stackView.alignment = .center 20 | stackView.translatesAutoresizingMaskIntoConstraints = false 21 | view.addSubview(stackView) 22 | 23 | NSLayoutConstraint.activate([ 24 | view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), 25 | view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), 26 | view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: stackView.topAnchor), 27 | ]) 28 | } 29 | 30 | private func installSubviews() { 31 | addSwitches() 32 | addSteppers() 33 | addSliders() 34 | addTextFields() 35 | } 36 | 37 | private func addSwitches() { 38 | _ = addSwitch(accessibilityLabel: "A switch") 39 | _ = addSwitch(accessibilityLabel: "A hidden switch", isHidden: true) 40 | _ = addSwitch(accessibilityLabel: "A disabled switch", isEnabled: false) 41 | 42 | let a11yLabeledSwitch = addSwitch(accessibilityLabel: "") 43 | a11yLabeledSwitch.accessibilityIdentifier = "a11y labeled switch" 44 | 45 | let a11yIdentifiedSwitch = addSwitch(accessibilityLabel: "") 46 | a11yIdentifiedSwitch.accessibilityIdentifier = "a11y identified switch" 47 | 48 | let offScreenSwitch = UISwitch() 49 | offScreenSwitch.accessibilityLabel = "An off screen switch" 50 | offScreenSwitch.addTarget(self, action: #selector(toggleSwitch), for: .valueChanged) 51 | view.addSubview(offScreenSwitch) 52 | offScreenSwitch.frame.origin.y = -100 53 | 54 | switchlabel.text = "Disabled" 55 | stackView.addArrangedSubview(switchlabel) 56 | } 57 | 58 | private func addSteppers() { 59 | _ = addStepper(accessibilityLabel: "A stepper") 60 | _ = addStepper(accessibilityLabel: "A hidden stepper", isHidden: true) 61 | _ = addStepper(accessibilityLabel: "A disabled stepper", isEnabled: false) 62 | 63 | let a11yLabeledStepper = addStepper(accessibilityLabel: "") 64 | a11yLabeledStepper.accessibilityLabel = "a11y labeled stepper" 65 | 66 | let a11yIdentifiedStepper = addStepper(accessibilityLabel: "") 67 | a11yIdentifiedStepper.accessibilityIdentifier = "a11y identified stepper" 68 | 69 | let offScreenStepper = UIStepper() 70 | offScreenStepper.accessibilityLabel = "An off screen stepper" 71 | offScreenStepper.value = 2 72 | offScreenStepper.addTarget(self, action: #selector(changeStepper), for: .valueChanged) 73 | view.addSubview(offScreenStepper) 74 | offScreenStepper.frame.origin.y = -100 75 | 76 | stepperLabel.text = "2.0" 77 | stackView.addArrangedSubview(stepperLabel) 78 | } 79 | 80 | private func addSliders() { 81 | _ = addSlider(accessibilityLabel: "A slider") 82 | _ = addSlider(accessibilityLabel: "A hidden slider", isHidden: true) 83 | _ = addSlider(accessibilityLabel: "A disabled slider", isEnabled: false) 84 | 85 | let a11yLabeledSlider = addSlider(accessibilityLabel: "") 86 | a11yLabeledSlider.accessibilityLabel = "a11y labeled slider" 87 | 88 | let a11yIdentifiedSlider = addSlider(accessibilityLabel: "") 89 | a11yIdentifiedSlider.accessibilityIdentifier = "a11y identified slider" 90 | 91 | let offScreenSlider = UISlider() 92 | offScreenSlider.value = 2 93 | offScreenSlider.maximumValue = 4 94 | offScreenSlider.accessibilityLabel = "An off screen slider" 95 | offScreenSlider.addTarget(self, action: #selector(changeSlider), for: .valueChanged) 96 | view.addSubview(offScreenSlider) 97 | offScreenSlider.frame.origin.y = -100 98 | 99 | sliderLabel.text = "20.0" 100 | stackView.addArrangedSubview(sliderLabel) 101 | } 102 | 103 | private func addTextFields() { 104 | _ = addTextField(placeholder: "Text field placeholder") 105 | _ = addTextField(placeholder: "Hidden text field placeholder", isHidden: true) 106 | _ = addTextField(placeholder: "Disabled text field placeholder", isEnabled: false) 107 | 108 | let a11yLabeledTextField = addTextField(placeholder: "") 109 | a11yLabeledTextField.accessibilityLabel = "a11y labeled text field" 110 | 111 | let a11yIdentifiedTextField = addTextField(placeholder: "") 112 | a11yIdentifiedTextField.accessibilityIdentifier = "a11y identified text field" 113 | 114 | let offScreenTextField = UITextField() 115 | offScreenTextField.placeholder = "Off screen text field placeholder" 116 | offScreenTextField.delegate = self 117 | view.addSubview(offScreenTextField) 118 | offScreenTextField.frame.origin.y = -100 119 | 120 | stackView.addArrangedSubview(textFieldLabel) 121 | } 122 | 123 | private func addSwitch(accessibilityLabel: String, isHidden: Bool = false, isEnabled: Bool = true) -> UISwitch { 124 | let `switch` = UISwitch() 125 | `switch`.isHidden = isHidden 126 | `switch`.isEnabled = isEnabled 127 | `switch`.accessibilityLabel = accessibilityLabel 128 | `switch`.addTarget(self, action: #selector(toggleSwitch), for: .valueChanged) 129 | stackView.addArrangedSubview(`switch`) 130 | return `switch` 131 | } 132 | 133 | private func addStepper(accessibilityLabel: String, isHidden: Bool = false, isEnabled: Bool = true) -> UIStepper { 134 | let stepper = UIStepper() 135 | stepper.value = 2 136 | stepper.isHidden = isHidden 137 | stepper.isEnabled = isEnabled 138 | stepper.accessibilityLabel = accessibilityLabel 139 | stepper.addTarget(self, action: #selector(changeStepper), for: .valueChanged) 140 | stackView.addArrangedSubview(stepper) 141 | return stepper 142 | } 143 | 144 | private func addSlider(accessibilityLabel: String, isHidden: Bool = false, isEnabled: Bool = true) -> UISlider { 145 | let slider = UISlider() 146 | slider.value = 20 147 | slider.maximumValue = 40 148 | slider.isHidden = isHidden 149 | slider.isEnabled = isEnabled 150 | slider.accessibilityLabel = accessibilityLabel 151 | slider.addTarget(self, action: #selector(changeSlider), for: .valueChanged) 152 | stackView.addArrangedSubview(slider) 153 | return slider 154 | } 155 | 156 | private func addTextField(placeholder: String, isHidden: Bool = false, isEnabled: Bool = true) -> UITextField { 157 | let textField = UITextField() 158 | textField.placeholder = placeholder 159 | textField.isHidden = isHidden 160 | textField.isEnabled = isEnabled 161 | textField.delegate = self 162 | stackView.addArrangedSubview(textField) 163 | return textField 164 | } 165 | 166 | @objc private func toggleSwitch(switch: UISwitch) { 167 | switchlabel.text = `switch`.isOn ? "Disabled" : "Enabled" 168 | } 169 | 170 | @objc private func changeStepper(stepper: UIStepper) { 171 | stepperLabel.text = "\(stepper.value)" 172 | } 173 | 174 | @objc private func changeSlider(slider: UISlider) { 175 | sliderLabel.text = "\(slider.value)" 176 | } 177 | } 178 | 179 | extension FormViewController: UITextFieldDelegate { 180 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 181 | textFieldLabel.text = textField.text 182 | return true 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Tests/RukaTests/Tests.swift: -------------------------------------------------------------------------------- 1 | import Ruka 2 | import XCTest 3 | 4 | class Tests: XCTestCase { 5 | override func setUpWithError() throws { 6 | continueAfterFailure = false 7 | } 8 | 9 | // MARK: Storyboard 10 | 11 | func test_findsAStoryboardBackedController() throws { 12 | let app = App(storyboard: "Main", identifier: "UIViewController identifier") 13 | XCTAssertNotNil(try app.label("Storyboard label text")) 14 | } 15 | 16 | // MARK: UILabel 17 | 18 | func test_findsALabel() throws { 19 | let controller = RootViewController() 20 | let app = App(controller: controller) 21 | 22 | let label = try app.label("Label text") 23 | XCTAssertNotNil(label) 24 | XCTAssertEqual(label?.superview?.superview, controller.view) 25 | } 26 | 27 | func test_findsALabelViaTheAccessibilityLabel() throws { 28 | let app = App(controller: RootViewController()) 29 | XCTAssertNotNil(try app.label("a11y labeled label")) 30 | } 31 | 32 | func test_findsALabelViaTheAccessibilityIdentifier() throws { 33 | let app = App(controller: RootViewController()) 34 | XCTAssertNotNil(try app.label("a11y identified label")) 35 | } 36 | 37 | func test_doesNotFindAHiddenLabel() throws { 38 | let app = App(controller: RootViewController(), failureBehavior: .doNothing) 39 | XCTAssertNil(try app.label("Hidden label text")) 40 | } 41 | 42 | func test_doesNotFindALabelOffTheScreen() throws { 43 | let app = App(controller: RootViewController(), failureBehavior: .doNothing) 44 | XCTAssertNil(try app.label("Off screen label text")) 45 | } 46 | 47 | // MARK: UIButton 48 | 49 | func test_findsAButton() throws { 50 | let controller = RootViewController() 51 | let app = App(controller: controller) 52 | 53 | let button = try app.button("Button title") 54 | XCTAssertNotNil(button) 55 | XCTAssertEqual(button?.superview?.superview, controller.view) 56 | } 57 | 58 | func test_findsAButtonViaTheAccessibilityLabel() throws { 59 | let app = App(controller: RootViewController()) 60 | XCTAssertNotNil(try app.button("a11y labeled button")) 61 | } 62 | 63 | func test_findsAButtonViaTheAccessibilityIdentifier() throws { 64 | let app = App(controller: RootViewController()) 65 | XCTAssertNotNil(try app.button("a11y identified button")) 66 | } 67 | 68 | func test_doesNotFindAHiddenButton() throws { 69 | let controller = RootViewController() 70 | let app = App(controller: controller, failureBehavior: .doNothing) 71 | 72 | XCTAssertNil(try app.button("Hidden button title")) 73 | } 74 | 75 | func test_doesNotFindAButtonOffTheScreen() throws { 76 | let app = App(controller: RootViewController(), failureBehavior: .doNothing) 77 | XCTAssertNil(try app.button("Off screen button title")) 78 | } 79 | 80 | func test_tapsAButton() throws { 81 | let controller = RootViewController() 82 | let app = App(controller: controller) 83 | 84 | try app.tapButton(title: "Button title") 85 | 86 | _ = try app.label("Changed label text") 87 | } 88 | 89 | func test_doesNotTapADisabledButton() throws { 90 | let controller = RootViewController() 91 | let app = App(controller: controller, failureBehavior: .doNothing) 92 | 93 | try app.tapButton(title: "Disabled button title") 94 | 95 | XCTAssertNil(try app.label("Changed label text")) 96 | } 97 | 98 | // MARK: UINavigationController 99 | 100 | func test_pushesAViewController() throws { 101 | let navigationController = UINavigationController(rootViewController: RootViewController()) 102 | let app = App(controller: navigationController) 103 | 104 | try app.tapButton(title: "Push view controller") 105 | 106 | XCTAssertEqual(navigationController.viewControllers.count, 2) 107 | } 108 | 109 | func test_popsAViewController() throws { 110 | let navigationController = UINavigationController(rootViewController: RootViewController()) 111 | let app = App(controller: navigationController) 112 | 113 | try app.tapButton(title: "Push view controller") 114 | XCTAssertEqual(navigationController.viewControllers.count, 2) 115 | 116 | try app.tapButton(title: "Pop view controller") 117 | XCTAssertEqual(navigationController.viewControllers.count, 1) 118 | } 119 | 120 | // MARK: Modal view controllers 121 | 122 | func test_presentsAViewController() throws { 123 | let controller = RootViewController() 124 | let app = App(controller: controller) 125 | 126 | try app.tapButton(title: "Present view controller") 127 | 128 | XCTAssertNotNil(controller.presentedViewController) 129 | } 130 | 131 | func test_dismissesAViewController() throws { 132 | let controller = RootViewController() 133 | let app = App(controller: controller, failureBehavior: .doNothing) 134 | XCTAssertNil(try app.button("Dismiss view controller")) 135 | 136 | try app.tapButton(title: "Present view controller") 137 | XCTAssertNotNil(try app.button("Dismiss view controller")) 138 | 139 | try app.tapButton(title: "Dismiss view controller") 140 | XCTAssertNil(try app.button("Dismiss view controller")) 141 | } 142 | 143 | func test_dismissNestedModalViewController() throws { 144 | let controller = RootViewController() 145 | let app = App(controller: controller, failureBehavior: .doNothing) 146 | XCTAssertNil(try app.button("Dismiss view controller")) 147 | 148 | try app.tapButton(title: "Present view controller") 149 | XCTAssertNotNil(try app.button("Dismiss view controller")) 150 | 151 | try app.tapButton(title: "Present view controller") 152 | XCTAssertNotNil(try app.button("Dismiss view controller")) 153 | 154 | try app.tapButton(title: "Dismiss view controller") 155 | XCTAssertNotNil(try app.button("Dismiss view controller")) 156 | 157 | try app.tapButton(title: "Dismiss view controller") 158 | XCTAssertNil(try app.button("Dismiss view controller")) 159 | } 160 | 161 | // MARK: UITabBarController 162 | 163 | func test_presentsAViewControllerOnSecondTabInTabBarController() throws { 164 | let tabBarController = TabBarViewController() 165 | let app = App(controller: tabBarController, failureBehavior: .doNothing) 166 | XCTAssertNil(try app.button("Present view controller from second tab")) 167 | 168 | tabBarController.selectedIndex = 1 169 | XCTAssertNotNil(try app.button("Present view controller from second tab")) 170 | try app.tapButton(title: "Present view controller from second tab") 171 | 172 | XCTAssertNotNil(tabBarController.secondTabViewController.presentedViewController) 173 | } 174 | 175 | // MARK: UIAlertController 176 | 177 | func test_findsAnAlert() throws { 178 | let controller = RootViewController() 179 | let app = App(controller: controller) 180 | 181 | try app.tapButton(title: "Show alert") 182 | XCTAssertNotNil(try app.label("Alert title")) 183 | XCTAssertNotNil(try app.label("Alert message.")) 184 | } 185 | 186 | func test_dismissesAnAlert() throws { 187 | let controller = RootViewController() 188 | let app = App(controller: controller, failureBehavior: .doNothing) 189 | 190 | try app.tapButton(title: "Show alert") 191 | XCTAssertNil(try app.button("Show alert")) 192 | 193 | app.alertViewController?.tapButton(title: "Dismiss") 194 | XCTAssertNotNil(try app.button("Show alert")) 195 | XCTAssertNotNil(try app.label("Changed label text")) 196 | } 197 | 198 | // MARK: UITableView 199 | 200 | func test_findsVisibleCells() throws { 201 | let app = App(controller: TableViewController()) 202 | XCTAssertEqual(app.tableView?.visibleCells.count ?? 0, 3) 203 | } 204 | 205 | func test_findsASpecificCell() throws { 206 | let app = App(controller: TableViewController(), failureBehavior: .doNothing) 207 | XCTAssertNotNil(try app.cell(containingText: "Three")) 208 | 209 | XCTAssertNotNil(try app.label("Label text")) 210 | XCTAssertNil(try app.cell(containingText: "Label text")) 211 | } 212 | 213 | func test_tapsACell() throws { 214 | let app = App(controller: TableViewController()) 215 | try app.cell(containingText: "Three")?.tap() 216 | XCTAssertNotNil(try app.label("Changed label text")) 217 | } 218 | 219 | // MARK: UISwitch 220 | 221 | func test_findsASwitchViaTheAccessibilityLabel() throws { 222 | let app = App(controller: FormViewController()) 223 | XCTAssertNotNil(try app.switch("a11y labeled switch")) 224 | } 225 | 226 | func test_findsASwitchViaTheAccessibilityIdentifier() throws { 227 | let app = App(controller: FormViewController()) 228 | XCTAssertNotNil(try app.switch("a11y identified switch")) 229 | } 230 | 231 | func test_doesNotFindAHiddenSwitch() throws { 232 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 233 | XCTAssertNil(try app.switch("A hidden switch")) 234 | } 235 | 236 | func test_doesNotFindASwitchOffTheScreen() throws { 237 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 238 | XCTAssertNil(try app.switch("An off screen switch")) 239 | } 240 | 241 | func test_togglesASwitch() throws { 242 | let app = App(controller: FormViewController()) 243 | let `switch` = try app.switch("A switch") 244 | XCTAssertNotNil(try app.label("Disabled")) 245 | 246 | `switch`?.toggle() 247 | XCTAssertNotNil(try app.label("Enabled")) 248 | } 249 | 250 | func test_doesNotToggleADisabledSwitch() throws { 251 | let app = App(controller: FormViewController()) 252 | let `switch` = try app.switch("A disabled switch") 253 | XCTAssertNotNil(try app.label("Disabled")) 254 | 255 | `switch`?.toggle() 256 | XCTAssertNotNil(try app.label("Disabled")) 257 | } 258 | 259 | // MARK: UIStepper 260 | 261 | func test_findsAStepperViaTheAccessibilityLabel() throws { 262 | let app = App(controller: FormViewController()) 263 | XCTAssertNotNil(try app.stepper("a11y labeled stepper")) 264 | } 265 | 266 | func test_findsAStepperViaTheAccessibilityIdentifier() throws { 267 | let app = App(controller: FormViewController()) 268 | XCTAssertNotNil(try app.stepper("a11y identified stepper")) 269 | } 270 | 271 | func test_doesNotFindAHiddenStepper() throws { 272 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 273 | XCTAssertNil(try app.stepper("A hidden stepper")) 274 | } 275 | 276 | func test_doesNotFindAStepperOffTheScreen() throws { 277 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 278 | XCTAssertNil(try app.stepper("An off screen stepper")) 279 | } 280 | 281 | func test_incrementsAStepper() throws { 282 | let app = App(controller: FormViewController()) 283 | try app.stepper("A stepper")?.increment() 284 | XCTAssertNotNil(try app.label("3.0")) 285 | } 286 | 287 | func test_decrementsAStepper() throws { 288 | let app = App(controller: FormViewController()) 289 | try app.stepper("A stepper")?.decrement() 290 | XCTAssertNotNil(try app.label("1.0")) 291 | } 292 | 293 | func test_doesNotIncrementADisabledStepper() throws { 294 | let app = App(controller: FormViewController()) 295 | try app.stepper("A disabled stepper")?.increment() 296 | XCTAssertNotNil(try app.label("2.0")) 297 | } 298 | 299 | func test_doesNotDecrementADisabledStepper() throws { 300 | let app = App(controller: FormViewController()) 301 | try app.stepper("A disabled stepper")?.decrement() 302 | XCTAssertNotNil(try app.label("2.0")) 303 | } 304 | 305 | // MARK: UISlider 306 | 307 | func test_findsASliderViaTheAccessibilityLabel() throws { 308 | let app = App(controller: FormViewController()) 309 | XCTAssertNotNil(try app.slider("a11y labeled slider")) 310 | } 311 | 312 | func test_findsASliderViaTheAccessibilityIdentifier() throws { 313 | let app = App(controller: FormViewController()) 314 | XCTAssertNotNil(try app.slider("a11y identified slider")) 315 | } 316 | 317 | func test_doesNotFindAHiddenSlider() throws { 318 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 319 | XCTAssertNil(try app.slider("A hidden slider")) 320 | } 321 | 322 | func test_doesNotFindASliderOffTheScreen() throws { 323 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 324 | XCTAssertNil(try app.slider("An off screen slider")) 325 | } 326 | 327 | func test_setsASlidersValue() throws { 328 | let app = App(controller: FormViewController()) 329 | try app.slider("A slider")?.set(value: 30) 330 | XCTAssertNotNil(try app.label("30.0")) 331 | } 332 | 333 | func test_doesNotSetADisabledSlidersValue() throws { 334 | let app = App(controller: FormViewController()) 335 | try app.slider("A disabled slider")?.set(value: 30) 336 | XCTAssertNotNil(try app.label("20.0")) 337 | } 338 | 339 | // MARK: UITextField 340 | 341 | func test_findsATextFieldViaThePlaceholder() throws { 342 | let app = App(controller: FormViewController()) 343 | XCTAssertNotNil(try app.textField("Text field placeholder")) 344 | } 345 | 346 | func test_findsATextFieldViaTheAccessibilityLabel() throws { 347 | let app = App(controller: FormViewController()) 348 | XCTAssertNotNil(try app.textField("a11y labeled text field")) 349 | } 350 | 351 | func test_findsATextFieldViaTheAccessibilityIdentifier() throws { 352 | let app = App(controller: FormViewController()) 353 | XCTAssertNotNil(try app.textField("a11y identified text field")) 354 | } 355 | 356 | func test_doesNotFindAHiddenTextField() throws { 357 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 358 | XCTAssertNil(try app.textField("Hidden text field placeholder")) 359 | } 360 | 361 | func test_doesNotFindATextFieldOffTheScreen() throws { 362 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 363 | XCTAssertNil(try app.textField("Off screen text field placeholder")) 364 | } 365 | 366 | func test_typesIntoATextField() throws { 367 | let app = App(controller: FormViewController()) 368 | let textField = try app.textField("Text field placeholder") 369 | 370 | textField?.type(text: "Some typed text.") 371 | XCTAssertEqual(textField?.text, "Some typed text.") 372 | XCTAssertNotNil(try app.label("Some typed text.")) 373 | } 374 | 375 | func test_doesNotTypeIntoADisabledTextField() throws { 376 | let app = App(controller: FormViewController(), failureBehavior: .doNothing) 377 | let textField = try app.textField("Disabled text field placeholder") 378 | 379 | textField?.type(text: "Some typed text.") 380 | XCTAssertEqual(textField?.text, "") 381 | XCTAssertNil(try app.label("Some typed text.")) 382 | } 383 | 384 | // MARK: Failure behavior 385 | 386 | func test_aMissingElement_raisesAnError() throws { 387 | let app = App(controller: RootViewController(), failureBehavior: .raiseException) 388 | XCTAssertThrowsError(try app.label("Missing element")) 389 | } 390 | 391 | func test_aMissingElement_isNil() throws { 392 | let app = App(controller: RootViewController(), failureBehavior: .doNothing) 393 | XCTAssertNil(try app.label("Missing element")) 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /Ruka.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | "Ruka::RukaPackageTests::ProductTarget" /* RukaPackageTests */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = OBJ_39 /* Build configuration list for PBXAggregateTarget "RukaPackageTests" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | OBJ_42 /* PBXTargetDependency */, 17 | ); 18 | name = RukaPackageTests; 19 | productName = RukaPackageTests; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | 84146B162580386900A9768A /* UISwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84146B142580386900A9768A /* UISwitch.swift */; }; 25 | 84146B172580386900A9768A /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84146B112580386900A9768A /* App.swift */; }; 26 | 84146B182580386900A9768A /* UIAlertController+ActionInvoker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84146B122580386900A9768A /* UIAlertController+ActionInvoker.swift */; }; 27 | 84146B192580386900A9768A /* UIView+SubviewFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84146B132580386900A9768A /* UIView+SubviewFinder.swift */; }; 28 | 841FE76F2580435600CBB8BF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 841FE769258040B200CBB8BF /* Main.storyboard */; }; 29 | 841FE7762580457900CBB8BF /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841FE7752580457900CBB8BF /* ModalViewController.swift */; }; 30 | 841FE7822581408300CBB8BF /* UITextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841FE7812581408300CBB8BF /* UITextField.swift */; }; 31 | 84584DCE258E83150098FDEE /* Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84584DCD258E83150098FDEE /* Animation.swift */; }; 32 | 847AF23225897D8200D81C33 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847AF23125897D8200D81C33 /* Identifiable.swift */; }; 33 | 847AF243258982F100D81C33 /* UIView+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847AF242258982F100D81C33 /* UIView+Identifiable.swift */; }; 34 | 847AF24A258983BA00D81C33 /* UIStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847AF249258983BA00D81C33 /* UIStepper.swift */; }; 35 | 847AF2512589852E00D81C33 /* UISlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847AF2502589852E00D81C33 /* UISlider.swift */; }; 36 | 84BC452B2582A48100ED9837 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC452A2582A48100ED9837 /* UITableViewCell.swift */; }; 37 | 84E75E3C258039E7001F8290 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E75E3B258039E7001F8290 /* AppDelegate.swift */; }; 38 | 84E75E3E258039E7001F8290 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E75E3D258039E7001F8290 /* SceneDelegate.swift */; }; 39 | 84E75E40258039E7001F8290 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E75E3F258039E7001F8290 /* ViewController.swift */; }; 40 | 84E75E43258039E7001F8290 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84E75E41258039E7001F8290 /* Main.storyboard */; }; 41 | 84E75E45258039E8001F8290 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84E75E44258039E8001F8290 /* Assets.xcassets */; }; 42 | 84E75E48258039E8001F8290 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84E75E46258039E8001F8290 /* LaunchScreen.storyboard */; }; 43 | F2D972DF258A876C0034F592 /* SecondTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D972DE258A876C0034F592 /* SecondTabViewController.swift */; }; 44 | F2D972E6258AACB10034F592 /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D972E5258AACB10034F592 /* TabBarViewController.swift */; }; 45 | F2D972ED258AB0360034F592 /* NestedModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D972EC258AB0360034F592 /* NestedModalViewController.swift */; }; 46 | OBJ_37 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 47 | OBJ_48 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Tests.swift */; }; 48 | OBJ_49 /* FormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* FormViewController.swift */; }; 49 | OBJ_50 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* RootViewController.swift */; }; 50 | OBJ_51 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* TableViewController.swift */; }; 51 | /* End PBXBuildFile section */ 52 | 53 | /* Begin PBXContainerItemProxy section */ 54 | 84146B082580385000A9768A /* PBXContainerItemProxy */ = { 55 | isa = PBXContainerItemProxy; 56 | containerPortal = OBJ_1 /* Project object */; 57 | proxyType = 1; 58 | remoteGlobalIDString = "Ruka::Ruka"; 59 | remoteInfo = Ruka; 60 | }; 61 | 84146B092580385000A9768A /* PBXContainerItemProxy */ = { 62 | isa = PBXContainerItemProxy; 63 | containerPortal = OBJ_1 /* Project object */; 64 | proxyType = 1; 65 | remoteGlobalIDString = "Ruka::RukaTests"; 66 | remoteInfo = RukaTests; 67 | }; 68 | 84E75E52258039ED001F8290 /* PBXContainerItemProxy */ = { 69 | isa = PBXContainerItemProxy; 70 | containerPortal = OBJ_1 /* Project object */; 71 | proxyType = 1; 72 | remoteGlobalIDString = 84E75E38258039E7001F8290; 73 | remoteInfo = "Host App"; 74 | }; 75 | /* End PBXContainerItemProxy section */ 76 | 77 | /* Begin PBXFileReference section */ 78 | 84146B0F2580386900A9768A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 79 | 84146B102580386900A9768A /* Ruka.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Ruka.h; sourceTree = ""; }; 80 | 84146B112580386900A9768A /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 81 | 84146B122580386900A9768A /* UIAlertController+ActionInvoker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+ActionInvoker.swift"; sourceTree = ""; }; 82 | 84146B132580386900A9768A /* UIView+SubviewFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+SubviewFinder.swift"; sourceTree = ""; }; 83 | 84146B142580386900A9768A /* UISwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISwitch.swift; sourceTree = ""; }; 84 | 84146B3F258038A300A9768A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 85 | 841FE71C25803AF000CBB8BF /* Ruka.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Ruka.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 86 | 841FE71D25803AF000CBB8BF /* RukaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RukaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 87 | 841FE71E25803AF000CBB8BF /* Host App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Host App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 88 | 841FE769258040B200CBB8BF /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 89 | 841FE7752580457900CBB8BF /* ModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = ""; }; 90 | 841FE7812581408300CBB8BF /* UITextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextField.swift; sourceTree = ""; }; 91 | 84584DCD258E83150098FDEE /* Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animation.swift; sourceTree = ""; }; 92 | 847AF23125897D8200D81C33 /* Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identifiable.swift; sourceTree = ""; }; 93 | 847AF242258982F100D81C33 /* UIView+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Identifiable.swift"; sourceTree = ""; }; 94 | 847AF249258983BA00D81C33 /* UIStepper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStepper.swift; sourceTree = ""; }; 95 | 847AF2502589852E00D81C33 /* UISlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISlider.swift; sourceTree = ""; }; 96 | 84BC452A2582A48100ED9837 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = ""; }; 97 | 84E75E3B258039E7001F8290 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 98 | 84E75E3D258039E7001F8290 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 99 | 84E75E3F258039E7001F8290 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 100 | 84E75E42258039E7001F8290 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 101 | 84E75E44258039E8001F8290 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 102 | 84E75E47258039E8001F8290 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 103 | 84E75E49258039E8001F8290 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 104 | F2D972DE258A876C0034F592 /* SecondTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondTabViewController.swift; sourceTree = ""; }; 105 | F2D972E5258AACB10034F592 /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = ""; }; 106 | F2D972EC258AB0360034F592 /* NestedModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedModalViewController.swift; sourceTree = ""; }; 107 | OBJ_12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 108 | OBJ_13 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; 109 | OBJ_15 /* FormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormViewController.swift; sourceTree = ""; }; 110 | OBJ_16 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; 111 | OBJ_17 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 112 | OBJ_23 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 113 | OBJ_24 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 114 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 115 | /* End PBXFileReference section */ 116 | 117 | /* Begin PBXFrameworksBuildPhase section */ 118 | 84E75E36258039E7001F8290 /* Frameworks */ = { 119 | isa = PBXFrameworksBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | OBJ_31 /* Frameworks */ = { 126 | isa = PBXFrameworksBuildPhase; 127 | buildActionMask = 0; 128 | files = ( 129 | ); 130 | runOnlyForDeploymentPostprocessing = 0; 131 | }; 132 | OBJ_52 /* Frameworks */ = { 133 | isa = PBXFrameworksBuildPhase; 134 | buildActionMask = 0; 135 | files = ( 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | /* End PBXFrameworksBuildPhase section */ 140 | 141 | /* Begin PBXGroup section */ 142 | 84E75E3A258039E7001F8290 /* Host App */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 84146B3F258038A300A9768A /* README.md */, 146 | 84E75E49258039E8001F8290 /* Info.plist */, 147 | 84E75E3B258039E7001F8290 /* AppDelegate.swift */, 148 | 84E75E3D258039E7001F8290 /* SceneDelegate.swift */, 149 | 84E75E3F258039E7001F8290 /* ViewController.swift */, 150 | 84E75E44258039E8001F8290 /* Assets.xcassets */, 151 | 84E75E46258039E8001F8290 /* LaunchScreen.storyboard */, 152 | 84E75E41258039E7001F8290 /* Main.storyboard */, 153 | ); 154 | path = "Host App"; 155 | sourceTree = ""; 156 | }; 157 | OBJ_10 /* Tests */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 84E75E3A258039E7001F8290 /* Host App */, 161 | OBJ_11 /* RukaTests */, 162 | OBJ_14 /* Support */, 163 | ); 164 | name = Tests; 165 | sourceTree = SOURCE_ROOT; 166 | }; 167 | OBJ_11 /* RukaTests */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | OBJ_12 /* Info.plist */, 171 | OBJ_13 /* Tests.swift */, 172 | ); 173 | name = RukaTests; 174 | path = Tests/RukaTests; 175 | sourceTree = SOURCE_ROOT; 176 | }; 177 | OBJ_14 /* Support */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 841FE769258040B200CBB8BF /* Main.storyboard */, 181 | OBJ_15 /* FormViewController.swift */, 182 | 841FE7752580457900CBB8BF /* ModalViewController.swift */, 183 | F2D972EC258AB0360034F592 /* NestedModalViewController.swift */, 184 | OBJ_16 /* RootViewController.swift */, 185 | OBJ_17 /* TableViewController.swift */, 186 | F2D972DE258A876C0034F592 /* SecondTabViewController.swift */, 187 | F2D972E5258AACB10034F592 /* TabBarViewController.swift */, 188 | ); 189 | path = Support; 190 | sourceTree = ""; 191 | }; 192 | OBJ_5 = { 193 | isa = PBXGroup; 194 | children = ( 195 | OBJ_23 /* LICENSE */, 196 | OBJ_24 /* README.md */, 197 | OBJ_6 /* Package.swift */, 198 | OBJ_7 /* Sources */, 199 | OBJ_10 /* Tests */, 200 | 841FE71C25803AF000CBB8BF /* Ruka.framework */, 201 | 841FE71D25803AF000CBB8BF /* RukaTests.xctest */, 202 | 841FE71E25803AF000CBB8BF /* Host App.app */, 203 | ); 204 | sourceTree = ""; 205 | }; 206 | OBJ_7 /* Sources */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | OBJ_8 /* Ruka */, 210 | ); 211 | name = Sources; 212 | sourceTree = SOURCE_ROOT; 213 | }; 214 | OBJ_8 /* Ruka */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 84146B102580386900A9768A /* Ruka.h */, 218 | 84146B0F2580386900A9768A /* Info.plist */, 219 | 84584DCD258E83150098FDEE /* Animation.swift */, 220 | 84146B112580386900A9768A /* App.swift */, 221 | 847AF23125897D8200D81C33 /* Identifiable.swift */, 222 | 84146B122580386900A9768A /* UIAlertController+ActionInvoker.swift */, 223 | 847AF2502589852E00D81C33 /* UISlider.swift */, 224 | 847AF249258983BA00D81C33 /* UIStepper.swift */, 225 | 84146B142580386900A9768A /* UISwitch.swift */, 226 | 84BC452A2582A48100ED9837 /* UITableViewCell.swift */, 227 | 841FE7812581408300CBB8BF /* UITextField.swift */, 228 | 847AF242258982F100D81C33 /* UIView+Identifiable.swift */, 229 | 84146B132580386900A9768A /* UIView+SubviewFinder.swift */, 230 | ); 231 | name = Ruka; 232 | path = Sources/Ruka; 233 | sourceTree = SOURCE_ROOT; 234 | }; 235 | /* End PBXGroup section */ 236 | 237 | /* Begin PBXNativeTarget section */ 238 | 84E75E38258039E7001F8290 /* Host App */ = { 239 | isa = PBXNativeTarget; 240 | buildConfigurationList = 84E75E4A258039E8001F8290 /* Build configuration list for PBXNativeTarget "Host App" */; 241 | buildPhases = ( 242 | 84E75E35258039E7001F8290 /* Sources */, 243 | 84E75E36258039E7001F8290 /* Frameworks */, 244 | 84E75E37258039E7001F8290 /* Resources */, 245 | ); 246 | buildRules = ( 247 | ); 248 | dependencies = ( 249 | ); 250 | name = "Host App"; 251 | productName = "Host App"; 252 | productReference = 841FE71E25803AF000CBB8BF /* Host App.app */; 253 | productType = "com.apple.product-type.application"; 254 | }; 255 | "Ruka::Ruka" /* Ruka */ = { 256 | isa = PBXNativeTarget; 257 | buildConfigurationList = OBJ_26 /* Build configuration list for PBXNativeTarget "Ruka" */; 258 | buildPhases = ( 259 | OBJ_29 /* Sources */, 260 | OBJ_31 /* Frameworks */, 261 | ); 262 | buildRules = ( 263 | ); 264 | dependencies = ( 265 | ); 266 | name = Ruka; 267 | productName = Ruka; 268 | productReference = 841FE71C25803AF000CBB8BF /* Ruka.framework */; 269 | productType = "com.apple.product-type.framework"; 270 | }; 271 | "Ruka::RukaTests" /* RukaTests */ = { 272 | isa = PBXNativeTarget; 273 | buildConfigurationList = OBJ_44 /* Build configuration list for PBXNativeTarget "RukaTests" */; 274 | buildPhases = ( 275 | OBJ_47 /* Sources */, 276 | OBJ_52 /* Frameworks */, 277 | ); 278 | buildRules = ( 279 | ); 280 | dependencies = ( 281 | OBJ_54 /* PBXTargetDependency */, 282 | 84E75E53258039ED001F8290 /* PBXTargetDependency */, 283 | ); 284 | name = RukaTests; 285 | productName = RukaTests; 286 | productReference = 841FE71D25803AF000CBB8BF /* RukaTests.xctest */; 287 | productType = "com.apple.product-type.bundle.unit-test"; 288 | }; 289 | "Ruka::SwiftPMPackageDescription" /* RukaPackageDescription */ = { 290 | isa = PBXNativeTarget; 291 | buildConfigurationList = OBJ_33 /* Build configuration list for PBXNativeTarget "RukaPackageDescription" */; 292 | buildPhases = ( 293 | OBJ_36 /* Sources */, 294 | ); 295 | buildRules = ( 296 | ); 297 | dependencies = ( 298 | ); 299 | name = RukaPackageDescription; 300 | productName = RukaPackageDescription; 301 | productType = "com.apple.product-type.framework"; 302 | }; 303 | /* End PBXNativeTarget section */ 304 | 305 | /* Begin PBXProject section */ 306 | OBJ_1 /* Project object */ = { 307 | isa = PBXProject; 308 | attributes = { 309 | LastSwiftMigration = 9999; 310 | LastSwiftUpdateCheck = 1220; 311 | LastUpgradeCheck = 9999; 312 | TargetAttributes = { 313 | 84E75E38258039E7001F8290 = { 314 | CreatedOnToolsVersion = 12.2; 315 | DevelopmentTeam = 6JS2VP4ATY; 316 | ProvisioningStyle = Automatic; 317 | }; 318 | "Ruka::RukaTests" = { 319 | TestTargetID = 84E75E38258039E7001F8290; 320 | }; 321 | }; 322 | }; 323 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Ruka" */; 324 | compatibilityVersion = "Xcode 3.2"; 325 | developmentRegion = en; 326 | hasScannedForEncodings = 0; 327 | knownRegions = ( 328 | en, 329 | Base, 330 | ); 331 | mainGroup = OBJ_5; 332 | productRefGroup = OBJ_5; 333 | projectDirPath = ""; 334 | projectRoot = ""; 335 | targets = ( 336 | "Ruka::Ruka" /* Ruka */, 337 | "Ruka::SwiftPMPackageDescription" /* RukaPackageDescription */, 338 | "Ruka::RukaPackageTests::ProductTarget" /* RukaPackageTests */, 339 | "Ruka::RukaTests" /* RukaTests */, 340 | 84E75E38258039E7001F8290 /* Host App */, 341 | ); 342 | }; 343 | /* End PBXProject section */ 344 | 345 | /* Begin PBXResourcesBuildPhase section */ 346 | 84E75E37258039E7001F8290 /* Resources */ = { 347 | isa = PBXResourcesBuildPhase; 348 | buildActionMask = 2147483647; 349 | files = ( 350 | 84E75E48258039E8001F8290 /* LaunchScreen.storyboard in Resources */, 351 | 84E75E45258039E8001F8290 /* Assets.xcassets in Resources */, 352 | 84E75E43258039E7001F8290 /* Main.storyboard in Resources */, 353 | 841FE76F2580435600CBB8BF /* Main.storyboard in Resources */, 354 | ); 355 | runOnlyForDeploymentPostprocessing = 0; 356 | }; 357 | /* End PBXResourcesBuildPhase section */ 358 | 359 | /* Begin PBXSourcesBuildPhase section */ 360 | 84E75E35258039E7001F8290 /* Sources */ = { 361 | isa = PBXSourcesBuildPhase; 362 | buildActionMask = 2147483647; 363 | files = ( 364 | 84E75E40258039E7001F8290 /* ViewController.swift in Sources */, 365 | 84E75E3C258039E7001F8290 /* AppDelegate.swift in Sources */, 366 | 84E75E3E258039E7001F8290 /* SceneDelegate.swift in Sources */, 367 | ); 368 | runOnlyForDeploymentPostprocessing = 0; 369 | }; 370 | OBJ_29 /* Sources */ = { 371 | isa = PBXSourcesBuildPhase; 372 | buildActionMask = 0; 373 | files = ( 374 | 847AF23225897D8200D81C33 /* Identifiable.swift in Sources */, 375 | 847AF2512589852E00D81C33 /* UISlider.swift in Sources */, 376 | 84146B192580386900A9768A /* UIView+SubviewFinder.swift in Sources */, 377 | 84146B162580386900A9768A /* UISwitch.swift in Sources */, 378 | 847AF24A258983BA00D81C33 /* UIStepper.swift in Sources */, 379 | 847AF243258982F100D81C33 /* UIView+Identifiable.swift in Sources */, 380 | 84146B172580386900A9768A /* App.swift in Sources */, 381 | 84146B182580386900A9768A /* UIAlertController+ActionInvoker.swift in Sources */, 382 | 84BC452B2582A48100ED9837 /* UITableViewCell.swift in Sources */, 383 | 84584DCE258E83150098FDEE /* Animation.swift in Sources */, 384 | 841FE7822581408300CBB8BF /* UITextField.swift in Sources */, 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | }; 388 | OBJ_36 /* Sources */ = { 389 | isa = PBXSourcesBuildPhase; 390 | buildActionMask = 0; 391 | files = ( 392 | OBJ_37 /* Package.swift in Sources */, 393 | ); 394 | runOnlyForDeploymentPostprocessing = 0; 395 | }; 396 | OBJ_47 /* Sources */ = { 397 | isa = PBXSourcesBuildPhase; 398 | buildActionMask = 0; 399 | files = ( 400 | F2D972DF258A876C0034F592 /* SecondTabViewController.swift in Sources */, 401 | F2D972ED258AB0360034F592 /* NestedModalViewController.swift in Sources */, 402 | OBJ_48 /* Tests.swift in Sources */, 403 | 841FE7762580457900CBB8BF /* ModalViewController.swift in Sources */, 404 | OBJ_49 /* FormViewController.swift in Sources */, 405 | F2D972E6258AACB10034F592 /* TabBarViewController.swift in Sources */, 406 | OBJ_50 /* RootViewController.swift in Sources */, 407 | OBJ_51 /* TableViewController.swift in Sources */, 408 | ); 409 | runOnlyForDeploymentPostprocessing = 0; 410 | }; 411 | /* End PBXSourcesBuildPhase section */ 412 | 413 | /* Begin PBXTargetDependency section */ 414 | 84E75E53258039ED001F8290 /* PBXTargetDependency */ = { 415 | isa = PBXTargetDependency; 416 | target = 84E75E38258039E7001F8290 /* Host App */; 417 | targetProxy = 84E75E52258039ED001F8290 /* PBXContainerItemProxy */; 418 | }; 419 | OBJ_42 /* PBXTargetDependency */ = { 420 | isa = PBXTargetDependency; 421 | target = "Ruka::RukaTests" /* RukaTests */; 422 | targetProxy = 84146B092580385000A9768A /* PBXContainerItemProxy */; 423 | }; 424 | OBJ_54 /* PBXTargetDependency */ = { 425 | isa = PBXTargetDependency; 426 | target = "Ruka::Ruka" /* Ruka */; 427 | targetProxy = 84146B082580385000A9768A /* PBXContainerItemProxy */; 428 | }; 429 | /* End PBXTargetDependency section */ 430 | 431 | /* Begin PBXVariantGroup section */ 432 | 84E75E41258039E7001F8290 /* Main.storyboard */ = { 433 | isa = PBXVariantGroup; 434 | children = ( 435 | 84E75E42258039E7001F8290 /* Base */, 436 | ); 437 | name = Main.storyboard; 438 | sourceTree = ""; 439 | }; 440 | 84E75E46258039E8001F8290 /* LaunchScreen.storyboard */ = { 441 | isa = PBXVariantGroup; 442 | children = ( 443 | 84E75E47258039E8001F8290 /* Base */, 444 | ); 445 | name = LaunchScreen.storyboard; 446 | sourceTree = ""; 447 | }; 448 | /* End PBXVariantGroup section */ 449 | 450 | /* Begin XCBuildConfiguration section */ 451 | 84E75E4B258039E8001F8290 /* Debug */ = { 452 | isa = XCBuildConfiguration; 453 | buildSettings = { 454 | ALWAYS_SEARCH_USER_PATHS = NO; 455 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 456 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 457 | CLANG_ANALYZER_NONNULL = YES; 458 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 459 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 460 | CLANG_CXX_LIBRARY = "libc++"; 461 | CLANG_ENABLE_MODULES = YES; 462 | CLANG_ENABLE_OBJC_WEAK = YES; 463 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 464 | CLANG_WARN_BOOL_CONVERSION = YES; 465 | CLANG_WARN_COMMA = YES; 466 | CLANG_WARN_CONSTANT_CONVERSION = YES; 467 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 468 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 469 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 470 | CLANG_WARN_EMPTY_BODY = YES; 471 | CLANG_WARN_ENUM_CONVERSION = YES; 472 | CLANG_WARN_INFINITE_RECURSION = YES; 473 | CLANG_WARN_INT_CONVERSION = YES; 474 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 475 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 476 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 477 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 478 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 479 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 480 | CLANG_WARN_STRICT_PROTOTYPES = YES; 481 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 482 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 483 | CLANG_WARN_UNREACHABLE_CODE = YES; 484 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 485 | CODE_SIGN_STYLE = Automatic; 486 | DEVELOPMENT_TEAM = 6JS2VP4ATY; 487 | ENABLE_STRICT_OBJC_MSGSEND = YES; 488 | ENABLE_TESTABILITY = YES; 489 | GCC_C_LANGUAGE_STANDARD = gnu11; 490 | GCC_DYNAMIC_NO_PIC = NO; 491 | GCC_NO_COMMON_BLOCKS = YES; 492 | GCC_PREPROCESSOR_DEFINITIONS = ( 493 | "DEBUG=1", 494 | "$(inherited)", 495 | ); 496 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 497 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 498 | GCC_WARN_UNDECLARED_SELECTOR = YES; 499 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 500 | GCC_WARN_UNUSED_FUNCTION = YES; 501 | GCC_WARN_UNUSED_VARIABLE = YES; 502 | INFOPLIST_FILE = "Host App/Info.plist"; 503 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 504 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 505 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 506 | MTL_FAST_MATH = YES; 507 | PRODUCT_BUNDLE_IDENTIFIER = "com.masilotti.Host-App"; 508 | PRODUCT_NAME = "$(TARGET_NAME)"; 509 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 510 | SWIFT_VERSION = 5.0; 511 | TARGETED_DEVICE_FAMILY = "1,2"; 512 | }; 513 | name = Debug; 514 | }; 515 | 84E75E4C258039E8001F8290 /* Release */ = { 516 | isa = XCBuildConfiguration; 517 | buildSettings = { 518 | ALWAYS_SEARCH_USER_PATHS = NO; 519 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 520 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 521 | CLANG_ANALYZER_NONNULL = YES; 522 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 523 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 524 | CLANG_CXX_LIBRARY = "libc++"; 525 | CLANG_ENABLE_MODULES = YES; 526 | CLANG_ENABLE_OBJC_WEAK = YES; 527 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 528 | CLANG_WARN_BOOL_CONVERSION = YES; 529 | CLANG_WARN_COMMA = YES; 530 | CLANG_WARN_CONSTANT_CONVERSION = YES; 531 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 532 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 533 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 534 | CLANG_WARN_EMPTY_BODY = YES; 535 | CLANG_WARN_ENUM_CONVERSION = YES; 536 | CLANG_WARN_INFINITE_RECURSION = YES; 537 | CLANG_WARN_INT_CONVERSION = YES; 538 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 539 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 540 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 541 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 542 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 543 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 544 | CLANG_WARN_STRICT_PROTOTYPES = YES; 545 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 546 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 547 | CLANG_WARN_UNREACHABLE_CODE = YES; 548 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 549 | CODE_SIGN_STYLE = Automatic; 550 | COPY_PHASE_STRIP = NO; 551 | DEVELOPMENT_TEAM = 6JS2VP4ATY; 552 | ENABLE_NS_ASSERTIONS = NO; 553 | ENABLE_STRICT_OBJC_MSGSEND = YES; 554 | GCC_C_LANGUAGE_STANDARD = gnu11; 555 | GCC_NO_COMMON_BLOCKS = YES; 556 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 557 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 558 | GCC_WARN_UNDECLARED_SELECTOR = YES; 559 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 560 | GCC_WARN_UNUSED_FUNCTION = YES; 561 | GCC_WARN_UNUSED_VARIABLE = YES; 562 | INFOPLIST_FILE = "Host App/Info.plist"; 563 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 564 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 565 | MTL_ENABLE_DEBUG_INFO = NO; 566 | MTL_FAST_MATH = YES; 567 | PRODUCT_BUNDLE_IDENTIFIER = "com.masilotti.Host-App"; 568 | PRODUCT_NAME = "$(TARGET_NAME)"; 569 | SWIFT_VERSION = 5.0; 570 | TARGETED_DEVICE_FAMILY = "1,2"; 571 | VALIDATE_PRODUCT = YES; 572 | }; 573 | name = Release; 574 | }; 575 | OBJ_27 /* Debug */ = { 576 | isa = XCBuildConfiguration; 577 | buildSettings = { 578 | ENABLE_TESTABILITY = YES; 579 | FRAMEWORK_SEARCH_PATHS = ( 580 | "$(inherited)", 581 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 582 | ); 583 | HEADER_SEARCH_PATHS = "$(inherited)"; 584 | INFOPLIST_FILE = Ruka.xcodeproj/Ruka_Info.plist; 585 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 586 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 587 | MACOSX_DEPLOYMENT_TARGET = 10.10; 588 | OTHER_CFLAGS = "$(inherited)"; 589 | OTHER_LDFLAGS = "$(inherited)"; 590 | OTHER_SWIFT_FLAGS = "$(inherited)"; 591 | PRODUCT_BUNDLE_IDENTIFIER = Ruka; 592 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 593 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 594 | SDKROOT = iphoneos; 595 | SKIP_INSTALL = YES; 596 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; 597 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 598 | SWIFT_VERSION = 5.0; 599 | TARGET_NAME = Ruka; 600 | TVOS_DEPLOYMENT_TARGET = 9.0; 601 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 602 | }; 603 | name = Debug; 604 | }; 605 | OBJ_28 /* Release */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | ENABLE_TESTABILITY = YES; 609 | FRAMEWORK_SEARCH_PATHS = ( 610 | "$(inherited)", 611 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 612 | ); 613 | HEADER_SEARCH_PATHS = "$(inherited)"; 614 | INFOPLIST_FILE = Ruka.xcodeproj/Ruka_Info.plist; 615 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 616 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 617 | MACOSX_DEPLOYMENT_TARGET = 10.10; 618 | OTHER_CFLAGS = "$(inherited)"; 619 | OTHER_LDFLAGS = "$(inherited)"; 620 | OTHER_SWIFT_FLAGS = "$(inherited)"; 621 | PRODUCT_BUNDLE_IDENTIFIER = Ruka; 622 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 623 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 624 | SDKROOT = iphoneos; 625 | SKIP_INSTALL = YES; 626 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; 627 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 628 | SWIFT_VERSION = 5.0; 629 | TARGET_NAME = Ruka; 630 | TVOS_DEPLOYMENT_TARGET = 9.0; 631 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 632 | }; 633 | name = Release; 634 | }; 635 | OBJ_3 /* Debug */ = { 636 | isa = XCBuildConfiguration; 637 | buildSettings = { 638 | CLANG_ENABLE_OBJC_ARC = YES; 639 | COMBINE_HIDPI_IMAGES = YES; 640 | COPY_PHASE_STRIP = NO; 641 | DEBUG_INFORMATION_FORMAT = dwarf; 642 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 643 | ENABLE_NS_ASSERTIONS = YES; 644 | GCC_OPTIMIZATION_LEVEL = 0; 645 | GCC_PREPROCESSOR_DEFINITIONS = ( 646 | "$(inherited)", 647 | "SWIFT_PACKAGE=1", 648 | "DEBUG=1", 649 | ); 650 | MACOSX_DEPLOYMENT_TARGET = 10.10; 651 | ONLY_ACTIVE_ARCH = YES; 652 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; 653 | PRODUCT_NAME = "$(TARGET_NAME)"; 654 | SDKROOT = iphoneos; 655 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos"; 656 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG"; 657 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 658 | USE_HEADERMAP = NO; 659 | }; 660 | name = Debug; 661 | }; 662 | OBJ_34 /* Debug */ = { 663 | isa = XCBuildConfiguration; 664 | buildSettings = { 665 | LD = /usr/bin/true; 666 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk -package-description-version 5.3.0"; 667 | SWIFT_VERSION = 5.0; 668 | }; 669 | name = Debug; 670 | }; 671 | OBJ_35 /* Release */ = { 672 | isa = XCBuildConfiguration; 673 | buildSettings = { 674 | LD = /usr/bin/true; 675 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk -package-description-version 5.3.0"; 676 | SWIFT_VERSION = 5.0; 677 | }; 678 | name = Release; 679 | }; 680 | OBJ_4 /* Release */ = { 681 | isa = XCBuildConfiguration; 682 | buildSettings = { 683 | CLANG_ENABLE_OBJC_ARC = YES; 684 | COMBINE_HIDPI_IMAGES = YES; 685 | COPY_PHASE_STRIP = YES; 686 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 687 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 688 | GCC_OPTIMIZATION_LEVEL = s; 689 | GCC_PREPROCESSOR_DEFINITIONS = ( 690 | "$(inherited)", 691 | "SWIFT_PACKAGE=1", 692 | ); 693 | MACOSX_DEPLOYMENT_TARGET = 10.10; 694 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; 695 | PRODUCT_NAME = "$(TARGET_NAME)"; 696 | SDKROOT = iphoneos; 697 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos"; 698 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE"; 699 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 700 | USE_HEADERMAP = NO; 701 | }; 702 | name = Release; 703 | }; 704 | OBJ_40 /* Debug */ = { 705 | isa = XCBuildConfiguration; 706 | buildSettings = { 707 | }; 708 | name = Debug; 709 | }; 710 | OBJ_41 /* Release */ = { 711 | isa = XCBuildConfiguration; 712 | buildSettings = { 713 | }; 714 | name = Release; 715 | }; 716 | OBJ_45 /* Debug */ = { 717 | isa = XCBuildConfiguration; 718 | buildSettings = { 719 | CLANG_ENABLE_MODULES = YES; 720 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 721 | FRAMEWORK_SEARCH_PATHS = ( 722 | "$(inherited)", 723 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 724 | ); 725 | HEADER_SEARCH_PATHS = "$(inherited)"; 726 | INFOPLIST_FILE = Ruka.xcodeproj/RukaTests_Info.plist; 727 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 728 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 729 | MACOSX_DEPLOYMENT_TARGET = 10.15; 730 | OTHER_CFLAGS = "$(inherited)"; 731 | OTHER_LDFLAGS = "$(inherited)"; 732 | OTHER_SWIFT_FLAGS = "$(inherited)"; 733 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 734 | SWIFT_VERSION = 5.0; 735 | TARGET_NAME = RukaTests; 736 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Host App.app/Host App"; 737 | TVOS_DEPLOYMENT_TARGET = 9.0; 738 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 739 | }; 740 | name = Debug; 741 | }; 742 | OBJ_46 /* Release */ = { 743 | isa = XCBuildConfiguration; 744 | buildSettings = { 745 | CLANG_ENABLE_MODULES = YES; 746 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 747 | FRAMEWORK_SEARCH_PATHS = ( 748 | "$(inherited)", 749 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 750 | ); 751 | HEADER_SEARCH_PATHS = "$(inherited)"; 752 | INFOPLIST_FILE = Ruka.xcodeproj/RukaTests_Info.plist; 753 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 754 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 755 | MACOSX_DEPLOYMENT_TARGET = 10.15; 756 | OTHER_CFLAGS = "$(inherited)"; 757 | OTHER_LDFLAGS = "$(inherited)"; 758 | OTHER_SWIFT_FLAGS = "$(inherited)"; 759 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 760 | SWIFT_VERSION = 5.0; 761 | TARGET_NAME = RukaTests; 762 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Host App.app/Host App"; 763 | TVOS_DEPLOYMENT_TARGET = 9.0; 764 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 765 | }; 766 | name = Release; 767 | }; 768 | /* End XCBuildConfiguration section */ 769 | 770 | /* Begin XCConfigurationList section */ 771 | 84E75E4A258039E8001F8290 /* Build configuration list for PBXNativeTarget "Host App" */ = { 772 | isa = XCConfigurationList; 773 | buildConfigurations = ( 774 | 84E75E4B258039E8001F8290 /* Debug */, 775 | 84E75E4C258039E8001F8290 /* Release */, 776 | ); 777 | defaultConfigurationIsVisible = 0; 778 | defaultConfigurationName = Release; 779 | }; 780 | OBJ_2 /* Build configuration list for PBXProject "Ruka" */ = { 781 | isa = XCConfigurationList; 782 | buildConfigurations = ( 783 | OBJ_3 /* Debug */, 784 | OBJ_4 /* Release */, 785 | ); 786 | defaultConfigurationIsVisible = 0; 787 | defaultConfigurationName = Release; 788 | }; 789 | OBJ_26 /* Build configuration list for PBXNativeTarget "Ruka" */ = { 790 | isa = XCConfigurationList; 791 | buildConfigurations = ( 792 | OBJ_27 /* Debug */, 793 | OBJ_28 /* Release */, 794 | ); 795 | defaultConfigurationIsVisible = 0; 796 | defaultConfigurationName = Release; 797 | }; 798 | OBJ_33 /* Build configuration list for PBXNativeTarget "RukaPackageDescription" */ = { 799 | isa = XCConfigurationList; 800 | buildConfigurations = ( 801 | OBJ_34 /* Debug */, 802 | OBJ_35 /* Release */, 803 | ); 804 | defaultConfigurationIsVisible = 0; 805 | defaultConfigurationName = Release; 806 | }; 807 | OBJ_39 /* Build configuration list for PBXAggregateTarget "RukaPackageTests" */ = { 808 | isa = XCConfigurationList; 809 | buildConfigurations = ( 810 | OBJ_40 /* Debug */, 811 | OBJ_41 /* Release */, 812 | ); 813 | defaultConfigurationIsVisible = 0; 814 | defaultConfigurationName = Release; 815 | }; 816 | OBJ_44 /* Build configuration list for PBXNativeTarget "RukaTests" */ = { 817 | isa = XCConfigurationList; 818 | buildConfigurations = ( 819 | OBJ_45 /* Debug */, 820 | OBJ_46 /* Release */, 821 | ); 822 | defaultConfigurationIsVisible = 0; 823 | defaultConfigurationName = Release; 824 | }; 825 | /* End XCConfigurationList section */ 826 | }; 827 | rootObject = OBJ_1 /* Project object */; 828 | } 829 | --------------------------------------------------------------------------------