Ver3.0 ๐ Swift์ SwiftUI๋ก ์์ํ๋ iOS ์ฑ ๊ฐ๋ฐ ์์ ์ ๋ณต

๐ Swift์ SwiftUI๋ก ์์ํ๋ iOS ์ฑ ๊ฐ๋ฐ ์์ ์ ๋ณต
์ ํ ์ํ๊ณ์ ์ต์ ๊ธฐ์ ๋ก ๋๋ง์ ์ฑ ๋ง๋ค๊ธฐ
์ผ ์ง์ง ์์ฆ iOS ์ฑ ๊ฐ๋ฐ ์์ํ๋ ค๋ฉด Swift๋ SwiftUI ๋ชจ๋ฅด๋ฉด ์ ๋๋ ์๋๊ฐ ์์ด ใ ใ ใ ์์ ์๋ Objective-C๋ก ๊ฐ๋ฐํ๊ณ UIKit์ผ๋ก UI ๋ง๋ค์๋๋ฐ, ์ง๊ธ์ ์์ ํจ๋ฌ๋ค์์ด ๋ฐ๋์๋ค๋๊น? ๐
์ ํ์ด 2014๋ ์ Swift๋ฅผ ๋ฐํํ๊ณ , 2019๋ ์ SwiftUI๋ฅผ ๊ณต๊ฐํ๋ฉด์ iOS ๊ฐ๋ฐ์ด ์ง์ง ํ๋ช ์ ์ผ๋ก ๋ณํ์ด. ์ฝ๋ ์์ ์ค์ด๋ค๊ณ , ์์ฐ์ฑ์ ๋ฏธ์น ๋ฏ์ด ์ฌ๋ผ๊ฐ๊ณ , ๋ญ๊ฐ ๋ง๋ค ๋ ์ฌ๋ฏธ๋ ํจ์ฌ ๋ํด์ก์ง ใ ใ
์ด ๊ธ์์๋ Swift ์ธ์ด์ ํต์ฌ ๊ฐ๋ ๋ถํฐ SwiftUI๋ก ์ค์ ์ฑ์ ๋ง๋๋ ๊ณผ์ ๊น์ง ์์ ๋ํ ์ผํ๊ฒ ๋ค๋ค๋ณผ ๊ฑฐ์ผ. ํ๋ก๊ทธ๋๋ฐ ์ด๋ณด๋ ๋ค๋ฅธ ์ธ์ด ๊ฒฝํ์๋ ์๊ด์์ด ๋ฐ๋ผ์ฌ ์ ์๊ฒ ์ค๋ช ํ ํ ๋๊น ๊ฑฑ์ ใดใด ๐
๐ฏ Swift ์ธ์ด์ ํ์๊ณผ ์ฒ ํ
Swift๋ ์ ํ์ด Chris Lattner๋ฅผ ์ค์ฌ์ผ๋ก ๊ฐ๋ฐํ ํ๋์ ์ธ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ผ. 2014๋ WWDC์์ ์ฒ์ ๊ณต๊ฐ๋์ ๋ ๊ฐ๋ฐ์๋ค ์ฌ์ด์์ ์์ ๋๋ฆฌ ๋ฌ์์ ใ ใ ใ
๐ฅ Swift๊ฐ ๋ฑ์ฅํ ์ด์
Objective-C๋ 1980๋ ๋์ ๋ง๋ค์ด์ง ์ธ์ด๋ผ ํ๋์ ์ธ ํ๋ก๊ทธ๋๋ฐ ํจ๋ฌ๋ค์์ ๋ฐ์ํ๊ธฐ ์ด๋ ค์ ์ด. ๋ฌธ๋ฒ๋ ๋ณต์กํ๊ณ , ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๋ ๊น๋ค๋ก์ ์ง. ์ ํ์ ๋ ์์ ํ๊ณ , ๋น ๋ฅด๊ณ , ํํ๋ ฅ ์๋ ์ธ์ด๊ฐ ํ์ํ๋ ๊ฑฐ์ผ.
Swift์ ํต์ฌ ์ฒ ํ์ ํฌ๊ฒ ์ธ ๊ฐ์ง๋ก ์์ฝํ ์ ์์ด:
1. ์์ ์ฑ(Safety) ๐ช
Swift๋ ์ปดํ์ผ ํ์์ ์ต๋ํ ๋ง์ ์๋ฌ๋ฅผ ์ก์๋ด๋ ค๊ณ ์ค๊ณ๋์ด. Optional ํ์
, ํ์
์์ ์ฑ, ์๋ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ(ARC) ๊ฐ์ ๊ธฐ๋ฅ๋ค์ด ๋ฐํ์ ํฌ๋์๋ฅผ ํ๊ธฐ์ ์ผ๋ก ์ค์ฌ์ค.
2. ์๋(Speed) โก
C++์ด๋ Objective-C์ ๋น๊ตํด๋ ์ฑ๋ฅ์ด ๋ค์ง์ง ์์. LLVM ์ปดํ์ผ๋ฌ ๊ธฐ๋ฐ์ผ๋ก ์ต์ ํ๊ฐ ์์ฒญ ์ ๋์ด ์๊ฑฐ๋ . ๋ฒค์น๋งํฌ ํ
์คํธ์์๋ Python๋ณด๋ค 100๋ฐฐ ์ด์ ๋น ๋ฅธ ๊ฒฝ์ฐ๊ฐ ๋ง์.
3. ํํ๋ ฅ(Expressiveness) โจ
์ฝ๋๊ฐ ์ฝ๊ธฐ ์ฝ๊ณ ์ง๊ด์ ์ด์ผ. ํจ์ํ ํ๋ก๊ทธ๋๋ฐ, ํ๋กํ ์ฝ ์งํฅ ํ๋ก๊ทธ๋๋ฐ ๊ฐ์ ํ๋์ ์ธ ํจ๋ฌ๋ค์์ ์์ฐ์ค๋ฝ๊ฒ ์ง์ํ์ง.
๐ก ๊ฟํ
Swift๋ ์คํ์์ค์ผ! Swift.org์์ ์์ค ์ฝ๋๋ฅผ ๋ณผ ์ ์๊ณ , ๋ฆฌ๋ ์ค๋ ์๋์ฐ์์๋ ์ฌ์ฉํ ์ ์์ด. iOS ๊ฐ๋ฐ๋ฟ๋ง ์๋๋ผ ์๋ฒ ์ฌ์ด๋ ๊ฐ๋ฐ(Vapor, Kitura)๋ ๊ฐ๋ฅํ๋ค๋ ๊ฑฐ ์๊ณ ์์์ด?
๐ Swift ๊ธฐ๋ณธ ๋ฌธ๋ฒ ๋ง์คํฐํ๊ธฐ
์ ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Swift ๋ฌธ๋ฒ์ ํํค์ณ๋ณด์ ใ ใ ๋ค๋ฅธ ์ธ์ด ๊ฒฝํ์ด ์๋ค๋ฉด ๊ธ๋ฐฉ ์ตํ ์ ์์ ๊ฑฐ์ผ!
๋ณ์์ ์์ ์ ์ธ
Swift์์๋ var๋ก ๋ณ์๋ฅผ, let์ผ๋ก ์์๋ฅผ ์ ์ธํด. ๊ฐ๋ฅํ๋ฉด let์ ์ฐ๋ ๊ฒ ์ข์ - ๋ถ๋ณ์ฑ์ด ์ฝ๋๋ฅผ ๋ ์์ ํ๊ฒ ๋ง๋ค๊ฑฐ๋ .
// ๋ณ์ ์ ์ธ
var userName = "๊น๊ฐ๋ฐ"
var age = 25
// ์์ ์ ์ธ
let birthYear = 1999
let pi = 3.14159
// ํ์
๋ช
์ (ํ์
์ถ๋ก ๋ ๊ฐ๋ฅํ์ง๋ง ๋ช
์์ ์ผ๋ก ์ธ ์๋ ์์ด)
var score: Int = 100
let message: String = "Hello, Swift!"
๊ธฐ๋ณธ ๋ฐ์ดํฐ ํ์ ๐
Swift๋ ํ์ ์ ์๊ฒฉํ ์ธ์ด์ผ. ์ฃผ์ ๋ฐ์ดํฐ ํ์ ๋ค์ ์์๋ณด์:
// ์ ์ํ
let smallNumber: Int8 = 127
let normalNumber: Int = 1000000
let bigNumber: Int64 = 9223372036854775807
// ์ค์ํ
let height: Float = 175.5
let weight: Double = 68.5 // Double์ด ๋ ์ ๋ฐํด
// ๋ถ๋ฆฌ์ธ
let isStudent: Bool = true
let hasLicense: Bool = false
// ๋ฌธ์์ด
let greeting: String = "์๋
ํ์ธ์"
let multiLine = """
์ฌ๋ฌ ์ค์
๋ฌธ์์ด๋
์ด๋ ๊ฒ ์ธ ์ ์์ด์
"""
// ๋ฌธ์
let firstLetter: Character = "A"
โ ํ์ ์ถ๋ก ์ ๋ง๋ฒ
Swift ์ปดํ์ผ๋ฌ๋ ๋๋ํด์ ์ด๊ธฐ๊ฐ์ ๋ณด๊ณ ํ์ ์ ์๋์ผ๋ก ์ถ๋ก ํด์ค. let name = "์ฒ ์"๋ผ๊ณ ์ฐ๋ฉด ์๋์ผ๋ก String ํ์ ์ผ๋ก ์ธ์ํ๋ ๊ฑฐ์ง. ๊ทผ๋ฐ ๋ช ์์ ์ผ๋ก ํ์ ์ ์จ์ฃผ๋ ๊ฒ ๊ฐ๋ ์ฑ ๋ฉด์์ ๋ ์ข์ ๋๋ ์์ด!
์ปฌ๋ ์ ํ์ ๐๏ธ
๋ฐฐ์ด(Array), ๋์ ๋๋ฆฌ(Dictionary), ์งํฉ(Set) ์ธ ๊ฐ์ง ์ฃผ์ ์ปฌ๋ ์ ์ด ์์ด:
// ๋ฐฐ์ด - ์์๊ฐ ์๋ ๋ฐ์ดํฐ ๋ชจ์
var fruits = ["์ฌ๊ณผ", "๋ฐ๋๋", "์ค๋ ์ง"]
fruits.append("ํฌ๋")
print(fruits[0]) // "์ฌ๊ณผ"
// ๋์
๋๋ฆฌ - ํค-๊ฐ ์์ผ๋ก ์ ์ฅ
var studentScores = ["์ฒ ์": 95, "์ํฌ": 88, "๋ฏผ์": 92]
studentScores["์ง์"] = 90
print(studentScores["์ฒ ์"] ?? 0) // 95
// ์งํฉ - ์ค๋ณต ์๋ ๋ฐ์ดํฐ ๋ชจ์
var uniqueNumbers: Set = [1, 2, 3, 3, 4, 5]
print(uniqueNumbers) // [1, 2, 3, 4, 5] ์์๋ ๋ณด์ฅ ์ ๋จ
์ต์ ๋(Optional) - Swift์ ํฌ๋ฌ ํผ์ฒ ๐
์ด๊ฑฐ ์ง์ง ์ค์ํด! ์ต์ ๋์ "๊ฐ์ด ์์ ์๋ ์๊ณ ์์ ์๋ ์๋ค"๋ ๊ฑธ ํ์ ์์คํ ์ผ๋ก ํํํ ๊ฑฐ์ผ. Null Pointer Exception ๊ฐ์ ๋ฐํ์ ์๋ฌ๋ฅผ ์ปดํ์ผ ํ์์ ์ก์๋ผ ์ ์์ง.
// ์ต์
๋ ์ ์ธ (ํ์
๋ค์ ? ๋ถ์)
var optionalName: String? = "ํ๊ธธ๋"
var optionalAge: Int? = nil
// ์ต์
๋ ๋ฐ์ธ๋ฉ - ์์ ํ๊ฒ ๊ฐ ์ถ์ถ
if let name = optionalName {
print("์ด๋ฆ์ \(name)์
๋๋ค")
} else {
print("์ด๋ฆ์ด ์์ต๋๋ค")
}
// guard let - ์กฐ๊ธฐ ์ข
๋ฃ ํจํด
func greet(name: String?) {
guard let unwrappedName = name else {
print("์ด๋ฆ์ ์ ์ ์์ต๋๋ค")
return
}
print("์๋
ํ์ธ์, \(unwrappedName)๋!")
}
// Nil ๋ณํฉ ์ฐ์ฐ์
let displayName = optionalName ?? "์ต๋ช
"
// ์ต์
๋ ์ฒด์ด๋
let uppercasedName = optionalName?.uppercased()
โ ๏ธ ๊ฐ์ ์ธ๋ํ ์ฃผ์!
optionalName! ์ด๋ ๊ฒ ๋๋ํ๋ก ๊ฐ์ ๋ก ๊ฐ์ ๊บผ๋ด๋ ๊ฑด ์ํํด. ๊ฐ์ด nil์ด๋ฉด ์ฑ์ด ํฌ๋์ ๋๊ฑฐ๋ ใ ใ ์ ๋ง ํ์คํ ๋๋ง ์ฐ๊ณ , ๊ฐ๋ฅํ๋ฉด ์ต์ ๋ ๋ฐ์ธ๋ฉ์ด๋ nil ๋ณํฉ ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์!
์ ์ด๋ฌธ๊ณผ ๋ฐ๋ณต๋ฌธ ๐
// if-else
let temperature = 25
if temperature > 30 {
print("๋์์")
} else if temperature > 20 {
print("์ ๋นํด์")
} else {
print("์ถ์์")
}
// switch - Swift์ switch๋ ์์ฒญ ๊ฐ๋ ฅํด!
let grade = "A"
switch grade {
case "A":
print("์ฐ์")
case "B", "C":
print("๋ณดํต")
case "D"..."F": // ๋ฒ์๋ ๊ฐ๋ฅ!
print("๋ฏธํก")
default:
print("์ ์ ์์")
}
// for-in ๋ฃจํ
for i in 1...5 { // 1๋ถํฐ 5๊น์ง (5 ํฌํจ)
print(i)
}
for i in 1..<5 { // 1๋ถํฐ 4๊น์ง (5 ๋ฏธํฌํจ)
print(i)
}
// ๋ฐฐ์ด ์ํ
for fruit in fruits {
print(fruit)
}
// ๋์
๋๋ฆฌ ์ํ
for (name, score) in studentScores {
print("\(name): \(score)์ ")
}
// while ๋ฃจํ
var count = 0
while count < 5 {
print(count)
count += 1
}
๐๏ธ ํจ์์ ํด๋ก์
ํจ์๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ฝ๋ ๋ธ๋ก์ด์ผ. Swift์ ํจ์๋ ์ผ๊ธ ๊ฐ์ฒด(First-class citizen)๋ผ์ ๋ณ์์ ํ ๋นํ๊ฑฐ๋ ๋ค๋ฅธ ํจ์์ ์ธ์๋ก ์ ๋ฌํ ์ ์์ด!
๊ธฐ๋ณธ ํจ์ ์ ์ธ
// ํ๋ผ๋ฏธํฐ์ ๋ฐํ๊ฐ์ด ์๋ ํจ์
func add(a: Int, b: Int) -> Int {
return a + b
}
let result = add(a: 5, b: 3) // 8
// ํ๋ผ๋ฏธํฐ ๋ ์ด๋ธ (์ธ๋ถ/๋ด๋ถ ์ด๋ฆ)
func greet(to name: String, from hometown: String) {
print("\(name)๋, \(hometown)์์ ์ค์
จ๊ตฐ์!")
}
greet(to: "์ฒ ์", from: "์์ธ")
// ๊ธฐ๋ณธ ํ๋ผ๋ฏธํฐ ๊ฐ
func introduce(name: String, age: Int = 20) {
print("\(name), \(age)์ด")
}
introduce(name: "์ํฌ") // ์ํฌ, 20์ด
introduce(name: "๋ฏผ์", age: 25) // ๋ฏผ์, 25์ด
// ๊ฐ๋ณ ํ๋ผ๋ฏธํฐ
func sum(_ numbers: Int...) -> Int {
return numbers.reduce(0, +)
}
print(sum(1, 2, 3, 4, 5)) // 15
ํด๋ก์ - ์ต๋ช ํจ์์ ๊ฐ๋ ฅํจ ๐
ํด๋ก์ ๋ ์ด๋ฆ ์๋ ํจ์๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ. ๋ค๋ฅธ ์ธ์ด์ ๋๋ค๋ ์ต๋ช ํจ์์ ๋น์ทํด.
// ๊ธฐ๋ณธ ํด๋ก์ ๋ฌธ๋ฒ
let multiply = { (a: Int, b: Int) -> Int in
return a * b
}
print(multiply(4, 5)) // 20
// ๋ฐฐ์ด ๋ฉ์๋์ ํด๋ก์
let numbers = [1, 2, 3, 4, 5]
// map - ๊ฐ ์์๋ฅผ ๋ณํ
let doubled = numbers.map { $0 * 2 }
print(doubled) // [2, 4, 6, 8, 10]
// filter - ์กฐ๊ฑด์ ๋ง๋ ์์๋ง ์ ํ
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // [2, 4]
// reduce - ํ๋์ ๊ฐ์ผ๋ก ์ถ์ฝ
let total = numbers.reduce(0) { $0 + $1 }
print(total) // 15
// ํํ ํด๋ก์ (Trailing Closure)
func performOperation(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int {
return operation(a, b)
}
let result1 = performOperation(a: 10, b: 5) { x, y in
return x - y
} // 5
๐ก ํด๋ก์ ์ถ์ฝ ๋ฌธ๋ฒ
Swift ํด๋ก์ ๋ ๋ฌธ๋งฅ์ ๋ฐ๋ผ ์์ฒญ ๊ฐ๊ฒฐํ๊ฒ ์ธ ์ ์์ด. $0, $1 ๊ฐ์ ๋จ์ถ ์ธ์ ์ด๋ฆ์ ์ฐ๊ฑฐ๋, ํ์ ์ถ๋ก ์ ํ์ฉํ๋ฉด ์ฝ๋๊ฐ ํจ์ฌ ๊น๋ํด์ ธ. ๊ทผ๋ฐ ๋๋ฌด ์ถ์ฝํ๋ฉด ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง ์ ์์ผ๋ ์ ์ ํ ์กฐ์ ํ๋ ๊ฒ ์ค์ํด!
๐จ ๊ตฌ์กฐ์ฒด์ ํด๋์ค
Swift๋ ๊ฐ์ฒด์งํฅ ํ๋ก๊ทธ๋๋ฐ๊ณผ ํ๋กํ ์ฝ ์งํฅ ํ๋ก๊ทธ๋๋ฐ์ ๋ชจ๋ ์ง์ํด. ๊ตฌ์กฐ์ฒด(Struct)์ ํด๋์ค(Class)์ ์ฐจ์ด๋ฅผ ์ดํดํ๋ ๊ฒ ์ง์ง ์ค์ํด!
๊ตฌ์กฐ์ฒด(Struct) - ๊ฐ ํ์
struct Person {
var name: String
var age: Int
// ๋ฉ์๋
func introduce() {
print("์๋
ํ์ธ์, \(name)์
๋๋ค. \(age)์ด์ด์์.")
}
// mutating ํค์๋ - ๊ตฌ์กฐ์ฒด ๋ด๋ถ ๊ฐ ๋ณ๊ฒฝ
mutating func haveBirthday() {
age += 1
}
}
var person1 = Person(name: "์ฒ ์", age: 20)
var person2 = person1 // ๋ณต์ฌ๋จ!
person2.name = "์ํฌ"
print(person1.name) // "์ฒ ์" - ์๋ณธ์ ๋ณํ์ง ์์
print(person2.name) // "์ํฌ"
ํด๋์ค(Class) - ์ฐธ์กฐ ํ์
class Vehicle {
var brand: String
var speed: Int
// ์ด๊ธฐํ ๋ฉ์๋
init(brand: String, speed: Int) {
self.brand = brand
self.speed = speed
}
func accelerate() {
speed += 10
print("\(brand) ์๋: \(speed)km/h")
}
// ์๋ฉธ์
deinit {
print("\(brand) ๊ฐ์ฒด๊ฐ ๋ฉ๋ชจ๋ฆฌ์์ ํด์ ๋จ")
}
}
// ์์
class Car: Vehicle {
var numberOfDoors: Int
init(brand: String, speed: Int, doors: Int) {
self.numberOfDoors = doors
super.init(brand: brand, speed: speed)
}
override func accelerate() {
speed += 20 // ์๋์ฐจ๋ ๋ ๋นจ๋ฆฌ ๊ฐ์
print("\(brand) ์๋์ฐจ ์๋: \(speed)km/h")
}
}
let car1 = Car(brand: "ํ๋", speed: 0, doors: 4)
let car2 = car1 // ๊ฐ์ ๊ฐ์ฒด๋ฅผ ์ฐธ์กฐ!
car2.brand = "๊ธฐ์"
print(car1.brand) // "๊ธฐ์" - ์ฐธ์กฐ๋ผ์ ์๋ณธ๋ ๋ณ๊ฒฝ๋จ
๐ค ๊ตฌ์กฐ์ฒด vs ํด๋์ค, ์ธ์ ๋ญ ์จ์ผ ํ ๊น?
๊ตฌ์กฐ์ฒด๋ฅผ ์ฐ๋ ๊ฒฝ์ฐ:
โข ๋ฐ์ดํฐ๋ฅผ ์บก์ํํ๋ ๊ฒ ์ฃผ ๋ชฉ์ ์ผ ๋
โข ์์์ด ํ์ ์์ ๋
โข ๋ณต์ฌ ๋์์ด ํฉ๋ฆฌ์ ์ผ ๋
โข ์๊ณ ๊ฐ๋จํ ๋ฐ์ดํฐ ๋ชจ๋ธ
ํด๋์ค๋ฅผ ์ฐ๋ ๊ฒฝ์ฐ:
โข ์์์ด ํ์ํ ๋
โข ์ฐธ์กฐ ์๋ฏธ๋ก ์ด ํ์ํ ๋ (์ฌ๋ฌ ๊ณณ์์ ๊ฐ์ ์ธ์คํด์ค ๊ณต์ )
โข Objective-C์ ์ํธ์ด์ฉ์ด ํ์ํ ๋
์ ํ ๊ณต์ ๊ฐ์ด๋์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ตฌ์กฐ์ฒด๋ฅผ ๊ถ์ฅํ๊ณ , ๊ผญ ํ์ํ ๋๋ง ํด๋์ค๋ฅผ ์ฐ๋ผ๊ณ ํด!
ํ๋กํผํฐ์ ๋ฉ์๋ ๐ฆ
struct Rectangle {
// ์ ์ฅ ํ๋กํผํฐ
var width: Double
var height: Double
// ์ฐ์ฐ ํ๋กํผํฐ
var area: Double {
return width * height
}
var perimeter: Double {
get {
return 2 * (width + height)
}
}
// ํ๋กํผํฐ ์ต์ ๋ฒ
var name: String = "์ฌ๊ฐํ" {
willSet {
print("์ด๋ฆ์ด \(newValue)๋ก ๋ณ๊ฒฝ๋ ์์ ")
}
didSet {
print("์ด๋ฆ์ด \(oldValue)์์ \(name)๋ก ๋ณ๊ฒฝ๋จ")
}
}
// ํ์
ํ๋กํผํฐ (static)
static var description = "4๊ฐ์ ๋ณ์ ๊ฐ์ง ๋ํ"
// ํ์
๋ฉ์๋
static func printDescription() {
print(description)
}
}
var rect = Rectangle(width: 10, height: 5)
print(rect.area) // 50.0
rect.name = "์ง์ฌ๊ฐํ" // ์ต์ ๋ฒ ๋์
๐ ํ๋กํ ์ฝ๊ณผ ์ต์คํ ์
ํ๋กํ ์ฝ์ ๋ค๋ฅธ ์ธ์ด์ ์ธํฐํ์ด์ค์ ๋น์ทํ๋ฐ, Swift์์๋ ํจ์ฌ ๊ฐ๋ ฅํด! ํ๋กํ ์ฝ ์งํฅ ํ๋ก๊ทธ๋๋ฐ(POP)์ Swift์ ํต์ฌ ํจ๋ฌ๋ค์ ์ค ํ๋์ผ.
ํ๋กํ ์ฝ ์ ์์ ์ฑํ
// ํ๋กํ ์ฝ ์ ์
protocol Drawable {
var color: String { get set }
func draw()
}
protocol Resizable {
mutating func resize(by factor: Double)
}
// ํ๋กํ ์ฝ ์ฑํ
struct Circle: Drawable, Resizable {
var color: String
var radius: Double
func draw() {
print("\(color) ์์์ ์์ ๊ทธ๋ฆฝ๋๋ค")
}
mutating func resize(by factor: Double) {
radius *= factor
}
}
// ํ๋กํ ์ฝ์ ํ์
์ผ๋ก ์ฌ์ฉ
func drawShape(_ shape: Drawable) {
shape.draw()
}
let circle = Circle(color: "๋นจ๊ฐ", radius: 5.0)
drawShape(circle) // "๋นจ๊ฐ ์์์ ์์ ๊ทธ๋ฆฝ๋๋ค"
ํ๋กํ ์ฝ ์ต์คํ ์ - ๊ธฐ๋ณธ ๊ตฌํ ์ ๊ณต โจ
protocol Identifiable {
var id: String { get }
func displayID()
}
// ํ๋กํ ์ฝ์ ๊ธฐ๋ณธ ๊ตฌํ ์ ๊ณต
extension Identifiable {
func displayID() {
print("ID: \(id)")
}
}
struct User: Identifiable {
var id: String
var name: String
// displayID() ๊ตฌํ ์ ํด๋ ๋จ!
}
let user = User(id: "user123", name: "๊น์ฒ ์")
user.displayID() // "ID: user123"
์ต์คํ ์ ์ผ๋ก ๊ธฐ๋ฅ ํ์ฅ ๐
// ๊ธฐ์กด ํ์
์ ๋ฉ์๋ ์ถ๊ฐ
extension Int {
func squared() -> Int {
return self * self
}
var isEven: Bool {
return self % 2 == 0
}
}
print(5.squared()) // 25
print(4.isEven) // true
// String ํ์ฅ
extension String {
func removeWhitespace() -> String {
return self.replacingOccurrences(of: " ", with: "")
}
var length: Int {
return self.count
}
}
let text = "Hello World"
print(text.removeWhitespace()) // "HelloWorld"
print(text.length) // 11
โ ํ๋กํ ์ฝ ์งํฅ ํ๋ก๊ทธ๋๋ฐ์ ์ฅ์
โข ๋ค์ค ์์ ๋ฌธ์ ํด๊ฒฐ (ํ๋กํ ์ฝ์ ์ฌ๋ฌ ๊ฐ ์ฑํ ๊ฐ๋ฅ)
โข ๊ฐ ํ์
(๊ตฌ์กฐ์ฒด)์์๋ ๋คํ์ฑ ๊ตฌํ ๊ฐ๋ฅ
โข ํ
์คํธํ๊ธฐ ์ฌ์ด ์ฝ๋ ์์ฑ ๊ฐ๋ฅ
โข ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ ๊ทน๋ํ
โก ์๋ฌ ์ฒ๋ฆฌ์ ์ต์ ๋ ์ฒด์ด๋
Swift์ ์๋ฌ ์ฒ๋ฆฌ๋ ๋ช ์์ ์ด๊ณ ์์ ํด. ์์ธ๊ฐ ๋ฐ์ํ ์ ์๋ ์ฝ๋๋ฅผ ๋ช ํํ๊ฒ ํ์ํ๊ณ ์ฒ๋ฆฌํด์ผ ํด.
// ์๋ฌ ํ์
์ ์
enum NetworkError: Error {
case badURL
case requestFailed
case unknown
}
// throwing ํจ์
func fetchData(from urlString: String) throws -> String {
guard !urlString.isEmpty else {
throw NetworkError.badURL
}
// ๋คํธ์ํฌ ์์ฒญ ์๋ฎฌ๋ ์ด์
let success = Bool.random()
if success {
return "๋ฐ์ดํฐ ์์ ์ฑ๊ณต"
} else {
throw NetworkError.requestFailed
}
}
// do-catch๋ก ์๋ฌ ์ฒ๋ฆฌ
do {
let data = try fetchData(from: "https://api.example.com")
print(data)
} catch NetworkError.badURL {
print("์๋ชป๋ URL์
๋๋ค")
} catch NetworkError.requestFailed {
print("์์ฒญ์ด ์คํจํ์ต๋๋ค")
} catch {
print("์ ์ ์๋ ์๋ฌ: \(error)")
}
// try? - ์ต์
๋๋ก ๋ณํ
let result = try? fetchData(from: "https://api.example.com")
// ์ฑ๊ณตํ๋ฉด String?, ์คํจํ๋ฉด nil
// try! - ์๋ฌ๊ฐ ์ ๋ ๋ฐ์ํ์ง ์๋๋ค๊ณ ํ์ ํ ๋ (์ํ!)
// let data = try! fetchData(from: "https://api.example.com")
๐จ SwiftUI ์์ํ๊ธฐ
์ ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก SwiftUI๋ก ๋์ด๊ฐ๋ณด์! SwiftUI๋ 2019๋ ์ ๋์จ ์ ํ์ ์ต์ UI ํ๋ ์์ํฌ์ผ. ๊ธฐ์กด UIKit๊ณผ๋ ์์ ํ ๋ค๋ฅธ ์ ์ธํ(Declarative) ๋ฐฉ์์ผ๋ก UI๋ฅผ ๋ง๋ค์ด ใ ใ ใ
๐ SwiftUI์ ํ๋ช ์ ์ธ ํน์ง
์ ์ธํ ๋ฌธ๋ฒ: "์ด๋ป๊ฒ(How)" ๋์ "๋ฌด์์(What)" ํํํด. UI๊ฐ ์ด๋ป๊ฒ ๋ณด์ฌ์ผ ํ๋์ง๋ง ์ ์ธํ๋ฉด SwiftUI๊ฐ ์์์ ๋ ๋๋งํด์ค.
์ค์๊ฐ ํ๋ฆฌ๋ทฐ: ์ฝ๋๋ฅผ ์์ ํ๋ฉด ์ฆ์ ์บ๋ฒ์ค์์ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ์ ์์ด. ์๋ฎฌ๋ ์ดํฐ ์คํ ์ ํด๋ ๋ผ!
ํฌ๋ก์ค ํ๋ซํผ: ๊ฐ์ ์ฝ๋๋ก iOS, iPadOS, macOS, watchOS, tvOS ์ฑ์ ๋ง๋ค ์ ์์ด.
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ: ๋ฐ์ดํฐ์ UI๊ฐ ์๋์ผ๋ก ๋๊ธฐํ๋ผ. ์ํ ๊ด๋ฆฌ๊ฐ ์์ฒญ ์ฌ์์ก์ง.
์ฒซ ๋ฒ์งธ SwiftUI ๋ทฐ ๋ง๋ค๊ธฐ ๐
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
.font(.largeTitle)
.foregroundColor(.blue)
.padding()
}
}
// ํ๋ฆฌ๋ทฐ
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
์ด๊ฒ ๋์ด์ผ! UIKit์์๋ UILabel ๋ง๋ค๊ณ , ์ ์ฝ์กฐ๊ฑด ์ค์ ํ๊ณ ... ์ฝ๋๊ฐ ์์ฒญ ๊ธธ์๋๋ฐ, SwiftUI๋ ์ด๋ ๊ฒ ๊ฐ๋จํด ใ ใ
๊ธฐ๋ณธ ๋ทฐ ์ปดํฌ๋ํธ๋ค ๐ฑ
struct BasicViewsExample: View {
var body: some View {
VStack(spacing: 20) {
// ํ
์คํธ
Text("์๋
ํ์ธ์!")
.font(.title)
.fontWeight(.bold)
// ์ด๋ฏธ์ง
Image(systemName: "star.fill")
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.yellow)
// ๋ฒํผ
Button(action: {
print("๋ฒํผ ํด๋ฆญ!")
}) {
Text("ํด๋ฆญํ์ธ์")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
// ํ
์คํธ ํ๋
TextField("์ด๋ฆ์ ์
๋ ฅํ์ธ์", text: .constant(""))
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
// ํ ๊ธ
Toggle("์๋ฆผ ๋ฐ๊ธฐ", isOn: .constant(true))
.padding(.horizontal)
}
.padding()
}
}
๋ ์ด์์ ์ปจํ ์ด๋ ๐
SwiftUI๋ VStack, HStack, ZStack์ผ๋ก ๋ ์ด์์์ ๊ตฌ์ฑํด. ์ง์ง ์ง๊ด์ ์ด์ผ!
struct LayoutExample: View {
var body: some View {
VStack { // ์ธ๋ก ์คํ
Text("์")
HStack { // ๊ฐ๋ก ์คํ
Text("์ผ์ชฝ")
Spacer() // ๊ณต๊ฐ ์ฑ์ฐ๊ธฐ
Text("์ค๋ฅธ์ชฝ")
}
ZStack { // ๊ฒน์น๊ธฐ ์คํ
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
Text("๊ฒน์นจ")
.foregroundColor(.white)
}
}
.padding()
}
}
๐ก Modifier ์์๊ฐ ์ค์ํด!
SwiftUI์ modifier๋ ์์์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ ธ. .background(Color.blue).padding()๊ณผ .padding().background(Color.blue)๋ ์์ ํ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ง๋ค์ด. ์ฒซ ๋ฒ์งธ๋ ๋ฐฐ๊ฒฝ ๋จผ์ ์ ์ฉํ๊ณ ํจ๋ฉ์ ์ฃผ๊ณ , ๋ ๋ฒ์งธ๋ ํจ๋ฉ ๋จผ์ ์ฃผ๊ณ ๋ฐฐ๊ฒฝ์ ์ ์ฉํ๊ฑฐ๋ !
๐ ์ํ ๊ด๋ฆฌ - State, Binding, ObservableObject
SwiftUI์ ์ง์ง ํ์๋ ์ํ ๊ด๋ฆฌ์ ์์ด! ๋ฐ์ดํฐ๊ฐ ๋ณํ๋ฉด UI๊ฐ ์๋์ผ๋ก ์ ๋ฐ์ดํธ๋๋ ๋ง๋ฒ ๊ฐ์ ์ผ์ด ์ผ์ด๋์ง ใ ใ ใ
@State - ๋ทฐ ๋ด๋ถ ์ํ
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("์นด์ดํธ: \(count)")
.font(.largeTitle)
HStack(spacing: 15) {
Button("-") {
count -= 1
}
.buttonStyle(.bordered)
Button("+") {
count += 1
}
.buttonStyle(.bordered)
}
}
.padding()
}
}
@State๋ฅผ ๋ถ์ด๋ฉด SwiftUI๊ฐ ์ด ๋ณ์๋ฅผ ๊ด์ฐฐํ๋ค๊ฐ ๊ฐ์ด ๋ฐ๋๋ฉด ์๋์ผ๋ก ๋ทฐ๋ฅผ ๋ค์ ๊ทธ๋ ค์ค. ๊ฐ๋ฐ์๋ UI ์ ๋ฐ์ดํธ ์ฝ๋๋ฅผ ์ ํ ์ ์จ๋ ๋ผ!
@Binding - ๋ถ๋ชจ-์์ ๋ทฐ ๊ฐ ๋ฐ์ดํฐ ๊ณต์
struct ParentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "์ผ์ง" : "๊บผ์ง")
.font(.title)
ChildView(isOn: $isOn) // $ ๋ถ์ฌ์ ๋ฐ์ธ๋ฉ ์ ๋ฌ
}
}
}
struct ChildView: View {
@Binding var isOn: Bool // ๋ถ๋ชจ์ ์ํ๋ฅผ ๋ฐ์
var body: some View {
Toggle("์ค์์น", isOn: $isOn)
.padding()
}
}
@ObservableObject - ๋ณต์กํ ๋ฐ์ดํฐ ๋ชจ๋ธ
// ๋ทฐ๋ชจ๋ธ ์ ์
class UserViewModel: ObservableObject {
@Published var username = ""
@Published var email = ""
@Published var isLoggedIn = false
func login() {
// ๋ก๊ทธ์ธ ๋ก์ง
isLoggedIn = true
}
func logout() {
username = ""
email = ""
isLoggedIn = false
}
}
// ๋ทฐ์์ ์ฌ์ฉ
struct LoginView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
VStack(spacing: 20) {
if viewModel.isLoggedIn {
Text("ํ์ํฉ๋๋ค, \(viewModel.username)๋!")
Button("๋ก๊ทธ์์") {
viewModel.logout()
}
} else {
TextField("์ฌ์ฉ์๋ช
", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
TextField("์ด๋ฉ์ผ", text: $viewModel.email)
.textFieldStyle(.roundedBorder)
Button("๋ก๊ทธ์ธ") {
viewModel.login()
}
.buttonStyle(.borderedProminent)
}
}
.padding()
}
}
๐ฏ ์ํ ๊ด๋ฆฌ ํ๋กํผํฐ ๋ํผ ์ ๋ฆฌ
@State: ๋ทฐ ๋ด๋ถ์ ๊ฐ๋จํ ์ํ (Int, String, Bool ๋ฑ)
@Binding: ๋ถ๋ชจ ๋ทฐ์ @State๋ฅผ ์์ ๋ทฐ์์ ์์ ํ ๋
@StateObject: ๋ทฐ๊ฐ ์์ ํ๋ ObservableObject (๋ทฐ ์๋ช ์ฃผ๊ธฐ์ ํจ๊ป)
@ObservedObject: ์ธ๋ถ์์ ์์ฑ๋ ObservableObject๋ฅผ ๊ด์ฐฐ
@EnvironmentObject: ์ฑ ์ ์ฒด์์ ๊ณต์ ํ๋ ๋ฐ์ดํฐ
๐ ๋ฆฌ์คํธ์ ๋ค๋น๊ฒ์ด์
์ค์ ์ฑ์์ ๊ฐ์ฅ ๋ง์ด ์ฐ๋ ํจํด์ด ๋ฆฌ์คํธ์ ๋ค๋น๊ฒ์ด์ ์ด์ผ. SwiftUI์์๋ ์ด๊ฒ๋ ์์ฒญ ์ฌ์!
List ๋ทฐ ๋ง๋ค๊ธฐ
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool
}
struct TodoListView: View {
@State private var todos = [
TodoItem(title: "SwiftUI ๊ณต๋ถํ๊ธฐ", isCompleted: false),
TodoItem(title: "์ฑ ๋ง๋ค๊ธฐ", isCompleted: false),
TodoItem(title: "์ฑ์คํ ์ด ์ถ์", isCompleted: false)
]
var body: some View {
NavigationView {
List {
ForEach(todos) { todo in
HStack {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(todo.isCompleted ? .green : .gray)
Text(todo.title)
.strikethrough(todo.isCompleted)
}
.onTapGesture {
if let index = todos.firstIndex(where: { $0.id == todo.id }) {
todos[index].isCompleted.toggle()
}
}
}
.onDelete { indexSet in
todos.remove(atOffsets: indexSet)
}
.onMove { source, destination in
todos.move(fromOffsets: source, toOffset: destination)
}
}
.navigationTitle("ํ ์ผ ๋ชฉ๋ก")
.toolbar {
EditButton()
}
}
}
}
NavigationLink๋ก ํ๋ฉด ์ ํ
struct MainView: View {
var body: some View {
NavigationView {
List {
NavigationLink("ํ๋กํ", destination: ProfileView())
NavigationLink("์ค์ ", destination: SettingsView())
NavigationLink("์ ๋ณด", destination: AboutView())
}
.navigationTitle("๋ฉ๋ด")
}
}
}
struct ProfileView: View {
var body: some View {
Text("ํ๋กํ ํ๋ฉด")
.navigationTitle("ํ๋กํ")
.navigationBarTitleDisplayMode(.inline)
}
}
โ List ์ฑ๋ฅ ์ต์ ํ ํ
โข Identifiable ํ๋กํ ์ฝ ์ฑํํด์ ๊ฐ ํญ๋ชฉ์ ๊ณ ์ ํ๊ฒ ์๋ณ
โข ๋ณต์กํ ๊ณ์ฐ์ computed property๋ ๋ฉ์๋๋ก ๋ถ๋ฆฌ
โข LazyVStack/LazyHStack ์ฌ์ฉํ๋ฉด ํ์ํ ๋๋ง ๋ทฐ ์์ฑ
โข ์ด๋ฏธ์ง๋ ๋น๋๊ธฐ๋ก ๋ก๋ํ๊ณ ์บ์ฑ ํ์ฉ
๐ญ ์ ๋๋ฉ์ด์ ๊ณผ ์ ์ค์ฒ
SwiftUI์ ์ ๋๋ฉ์ด์ ์ ์ง์ง ์ฝ๊ณ ์์ฐ์ค๋ฌ์. ๋ช ์ค ์ฝ๋๋ก ๋ฉ์ง ํจ๊ณผ๋ฅผ ๋ง๋ค ์ ์์ด!
๊ธฐ๋ณธ ์ ๋๋ฉ์ด์
struct AnimationExample: View {
@State private var isExpanded = false
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.blue)
.frame(width: isExpanded ? 300 : 100,
height: isExpanded ? 300 : 100)
.animation(.spring(response: 0.5, dampingFraction: 0.6), value: isExpanded)
Button(isExpanded ? "์ถ์" : "ํ๋") {
isExpanded.toggle()
}
.padding()
}
}
}
๋ค์ํ ์ ๋๋ฉ์ด์ ํ์
struct AnimationTypesView: View {
@State private var offset: CGFloat = 0
var body: some View {
VStack(spacing: 30) {
Circle()
.fill(Color.red)
.frame(width: 50, height: 50)
.offset(x: offset)
.animation(.linear(duration: 1), value: offset)
Circle()
.fill(Color.green)
.frame(width: 50, height: 50)
.offset(x: offset)
.animation(.easeInOut(duration: 1), value: offset)
Circle()
.fill(Color.blue)
.frame(width: 50, height: 50)
.offset(x: offset)
.animation(.spring(), value: offset)
Button("์ ๋๋ฉ์ด์
์์") {
offset = offset == 0 ? 150 : 0
}
}
}
}
์ ์ค์ฒ ์ธ์ ๐
struct GestureExample: View {
@State private var offset = CGSize.zero
@State private var scale: CGFloat = 1.0
@State private var rotation: Angle = .zero
var body: some View {
VStack(spacing: 50) {
// ๋๋๊ทธ ์ ์ค์ฒ
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
withAnimation(.spring()) {
offset = .zero
}
}
)
// ํ๋/์ถ์ ์ ์ค์ฒ
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
.scaleEffect(scale)
.gesture(
MagnificationGesture()
.onChanged { value in
scale = value
}
.onEnded { _ in
withAnimation {
scale = 1.0
}
}
)
// ํ์ ์ ์ค์ฒ
RoundedRectangle(cornerRadius: 20)
.fill(Color.purple)
.frame(width: 100, height: 100)
.rotationEffect(rotation)
.gesture(
RotationGesture()
.onChanged { value in
rotation = value
}
.onEnded { _ in
withAnimation {
rotation = .zero
}
}
)
}
}
}
๐ ๋คํธ์ํน๊ณผ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
์ค์ ์ฑ์ ์๋ฒ์ ํต์ ํด์ผ ํ์์? Swift์ URLSession๊ณผ Codable์ ํ์ฉํ๋ฉด API ํต์ ์ด ์์ฒญ ์ฌ์!
JSON ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง
// JSON ์๋ต ๊ตฌ์กฐ
struct User: Codable, Identifiable {
let id: Int
let name: String
let email: String
let username: String
}
struct Post: Codable, Identifiable {
let id: Int
let userId: Int
let title: String
let body: String
}
// API ์๋น์ค ํด๋์ค
class APIService {
static let shared = APIService()
private let baseURL = "https://jsonplaceholder.typicode.com"
func fetchUsers() async throws -> [User] {
guard let url = URL(string: "\(baseURL)/users") else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
let users = try JSONDecoder().decode([User].self, from: data)
return users
}
func fetchPosts() async throws -> [Post] {
guard let url = URL(string: "\(baseURL)/posts") else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
let posts = try JSONDecoder().decode([Post].self, from: data)
return posts
}
}
๋ทฐ๋ชจ๋ธ์์ API ํธ์ถ
class PostsViewModel: ObservableObject {
@Published var posts: [Post] = []
@Published var isLoading = false
@Published var errorMessage: String?
func loadPosts() {
isLoading = true
errorMessage = nil
Task {
do {
let fetchedPosts = try await APIService.shared.fetchPosts()
await MainActor.run {
self.posts = fetchedPosts
self.isLoading = false
}
} catch {
await MainActor.run {
self.errorMessage = "๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค: \(error.localizedDescription)"
self.isLoading = false
}
}
}
}
}
// ๋ทฐ์์ ์ฌ์ฉ
struct PostsView: View {
@StateObject private var viewModel = PostsViewModel()
var body: some View {
NavigationView {
Group {
if viewModel.isLoading {
ProgressView("๋ก๋ฉ ์ค...")
} else if let error = viewModel.errorMessage {
VStack {
Text(error)
.foregroundColor(.red)
Button("๋ค์ ์๋") {
viewModel.loadPosts()
}
}
} else {
List(viewModel.posts) { post in
VStack(alignment: .leading, spacing: 8) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
.padding(.vertical, 4)
}
}
}
.navigationTitle("๊ฒ์๊ธ")
.onAppear {
viewModel.loadPosts()
}
}
}
}
๐ก async/await์ ์ฅ์
Swift 5.5๋ถํฐ ๋์ ๋ async/await๋ ๋น๋๊ธฐ ์ฝ๋๋ฅผ ๋๊ธฐ ์ฝ๋์ฒ๋ผ ์ฝ๊ธฐ ์ฝ๊ฒ ๋ง๋ค์ด์ค. ์ฝ๋ฐฑ ์ง์ฅ์์ ๋ฒ์ด๋ ์ ์์ง ใ ใ ใ MainActor.run์ ์ฌ์ฉํ๋ฉด UI ์ ๋ฐ์ดํธ๋ฅผ ๋ฉ์ธ ์ค๋ ๋์์ ์์ ํ๊ฒ ํ ์ ์์ด!
๐พ ๋ฐ์ดํฐ ์์์ฑ - UserDefaults, CoreData, SwiftData
์ฑ์ ์ข ๋ฃํด๋ ๋ฐ์ดํฐ๊ฐ ๋จ์์์ด์ผ ํ์์? ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ์ฌ๋ฌ ๋ฐฉ๋ฒ์ ์์๋ณด์!
UserDefaults - ๊ฐ๋จํ ์ค์ ๊ฐ ์ ์ฅ
class SettingsManager: ObservableObject {
@Published var isDarkMode: Bool {
didSet {
UserDefaults.standard.set(isDarkMode, forKey: "isDarkMode")
}
}
@Published var fontSize: Double {
didSet {
UserDefaults.standard.set(fontSize, forKey: "fontSize")
}
}
init() {
self.isDarkMode = UserDefaults.standard.bool(forKey: "isDarkMode")
self.fontSize = UserDefaults.standard.double(forKey: "fontSize")
if fontSize == 0 {
fontSize = 16.0 // ๊ธฐ๋ณธ๊ฐ
}
}
}
struct SettingsView: View {
@StateObject private var settings = SettingsManager()
var body: some View {
Form {
Section("ํ๋ฉด ์ค์ ") {
Toggle("๋คํฌ ๋ชจ๋", isOn: $settings.isDarkMode)
HStack {
Text("๊ธ์ ํฌ๊ธฐ")
Slider(value: $settings.fontSize, in: 12...24, step: 1)
Text("\(Int(settings.fontSize))")
}
}
}
.navigationTitle("์ค์ ")
}
}
CoreData - ๋ณต์กํ ๋ฐ์ดํฐ ๊ด๋ฆฌ
// CoreData ์คํ ์ค์
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "MyApp")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("CoreData ๋ก๋ ์คํจ: \(error)")
}
}
}
func save() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
print("์ ์ฅ ์คํจ: \(error)")
}
}
}
}
// ๋ทฐ์์ CoreData ์ฌ์ฉ
struct NoteListView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Note.timestamp, ascending: false)],
animation: .default)
private var notes: FetchedResults<Note>
var body: some View {
NavigationView {
List {
ForEach(notes) { note in
VStack(alignment: .leading) {
Text(note.title ?? "์ ๋ชฉ ์์")
.font(.headline)
Text(note.content ?? "")
.font(.subheadline)
.lineLimit(2)
}
}
.onDelete(perform: deleteNotes)
}
.navigationTitle("๋
ธํธ")
.toolbar {
Button(action: addNote) {
Label("์ถ๊ฐ", systemImage: "plus")
}
}
}
}
private func addNote() {
withAnimation {
let newNote = Note(context: viewContext)
newNote.id = UUID()
newNote.title = "์ ๋
ธํธ"
newNote.content = ""
newNote.timestamp = Date()
PersistenceController.shared.save()
}
}
private func deleteNotes(offsets: IndexSet) {
withAnimation {
offsets.map { notes[$0] }.forEach(viewContext.delete)
PersistenceController.shared.save()
}
}
}
SwiftData - iOS 17์ ์๋ก์ด ๋ฐ์ดํฐ ํ๋ ์์ํฌ ๐
import SwiftData
// ๋ชจ๋ธ ์ ์
@Model
class Task {
var id: UUID
var title: String
var isCompleted: Bool
var createdAt: Date
init(title: String) {
self.id = UUID()
self.title = title
self.isCompleted = false
self.createdAt = Date()
}
}
// ์ฑ์์ SwiftData ์ค์
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Task.self)
}
}
// ๋ทฐ์์ ์ฌ์ฉ
struct TaskListView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]
var body: some View {
List {
ForEach(tasks) { task in
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.onTapGesture {
task.isCompleted.toggle()
}
Text(task.title)
}
}
.onDelete { indexSet in
for index in indexSet {
modelContext.delete(tasks[index])
}
}
}
}
}
๐ฏ ๋ฐ์ดํฐ ์ ์ฅ ๋ฐฉ๋ฒ ์ ํ ๊ฐ์ด๋
UserDefaults: ๊ฐ๋จํ ์ค์ ๊ฐ, ์์ ๋ฐ์ดํฐ (๋คํฌ๋ชจ๋, ์ธ์ด ์ค์ ๋ฑ)
FileManager: ์ด๋ฏธ์ง, ๋ฌธ์ ํ์ผ ๋ฑ ํฐ ํ์ผ
CoreData: ๋ณต์กํ ๊ด๊ณํ ๋ฐ์ดํฐ, iOS 3.0๋ถํฐ ์ฌ์ฉ ๊ฐ๋ฅ
SwiftData: ํ๋์ ์ด๊ณ ๊ฐ๊ฒฐํ ๋ฌธ๋ฒ, iOS 17 ์ด์
Realm: ์๋ํํฐ DB, ๋น ๋ฅธ ์ฑ๋ฅ๊ณผ ์ฌ์ด ์ฌ์ฉ
๐๏ธ ์ฑ ์ํคํ ์ฒ ํจํด
์ฝ๋๊ฐ ๋ง์์ง๋ฉด ๊ตฌ์กฐ๊ฐ ์ค์ํด์ ธ. ์ข์ ์ํคํ ์ฒ๋ ์ ์ง๋ณด์๋ฅผ ์ฝ๊ฒ ๋ง๋ค๊ณ , ๋ฒ๊ทธ๋ฅผ ์ค์ฌ์ฃผ๊ณ , ํ ํ์ ์ ์ํํ๊ฒ ํด์ค!
MVVM ํจํด - SwiftUI์ ๋ฒ ์คํธ ํ๋ ๋ ๐ค
MVVM(Model-View-ViewModel)์ SwiftUI์ ์ฐฐ๋ก๊ถํฉ์ด์ผ. ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ด ์๋์ผ๋ก ๋๋๊น ์ฝ๋๊ฐ ์์ฒญ ๊น๋ํด์ง์ง.
// Model - ๋ฐ์ดํฐ ๊ตฌ์กฐ
struct Product: Identifiable {
let id: UUID
var name: String
var price: Double
var quantity: Int
}
// ViewModel - ๋น์ฆ๋์ค ๋ก์ง
class ShoppingCartViewModel: ObservableObject {
@Published var products: [Product] = []
var totalPrice: Double {
products.reduce(0) { $0 + ($1.price * Double($1.quantity)) }
}
var itemCount: Int {
products.reduce(0) { $0 + $1.quantity }
}
func addProduct(_ product: Product) {
if let index = products.firstIndex(where: { $0.id == product.id }) {
products[index].quantity += 1
} else {
products.append(product)
}
}
func removeProduct(_ product: Product) {
products.removeAll { $0.id == product.id }
}
func updateQuantity(for product: Product, quantity: Int) {
if let index = products.firstIndex(where: { $0.id == product.id }) {
if quantity > 0 {
products[index].quantity = quantity
} else {
products.remove(at: index)
}
}
}
}
// View - UI ํํ
struct ShoppingCartView: View {
@StateObject private var viewModel = ShoppingCartViewModel()
var body: some View {
NavigationView {
VStack {
if viewModel.products.isEmpty {
Text("์ฅ๋ฐ๊ตฌ๋๊ฐ ๋น์ด์์ต๋๋ค")
.foregroundColor(.secondary)
} else {
List {
ForEach(viewModel.products) { product in
HStack {
VStack(alignment: .leading) {
Text(product.name)
.font(.headline)
Text("\(product.price, specifier: "%.0f")์")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Stepper("\(product.quantity)",
value: Binding(
get: { product.quantity },
set: { viewModel.updateQuantity(for: product, quantity: $0) }
),
in: 0...99)
}
}
.onDelete { indexSet in
indexSet.forEach { index in
viewModel.removeProduct(viewModel.products[index])
}
}
}
VStack(spacing: 10) {
HStack {
Text("์ด \(viewModel.itemCount)๊ฐ ์ํ")
Spacer()
Text("\(viewModel.totalPrice, specifier: "%.0f")์")
.font(.title2)
.fontWeight(.bold)
}
.padding()
Button("๊ฒฐ์ ํ๊ธฐ") {
// ๊ฒฐ์ ๋ก์ง
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
.background(Color(.systemBackground))
}
}
.navigationTitle("์ฅ๋ฐ๊ตฌ๋")
}
}
}
โ MVVM์ ์ฅ์
โข View์ ๋น์ฆ๋์ค ๋ก์ง์ด ์์ ํ ๋ถ๋ฆฌ๋ผ
โข ViewModel์ ๋
๋ฆฝ์ ์ผ๋ก ํ
์คํธํ ์ ์์ด
โข SwiftUI์ @Published์ ์๋ฒฝํ๊ฒ ํธํ๋ผ
โข ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ด ๋์์ ธ
โข ์ฌ๋ฌ View์์ ๊ฐ์ ViewModel์ ๊ณต์ ํ ์ ์์ด
๐ ๋ณด์๊ณผ ์ธ์ฆ
์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ ์ฑ์ด๋ผ๋ฉด ๋ณด์์ด ์ง์ง ์ค์ํด! ํค์ฒด์ธ, ์์ฒด ์ธ์ฆ ๊ฐ์ ๊ฑธ ์์๋ณด์.
Keychain - ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์์ ํ๊ฒ ์ ์ฅ
import Security
class KeychainManager {
static let shared = KeychainManager()
func save(key: String, data: Data) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary) // ๊ธฐ์กด ํญ๋ชฉ ์ญ์
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
func load(key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
return status == errSecSuccess ? result as? Data : nil
}
func delete(key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess
}
}
// ์ฌ์ฉ ์์
extension KeychainManager {
func saveToken(_ token: String) -> Bool {
guard let data = token.data(using: .utf8) else { return false }
return save(key: "authToken", data: data)
}
func loadToken() -> String? {
guard let data = load(key: "authToken") else { return nil }
return String(data: data, encoding: .utf8)
}
func deleteToken() -> Bool {
return delete(key: "authToken")
}
}
์์ฒด ์ธ์ฆ (Face ID / Touch ID) ๐ค
import LocalAuthentication
class BiometricAuthManager {
static let shared = BiometricAuthManager()
func canUseBiometrics() -> Bool {
let context = LAContext()
var error: NSError?
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
}
func getBiometricType() -> String {
let context = LAContext()
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
switch context.biometryType {
case .faceID:
return "Face ID"
case .touchID:
return "Touch ID"
case .none:
return "์์"
@unknown default:
return "์ ์ ์์"
}
}
func authenticate(reason: String, completion: @escaping (Bool, Error?) -> Void) {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
completion(false, error)
return
}
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason) { success, error in
DispatchQueue.main.async {
completion(success, error)
}
}
}
}
// SwiftUI์์ ์ฌ์ฉ
struct BiometricLoginView: View {
@State private var isUnlocked = false
@State private var showError = false
@State private var errorMessage = ""
var body: some View {
VStack(spacing: 30) {
if isUnlocked {
Text("์ธ์ฆ ์ฑ๊ณต! ๐")
.font(.title)
.foregroundColor(.green)
} else {
Image(systemName: "faceid")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
Text("์ฑ์ ์ฌ์ฉํ๋ ค๋ฉด ์ธ์ฆ์ด ํ์ํฉ๋๋ค")
.multilineTextAlignment(.center)
Button("์ธ์ฆํ๊ธฐ") {
authenticate()
}
.buttonStyle(.borderedProminent)
}
}
.alert("์ธ์ฆ ์คํจ", isPresented: $showError) {
Button("ํ์ธ", role: .cancel) { }
} message: {
Text(errorMessage)
}
}
private func authenticate() {
BiometricAuthManager.shared.authenticate(reason: "์ฑ์ ์ ๊ทผํ๊ธฐ ์ํด ์ธ์ฆ์ด ํ์ํฉ๋๋ค") { success, error in
if success {
isUnlocked = true
} else {
errorMessage = error?.localizedDescription ?? "์ธ์ฆ์ ์คํจํ์ต๋๋ค"
showError = true
}
}
}
}
๐ฑ ์ค์ ํ๋ก์ ํธ - ๋ ์จ ์ฑ ๋ง๋ค๊ธฐ
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์์ ใ ใ ใ ์ค์ ๋ก ๋์ํ๋ ๋ ์จ ์ฑ์ ๋ง๋ค์ด๋ณด์! OpenWeatherMap API๋ฅผ ์ฌ์ฉํ ๊ฑฐ์ผ.
1๋จ๊ณ: ๋ฐ์ดํฐ ๋ชจ๋ธ ์ ์
// ๋ ์จ ์๋ต ๋ชจ๋ธ
struct WeatherResponse: Codable {
let name: String
let main: MainWeather
let weather: [Weather]
let wind: Wind
}
struct MainWeather: Codable {
let temp: Double
let feelsLike: Double
let humidity: Int
enum CodingKeys: String, CodingKey {
case temp
case feelsLike = "feels_like"
case humidity
}
}
struct Weather: Codable {
let id: Int
let main: String
let description: String
let icon: String
}
struct Wind: Codable {
let speed: Double
}
2๋จ๊ณ: API ์๋น์ค ๊ตฌํ
class WeatherService {
static let shared = WeatherService()
private let apiKey = "YOUR_API_KEY" // OpenWeatherMap์์ ๋ฐ๊ธ๋ฐ์ ํค
private let baseURL = "https://api.openweathermap.org/data/2.5/weather"
func fetchWeather(city: String) async throws -> WeatherResponse {
var components = URLComponents(string: baseURL)
components?.queryItems = [
URLQueryItem(name: "q", value: city),
URLQueryItem(name: "appid", value: apiKey),
URLQueryItem(name: "units", value: "metric"),
URLQueryItem(name: "lang", value: "kr")
]
guard let url = components?.url else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let weather = try JSONDecoder().decode(WeatherResponse.self, from: data)
return weather
}
func getWeatherIcon(code: String) -> String {
// ๋ ์จ ์์ด์ฝ ๋งคํ
switch code {
case "01d": return "sun.max.fill"
case "01n": return "moon.stars.fill"
case "02d", "02n": return "cloud.sun.fill"
case "03d", "03n", "04d", "04n": return "cloud.fill"
case "09d", "09n": return "cloud.rain.fill"
case "10d", "10n": return "cloud.heavyrain.fill"
case "11d", "11n": return "cloud.bolt.fill"
case "13d", "13n": return "snow"
case "50d", "50n": return "cloud.fog.fill"
default: return "questionmark"
}
}
}
3๋จ๊ณ: ViewModel ๊ตฌํ
class WeatherViewModel: ObservableObject {
@Published var weather: WeatherResponse?
@Published var isLoading = false
@Published var errorMessage: String?
func loadWeather(for city: String) {
guard !city.isEmpty else { return }
isLoading = true
errorMessage = nil
Task {
do {
let fetchedWeather = try await WeatherService.shared.fetchWeather(city: city)
await MainActor.run {
self.weather = fetchedWeather
self.isLoading = false
}
} catch {
await MainActor.run {
self.errorMessage = "๋ ์จ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค"
self.isLoading = false
}
}
}
}
}
4๋จ๊ณ: UI ๊ตฌํ
struct WeatherView: View {
@StateObject private var viewModel = WeatherViewModel()
@State private var cityName = "Seoul"
var body: some View {
NavigationView {
ZStack {
// ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋์ธํธ
LinearGradient(
colors: [Color.blue.opacity(0.6), Color.purple.opacity(0.4)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
VStack(spacing: 20) {
// ๊ฒ์ ๋ฐ
HStack {
TextField("๋์ ์ด๋ฆ", text: $cityName)
.textFieldStyle(.roundedBorder)
Button(action: {
viewModel.loadWeather(for: cityName)
}) {
Image(systemName: "magnifyingglass")
.padding(10)
.background(Color.white.opacity(0.3))
.clipShape(Circle())
}
}
.padding()
if viewModel.isLoading {
ProgressView()
.scaleEffect(1.5)
.tint(.white)
} else if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.white)
.padding()
} else if let weather = viewModel.weather {
weatherContent(weather)
} else {
Text("๋์๋ฅผ ๊ฒ์ํด์ฃผ์ธ์")
.foregroundColor(.white)
.font(.title3)
}
Spacer()
}
}
.navigationTitle("๋ ์จ")
.navigationBarTitleDisplayMode(.inline)
}
.onAppear {
viewModel.loadWeather(for: cityName)
}
}
@ViewBuilder
private func weatherContent(_ weather: WeatherResponse) -> some View {
VStack(spacing: 30) {
// ๋์ ์ด๋ฆ
Text(weather.name)
.font(.system(size: 40, weight: .medium))
.foregroundColor(.white)
// ๋ ์จ ์์ด์ฝ
if let weatherInfo = weather.weather.first {
Image(systemName: WeatherService.shared.getWeatherIcon(code: weatherInfo.icon))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 120, height: 120)
.foregroundColor(.white)
.shadow(radius: 10)
Text(weatherInfo.description)
.font(.title2)
.foregroundColor(.white)
}
// ์จ๋
Text("\(Int(weather.main.temp))ยฐC")
.font(.system(size: 70, weight: .thin))
.foregroundColor(.white)
Text("์ฒด๊ฐ ์จ๋: \(Int(weather.main.feelsLike))ยฐC")
.font(.title3)
.foregroundColor(.white.opacity(0.8))
// ์์ธ ์ ๋ณด
HStack(spacing: 50) {
VStack {
Image(systemName: "humidity.fill")
.font(.title2)
Text("\(weather.main.humidity)%")
.font(.title3)
Text("์ต๋")
.font(.caption)
}
VStack {
Image(systemName: "wind")
.font(.title2)
Text("\(String(format: "%.1f", weather.wind.speed))m/s")
.font(.title3)
Text("ํ์")
.font(.caption)
}
}
.foregroundColor(.white)
.padding()
.background(Color.white.opacity(0.2))
.cornerRadius(15)
}
.padding()
}
}
๐ก API ํค ๋ณด์
์ค์ ์ฑ์ ๋ฐฐํฌํ ๋๋ API ํค๋ฅผ ์ฝ๋์ ์ง์ ๋ฃ์ผ๋ฉด ์ ๋ผ! .xcconfig ํ์ผ์ด๋ ํ๊ฒฝ ๋ณ์๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, ์๋ฒ๋ฅผ ๊ฑฐ์ณ์ API๋ฅผ ํธ์ถํ๋ ๊ฒ ์์ ํด. GitHub์ ์ฌ๋ฆด ๋๋ .gitignore์ API ํค ํ์ผ์ ์ถ๊ฐํ๋ ๊ฑฐ ์์ง ๋ง!
๐งช ํ ์คํธ์ ๋๋ฒ๊น
๋ฒ๊ทธ ์๋ ์ฝ๋๋ ์์ด ใ ใ ใ ํ ์คํธ์ ๋๋ฒ๊น ๊ธฐ์ ์ ์ตํ๋๋ฉด ๊ฐ๋ฐ ์๋๊ฐ ํจ์ฌ ๋นจ๋ผ์ ธ!
Unit Test - ๋จ์ ํ ์คํธ
import XCTest
@testable import MyApp
class CalculatorTests: XCTestCase {
var calculator: Calculator!
override func setUp() {
super.setUp()
calculator = Calculator()
}
override func tearDown() {
calculator = nil
super.tearDown()
}
func testAddition() {
let result = calculator.add(5, 3)
XCTAssertEqual(result, 8, "5 + 3์ 8์ด์ด์ผ ํฉ๋๋ค")
}
func testDivision() {
let result = calculator.divide(10, 2)
XCTAssertEqual(result, 5, "10 / 2๋ 5์ฌ์ผ ํฉ๋๋ค")
}
func testDivisionByZero() {
XCTAssertThrowsError(try calculator.divide(10, 0)) { error in
XCTAssertEqual(error as? CalculatorError, .divisionByZero)
}
}
}
// ViewModel ํ
์คํธ
class WeatherViewModelTests: XCTestCase {
var viewModel: WeatherViewModel!
override func setUp() {
super.setUp()
viewModel = WeatherViewModel()
}
func testLoadWeatherSuccess() async {
// Given
let expectation = XCTestExpectation(description: "๋ ์จ ๋ก๋ ์ฑ๊ณต")
// When
viewModel.loadWeather(for: "Seoul")
// Then
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
XCTAssertNotNil(self.viewModel.weather)
XCTAssertNil(self.viewModel.errorMessage)
XCTAssertFalse(self.viewModel.isLoading)
expectation.fulfill()
}
await fulfillment(of: [expectation], timeout: 5)
}
}
UI Test - UI ํ ์คํธ
import XCTest
class MyAppUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
// ๋ก๊ทธ์ธ ํ๋ฉด ์์ ํ์ธ
let usernameField = app.textFields["username"]
XCTAssertTrue(usernameField.exists)
let passwordField = app.secureTextFields["password"]
XCTAssertTrue(passwordField.exists)
// ์
๋ ฅ
usernameField.tap()
usernameField.typeText("testuser")
passwordField.tap()
passwordField.typeText("password123")
// ๋ก๊ทธ์ธ ๋ฒํผ ํญ
app.buttons["๋ก๊ทธ์ธ"].tap()
// ๊ฒฐ๊ณผ ํ์ธ
let welcomeText = app.staticTexts["ํ์ํฉ๋๋ค"]
XCTAssertTrue(welcomeText.waitForExistence(timeout: 3))
}
func testNavigationFlow() {
// ํญ ๋ฐ ํ
์คํธ
let homeTab = app.tabBars.buttons["ํ"]
homeTab.tap()
XCTAssertTrue(app.navigationBars["ํ"].exists)
let profileTab = app.tabBars.buttons["ํ๋กํ"]
profileTab.tap()
XCTAssertTrue(app.navigationBars["ํ๋กํ"].exists)
}
}
๋๋ฒ๊น ํ๊ณผ ํธ๋ฆญ ๐
// 1. print ๋์ os_log ์ฌ์ฉ
import os.log
let logger = Logger(subsystem: "com.myapp", category: "network")
func fetchData() {
logger.debug("๋ฐ์ดํฐ ์์ฒญ ์์")
logger.info("URL: \(url)")
logger.error("์๋ฌ ๋ฐ์: \(error)")
}
// 2. Breakpoint ํ์ฉ
// Xcode์์ ์ค ๋ฒํธ ํด๋ฆญํ๋ฉด ๋ธ๋ ์ดํฌํฌ์ธํธ ์ค์
// ์กฐ๊ฑด๋ถ ๋ธ๋ ์ดํฌํฌ์ธํธ: ์ฐํด๋ฆญ > Edit Breakpoint
// 3. LLDB ๋ช
๋ น์ด
// po (print object): ๊ฐ์ฒด ์ถ๋ ฅ
// p (print): ๊ฐ ์ถ๋ ฅ
// expr (expression): ์ฝ๋ ์คํ
// 4. View Hierarchy ๋๋ฒ๊น
// Debug > View Debugging > Capture View Hierarchy
// 5. Memory Graph ๋๋ฒ๊น
// Debug > Memory Graph
// ๋ฉ๋ชจ๋ฆฌ ๋์ ์ฐพ๊ธฐ์ ์ ์ฉ
๐ ์ฑ ๋ฐฐํฌ์ ์ต์ ํ
์ฑ์ ๋ง๋ค์์ผ๋ฉด ์ด์ ์ธ์์ ๊ณต๊ฐํ ์ฐจ๋ก์ผ! ์ฑ์คํ ์ด ๋ฐฐํฌ ๊ณผ์ ๊ณผ ์ต์ ํ ํ์ ์์๋ณด์.
์ฑ์คํ ์ด ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์คํธ โ
1. ๊ฐ๋ฐ์ ๊ณ์ ์ค๋น
โข Apple Developer Program ๊ฐ์
(์ฐ $99)
โข App Store Connect ์ ๊ทผ ๊ถํ ํ์ธ
2. ์ฑ ์ ๋ณด ์ค๋น
โข ์ฑ ์ด๋ฆ, ๋ถ์ ๋ชฉ, ์ค๋ช
โข ํค์๋ (์ต๋ 100์)
โข ์นดํ
๊ณ ๋ฆฌ ์ ํ
โข ์ฐ๋ น ๋ฑ๊ธ ์ค์
3. ์คํฌ๋ฆฐ์ท๊ณผ ํ๋ฆฌ๋ทฐ
โข ๋ค์ํ ๊ธฐ๊ธฐ ํฌ๊ธฐ๋ณ ์คํฌ๋ฆฐ์ท (ํ์)
โข ์ฑ ํ๋ฆฌ๋ทฐ ๋น๋์ค (์ ํ)
โข ์์ด์ฝ (1024x1024px)
4. ๊ฐ์ธ์ ๋ณด ๋ณดํธ
โข ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ URL
โข ์์งํ๋ ๋ฐ์ดํฐ ์ ํ ๋ช
์
โข ๋ฐ์ดํฐ ์ฌ์ฉ ๋ชฉ์ ์ค๋ช
5. ๋น๋ ์
๋ก๋
โข Archive ์์ฑ
โข Xcode Organizer์์ ์
๋ก๋
โข TestFlight ๋ฒ ํ ํ
์คํธ (์ ํ)
์ฑ๋ฅ ์ต์ ํ ๊ธฐ๋ฒ โก
// 1. ์ด๋ฏธ์ง ์ต์ ํ
struct OptimizedImageView: View {
let imageURL: URL
var body: some View {
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure:
Image(systemName: "photo")
@unknown default:
EmptyView()
}
}
.frame(width: 200, height: 200)
.clipped()
}
}
// 2. LazyStack ์ฌ์ฉ
struct OptimizedListView: View {
let items: [Item]
var body: some View {
ScrollView {
LazyVStack(spacing: 10) { // Lazy๋ก ํ์ํ ๋๋ง ๋ก๋
ForEach(items) { item in
ItemRow(item: item)
}
}
}
}
}
// 3. ๊ณ์ฐ ๊ฒฐ๊ณผ ์บ์ฑ
class DataProcessor {
private var cache: [String: ProcessedData] = [:]
func process(_ input: String) -> ProcessedData {
if let cached = cache[input] {
return cached // ์บ์๋ ๊ฒฐ๊ณผ ๋ฐํ
}
let result = expensiveOperation(input)
cache[input] = result
return result
}
private func expensiveOperation(_ input: String) -> ProcessedData {
// ๋ฌด๊ฑฐ์ด ์ฐ์ฐ
return ProcessedData()
}
}
// 4. ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋ ํ์ฉ
func loadLargeData() {
DispatchQueue.global(qos: .userInitiated).async {
let data = performHeavyTask()
DispatchQueue.main.async {
// UI ์
๋ฐ์ดํธ๋ ๋ฉ์ธ ์ค๋ ๋์์
self.updateUI(with: data)
}
}
}
// 5. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
class ImageCache {
static let shared = ImageCache()
private var cache = NSCache<NSString, UIImage>()
init() {
cache.countLimit = 100 // ์ต๋ 100๊ฐ
cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
}
func setImage(_ image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
}
โ ๏ธ ํํ ์ค์๋ค
โข ๋ฉ์ธ ์ค๋ ๋์์ ๋ฌด๊ฑฐ์ด ์์
์ํ โ ์ฑ์ด ๋ฒ๋ฒ
๊ฑฐ๋ฆผ
โข ๋ฉ๋ชจ๋ฆฌ ๋์ (๊ฐํ ์ฐธ์กฐ ์ํ) โ ์ฑ ํฌ๋์
โข ๊ณผ๋ํ ๋คํธ์ํฌ ์์ฒญ โ ๋ฐฐํฐ๋ฆฌ ์๋ชจ
โข ์ด๋ฏธ์ง ์์ถ ์ ํจ โ ์ฑ ์ฉ๋ ์ฆ๊ฐ
โข ์๋ฌ ์ฒ๋ฆฌ ์ ํจ โ ์์์น ๋ชปํ ํฌ๋์
๐ ํ์ต ๋ฆฌ์์ค์ ์ปค๋ฎค๋ํฐ
ํผ์ ๊ณต๋ถํ๊ธฐ ํ๋ค ๋๊ฐ ์์์? ์ข์ ํ์ต ์๋ฃ์ ์ปค๋ฎค๋ํฐ๋ฅผ ์๊ฐํ ๊ฒ!
๐ ๊ณต์ ๋ฌธ์
โข Swift.org - Swift ์ธ์ด ๊ณต์ ๋ฌธ์
โข Apple Developer Documentation - ๋ชจ๋ ํ๋ ์์ํฌ ๋ฌธ์
โข Human Interface Guidelines - ๋์์ธ ๊ฐ์ด๋๋ผ์ธ
โข WWDC Videos - ์ ํ ๊ฐ๋ฐ์ ์ปจํผ๋ฐ์ค ์์
๐ป ์จ๋ผ์ธ ๊ฐ์
โข Stanford CS193p - ๋ฌด๋ฃ SwiftUI ๊ฐ์ (์ต๊ณ !)
โข Hacking with Swift - Paul Hudson์ ๋ฌด๋ฃ ํํ ๋ฆฌ์ผ
โข Ray Wenderlich - ์ ๋ฃ/๋ฌด๋ฃ ๊ฐ์ ํผํฉ
โข Udemy - ๋ค์ํ ํ๊ธ ๊ฐ์
๐ฅ ์ปค๋ฎค๋ํฐ
โข Swift Forums - ๊ณต์ ํฌ๋ผ
โข Stack Overflow - ์ง๋ฌธ/๋ต๋ณ
โข Reddit r/swift, r/iOSProgramming
โข ํ๊ตญ iOS ๊ฐ๋ฐ์ ๋ชจ์ (ํ์ด์ค๋ถ ๊ทธ๋ฃน)
โข Let'Swift - ํ๊ตญ Swift ์ปจํผ๋ฐ์ค
๐ฑ ์ ์ฉํ ๋๊ตฌ
โข SF Symbols - ์ ํ ์์ด์ฝ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
โข Figma - UI ๋์์ธ ๋๊ตฌ
โข Postman - API ํ
์คํธ
โข Charles Proxy - ๋คํธ์ํฌ ๋๋ฒ๊น
โข Instruments - ์ฑ๋ฅ ๋ถ์
๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท์์๋ iOS ๊ฐ๋ฐ ๊ด๋ จ ๋ฉํ ๋ง์ด๋ ๊ฐ์๋ฅผ ์ฐพ์ ์ ์์ด! ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค์๊ฒ ์ง์ ๋ฐฐ์ธ ์ ์๋ ๊ธฐํ๋๊น ํ๋ฒ ๋๋ฌ๋ณด๋ ๊ฒ๋ ์ข์ ๊ฑฐ์ผ ใ ใ
๐ฎ iOS ๊ฐ๋ฐ์ ๋ฏธ๋
๊ธฐ์ ์ ๊ณ์ ๋ฐ์ ํ์์? ์์ผ๋ก ์ฃผ๋ชฉํด์ผ ํ ํธ๋ ๋๋ค์ ์ดํด๋ณด์!
SwiftUI์ ์งํ ๐ฑ
SwiftUI๋ ๋งค๋ WWDC์์ ์์ฒญ๋ ์ ๋ฐ์ดํธ๋ฅผ ๋ฐ๊ณ ์์ด. iOS 17์์๋ ๋ ๋ง์ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋๊ณ , UIKit๊ณผ์ ํตํฉ๋ ๋ ์ฌ์์ก์ง. ์์ผ๋ก ๋ช ๋ ์์ UIKit์ ์์ ํ ๋์ฒดํ ๊ฐ๋ฅ์ฑ๋ ์์ด!
Swift Concurrency โก
async/await, Actor, TaskGroup ๊ฐ์ ๋์์ฑ ๊ธฐ๋ฅ๋ค์ด ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด. ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ์ด ํจ์ฌ ์ฝ๊ณ ์์ ํด์ก์ง. ์ด์ GCD๋ Operation Queue ๋์ Swift Concurrency๋ฅผ ์ฐ๋ ๊ฒ ํ์ค์ด ๋๊ณ ์์ด.
SwiftData ๐พ
iOS 17์์ ๋์จ SwiftData๋ CoreData๋ฅผ ๋์ฒดํ ์ฐจ์ธ๋ ๋ฐ์ดํฐ ํ๋ ์์ํฌ์ผ. ๋ฌธ๋ฒ์ด ํจ์ฌ ๊ฐ๊ฒฐํ๊ณ SwiftUI์ ์๋ฒฝํ๊ฒ ํตํฉ๋ผ. ์์ผ๋ก ์ ํ๋ก์ ํธ๋ SwiftData๋ฅผ ์ฐ๋ ๊ฒ ๋์ธ๊ฐ ๋ ๊ฑฐ์ผ.
Vision Pro์ visionOS ๐ฅฝ
์ ํ์ ๊ณต๊ฐ ์ปดํจํ ํ๋ซํผ์ธ Vision Pro๊ฐ ์ถ์๋๋ฉด์ visionOS ๊ฐ๋ฐ๋ ์ฃผ๋ชฉ๋ฐ๊ณ ์์ด. SwiftUI๋ก 3D ๊ณต๊ฐ UI๋ฅผ ๋ง๋ค ์ ์๋ค๋ ์ง์ง ๋ฏธ๋์ ์ด์ง ์์? ใ ใ ใ
AI/ML ํตํฉ ๐ค
Core ML, Create ML๋ก ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ์ ์ฑ์ ํตํฉํ๋ ๊ฒ ์ ์ ์ฌ์์ง๊ณ ์์ด. ์ด๋ฏธ์ง ์ธ์, ์์ฐ์ด ์ฒ๋ฆฌ, ์ถ์ฒ ์์คํ ๊ฐ์ ๊ธฐ๋ฅ์ ์ง์ ๊ตฌํํ ์ ์์ง. ์์ผ๋ก๋ AI ๊ธฐ๋ฅ์ด ์๋ ์ฑ์ด ์ด์ํ ์ ๋๊ฐ ๋ ๊ฑฐ์ผ.
ํฌ๋ก์ค ํ๋ซํผ ๊ฐ๋ฐ ๐
SwiftUI๋ iOS, macOS, watchOS, tvOS๋ฅผ ๋ชจ๋ ์ง์ํด. ํ๋์ ์ฝ๋๋ฒ ์ด์ค๋ก ์ฌ๋ฌ ํ๋ซํผ์ ๋ฐฐํฌํ ์ ์๋ค๋ ๊ฑด ์์ฒญ๋ ์ฅ์ ์ด์ผ. ๊ฐ๋ฐ ์๊ฐ๊ณผ ๋น์ฉ์ ํฌ๊ฒ ์ค์ผ ์ ์๊ฑฐ๋ !
๐ผ ์ทจ์ ๊ณผ ์ปค๋ฆฌ์ด
iOS ๊ฐ๋ฐ์๋ก ์ทจ์ ํ๋ ค๋ฉด ๋ญ ์ค๋นํด์ผ ํ ๊น? ์ค๋ฌด์์ ์๊ตฌํ๋ ์คํฌ๋ค์ ์ ๋ฆฌํด๋ดค์ด!
โ ์ฃผ๋์ด ๊ฐ๋ฐ์ ํ์ ์คํฌ
๊ธฐ์ ์คํฌ:
โข Swift ์ธ์ด ๊ธฐ์ด (๋ฌธ๋ฒ, ์ต์
๋, ํด๋ก์ ๋ฑ)
โข SwiftUI ๋๋ UIKit ์ค ํ๋ ์ด์
โข Git/GitHub ์ฌ์ฉ๋ฒ
โข RESTful API ํต์
โข ๊ธฐ๋ณธ์ ์ธ ๋์์ธ ํจํด (MVC, MVVM)
์ํํธ ์คํฌ:
โข ๋ฌธ์ ํด๊ฒฐ ๋ฅ๋ ฅ
โข ์ฝ๋ ๋ฆฌ๋ทฐ ๊ฒฝํ
โข ํ์
๋๊ตฌ ์ฌ์ฉ (Jira, Slack ๋ฑ)
โข ๊ธฐ์ ๋ฌธ์ ์์ฑ ๋ฅ๋ ฅ
โข ์ง์์ ์ธ ํ์ต ์์ง
ํฌํธํด๋ฆฌ์ค:
โข ์ฑ์คํ ์ด์ ์ถ์ํ ์ฑ 1~2๊ฐ
โข GitHub์ ์ ๋ฆฌ๋ ํ๋ก์ ํธ
โข ๊ธฐ์ ๋ธ๋ก๊ทธ ์ด์ (์ ํ)
โข ์คํ์์ค ๊ธฐ์ฌ ๊ฒฝํ (์ ํ)
์ค๋ฌด ํ๋ก์ ํธ ์์ด๋์ด ๐ก
ํฌํธํด๋ฆฌ์ค์ฉ์ผ๋ก ๋ง๋ค๊ธฐ ์ข์ ์ฑ๋ค์ ์ถ์ฒํด์ค๊ฒ:
1. ํ ์ผ ๊ด๋ฆฌ ์ฑ
โข CoreData/SwiftData๋ก ๋ฐ์ดํฐ ์ ์ฅ
โข ์นดํ
๊ณ ๋ฆฌ, ์ฐ์ ์์, ๋ง๊ฐ์ผ ๊ธฐ๋ฅ
โข ์์ ฏ ์ง์
โข ๋คํฌ ๋ชจ๋ ์ง์
2. ๋ ์จ ์ฑ
โข OpenWeatherMap API ์ฐ๋
โข ์์น ๊ธฐ๋ฐ ๋ ์จ ์ ๋ณด
โข ์ ๋๋ฉ์ด์
ํจ๊ณผ
โข ์๋ฆผ ๊ธฐ๋ฅ
3. SNS ํด๋ก
โข Firebase ๋ฐฑ์๋
โข ์ด๋ฏธ์ง ์
๋ก๋/๋ค์ด๋ก๋
โข ์ค์๊ฐ ์ฑํ
โข ํธ์ ์๋ฆผ
4. ์ํ/์ฑ
์ถ์ฒ ์ฑ
โข TMDB API ๋๋ ์๋ผ๋ API
โข ๊ฒ์ ๊ธฐ๋ฅ
โข ์ฆ๊ฒจ์ฐพ๊ธฐ/์์๋ฆฌ์คํธ
โข ๋ฆฌ๋ทฐ ์์ฑ
5. ๊ฐ๊ณ๋ถ ์ฑ
โข ์์
/์ง์ถ ๊ด๋ฆฌ
โข ์นดํ
๊ณ ๋ฆฌ๋ณ ํต๊ณ
โข ์ฐจํธ/๊ทธ๋ํ ์๊ฐํ
โข CSV ๋ด๋ณด๋ด๊ธฐ
๐ก ํฌํธํด๋ฆฌ์ค ์์ฑ ํ
โข README.md๋ฅผ ์์ธํ๊ฒ ์์ฑํด (๊ธฐ๋ฅ, ๊ธฐ์ ์คํ, ์คํฌ๋ฆฐ์ท)
โข ์ฝ๋์ ์ฃผ์์ ๋ฌ์์ ์๋๋ฅผ ๋ช
ํํ ํด
โข ์ปค๋ฐ ๋ฉ์์ง๋ฅผ ์๋ฏธ์๊ฒ ์์ฑํด
โข ์ฑ์คํ ์ด ๋งํฌ๊ฐ ์์ผ๋ฉด ํฐ ํ๋ฌ์ค!
โข ๊ธฐ์ ๋ธ๋ก๊ทธ์ ๊ฐ๋ฐ ๊ณผ์ ์ ์ ๋ฆฌํ๋ฉด ๋ ์ข์
๐ฏ ์ค๋ฌด ๊ฐ๋ฐ ํ
ํ๊ต๋ ๊ฐ์์์๋ ์ ์๋ ค์ฃผ๋ ์ค๋ฌด ๊ฟํ๋ค์ ๊ณต์ ํ ๊ฒ ใ ใ ใ
1. ์ฝ๋ ๋ฆฌ๋ทฐ ๋ฌธํ ๐
// ๋์ ์
func getData() {
// 100์ค์ ๋ณต์กํ ์ฝ๋...
}
// ์ข์ ์
func fetchUserData() {
validateInput()
performNetworkRequest()
parseResponse()
updateUI()
}
private func validateInput() {
// ์
๋ ฅ ๊ฒ์ฆ
}
private func performNetworkRequest() {
// ๋คํธ์ํฌ ์์ฒญ
}
// ํจ์๋ฅผ ์๊ฒ ๋๋๊ณ , ์ด๋ฆ์ ๋ช
ํํ๊ฒ!
2. ์๋ฌ ์ฒ๋ฆฌ ์ ๋ต ๐จ
enum NetworkError: LocalizedError {
case invalidURL
case noData
case decodingError
case serverError(statusCode: Int)
var errorDescription: String? {
switch self {
case .invalidURL:
return "์๋ชป๋ URL์
๋๋ค"
case .noData:
return "๋ฐ์ดํฐ๋ฅผ ๋ฐ์ง ๋ชปํ์ต๋๋ค"
case .decodingError:
return "๋ฐ์ดํฐ ํ์ฑ์ ์คํจํ์ต๋๋ค"
case .serverError(let code):
return "์๋ฒ ์๋ฌ (์ฝ๋: \(code))"
}
}
}
// ์ฌ์ฉ์์๊ฒ ์น์ ํ ์๋ฌ ๋ฉ์์ง ์ ๊ณต!
3. ์์กด์ฑ ์ฃผ์ ๐
// ํ
์คํธํ๊ธฐ ์ด๋ ค์ด ์ฝ๋
class UserViewModel {
func loadUser() {
let user = APIService.shared.fetchUser() // ํ๋์ฝ๋ฉ๋ ์์กด์ฑ
// ...
}
}
// ํ
์คํธํ๊ธฐ ์ฌ์ด ์ฝ๋
protocol UserServiceProtocol {
func fetchUser() async throws -> User
}
class UserViewModel {
private let userService: UserServiceProtocol
init(userService: UserServiceProtocol = APIService.shared) {
self.userService = userService
}
func loadUser() async {
do {
let user = try await userService.fetchUser()
// ...
} catch {
// ์๋ฌ ์ฒ๋ฆฌ
}
}
}
// ํ
์คํธ์์ Mock ๊ฐ์ฒด ์ฃผ์
๊ฐ๋ฅ!
4. ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ ๐
// Config.xcconfig ํ์ผ ์์ฑ
API_BASE_URL = https:/$()/api.example.com
API_KEY = your_secret_key_here
// Info.plist์ ์ถ๊ฐ
<key>APIBaseURL</key>
<string>$(API_BASE_URL)</string>
// Swift์์ ์ฝ๊ธฐ
enum Config {
static var apiBaseURL: String {
guard let url = Bundle.main.object(forInfoDictionaryKey: "APIBaseURL") as? String else {
fatalError("API Base URL not found")
}
return url
}
}
// ๊ฐ๋ฐ/์คํ
์ด์ง/ํ๋ก๋์
ํ๊ฒฝ๋ณ๋ก ๋ค๋ฅธ ์ค์ ์ฌ์ฉ!
5. ๋ก๊น ์ ๋ต ๐
import os.log
enum LogCategory: String {
case network = "Network"
case database = "Database"
case ui = "UI"
case general = "General"
}
class AppLogger {
static func log(_ message: String, category: LogCategory = .general, type: OSLogType = .default) {
let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: category.rawValue)
switch type {
case .debug:
logger.debug("\(message)")
case .info:
logger.info("\(message)")
case .error:
logger.error("\(message)")
case .fault:
logger.fault("\(message)")
default:
logger.log("\(message)")
}
}
}
// ์ฌ์ฉ
AppLogger.log("API ์์ฒญ ์์", category: .network, type: .info)
AppLogger.log("๋ฐ์ดํฐ ์ ์ฅ ์คํจ", category: .database, type: .error)
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์... ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ง์ง ๋๋จํด ใ ใ ใ ์์ฒญ ๊ธด ๊ธ์ด์๋๋ฐ ๋๊น์ง ๋ฐ๋ผ์์ค์ ๊ณ ๋ง์! ๐
Swift์ SwiftUI๋ก iOS ์ฑ์ ๊ฐ๋ฐํ๋ ๊ฑด ์ฒ์์ ์ด๋ ค์ ๋ณด์ผ ์ ์์ด. ๊ทผ๋ฐ ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐฐ์ฐ๋ค ๋ณด๋ฉด ์ด๋์ ๋ฉ์ง ์ฑ์ ๋ง๋ค๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ. ๋๋ ์ฒ์์ "Hello, World!" ์ถ๋ ฅํ๋ ๊ฒ๋ ์ ๊ธฐํ๋๋ฐ, ์ง๊ธ์ ๋ณต์กํ ์ฑ๋ ๋๋ฑ ๋ง๋ค ์ ์๊ฒ ๋๊ฑฐ๋ ใ ใ
์ค์ํ ๊ฑด ํฌ๊ธฐํ์ง ์๋ ๊ฑฐ์ผ. ์๋ฌ๊ฐ ๋๋, ์ฑ์ด ํฌ๋์ ๋๋, ์ดํด๊ฐ ์ ๋๋ ๊ฐ๋ ์ด ์์ด๋ ๊ณ์ ๋์ ํ๋ค ๋ณด๋ฉด ๋ฐ๋์ ์ค๋ ฅ์ด ๋์ด. ์คํ์ค๋ฒํ๋ก์ฐ, ๊ณต์ ๋ฌธ์, ์ปค๋ฎค๋ํฐ๋ฅผ ์ ๊ทน ํ์ฉํ๊ณ , ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ ๊ท๋ชจ๋ฅผ ํค์๋๊ฐ๋ ๊ฑฐ์ง!
๊ทธ๋ฆฌ๊ณ ํผ์ ๊ณต๋ถํ๊ธฐ ํ๋ค ๋๋ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ฉํ ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ์ค๋ฌด ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์์๊ฒ ์ง์ ๋ฐฐ์ฐ๋ฉด ํจ์ฌ ๋น ๋ฅด๊ฒ ์ฑ์ฅํ ์ ์๊ฑฐ๋ . ์ฝ๋ ๋ฆฌ๋ทฐ๋ ๋ฐ๊ณ , ์ค๋ฌด ํ๋ ์ป๊ณ , ์ปค๋ฆฌ์ด ์กฐ์ธ๋ ๋ค์ ์ ์์ผ๋๊น ์ผ์์ผ์กฐ์ง ใ ใ ใ
์์ผ๋ก์ ํ์ต ๋ก๋๋งต ๐บ๏ธ
1๋จ๊ณ (1~2๊ฐ์): Swift ๊ธฐ๋ณธ ๋ฌธ๋ฒ ์๋ฒฝํ ์ตํ๊ธฐ
๋ณ์, ํจ์, ํด๋์ค, ํ๋กํ ์ฝ ๋ฑ ๊ธฐ์ด๋ฅผ ํํํ!
2๋จ๊ณ (2~3๊ฐ์): SwiftUI๋ก ๊ฐ๋จํ ์ฑ ๋ง๋ค๊ธฐ
ํ ์ผ ๊ด๋ฆฌ, ๊ณ์ฐ๊ธฐ, ๋ฉ๋ชจ์ฅ ๊ฐ์ ๊ธฐ๋ณธ ์ฑ๋ค
3๋จ๊ณ (3~4๊ฐ์): API ์ฐ๋๊ณผ ๋ฐ์ดํฐ ๊ด๋ฆฌ
๋คํธ์ํน, JSON ํ์ฑ, CoreData/SwiftData
4๋จ๊ณ (4~6๊ฐ์): ์ค์ ํ๋ก์ ํธ
์ฑ์คํ ์ด์ ์ถ์ํ ์ ์๋ ์์ฑ๋ ๋์ ์ฑ
5๋จ๊ณ (๊ณ์): ๊ณ ๊ธ ์ฃผ์ ์ ์ต์ ๊ธฐ์
Combine, async/await, TCA, ์ฑ๋ฅ ์ต์ ํ ๋ฑ
๐ฏ ์ต์ข ์กฐ์ธ
โข ๋งค์ผ ์ฝ๋ฉํ๊ธฐ: ํ๋ฃจ 30๋ถ์ด๋ผ๋ ๊พธ์คํ ํ๋ ๊ฒ ์ค์ํด
โข ์์ ์ฑ๊ณต ๊ฒฝํ: ๊ฐ๋จํ ๊ธฐ๋ฅ์ด๋ผ๋ ์์ฑํ๋ฉด ์ฑ์ทจ๊ฐ์ด ์๊ฒจ
โข ์ปค๋ฎค๋ํฐ ์ฐธ์ฌ: ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ํตํ๋ฉด์ ๋ฐฐ์
โข ์คํ์์ค ์ฝ๊ธฐ: ์ ๋ง๋ค์ด์ง ์ฝ๋๋ฅผ ๋ณด๋ฉด์ ๋ฐฐ์ฐ๋ ๊ฒ ๋ง์
โข ๋ธ๋ก๊ทธ ์์ฑ: ๋ฐฐ์ด ๊ฑธ ์ ๋ฆฌํ๋ฉด ๋ ์ค๋ ๊ธฐ์ต์ ๋จ์
โข ์คํจ๋ฅผ ๋๋ ค์ํ์ง ๋ง: ์๋ฌ๋ ๋ฐฐ์์ ๊ธฐํ์ผ!
iOS ๊ฐ๋ฐ์ ์ ๋ง ์ฌ๋ฏธ์๊ณ ๋ณด๋์ฐฌ ๋ถ์ผ์ผ. ๋ด๊ฐ ๋ง๋ ์ฑ์ ์ ์ธ๊ณ ์ฌ๋๋ค์ด ์ฌ์ฉํ๋ค๋ ๊ฒ ์ผ๋ง๋ ๋ฉ์ง ์ผ์ด์ผ? ๐ ๊ทธ๋ฆฌ๊ณ iOS ๊ฐ๋ฐ์๋ ์์๋ ๋ง๊ณ ๋์ฐ๋ ์ข์ ํธ์ด๋ผ์ ์ปค๋ฆฌ์ด ์ธก๋ฉด์์๋ ์ข์ ์ ํ์ด์ง.
์ ํ์ ๋งค๋ ์๋ก์ด ๊ธฐ์ ๊ณผ ํ๋ ์์ํฌ๋ฅผ ๋ฐํํ๊ณ ์์ด. SwiftUI๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์๊ณ , Vision Pro ๊ฐ์ ์๋ก์ด ํ๋ซํผ๋ ๋์ค๊ณ ์์์? ๊ทธ๋งํผ ๋ฐฐ์ธ ๊ฒ ๋ง๋ค๋ ๋ป์ด๊ธฐ๋ ํ์ง๋ง, ๋์์ ๊ธฐํ๋ ๋ง๋ค๋ ๊ฑฐ์ผ. ์ง๊ธ ์์ํ๋ฉด ๋ฑ ์ข์ ํ์ด๋ฐ์ด๋ผ๊ณ ์๊ฐํด!
์, ์ด์ Xcode๋ฅผ ์ด๊ณ ์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ด. "Hello, World!"๋ถํฐ ์์ํด์ ์ ์ ๋ณต์กํ ์ฑ์ ๋ง๋ค์ด๊ฐ๋ ๊ฑฐ์ผ. ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ๊ตฌ๊ธ๋งํ๊ณ , ์คํ์ค๋ฒํ๋ก์ฐ ์ฐพ์๋ณด๊ณ , ๊ณต์ ๋ฌธ์ ์ฝ์ด๋ณด๊ณ ... ๊ทธ๋ ๊ฒ ํ๋์ฉ ํด๊ฒฐํด๋๊ฐ๋ค ๋ณด๋ฉด ์ด๋์ ๋ฉ์ง iOS ๊ฐ๋ฐ์๊ฐ ๋์ด ์์ ๊ฑฐ์ผ! ๐ช
ํ์ดํ ! ๋๋ผ๋ฉด ์ถฉ๋ถํ ํ ์ ์์ด! ๐โจ
๐๐ฑ๐ป
Happy Coding with Swift & SwiftUI!
์ด ๊ธ์ด ๋์์ด ๋์๋ค๋ฉด, ์ง์ ์ฑ์ ๋ง๋ค์ด๋ณด๋ฉด์ ์ค๋ ฅ์ ํค์๋ณด์ธ์!
Swift์ SwiftUI๋ก ์ฌ๋ฌ๋ถ๋ง์ ๋ฉ์ง ์ฑ์ ๋ง๋ค์ด๋ณด์ธ์ ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.