게시글
질문&답변
Sorted, Filter, Map - UserViewModel 부분 오류 발생
안녕하세요. nonehour 님에러 발생 원인현재 코드에서 두 가지 주요 문제가 발생하고 있습니다:ObservableObject 프로토콜 준수 실패: UserViewModel 클래스가 ObservableObject를 채택했지만 필요한 요구사항을 충족하지 못하고 있습니다.Combine 프레임워크 누락: @Published 프로퍼티 래퍼와 관련된 초기화자를 찾을 수 없다는 에러가 발생합니다.해결 방법파일 상단의 import 구문을 다음과 같이 수정해주세요:import SwiftUI import Combine // 이 줄을 추가해주세요 왜 이런 차이가 발생하는가이런 현상이 발생하는 이유는 개발 환경의 차이 때문입니다:Xcode 버전 차이: 최신 Xcode에서는 SwiftUI를 import할 때 Combine도 함께 자동으로 가져오는 경우가 있지만, 구버전이나 특정 설정에서는 그렇지 않을 수 있습니다.iOS 배포 타겟: 프로젝트의 iOS 최소 지원 버전에 따라 자동 import 동작이 달라질 수 있습니다.프로젝트 설정: 프로젝트 생성 방식이나 빌드 설정에 따라 모듈 import 방식이 다를 수 있습니다.근본적인 이해@Published와 ObservableObject는 실제로 Combine 프레임워크의 핵심 구성 요소입니다. 따라서 이들을 사용할 때는 명시적으로 Combine을 import하는 것이 가장 안전하고 명확한 방법입니다.강의 예제와 달라서 에러가 발생된점 양해 드립니다.감사합니다.Jacob
- 0
- 1
- 31
질문&답변
NavigationBarItems Deprecated
안녕하세요 최수훈 님. 제가 질문 확인이 늦어서 답변이 이제 드립니다. .NavigationBarItems 는 Deprecated 되어서 .toolbar() 사용하게끔 권장되고 있습니다. 질문주신 ListBasic 부분에서 EditButton() 을 사용했는데, 변경된 코드는 다음과 같습니다. // 기존 코드 .navigationBarItems(leading: EditButton(), trailing: addButton) // 변경된 코드 .toolbar() 사용 .toolbar { ToolbarItem(placement: .navigationBarLeading) { EditButton() } ToolbarItem(placement: .navigationBarTrailing) { addButton } 다른 NavigationBarItems 사용한 강의 예제들도 .toolbar{} 를 사용하는 방식으로 변경하여 강의 제공 예제 파일에 업데이트 하도록 하겠습니다.감사합니다제이콥
- 0
- 3
- 34
질문&답변
ViewBuilder 강의 관련 질문입니다.
안녕하세요 ducduck 님. 질문 주셔서 감사합니다.정리하자면 제가 설명한 부분: LocalViewBuilder에 선언된 ViewType을 사용해서 조건부 뷰를 만들기 위해 @ViewBuilder가 필요하다.질문자님이 이해하신 부분: 각 조건에서 반환하는 뷰들이 서로 다른 타입(Text, VStack, Image)이라서 @ViewBuilder가 필요하다.실제로는 둘 다 연결된 같은 이유입니다!ViewType enum을 사용해서 → 조건부 분기를 만들고 → 각 분기에서 다른 타입의 뷰를 반환하기 때문에 → @ViewBuilder가 필요예시로 @ViewBuilder private var numberPlate: some View { if type == .one { Text("1") } // Text 타입 else if type == .two { VStack{} } // VStack 타입 else { Image("3") } // Image 타입 }SwiftUI에서 some View는 단일 타입만 반환할 수 있는데, @ViewBuilder가 이런 다른 타입들을 하나로 통합해주는 역할을 합니다.그렇게 때문에 ViewType 을 활용한 조건부 뷰 구현과 서로 다른 타입 처리, 둘 다 @ViewBuilder 가 필요한 이유입니다
- 0
- 1
- 38
질문&답변
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
- 130
질문&답변
Xcode version 문제
안녕하세요 miso lim 님.제공된 강의 code 파일을 xcode 16.2 버전 (iOS 18) 에서 실행해보니 이상 없이 잘 되었습니다.혹시 강의 중간마다 에러가 발생 되면 언제든지 질문 주시면 버전에 맞춰서 수정된 코드로 답변해 드리겠습니다.감사합니다Jacob
- 0
- 2
- 137
질문&답변
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
- 109
질문&답변
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
- 229
질문&답변
5월 업데이트 예정이었던 , iOS17 컨텐츠 SwifData 업데이트 언제 되나요 ?
안녕하세요 랑프님.제가 개인적인 사정으로인해서 업데이트가 지연되고 있습니다. 7월 중으로 최대한 빨리 SwiftData 부분 강의 업로드 하겠습니다. 양해 부탁드립니다. 감사합니다.
- 0
- 1
- 222
질문&답변
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
- 253
질문&답변
페이지 이동에 관해 질문이 있습니다 !
안녕하세요 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
- 301