๊ฐ๋ฐ ๊ธฐ๊ฐ : 2024. 9. 13 ~ 2024. 10. 3 (3์ฃผ)
๊ฐ๋ฐ ์ธ์ : 1์ธ (๊ธฐํ/๋์์ธ/๊ฐ๋ฐ)
- Language: Swift 5.10
- Framework: SwiftUI
- Minimum Target: iOS 17.0
- Architecture: MVVM
- Design Pattern: Repository Pattern
- Local Database: Realm
- Network: URLSession
- Media: YouTubePlayerKit
- IDE: Xcode 15.3
- ๊ธฐ์์ฒญ API์ URLSession์ ํ์ฉํ ๋น๋๊ธฐ ๋คํธ์ํฌ ํต์ ์ผ๋ก ์ค์๊ฐ ํด์ ๋ฐ์ดํฐ fetching
- APIEndPoint enum๊ณผ Result type์ ํ์ฉํ type-safe ๋คํธ์ํน ๋ ์ด์ด ๊ตฌํ
- Published ํ๋กํผํฐ์ ObservableObject๋ฅผ ํ์ฉํ ์ค์๊ฐ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ๋ฐ UI ์ ๋ฐ์ดํธ
- WKWebView์ YouTubePlayerKit์ ํ์ฉํ ์ด๋ฏธ์ง/๋น๋์ค/์ ํ๋ธ ์คํธ๋ฆฌ๋ฐ ๊ตฌํ
- URL ํจํด ๋งค์นญ์ ํตํ ์๋ ํฌ๋งท ๊ฐ์ง ๋ฐ ์ ์ ํ ๋ทฐ ์ปดํฌ๋ํธ ์ ํ
- ์ฃผ๊ธฐ์ ๋ฐ์ดํฐ ๋ฆฌํ๋ ์์ ์ ๋๋ฉ์ด์ ์ฒ๋ฆฌ๋ก ๋๊น์๋ ์ค์๊ฐ ์์ ์ ๊ณต
- ๋คํฌ๋ชจ๋ ๋์์ ์ํ ๋์ JavaScript ์ธ์ ์
- CoreLocation๊ณผ MapKit ํ๋ ์์ํฌ๋ฅผ ํ์ฉํ ์ค์๊ฐ ์์น ํธ๋ํน
- MKCoordinateRegion๊ณผ MapCameraPosition์ ํ์ฉํ ์ง๋ ๋ทฐ ์ปจํธ๋กค
- MapAnnotation์ ํ์ฉํ ์ปค์คํ ๋ง์ปค ๋ฐ ์ค์๊ฐ ํ๊ณ ์ ๋ณด ๊ตฌํ
- Repository Pattern์ ํ์ฉํ ๋ฐ์ดํฐ ์ ๊ทผ ๊ณ์ธต ์ถ์ํ
- Protocol ๊ธฐ๋ฐ ์์กด์ฑ ์ฃผ์ ์ผ๋ก ํ ์คํธ ์ฉ์ด์ฑ ํ๋ณด
- Realm DB๋ฅผ ํ์ฉํ ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ฐ ๋ค์ด์ด๋ฆฌ ๋ฐ์ดํฐ์ CRUD ์์ ๊ตฌํ
- ํ๊ณ /์์จ/์๋ณด ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ฐ ๋ณ๋ API๋ก ํธ์ถํ์ฌ ๋ฐ์ํ๋ ์ฑ๋ฅ ์ด์๋ก ์ธํด ๋น๋๊ธฐ ๋ฐ์ดํฐ์ ์ํ ๊ด๋ฆฌ์ UI ์ ๋ฐ์ดํธ ์ Race Condition ๋ฐ์
// Swift Concurrency์ TaskGroup์ ํ์ฉํ ๋์์ฑ ์ ์ด
func fetchBeachData() async throws -> BeachData {
try await withThrowingTaskGroup(of: APIResponse.self) { group in
group.addTask { await fetchWaveHeight() }
group.addTask { await fetchWaterTemperature() }
group.addTask { await fetchForecast() }
return try await group.reduce(into: BeachData()) { result, response in
result.update(with: response)
}
}
}
- ๋ค์์ ์น์บ ์คํธ๋ฆผ ๋์ ๋ก๋ ์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๊ธ์ฆ
- WKWebView ์ฌ์ฌ์ฉ ์ ๋ฐ์ํ๋ ๋ฆฌ์์ค ๋์
final class WebViewManager {
private var webViewPool: [String: WeakWebView] = [:]
func dequeueWebView(for url: URL) -> WKWebView {
cleanUnusedViews()
if let weakView = webViewPool[url.absoluteString],
let view = weakView.view {
return view
}
let view = configureNewWebView()
webViewPool[url.absoluteString] = WeakWebView(view: view)
return view
}
private func configureNewWebView() -> WKWebView {
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
return WKWebView(frame: .zero, configuration: config)
}
- Realm ๊ฐ์ฒด ์ ๋ฐ์ดํธ ์ SwiftUI View ๊ฐฑ์ ๋๋ฝ
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ UI ์ ๋ฐ์ดํธ ์๋๋ก ์ธํ ํฌ๋์
class BeachViewModel: ObservableObject {
private var notificationTokens: [NotificationToken] = []
func observeRealmChanges() {
let token = realm.objects(Beach.self).observe { [weak self] changes in
DispatchQueue.main.async {
self?.objectWillChange.send()
}
}
notificationTokens.append(token)
}
}
protocol DataBase {
func read<T: Object>(_ object: T.Type) -> Results<T>
func write<T: Object>(_ object: T)
func delete<T: Object>(_ object: T)
}
protocol NetworkService {
func fetch<T: Decodable>(endpoint: APIEndPoint) async throws -> T
}
- Protocol์ ํ์ฉํด ๋ชจ๋ ๊ฐ ๊ฒฐํฉ๋ ์ต์ํ
- Repository Pattern์ ํตํ ๋ฐ์ดํฐ ์ ๊ทผ ๊ณ์ธต ์ผ์ํ
// ํ
์คํธ๋ฅผ ๊ณ ๋ คํ ์์กด์ฑ ์ฃผ์
ํ์
class BeachViewModel {
private let networkService: NetworkService
private let database: DataBase
init(
networkService: NetworkService = LiveNetworkService(),
database: DataBase = RealmDatabase()
) {
self.networkService = networkService
self.database = database
}
}
- Unit Test ๋ฐ UI Test ์ฝ๋ ๋ฏธ๊ตฌํ
- Mock ๊ฐ์ฒด๋ฅผ ํ์ฉํ ํ ์คํธ ์๋๋ฆฌ์ค ๋ถ์ฌ
// ์ฒด๊ณ์ ์ธ ์๋ฌ ํ์
์ ์ ํ์
enum AppError: Error {
case network(NetworkError)
case database(DatabaseError)
case validation(ValidationError)
var localizedDescription: String {
// ์ฌ์ฉ์ ์นํ์ ์ธ ์๋ฌ ๋ฉ์์ง
}
}
- // ์ฒด๊ณ์ ์ธ ์๋ฌ ํ์ ์ ์ ํ์
- ํตํฉ๋ ์๋ฌ ์ฒ๋ฆฌ ์์คํ ๋ถ์ฌ