Use when building iOS apps with UIKit, implementing MVVM/MVC/Coordinator patterns, or integrating UIKit with SwiftUI.
Provides architectural patterns for UIKit apps including MVVM, Coordinator, and dependency injection. Use when building UIKit-based iOS applications or integrating UIKit with SwiftUI.
/plugin marketplace add TheBushidoCollective/han/plugin install jutsu-go@hanThis skill is limited to using the following tools:
Architectural patterns and best practices for UIKit-based iOS applications.
The Model-View-ViewModel pattern separates concerns:
// Model
struct User {
let id: String
let firstName: String
let lastName: String
let email: String
}
// ViewModel
class UserProfileViewModel {
private let user: User
var displayName: String {
"\(user.firstName) \(user.lastName)"
}
var emailDisplay: String {
user.email.lowercased()
}
init(user: User) {
self.user = user
}
}
// View
class UserProfileViewController: UIViewController {
private let viewModel: UserProfileViewModel
init(viewModel: UserProfileViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = viewModel.displayName
emailLabel.text = viewModel.emailDisplay
}
}
Coordinators handle navigation flow, removing navigation logic from view controllers:
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get }
func start()
}
class AppCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = HomeViewController()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
func showDetail(for item: Item) {
let detailCoordinator = DetailCoordinator(
navigationController: navigationController,
item: item
)
childCoordinators.append(detailCoordinator)
detailCoordinator.start()
}
}
Inject dependencies through initializers for testability:
protocol UserServiceProtocol {
func fetchUser(id: String) async throws -> User
}
class UserViewController: UIViewController {
private let userService: UserServiceProtocol
private let userId: String
init(userService: UserServiceProtocol, userId: String) {
self.userService = userService
self.userId = userId
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) not supported")
}
}
class ProfileView: UIView {
private let avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupConstraints()
}
private func setupViews() {
addSubview(avatarImageView)
addSubview(nameLabel)
}
private func setupConstraints() {
NSLayoutConstraint.activate([
avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 16),
avatarImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
avatarImageView.widthAnchor.constraint(equalToConstant: 80),
avatarImageView.heightAnchor.constraint(equalToConstant: 80),
nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 12),
nameLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
])
}
}
class ItemListViewController: UIViewController {
enum Section { case main }
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
configureDataSource()
}
private func configureCollectionView() {
let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, indexPath, item in
var content = cell.defaultContentConfiguration()
content.text = item.title
content.secondaryText = item.subtitle
cell.contentConfiguration = content
}
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
collectionView, indexPath, item in
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
}
func updateItems(_ items: [Item]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Hosting SwiftUI in UIKit:
class SettingsViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swiftUIView = SettingsView()
let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.frame = view.bounds
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostingController.didMove(toParent: self)
}
}
Wrapping UIKit in SwiftUI:
struct MapViewRepresentable: UIViewRepresentable {
@Binding var region: MKCoordinateRegion
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewRepresentable
init(_ parent: MapViewRepresentable) {
self.parent = parent
}
}
}
class DataViewController: UIViewController {
private var loadTask: Task<Void, Never>?
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadData()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
loadTask?.cancel()
}
private func loadData() {
loadTask = Task {
do {
let data = try await fetchData()
guard !Task.isCancelled else { return }
updateUI(with: data)
} catch {
showError(error)
}
}
}
}
class NetworkViewController: UIViewController {
private let networkService: NetworkService
func fetchData() {
// Use [weak self] to prevent retain cycles
networkService.fetch { [weak self] result in
guard let self else { return }
switch result {
case .success(let data):
self.handleData(data)
case .failure(let error):
self.showError(error)
}
}
}
}
Bad: Putting everything in one view controller.
Good: Extract into separate types:
Bad: Complex storyboard with many segues.
Good: Use coordinators with programmatic navigation.
Bad:
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! CustomCell
Good:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? CustomCell else {
fatalError("Unable to dequeue CustomCell")
}
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.