포스트

BB Quotes (fin)

Version 2로 업그레이드

이어서 계속 작성해보도록 한다

4. Extenstion을 사용하여 코드 간소화

현재 Image(show.lowercased().replacingOccurrences(of: " ", with: "")) 이런식으로

코드가 약간 길어지는것을 Extension을 활용하여 조금 간소화를 해보도록 한다.

이렇게 Extension으로 관리를하면 View 쪽은 코드가 간략하여 유지 보수 하기에 용이해진다.

1
2
3
4
5
6
7
8
9
extension String {
    func removeSpaces() -> String {
        self.replacingOccurrences(of: " ", with: "")
    }
    
    func removeCaseAndSpace() -> String {
        self.removeSpaces().lowercased()
    }
}

이렇게 만들어 주었다.

그리고 필요한부분에 맞춰 적용을 해주자

1
2
3
4
5
// before
Image(show.lowercased().replacingOccurrences(of: " ", with: ""))

// after
Image(show.removeCaseAndSpace())

이런식으로 간소화 된걸 알 수 있다.

5. constant를 사용한 코드 관리

ContentView 만봐도

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ContentView: View {
    var body: some View {
        TabView {
            Tab("Breaking Bad", systemImage: "tortoise") {
                QuoteView(show: "Breaking Bad")
                    .toolbarBackgroundVisibility(.visible, for: .tabBar)
            }
            
            Tab("Better Call Saul", systemImage: "briefcase") {
                QuoteView(show: "Better Call Saul")
                    .toolbarBackgroundVisibility(.visible, for: .tabBar)
            }
            
            Tab("El Camino", systemImage: "car") {
                QuoteView(show: "El Camino")
                    .toolbarBackgroundVisibility(.visible, for: .tabBar)
            }
        }
        .preferredColorScheme(.dark)
    }
}

지금 “Breaking Bad” 같이 문자열을 그대로 사용하여 값을 전달하는데, 이렇게 직접적으로 입력하게 될 경우 오타가 발생할 경우도 있다.

그렇기에 이런것들은 상수(Constants)로 관리를 해주면 오타방지도 되면서 이후에 사용하기도 편리하다.

1
2
3
4
5
enum Constants {
    static let bbName = "Breaking Bad"
    static let bcsName = "Better Call Saul"
    static let ecName = "El Camino"
}

그리고 이렇게 바꿔주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct ContentView: View {
    var body: some View {
        TabView {
            Tab(Constants.bbName, systemImage: "tortoise") {
                QuoteView(show: Constants.bbName)
            }
            
            Tab(Constants.bcsName, systemImage: "briefcase") {
                QuoteView(show: Constants.bcsName)
            }
            
            Tab(Constants.ecName, systemImage: "car") {
                QuoteView(show: Constants.ecName)
            }
        }
        .preferredColorScheme(.dark)
    }
}

struct QuoteView: View {
    let vm = ViewModel()
    let show: String
    
    @State var showCharacterInfo = false
    
    var body: some View {
        GeometryReader { geo in
            // 생략
        }
        .ignoresSafeArea()
        .toolbarBackgroundVisibility(.visible, for: .tabBar) // moved
        .sheet(isPresented: $showCharacterInfo) {
            CharacterView(character: vm.character, show: show)
        }
    }
}

이렇게 constansts를 사용하면서 각 Tab에 달려있던 toolbarBackgroundVisibility Modifier를 QuoteView에 달아준다.

6. Episode 가져오기

Image

이렇게 정보를 제공하는걸 만들어 본다.

1. 모델링

Episode 역시 Modeling이 필요하니 그걸 먼저 해주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Episode: Decodable {
    let episode: Int // 101, 512
    let title: String
    let image: URL
    let synopsis: String
    let writtenBy: String
    let directedBy: String
    let airDate: String

    var seasonEpisode: String {
        "Season \(episode / 100) Episode \(episode % 100)"
    }
}

이때 seasonEpisode의 경우

sample을 보게되면 "episode": 101 이런식으로 적혀있다.

그렇기에 100을 나누어서 몫은 시즌을, 나머지는 그 시즌의 회차로 정의 한다.

2. fetch 코드 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func fetchEpisode(from show: String) async throws -> Episode? {
    let episodeURL = baseURL.appending(path: "episodes")
    let fetchURL = episodeURL.appending(queryItems: [URLQueryItem(name: "production", value: show)])
    
    let (data, response) = try await URLSession.shared.data(from: fetchURL)
    
    guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
        throw FetchError.badResponse
    }
    
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    
    let episodes = try decoder.decode([Episode].self, from: data)
    
    return episodes.randomElement()
}

뭐 딱히 언급할만한 건 없다.

에피소드가 존재하지않을때를 대비해 Optional로 해준것 말곤 없다.

