Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2.0.0] TCA기반의 메인 공지화면 리스트 구현 #34

Merged
merged 9 commits into from
Nov 26, 2023
Prev Previous commit
Next Next commit
[수정] NoticeListFeature.State/provider 에 BindingState 적용
x-0o0 committed Nov 25, 2023
commit a35c8464b1fec9dcc54322112bbf88bbff293fc3
15 changes: 12 additions & 3 deletions KuringApp/KuringApp/NoticeList/DepartmentSelectorLink.swift
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import SwiftUI

struct DepartmentSelectorLink: View {
let department: NoticeProvider
@Binding var isLoading: Bool
let action: () -> Void

var body: some View {
@@ -20,18 +21,26 @@ struct DepartmentSelectorLink: View {

Spacer()

Image(systemName: "chevron.right")
if isLoading {
ProgressView()
} else {
Image(systemName: "chevron.right")
}
}
.contentShape(Rectangle())
.padding(.horizontal, 20)
.padding(.vertical, 16)
.onTapGesture(perform: action)
.onTapGesture {
guard !isLoading else { return }
action()
}
}
}

#Preview {
DepartmentSelectorLink(
department: .init(name: "산업디자인학과", hostPrefix: "kuid", korName: "산업디자인학과", category: .학과)
department: .init(name: "산업디자인학과", hostPrefix: "kuid", korName: "산업디자인학과", category: .학과),
isLoading: .constant(false)
) {
// 액션 정의. 예) `viewStore.send(.changeDepartmentButtonTapped)`
}
97 changes: 49 additions & 48 deletions KuringApp/KuringApp/NoticeList/NoticeContentView.swift
Original file line number Diff line number Diff line change
@@ -18,8 +18,10 @@ struct NoticeListFeature: Reducer {
/// 현재 공지리스트를 제공하는 `NoticeProvider` 값
///
/// - IMPORTANT: 추가한 학과가 있으면 추가한 학과의 첫번째 값이 초기값으로 세팅되고 없으면 `.학사`
var provider: NoticeProvider = NoticeProvider.departments.first ?? NoticeProvider.학사
@BindingState var provider: NoticeProvider = NoticeProvider.departments.first ?? NoticeProvider.학사

/// 현재 공지를 가져오는 중인지 알려주는 Bool 값
@BindingState var isLoading = false

/// 공지
var noticeDictionary: [NoticeProvider: NoticeInfo] = [:]
@@ -36,14 +38,15 @@ struct NoticeListFeature: Reducer {
}
}

