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

탭 바 & 뷰 구조 작성 #35

Merged
merged 10 commits into from
Aug 12, 2023
69 changes: 69 additions & 0 deletions Projects/Domain/Sources/Client/LocalStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// LocalStorageClient.swift
// Domain
//
// Created by Young Bin on 2023/08/10.
// Copyright © 2023 team.humanwave. All rights reserved.
//

import ComposableArchitecture
import Foundation

public struct LocalStorage {
enebin marked this conversation as resolved.
Show resolved Hide resolved
public enum Key: String {
case isLoggedIn
case visitCount

var valueType: Any.Type {
switch self {
case .isLoggedIn:
return Bool.self
case .visitCount:
return Int.self
}
}
}

private let storage: UserDefaults

init(storage: UserDefaults = .standard) {
self.storage = storage
}

/// 알아서 파싱해서 쓰시길.. 더 빡세게 잡으려면 우리가 귀찮아짐
public func get(_ key: Key) -> Any? {
switch key {
case .isLoggedIn:
return storage.bool(forKey: key.rawValue)
case .visitCount:
return storage.integer(forKey: key.rawValue)
}
}

public func set(_ value: Any, forKey key: Key) {
guard type(of: value) == key.valueType else {
assertionFailure("Invalid type for key: \(key.rawValue)")
return
}

storage.set(value, forKey: key.rawValue)
}
}

extension LocalStorage {
public static let shared = LocalStorage()
}

extension LocalStorage: DependencyKey {
public static var liveValue = LocalStorage()
public static func testValue(storage: UserDefaults) -> LocalStorage {
LocalStorage(storage: storage)
}
}

extension DependencyValues {
public var localStorage: LocalStorage {
get { self[LocalStorage.self] }
set { self[LocalStorage.self] = newValue }
}
}
21 changes: 21 additions & 0 deletions Projects/Features/Sources/MainPage/MainPageFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// MainPageFeature.swift
// Features
//
// Created by 이영빈 on 2023/08/10.
// Copyright © 2023 team.humanwave. All rights reserved.
//

import Foundation
import ComposableArchitecture

public struct MainPageFeature: Reducer {
public struct State: Equatable {}
public struct Action: Equatable {}

public var body: some Reducer<State, Action> {
Reduce { state, action in
return .none
}
}
}
60 changes: 60 additions & 0 deletions Projects/Features/Sources/MainPage/MainPageView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// KeymeMainView.swift
// Keyme
//
// Created by 이영빈 on 2023/08/09.
// Copyright © 2023 team.humanwave. All rights reserved.
//

import SwiftUI
import SwiftUIIntrospect
import ComposableArchitecture

struct KeymeMainView: View {
@State private var selectedTab = 0

var body: some View {
TabView(selection: $selectedTab) {
TestView(store: Store(
initialState: TestStore.State(),
reducer: TestStore()))
.tabItem {
Image(systemName: "1.square.fill")
Text("Home")
}
.tag(0)

Text("My page content")
.tabItem {
Image(systemName: "2.square.fill")
Text("My page")
}
.tag(1)
}
.introspect(.tabView, on: .iOS(.v16, .v17)) { tabViewController in
let tabBar = tabViewController.tabBar

let barAppearance = UITabBarAppearance()
barAppearance.configureWithOpaqueBackground()
barAppearance.backgroundColor = .black

let itemAppearance = UITabBarItemAppearance()
itemAppearance.selected.iconColor = .white
itemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.white]
itemAppearance.normal.iconColor = .gray
itemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.gray]

tabBar.standardAppearance = barAppearance
tabBar.standardAppearance.inlineLayoutAppearance = itemAppearance
tabBar.standardAppearance.stackedLayoutAppearance = itemAppearance
tabBar.standardAppearance.compactInlineLayoutAppearance = itemAppearance
tabBar.scrollEdgeAppearance = tabBar.standardAppearance
}
}
}

struct KeymeTabView_Previews: PreviewProvider {
static var previews: some View {
KeymeMainView()
}
}
28 changes: 28 additions & 0 deletions Projects/Features/Sources/Onboarding/OnboardingFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// Onboarding.swift
// Features
//
// Created by 이영빈 on 2023/08/10.
// Copyright © 2023 team.humanwave. All rights reserved.
//

import Foundation
import ComposableArchitecture

public struct OnboardingFeature: Reducer {
public enum State: Equatable {
case notDetermined
case completed
case needsOnboarding
}
public enum Action: Equatable {
case succeeded
case failed
}

public var body: some Reducer<State, Action> {
Reduce { state, action in
return .none
}
}
}
25 changes: 25 additions & 0 deletions Projects/Features/Sources/Onboarding/OnboardingView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// OnboardingView.swift
// Keyme
//
// Created by 이영빈 on 2023/08/10.
// Copyright © 2023 team.humanwave. All rights reserved.
//

