Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/UitbreidenOS/UitKit --skill ios명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | ios |
| description | - Building iOS, iPadOS, or macOS apps with SwiftUI |
@State, @Binding, @ObservedObject, etc.)| Wrapper | Owner | Use case |
|---|---|---|
@State | The view itself | Local, value-type UI state (toggle, text field input, counter) |
@Binding | Parent passes it down | Two-way connection to a parent's @State |
@StateObject | The view itself | View creates and owns a reference-type ObservableObject |
@ObservedObject | Parent passes it in | View observes a reference-type object owned elsewhere |
@EnvironmentObject | Injected via .environmentObject() | Shared observable object propagated through the view hierarchy |
@Environment | SwiftUI environment | System values: colorScheme, dismiss, openURL, custom environment keys |
// @State — local value owned by this view
struct ToggleRow: View {
@State private var isOn = false
var body: some View {
Toggle("Notifications", isOn: $isOn) // $ produces a Binding
}
}
// @Binding — parent controls truth
struct ToggleRow: View {
@Binding var isOn: Bool // parent passes $parentState.isOn
var body: some View {
Toggle("Notifications", isOn: $isOn)
}
}
// @StateObject — view owns and creates the object
struct ProfileView: View {
@StateObject private var viewModel = ProfileViewModel()
var body: some View {
Text(viewModel.name)
}
}
// @ObservedObject — object is passed in, owned elsewhere
struct ProfileView: View {
@ObservedObject var viewModel: ProfileViewModel
body: {
(viewModel.name)
}
}
: {
session:
body: {
(session.displayName)
}
}
().environmentObject(())
Rule: use @StateObject when the view creates the object. Use @ObservedObject when the object is injected. Never create an ObservableObject directly in the body property — it will be recreated on every render.
// Models
struct User: Decodable {
let id: Int
let name: String
let email: String
}
// Network layer
struct APIClient {
let baseURL = URL(string: "https://api.example.com")!
let decoder = JSONDecoder()
func fetchUser(id: Int) async throws -> User {
let url = baseURL.appendingPathComponent("users/\(id)")
var request = URLRequest(url: url)
request.setValue("Bearer \(TokenStore.current)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard ().contains(http.statusCode) {
.httpError(http.statusCode)
}
decoder.decode(., from: data)
}
}
: {
user: ?
error: ?
isLoading
client ()
(: ) {
isLoading
{ isLoading }
{
user client.fetchUser(id: id)
} {
.error error
}
}
}
: {
vm ()
userId:
body: {
{
vm.isLoading {
()
} user vm.user {
(user.name)
} error vm.error {
()
}
}
.task { vm.load(id: userId) }
}
}
Always mark ViewModels @MainActor to ensure @Published mutations happen on the main thread.
// Persistence.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let controller = PersistenceController(inMemory: true)
let ctx = controller.container.viewContext
// Insert sample data for previews
let item = Item(context: ctx)
item.timestamp = Date()
try? ctx.save()
return controller
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyApp") // matches .xcdatamodeld filename
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error { fatalError("Core Data failed: ") }
}
container.viewContext.automaticallyMergesChangesFromParent
container.viewContext.mergePolicy
}
}
: {
persistence .shared
body: {
{
()
.environment(\.managedObjectContext, persistence.container.viewContext)
}
}
}
: {
(\.managedObjectContext) ctx
(
sortDescriptors: [(\.timestamp, order: .reverse)],
animation: .default
)
items: <>
() {
item (context: ctx)
item.timestamp ()
ctx.save()
}
}
For background operations, use container.newBackgroundContext() or container.performBackgroundTask { ctx in }.
import Combine
class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [Product] = []
@Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
private let api = APIClient()
init() {
$query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { $0.count >= 2 }
.handleEvents(receiveOutput: { [weak self] _ in self?.isLoading = true })
.flatMap { [weak self] q -> AnyPublisher<[Product], Never> in
guard let self else { return Empty().eraseToAnyPublisher() }
.api.search(query: q)
.catch { ([]) }
.eraseToAnyPublisher()
}
.receive(on: .main)
.handleEvents(receiveOutput: { [ ] .isLoading })
.assign(to: )
}
}
Use .sink when you need side effects. Use .assign(to:) to drive a @Published property. Always store subscriptions in Set<AnyCancellable> or use the &$published form to avoid premature cancellation.
Before submitting to App Store Connect:
Technical requirements:
Info.plist (camera, location, microphone, contacts, etc.)LaunchScreen.storyboard configuredPrivacy:
PrivacyInfo.xcprivacy) required for apps using specific APIs (file timestamp APIs, system boot time, disk space, active keyboard, user defaults)App Store Connect:
Testing before submission:
A settings screen demonstrating multiple property wrappers together:
@MainActor
class SettingsViewModel: ObservableObject {
@Published var notificationsEnabled = false
@Published var theme: AppTheme = .system
func save() async {
try? await UserPreferencesAPI.save(
notifications: notificationsEnabled,
theme: theme
)
}
}
struct SettingsView: View {
@StateObject private var vm = SettingsViewModel()
@EnvironmentObject var session: UserSession
@Environment(\.dismiss) private var dismiss
var body: some View {
Form {
Section("Account") {
Text(session.email).foregroundStyle(.secondary)
}
Section("Preferences") {
Toggle("Notifications", isOn: $vm.notificationsEnabled)
Picker("Theme", selection: $vm.theme) {
ForEach(AppTheme.allCases) { (.label).tag() }
}
}
}
.navigationTitle()
.toolbar {
(placement: .confirmationAction) {
() {
{
vm.save()
dismiss()
}
}
}
}
}
}