enum Action {
enum Action: BindableAction {
case binding(BindingAction<State>)

/// `onAppear` 이 호출된 경우
case onAppear

/// 학과 변경하기 버튼을 탭한 경우
case changeDepartmentButtonTapped

/// 공지 카테고리 세그먼트를 탭한 경우
/// - Parameter noticeType: 선택한 공지 카테고리.
case noticeTypeSegmentTapped(NoticeType)

/// ``DepartmentSelectorFeature`` 의 Presentation 액션
case changeDepartment(PresentationAction<DepartmentSelectorFeature.Action>)

@@ -54,7 +57,7 @@ struct NoticeListFeature: Reducer {
case fetchNotices

/// 네트워크 요청에 대한 응답을 받은 경우
case responseNotices(TaskResult<(NoticeProvider, [Notice])>)
case noticesResponse(TaskResult<(NoticeProvider, [Notice])>)

/// 북마크 버튼을 탭한 경우
/// - Parameter notice: 북마크 액션 대상인 공지
@@ -67,39 +70,37 @@ struct NoticeListFeature: Reducer {
}
}

enum CancelID {
case fetchNotices
}

@Dependency(\.kuringLink) var kuringLink

var body: some ReducerOf<Self> {
BindingReducer()

Reduce { state, action in
switch action {
case .binding(\.$provider):
return .send(.fetchNotices)

case .onAppear:
return .send(.fetchNotices)

case .changeDepartmentButtonTapped:
state.changeDepartment = DepartmentSelectorFeature.State(
currentDepartment: state.provider,
addedDepartment: IdentifiedArray(uniqueElements: NoticeProvider.departments) // TODO: Dependency
)
return .none

case let .noticeTypeSegmentTapped(noticeType):
let provider = noticeType.provider
guard provider.id != state.provider.id else {
return .none
}
state.provider = provider
return .run { send in
await send(.fetchNotices)
}

case let .changeDepartment(.presented(.delegate(delegate))):
switch delegate {
case .editDepartment:
state.changeDepartment = nil
return .send(.delegate(.editDepartment))
}

// TODO: Delegate
case .delegate:
return .none

case .changeDepartment(.presented(.selectDepartment)):
/// ``DepartmentSelectorFeature`` 액션
guard let selectedDepartment = state.changeDepartment?.currentDepartment else {
@@ -109,17 +110,14 @@ struct NoticeListFeature: Reducer {
return .none

case .changeDepartment(.dismiss):
return .run { send in
await send(.fetchNotices)
}

case .changeDepartment:
return .none
return .send(.fetchNotices)

case .fetchNotices:
if state.provider == .emptyDepartment {
return .none
}
state.isLoading = true

return .run { [provider = state.provider, noticeDictionary = state.noticeDictionary] send in
let retrievalInfo = noticeDictionary[provider] ?? State.NoticeInfo()

@@ -128,21 +126,23 @@ struct NoticeListFeature: Reducer {
} else {
nil
}

do {
let notices = try await kuringLink.fetchNotices(
retrievalInfo.loadLimit,
provider.category == .학과 ? "dep" : provider.hostPrefix, // TODO: korean name 도 쓸 거 고려해서 문자열 말고 좀 더 나은걸로
department,
retrievalInfo.page
await send(
.noticesResponse(
TaskResult {
let notices = try await kuringLink.fetchNotices(
retrievalInfo.loadLimit,
provider.category == .학과 ? "dep" : provider.hostPrefix, // TODO: korean name 도 쓸 거 고려해서 문자열 말고 좀 더 나은걸로
department,
retrievalInfo.page
)
return (provider, notices)
}
)
await send(.responseNotices(.success((provider, notices))))
} catch {
await send(.responseNotices(.failure(error)))
}
)
}
.cancellable(id: CancelID.fetchNotices, cancelInFlight: true)

case let .responseNotices(.success((noticeType, notices))):
case let .noticesResponse(.success((noticeType, notices))):
if state.noticeDictionary[noticeType] == nil {
state.noticeDictionary[noticeType] = State.NoticeInfo()
}
@@ -153,16 +153,20 @@ struct NoticeListFeature: Reducer {
noticeInfo.notices += notices

state.noticeDictionary[noticeType] = noticeInfo

state.isLoading = false
return .none

case let .responseNotices(.failure(error)):
case let .noticesResponse(.failure(error)):
print(error.localizedDescription)
state.isLoading = false
return .none

case let .bookmarkTapped(notice):
print("공지#\(notice.articleId)을 북마크 했습니다.")
return .none

case .binding, .delegate, .changeDepartment:
return .none
}
}
.ifLet(\.$changeDepartment, action: /Action.changeDepartment) {
@@ -181,20 +185,17 @@ struct NoticeContentView: View {
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack(spacing: 0) {
// 공지 카테고리 리스트
NoticeTypePicker(store: self.store)
.frame(height: 48)
.onAppear {
print("on Appear")
viewStore.send(.fetchNotices)
}
NoticeCategoryPicker(selection: viewStore.$provider)

if viewStore.provider == .emptyDepartment {
NoDepartmentView()
} else {
NoticeList(store: self.store)
}
}
.onAppear {
viewStore.send(.onAppear)
}
}
.sheet(
store: self.store.scope(
6 changes: 4 additions & 2 deletions KuringApp/KuringApp/NoticeList/NoticeList.swift
Original file line number Diff line number Diff line change
@@ -11,7 +11,6 @@ import ComposableArchitecture
struct NoticeList: View {
let store: StoreOf<NoticeListFeature>


var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
let noticeType = viewStore.provider
@@ -49,7 +48,10 @@ struct NoticeList: View {
.listStyle(.plain)
} header: {
if viewStore.provider.category == .학과 {
DepartmentSelectorLink(department: viewStore.provider) {
DepartmentSelectorLink(
department: viewStore.provider,
isLoading: viewStore.$isLoading
) {
viewStore.send(.changeDepartmentButtonTapped)
}
} else {
35 changes: 19 additions & 16 deletions KuringApp/KuringApp/NoticeList/NoticeTypePicker.swift
Original file line number Diff line number Diff line change
@@ -9,30 +9,33 @@ import Model
import SwiftUI
import ComposableArchitecture

struct NoticeTypePicker: View {
let store: StoreOf<NoticeListFeature>
struct NoticeCategoryPicker: View {
@Binding var selection: NoticeProvider

var body: some View {
WithViewStore(self.store, observe: { $0.provider }) { viewStore in
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 0) {
ForEach(NoticeType.allCases, id: \.self) { noticeType in
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 0) {
ForEach(NoticeType.allCases, id: \.self) { category in
Button {
selection = category.provider
} label: {
NoticeTypeColumn(
noticeType: noticeType,
selectedID: viewStore.category.id
noticeType: category,
selectedID: selection.category.id
)
.onTapGesture {
viewStore.send(.noticeTypeSegmentTapped(noticeType))
withAnimation {
proxy.scrollTo(noticeType, anchor: .center)
}
}
}
.buttonStyle(.plain)
}
}
.padding(.leading, 10)
.onChange(of: selection) { value in
withAnimation {
proxy.scrollTo(value.category.id, anchor: .center)
}
.padding(.leading, 10)
}
}
}
.frame(height: 48)
}
}