상시 학생 할인중
강의 금액이 부담되는 학생분들을 위해서 본인 학교 메일 계정으로 예) @***.ac.kr , @***.edu
이메일 보내주시면 전 강의 할인 쿠폰을 보내 드립니다.
👉 jacobko@kakao.com
안녕하세요. 😀
뉴질랜드에서 iOS 개발을 하고 있는 Jacob 입니다.
SwiftUI 을 통해 누구나 쉽게 iOS APP 을 만들수 있도록 다양한 강의를 제작중에 있습니다.
SwiftUI 뿐만 아니라 다양한 iOS 개발 관련 자료들을 저의 Jacob's DevLog 에서 만나실 수 있습니다.
Hello. 😀
I'm Jacob,living in New Zealand.
I'm currently working on creating various tutorials to make it easy for anyone to develop iOS apps through SwiftUI.
You can find not only SwiftUI but also various iOS development-related materials on my Jacob's DevLog.
강의
로드맵
전체 1수강평
- [Lv.3] 실전 네트워크 통신 - SwiftUI Combine, Async/Await
- [Lv.3] 실전 네트워크 통신 - SwiftUI Combine, Async/Await
- [Lv.3] 실전 네트워크 통신 - SwiftUI Combine, Async/Await
- [Lv.1] iOS 17 앱 개발 기초 - SwiftUI로 시작하기
- [Lv.1] iOS 17 앱 개발 기초 - SwiftUI로 시작하기
게시글
질문&답변
CoreData Array의 변화에 따른 SwiftUI View 변화 적용(with @Observable Macro)
안녕하세요 ycc3819 님.질문해주신 @Observable을 활용한 코드에서, 클릭 시 ~가 View에 바로 반영되지 않고, 앱을 재시작하거나 새로운 데이터를 입력할 때 한 번에 업데이트되는 현상을 발견했습니다. 이 문제를 해결하기 위해 여러 시도를 해본 결과, @State와 @StateObject의 차이로 인해 Core Data와 View의 상태가 즉시 동기화되지 않았음을 알게 되었습니다. 1. Core Data에서 @StateObject를 사용하는 이유• Core Data의 상태는 View가 재생성되더라도 유지되어야 합니다. 따라서, @StateObject로 ViewModel을 관리하는 것이 적합합니다.• @StateObject는 ViewModel과 View의 생명주기를 연결하고, 상태가 변경될 때 View에 자동으로 반영되도록 보장합니다. 2. @State와 @StateObject의 선택 기준• @State:• 단순 상태(문자열, 정수, 부울 등)를 관리할 때 적합.• View 외부와의 상태 공유가 필요하지 않은 경우 사용.• @StateObject:• Core Data, 비동기 작업, ViewModel 등 객체 기반 상태 관리가 필요할 때 적합.• View와 ViewModel의 생명주기를 연결하고 상태를 유지. 3. 적용 예시• Core Data와 같은 외부 데이터베이스를 관리할 때는 ViewModel에서 Core Data를 초기화하고, @StateObject를 통해 ViewModel과 View를 연결해야 데이터가 재사용되고 상태가 유지됩니다. 저 역시 질문을 통해 새로운 관점을 배울 수 있었고, 여러 시도를 통해 이 문제를 해결할 수 있었습니다. 질문해주셔서 감사드리며, 앞으로도 궁금한 점이 있으면 언제든지 알려주세요.감사합니다.Jacobimport SwiftUI import CoreData import Observation @Observable // Observable 사용 class CoreDataInterViewModel2 { let container: NSPersistentContainer var savedEntities: [Fish] = [] // @Published 사용 안함 init() { container = NSPersistentContainer(name: "FishContainer") container.loadPersistentStores { (description, error) in if let error = error { print("ERROR LOADING CORE DATA. \(error)") } else { print("SUCCESSFULLY LOADED CORE DATA: \(description)") } } fetchFish() } func fetchFish() { let request = NSFetchRequest(entityName: "Fish") do { savedEntities = try container.viewContext.fetch(request) } catch { print("ERROR FETCHING CORE DATA: \(error)") } } func saveData() { do { try container.viewContext.save() fetchFish() } catch { print("ERROR SAVING CORE DATA: \(error)") } } func addFish(text: String) { let newFish = Fish(context: container.viewContext) newFish.name = text saveData() } func deleteFish(indexSet: IndexSet) { guard let index = indexSet.first else { return } let entity = savedEntities[index] container.viewContext.delete(entity) saveData() } func updateFish(fish: Fish) { let currentName = fish.name ?? "" fish.name = currentName + "~" saveData() } } // MARK: - VIEW struct CoreDataInter2: View { @State private var textFieldText: String = "" @StateObject var vm: CoreDataInterViewModel = .init() // 기존 @StateObject 를 사용 var body: some View { NavigationStack { VStack(spacing: 20) { TextField("새로운 생선을 입력하세요", text: $textFieldText) .textFieldStyle(.roundedBorder) Button { guard !textFieldText.isEmpty else { return } vm.addFish(text: textFieldText) textFieldText = "" } label: { Text("추가하기") .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } List { ForEach(vm.savedEntities, id: \.self) { fish in Text(fish.name ?? "이름 없음") .onTapGesture { vm.updateFish(fish: fish) } } .onDelete { indexSet in vm.deleteFish(indexSet: indexSet) } } .listStyle(.plain) } .padding() .navigationTitle("Fish Market") } } } #Preview { CoreDataInter2() }
- 0
- 2
- 25
질문&답변
Xcode version 문제
안녕하세요 miso lim 님.제공된 강의 code 파일을 xcode 16.2 버전 (iOS 18) 에서 실행해보니 이상 없이 잘 되었습니다.혹시 강의 중간마다 에러가 발생 되면 언제든지 질문 주시면 버전에 맞춰서 수정된 코드로 답변해 드리겠습니다.감사합니다Jacob
- 0
- 2
- 28
질문&답변
init-deinit의 무한루핑을 벗어나는 방법이 궁금합니다.
안녕하세요 ycc3819 님.저도 확인한 결과 @Observable 로 변경할 시 무한 루핑이 발생되는것을 확인하였습니다.발생한 원인을 보자면..1. 무한 루핑 발생 원인• @State는 값 타입 상태를 관리하기 위해 설계되었습니다. 하지만 @Observable은 클래스 타입(참조 타입)을 기반으로 동작하며, SwiftUI가 상태 변경을 감지할 때 ViewModel이 반복적으로 초기화되는 문제가 발생합니다.• 이로 인해 init과 deinit이 무한히 호출되며 루핑이 발생합니다.2. 그래서 강의 코드 처럼 @StateObject를 사용해야 하는 이유• 기존 강의 코드에서 @StateObject는 ObservableObject와 함께 사용되며, SwiftUI 뷰의 생명주기와 ViewModel의 초기화/해제를 안정적으로 관리합니다.• @StateObject는 뷰가 다시 렌더링되더라도 ViewModel을 재사용하기 때문에 무한 루핑 문제가 발생하지 않습니다.그래서 강의 내용 코드를 @Observable 로 바꿀때는 강의에서 다루지 않은 .onAppear, .onDisappear 를 사용해서 Weak self 약한 참조 내용을 확인 하실 수 있습니다. (@State, 와 init, deinit 을 사용하게 되면 라이프 사이클 충돌로 인해 무한 루핑이 되기 때문입니다. )예시 코드import SwiftUI import Observation // @Observable 사용을 위해 임포트 // MARK: - VIEWMODEL @Observable class WeakSelfInterViewModel { var data: String? = nil // 데이터를 관리하는 상태 변수 init() { getData() // 초기 데이터 로드 } func getData() { // 비동기로 데이터를 로드하며, 강한 참조 순환을 방지하기 위해 [weak self] 사용 DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in self?.data = "NEW DATA!" // 데이터 업데이트 } } } // MARK: - SCREEN 1 struct WeakSelfInter: View { var body: some View { NavigationStack { // 2번 페이지로 이동하며 ViewModel은 2번 페이지에서만 생성 및 관리 NavigationLink("2번째 페이지로 이동") { WeakSelfInter2() } .navigationTitle("1번째 페이지") // 네비게이션 타이틀 설정 } } } // MARK: - SCREEN 2 struct WeakSelfInter2: View { @State private var vm = WeakSelfInterViewModel() // @State로 ViewModel 관리 var body: some View { VStack { Text("2번째 페이지") .font(.largeTitle) .foregroundColor(.red) // ViewModel의 데이터가 있으면 표시 if let data = vm.data { Text(data) } } .onAppear { // 뷰가 나타날 때 데이터 로드 확인 print("뷰가 나타남") } .onDisappear { // 뷰가 사라질 때 로그 출력 print("뷰가 사라짐") // ViewModel은 @State로 관리되므로 자동으로 메모리에서 해제됨 } } } // MARK: - PREVIEW #Preview { WeakSelfInter() // 미리보기에서 WeakSelfInter 화면 확인 }
- 1
- 2
- 40
질문&답변
Apple 공식 문서 보는법
안녕하세요 이재영 님.애플 공식문서 조회 하는 것에 대해서 몇가지 저의 팁을 드리자면 https://developer.apple.com/documentation/애플 문서는 주제별로 잘 정리되어 있지만, 특정 기능을 바로 찾기 위해서는 먼저 개념 파악이 중요합니다. 예를 들어, SwiftUI에서 뷰를 다룬다면 “View Protocol”로 검색을 시작하는 것이 유용합니다. 키워드 간결화: 문서 검색은 구체적인 용어를 간단하게 입력할수록 정확한 결과를 얻을 수 있습니다. 예를 들어 “SwiftUI delete text items in loop”보다는 “SwiftUI List delete”처럼 핵심 단어로 줄여서 검색해보세요.메서드 이름, 프로토콜 사용: Swift나 SwiftUI에서 제공하는 메서드나 프로토콜 이름을 직접 검색하면 유용합니다. 예를 들어, “ForEach”를 이용한 반복 처리를 문서에서 찾아보고, 그 예시를 통해 구현 방법을 이해할 수 있습니다.예를 들어, 반복문을 통해 텍스트를 한 번에 지우는 기능을 찾고자 한다면:• 먼저 “SwiftUI ForEach”를 검색합니다. 이 반복문 구조에 대한 문서를 찾고, ForEach 내부에서 데이터를 처리하는 방법을 이해할 수 있습니다.• 그다음으로 “SwiftUI delete items”나 “SwiftUI onDelete”와 같은 키워드로 검색합니다. SwiftUI의 List에서 항목을 삭제하는 기능은 주로 onDelete() 메서드로 처리합니다.• 이때 관련 문서를 찾으면, 다양한 예제와 함께 사용법을 배울 수 있습니다. 예를 들어, ForEach와 onDelete()를 결합하면 리스트의 여러 텍스트 항목을 반복적으로 삭제하는 기능을 구현할 수 있습니다.SwiftData 강의 부분은 오늘이나 내일 중으로 업데이트 예정입니다.감사합니다.Jacob
- 0
- 2
- 125
질문&답변
5월 업데이트 예정이었던 , iOS17 컨텐츠 SwifData 업데이트 언제 되나요 ?
안녕하세요 랑프님.제가 개인적인 사정으로인해서 업데이트가 지연되고 있습니다. 7월 중으로 최대한 빨리 SwiftData 부분 강의 업로드 하겠습니다. 양해 부탁드립니다. 감사합니다.
- 0
- 1
- 164
질문&답변
GalleryView 작성시, @State? @Bidnable? 의 차이점이 뭔지 궁금합니다.
안녕하세요 ycc3819 님제가 확인이 늦어 이제 답변드리네요 죄송합니다.질문주신 부분과 링크에서 나타난 것은 @State와 @Observable을 함께 사용할 때 주의할 점은 다음과 같습니다: • @State와 @Observable 사용 시점: @Observable 인스턴스를 생성하는 뷰만 @State를 사용합니다. 다른 뷰에서는 let으로 선언하여 사용합니다. • @Observable을 let으로 선언: 다른 뷰에서 @Observable 인스턴스를 받을 때는 let으로 선언해도 안전합니다. 인스턴스를 소유하는 뷰는 @State를 사용해야 합니다. 이 원칙을 따르면 뷰의 상태 관리와 데이터 흐름이 명확해집니다.그렇기 때문에 최소 뷰의 소유권을 가지고 있는 최상위에서는 @State 을 사용하고, 그것을 인스턴스 받어서 사용할때는 let 을 사용하게 이론적으로 안전합니다. 하지만, var 을 사용해도 error 가 발생되거나 그러진 않지만 safe code 을 위해서는 let 사용이 권장되는것입니다. @State 는 처음 instance 생성할때 viewModel 을 선언할때 사용되며 @Bindable 은 선언된 ViewModel 을 binding 하는 역활을 합니다. 자세한 사항은 iOS17 강의에서 Observation 부분을 참고하시면 됩니다. Project 에서 @Observable 을 사용해서 변환하는것도한 코드 변환 하고 확인 후에 강의및 예제 코드도 조만간 업데이트 하겠습니다.
- 0
- 2
- 187
질문&답변
페이지 이동에 관해 질문이 있습니다 !
안녕하세요 oilater 님.확인이 늦어 이제 답변드리네요 죄송합니다.페이지 연결 느낌이 아니라 다른 페이지로 넘기는것은 .fullScreenCover 와 .presentationDetents 을 활용해서 sheet 의 크기와 페이지 전환을 NavigationLink 를 사용하지 않고도 구현 할 수 있습니다.자세한 사항은 링크를 참조해주세요https://sarunw.com/posts/swiftui-bottom-sheet/감사합니다예제 코드는 다음과 같습니다.import SwiftUI struct test1: View { @State private var showFullScreen = false var body: some View { VStack { Text("Home Page") .font(.largeTitle) .padding() Button(action: { showFullScreen.toggle() }) { Text("Go to Next Page") .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } // .fullScreenCover(isPresented: $showFullScreen) { // NextPageView() // FullScreenColor 와 같이 Sheet} .sheet(isPresented: $showFullScreen, content: { NextPageView() // .presentationDetents([.medium]) // 중간 사이즈 sheet // .presentationDetents([.fraction(0.7)]) // 스크린 사이즈의 70% 만 .presentationDetents([.fraction(0.3)]) // 스크린 사이즈의 30% 만 }) } } } struct NextPageView: View { @Environment(\.presentationMode) var presentationMode var body: some View { VStack { Text("Next Page") .font(.largeTitle) .padding() Button(action: { presentationMode.wrappedValue.dismiss() }) { Text("Go Back") .padding() .background(Color.red) .foregroundColor(.white) .cornerRadius(8) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) .edgesIgnoringSafeArea(.all) } } #Preview { test1() } (사진)
- 0
- 2
- 247
질문&답변
Spacer() 를 넣으니 예제처럼 SafeArea 확보가 안됩니다.
안녕하세요 랑프님.확인이 늦어 이제 답변 드리네요 죄송합니다. iOS15 버전 이후로는 .background 적용시 자동으로 ignoreSafeArea 가 적용되게 xcode 에서 설정이 되었습니다. 그래서 .ignoreSafeArea(edges:[]) 를 사용하시면 위아래 색이 적용안하게 하실 수 있습니다.보다 자세한 사항은 아래 링크를 확인해 주세요감사합니다. https://developer.apple.com/documentation/swiftui/view/background(_:ignoressafeareaedges:) (사진)struct MatchedGeomatryEffectInter: View { @State private var isToggled: Bool = false @Namespace private var namespace var body: some View { VStack { if !isToggled { RoundedRectangle(cornerRadius: 20) .matchedGeometryEffect(id: "rectangle", in: namespace) .frame(width: 100, height: 100) // matchedGeometry Effect id 값과 nameSpace } // .offset(y: isToggled ? UIScreen.main.bounds.height * 0.7 : 0) Spacer() if isToggled { RoundedRectangle(cornerRadius: 20) .matchedGeometryEffect(id: "rectangle", in: namespace) .frame(width: 300, height: 100) } } //: VSTACK .frame(maxWidth: .infinity, maxHeight: .infinity) // 질문 답변 // .background(Color.green.ignoresSafeArea(edges: .bottom)) // 위에만 색적용 안되게 하기 .background(Color.green.ignoresSafeArea(edges: [])) // 모든 부위 .top, .bottom, .leading, .traling 적용안시키기 .onTapGesture { withAnimation(.spring()) { isToggled.toggle() } } } }
- 0
- 3
- 309
질문&답변
Local Notification 예제 문의
안녕하세요 랑프님. 앱이 활성화될 때마다 UIApplication.shared.applicationIconBadgeNumber = 0으로 뱃지가 초기화되므로, 로컬 알림을 여러 번 발송해도 모든 알림이 뱃지 값이 1인 상태로 도착합니다. 이는 알림의 뱃지 값이 항상 초기화되기 때문에 발생합니다.이를 해결하기 위해서 UserDefaults 를 사용해서 뱃지 값을 저장하고 불러오는 방법을 사용하면 됩니다. 수정된 코드는 다음과 같습니다. *수정한 부분만 사진으로 올려드리고 전체 코드는 맨아래 있습니다.앱이 종료 되어도 뱃지값 저장하기(사진)CancelNotifiactaion 에서 값 초기화(사진)View에서 화면이 나타날때 reset 하는거 삭제(사진)수정된 코드class NotificationManager { static let instance = NotificationManager() // 인스턴스 생성 func requestAuthorization() { let option: UNAuthorizationOptions = [.alert, .sound, .badge] // UserNotification 접근 UNUserNotificationCenter.current().requestAuthorization(options: option) { (success, error) in if let error = error { print("에러: \(error)") } else { print("성공") } } } func scheduleNotification() { // UserDefaults 에다가 자장할 currentBadgeCount let currentBadgeCount = UserDefaults.standard.integer(forKey: "badgeCount") // 값을 1씩 증가 시킴 let newBadgeCount = currentBadgeCount + 1 // 증가한다음에서는 값을 저장 UserDefaults.standard.set(newBadgeCount, forKey: "badgeCount") // notification 내용 설정 let content = UNMutableNotificationContent() content.title = "Local Notification 테스트 1" content.subtitle = "앱 알람 테스트 중입니다" content.sound = .default content.badge = NSNumber(value: newBadgeCount) // 1씩 증가 시킴 // Trigger 2가지 종류 // 1.시간 기준 : Interval - 몇 초 뒤에 울릴것인지 딜레이 설정 repeats 반복 여부 설정 (최소 1분이여지 반복이 돔) let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 5.0, repeats: false) // 2.날짜 기준 : DateMating 은 DateComponent 기준맞는 알림 var dateComponents = DateComponents() dateComponents.hour = 8 // hour 를 24시간 기준 dateComponents.minute = 30 dateComponents.weekday = 1 // 1은 일요일이 되고, 6은 금요일이 됨 // let calendarTigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) // 설정한 값을 NotificationCenter 에 요청하기 let request = UNNotificationRequest( identifier: UUID().uuidString, // 각각의 request ID 값 String 을 uuid 값으로 설정 content: content, trigger: timeTrigger) UNUserNotificationCenter.current().add(request) } // 생성된 Notification Cancel 하기 func cancelNotification() { // peding notification 은 tigger 상에서 만족된 조건이 되어도 더이상 notification 되지 않게 하기 UNUserNotificationCenter.current().removeAllPendingNotificationRequests() // 아이폰 상태바를 내렸을때 남아있는 notification 없애기 UNUserNotificationCenter.current().removeAllDeliveredNotifications() // Cancel 하면 UserDefault 값 초기화 하기 UserDefaults.standard.set(0, forKey: "badgeCount") UIApplication.shared.applicationIconBadgeNumber = 0 } } struct LocalNotificationInter: View { @Environment(\.scenePhase) var scenePhase var body: some View { VStack (spacing: 40) { Button { NotificationManager.instance.requestAuthorization() } label: { Text("권한 요청하기") .font(.title2) .fontWeight(.semibold) .foregroundColor(.black) .padding() .background(Color.green) .cornerRadius(10) } Button { NotificationManager.instance.scheduleNotification() } label: { Text("Time Notification") .font(.title2) .fontWeight(.semibold) .foregroundColor(.black) .padding() .background(Color.green) .cornerRadius(10) } Button { NotificationManager.instance.scheduleNotification() } label: { Text("Calendar Notification") .font(.title2) .fontWeight(.semibold) .foregroundColor(.black) .padding() .background(Color.green) .cornerRadius(10) } Button { NotificationManager.instance.cancelNotification() } label: { Text("Notification 취소하기") .font(.title2) .fontWeight(.semibold) .foregroundColor(.black) .padding() .background(Color.green) .cornerRadius(10) } } //: VSTACK // schne 이 나타 날때 Badge 0 으로 초기화 하기 .onChange(of: scenePhase) { newValue in if newValue == .active { // 0으로 초기화 되지 않게 reset 하지 않기 // UIApplication.shared.applicationIconBadgeNumber = 0 } } } }
- 0
- 2
- 219
질문&답변
Local Notification 강의 편집오류 있어요
안녕하세요 랑프님.컷편집 오류 확인하였습니다.원본파일에서 다시 수정하여 재 업로드 하겠습니다.감사합니다
- 0
- 2
- 154