ViewModel로 가서도 sample 데이터를 가져오기위해 코드를 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var episode: Episode
    // 생략
    init() {
        // 생략
        
        let episodeData = try! Data(contentsOf: Bundle.main.url(forResource: "sampleepisode", withExtension: "json")!)
        episode = try! decoder.decode(Episode.self, from: episodeData)
    }

    func getEpisode(for show: String) async {
        status = .fetching
        
        do {
            if let unwrappedEpisode = try await fetcher.fetchEpisode(from: show) {
                episode = unwrappedEpisode
            }
            
            status = .success
        } catch {
            status = .failed(error: error)
        }
    }

ViewModel에 있던 getData함수는 getQuoteData로 명칭 변경

이유는 QuoteData와 Episode 가져오는걸 분리하기 위해서.

3. EpsodeView 만들기

QuoteView를 FetchView로 이름을 바꿔준다.

하나의 뷰를 재활용할 예정

1
2
3
4
5
6
7
8
9
10
11
12
 HStack {
    Button {
        // 생략
    }
    
    Spacer()
    
    Button {
       // 생략
    }
}
.padding(.horizontal, 30)

Image

이렇게 나누어 준다.

하지만

Image

작동하지 않는다.

그 이유는

case .success: 일때 우리가 quote에 대한 조건만 처리 해뒀기 때문

그렇기에 viewmodel에서 별도의 case를 하나 더 만들어줄 필요가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
enum FetchStatus {
    case notStarted
    case fetching
    case successQuote
    case successEpisode
    case failed(error: Error)
}

    func getQuoteData(for show: String) async {
        status = .fetching
        
        do {
            // 생략
            
            status = .successQuote
        } catch {
            status = .failed(error: error)
        }
    }
    
    func getEpisode(for show: String) async {
        status = .fetching
        
        do {
            // 생략
            
            status = .successEpisode
        } catch {
            status = .failed(error: error)
        }
    }

이렇게 다시 추가하고 수정했다면

1
2
3
4
5
6
case .successQuote:
    // 생략
case .successEpisode:
    VStack(alignment: .leading) {
        EpisodeView()
    }

이런식으로 case도 바꿔준다.

이제 새롭게 만든 EpisodeView UI를 디자인 해주자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct EpisodeView: View {
    let episode: Episode
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(episode.title)
                .font(.largeTitle)
            
            Text(episode.title)
                .font(.title2)
            
            AsyncImage(url: episode.image) { image in
                image
                    .resizable()
                    .scaledToFit()
                    .clipShape(.rect(cornerRadius: 15))
            } placeholder: {
                ProgressView()
            }
            
            Text(episode.synopsis)
                .font(.title3)
                .minimumScaleFactor(0.5)
                .padding(.bottom)
            
            Text("Written By: \(episode.writtenBy)")
            
            Text("Directed By: \(episode.directedBy)")
            
            Text("Aired: \(episode.airDate)")
        }
        .padding()
        .foregroundStyle(.white)
        .background(.black.opacity(0.6))
        .clipShape(.rect(cornerRadius: 25))
        .padding(.horizontal)
    }
}

Image

그럼 이렇게 나오게 된다.

이제 실행해보면

Image

잘된다.

💡 BB Quotes Coding Challenge 요약


✅ Challenge 1: 앱 시작 시 자동으로 Quote 가져오기

  • 현재는 앱을 켜면 화면이 비어 있음
  • 앱이 실행되자마자 자동으로 Quote를 fetch해서 보여주도록 설정
  • 이후 버튼 탭 시에는 기존처럼 작동

🖼️ Challenge 2: 캐릭터 이미지 랜덤 선택

  • 현재는 항상 이미지 배열의 첫 번째 이미지를 사용 중
  • character.images 배열에서 랜덤한 이미지 선택해서 보여주기

🎭 Challenge 3: 랜덤 캐릭터 Fetch

  • 랜덤 quote 또는 episode와 비슷하게 랜덤 캐릭터도 fetch
  • URL: https://.../characters/random
  • 다만 show 구분이 없기 때문에 productions 속성을 활용해 현재 탭(show)에 맞는 캐릭터인지 확인
  • show가 맞지 않다면 무시하거나 다시 fetch하도록 처리

💬 Challenge 4: CharacterView에 캐릭터 Quote 추가

  • 기존 CharacterView에 해당 캐릭터의 랜덤 Quote 하나 추가
  • 버튼을 눌러 해당 캐릭터의 다른 Quote를 가져올 수 있도록 설정
  • 사용 API:
    https://.../quote/random?author={characterName}

🟡 Challenge 5: 가끔 Simpsons Quote 가져오기

  • 일정 확률 또는 주기로 Simpsons Quote를 가져오도록 설정
    • 예: 5번 중 1번, 혹은 20% 확률
  • Simpsons API:
    https://thesimpsonsquoteapi.glitch.me/quotes
  • 응답은 하나의 quote만 담긴 배열 형태
    • quote, character, image 정보 포함

Challenge 나중에 해보는걸로…

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.