Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions iOS/BaseballApp/BaseballApp/Controllers/LobbyViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,34 @@ import Combine
class LobbyViewController: UIViewController {

@IBOutlet weak var roomsTableView: UITableView!
var viewModel: RoomsViewModel?
var cancelBag = Set<AnyCancellable>()
var viewModel: RoomsViewModel!

override func viewDidLoad() {
super.viewDidLoad()
roomsTableView.dataSource = self

viewModel = RoomsViewModel()
bind()
viewModel.load {
DispatchQueue.main.async {
self.roomsTableView.reloadData()
}
}
}

}

extension LobbyViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.rooms.data.count
}

func bind() {
viewModel?.$rooms
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] _ in
self?.roomsTableView.reloadData()
})
.store(in: &self.cancelBag)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: RoomTableViewCell.identifier, for: indexPath) as? RoomTableViewCell else {
return UITableViewCell()
}

let data = viewModel.rooms.data[indexPath.row]
cell.fill(data: data)
return cell
}
}

38 changes: 20 additions & 18 deletions iOS/BaseballApp/BaseballApp/Controllers/PlayViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ class PlayViewController: UIViewController {
@IBOutlet weak var playInformationStackView: UIStackView!
@IBOutlet weak var scoreHeaderView: ScoreHeaderView!

var cancelBag = Set<AnyCancellable>()
var currentPlayerView: CurrentPlayerView!
var count: Int = 0
var viewModel: GameViewModel? {
var viewModel: GameViewModel! {
didSet {
count = viewModel?.game?.pitcherHistory.count ?? 0
count = viewModel?.game?.data.pitchHistories.count ?? 0
Copy link

@mienne mienne May 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 가져오게 되면 PlayerViewController에서 모델과 관계가 생깁니다. 이러면 GameViewModel 만든 의미가 없습니다. Model 정보(viewModel?.game?.data.pitchHistories.count)를 ViewModel(viewModel.numbersOfGame 이나 viewModel.gameCount)에서 View 표시할 정보로 가공하고 ViewController에서 ViewModel 정보 가져와서 사용하는 것이 좋습니다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그렇군요! 수정해보도록 하겠습니다!😉

}
}

Expand All @@ -40,17 +40,16 @@ class PlayViewController: UIViewController {

viewModel = GameViewModel()
configureUI()
}

// MARK: - Private Functions
private func bind() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bind가 사라졌군요😭

viewModel?.$game
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] game in
self?.scoreHeaderView.configureAway(score: game?.away.score ?? 0)
self?.scoreHeaderView.configureHome(score: game?.home.score ?? 0)
})
.store(in: &self.cancelBag)
viewModel.load { game in
DispatchQueue.main.async { [weak self] in
self?.scoreHeaderView.configureAway(score: game.awayTeam.score)
self?.scoreHeaderView.configureHome(score: game.homeTeam.score)
self?.currentPlayerView.configure(batter: game.batter, status: game.batterStatus)
self?.currentPlayerView.configure(pitcher: game.pitcher, status: game.pitcherStatus)
self?.currentPlayerView.configure(playerRole: game.myRole)
self?.pitcherHistoryTableView.reloadData()
}
}
}

private func configureUI() {
Expand All @@ -62,7 +61,7 @@ class PlayViewController: UIViewController {
options: nil)?.first as? CurrentPlayerView else {
return
}

self.currentPlayerView = currentPlayerView
playInformationStackView.addArrangedSubview(groundView)
playInformationStackView.addArrangedSubview(currentPlayerView)
playInformationStackView.addArrangedSubview(pitcherHistoryTableView)
Expand All @@ -79,12 +78,15 @@ class PlayViewController: UIViewController {

extension PlayViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel?.game?.pitcherHistory.count ?? 0
return viewModel?.game?.data.pitchHistories.count ?? 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 위 코멘트와 동일한 부분입니다!

}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: PitcherRecordTableViewCell.identifier, for: indexPath) as! PitcherRecordTableViewCell
cell.backgroundColor = .systemRed
guard let cell = tableView.dequeueReusableCell(withIdentifier: PitcherRecordTableViewCell.identifier, for: indexPath) as? PitcherRecordTableViewCell,
let record = viewModel.game?.data.pitchHistories[indexPath.row] else {
return UITableViewCell()
}
cell.configure(record: record)
return cell
}
}
53 changes: 43 additions & 10 deletions iOS/BaseballApp/BaseballApp/Entity/Game.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,69 @@

import Foundation

struct GameResponse: Decodable {
let data: Game
}

