Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
01728cb
feat: 오프스크린 없는 analytic 그림자 ShapeStyle.shadow(_:) API 추가
knine79 Jun 26, 2026
27c5bd8
refactor: TextField·Select 표면 그림자를 analytic으로 전환해 오프스크린 렌더링 제거
knine79 Jun 26, 2026
4cedb5c
refactor: SegmentedControl·TextArea 인디케이터 그림자를 analytic으로 전환해 오프스크린 렌…
knine79 Jun 26, 2026
9233057
refactor: FramedStyle 그림자를 analytic ShapeStyle.shadow로 전환해 오프스크린 렌더링 제거
knine79 Jun 26, 2026
8cd9b87
fix: Switch를 커스텀 Capsule 구현으로 전환해 tint 기인 on/off jank 제거
knine79 Jun 29, 2026
11fcf32
fix: MenuOptionRow 라벨을 최대폭으로 고정해 메뉴 선택 시 reflow jank 완화
knine79 Jun 29, 2026
30e9b8e
docs: analytic 그림자 API 추가에 따른 문서·라이선스 갱신
knine79 Jun 29, 2026
5afe089
fix: 라이선스 페치 시 HTTP 상태로 404·에러 응답 판정
knine79 Jun 29, 2026
a0a02d0
fix: curl 폴백에서 -k(insecure) 제거해 TLS 검증 유지
knine79 Jun 29, 2026
2848eca
docs: 라이선스 재생성 — 생성기 HTTP 오류 처리 수정 반영(lottie NOTICE 400 블록 제거)
knine79 Jun 29, 2026
7c68213
chore: Blueprint 빌드넘버 20260629003 갱신 (TestFlight 배포)
knine79 Jun 29, 2026
a6c0f1b
refactor: Switch thumb 크기·트랙 색상 semantic 토큰화
knine79 Jun 30, 2026
b0bdeb8
chore: Blueprint 빌드넘버 20260630001 갱신 (TestFlight 배포)
knine79 Jun 30, 2026
5a0ff85
fix: Switch thumb iOS 26 미만에서 기존 원형 모양 유지
knine79 Jun 30, 2026
57326af
chore: Blueprint 빌드넘버 20260630002 갱신 (TestFlight 배포)
knine79 Jun 30, 2026
9e5f2f9
refactor: Switch 애니메이션 spring에서 easeOut으로 변경
knine79 Jul 3, 2026
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Sources/Blueprint/Blueprint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20260629002;
CURRENT_PROJECT_VERSION = 20260630002;
DEVELOPMENT_TEAM = YEX9K6G97T;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand Down Expand Up @@ -579,7 +579,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20260629002;
CURRENT_PROJECT_VERSION = 20260630002;
DEVELOPMENT_TEAM = YEX9K6G97T;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand Down
9 changes: 7 additions & 2 deletions Sources/Blueprint/Sources/Utilities/PreviewOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,14 @@ struct MenuOptionRow<Content: View, Accessory: View>: View {
var body: some View {
HStack {
Text(title)
Menu(menuLabel) { content }
// 메뉴 라벨이 가용 최대폭을 차지하도록 둔다. 선택값(menuLabel)이 폭이 다른
// 문자열로 바뀌어도 버튼 프레임이 그대로라 행이 reflow되지 않는다.
Menu {
content
} label: {
Text(menuLabel).frame(maxWidth: .infinity, alignment: .leading)
}
accessory
Spacer(minLength: 0)
}
}
}
Expand Down
75 changes: 47 additions & 28 deletions Sources/Montage/1 Components/3 Selection And Input/Control.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,42 +76,61 @@ struct Control: View {
// MARK: - Body

@SwiftUI.State private var isPressed = false
private var originalToggleSize: CGSize {
if #available(iOS 26.0, *) {
.init(width: 65, height: 30)

@ViewBuilder private var switchThumb: some View {
let thumbInset: CGFloat = 2
let thumbHeight = boxSize.height - thumbInset * 2
if #available(iOS 26, *) {
let thumbWidth = thumbHeight * 1.4
let thumbTravel = boxSize.width - thumbWidth - thumbInset * 2
Capsule()
.fill(SwiftUI.Color.semantic(.staticWhite))
.shadow(color: SwiftUI.Color.black.opacity(0.12), radius: 2, x: 0, y: 1)
.frame(width: thumbWidth, height: thumbHeight)
.offset(x: state.isUnchecked ? -thumbTravel / 2 : thumbTravel / 2)
} else {
.init(width: 51, height: 31)
let thumbWidth = thumbHeight
let thumbTravel = boxSize.width - thumbWidth - thumbInset * 2
Circle()
.fill(SwiftUI.Color.semantic(.staticWhite))
.shadow(color: SwiftUI.Color.black.opacity(0.12), radius: 2, x: 0, y: 1)
.frame(width: thumbWidth, height: thumbHeight)
.offset(x: state.isUnchecked ? -thumbTravel / 2 : thumbTravel / 2)
}
}

var body: some View {
switch variant {
case .switch:
Toggle(
"",
isOn: Binding(
get: {
!state.isUnchecked
},
set: {
onSelect?($0 ? .checked : .unchecked)
})
)
.labelsHidden()
.disabled(disable)
.tint(backgroundColor)
.transformEffect(
CGAffineTransform(
scaleX: boxSize.width / originalToggleSize.width,
y: boxSize.height / originalToggleSize.height
)
)
// 네이티브 `Toggle`과 `UISwitch`는 `tintColor`를 커스텀 색으로 바꾸는 순간 iOS 26
// Liquid Glass 최적화 경로를 벗어나 on/off 애니메이션 프레임 드랍이 일어나서 커스텀으로 구현했다.
// (색을 `.tint`로 주든 `onTintColor`로 주든 동일).
ZStack {
Capsule()
.fill(SwiftUI.Color.semantic(.lineNormal).opacity(disable ? 0.43 : 1))
Capsule()
.fill(backgroundColor)
}
.frame(width: boxSize.width, height: boxSize.height)
.offset(
CGSize(
width: (originalToggleSize.width - boxSize.width) / 2 - 1,
height: (originalToggleSize.height - boxSize.height) / 2
))
.overlay {
switchThumb
}
.animation(.easeOut(duration: 0.2), value: state.isUnchecked)
.contentShape(Capsule())
.onTapGesture {
guard disable == false else { return }
onSelect?(state.isUnchecked ? .checked : .unchecked)
}
.accessibilityRepresentation {
Toggle(
"",
isOn: Binding(
get: { !state.isUnchecked },
set: { onSelect?($0 ? .checked : .unchecked) })
)
.labelsHidden()
.disabled(disable)
}
default:
HStack(alignment: .top, spacing: spacing) {
Group {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ public enum FramedStyle {
.opacity(disabled ? 0.43 : 1)
}
.clipShape(RoundedRectangle(cornerRadius: borderRadius))
.shadow(shadowLevel)
// 그림자를 배경 Shape의 fill에 analytic(`ShapeStyle.shadow`)으로 적용해 오프스크린
// 패스를 피한다. 프레임 실루엣(둥근 사각형) 기준으로 그림자가 그려진다.
.background(
RoundedRectangle(cornerRadius: borderRadius)
.fill(SwiftUI.Color.semantic(.backgroundNormal).shadow(shadowLevel))
)
.disabled(disabled)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,18 @@ public struct SegmentedControl: View {
Group {
switch variant {
case .solid:
// 그림자를 ZStack(합성 뷰)이 아니라 첫 pure shape(`.fill`)에 적용해
// analytic으로 캐스팅한다(오프스크린 패스 제거). 동일 실루엣이라 외형 동일.
ZStack {
RoundedRectangle(cornerRadius: buttonCornerRadius)
.foregroundStyle(SwiftUI.Color.semantic(.backgroundElevated))
.fill(SwiftUI.Color.semantic(.backgroundElevated))
.shadow(
color: .semantic(.staticBlack).opacity(0.08),
radius: buttonCornerRadius
)
RoundedRectangle(cornerRadius: buttonCornerRadius)
.foregroundStyle(SwiftUI.Color.semantic(.staticWhite).opacity(0.28))
}
.shadow(
color: .semantic(.staticBlack).opacity(0.08),
radius: buttonCornerRadius
)
.offset(x: buttonWidth * CGFloat(selectedIndex), y: 0)
.if(index == 0)
case .outlined:
Expand Down
30 changes: 15 additions & 15 deletions Sources/Montage/1 Components/3 Selection And Input/Select.swift
Original file line number Diff line number Diff line change
Expand Up @@ -390,31 +390,31 @@ public struct Select: View {
.rotationEffect(.degrees(menuPresented.wrappedValue ? 180 : 0))
}
.padding(.all, 12)
// 둥근 표면을 배경 Shape로 직접 그려 `clipShape`의 오프스크린 마스킹을 제거하고,
// 그림자는 pure shape(`.fill`)에 적용해 analytic으로 캐스팅한다(별도 오프스크린 패스 없음).
// 외형(둥근 모서리·머티리얼·옅은 그림자·테두리)은 동일하게 유지된다.
.background {
let surface = RoundedRectangle(cornerRadius: 12)
if disable {
SwiftUI.Color.semantic(.fillAlternative)
surface
.fill(SwiftUI.Color.semantic(.fillAlternative))
.shadow(color: .semantic(.staticBlack).opacity(0.03), radius: 2, x: 0, y: 1)
} else {
if colorScheme == .light {
SwiftUI.Color.atomic(.common100).opacity(0.8)
.background(.ultraThinMaterial)
} else {
SwiftUI.Color.atomic(.coolNeutral17).opacity(0.61)
.background(.ultraThinMaterial)
}
surface
.fill(
colorScheme == .light
? SwiftUI.Color.atomic(.common100).opacity(0.8)
: SwiftUI.Color.atomic(.coolNeutral17).opacity(0.61)
)
.shadow(color: .semantic(.staticBlack).opacity(0.03), radius: 2, x: 0, y: 1)
.background(.ultraThinMaterial, in: surface)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay {
RoundedRectangle(cornerRadius: 12)
.inset(by: 0.5)
.strokeBorder(strokeColor, lineWidth: menuPresented.wrappedValue ? 2 : 1)
}
.shadow(
color: .semantic(.staticBlack).opacity(0.03),
radius: 2,
x: 0,
y: 1
)
}

if !description.isEmpty {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,12 +639,14 @@ public struct TextArea: View {

private var indicator: some View {
ZStack {
// 그림자를 ZStack(합성 뷰)이 아니라 첫 pure shape(`.fill`)에 적용해 analytic으로
// 캐스팅한다(오프스크린 패스 제거). 동일 실루엣이라 외형은 동일하게 유지된다.
RoundedRectangle(cornerRadius: segmentRadius)
.foregroundStyle(SwiftUI.Color.semantic(.backgroundElevated))
.fill(SwiftUI.Color.semantic(.backgroundElevated))
.shadow(color: .semantic(.staticBlack).opacity(0.08), radius: segmentRadius)
RoundedRectangle(cornerRadius: segmentRadius)
.foregroundStyle(SwiftUI.Color.semantic(.staticWhite).opacity(0.28))
}
.shadow(color: .semantic(.staticBlack).opacity(0.08), radius: segmentRadius)
.frame(width: segmentSide, height: segmentSide)
.offset(x: segmentSide * CGFloat(selectedIndex))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ private extension TextField {
.strokeBorder(fieldStrokeColor, lineWidth: 1)
}
.background { focusRing }
.shadow(color: .black.opacity(0.03), radius: 1, x: 0, y: 1)
.contentShape(RoundedRectangle(cornerRadius: size.cornerRadius))
.onTapGesture {
textFieldFocusState = true
Expand Down Expand Up @@ -426,20 +425,24 @@ private extension TextField {

@ViewBuilder
var fieldBackground: some View {
Group {
if disable {
SwiftUI.Color.semantic(.fillAlternative)
} else if colorScheme == .light {
SwiftUI.Color.atomic(.common100)
.opacity(0.8)
.background(.ultraThinMaterial)
} else {
SwiftUI.Color.atomic(.coolNeutral17)
.opacity(0.61)
.background(.ultraThinMaterial)
}
// 둥근 표면을 배경 Shape로 직접 그려 `clipShape`의 오프스크린 마스킹을 제거하고,
// 그림자는 pure shape(`.fill`)에 적용해 analytic으로 캐스팅한다(별도 오프스크린 패스 없음).
// 외형(둥근 모서리·머티리얼·옅은 그림자)은 동일하게 유지된다.
let surface = RoundedRectangle(cornerRadius: size.cornerRadius)
if disable {
surface
.fill(SwiftUI.Color.semantic(.fillAlternative))
.shadow(color: .black.opacity(0.03), radius: 1, x: 0, y: 1)
} else {
surface
.fill(
colorScheme == .light
? SwiftUI.Color.atomic(.common100).opacity(0.8)
: SwiftUI.Color.atomic(.coolNeutral17).opacity(0.61)
)
.shadow(color: .black.opacity(0.03), radius: 1, x: 0, y: 1)
.background(.ultraThinMaterial, in: surface)
}
.clipShape(RoundedRectangle(cornerRadius: size.cornerRadius))
}

@ViewBuilder
Expand Down
Loading
Loading