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
25 changes: 19 additions & 6 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ permissions:
jobs:
build-and-test:
name: Build and test
runs-on: macos-26
runs-on: xcode-27
timeout-minutes: 30

steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Select Xcode 26.6
run: sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer
- name: Verify Xcode 27 toolchain
run: |
xcodebuild -version
xcodebuild -showsdks

- name: Select an available iPhone simulator
shell: bash
Expand All @@ -33,23 +35,34 @@ jobs:
runtimes = json.load(sys.stdin)["devices"]
iphones = [
device
for devices in runtimes.values()
for runtime, devices in runtimes.items()
if "iOS-27" in runtime
for device in devices
if device["name"].startswith("iPhone") and device["isAvailable"]
]
if not iphones:
raise SystemExit("No available iPhone simulator found")
raise SystemExit("No available iOS 27 iPhone simulator found")
print(iphones[0]["udid"])
'
)"
echo "SIMULATOR_ID=$simulator_id" >> "$GITHUB_ENV"

- name: Build and run unit tests
- name: Build and run unit and interaction tests
run: |
xcodebuild test \
-project DotoryCount.xcodeproj \
-scheme DotoryCount \
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
-only-testing:DotoryCountTests \
-only-testing:DotoryCountUITests/DotoryCountUITests \
-resultBundlePath TestResults.xcresult \
-parallel-testing-enabled NO \
CODE_SIGNING_ALLOWED=NO

- name: Preserve test results and screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: ios-test-results
path: TestResults.xcresult
retention-days: 7
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to DotoryCount are recorded in this file.

## [0.6.0] - 2026-10-04

- Refine the app around a photographic jar exhibit, warm paper surfaces,
editorial typography, and quiet supporting day counts.
- Reserve Liquid Glass for interactive controls; add adaptive dark appearance
and scrollable layouts for larger text.
- Redesign anniversary editing with a live jar preview, calendar/direct date
entry, and quick date presets without automatically opening the keyboard.
- Share the exact calendar calculation and jar artwork between app and widget;
schedule seven midnight widget updates for accurate future day counts.
- Preserve existing local records, 36 fill stages, golden milestones, and replay.
- Build with Xcode 27 / iOS 27 SDK; app and widget version 0.6.0 (build 12).

## [0.5.0] - 2026-09-26

- Apply Liquid Glass to the dashboard action and information panel on iOS 26+,
Expand Down
45 changes: 45 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# DotoryCount design direction

## 시간을 담는 작은 오브제

DotoryCount의 중심은 날짜 숫자가 아니라 유리병을 보고 도토리를 담는 경험이다.
앱 아이콘의 사진형 유리병과 도토리를 시각 언어의 기준으로 삼는다. 화면은
따뜻한 종이 위에 하나의 오브제를 전시한 듯 조용하고 여유 있게 구성한다.

### 시각 원칙

- 따뜻한 아이보리, 잉크 같은 갈색, 황금도토리의 금색을 사용한다.
- 사진형 병은 화면에서 가장 큰 요소다. 장식용 도형과 큰 카드로 둘러싸지 않는다.
- 기념일 이름은 부드러운 세리프, 날짜 숫자는 가독성 좋은 라운드 숫자로 표현한다.
- 본문은 시스템 글꼴과 Dynamic Type을 사용한다. 큰 글자는 자연스럽게 줄바꿈하고 스크롤한다.
- Liquid Glass는 편집, 재생, 저장 같은 조작부에 사용한다. 읽는 정보는 무광 표면에 둔다.
- 밝은 화면과 어두운 화면 모두 따뜻한 색조와 읽기 쉬운 대비를 유지한다.

### 화면과 흐름

1. 첫 시작: 빈 유리병, 짧은 설명, 한 개의 기념일 만들기 버튼.
2. 홈: 이름과 시작일 → 병과 낙하 → 다시 담기 → 작은 D+ 숫자 → 다음 황금도토리.
3. 편집: 이름과 기준일을 각각 명확한 입력 영역에 놓고 아래에서 결과를 미리 확인한다.
4. 날짜 선택: 선택된 날짜를 먼저 보여주고 달력·직접 입력·빠른 선택을 제공한다.
5. 황금도토리: 이미지, 기념일, 날짜, 짧은 설명에 집중한다.
6. 홈 화면 위젯: 투명 여백을 잘라낸 큰 병과 아래의 작은 D+ 숫자.
7. 잠금화면 위젯: 이름과 숫자만 간결하게 읽히는 직사각형 구성.

