Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// LoadingView.swift
// AppProduct
//
// Created by 이예지 on 1/4/26.
//

import SwiftUI

// MARK: - LoadingView

struct LoadingView<Content: View>: View {

// MARK: - Properties
private let content: Content

@Environment(\.loadingViewIsPresented) private var isPresented
@Environment(\.loadingViewMessage) private var message
@Environment(\.loadingViewSize) private var size

// MARK: - Initializer

init(@ViewBuilder content: () -> Content) {
self.content = content()
}

// MARK: - Body

var body: some View {
ZStack {
content

if isPresented {
LoadingViewContent(message: message, size: size)
.equatable()
}
}
}
}

// MARK: - LoadingViewContent (Presenter)
private struct LoadingViewContent: View, Equatable {
let message: String?
let size: LoadingViewSize

static func == (lhs: LoadingViewContent, rhs: LoadingViewContent) -> Bool {
lhs.message == rhs.message &&
lhs.size == rhs.size
}

var body: some View {
VStack(spacing: 10) {
ProgressView()
.progressViewStyle(.circular)
.frame(width: size.size.width , height: size.size.height)
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 부분은 controlSize() 수정자를 사용해서 아래와 같이 작성하는게 좋을거 같아요

Suggested change
.frame(width: size.size.width , height: size.size.height)
.controlSize(size == .large ? .large : .small)


if let message {
Text(message)
.font(size.font)
}
}
}
}


// MARK: - LoadingView + AnyLoadingView

extension LoadingView: AnyLoadingView { }

// MARK: - Preview

#Preview("Loading ON + Message + Size") {
LoadingView {
RoundedRectangle(cornerRadius: 16)
.foregroundStyle(Color.accent100)
.frame(height: 160)
.overlay { Text("Content") }
.padding()
}
.presented(.constant(true))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// LoadingViewEnvironment.swift
// AppProduct
//
// Created by 이예지 on 1/4/26.
//

import SwiftUI

// MARK: - Environment Keys

struct LoadingViewSizeKey: EnvironmentKey {
static let defaultValue: LoadingViewSize = .large
}

struct LoadingViewIsPresentedKey: EnvironmentKey {
static let defaultValue: Bool = false
}

struct LoadingViewMessageKey: EnvironmentKey {
static let defaultValue: String = "Loading..."
}
Copy link
Contributor

Choose a reason for hiding this comment

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

LodingView.swift에서 43번째 줄에서는 옵셔널로 받고 있는데, 해당 키에서 기본값으로 'Loading...'으로 명시해두면 LodingView.swift에서 옵셔널로 선언한 의미가 없어 보입니다.
제 생각에는 LoadingViewMessage는 Modifier로 제공하는 것보다 기본 파라미터를 옵셔널로 하는 것이 좋을거 같습니다! 해당 컴포넌트에서는 필수적인 기능이니까요

Copy link
Contributor

Choose a reason for hiding this comment

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

머져 내 닉네임 제.옹. 입.니.다.만. ^^ ㅋㅋㅋㅋㅋㅋ

Copy link
Contributor

Choose a reason for hiding this comment

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

앗..오타 스미마셍 ㅋㅎㅋㅎㅋㅎㅋㅎㅋㅎ


// MARK: - EnvironmentValues Extension

extension EnvironmentValues {
var loadingViewSize: LoadingViewSize {
get { self[LoadingViewSizeKey.self] }
set { self[LoadingViewSizeKey.self] = newValue }
}

var loadingViewIsPresented: Bool {
get { self[LoadingViewIsPresentedKey.self] }
set { self[LoadingViewIsPresentedKey.self] = newValue }
}

var loadingViewMessage: String {
get { self[LoadingViewMessageKey.self] }
set { self[LoadingViewMessageKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// LoadingViewModifier.swift
// AppProduct
//
// Created by 이예지 on 1/4/26.
//

import SwiftUI

// MARK: - AnyLoadingView Protocol

/// LoadingView 전용 프로토콜
/// 이 프로토콜을 준수하는 View만 LoadingView modifier 사용 가능
protocol AnyLoadingView: View { }


// MARK: - ViewModifiers

struct LoadingViewSizeModifier: ViewModifier {
let size: LoadingViewSize

func body(content: Content) -> some View {
content.environment(\.loadingViewSize, size)
}
}

struct LoadingViewIsPresentedModifier: ViewModifier {
@Binding var isPresented: Bool

func body(content: Content) -> some View {
content.environment(\.loadingViewIsPresented, isPresented)
}
}

struct LoadingViewMessageModifier: ViewModifier {
let message: String

func body(content: Content) -> some View {
content.environment(\.loadingViewMessage, message)
}
}

// MARK: - AnyLoadingView Extension

extension AnyLoadingView {

/// 로딩 사이즈 설정
/// - Parameter size: small, large
func loadingSize(_ size: LoadingViewSize) -> some View {
self.modifier(LoadingViewSizeModifier(size: size))
}

/// 로딩 isPresented
func presented(_ isPresented: Binding<Bool>) -> some View {
self.modifier(LoadingViewIsPresentedModifier(isPresented: isPresented))
}

/// 로딩 메시지 설정
func loadingMessage(_ message: String) -> some View {
self.modifier(LoadingViewMessageModifier(message: message))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Modifier chaining limitation due to return type.

The modifiers return some View instead of preserving the AnyLoadingView conformance, which breaks fluent API chaining. After calling one modifier, subsequent AnyLoadingView-specific modifiers won't be available.

For example, this won't compile:

LoadingView { Text("Content") }
    .loadingSize(.small)
    .presented(.constant(true)) // Error: 'some View' has no member 'presented'

Consider one of these solutions:

🔎 Option 1: Move modifiers to a View extension (simpler)
-extension AnyLoadingView {
+extension View {
     
     /// 로딩 사이즈 설정
     /// - Parameter size: small, large
     func loadingSize(_ size: LoadingViewSize) -> some View {
-        self.modifier(LoadingViewSizeModifier(size: size))
+        modifier(LoadingViewSizeModifier(size: size))
     }
     
     /// 로딩 isPresented
     func presented(_ isPresented: Binding<Bool>) -> some View {
-        self.modifier(LoadingViewIsPresentedModifier(isPresented: isPresented))
+        modifier(LoadingViewIsPresentedModifier(isPresented: isPresented))
     }
     
     /// 로딩 메시지 설정
     func loadingMessage(_ message: String) -> some View {
-        self.modifier(LoadingViewMessageModifier(message: message))
+        modifier(LoadingViewMessageModifier(message: message))
     }
 }
🔎 Option 2: Use wrapper types to preserve AnyLoadingView conformance

This approach is more complex but preserves the type constraint if that's important for your API design.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// LoadingViewSize.swift
// AppProduct
//
// Created by 이예지 on 1/4/26.
//

import SwiftUI

// MARK: - LoadingViewSize

/// LoadingView 사이즈 유형 (향후 확장용)
enum LoadingViewSize {
case small
case large

var size: CGSize {
switch self {
case .small:
return CGSize(width: 40, height: 40)
case .large:
return CGSize(width: 80, height: 80)
}
}

var font: Font {
switch self {
case .small:
return .app(.footnote, weight: .regular)
case .large:
return .app(.body, weight: .bold)
}
}
}