struct Game: Decodable {
let home: Team
let away: Team
let strike: Int
let ball: Int
let out: Int
let awayTeam: Team
let homeTeam: Team
let inning: String
let myRole: String
let halves: String
let pitcher: Player
let pitcherStatus: String
let batter: Player
let batterStatus: String
let base1: String?
let base2: String?
let base3: String?
let pitcherHistory: [Record]
let pitchHistories: [Record]
let myRole: String

enum CodingKeys: String, CodingKey {
case strike, ball, out, inning, halves, pitcher, batter, base1, base2, base3
case awayTeam = "away_team"
case homeTeam = "home_team"
case pitcherStatus = "pitcher_status"
case batterStatus = "batter_status"
case pitchHistories = "pitch_histories"
case myRole = "my_role"
}
}

struct Player: Decodable {
let teamId: Int
let uniformNumber: Int
let name: String

enum CodingKeys: String, CodingKey {
case name
case teamId = "team_id"
case uniformNumber = "uniform_number"
}
}

struct Record: Decodable {
let pitcher: Player
let batter: Player
let result: String
let pitchStrikeCount: String
let strikeCount: Int
let ballCount: Int

enum CodingKeys: String, CodingKey {
case result
case pitchStrikeCount = "log"
case pitcher, batter, result
case strikeCount = "strike_count"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case strikeCount = "strike_count" 직접 작성하는 방법 말고도 디코딩 할 때 옵션을 줄 수 있습니다.

참고: JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오! 참고해서 수정해보겠습니다.😉 감사합니다.

case ballCount = "ball_count"
}
}

struct Team: Decodable {
let name: String
let score: Int
let position: String
let player: String
let playerStatus: String
let role: String
}

struct Scoreboard: Decodable {
Expand Down
10 changes: 5 additions & 5 deletions iOS/BaseballApp/BaseballApp/Entity/Room.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import Foundation

struct RoomResponse: Decodable {
let rooms: [Room]
let data: [Room]
}

struct Room: Decodable {
let id: Int
let number: String
let away: String
let home: String
let available: Bool
let awayTeam: String?
let homeTeam: String?
let awayUserEmail: String?
let homeUserEmail: String?
}
33 changes: 13 additions & 20 deletions iOS/BaseballApp/BaseballApp/Network/APIRequestManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,34 @@ import Foundation
import Combine

class APIRequestManager {
private var cancelBag = Set<AnyCancellable>()

private func createRequest(url: URL, method: HTTPMethod, httpBody: Data? = nil) -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.httpBody = httpBody
return request
}
func fetchRooms(url: URL, method: HTTPMethod, httpBody: Data? = nil) {

func fetch<T: Decodable>(url: URL, method: HTTPMethod, httpBody: Data? = nil) -> AnyPublisher<T, Error> {
let request = createRequest(url: url, method: method)
URLSession.shared.dataTaskPublisher(for: request)
return URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: RoomResponse.self, decoder: JSONDecoder())
.replaceError(with: RoomResponse(rooms: []))
.assign(to: \.rooms, on: RoomsViewModel())
.store(in: &self.cancelBag)
}

func fetchGame(url: URL, method: HTTPMethod, httpBody: Data? = nil) {
let request = createRequest(url: url, method: method)
URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: Game?.self, decoder: JSONDecoder())
.replaceError(with: nil)
.assign(to: \.game, on: GameViewModel())
.store(in: &self.cancelBag)
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}