### 만듦새 기준

- 병의 바닥·유리 반사·도토리 그림자는 앱과 위젯에서 같은 렌더링을 사용한다.
- 도토리는 입구를 통해 떨어지고, 바닥 위에 놓이며, 재생할 때 회전과 튀는 방향이 달라진다.
- 병과 조작 영역을 독립된 레이아웃으로 만들어 이름이나 숫자가 병 위에 겹치지 않는다.
- 기념일 날짜 계산, 윤년, 주년, 황금도토리는 앱과 위젯에서 동일한 계산기를 사용한다.
- 위젯에는 앞으로 7일의 자정 표시를 미리 제공한다.
- 동작 줄이기에서는 정지된 병을 표시하고 VoiceOver로 병·날짜·조작을 설명한다.

### 플랫폼 검증

개발 및 시뮬레이터 빌드는 Xcode 27 / iOS 27 SDK를 사용한다. 2026-10-04에
iPhone Air의 iOS 27.0.1 (24A446)에서 설치·실행과 다크 모드 홈 화면 배치를
확인했다. 기기의 패치 버전과 SDK 버전은 따로 기록한다. 자동 테스트 결과와
실기기에서 직접 확인한 항목은 구분한다.

이전 지원 범위인 iOS 17 이상은 유지한다. iOS 26 이상은 시스템 Liquid Glass,
그 이전은 표준 소재·버튼을 사용한다.
37 changes: 20 additions & 17 deletions DotoryCount.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
D06000000000000000000001 /* Shared */ = {isa = PBXFileSystemSynchronizedRootGroup; path = Shared; sourceTree = "<group>"; };
D05000000000000000000003 /* DotoryCountWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; path = DotoryCountWidget; sourceTree = "<group>"; };
A607FF973055937D00D23132 /* DotoryCount */ = {
isa = PBXFileSystemSynchronizedRootGroup;
Expand Down Expand Up @@ -89,6 +90,7 @@
children = (
A607FF973055937D00D23132 /* DotoryCount */,
D05000000000000000000003 /* DotoryCountWidget */,
D06000000000000000000001 /* Shared */,
A607FFA53055937F00D23132 /* DotoryCountTests */,
A607FFAF3055937F00D23132 /* DotoryCountUITests */,
A607FF963055937D00D23132 /* Products */,
Expand All @@ -115,7 +117,7 @@
buildPhases = (D05000000000000000000006 /* Sources */, D05000000000000000000004 /* Frameworks */, D05000000000000000000007 /* Resources */);
buildRules = ();
dependencies = ();
fileSystemSynchronizedGroups = (D05000000000000000000003 /* DotoryCountWidget */);
fileSystemSynchronizedGroups = (D05000000000000000000003 /* DotoryCountWidget */, D06000000000000000000001 /* Shared */);
name = DotoryCountWidget;
packageProductDependencies = ();
productName = DotoryCountWidget;
Expand All @@ -136,6 +138,7 @@
dependencies = (D0500000000000000000000A /* PBXTargetDependency */);
fileSystemSynchronizedGroups = (
A607FF973055937D00D23132 /* DotoryCount */,
D06000000000000000000001 /* Shared */,
);
name = DotoryCount;
packageProductDependencies = (
Expand Down Expand Up @@ -305,12 +308,12 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = DotoryCountWidget/DotoryCountWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = W395S42ZJ9;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Configuration/WidgetInfo.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MARKETING_VERSION = 0.5.0;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dotoryman.dotorycount.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -321,12 +324,12 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = DotoryCountWidget/DotoryCountWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = W395S42ZJ9;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Configuration/WidgetInfo.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MARKETING_VERSION = 0.5.0;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dotoryman.dotorycount.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -461,7 +464,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = W395S42ZJ9;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand All @@ -474,7 +477,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.5.0;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dotoryman.dotorycount;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
Expand All @@ -494,7 +497,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = W395S42ZJ9;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand All @@ -507,7 +510,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.5.0;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dotoryman.dotorycount;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
Expand All @@ -525,10 +528,10 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 12;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MARKETING_VERSION = 0.4.2;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dotoryman.dotorycount.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -546,10 +549,10 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 12;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MARKETING_VERSION = 0.4.2;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dotoryman.dotorycount.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -566,9 +569,9 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 12;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.4.2;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dotoryman.dotorycount.uitests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -585,9 +588,9 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 12;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.4.2;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dotoryman.dotorycount.uitests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand Down
25 changes: 17 additions & 8 deletions DotoryCount/DesignSystem/AppTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import UIKit

enum AppTheme {
static let background = adaptive(
light: UIColor(red: 0.97, green: 0.95, blue: 0.90, alpha: 1),
dark: UIColor(red: 0.10, green: 0.085, blue: 0.07, alpha: 1)
light: UIColor(red: 0.982, green: 0.971, blue: 0.947, alpha: 1),
dark: UIColor(red: 0.105, green: 0.094, blue: 0.081, alpha: 1)
)
static let surface = adaptive(
light: UIColor(white: 1, alpha: 0.72),
dark: UIColor(red: 0.18, green: 0.16, blue: 0.13, alpha: 0.92)
light: UIColor(red: 1, green: 0.992, blue: 0.977, alpha: 1),
dark: UIColor(red: 0.17, green: 0.151, blue: 0.125, alpha: 1)
)
static let accent = adaptive(
light: UIColor(red: 0.43, green: 0.27, blue: 0.13, alpha: 1),
dark: UIColor(red: 0.83, green: 0.64, blue: 0.40, alpha: 1)
light: UIColor(red: 0.37, green: 0.25, blue: 0.16, alpha: 1),
dark: UIColor(red: 0.89, green: 0.73, blue: 0.53, alpha: 1)
)
static let acorn = adaptive(
light: UIColor(red: 0.55, green: 0.32, blue: 0.13, alpha: 1),
Expand All @@ -35,10 +35,19 @@ enum AppTheme {
light: UIColor(red: 0.78, green: 0.90, blue: 0.90, alpha: 1),
dark: UIColor(red: 0.43, green: 0.62, blue: 0.64, alpha: 1)
)
static let secondaryText = Color.primary.opacity(0.62)
static let ink = adaptive(
light: UIColor(red: 0.24, green: 0.20, blue: 0.16, alpha: 1),
dark: UIColor(red: 0.95, green: 0.91, blue: 0.85, alpha: 1)
)
static let secondaryText = adaptive(
light: UIColor(red: 0.49, green: 0.44, blue: 0.38, alpha: 1),
dark: UIColor(red: 0.71, green: 0.66, blue: 0.58, alpha: 1)
)
static let divider = ink.opacity(0.10)
static let gold = Color(red: 0.67, green: 0.47, blue: 0.19)

static let cardCornerRadius: CGFloat = 24
static let horizontalPadding: CGFloat = 20
static let horizontalPadding: CGFloat = 26

private static func adaptive(light: UIColor, dark: UIColor) -> Color {
Color(uiColor: UIColor { traits in
Expand Down
60 changes: 60 additions & 0 deletions DotoryCount/DesignSystem/EditorialComponents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import SwiftUI

struct PaperBackdrop: View {
var body: some View {
AppTheme.background
.overlay(alignment: .top) {
LinearGradient(colors: [AppTheme.surface.opacity(0.65), .clear],
startPoint: .top, endPoint: .bottom)
.frame(height: 460)
}
.ignoresSafeArea()
}
}

struct BrandWordmark: View {
var body: some View {
HStack(spacing: 7) {
Image("AcornSprite").resizable().scaledToFit().frame(width: 24, height: 24)
.accessibilityHidden(true)
Text("DOTORY COUNT")
.font(.system(size: 10, weight: .semibold, design: .rounded))
.tracking(2.3)
.foregroundStyle(AppTheme.accent)
}
.accessibilityLabel("도토리 카운트")
}
}

struct EditorialEyebrow: View {
let title: String
var body: some View {
Text(title)
.font(.caption.weight(.medium))
.foregroundStyle(AppTheme.secondaryText)
}
}

struct PaperSurface: ViewModifier {
var cornerRadius: CGFloat = 24
func body(content: Content) -> some View {
content
.background(AppTheme.surface, in: RoundedRectangle(cornerRadius: cornerRadius))
.overlay {
RoundedRectangle(cornerRadius: cornerRadius)
.strokeBorder(AppTheme.divider.opacity(0.55), lineWidth: 0.5)
}
}
}

/// Glass is a functional layer. The photograph and reading surfaces remain matte.
struct GlassControlGroup<Content: View>: View {
@ViewBuilder var content: () -> Content
var body: some View {
if #available(iOS 26.0, *) {
GlassEffectContainer(spacing: 16) { content() }
} else {
content()
}
}
}
Loading
Loading