import SwiftUI
import ComposableArchitecture

// FIXME: Temp
public struct OnboardingView: View {
private let store: StoreOf<OnboardingFeature>

public init(store: StoreOf<OnboardingFeature>) {
self.store = store
}

public var body: some View {
Button(action: { store.send(.succeeded) }) {
Text("온보딩?")
}
}
}
121 changes: 121 additions & 0 deletions Projects/Features/Sources/Root/RootFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// RootFeature.swift
// Features
//
// Created by 이영빈 on 2023/08/10.
// Copyright © 2023 team.humanwave. All rights reserved.
//

import Foundation

import Domain
import Network
import ComposableArchitecture

public struct RootFeature: Reducer {
@Dependency(\.localStorage) private var localStorage

public init() {}

public struct State: Equatable {
@PresentationState public var logInStatus: SignInFeature.State?
@PresentationState public var onboardingStatus: OnboardingFeature.State?

public init(isLoggedIn: Bool? = nil, doneOnboarding: Bool? = nil) {
if let isLoggedIn {
logInStatus = isLoggedIn ? .loggedIn : .loggedOut
} else {
logInStatus = .notDetermined
}

if let doneOnboarding {
onboardingStatus = doneOnboarding ? .completed : .needsOnboarding
} else {
onboardingStatus = .notDetermined
}
}
}

public enum Action: Equatable {
case login(PresentationAction<SignInFeature.Action>)
case onboarding(PresentationAction<OnboardingFeature.Action>)
case mainPage(MainPageFeature.Action)

case onboardingChecked(TaskResult<Bool>)
case logInChecked(Bool)

case checkOnboardingStatus
case checkLoginStatus
}

public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .login(.presented(let result)):
switch result {
case .succeeded:
localStorage.set(true, forKey: .isLoggedIn)
state.logInStatus = .loggedIn
case .failed:
localStorage.set(false, forKey: .isLoggedIn)
state.logInStatus = .loggedOut
}
return .none

case .onboarding(.presented(let result)):
switch result {
case .succeeded:
state.onboardingStatus = .completed
case .failed:
state.onboardingStatus = .needsOnboarding
}
return .none

case .logInChecked(let result):
switch result {
case true:
state.logInStatus = .loggedIn
case false:
state.logInStatus = .loggedOut
}
return .none

case .onboardingChecked(.success(let result)):
switch result {
case true:
state.onboardingStatus = .completed
case false:
state.onboardingStatus = .needsOnboarding
}
return .none

case .checkLoginStatus:
let isLoggedIn = localStorage.get(.isLoggedIn) as? Bool ?? false
return .run { send in
await send(.logInChecked(isLoggedIn))
}

case .checkOnboardingStatus:
return .run(priority: .userInitiated) { send in
await send(.onboardingChecked(
TaskResult {
// TODO: API 갈아끼우기
try await Task.sleep(until: .now + .seconds(3), clock: .continuous)

return true
}
))
}

default:
return .none
}
}
.ifLet(\.$logInStatus, action: /Action.login) {
SignInFeature()
}
.ifLet(\.$onboardingStatus, action: /Action.onboarding) {
OnboardingFeature()
}
}
}
62 changes: 62 additions & 0 deletions Projects/Features/Sources/Root/RootView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// RootView.swift
// Keyme
//
// Created by 이영빈 on 2023/08/09.
// Copyright © 2023 team.humanwave. All rights reserved.
//

import SwiftUI
import ComposableArchitecture

public struct RootView: View {
private let store: StoreOf<RootFeature>

public init() {
self.store = Store(initialState: RootFeature.State()) {
RootFeature()._printChanges()
}

store.send(.checkLoginStatus)
store.send(.checkOnboardingStatus) // For 디버깅, 의도적으로 3초 딜레이
}

public var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
if viewStore.logInStatus == .notDetermined {
// 여기 걸리면 에러임. 조심하셈.
EmptyView()
} else if viewStore.logInStatus == .loggedOut {
// 회원가입을 하지 않았거나 로그인을 하지 않은 유저
let loginStore = store.scope(
state: \.$logInStatus,
action: RootFeature.Action.login)

IfLetStore(loginStore) { store in
SignInView(store: store)
}
} else if viewStore.onboardingStatus == .notDetermined {
// 온보딩 상태를 로딩 중
ProgressView()
} else if viewStore.onboardingStatus == .needsOnboarding {
// 가입했지만 온보딩을 하지 않고 종료했던 유저
let onboardingStore = store.scope(
state: \.$onboardingStatus,
action: RootFeature.Action.onboarding)

IfLetStore(onboardingStore) { store in
OnboardingView(store: store)
}
} else {
// 가입했고 온보딩을 진행한 유저
KeymeMainView()
}
}
}
}

struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView()
}
}
Loading