struct Endpoint {
enum Path {
static let gameList = "/game/list"
static let gameStatus = "/game/status"
}

static func url(path: String) -> URL? {
var components = URLComponents()
components.scheme = "https"
components.host = "api.github.com"
components.scheme = "http"
components.host = "52.78.19.43"
components.port = 8080
components.path = path
return components.url
}
Expand Down
5 changes: 3 additions & 2 deletions iOS/BaseballApp/BaseballApp/UseCases/GameUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
//

import Foundation
import Combine

class GameUseCase {
let apiRequestManager = APIRequestManager()

func start(url: URL) {
apiRequestManager.fetchGame(url: url, method: .get)
func start(url: URL) -> AnyPublisher<GameResponse, Error> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네트워킹 관련 로직을 고치니 한결 나아졌네요. 잘 수정하였습니다👏

return apiRequestManager.fetch(url: url, method: .get)
}
}
5 changes: 3 additions & 2 deletions iOS/BaseballApp/BaseballApp/UseCases/RoomsUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
//

import Foundation
import Combine

class RoomsUseCase {
let apiRequestManager = APIRequestManager()

func start(url: URL) {
apiRequestManager.fetchRooms(url: url, method: .get)
func start(url: URL) -> AnyPublisher<RoomResponse, Error> {
return apiRequestManager.fetch(url: url, method: .get)
}
}
31 changes: 18 additions & 13 deletions iOS/BaseballApp/BaseballApp/ViewModel/GameViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,27 @@
//

import Foundation
import Combine

class GameViewModel {
@Published var game: Game?

@Published var game: GameResponse?
let gameUseCase = GameUseCase()
var cancelBag = Set<AnyCancellable>()

func load() {
guard let url = Endpoint.url(path: "/room/playInfo") else { return }
gameUseCase.start(url: url)
}

func getAwayScore() -> Int {
return game?.away.score ?? 0
}

func getHomeScore() -> Int {
return game?.home.score ?? 0
func load(completionHandler: @escaping (Game) -> Void) {
guard let url = Endpoint.url(path: Endpoint.Path.gameStatus) else { return }
let publisher = gameUseCase.start(url: url)
publisher.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
Copy link

@mienne mienne May 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

당장 수정해야 할 부분은 아니지만 에러에 대해서 print로 처리해서 추가적으로 코멘트 남깁니다. 추후, 지금 프로젝트를 개선하거나 앱을 만들 때 고려해야 할 부분입니다.

  • 실패 케이스에서도 서버 응답 실패, 지하철이나 네트워크가 잘 통하지 않는 곳에서 네트워크 OFF 되는 경우 등 다양하게 있습니다. 이 부분도 어떻게 처리할지, 예를 들어 화면에서 사용자에게 메세지를 알려줄 것인지, 재시도 요청할지 등 항상 고려해야 합니다🤔
  • API 요청하고 나서 화면이 구성할 때까지 사용자를 마냥 기다리게 하는 것이 아니라 로딩 인디케이터를 노출하거나 스켈레톤 뷰로 화면 구조가 어떻게 생겼는지 미리 알려줄 수 있습니다. 결과적으로 API 요청, 응답, 화면 구성할 때도 어떤 처리를 할 것인지 고려해야 합니다!

실제 앱 다운 받아서 네트워크 OFF 일 때나 화면 로딩이 어떻게 처리되어 있는지 한번 확인해보세요!

}
} receiveValue: { (response) in
self.game = response
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클로저 self 참조로 인해 순환 참조가 되서 메모리 릭 발생할 여지가 있습니다. 단순한 방법으로 deinit에서 로그 찍어서 잘 호출하는지 확인하는 방법이 있습니다. 또는 Xcode > Memory Graph Debugger 활용하는 방법이 있습니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weak self 약한 참조 관계로 수정했습니다. 😊

completionHandler(response.data)
}
.store(in: &cancelBag)
}
}
23 changes: 18 additions & 5 deletions iOS/BaseballApp/BaseballApp/ViewModel/RoomsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,26 @@
//

import Foundation
import Combine

class RoomsViewModel {
@Published var rooms: RoomResponse = RoomResponse(rooms: [])
@Published var rooms: RoomResponse = RoomResponse(data: [])
var cancelBag = Set<AnyCancellable>()
let roomsUseCase = RoomsUseCase()

func load() {
guard let url = Endpoint.url(path: "/room/list") else { return }
roomsUseCase.start(url: url)

func load(completionHandler: @escaping () -> Void) {
guard let url = Endpoint.url(path: Endpoint.Path.gameList) else { return }
let publisher = roomsUseCase.start(url: url)
publisher.sink { (completion) in
switch completion {
case .finished:
completionHandler()
case .failure(let error):
print(error.localizedDescription)
}
} receiveValue: { (response) in
self.rooms = response
}
.store(in: &cancelBag)
}
}
6 changes: 3 additions & 3 deletions iOS/BaseballApp/BaseballApp/Views/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="z87-J8-Gpn">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="z87-J8-Gpn">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
Expand Down Expand Up @@ -30,7 +30,7 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="RoomTableViewCell" rowHeight="128" id="6g6-nx-YgT" customClass="RoomTableViewCell" customModule="BaseballApp" customModuleProvider="target">
<rect key="frame" x="0.0" y="24.5" width="414" height="128"/>
<rect key="frame" x="0.0" y="28" width="414" height="128"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="6g6-nx-YgT" id="cIk-9u-0ic">
<rect key="frame" x="0.0" y="0.0" width="414" height="128"/>
Expand Down
Loading