앨런 iOS 앱 개발 (15개의 앱을 만들면서 근본원리부터 배우는 UIKit) - MVVM까지
영상 초반부(00:30 - 04:00)에 대한 의문점이 생겨 질문 남깁니다! 텍스트필드의 경우 VC(뷰컨트롤러)와 '완전히 분리된 객체'이며 유저와 직접적인 커뮤니케이션을 하고, 버튼의 경우 VC와 '직접적인 커뮤니케이션을 한다' 라고 하셨는데, 이 부분에서 살짝 의문이 듭니다. 텍스트필드와 버튼의 차이가 동작의 형태나 이벤트 흐름의 복잡성에서 발생한다는 점에는 공감하지만, 버튼 역시 UITextField와 마찬가지로 VC와는 별도의 객체로 존재하고, 이벤트 발생 시 target–action 메커니즘을 통해 결과를 VC로 전달하는 구조로 이해하고 있습니다. 이런 관점에서 봤을 때, “버튼은 VC와 직접 커뮤니케이션한다”는 표현이 개념적으로 어떤 의미를 가지는지 조금 더 설명을 부탁드려도 될까요?
안녕하세요 프로그램 좀비님 좋은 점심입니다 :) 저는 ios 앱 개발만 진행중입니다. ios 앱에 어떤 광고 유형들 추가하면 좋을지 고민이 되어 질문 올렸습니다. 1. ios의 경우 스플래시 이후 전면광고 대신 오프닝 광고를 추가하시나요? 스플래시 이후의 전면광고는 평점 관리나 이탈률에 취약할것같고 오프닝은 ecpm이 낮을것같아 고민됩니다. 오프닝 추가하고 특정 행동 이후 전면광고가 최선일까요? 2. ios에서 스플래시 후 전면광고로 애드몹 정책에 문제가 생기신적이 있으신가요? 3. 첫 앱 사용자에게는 이탈률을 줄이기 위해 광고를 가리거나 하시나요?
{ "Information": "Thank you for using Alpha Vantage! This is a premium endpoint. You may subscribe to any of the premium plans at https://www.alphavantage.co/premium/ to instantly unlock all premium endpoints" } 선생님, 그래프 그리기 위한 API가 프리미엄 요금제를 구독해야만 가능하다고 하는데 어떻게 하면 좋을까요? ㅜㅜ
앨런 iOS 앱 개발 (15개의 앱을 만들면서 근본원리부터 배우는 UIKit) - MVVM까지
let backButton: UIButton = { let button = UIButton(type: .custom) button.setTitle("Back", for: .normal) button.setTitleColor(.white, for: .normal) button.backgroundColor = UIColor.blue button.titleLabel?.font = .boldSystemFont(ofSize: 20) button.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside) return button }() backButton 설정시에 addTarget 첫번째 인자로 self 로 전달하니까 'self' refers to the method 'FirstViewController.self', which may be unexpected 라는 warning 문구가 xcode 내에 표시되어서 xcode 에서 추천하는 방향대로 아래 코드블록 처럼 FirstViewController.self 로 전달하니까 backButton 클릭시 Exception 이 발생하는데요. button.addTarget(FirstViewController.self, action: #selector(backButtonTapped), for: .touchUpInside) 이 부분 왜 Excpetion 이 발생하는건지 궁금합니다. self 로 전달하면 문제는 없는데 경고문구가 나오는게 신경쓰입니다. 경고문구를 없애고 버튼 클릭에도 문제가 없으려면 어떻게 하는게 좋을까요? Exception 문구는 아래와 같습니다. *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[NextVC.FirstViewController backButtonTapped]: unrecognized selector sent to class 0x100e4a3d8' *** First throw call stack: (0x19df92964 0x19ae81814 0x19e02d140 0x19df1235c 0x19df1a200 0x1a4c49f58 0x1a4501154 0x1a4501498 0x1a44fdf08 0x1a450001c 0x1a4c7a2d4 0x1a4c7b734 0x1a4c5e170 0x1a3892d38 0x1a38a1e28 0x1a3894c50 0x1a38a2ee4 0x1a38a2374 0x28cd15560 0x19dee34cc 0x19df130b0 0x19df12fd8 0x19deeac1c 0x19dee9a6c 0x23fb18498 0x1a38c2df8 0x1a386be54 0x1a3997820 0x100e365cc 0x100e3653c 0x100e36648 0x19aed6e28) libc++abi: terminating due to uncaught exception of type NSException
강의 내용중에 아래 2가지가 가능하다고 했는데 1번 TextField("이메일을 입력해주세요", text: $store.email.sending(\.inputEmail)) 2번 TextField("이메일을 입력해주세요", text: Binding(get: { store.email }, set: { email in store.send(.inputEmail(email)) })) 2번 코드로 작성후 이메일주소를 수정후 저장 하니깐 아래와 같은 에러가 발생했습니다. A "forEach" at "AppStore/MyPageReducer.swift:96" received an action for a missing element. Action: MyPageStackReducer.Action.email(.inputEmail) This is generally considered an application logic error, and can happen for a few reasons: A parent reducer removed an element with this ID before this reducer ran. This reducer must run before any other reducer removes an element, which ensures that element reducers can handle their actions while their state is still available. An in-flight effect emitted this action when state contained no element at this ID. While it may be perfectly reasonable to ignore this action, consider canceling the associated effect before an element is removed, especially if it is a long-living effect. This action was sent to the store while its state contained no element at this ID. To fix this make sure that actions for this reducer can only be sent from a store when its state contains an element at this id. In SwiftUI applications, use "NavigationStack.init(path:)" with a binding to a store. 확인해 보니 pop시 Binding의 set 이 호출되서 이미 path에 없는 element를 호출하게 되서 해당 에러가 발생한다고 합니다. 그럼 TCA 환경에서는 1번을 꼭 사용해야 하는 건지요 ?
강의 내용처럼 EditNameReducer 에서 ReducerOf<Self> 를 사용하면 아래처럼 에러가 발생합니다. @Reducer 매크로 타입은 빌드시점이 되야 EditNameReducer 가 Reducer 타입인걸 알기 때문에 사용 못한다고 하는데 강의는 에러 없이 되서 xcode26.2 에서 왜 안되는지 알고 싶습니다. 결국 아래처럼 변경해서 사용 했습니다. var body: some Reducer <State, Action> { Reduce { state, action in return .none } }
18분 55초 쯤 예시를 보여주시면서 /// 2) (액터 외부) 동기 코드가 (일반) 비동기 코드에서 불려지면.. 어떤 (컨텍스트에 해당하는) 작업(Task) 내부에서 실행됨 ===> Task 격리 func readAny(with books: [Book]) async { if let book = books.randomElement() { readBook(with: book) } } Task { let account = LibraryAccount(idNumber: 1) /// 액터 let books = await account.booksOnLoan /// 액터내부의 (상태)값 가져오기 (Sendable타입) await readAny(with: books) /// 액터 외부에서 실행 (일반적으로 이런 구현은 옳지 않음) } 위와 같은 코드에서 Task 가 3번 스레드에서 돌아가고 있었다고 치면, account.booksOnLoan 을 가져오는건 2번 스레드로 갈 수 있다고 쳐도, await readAny(with:) 는 다시 Task 와 동일한 3번 스레드에서 돌아가게 될거다! 라고 말씀을 주셨는데요 사실 nonisolated async func 인 readAny 를 await 로 호출을 하는 순간, 해당 함수는 Task 의 스레드는 무관한게 아무 스레드에서나 돌아갈 수 있다고 생각을 했습니다 (실제로 Task 를 mainActor context 로 두고 해당 코드를 돌려봐도 readyAny 는 main 에서 돌아가지 않았구여!) 이해 편의상 돌려서 설명을 주시건가??? 생각이 들지만 혹시 정확히 어떤 의미로 요렇게 말씀을 주신건지 궁금합니다~ 감사합니다 저장
안녕하세요 앨런님! 19강에서 actor 를 이해 편의상 "특정한 하나의 스레드에서만 실행"되는 것처럼 그림을 첨부해주셨는데요 (물론 설명으로는 한번에 하나씩 실행되는건 맞지만 스레드를 특정할 수는 없다" 라고 말씀주셨지만요) 그러면 이해 편의상이 아니라, 좀 더 정확하게는 제가 이해한 내용이 맞을지 한번 확인 부탁드려도 될까요? 항상 좋은 강의 감사합니다. 기본적으로 actor 의 실행 스레드는 고정되어있지 않음 Actor 의 메서드나 데이터에 접근하면 serial executor 에 작업이 쌓일 것 이때 actor 는 선입 선출로 동작하지 않으므로, 우선 순위에 따라 실행 순서는 달라질 수 있음 Actor 의 작업이 실행될때 어느 스레드에서 실행할건지는 상관없고 중요하지도 않음. 한 작업이 끝나고 나면, serial executor 에서 대기하고 있던 작업은 어느 스레드에서든 heap 에 저장된 데이터 읽어서, 그 데이터 기반으로 작업하면 되는 것. 중요한건 데이터 경쟁이 발생하지 않게 serial executor 에 넣어서 actor 관련 작업은 한번에 하나의 스레드에서만 돌게 하는 매커니즘
앨런 iOS 앱 개발 (15개의 앱을 만들면서 근본원리부터 배우는 UIKit) - MVVM까지
6:09 쯤에 UIView 에 대해서 Add New Contraints 하실때 따라해보면 Top alignment 가 맞지않는게 좀 다르게 설정이 되는것 같습니다. Inspector 패널에서 확인해보면 Top Alignment Constraint 가 First Item : View.top Second Item : Safe Area.top 이런식으로 적용되고있는데요. 이부분 설정을 어떻게해야 강의에 나오는것 처럼 수직정렬이 반듯하게 적용이 되는걸까요?
안녕하세요. 강의 듣다가 테스트 중에 문제가 발생하여 해당 내용에 대해 공유를 드리고자 이렇게 글 남깁니다. 문제가 발생한 부분은 음원을 audioPlayer 객체로 읽어 들이는 과정에서 제대로 처리가 되지 않아 _isLoading State 값이 변경되지 않고 무한 로딩이 걸리는 문제가 있습니다. 원인을 찾고자 try ~ catch 문을 통해 에러 메세지를 출력해 보았다가 _isLoading State가 정상적으로 업데이트 되어 화면 UI가 나오기 시작했습니다. for (final note in notes) { final player = AudioPlayer(); try { await player.setAsset('assets/$note'); } catch (e) { print(e); } _audioPlayers.add(player); } 다만 이렇게 수정한 후에는 맨 첫 번째 '도' 건반의 소리가 나지 않았습니다. 다른 음원은 모두 정상적으로 로드했지만, 첫 번째 'do1.wav' 만 로드가 되지 않고 에러가 발생합니다. Playback error androidx.media3.exoplayer.ExcoPlaybackException: Source Error 여러 고민을 해 보다가 혹시나 싶어 'do1.wav'의 음원의 이름을 'do.wav'로 바꾸니 정상적으로 작동하였습니다. 정확한 원인은 모르겠지만, 혹시 같은 문제를 겪는 분은 이름을 바꿔보는 걸 시도해 보시면 좋을 거 같습니다!
안녕하세요. 앱 개발에도 관심이 생겨 Flutter에 도전을 하고 있습니다. 초입문편이 있길래 구매하여 강의 시청 중에 있는데요. 강의에서 보여지는 Android Studio 버전이 저와 달라서 일단 헷갈리는 부분이 있습니다. 현재 다운로드하는 시점에서는 Android Studio Otter로 되어 있는데, 현재 보여주시는 UI와 상당히 달라져 있어서 약간 진행하는데 텀이 발생하고 있습니다. 제가 설치한 Android Studio Otter에서는 Device Manager > device > Edit 을 클릭하여 들어가 보아도 Device Frame 관련 체크박스가 보이지 않습니다. Otter에서는 따로 지원하지 않는 것인가요? 참고로 밖으로 꺼내지 않고 사이드바에 있을 경우엔 따로 Device Frame을 제거할 수 있더라구요 .. 밖으로 빼냈을 경우에만 Device Frame 체크박스를 찾을 수 없는 상황입니다 ..!
actor TaskImageDownloader { /// (다운로드) 상태를 저장하기 위한 열거형 정의 enum DownloadState { case completed(UIImage) case loading(Task<UIImage, Error>) case failed } private(set) var cache: [String: DownloadState] = [:] func image(from url: String) async throws -> UIImage { /// 기존에 저장된 상태가 있는 경우 (캐시 먼저 확인) if let cachedState = cache[url] { switch cachedState { case .completed(let image): /// 이미지 리턴 return image case .loading(let task): /// 작업(Task)을 기다렸다가 ===> 이미지 리턴 return try await task.value case .failed: throw "이미지 다운로드 실패" } } /// 작업(Task)을 생성 let task = Task.detached<UIImage, Error> { let image = try await downloadImage(from: url) return image } /// 일단 (완료되지 않은) 작업 상태를 보관 cache[url] = .loading(task) do { /// 작업의 완료를 기다렸다가 ===> 완료되면 ===> 완료상태(이미지)로 바꿔서 보관 let image = try await task.value cache[url] = .completed(image) return image } catch { cache[url] = .failed // 에러 발생의 경우 throw "이미지 다운로드 실패" } } } Actor에서 Task vs Task.detached 사용 시 재진입 문제 질문 안녕하세요. 위 코드에서 Task.detached 대신 일반 Task {} 를 사용하면 재진입 문제가 발생할 수 있을 것 같아 질문드립니다. 제가 이해한 바로는: Task {} 를 사용하면 생성된 작업이 actor의 context를 상속받아 serial executor에서 실행됩니다 다운로드 작업 중 await 를 만나면 actor가 suspension되고, 이 시점에 다른 스레드에서 해당 actor에 재진입할 수 있습니다 따라서 동일한 URL에 대해 중복으로 다운로드 작업이 생성될 가능성이 있습니다 반면 Task.detached 를 사용하면 actor context와 독립적으로 실행되어 이러한 재진입 문제를 방지할 수 있는 것으로 이해했습니다. 제가 이해한 내용이 맞는지 확인 부탁드립니다. 감사합니다.
- 학습 관련 질문을 남겨주세요. 상세히 작성하면 더 좋아요! - 먼저 유사한 질문이 있었는지 검색해보세요. - 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요. 안녕하세요. 좋은 강의 정말 잘 듣고 있습니다. 강의를 통해 MVVM 구조와 클린 아키텍처에 대해 배우고, 진행 중인 프로젝트에 적용해보고 있는데, 어려움을 겪고 있어 조언을 구하고자 질문 글을 올리게 되었습니다. 회원 정보 관련 api - repository - use_case - view_model -view 구조에서, 로그인 화면에서 로그인 후 받아오는 회원 정보를 어디에 저장해 두어야 되는지 잘 모르겠습니다. 회원정보는 마이페이지, 피드 등 여러 view에 사용되는데, 그렇다면 각각의 view_model은 회원정보를 어디에서 읽어오는게 좋은지 궁금합니다. 새로 고침 관련 note_app 강의 예제 에서는 노트 목록과 삭제가 하나의 화면(notes_view)에 있습니다. 제가 구현하는 구조에서는 두 개의 view로 분리되어 있습니다. 1번 view: 노트 목록 화면 2번 view: 노트 상세 + 삭제 화면 각 화면은 서로 다른 view_model을 사용합니다. 노트 상세 화면에서 노트를 삭제했을 때, 노트 목록 화면을 어떻게 다시 업데이트 하는 것이 적절한지 고민입니다. 생각나는 방법을 아래에 작성해보았는데, 어떤 방법을 사용하는 게 깔끔할지, 이외에 더 좋은 방법이 있는지 궁금합니다. 두 개의 view가 하나의 view_model을 공용으로 사용 노트 상세 화면에서 노트 목록 view_model를 read 하여 _loadNotes()를 직접 호출 노트 목록도 회원 정보처럼 별도로 저장해두고, view_model은 읽어오기 감사합니다!