├── .gitattributes ├── HackMan-Banner.png ├── HackMan-Preview.gif ├── Sources ├── HackManLib │ ├── Generators │ │ ├── AssetCatalog │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ └── AssetCatalog.swift │ │ ├── ReusableView │ │ │ ├── ReusableView.stf │ │ │ ├── UITableViewExtensions.stf │ │ │ ├── ReusableView.swift │ │ │ └── UICollectionViewExtensions.stf │ │ ├── Coordinator │ │ │ ├── Coordinator.stf │ │ │ └── Coordinator.swift │ │ ├── Model │ │ │ ├── Model.stf │ │ │ └── Model.swift │ │ ├── Localization │ │ │ └── Localization.swift │ │ ├── ViewController │ │ │ ├── ChildCoordinator.stf │ │ │ ├── ViewController.stf │ │ │ └── ViewController.swift │ │ ├── LaunchScreen │ │ │ ├── LaunchScreen.swift │ │ │ └── LaunchScreen.storyboard │ │ ├── NewProject │ │ │ ├── project.yml │ │ │ ├── NewProject.swift │ │ │ └── gitignore │ │ ├── ViewControllerInformation │ │ │ ├── ChildCoordinator.stf │ │ │ ├── ViewControllerInformation.swift │ │ │ ├── ViewControllerInformation.stf │ │ │ ├── Licence.html │ │ │ ├── Publisher.html │ │ │ ├── DataProtection.html │ │ │ └── LegalNotices.html │ │ ├── CoordinatorChild │ │ │ ├── CoordinatorChild.stf │ │ │ └── CoordinatorChild.swift │ │ ├── ViewControllerWeb │ │ │ ├── ViewControllerWeb.swift │ │ │ └── ViewControllerWeb.stf │ │ ├── CoordinatorMain │ │ │ ├── CoordinatorMain.stf │ │ │ └── CoordinatorMain.swift │ │ ├── AppDelegate │ │ │ ├── AppDelegate.swift │ │ │ └── AppDelegate.stf │ │ ├── TableViewCell │ │ │ ├── TableViewCell.swift │ │ │ └── TableViewCell.stf │ │ ├── SearchResultsController │ │ │ ├── SearchResultsController.swift │ │ │ └── SearchResultsController.stf │ │ ├── CollectionViewCell │ │ │ ├── CollectionViewCell.swift │ │ │ └── CollectionViewCell.stf │ │ ├── ViewControllerDetail │ │ │ ├── ViewControllerDetail.swift │ │ │ └── ViewControllerDetail.stf │ │ ├── Scaffold │ │ │ └── Scaffold.swift │ │ ├── DataSourceTableView │ │ │ ├── DataSourceTableView.swift │ │ │ └── DataSourceTableView.stf │ │ ├── DataSourceTableViewSearchResults │ │ │ ├── DataSourceTableViewSearchResults.stf │ │ │ └── DataSourceTableViewSearchResults.swift │ │ ├── ViewControllerCollection │ │ │ ├── ViewControllerCollection.swift │ │ │ └── ViewControllerCollection.stf │ │ └── ViewControllerTable │ │ │ ├── ViewControllerTable.swift │ │ │ └── ViewControllerTable.stf │ ├── Extensions │ │ └── StringColorExtensions.swift │ ├── Model │ │ ├── Resource.swift │ │ └── Property.swift │ ├── Generator.swift │ ├── CommandLineRunner.swift │ └── Writer.swift └── HackMan │ └── main.swift ├── Tests ├── LinuxMain.swift └── HackManTests │ ├── XCTestManifests.swift │ ├── HackManResourceTests.swift │ └── HackManTests.swift ├── LICENSE ├── Package.swift ├── Package.resolved ├── .gitignore ├── CODE_OF_CONDUCT.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /HackMan-Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cosmo/HackMan/HEAD/HackMan-Banner.png -------------------------------------------------------------------------------- /HackMan-Preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cosmo/HackMan/HEAD/HackMan-Preview.gif -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/AssetCatalog/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import HackManTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += HackManTests.allTests() 7 | tests += HackManStringTests.allTests() 8 | tests += HackManResourceTests.allTests() 9 | XCTMain(tests) 10 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ReusableView/ReusableView.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol ReusableView: class {} 4 | extension ReusableView where Self: UIView { 5 | static var reuseIdentifier: String { 6 | return String(describing: self) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/HackManTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(HackManTests.allTests), 7 | testCase(HackManResourceTests.allTests), 8 | ] 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/Coordinator/Coordinator.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol Coordinator { 4 | var children: [Coordinator] { get set } 5 | var parent: Coordinator? { get set } 6 | var containerController: UIViewController { get set } 7 | 8 | func start() 9 | } 10 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/Model/Model.stf: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct {{ resource.name }} { 4 | {% for property in resource.properties %} 5 | let {{ property.name }}: {{ property.valueType }} 6 | {% endfor %} 7 | 8 | static func sampleData() -> [{{ resource.name }}] { 9 | return [{% for mock in mocks %} 10 | {{ mock }}, {% endfor %} 11 | ] 12 | } 13 | } 14 | 15 | extension {{ resource.name }}: Codable { 16 | } 17 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/Localization/Localization.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc(Localization) 4 | class Localization: NSObject, Generator { 5 | required override init() {} 6 | 7 | func generate(arguments: [String], options: [String]) { 8 | print("NOT IMPLEMENTED!") 9 | } 10 | 11 | static func help() { 12 | print("Not implemented.") 13 | } 14 | 15 | static func singleLineUsage() -> String { 16 | return "Not implemented." 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewController/ChildCoordinator.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class {{ resource.name }}Coordinator: Coordinator { 4 | var children = [Coordinator]() 5 | var parent: Coordinator? 6 | var containerController: UIViewController 7 | 8 | init(navigationController: UINavigationController) { 9 | self.containerController = navigationController 10 | } 11 | 12 | func start() { 13 | let viewController = {{ resource.name }}ViewController() 14 | viewController.coordinator = self 15 | (containerController as? UINavigationController)?.pushViewController(viewController, animated: false) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ReusableView/UITableViewExtensions.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableView { 4 | func register(_ cellClass: T.Type) where T: ReusableView { 5 | self.register(cellClass.self, forCellReuseIdentifier: cellClass.reuseIdentifier) 6 | } 7 | 8 | func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T where T: ReusableView { 9 | guard let cell = self.dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 10 | preconditionFailure("Could not dequeue cell with identifier: \(T.reuseIdentifier)") 11 | } 12 | return cell 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/HackManLib/Extensions/StringColorExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Devran on 06.06.19. 6 | // 7 | 8 | import Foundation 9 | 10 | extension StringLiteralType { 11 | public enum TerminalColor: String { 12 | case black = "\u{001B}[0;30m" 13 | case red = "\u{001B}[0;31m" 14 | case green = "\u{001B}[0;32m" 15 | case yellow = "\u{001B}[0;33m" 16 | case blue = "\u{001B}[0;34m" 17 | case magenta = "\u{001B}[0;35m" 18 | case cyan = "\u{001B}[0;36m" 19 | case white = "\u{001B}[0;37m" 20 | case `default` = "\u{001B}[0;0m" 21 | } 22 | 23 | func colored(_ color: TerminalColor) -> String { 24 | return "\(color.rawValue)\(self)\(TerminalColor.default.rawValue)" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/Coordinator/Coordinator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(Coordinator) 5 | class Coordinator: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | let rendered = try! environment.renderTemplate(name: "Coordinator.stf") 10 | 11 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Protocols/Coordinator.swift", contents: rendered, options: options) 12 | } 13 | 14 | static func help() { 15 | print("Usage: hackman generate \(singleLineUsage())") 16 | print() 17 | print("Example:") 18 | print(" hackman generate coordinator") 19 | } 20 | 21 | static func singleLineUsage() -> String { 22 | return "coordinator" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/LaunchScreen/LaunchScreen.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc(LaunchScreen) 4 | class LaunchScreen: NSObject, Generator { 5 | required override init() {} 6 | 7 | func generate(arguments: [String], options: [String]) { 8 | let url = URL(fileURLWithPath: "\(basePath)/LaunchScreen.storyboard") 9 | let contents = try! String(contentsOf: url, encoding: String.Encoding.utf8) 10 | 11 | Writer.createPath("Source") 12 | Writer.createFile("\(Writer.extractSourcePath(options: options))/LaunchScreen.storyboard", contents: contents, options: options) 13 | } 14 | 15 | static func help() { 16 | print("Usage: hackman generate \(singleLineUsage())") 17 | print() 18 | print("Example:") 19 | print(" hackman generate launch_screen") 20 | } 21 | 22 | static func singleLineUsage() -> String { 23 | return "launch_screen" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/HackManTests/HackManResourceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HackManLib 3 | 4 | final class HackManResourceTests: XCTestCase { 5 | func testResource() throws { 6 | let resource = Resource(name: "tooth", properties: nil) 7 | 8 | XCTAssertEqual(resource.name, "Tooth") 9 | XCTAssertEqual(resource.nameForFunction, "tooth") 10 | XCTAssertEqual(resource.pluralizedName, "Teeth") 11 | XCTAssertEqual(resource.pluralizedNameForFunction, "teeth") 12 | 13 | let resource2 = Resource(name: "money", properties: nil) 14 | 15 | XCTAssertEqual(resource2.name, "Money") 16 | XCTAssertEqual(resource2.nameForFunction, "money") 17 | XCTAssertEqual(resource2.pluralizedName, "MoneyItems") 18 | XCTAssertEqual(resource2.pluralizedNameForFunction, "moneyItems") 19 | } 20 | 21 | static var allTests = [ 22 | ("testResource", testResource), 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/NewProject/project.yml: -------------------------------------------------------------------------------- 1 | name: {{ projectName }} 2 | configs: 3 | Enterprise Debug: debug 4 | AppStore Debug: debug 5 | Enterprise Release: release 6 | AppStore Release: release 7 | options: 8 | bundleIdPrefix: {{ bundleIdPrefix }} 9 | targets: 10 | {{ projectName }}-iOS: 11 | type: application 12 | platform: iOS 13 | deploymentTarget: "12.1" 14 | sources: [{{ source }}] 15 | scheme: 16 | configVariants: 17 | - Enterprise 18 | - AppStore 19 | info: 20 | path: Source/Info.plist 21 | properties: 22 | UISupportedInterfaceOrientations: [UIInterfaceOrientationPortrait, UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight] 23 | UISupportedInterfaceOrientations~ipad: [UIInterfaceOrientationPortrait, UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight, UIInterfaceOrientationPortraitUpsideDown] 24 | UILaunchStoryboardName: LaunchScreen 25 | -------------------------------------------------------------------------------- /Sources/HackManLib/Model/Resource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Devran on 01.09.19. 6 | // 7 | 8 | import Foundation 9 | import StringCase 10 | 11 | struct Resource { 12 | var name: String 13 | var properties: [Property]? 14 | var nameForFunction: String 15 | var pluralizedName: String 16 | var pluralizedNameForFunction: String 17 | 18 | init(name: String, properties: [Property]?) { 19 | self.name = name.upperCamelCased() 20 | self.nameForFunction = self.name.lowerCamelCased() 21 | 22 | let pluralizedName = self.name.pluralized() 23 | if pluralizedName == self.name { 24 | self.pluralizedName = self.name + "Items" 25 | } else { 26 | self.pluralizedName = self.name.pluralized() 27 | } 28 | 29 | self.pluralizedNameForFunction = self.pluralizedName.lowerCamelCased() 30 | 31 | self.properties = properties 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerInformation/ChildCoordinator.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class InformationCoordinator: Coordinator { 4 | var children = [Coordinator]() 5 | var parent: Coordinator? 6 | var containerController: UIViewController 7 | 8 | init(navigationController: UINavigationController) { 9 | self.containerController = navigationController 10 | } 11 | 12 | func start() { 13 | let viewController = InformationViewController() 14 | viewController.coordinator = self 15 | (containerController as? UINavigationController)?.pushViewController(viewController, animated: false) 16 | } 17 | 18 | func detail(item: InformationViewController.Item) { 19 | guard let url = item.url else { return } 20 | let viewController = WebViewController(url: url) 21 | viewController.title = item.name 22 | (containerController as? UINavigationController)?.pushViewController(viewController, animated: true) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Devran Uenal on 04.06.19. 6 | // 7 | 8 | import Foundation 9 | import PathKit 10 | import Stencil 11 | 12 | public protocol Generator { 13 | init() 14 | func generate(arguments: [String], options: [String]) 15 | static func help() 16 | static func singleLineUsage() -> String 17 | } 18 | 19 | extension Generator { 20 | var generatorName: String { 21 | return String(describing: type(of: self)) 22 | } 23 | 24 | var basePath: Path { 25 | var bundlePath = Bundle.main.bundlePath.split(separator: "/") 26 | bundlePath.removeLast(2) 27 | let generatorsPath = bundlePath.joined(separator: "/") 28 | return Path("/\(generatorsPath)/Sources/HackManLib/Generators/\(generatorName)") 29 | } 30 | 31 | var loader: FileSystemLoader { 32 | return FileSystemLoader(paths: [basePath]) 33 | } 34 | 35 | var environment: Environment { 36 | return Environment(loader: loader) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/HackMan/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Devran on 15.08.19. 6 | // 7 | 8 | import Stencil 9 | import Foundation 10 | import HackManLib 11 | 12 | var arguments = CommandLine.arguments 13 | 14 | let options = arguments.filter { $0.starts(with: "-") } 15 | arguments = arguments.filter { !options.contains($0) } 16 | 17 | do { 18 | try _ = CommandLineRunner(arguments: arguments, options: options) 19 | } catch let error as CommandLineRunner.CommandError { 20 | switch error { 21 | case CommandLineRunner.CommandError.noCommand: 22 | CommandLineRunner.printCommandUsage() 23 | case .unknownCommand(let name): 24 | print("Unknown command \"\(name)\".") 25 | print() 26 | CommandLineRunner.printCommandUsage() 27 | } 28 | } catch let error as CommandLineRunner.GeneratorCommandError { 29 | switch error { 30 | case .noGenerator: 31 | CommandLineRunner.printGeneratorUsage() 32 | case .unknownGenerator: 33 | print("Unknown generator.") 34 | print() 35 | CommandLineRunner.printGeneratorUsage() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/CoordinatorChild/CoordinatorChild.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class {{ resource.pluralizedName }}Coordinator: Coordinator { 4 | var children = [Coordinator]() 5 | var parent: Coordinator? 6 | var containerController: UIViewController 7 | 8 | init(navigationController: UINavigationController) { 9 | self.containerController = navigationController 10 | } 11 | 12 | func start() { 13 | let viewController = {{ resource.pluralizedName }}ViewController() 14 | viewController.coordinator = self 15 | (containerController as? UINavigationController)?.pushViewController(viewController, animated: false) 16 | } 17 | 18 | func detail({{ resource.nameForFunction }}: {{ resource.name }}) { 19 | let viewController = {{ resource.name }}ViewController({{ resource.nameForFunction }}: {{ resource.nameForFunction }}) 20 | viewController.coordinator = self 21 | viewController.title = "{{ resource.name }} Detail" 22 | (containerController as? UINavigationController)?.pushViewController(viewController, animated: true) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerWeb/ViewControllerWeb.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(ViewControllerWeb) 5 | class ViewControllerWeb: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 10 | let context: [String: Any] = [ 11 | "coordinator": containsCoordinator 12 | ] 13 | 14 | let rendered = try! environment.renderTemplate(name: "ViewControllerWeb.stf", context: context) 15 | 16 | Writer.createFile("\(Writer.extractSourcePath(options: options))/ViewControllers/Generic/WebViewController.swift", contents: rendered, options: options) 17 | } 18 | 19 | static func help() { 20 | print("Usage: hackman generate \(singleLineUsage())") 21 | print() 22 | print("Example:") 23 | print(" hackman generate view_controller_web") 24 | } 25 | 26 | static func singleLineUsage() -> String { 27 | return "view_controller_web" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Devran "Cosmo" Uenal 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 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewController/ViewController.stf: -------------------------------------------------------------------------------- 1 | // 2 | // {{ resource.name }}ViewController.swift 3 | // 4 | 5 | import UIKit 6 | 7 | class {{ resource.name }}ViewController: UIViewController { 8 | {% if coordinator %} 9 | weak var coordinator: {{ resource.name }}Coordinator? 10 | {% endif %} 11 | 12 | convenience init() { 13 | self.init(nibName: nil, bundle: nil) 14 | } 15 | 16 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 17 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 18 | commonInit() 19 | } 20 | 21 | required init?(coder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | func commonInit() { 26 | title = NSLocalizedString("{{ resource.name }}", comment: "{{ resource.name }} Title (Singular)") 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | navigationController?.navigationBar.prefersLargeTitles = true 33 | 34 | view.backgroundColor = UIColor.white 35 | 36 | setupConstraints() 37 | } 38 | 39 | func setupConstraints() { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/CoordinatorMain/CoordinatorMain.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class MainCoordinator: Coordinator { 4 | var children = [Coordinator]() 5 | var parent: Coordinator? 6 | var containerController: UIViewController 7 | 8 | init(tabBarController: UITabBarController) { 9 | self.containerController = tabBarController 10 | } 11 | 12 | func start() { 13 | children = [ 14 | {{ children|join:", " }} 15 | ] 16 | 17 | for var childCoordinator in children { 18 | childCoordinator.parent = self 19 | childCoordinator.start() 20 | } 21 | 22 | (containerController as? UITabBarController)?.viewControllers = children.map { $0.containerController } 23 | } 24 | 25 | {% for resource in resources %} 26 | func select{{ resource.name }}({{ resource.nameForFunction }}: {{ resource.name }}) { 27 | (containerController as? UITabBarController)?.selectedIndex = children.firstIndex { $0 is {{ resource.pluralizedName }}Coordinator } ?? 0 28 | let coordinator = children.first { $0 is {{ resource.pluralizedName }}Coordinator } as? {{ resource.pluralizedName }}Coordinator 29 | coordinator?.detail({{ resource.nameForFunction }}: {{ resource.nameForFunction }}) 30 | } 31 | {% endfor %} 32 | } 33 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/AppDelegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(AppDelegate) 5 | class AppDelegate: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 10 | if containsCoordinator { 11 | Coordinator().generate(arguments: arguments, options: options) 12 | CoordinatorMain().generate(arguments: arguments, options: options) 13 | } 14 | 15 | let context: [String: Any] = [ 16 | "coordinator": containsCoordinator 17 | ] 18 | 19 | let rendered = try! environment.renderTemplate(name: "AppDelegate.stf", context: context) 20 | 21 | Writer.createFile("\(Writer.extractSourcePath(options: options))/AppDelegate.swift", contents: rendered, options: options) 22 | } 23 | 24 | static func help() { 25 | print("Usage: hackman generate \(singleLineUsage())") 26 | print() 27 | print("Example:") 28 | print(" hackman generate app_delegate") 29 | } 30 | 31 | static func singleLineUsage() -> String { 32 | return "app_delegate" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/AssetCatalog/AssetCatalog.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc(AssetCatalog) 4 | class AssetCatalog: NSObject, Generator { 5 | required override init() {} 6 | 7 | func generate(arguments: [String], options: [String]) { 8 | let url = URL(fileURLWithPath: "\(basePath)/Assets.xcassets") 9 | let contentsUrl1 = url.appendingPathComponent("Contents.json") 10 | let contents = try! String(contentsOf: contentsUrl1, encoding: String.Encoding.utf8) 11 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Assets.xcassets/Contents.json", contents: contents, options: options) 12 | 13 | let contentsUrl2 = url.appendingPathComponent("AppIcon.appiconset/Contents.json") 14 | let contents2 = try! String(contentsOf: contentsUrl2, encoding: String.Encoding.utf8) 15 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Assets.xcassets/AppIcon.appiconset/Contents.json", contents: contents2, options: options) 16 | } 17 | 18 | static func help() { 19 | print("Usage: hackman generate \(singleLineUsage())") 20 | print() 21 | print("Example:") 22 | print(" hackman generate asset_catalog") 23 | } 24 | 25 | static func singleLineUsage() -> String { 26 | return "asset_catalog" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ReusableView/ReusableView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(ReusableView) 5 | class ReusableView: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | let rendered = try! environment.renderTemplate(name: "ReusableView.stf") 10 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Protocols/ReusableView.swift", contents: rendered, options: options) 11 | 12 | let rendered2 = try! environment.renderTemplate(name: "UICollectionViewExtensions.stf") 13 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Protocols/UICollectionViewExtensions.swift", contents: rendered2, options: options) 14 | 15 | let rendered3 = try! environment.renderTemplate(name: "UITableViewExtensions.stf") 16 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Protocols/UITableViewExtensions.swift", contents: rendered3, options: options) 17 | } 18 | 19 | static func help() { 20 | print("Usage: hackman generate \(singleLineUsage())") 21 | print() 22 | print("Example:") 23 | print(" hackman generate reusable_view") 24 | } 25 | 26 | static func singleLineUsage() -> String { 27 | return "reusable_view" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ReusableView/UICollectionViewExtensions.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UICollectionView { 4 | func register(_ cellClass: T.Type) where T: ReusableView { 5 | self.register(cellClass.self, forCellWithReuseIdentifier: cellClass.reuseIdentifier) 6 | } 7 | 8 | func register(_ cellClass: T.Type, kind: String) where T: ReusableView { 9 | self.register(cellClass.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: cellClass.reuseIdentifier) 10 | } 11 | 12 | func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T where T: ReusableView { 13 | guard let cell = self.dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 14 | preconditionFailure("Could not dequeue cell with identifier: \(T.reuseIdentifier)") 15 | } 16 | return cell 17 | } 18 | 19 | func dequeueReusableSupplementaryView(forIndexPath indexPath: IndexPath, kind: String) -> T where T: ReusableView { 20 | guard let view = self.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 21 | preconditionFailure("Could not dequeue reusableView with identifier: \(T.reuseIdentifier)") 22 | } 23 | return view 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/CoordinatorChild/CoordinatorChild.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | import GrammaticalNumber 4 | 5 | @objc(CoordinatorChild) 6 | class CoordinatorChild: NSObject, Generator { 7 | required override init() {} 8 | 9 | func generate(arguments: [String], options: [String]) { 10 | guard !arguments.isEmpty else { 11 | type(of: self).help() 12 | exit(0) 13 | } 14 | 15 | var mutableArguments = arguments 16 | let resource = Resource( 17 | name: mutableArguments.removeFirst(), 18 | properties: Property.createList(inputStrings: mutableArguments) 19 | ) 20 | let context: [String: Any] = [ 21 | "resource": resource 22 | ] 23 | 24 | let rendered = try! environment.renderTemplate(name: "CoordinatorChild.stf", context: context) 25 | 26 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Coordinators/\(resource.pluralizedName)Coordinator.swift", contents: rendered, options: options) 27 | } 28 | 29 | static func help() { 30 | print("Usage: hackman generate \(singleLineUsage())") 31 | print() 32 | print("Example:") 33 | print(" hackman generate coordinator_child song") 34 | } 35 | 36 | static func singleLineUsage() -> String { 37 | return "coordinator_child NAME" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "HackMan", 8 | products: [ 9 | .executable(name: "hackman", targets: ["HackMan"]), 10 | .library(name: "HackManLib", targets: ["HackManLib"]) 11 | ], 12 | dependencies: [ 13 | // Dependencies declare other packages that this package depends on. 14 | .package(url: "https://github.com/stencilproject/Stencil.git", .branch("master")), 15 | .package(url: "https://github.com/Cosmo/GrammaticalNumber.git", from: "0.0.3"), 16 | .package(url: "https://github.com/Cosmo/StringCase.git", from: "1.0.1"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "HackManLib", 23 | dependencies: [ 24 | "Stencil", 25 | "GrammaticalNumber", 26 | "StringCase"] 27 | ), 28 | .target( 29 | name: "HackMan", 30 | dependencies: ["HackManLib"]), 31 | .testTarget( 32 | name: "HackManTests", 33 | dependencies: ["HackManLib"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/TableViewCell/TableViewCell.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(TableViewCell) 5 | class TableViewCell: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | guard !arguments.isEmpty else { 10 | type(of: self).help() 11 | exit(0) 12 | } 13 | 14 | var mutableArguments = arguments 15 | let resource = Resource( 16 | name: mutableArguments.removeFirst(), 17 | properties: Property.createList(inputStrings: mutableArguments) 18 | ) 19 | let context: [String: Any] = [ 20 | "resource": resource 21 | ] 22 | 23 | let rendered = try! environment.renderTemplate(name: "TableViewCell.stf", context: context) 24 | 25 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Views/Cells/\(resource.name)TableViewCell.swift", contents: rendered, options: options) 26 | } 27 | 28 | static func help() { 29 | print("Usage: hackman generate \(singleLineUsage())") 30 | print() 31 | print("Example:") 32 | print(" hackman generate table_view_cell song title:string artist_name:string album_name:string") 33 | } 34 | 35 | static func singleLineUsage() -> String { 36 | return "table_view_cell NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/SearchResultsController/SearchResultsController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | import GrammaticalNumber 4 | 5 | @objc(SearchResultsController) 6 | class SearchResultsController: NSObject, Generator { 7 | required override init() {} 8 | 9 | func generate(arguments: [String], options: [String]) { 10 | guard !arguments.isEmpty else { 11 | type(of: self).help() 12 | exit(0) 13 | } 14 | 15 | var mutableArguments = arguments 16 | let resource = Resource( 17 | name: mutableArguments.removeFirst(), 18 | properties: Property.createList(inputStrings: mutableArguments) 19 | ) 20 | let context: [String: Any] = [ 21 | "resource": resource 22 | ] 23 | 24 | let rendered = try! environment.renderTemplate(name: "SearchResultsController.stf", context: context) 25 | Writer.createFile("\(Writer.extractSourcePath(options: options))/ViewControllers/\(resource.pluralizedName)/\(resource.name)SearchResultsViewController.swift", contents: rendered, options: options) 26 | } 27 | 28 | static func help() { 29 | print("Usage: hackman generate \(singleLineUsage())") 30 | print() 31 | print("Example:") 32 | print(" search_results_controller song") 33 | } 34 | 35 | static func singleLineUsage() -> String { 36 | return "search_results_controller NAME" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/CollectionViewCell/CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(CollectionViewCell) 5 | class CollectionViewCell: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | guard !arguments.isEmpty else { 10 | type(of: self).help() 11 | exit(0) 12 | } 13 | 14 | var mutableArguments = arguments 15 | let resource = Resource( 16 | name: mutableArguments.removeFirst(), 17 | properties: Property.createList(inputStrings: mutableArguments) 18 | ) 19 | let context: [String: Any] = [ 20 | "resource": resource 21 | ] 22 | 23 | let rendered = try! environment.renderTemplate(name: "CollectionViewCell.stf", context: context) 24 | 25 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Views/Cells/\(resource.name)CollectionViewCell.swift", contents: rendered, options: options) 26 | } 27 | 28 | static func help() { 29 | print("Usage: hackman generate \(singleLineUsage())") 30 | print() 31 | print("Example:") 32 | print(" hackman generate collection_view_cell song title:string artist_name:string album_name:string") 33 | } 34 | 35 | static func singleLineUsage() -> String { 36 | return "collection_view_cell NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/SearchResultsController/SearchResultsController.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class {{ resource.name }}SearchResultsViewController: UIViewController { 4 | lazy var tableView: UITableView = { 5 | let tableView = UITableView(frame: CGRect.zero, style: UITableView.Style.plain) 6 | tableView.keyboardDismissMode = UIScrollView.KeyboardDismissMode.onDrag 7 | return tableView 8 | }() 9 | 10 | init() { 11 | super.init(nibName: nil, bundle: nil) 12 | commonInit() 13 | } 14 | 15 | required init?(coder: NSCoder) { 16 | super.init(coder: coder) 17 | commonInit() 18 | } 19 | 20 | func commonInit() { 21 | 22 | } 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | view.backgroundColor = UIColor.white 27 | 28 | view.addSubview(tableView) 29 | 30 | setupConstraints() 31 | } 32 | 33 | func setupConstraints() { 34 | tableView.translatesAutoresizingMaskIntoConstraints = false 35 | tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true 36 | tableView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true 37 | view.safeAreaLayoutGuide.rightAnchor.constraint(equalTo: tableView.rightAnchor).isActive = true 38 | view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: tableView.bottomAnchor).isActive = true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "GrammaticalNumber", 6 | "repositoryURL": "https://github.com/Cosmo/GrammaticalNumber.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "3827665bd38a0fd5f215220b26c92df876539af0", 10 | "version": "0.0.3" 11 | } 12 | }, 13 | { 14 | "package": "PathKit", 15 | "repositoryURL": "https://github.com/kylef/PathKit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511", 19 | "version": "1.0.0" 20 | } 21 | }, 22 | { 23 | "package": "Spectre", 24 | "repositoryURL": "https://github.com/kylef/Spectre.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", 28 | "version": "0.9.0" 29 | } 30 | }, 31 | { 32 | "package": "Stencil", 33 | "repositoryURL": "https://github.com/stencilproject/Stencil.git", 34 | "state": { 35 | "branch": "master", 36 | "revision": "e516ca9389b64da70b71a461925bbca66f65fe61", 37 | "version": null 38 | } 39 | }, 40 | { 41 | "package": "StringCase", 42 | "repositoryURL": "https://github.com/Cosmo/StringCase.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "8b1c23b18382a67d44f96e609eaaaf7842f773e8", 46 | "version": "1.0.1" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/NewProject/NewProject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | import GrammaticalNumber 4 | import StringCase 5 | 6 | @objc(NewProject) 7 | class NewProject: NSObject, Generator { 8 | required override init() {} 9 | 10 | func generate(arguments: [String], options: [String]) { 11 | guard !arguments.isEmpty else { 12 | type(of: self).help() 13 | exit(0) 14 | } 15 | 16 | var mutableArguments = arguments 17 | let projectName = mutableArguments.removeFirst() 18 | 19 | let context = [ 20 | "projectName": projectName, 21 | "source": Writer.extractSourcePath(options: options), 22 | "bundleIdPrefix": "com.\(projectName.upperCamelCased())" 23 | ] 24 | 25 | let rendered = try! environment.renderTemplate(name: "project.yml", context: context) 26 | Writer.createFile("\(projectName)/project.yml", contents: rendered, options: options) 27 | 28 | let rendered2 = try! environment.renderTemplate(name: "gitignore", context: context) 29 | Writer.createFile("\(projectName)/.gitignore", contents: rendered2, options: options) 30 | 31 | print() 32 | print("Now go to your project directory (\"cd \(projectName)\") to run other commands (hackman generate).") 33 | } 34 | 35 | static func help() { 36 | print("Usage: hackman generate \(singleLineUsage())") 37 | print() 38 | print("Example:") 39 | print(" hackman new AwesomeApp") 40 | } 41 | 42 | static func singleLineUsage() -> String { 43 | return "new APP_NAME" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerDetail/ViewControllerDetail.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | import GrammaticalNumber 4 | 5 | @objc(ViewControllerDetail) 6 | class ViewControllerDetail: NSObject, Generator { 7 | required override init() {} 8 | 9 | func generate(arguments: [String], options: [String]) { 10 | guard !arguments.isEmpty else { 11 | type(of: self).help() 12 | exit(0) 13 | } 14 | 15 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 16 | var mutableArguments = arguments 17 | let resource = Resource( 18 | name: mutableArguments.removeFirst(), 19 | properties: Property.createList(inputStrings: mutableArguments) 20 | ) 21 | let context: [String: Any] = [ 22 | "resource": resource, 23 | "coordinator": containsCoordinator 24 | ] 25 | 26 | let rendered = try! environment.renderTemplate(name: "ViewControllerDetail.stf", context: context) 27 | Writer.createFile("\(Writer.extractSourcePath(options: options))/ViewControllers/\(resource.pluralizedName)/\(resource.name)ViewController.swift", contents: rendered, options: options) 28 | } 29 | 30 | static func help() { 31 | print("Usage: hackman generate \(singleLineUsage())") 32 | print() 33 | print("Example:") 34 | print(" hackman generate view_controller_detail song title:string artist_name:string album_name:string") 35 | } 36 | 37 | static func singleLineUsage() -> String { 38 | return "view_controller_detail NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/Scaffold/Scaffold.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(Scaffold) 5 | class Scaffold: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | guard !arguments.isEmpty else { 10 | type(of: self).help() 11 | exit(0) 12 | } 13 | 14 | ReusableView().generate(arguments: arguments, options: options) 15 | Model().generate(arguments: arguments, options: options) 16 | ViewControllerDetail().generate(arguments: arguments, options: options) 17 | 18 | if options.contains("--view=table") { 19 | ViewControllerTable().generate(arguments: arguments, options: options) 20 | TableViewCell().generate(arguments: arguments, options: options) 21 | } else { 22 | ViewControllerCollection().generate(arguments: arguments, options: options) 23 | CollectionViewCell().generate(arguments: arguments, options: options) 24 | } 25 | 26 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 27 | if containsCoordinator { 28 | CoordinatorChild().generate(arguments: arguments, options: options) 29 | } 30 | } 31 | 32 | static func help() { 33 | print("Usage: hackman generate \(singleLineUsage())") 34 | print() 35 | print("Example:") 36 | print(" hackman generate scaffold song title:string artist_name:string album_name:string") 37 | } 38 | 39 | static func singleLineUsage() -> String { 40 | return "scaffold NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/CoordinatorMain/CoordinatorMain.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | import GrammaticalNumber 4 | import StringCase 5 | 6 | @objc(CoordinatorMain) 7 | class CoordinatorMain: NSObject, Generator { 8 | required override init() {} 9 | 10 | func generate(arguments: [String], options: [String]) { 11 | let resources = arguments.map { Resource(name: $0, properties: nil) } 12 | var children = resources.map { "\($0.pluralizedName)Coordinator(navigationController: UINavigationController())" } 13 | 14 | if let result = (options.filter { $0.contains("include") }).first?.split(separator: "=").last { 15 | for additionalControllerName in result.split(separator: ",") { 16 | let additionalControllerName = String(additionalControllerName).upperCamelCased() 17 | children.append("\(additionalControllerName)Coordinator(navigationController: UINavigationController())") 18 | } 19 | } 20 | 21 | let context: [String: Any] = [ 22 | "resources": resources, 23 | "children": children 24 | ] 25 | 26 | let rendered = try! environment.renderTemplate(name: "CoordinatorMain.stf", context: context) 27 | 28 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Coordinators/MainCoordinator.swift", contents: rendered, options: options) 29 | } 30 | 31 | static func help() { 32 | print("Usage: hackman generate \(singleLineUsage())") 33 | print() 34 | print("Example:") 35 | print(" hackman generate coordinator_main") 36 | } 37 | 38 | static func singleLineUsage() -> String { 39 | return "coordinator_main" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/DataSourceTableView/DataSourceTableView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | import GrammaticalNumber 4 | 5 | @objc(DataSourceTableView) 6 | class DataSourceTableView: NSObject, Generator { 7 | required override init() {} 8 | 9 | func generate(arguments: [String], options: [String]) { 10 | guard !arguments.isEmpty else { 11 | type(of: self).help() 12 | exit(0) 13 | } 14 | 15 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 16 | 17 | var mutableArguments = arguments 18 | let resourceName = mutableArguments.removeFirst() 19 | 20 | let resource = Resource( 21 | name: resourceName, 22 | properties: Property.createList(inputStrings: mutableArguments) 23 | ) 24 | let context: [String: Any] = [ 25 | "resource": resource, 26 | "coordinator": containsCoordinator 27 | ] 28 | 29 | let rendered = try! environment.renderTemplate(name: "DataSourceTableView.stf", context: context) 30 | Writer.createFile("\(Writer.extractSourcePath(options: options))/DataSources/\(resource.pluralizedName)/\(resource.pluralizedName)TableViewDataSource.swift", contents: rendered, options: options) 31 | } 32 | 33 | static func help() { 34 | print("Usage: hackman generate \(singleLineUsage())") 35 | print() 36 | print("Example:") 37 | print(" hackman generate data_source_table_view song title:string artist_name:string album_name:string") 38 | } 39 | 40 | static func singleLineUsage() -> String { 41 | return "data_source_table_view NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/DataSourceTableView/DataSourceTableView.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class {{ resource.pluralizedName }}DataSource: NSObject, UITableViewDataSource { 4 | {% if coordinator %} 5 | weak var coordinator: {{ resource.pluralizedName }}Coordinator? 6 | {% endif %} 7 | 8 | var {{ resource.pluralizedNameForFunction }}: [{{ resource.name }}] { 9 | return {{ resource.name }}.sampleData() 10 | } 11 | 12 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 13 | // Asks your data source object for the number of items in the specified section. 14 | return {{ resource.pluralizedNameForFunction }}.count 15 | } 16 | 17 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 18 | let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as {{ resource.name }}TableViewCell 19 | let {{ resource.nameForFunction }} = {{ resource.pluralizedNameForFunction }}[indexPath.item] 20 | {% for property in resource.properties %} 21 | cell.{{ property.name }}Label.text = String(describing: {{ resource.nameForFunction }}.{{ property.name }}) 22 | {% endfor %} 23 | return cell 24 | } 25 | } 26 | 27 | extension {{ resource.pluralizedName }}DataSource: UITableViewDelegate { 28 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 29 | // Tells the delegate that the item at the specified index path was selected. 30 | print("Selected item \(indexPath.item) in section \(indexPath.section)") 31 | {% if coordinator %} 32 | coordinator?.detail({{ resource.nameForFunction }}: {{ resource.pluralizedNameForFunction }}[indexPath.row]) 33 | {% endif %} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/Model/Model.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(Model) 5 | class Model: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | guard !arguments.isEmpty else { 10 | type(of: self).help() 11 | exit(0) 12 | } 13 | 14 | let strings = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven"] 15 | 16 | var mutableArguments = arguments 17 | let resource = Resource( 18 | name: mutableArguments.removeFirst(), 19 | properties: Property.createList(inputStrings: mutableArguments) 20 | ) 21 | 22 | let mocks = strings.map { (string) in 23 | return "\(resource.name)(\( (resource.properties ?? []).map { return "\($0.name): \($0.stringForMockContent(placeholder: string))" }.joined(separator: ", ") ))" 24 | } 25 | 26 | let context: [String: Any] = [ 27 | "resource": resource, 28 | "mocks": mocks 29 | ] 30 | 31 | let rendered = try! environment.renderTemplate(name: "Model.stf", context: context) 32 | 33 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Models/\(resource.name).swift", contents: rendered, options: options) 34 | } 35 | 36 | static func help() { 37 | print("Usage: hackman generate \(singleLineUsage())") 38 | print() 39 | print("Example:") 40 | print(" hackman generate model song title:string artist_name:string album_name:string") 41 | } 42 | 43 | static func singleLineUsage() -> String { 44 | return "model NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …." 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/DataSourceTableViewSearchResults/DataSourceTableViewSearchResults.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class {{ resource.pluralizedName }}SearchResultsDataSource: NSObject, UITableViewDataSource { 4 | {% if coordinator %} 5 | weak var coordinator: {{ resource.pluralizedName }}Coordinator? 6 | {% endif %} 7 | 8 | var filterString: String? 9 | 10 | var {{ resource.pluralizedNameForFunction }}: [{{ resource.name }}] { 11 | return {{ resource.name }}.sampleData() 12 | } 13 | 14 | var filtered: [{{ resource.name }}] { 15 | guard let filterString = filterString else { return [] } 16 | return {{ resource.pluralizedNameForFunction }}.filter { String(describing: $0).lowercased().contains(filterString) } 17 | } 18 | 19 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 20 | return filtered.count 21 | } 22 | 23 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 24 | let cell = UITableViewCell(style: UITableViewCell.CellStyle.subtitle, reuseIdentifier: nil) 25 | cell.textLabel?.text = filtered[indexPath.row].{{ resource.properties.first.name }} 26 | cell.detailTextLabel?.text = String(describing: filtered[indexPath.row]) 27 | cell.accessoryType = UITableViewCell.AccessoryType.disclosureIndicator 28 | return cell 29 | } 30 | } 31 | 32 | extension {{ resource.pluralizedName }}SearchResultsDataSource: UITableViewDelegate { 33 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 34 | {% if coordinator %} 35 | coordinator?.detail({{ resource.nameForFunction }}: {{ resource.pluralizedNameForFunction }}[indexPath.item]) 36 | {% endif %} 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/DataSourceTableViewSearchResults/DataSourceTableViewSearchResults.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | import GrammaticalNumber 4 | 5 | @objc(DataSourceTableViewSearchResults) 6 | class DataSourceTableViewSearchResults: NSObject, Generator { 7 | required override init() {} 8 | 9 | func generate(arguments: [String], options: [String]) { 10 | guard !arguments.isEmpty else { 11 | type(of: self).help() 12 | exit(0) 13 | } 14 | 15 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 16 | 17 | var mutableArguments = arguments 18 | let resourceName = mutableArguments.removeFirst() 19 | 20 | let resource = Resource( 21 | name: resourceName, 22 | properties: Property.createList(inputStrings: mutableArguments) 23 | ) 24 | let context: [String: Any] = [ 25 | "resource": resource, 26 | "coordinator": containsCoordinator 27 | ] 28 | 29 | let rendered = try! environment.renderTemplate(name: "DataSourceTableViewSearchResults.stf", context: context) 30 | Writer.createFile("\(Writer.extractSourcePath(options: options))/DataSources/\(resource.pluralizedName)/\(resource.pluralizedName)SearchResultsTableViewDataSource.swift", contents: rendered, options: options) 31 | } 32 | 33 | static func help() { 34 | print("Usage: hackman generate \(singleLineUsage())") 35 | print() 36 | print("Example:") 37 | print(" hackman generate data_source_table_view_search_results song title:string artist_name:string album_name:string") 38 | } 39 | 40 | static func singleLineUsage() -> String { 41 | return "data_source_table_view_search_results NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerCollection/ViewControllerCollection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(ViewControllerCollection) 5 | class ViewControllerCollection: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | guard !arguments.isEmpty else { 10 | type(of: self).help() 11 | exit(0) 12 | } 13 | 14 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 15 | var mutableArguments = arguments 16 | let resource = Resource( 17 | name: mutableArguments.removeFirst(), 18 | properties: Property.createList(inputStrings: mutableArguments) 19 | ) 20 | let context: [String: Any] = [ 21 | "resource": resource, 22 | "coordinator": containsCoordinator 23 | ] 24 | 25 | let rendered = try! environment.renderTemplate(name: "ViewControllerCollection.stf", context: context) 26 | Writer.createFile("\(Writer.extractSourcePath(options: options))/ViewControllers/\(resource.pluralizedName)/\(resource.pluralizedName)ViewController.swift", contents: rendered, options: options) 27 | 28 | SearchResultsController().generate(arguments: arguments, options: options) 29 | DataSourceTableViewSearchResults().generate(arguments: arguments, options: options) 30 | } 31 | 32 | static func help() { 33 | print("Usage: hackman generate \(singleLineUsage())") 34 | print() 35 | print("Example:") 36 | print(" hackman generate view_controller_collection song title:string artist_name:string album_name:string") 37 | } 38 | 39 | static func singleLineUsage() -> String { 40 | return "view_controller_collection NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewController/ViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | import GrammaticalNumber 4 | 5 | @objc(ViewController) 6 | class ViewController: NSObject, Generator { 7 | required override init() {} 8 | 9 | func generate(arguments: [String], options: [String]) { 10 | guard !arguments.isEmpty else { 11 | type(of: self).help() 12 | exit(0) 13 | } 14 | 15 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 16 | 17 | 18 | var mutableArguments = arguments 19 | let resource = Resource( 20 | name: mutableArguments.removeFirst(), 21 | properties: Property.createList(inputStrings: mutableArguments) 22 | ) 23 | let context: [String: Any] = [ 24 | "resource": resource, 25 | "coordinator": containsCoordinator 26 | ] 27 | 28 | let rendered = try! environment.renderTemplate(name: "ViewController.stf", context: context) 29 | 30 | Writer.createFile("\(Writer.extractSourcePath(options: options))/ViewControllers/\(resource.name)/\(resource.name)ViewController.swift", contents: rendered, options: options) 31 | 32 | if containsCoordinator { 33 | let rendered2 = try! environment.renderTemplate(name: "ChildCoordinator.stf", context: context) 34 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Coordinator/\(resource.name)Coordinator.swift", contents: rendered2, options: options) 35 | } 36 | } 37 | 38 | static func help() { 39 | print("Usage: hackman generate \(singleLineUsage())") 40 | print() 41 | print("Example:") 42 | print(" hackman generate view_controller song") 43 | } 44 | 45 | static func singleLineUsage() -> String { 46 | return "view_controller NAME" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerInformation/ViewControllerInformation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(ViewControllerInformation) 5 | class ViewControllerInformation: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 10 | 11 | let context: [String: Any] = [ 12 | "coordinator": containsCoordinator 13 | ] 14 | 15 | let rendered = try! environment.renderTemplate(name: "ViewControllerInformation.stf", context: context) 16 | Writer.createFile("\(Writer.extractSourcePath(options: options))/ViewControllers/InformationViewController.swift", contents: rendered, options: options) 17 | 18 | for page in ["DataProtection", "LegalNotices", "Licence", "Publisher"] { 19 | let renderedPage = try! environment.renderTemplate(name: "\(page).html") 20 | Writer.createFile("\(Writer.extractSourcePath(options: options))/StaticPages/\(page).html", contents: renderedPage, options: options) 21 | } 22 | 23 | if containsCoordinator { 24 | let rendered2 = try! environment.renderTemplate(name: "ChildCoordinator.stf") 25 | Writer.createFile("\(Writer.extractSourcePath(options: options))/Coordinators/InformationCoordinator.swift", contents: rendered2, options: options) 26 | } 27 | 28 | ViewControllerWeb().generate(arguments: arguments, options: options) 29 | } 30 | 31 | static func help() { 32 | print("Usage: hackman generate \(singleLineUsage())") 33 | print() 34 | print("Example:") 35 | print(" hackman generate view_controller_information") 36 | } 37 | 38 | static func singleLineUsage() -> String { 39 | return "view_controller_information" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerTable/ViewControllerTable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Stencil 3 | 4 | @objc(ViewControllerTable) 5 | class ViewControllerTable: NSObject, Generator { 6 | required override init() {} 7 | 8 | func generate(arguments: [String], options: [String]) { 9 | guard !arguments.isEmpty else { 10 | type(of: self).help() 11 | exit(0) 12 | } 13 | 14 | let containsCoordinator = options.contains("-c") || options.contains("--coordinator") 15 | var mutableArguments = arguments 16 | let resource = Resource( 17 | name: mutableArguments.removeFirst(), 18 | properties: Property.createList(inputStrings: mutableArguments) 19 | ) 20 | let context: [String: Any] = [ 21 | "resource": resource, 22 | "coordinator": containsCoordinator 23 | ] 24 | 25 | let rendered = try! environment.renderTemplate(name: "ViewControllerTable.stf", context: context) 26 | Writer.createFile("\(Writer.extractSourcePath(options: options))/ViewControllers/\(resource.pluralizedName)/\(resource.pluralizedName)ViewController.swift", contents: rendered, options: options) 27 | 28 | SearchResultsController().generate(arguments: arguments, options: options) 29 | DataSourceTableView().generate(arguments: arguments, options: options) 30 | DataSourceTableViewSearchResults().generate(arguments: arguments, options: options) 31 | } 32 | 33 | static func help() { 34 | print("Usage: hackman generate \(singleLineUsage())") 35 | print() 36 | print("Example:") 37 | print(" hackman generate view_controller_table song title:string artist_name:string album_name:string") 38 | } 39 | 40 | static func singleLineUsage() -> String { 41 | return "view_controller_table NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] …" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/AssetCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | # 51 | # Add this line if you want to avoid checking in source code from the Xcode workspace 52 | # *.xcworkspace 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # Accio dependency management 62 | Dependencies/ 63 | .accio/ 64 | 65 | # fastlane 66 | # 67 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 68 | # screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots/**/*.png 75 | fastlane/test_output 76 | 77 | # Code Injection 78 | # 79 | # After new code Injection tools there's a generated folder /iOSInjectionProject 80 | # https://github.com/johnno1962/injectionforxcode 81 | 82 | iOSInjectionProject/ 83 | .DS_Store 84 | .swiftpm 85 | -------------------------------------------------------------------------------- /Sources/HackManLib/Model/Property.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Devran Uenal on 04.06.19. 6 | // 7 | 8 | import Foundation 9 | import StringCase 10 | 11 | struct Property: Equatable { 12 | public let name: String 13 | public var upperCamelCasedName: String 14 | public let valueType: String 15 | 16 | public var isCustomResource: Bool { 17 | return !Property.basicTypes.contains(valueType) 18 | } 19 | 20 | static var basicTypes = [ 21 | "Bool", 22 | "Date", 23 | "Double", 24 | "Float", 25 | "Int", 26 | "String", 27 | ] 28 | 29 | private static var defaultValueType = "String" 30 | 31 | init(name: String, valueType: String?) { 32 | self.name = name.lowerCamelCased() 33 | self.upperCamelCasedName = self.name.upperCamelCased() 34 | 35 | self.valueType = valueType?.upperCamelCased() ?? Property.defaultValueType 36 | } 37 | 38 | init?(input: String) { 39 | let splitStrings = input.split(separator: ":").map({ String($0) }) 40 | guard let name = splitStrings.first else { return nil } 41 | self.init(name: name, valueType: splitStrings.count > 1 ? splitStrings[1] : nil) 42 | } 43 | 44 | static func createList(inputStrings: [String]) -> [Property] { 45 | return inputStrings.compactMap { Property(input: $0) } 46 | } 47 | } 48 | 49 | extension Property { 50 | func stringForMockContent(placeholder: String?) -> String { 51 | switch valueType { 52 | case "Bool": 53 | return Int.random(in: 0...1) == 1 ? "true" : "false" 54 | case "Date": 55 | return "Date()" 56 | case "Double": 57 | return String(Double.random(in: 1..<100)) 58 | case "Float": 59 | return String(Float.random(in: 1..<100)) 60 | case "Int": 61 | return String(Int.random(in: 1..<100)) 62 | case "String": 63 | guard let placeholder = placeholder else { 64 | return "String" 65 | } 66 | return "\"\(placeholder)\"" 67 | default: 68 | return "\(valueType).sampleData().first!" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/HackManTests/HackManTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HackManLib 3 | 4 | final class HackManTests: XCTestCase { 5 | func testSnakeCasedInput() throws { 6 | let propertiesString = "name:string uuid weight:double is_enabled:bool artist:artist created_at:date updated_at:date".split(separator: " ").map { String($0) } 7 | let properties = Property.createList(inputStrings: propertiesString) 8 | 9 | XCTAssertEqual(properties.count, 7) 10 | 11 | let verification = [ 12 | Property(name: "name", valueType: "String"), 13 | Property(name: "uuid", valueType: "String"), 14 | Property(name: "weight", valueType: "Double"), 15 | Property(name: "isEnabled", valueType: "Bool"), 16 | Property(name: "artist", valueType: "Artist"), 17 | Property(name: "createdAt", valueType: "Date"), 18 | Property(name: "updatedAt", valueType: "Date") 19 | ] 20 | 21 | XCTAssertEqual(properties, verification) 22 | } 23 | 24 | func testCamelCasedInput() throws { 25 | let propertiesString = "name:String uuid weight:Double isEnabled:Bool artist:Artist createdAt:Date updatedAt:Date".split(separator: " ").map { String($0) } 26 | let properties = Property.createList(inputStrings: propertiesString) 27 | 28 | XCTAssertEqual(properties.count, 7) 29 | 30 | let verification = [ 31 | Property(name: "name", valueType: "String"), 32 | Property(name: "uuid", valueType: "String"), 33 | Property(name: "weight", valueType: "Double"), 34 | Property(name: "isEnabled", valueType: "Bool"), 35 | Property(name: "artist", valueType: "Artist"), 36 | Property(name: "createdAt", valueType: "Date"), 37 | Property(name: "updatedAt", valueType: "Date") 38 | ] 39 | 40 | XCTAssertEqual(properties, verification) 41 | } 42 | 43 | /// Returns path to the built products directory. 44 | var productsDirectory: URL { 45 | #if os(macOS) 46 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 47 | return bundle.bundleURL.deletingLastPathComponent() 48 | } 49 | fatalError("couldn't find the products directory") 50 | #else 51 | return Bundle.main.bundleURL 52 | #endif 53 | } 54 | 55 | static var allTests = [ 56 | ("testSnakeCasedInput", testSnakeCasedInput), 57 | ("testCamelCasedInput", testCamelCasedInput), 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/AppDelegate/AppDelegate.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 8 | // Override point for customization after application launch. 9 | window = UIWindow(frame: UIScreen.main.bounds) 10 | 11 | {% if coordinator %} 12 | let coordinator = MainCoordinator(tabBarController: UITabBarController()) 13 | coordinator.start() 14 | 15 | window?.rootViewController = coordinator.containerController 16 | {% else %} 17 | window?.rootViewController = UINavigationController(rootViewController: UIViewController()) 18 | {% endif %} 19 | window?.makeKeyAndVisible() 20 | 21 | return true 22 | } 23 | 24 | func applicationWillResignActive(_ application: UIApplication) { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | func applicationDidEnterBackground(_ application: UIApplication) { 30 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 31 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 32 | } 33 | 34 | func applicationWillEnterForeground(_ application: UIApplication) { 35 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 36 | } 37 | 38 | func applicationDidBecomeActive(_ application: UIApplication) { 39 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 40 | } 41 | 42 | func applicationWillTerminate(_ application: UIApplication) { 43 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerWeb/ViewControllerWeb.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import WebKit 3 | import SafariServices 4 | 5 | class WebViewController: UIViewController { 6 | {% if coordinator %} 7 | weak var coordinator: InformationCoordinator? 8 | {% endif %} 9 | var url: URL? 10 | 11 | lazy var webView: WKWebView = { 12 | let source = """ 13 | var meta = document.createElement('meta'); 14 | meta.name = "viewport"; 15 | meta.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"; 16 | document.head.appendChild(meta); 17 | """ 18 | 19 | let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) 20 | let userContentController: WKUserContentController = WKUserContentController() 21 | userContentController.addUserScript(script) 22 | 23 | let configuration: WKWebViewConfiguration = WKWebViewConfiguration() 24 | configuration.userContentController = userContentController 25 | 26 | let webView = WKWebView(frame: CGRect.zero, configuration: configuration) 27 | webView.navigationDelegate = self 28 | if let url = url { 29 | webView.load(URLRequest(url: url)) 30 | } 31 | return webView 32 | }() 33 | 34 | convenience init(url: URL) { 35 | self.init(nibName: nil, bundle: nil, url: url) 36 | } 37 | 38 | init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?, url: URL) { 39 | self.url = url 40 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 41 | commonInit() 42 | } 43 | 44 | required init?(coder: NSCoder) { 45 | super.init(coder: coder) 46 | commonInit() 47 | } 48 | 49 | func commonInit() { 50 | } 51 | 52 | override func viewDidLoad() { 53 | super.viewDidLoad() 54 | 55 | view.backgroundColor = UIColor.white 56 | 57 | setupConstraints() 58 | } 59 | 60 | override func loadView() { 61 | view = webView 62 | } 63 | 64 | func setupConstraints() { 65 | 66 | } 67 | } 68 | 69 | extension WebViewController: WKNavigationDelegate { 70 | func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 71 | if let url = navigationAction.request.url, navigationAction.navigationType == .linkActivated { 72 | if url.absoluteString.hasPrefix("http") { 73 | let viewController = SFSafariViewController(url: url) 74 | present(viewController, animated: true) 75 | } else { 76 | UIApplication.shared.open(url) 77 | } 78 | decisionHandler(.cancel) 79 | } else { 80 | decisionHandler(.allow) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/TableViewCell/TableViewCell.stf: -------------------------------------------------------------------------------- 1 | // 2 | // {{ resource.name }}TableViewCell.swift 3 | // 4 | 5 | import UIKit 6 | 7 | class {{ resource.name }}TableViewCell: UITableViewCell { 8 | var stackView: UIStackView = { 9 | let stackView = UIStackView() 10 | stackView.axis = NSLayoutConstraint.Axis.vertical 11 | return stackView 12 | }() 13 | 14 | {% for property in resource.properties %} 15 | lazy var {{ property.name }}Label: UILabel = { 16 | let label = UILabel() 17 | {% if forloop.counter == 0 %} 18 | label.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.headline) 19 | {% elif forloop.counter == 1 %} 20 | label.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.subheadline) 21 | {% else %} 22 | label.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body) 23 | {% endif %} 24 | label.numberOfLines = 0 25 | return label 26 | }() 27 | {% endfor %} 28 | 29 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 30 | super.init(style: style, reuseIdentifier: reuseIdentifier) 31 | commonInit() 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | commonInit() 37 | } 38 | 39 | func commonInit() { 40 | preservesSuperviewLayoutMargins = true 41 | contentView.preservesSuperviewLayoutMargins = true 42 | 43 | contentView.backgroundColor = UIColor.white 44 | 45 | {% for property in resource.properties %} 46 | stackView.addArrangedSubview({{ property.name }}Label) 47 | {% endfor %} 48 | 49 | contentView.addSubview(stackView) 50 | 51 | setupConstraints() 52 | } 53 | 54 | func setupConstraints() { 55 | // This fixes an iOS 12 Bug 56 | // https://stackoverflow.com/a/52394336/2494219 57 | contentView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 58 | contentView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 59 | contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true 60 | contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 61 | 62 | stackView.translatesAutoresizingMaskIntoConstraints = false 63 | stackView.layoutMarginsGuide.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor).isActive = true 64 | stackView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true 65 | contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: stackView.layoutMarginsGuide.bottomAnchor).isActive = true 66 | contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: stackView.layoutMarginsGuide.trailingAnchor).isActive = true 67 | } 68 | } 69 | 70 | extension {{ resource.name }}TableViewCell: ReusableView {} 71 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/CollectionViewCell/CollectionViewCell.stf: -------------------------------------------------------------------------------- 1 | // 2 | // {{ resource.name }}CollectionViewCell.swift 3 | // 4 | 5 | import UIKit 6 | 7 | class {{ resource.name }}CollectionViewCell: UICollectionViewCell { 8 | var stackView: UIStackView = { 9 | let stackView = UIStackView() 10 | stackView.axis = NSLayoutConstraint.Axis.vertical 11 | return stackView 12 | }() 13 | 14 | {% for property in resource.properties %} 15 | lazy var {{ property.name }}Label: UILabel = { 16 | let label = UILabel() 17 | {% if forloop.counter == 0 %} 18 | label.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.headline) 19 | {% elif forloop.counter == 1 %} 20 | label.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.subheadline) 21 | {% else %} 22 | label.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body) 23 | {% endif %} 24 | label.numberOfLines = 0 25 | return label 26 | }() 27 | {% endfor %} 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | commonInit() 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | commonInit() 37 | } 38 | 39 | func commonInit() { 40 | preservesSuperviewLayoutMargins = true 41 | contentView.preservesSuperviewLayoutMargins = true 42 | 43 | contentView.backgroundColor = UIColor.lightGray 44 | 45 | {% for property in resource.properties %} 46 | stackView.addArrangedSubview({{ property.name }}Label) 47 | {% endfor %} 48 | 49 | contentView.addSubview(stackView) 50 | 51 | setupConstraints() 52 | } 53 | 54 | func setupConstraints() { 55 | // This fixes an iOS 12 Bug 56 | // https://stackoverflow.com/a/52394336/2494219 57 | contentView.translatesAutoresizingMaskIntoConstraints = false 58 | contentView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 59 | contentView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 60 | contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true 61 | contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 62 | 63 | stackView.translatesAutoresizingMaskIntoConstraints = false 64 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true 65 | stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true 66 | contentView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true 67 | contentView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true 68 | } 69 | 70 | override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { 71 | self.updateConstraintsIfNeeded() 72 | 73 | let attributes = super.preferredLayoutAttributesFitting(layoutAttributes) 74 | var frame = attributes.frame 75 | frame.size.width = layoutAttributes.size.width 76 | attributes.frame = frame 77 | return attributes 78 | } 79 | } 80 | 81 | extension {{ resource.name }}CollectionViewCell: ReusableView {} 82 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at me@devranuenal.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerDetail/ViewControllerDetail.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class {{ resource.name }}ViewController: UIViewController { 4 | {% if coordinator %} 5 | weak var coordinator: {{ resource.pluralizedName }}Coordinator? 6 | {% endif %} 7 | var {{ resource.nameForFunction }}: {{ resource.name }} 8 | 9 | var stackView: UIStackView = { 10 | let stackView = UIStackView() 11 | stackView.axis = NSLayoutConstraint.Axis.vertical 12 | return stackView 13 | }() 14 | 15 | {% for property in resource.properties %} 16 | 17 | {% if property.isCustomResource %} 18 | lazy var {{ property.name }}Button: UIButton = { 19 | let button = UIButton(type: UIButton.ButtonType.system) 20 | button.contentHorizontalAlignment = .left 21 | return button 22 | }() 23 | {% else %} 24 | lazy var {{ property.name }}Label: UILabel = { 25 | let label = UILabel() 26 | return label 27 | }() 28 | {% endif %} 29 | 30 | {% endfor %} 31 | 32 | 33 | convenience init({{ resource.nameForFunction }}: {{ resource.name }}) { 34 | self.init(nibName: nil, bundle: nil, {{ resource.nameForFunction }}: {{ resource.nameForFunction }}) 35 | } 36 | 37 | init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?, {{ resource.nameForFunction }}: {{ resource.name }}) { 38 | self.{{ resource.nameForFunction }} = {{ resource.nameForFunction }} 39 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 40 | commonInit() 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | func commonInit() { 48 | title = NSLocalizedString("{{ resource.name }}", comment: "{{ resource.pluralizedName }} Title (Singular)") 49 | } 50 | 51 | override func viewDidLoad() { 52 | super.viewDidLoad() 53 | 54 | navigationController?.navigationBar.prefersLargeTitles = true 55 | 56 | view.backgroundColor = UIColor.white 57 | 58 | {% for property in resource.properties %} 59 | {% if property.isCustomResource %} 60 | {{ property.name }}Button.addTarget(self, action: #selector({{ property.name }}ButtonAction(sender:)), for: UIControl.Event.touchUpInside) 61 | {{ property.name }}Button.setTitle(String(describing: {{ resource.nameForFunction }}.{{ property.name }}), for: UIControl.State.normal) 62 | stackView.addArrangedSubview({{ property.name }}Button) 63 | {% else %} 64 | {{ property.name }}Label.text = String(describing: {{ resource.nameForFunction }}.{{ property.name }}) 65 | stackView.addArrangedSubview({{ property.name }}Label) 66 | {% endif %} 67 | {% endfor %} 68 | 69 | view.addSubview(stackView) 70 | 71 | setupConstraints() 72 | } 73 | 74 | func setupConstraints() { 75 | stackView.translatesAutoresizingMaskIntoConstraints = false 76 | stackView.layoutMarginsGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true 77 | stackView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor).isActive = true 78 | view.layoutMarginsGuide.trailingAnchor.constraint(equalTo: stackView.layoutMarginsGuide.trailingAnchor).isActive = true 79 | } 80 | } 81 | 82 | {% for property in resource.properties %} 83 | {% if property.isCustomResource %} 84 | extension {{ resource.name }}ViewController { 85 | @objc func {{ property.name }}ButtonAction(sender: UIButton) { 86 | {% if coordinator %} 87 | (coordinator?.parent as? MainCoordinator)?.select{{ property.upperCamelCasedName }}({{ property.name }}: {{ resource.nameForFunction }}.{{ property.name }}) 88 | {% endif %} 89 | } 90 | } 91 | {% endif %} 92 | {% endfor %} 93 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/NewProject/gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/xcode,macos,swift,carthage,cocoapods 3 | # Edit at https://www.gitignore.io/?templates=xcode,macos,swift,carthage,cocoapods 4 | 5 | ### Carthage ### 6 | # Carthage 7 | # 8 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 9 | # Carthage/Checkouts 10 | 11 | Carthage/Build 12 | 13 | ### CocoaPods ### 14 | ## CocoaPods GitIgnore Template 15 | 16 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 17 | # - Also handy if you have a large number of dependant pods 18 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 19 | Pods/ 20 | 21 | ### macOS ### 22 | # General 23 | .DS_Store 24 | .AppleDouble 25 | .LSOverride 26 | 27 | # Icon must end with two \r 28 | Icon 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Swift ### 50 | # Xcode 51 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 52 | 53 | ## Build generated 54 | build/ 55 | DerivedData/ 56 | 57 | ## Various settings 58 | *.pbxuser 59 | !default.pbxuser 60 | *.mode1v3 61 | !default.mode1v3 62 | *.mode2v3 63 | !default.mode2v3 64 | *.perspectivev3 65 | !default.perspectivev3 66 | xcuserdata/ 67 | 68 | ## Other 69 | *.moved-aside 70 | *.xccheckout 71 | *.xcscmblueprint 72 | 73 | ## Obj-C/Swift specific 74 | *.hmap 75 | *.ipa 76 | *.dSYM.zip 77 | *.dSYM 78 | 79 | ## Playgrounds 80 | timeline.xctimeline 81 | playground.xcworkspace 82 | 83 | # Swift Package Manager 84 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 85 | # Packages/ 86 | # Package.pins 87 | # Package.resolved 88 | .build/ 89 | 90 | # CocoaPods 91 | # We recommend against adding the Pods directory to your .gitignore. However 92 | # you should judge for yourself, the pros and cons are mentioned at: 93 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 94 | # Pods/ 95 | # Add this line if you want to avoid checking in source code from the Xcode workspace 96 | # *.xcworkspace 97 | 98 | # Carthage 99 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 100 | # Carthage/Checkouts 101 | 102 | 103 | # fastlane 104 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 105 | # screenshots whenever they are needed. 106 | # For more information about the recommended setup visit: 107 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 108 | 109 | fastlane/report.xml 110 | fastlane/Preview.html 111 | fastlane/screenshots/**/*.png 112 | fastlane/test_output 113 | 114 | # Code Injection 115 | # After new code Injection tools there's a generated folder /iOSInjectionProject 116 | # https://github.com/johnno1962/injectionforxcode 117 | 118 | iOSInjectionProject/ 119 | 120 | ### Xcode ### 121 | # Xcode 122 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 123 | 124 | ## User settings 125 | 126 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 127 | 128 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 129 | 130 | ### Xcode Patch ### 131 | *.xcodeproj/* 132 | !*.xcodeproj/project.pbxproj 133 | !*.xcodeproj/xcshareddata/ 134 | !*.xcworkspace/contents.xcworkspacedata 135 | /*.gcno 136 | **/xcshareddata/WorkspaceSettings.xcsettings 137 | 138 | # End of https://www.gitignore.io/api/xcode,macos,swift,carthage,cocoapods 139 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerInformation/ViewControllerInformation.stf: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class InformationViewController: UIViewController { 4 | {% if coordinator %} 5 | weak var coordinator: InformationCoordinator? 6 | {% endif %} 7 | 8 | lazy var tableView: UITableView = { 9 | let tableView = UITableView(frame: CGRect.zero, style: UITableView.Style.grouped) 10 | tableView.delegate = self 11 | tableView.dataSource = self 12 | return tableView 13 | }() 14 | 15 | convenience init() { 16 | self.init(nibName: nil, bundle: nil) 17 | } 18 | 19 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 20 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 21 | commonInit() 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | super.init(coder: coder) 26 | commonInit() 27 | } 28 | 29 | func commonInit() { 30 | title = NSLocalizedString("Information", comment: "Information Title") 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | navigationController?.navigationBar.prefersLargeTitles = true 36 | 37 | view.addSubview(tableView) 38 | 39 | setupConstraints() 40 | } 41 | 42 | override func viewWillAppear(_ animated: Bool) { 43 | if let index = self.tableView.indexPathForSelectedRow { 44 | self.tableView.deselectRow(at: index, animated: true) 45 | } 46 | } 47 | 48 | private func setupConstraints() { 49 | tableView.translatesAutoresizingMaskIntoConstraints = false 50 | tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 51 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 52 | view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor).isActive = true 53 | view.bottomAnchor.constraint(equalTo: tableView.bottomAnchor).isActive = true 54 | } 55 | } 56 | 57 | extension InformationViewController { 58 | func item(for indexPath: IndexPath) -> Item { 59 | return contents[indexPath.section].data[indexPath.item] 60 | } 61 | } 62 | 63 | extension InformationViewController: UITableViewDataSource { 64 | struct Section { 65 | var name: String? 66 | var data: [Item] 67 | } 68 | 69 | struct Item { 70 | var name: String 71 | var url: URL? 72 | } 73 | 74 | var contents: [Section] { 75 | return [ 76 | Section( 77 | name: nil, 78 | data: [ 79 | Item(name: NSLocalizedString("Licence", comment: ""), url: Bundle.main.url(forResource: "Licence", withExtension: "html")), 80 | Item(name: NSLocalizedString("Data Protection", comment: ""), url: Bundle.main.url(forResource: "DataProtection", withExtension: "html")), 81 | Item(name: NSLocalizedString("Publisher", comment: "German 'Impressum' by Law"), url: Bundle.main.url(forResource: "Publisher", withExtension: "html")) 82 | ] 83 | ), 84 | Section( 85 | name: nil, 86 | data: [ 87 | Item(name: NSLocalizedString("Legal Notices", comment: "3rd Party Licences"), url: Bundle.main.url(forResource: "LegalNotices", withExtension: "html")) 88 | ] 89 | ) 90 | ] 91 | } 92 | 93 | func numberOfSections(in tableView: UITableView) -> Int { 94 | return contents.count 95 | } 96 | 97 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 98 | return contents[section].data.count 99 | } 100 | 101 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 102 | let information = item(for: indexPath) 103 | let cell = UITableViewCell() 104 | cell.textLabel?.text = information.name 105 | cell.accessoryType = .disclosureIndicator 106 | return cell 107 | } 108 | } 109 | 110 | extension InformationViewController: UITableViewDelegate { 111 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 112 | {% if coordinator %} 113 | coordinator?.detail(item: item(for: indexPath)) 114 | {% endif %} 115 | } 116 | 117 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 118 | return contents[section].name 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/HackManLib/CommandLineRunner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Devran on 15.08.19. 6 | // 7 | 8 | import Foundation 9 | import StringCase 10 | 11 | public struct CommandLineRunner { 12 | public enum Command { 13 | case new 14 | case generate 15 | case help 16 | case unknown(name: String) 17 | 18 | init(name: String) { 19 | switch name.lowercased() { 20 | case "new": self = .new 21 | case "generate", "g": self = .generate 22 | case "help": self = .help 23 | default: self = .unknown(name: name) 24 | } 25 | } 26 | } 27 | 28 | public init(arguments: [String], options: [String]) throws { 29 | var arguments = arguments 30 | let options = options 31 | 32 | // No need for executable name 33 | arguments.removeFirst() 34 | 35 | guard !arguments.isEmpty else { 36 | throw CommandError.noCommand 37 | } 38 | 39 | let command = Command(name: arguments.removeFirst().lowercased()) 40 | 41 | if options.contains("--verbose") { 42 | print("Command: \(command)") 43 | print("Arguments: \(arguments)") 44 | print("Options: \(options)") 45 | print() 46 | } 47 | 48 | switch command { 49 | case .new: 50 | NewProject().generate(arguments: arguments, options: options) 51 | Writer.finish() 52 | case .generate: 53 | guard !arguments.isEmpty else { throw GeneratorCommandError.noGenerator } 54 | let generatorName = arguments.removeFirst().upperCamelCased() 55 | guard let generator = NSClassFromString(generatorName) as? Generator.Type else { 56 | throw GeneratorCommandError.unknownGenerator 57 | } 58 | 59 | if options.contains("-h") || options.contains("--help") { 60 | generator.help() 61 | } else { 62 | generator.init().generate(arguments: arguments, options: options) 63 | } 64 | 65 | Writer.finish() 66 | case .help: 67 | print("Find help on: https://github.com/Cosmo/HackMan") 68 | case .unknown(let name): 69 | throw CommandError.unknownCommand(name: name) 70 | } 71 | } 72 | } 73 | 74 | extension CommandLineRunner { 75 | public enum CommandError: Error { 76 | case noCommand 77 | case unknownCommand(name: String) 78 | } 79 | 80 | public static func printCommandUsage() { 81 | print("Usage: hackman COMMAND") 82 | print() 83 | print("Commands:") 84 | print(" new APP_NAME") 85 | print(" generate") 86 | print() 87 | print("Example:") 88 | print(" hackman new AwesomeApp") 89 | print(" hackman generate") 90 | } 91 | } 92 | 93 | extension CommandLineRunner { 94 | public enum GeneratorCommandError: Error { 95 | case noGenerator 96 | case unknownGenerator 97 | } 98 | 99 | public static func printGeneratorUsage() { 100 | let generators: [Generator.Type] = [ 101 | AppDelegate.self, 102 | AssetCatalog.self, 103 | CollectionViewCell.self, 104 | Coordinator.self, 105 | CoordinatorChild.self, 106 | CoordinatorMain.self, 107 | DataSourceTableView.self, 108 | DataSourceTableViewSearchResults.self, 109 | LaunchScreen.self, 110 | Model.self, 111 | ReusableView.self, 112 | Scaffold.self, 113 | SearchResultsController.self, 114 | TableViewCell.self, 115 | ViewController.self, 116 | ViewControllerCollection.self, 117 | ViewControllerDetail.self, 118 | ViewControllerInformation.self, 119 | ViewControllerTable.self, 120 | ViewControllerWeb.self, 121 | ] 122 | 123 | print("Usage: hackman generate GENERATOR") 124 | print() 125 | print("Generators:") 126 | for generator in generators { 127 | print(" \(generator.singleLineUsage())") 128 | } 129 | print() 130 | print("Example:") 131 | print(" hackman generate scaffold song title:string artist_name:string album_name:string") 132 | print() 133 | print("Make sure to run generators inside of your project directory.") 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerInformation/Licence.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Licence

7 |

Subtitle

8 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

9 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

10 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

11 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

12 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

13 |

Subtitle

14 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

15 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

16 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

17 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

18 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/HackManLib/Writer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Devran on 15.08.19. 6 | // 7 | 8 | import Foundation 9 | import PathKit 10 | 11 | public protocol Writeable { 12 | var path: String { get } 13 | func create() 14 | } 15 | 16 | public struct Writer { 17 | static var creatables: [Writeable] = [] 18 | 19 | public static func extractSourcePath(options: [String]) -> String { 20 | if let result = (options.filter { $0.contains("--source") }).first?.split(separator: "=").last { 21 | return String(result) 22 | } else { 23 | return "Source" 24 | } 25 | } 26 | 27 | public struct Directory: Writeable, Hashable { 28 | public var path: String 29 | public func create() { 30 | guard let directoryName = path.split(separator: "/").last else { fatalError() } 31 | 32 | let depth = path.split(separator: "/").count 33 | let indention = String(repeating: " ", count: depth) 34 | 35 | if Path(path).exists { 36 | print(" \("invoke".colored(.white)) \(indention)\(directoryName)") 37 | } else { 38 | try? FileManager().createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 39 | print(" \("create".colored(.green)) \(indention)\(directoryName)") 40 | } 41 | } 42 | } 43 | 44 | public struct File: Writeable, Hashable { 45 | public var path: String 46 | var contents: String 47 | var isForced: Bool = false 48 | 49 | public func create() { 50 | let paths = path.split(separator: "/") 51 | guard let fileName = paths.last else { return } 52 | let indention = String(repeating: " ", count: paths.count) 53 | var isAllowedToWrite = false 54 | var isConflictPresent = false 55 | 56 | if Path(path).exists { 57 | if let oldContents: String = try? Path(path).read(String.Encoding.utf8), oldContents == contents { 58 | print(" \("identical".colored(.green)) \(indention)\(fileName)") 59 | } else if isForced { 60 | print(" \("forced".colored(.red)) \(indention)\(fileName)") 61 | isAllowedToWrite = true 62 | } else { 63 | print(" \("conflict".colored(.red)) \(indention)\(fileName)") 64 | isConflictPresent = true 65 | } 66 | } else { 67 | print(" \("create".colored(.green)) \(indention)\(fileName)") 68 | isAllowedToWrite = true 69 | } 70 | 71 | if isConflictPresent { 72 | print("Overwrite \(fileName)? Enter [y/n]: ", terminator: "") 73 | if readLine() == "y" { 74 | print(" \("overwrite".colored(.yellow)) \(indention)\(fileName)") 75 | isAllowedToWrite = true 76 | } 77 | } 78 | 79 | guard isAllowedToWrite else { return } 80 | try? contents.write(toFile: path, atomically: true, encoding: String.Encoding.utf8) 81 | } 82 | } 83 | 84 | public static func basePath(of filePath: String) -> String { 85 | var directories = filePath.split(separator: "/") 86 | directories.removeLast() 87 | return directories.joined(separator: "/") 88 | } 89 | 90 | public static func createPath(_ path: String) { 91 | var previousPath: String? 92 | for path in path.split(separator: "/") { 93 | let nextPath = [previousPath, String(path)].compactMap { $0 }.joined(separator: "/") 94 | 95 | let directory = Directory(path: nextPath) 96 | if !creatables.contains(where: { $0.path == directory.path }) { 97 | creatables.append(directory) 98 | } 99 | 100 | previousPath = nextPath 101 | } 102 | } 103 | 104 | public static func createFile(_ path: String, contents: String, options: [String] = []) { 105 | createPath(basePath(of: path)) 106 | 107 | let isForced = options.contains("--force") || options.contains("-f") 108 | 109 | creatables.append(File(path: path, contents: contents, isForced: isForced)) 110 | } 111 | 112 | public static func finish() { 113 | for creatable in creatables.sorted(by: { $0.path < $1.path }) { 114 | creatable.create() 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerInformation/Publisher.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Publisher

7 |

Subtitle

8 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

9 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

10 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

11 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

12 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

13 |

Subtitle

14 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

15 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

16 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

17 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

18 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerInformation/DataProtection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Data Protection

7 |

Subtitle

8 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

9 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

10 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

11 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

12 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

13 |

Subtitle

14 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

15 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

16 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

17 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

18 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerInformation/LegalNotices.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Legal Notices

7 |

Subtitle

8 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

9 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

10 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

11 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

12 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

13 |

Subtitle

14 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

15 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

16 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

17 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

18 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerTable/ViewControllerTable.stf: -------------------------------------------------------------------------------- 1 | // 2 | // {{ resource.pluralizedName }}ViewController.swift 3 | // 4 | 5 | import UIKit 6 | 7 | class {{ resource.pluralizedName }}ViewController: UIViewController { 8 | {% if coordinator %} 9 | weak var coordinator: {{ resource.pluralizedName }}Coordinator? 10 | {% endif %} 11 | 12 | let searchResultsData = {{ resource.pluralizedName }}SearchResultsDataSource() 13 | let data = {{ resource.pluralizedName }}DataSource() 14 | 15 | lazy var searchController: UISearchController = { 16 | let searchResultsController = {{ resource.name }}SearchResultsViewController() 17 | self.searchResultsData.coordinator = coordinator 18 | searchResultsController.tableView.dataSource = self.searchResultsData 19 | searchResultsController.tableView.delegate = self.searchResultsData 20 | let searchController = UISearchController(searchResultsController: searchResultsController) 21 | // searchController.searchBar.scopeButtonTitles = ["All", "Option 1", "Option 2", "Other"] 22 | searchController.searchResultsUpdater = self 23 | searchController.obscuresBackgroundDuringPresentation = true 24 | searchController.hidesNavigationBarDuringPresentation = true 25 | definesPresentationContext = true 26 | return searchController 27 | }() 28 | 29 | lazy var tableView: UITableView = { 30 | let tableView = UITableView(frame: CGRect.zero) 31 | tableView.backgroundColor = UIColor.clear 32 | tableView.delegate = self.data 33 | tableView.dataSource = self.data 34 | tableView.register({{ resource.name }}TableViewCell.self) 35 | tableView.refreshControl = refreshControl 36 | tableView.rowHeight = UITableView.automaticDimension 37 | tableView.estimatedRowHeight = 44 38 | return tableView 39 | }() 40 | 41 | lazy var refreshControl: UIRefreshControl = { 42 | let refreshControl = UIRefreshControl() 43 | let refreshTitle = NSLocalizedString("Refreshing {{ resource.pluralizedName }} …", comment: "{{ resource.pluralizedName }} Refresh Text") 44 | refreshControl.attributedTitle = NSAttributedString(string: refreshTitle) 45 | refreshControl.addTarget(self, action: #selector(refresh{{ resource.pluralizedName }}(sender:)), for: UIControl.Event.valueChanged) 46 | return refreshControl 47 | }() 48 | 49 | convenience init() { 50 | self.init(nibName: nil, bundle: nil) 51 | } 52 | 53 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 54 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 55 | commonInit() 56 | } 57 | 58 | required init?(coder: NSCoder) { 59 | super.init(coder: coder) 60 | commonInit() 61 | } 62 | 63 | func commonInit() { 64 | title = NSLocalizedString("{{ resource.pluralizedName }}", comment: "{{ resource.pluralizedName }} Title (Plural)") 65 | } 66 | 67 | override func viewDidLoad() { 68 | super.viewDidLoad() 69 | 70 | navigationController?.navigationBar.prefersLargeTitles = true 71 | navigationItem.searchController = searchController 72 | navigationItem.hidesSearchBarWhenScrolling = false 73 | 74 | {% if coordinator %} 75 | data.coordinator = coordinator 76 | {% endif %} 77 | 78 | view.backgroundColor = UIColor.white 79 | 80 | view.addSubview(tableView) 81 | 82 | setupConstraints() 83 | } 84 | 85 | override func viewWillAppear(_ animated: Bool) { 86 | super.viewWillAppear(animated) 87 | 88 | if let indexPath = tableView.indexPathForSelectedRow { 89 | transitionCoordinator?.animate(alongsideTransition: { context in 90 | self.tableView.deselectRow(at: indexPath, animated: true) 91 | }, completion: { context in 92 | if context.isCancelled { 93 | self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func setupConstraints() { 100 | tableView.translatesAutoresizingMaskIntoConstraints = false 101 | tableView.safeAreaLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true 102 | tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true 103 | view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: tableView.trailingAnchor).isActive = true 104 | view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor).isActive = true 105 | } 106 | } 107 | 108 | extension {{ resource.pluralizedName }}ViewController { 109 | @objc func refresh{{ resource.pluralizedName }}(sender: UIRefreshControl) { 110 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: { 111 | sender.endRefreshing() 112 | }) 113 | } 114 | } 115 | 116 | extension {{ resource.pluralizedName }}ViewController: UISearchResultsUpdating { 117 | func updateSearchResults(for searchController: UISearchController) { 118 | // Called when the search bar's text or scope has changed or when the search bar becomes first responder. 119 | searchResultsData.filterString = searchController.searchBar.text?.lowercased() 120 | (searchController.searchResultsController as? {{ resource.name }}SearchResultsViewController)?.tableView.reloadData() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/LaunchScreen/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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Sources/HackManLib/Generators/ViewControllerCollection/ViewControllerCollection.stf: -------------------------------------------------------------------------------- 1 | // 2 | // {{ resource.pluralizedName }}ViewController.swift 3 | // 4 | 5 | import UIKit 6 | 7 | class {{ resource.pluralizedName }}ViewController: UIViewController { 8 | {% if coordinator %} 9 | weak var coordinator: {{ resource.pluralizedName }}Coordinator? 10 | {% endif %} 11 | 12 | let searchResultsData = {{ resource.pluralizedName }}SearchResultsDataSource() 13 | 14 | lazy var searchController: UISearchController = { 15 | let searchResultsController = {{ resource.name }}SearchResultsViewController() 16 | self.searchResultsData.coordinator = coordinator 17 | searchResultsController.tableView.dataSource = self.searchResultsData 18 | searchResultsController.tableView.delegate = self.searchResultsData 19 | let searchController = UISearchController(searchResultsController: searchResultsController) 20 | // searchController.searchBar.scopeButtonTitles = ["All", "Option 1", "Option 2", "Other"] 21 | searchController.searchResultsUpdater = self 22 | searchController.obscuresBackgroundDuringPresentation = true 23 | searchController.hidesNavigationBarDuringPresentation = true 24 | definesPresentationContext = true 25 | return searchController 26 | }() 27 | 28 | var collectionViewLayout: UICollectionViewFlowLayout = { 29 | let collectionViewLayout = UICollectionViewFlowLayout() 30 | collectionViewLayout.minimumInteritemSpacing = 10 31 | collectionViewLayout.minimumLineSpacing = 10 32 | collectionViewLayout.itemSize = UICollectionViewFlowLayout.automaticSize 33 | collectionViewLayout.estimatedItemSize = CGSize(width: 180, height: 180) 34 | return collectionViewLayout 35 | }() 36 | 37 | lazy var collectionView: UICollectionView = { 38 | let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: collectionViewLayout) 39 | collectionView.backgroundColor = UIColor.clear 40 | collectionView.delegate = self 41 | collectionView.dataSource = self 42 | collectionView.register({{ resource.name }}CollectionViewCell.self) 43 | collectionView.preservesSuperviewLayoutMargins = true 44 | collectionView.alwaysBounceVertical = true 45 | collectionView.refreshControl = refreshControl 46 | return collectionView 47 | }() 48 | 49 | lazy var refreshControl: UIRefreshControl = { 50 | let refreshControl = UIRefreshControl() 51 | let refreshTitle = NSLocalizedString("Refreshing {{ resource.pluralizedName }} …", comment: "{{ resource.pluralizedName }} Refresh Text") 52 | refreshControl.attributedTitle = NSAttributedString(string: refreshTitle) 53 | refreshControl.addTarget(self, action: #selector(refresh{{ resource.pluralizedName }}(sender:)), for: UIControl.Event.valueChanged) 54 | return refreshControl 55 | }() 56 | 57 | convenience init() { 58 | self.init(nibName: nil, bundle: nil) 59 | } 60 | 61 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 62 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 63 | commonInit() 64 | } 65 | 66 | required init?(coder: NSCoder) { 67 | super.init(coder: coder) 68 | commonInit() 69 | } 70 | 71 | func commonInit() { 72 | title = NSLocalizedString("{{ resource.pluralizedName }}", comment: "{{ resource.pluralizedName }} Title (Plural)") 73 | } 74 | 75 | override func viewDidLoad() { 76 | super.viewDidLoad() 77 | 78 | navigationController?.navigationBar.prefersLargeTitles = true 79 | navigationItem.searchController = searchController 80 | navigationItem.hidesSearchBarWhenScrolling = false 81 | 82 | view.backgroundColor = UIColor.white 83 | 84 | view.addSubview(collectionView) 85 | 86 | setupConstraints() 87 | } 88 | 89 | func setupConstraints() { 90 | collectionView.translatesAutoresizingMaskIntoConstraints = false 91 | collectionView.safeAreaLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true 92 | collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true 93 | view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor).isActive = true 94 | view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: collectionView.safeAreaLayoutGuide.bottomAnchor).isActive = true 95 | } 96 | } 97 | 98 | extension {{ resource.pluralizedName }}ViewController { 99 | var {{ resource.pluralizedNameForFunction }}: [{{ resource.name }}] { 100 | return {{ resource.name }}.sampleData() 101 | } 102 | } 103 | 104 | extension {{ resource.pluralizedName }}ViewController: UICollectionViewDataSource { 105 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 106 | // Asks your data source object for the number of items in the specified section. 107 | return {{ resource.pluralizedNameForFunction }}.count 108 | } 109 | 110 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 111 | // Asks your data source object for the cell that corresponds to the specified item in the collection view. 112 | let cell = collectionView.dequeueReusableCell(forIndexPath: indexPath) as {{ resource.name }}CollectionViewCell 113 | let {{ resource.nameForFunction }} = {{ resource.pluralizedNameForFunction }}[indexPath.item] 114 | {% for property in resource.properties %} 115 | cell.{{ property.name }}Label.text = String(describing: {{ resource.nameForFunction }}.{{ property.name }}) 116 | {% endfor %} 117 | return cell 118 | } 119 | } 120 | 121 | extension {{ resource.pluralizedName }}ViewController: UICollectionViewDelegate { 122 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 123 | // Tells the delegate that the item at the specified index path was selected. 124 | print("Selected item \(indexPath.item) in section \(indexPath.section)") 125 | {% if coordinator %} 126 | coordinator?.detail({{ resource.nameForFunction }}: {{ resource.pluralizedNameForFunction }}[indexPath.item]) 127 | {% endif %} 128 | } 129 | } 130 | 131 | extension {{ resource.pluralizedName }}ViewController: UICollectionViewDelegateFlowLayout { 132 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 133 | let distance = (collectionViewLayout as? UICollectionViewFlowLayout)?.minimumInteritemSpacing ?? 0 134 | let width = collectionView.frame.width - view.layoutMargins.left - view.layoutMargins.right 135 | return CGSize(width: (width - distance) / 3, height: 100) 136 | } 137 | } 138 | 139 | extension {{ resource.pluralizedName }}ViewController { 140 | @objc func refresh{{ resource.pluralizedName }}(sender: UIRefreshControl) { 141 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: { 142 | sender.endRefreshing() 143 | }) 144 | } 145 | } 146 | 147 | extension {{ resource.pluralizedName }}ViewController: UISearchResultsUpdating { 148 | func updateSearchResults(for searchController: UISearchController) { 149 | // Called when the search bar's text or scope has changed or when the search bar becomes first responder. 150 | searchResultsData.filterString = searchController.searchBar.text?.lowercased() 151 | (searchController.searchResultsController as? {{ resource.name }}SearchResultsViewController)?.tableView.reloadData() 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  text 2 | 3 | # HackMan 4 | 5 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://www.opensource.org/licenses/MIT) 6 | 7 | HackMan is a simple tool for generating boilerplate code directly via the command line. 8 | 9 | Let `hackman` do the boring work and save some time. 10 | Hackman is heavily inspired by the `rails` command. 11 | 12 | ## Installation 13 | 14 | ### Clone and build 15 | 16 | ```sh 17 | git clone git@github.com:Cosmo/HackMan.git 18 | cd HackMan 19 | swift build -c release 20 | ``` 21 | 22 | ### Add `hackman` executable to your `PATH`. 23 | 24 | ```sh 25 | PATH=$PATH:$(pwd)/.build/release 26 | ``` 27 | 28 | or make it persistent 29 | 30 | For `zsh` Users (default on macOS Catalina) 31 | ```sh 32 | echo "export PATH=\"\$PATH:$(pwd)/.build/release\"" >> ~/.zshrc 33 | ``` 34 | 35 | For `bash` Users (default on macOS Mojave) 36 | ```sh 37 | echo "export PATH=\"\$PATH:$(pwd)/.build/release\"" >> ~/.bash_profile 38 | ``` 39 | 40 | ## Usage 41 | 42 | ### New Project 43 | 44 | ```sh 45 | # Create new project directory including a "project.yml" for xcodegen 46 | hackman new APP_NAME 47 | 48 | # Change into your project directory 49 | cd APP_NAME 50 | ``` 51 | 52 | ### Generators 53 | 54 | Run these generators inside of your project directory. 55 | 56 | ```sh 57 | # Create an AppDelegate 58 | hackman generate app_delegate 59 | 60 | Options 61 | --coordinator, -c 62 | Adds coordinator support 63 | 64 | --force, -f 65 | Force override existing files 66 | ``` 67 | ```sh 68 | # Create an empty AssetCatalog 69 | hackman generate asset_catalog 70 | 71 | Options 72 | --force, -f 73 | Force override existing files 74 | ``` 75 | ```sh 76 | # Create a LaunchScreen-Storyboard 77 | hackman generate launch_screen 78 | 79 | Options 80 | --force, -f 81 | Force override existing files 82 | ``` 83 | ```sh 84 | # Create a ReusableView protocol and useful extensions for UICollectionViews and UITableViews 85 | hackman generate reusable_view 86 | 87 | Options 88 | --force, -f 89 | Force override existing files 90 | ``` 91 | ```sh 92 | # Create a Coordinator protocol 93 | hackman generate coordinator 94 | 95 | Options 96 | --force, -f 97 | Force override existing files 98 | ``` 99 | ```sh 100 | # Create a MainCoordinator 101 | hackman generate coordinator_main NAME NAME … 102 | 103 | Options 104 | --force, -f 105 | Force override existing files 106 | 107 | --include=NAME,NAME,… 108 | Include ViewControllers that were not generated via scaffold. 109 | Names must be separated with commas. Spaces between names are not allowed. 110 | ``` 111 | ```sh 112 | # Create a Child-Coordinator with the given name 113 | hackman generate coordinator_child NAME 114 | 115 | Options 116 | --force, -f 117 | Force override existing files 118 | ``` 119 | ```sh 120 | # Create a Model with the given name and properties 121 | hackman generate model NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] … 122 | 123 | Options 124 | --force, -f 125 | Force override existing files 126 | ``` 127 | ```sh 128 | # Create a UIViewController-Subclass with the given name 129 | hackman generate view_controller NAME 130 | 131 | Options 132 | --coordinator, -c 133 | Adds coordinator support 134 | 135 | --force, -f 136 | Force override existing files 137 | ``` 138 | ```sh 139 | # Create a ViewControllerCollection (UIViewController-Subclass with a UICollectionView) and UICollectionViewDataSource 140 | hackman generate view_controller_collection NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] … 141 | 142 | Options 143 | --coordinator, -c 144 | Adds coordinator support 145 | 146 | --force, -f 147 | Force override existing files 148 | ``` 149 | ```sh 150 | # Create a UICollectionViewCell-Subclass with the given namen and properties as UILabels 151 | hackman generate collection_view_cell NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] … 152 | 153 | Options 154 | --force, -f 155 | Force override existing files 156 | ``` 157 | ```sh 158 | # Create a ViewControllerTable (UIViewController-Subclass with a UITableView) and UITableViewDataSource 159 | hackman generate view_controller_table NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] … 160 | 161 | Options 162 | --coordinator, -c 163 | Adds coordinator support 164 | 165 | --force, -f 166 | Force override existing files 167 | ``` 168 | ```sh 169 | # Create a UITableViewCell-Subclass with the given namen and properties as UILabels 170 | hackman generate table_view_cell NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] … 171 | 172 | Options 173 | --force, -f 174 | Force override existing files 175 | ``` 176 | ```sh 177 | # Create a ViewControllerDetail (UIViewController-Subclass) with the given namen and properties as UILabels 178 | hackman generate view_controller_detail NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] … 179 | 180 | Options 181 | --coordinator, -c 182 | Adds coordinator support 183 | 184 | --force, -f 185 | Force override existing files 186 | ``` 187 | ```sh 188 | # Create a UIViewController-Subclass with a UIWebView 189 | hackman generate view_controller_web 190 | 191 | Options 192 | --coordinator, -c 193 | Adds coordinator support 194 | 195 | --force, -f 196 | Force override existing files 197 | ``` 198 | ```sh 199 | # Create a UIViewController-Subclass with entry points for legal documents 200 | hackman generate view_controller_information 201 | 202 | Options 203 | --coordinator, -c 204 | Adds coordinator support 205 | 206 | --force, -f 207 | Force override existing files 208 | ``` 209 | ```sh 210 | # Create Model, UICollectionView/UITableView Extensions, UIViewController with UICollectionView/UITableView, ViewControllerDetail, ChildCoordinator, Coordinator Protocol and ReusableView Protocol 211 | hackman generate scaffold NAME [PROPERTY[:TYPE] PROPERTY[:TYPE]] … 212 | 213 | # By default, the scaffold will be UICollectionView based. 214 | # In order to create UITableView based scaffolds, pass the --view=table at the end. 215 | # Like so: 216 | hackman generate scaffold song title:string year:int --view=table 217 | 218 | Options 219 | --coordinator, -c 220 | Adds coordinator support 221 | 222 | --force, -f 223 | Force override existing files 224 | ``` 225 | 226 | You can also write `hackman g` instead of `hackman generate`. 227 | 228 | ### Properties 229 | 230 | When creating scaffolds, models, controllers you can also specify multiple fields to be generated automatically 231 | 232 | ```sh 233 | hackman g scaffold author name:string birthday:date 234 | ``` 235 | This commands creates among other things a struct of type `Author` with the following properties. 236 | ```swift 237 | let name: String 238 | let birthday: Date 239 | ``` 240 | 241 | You can also reference to your custom types. 242 | ```sh 243 | hackman g scaffold Article title:string body:string published_at:date author:author 244 | ``` 245 | This will generate the following properties inside of the `Article` model. 246 | ```swift 247 | let title: String 248 | let body: String 249 | let publishedAt: Date 250 | let author: Author 251 | ``` 252 | 253 | If you omit the property type, `hackman` assumes you want a property of type `String`. 254 | ```sh 255 | hackman g scaffold article title body published_at:date author:author 256 | ``` 257 | 258 | 259 | ### An example of a fully working app 260 | 261 | ```sh 262 | hackman new MusicApp 263 | cd MusicApp 264 | hackman g app_delegate --coordinator 265 | hackman g asset_catalog 266 | hackman g launch_screen 267 | hackman g scaffold artist name --coordinator 268 | hackman g scaffold song title year:int --coordinator 269 | hackman g scaffold album name uuid artist:artist created_at:date updated_at:date --coordinator 270 | hackman g view_controller_information --coordinator 271 | hackman g coordinator_main song artist album --include=information 272 | xcodegen 273 | open MusicApp.xcodeproj 274 | ``` 275 | 276 | #### Demo 277 | 278 | ![Demo](https://github.com/Cosmo/HackMan/blob/master/HackMan-Preview.gif?raw=true) 279 | 280 | ## Requirements 281 | 282 | * Xcode 10 283 | * Swift 5 284 | 285 | ## Todos 286 | 287 | * [ ] Easier setup 288 | * [ ] Easy support for custom generators 289 | * [x] Add help (-h / --help) 290 | * [ ] SwiftUI based templates 291 | * [ ] Generator for localization 292 | 293 | ## Contact 294 | 295 | * Devran "Cosmo" Uenal 296 | * Twitter: [@maccosmo](http://twitter.com/maccosmo) 297 | 298 | ## License 299 | 300 | HackMan is released under the [MIT License](http://www.opensource.org/licenses/MIT). 301 | --------------------------------------------------------------------------------