Skip to content
Merged
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
39 changes: 39 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Build

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build (${{ matrix.platform }})
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
include:
- platform: iOS
destination: generic/platform=iOS Simulator
- platform: Mac Catalyst
destination: generic/platform=macOS,variant=Mac Catalyst
- platform: macOS
destination: platform=macOS

steps:
- uses: actions/checkout@v6
- name: Build
run: >-
xcodebuild
-scheme CustomNavigationTitle
-destination '${{ matrix.destination }}'
CODE_SIGNING_ALLOWED=NO
build
Binary file added Assets/demo02.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct ContentView: View {
NavigationStack{
List {
Section{
VStack(spacing: 10){
VStack(alignment: .leading, spacing: 10){
Image(systemName: "hand.raised.app.fill")
.resizable()
.scaledToFit()
Expand All @@ -24,12 +24,11 @@ struct ContentView: View {
.bold()
.titleVisibilityAnchor()
Text("Control which apps can access your data, location, camera, and microphone, and manage safety protections. [Learn more..](https://www.apple.com).")
.multilineTextAlignment(.center)
.multilineTextAlignment(.leading)

}
.padding(22)
.frame(maxWidth: .infinity, alignment: .center)
.overlay(alignment: .topLeading){
.frame(maxWidth: .infinity, alignment: .leading)
.overlay(alignment: .top){
Text("This is Demo")
.fontWeight(.heavy)
.foregroundStyle(.red)
Expand All @@ -43,7 +42,7 @@ struct ContentView: View {
}
}
#if canImport(UIKit)
.listStyle(.insetGrouped)
.listStyle(.automatic)
#endif
.scrollAwareTitle("Privacy & Security")
.navigationTitle("Hello!")
Expand Down
4 changes: 3 additions & 1 deletion README-ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

CustomNavigationTitleは、スクロールに応じてナビゲーションタイトルをアニメーション付きで表示・非表示にするSwiftUI用のシンプルなパッケージです。(この効果はApple純正の設定アプリ、メールアプリ、AppStoreアプリで使われています。)

![Demo](Assets/demo01.gif)
| | |
|---|---|
| <img src="Assets/demo01.gif" width="300"> | <img src="Assets/demo02.gif" width="300"> |

## 特徴
- スクロールに応じてタイトルがフェードイン・アウト
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

CustomNavigationTitle is a simple SwiftUI package that animates the display and hiding of the navigation title based on scrolling. (This effect is used in Apple's native Settings, Mail, and App Store apps.)

![Demo](Assets/demo01.gif)
| | |
|---|---|
| <img src="Assets/demo01.gif" width="300"> | <img src="Assets/demo02.gif" width="300"> |

## Features
- The title fades in and out based on scrolling.
Expand Down
62 changes: 51 additions & 11 deletions Sources/CustomNavigationTitle/CustomNavigationTitle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,20 @@ extension View {
}
}

private struct ScrollAwareTitleModifier<V: View>: ViewModifier {
struct ScrollAwareTitleModifier<V: View>: ViewModifier {
@State private var isShowNavigationTitle = false
@Environment(\.scrollAwareTitleAnimation) private var animation

let title: V
let preferredSolariumEffect: Bool
var useSolariumEffect: Bool {
#if os(iOS)
if #available(iOS 26.0, *) {
return preferredSolariumEffect
}
#endif
return false
}

func body(content: Content) -> some View {
content
Expand All @@ -43,46 +53,76 @@ private struct ScrollAwareTitleModifier<V: View>: ViewModifier {
.toolbar {
ToolbarItem(placement: .principal) {
title
.bold()
.font(titleFont)
.lineLimit(1)
.opacity(isShowNavigationTitle ? 1 : 0)
.animation(animation, value: isShowNavigationTitle)
.blur(radius: preferredSolariumEffect ? (isShowNavigationTitle ? 0 : 4) : 0)
.offset(y: preferredSolariumEffect ? (isShowNavigationTitle ? 0 : 15) : 0)
.animation(resolvedAnimation, value: isShowNavigationTitle)
}
#if compiler(>=6.2)
#if compiler(>=6.2)
.modifier{
if #available(iOS 26.0, macOS 26.0, macCatalyst 26.0, *) {
$0.sharedBackgroundVisibility(.hidden)
} else {
$0
}
}
#endif
#endif
}
#if canImport(UIKit)
#if canImport(UIKit)
.navigationBarTitleDisplayMode(.inline)
#endif
#endif
}

private var titleFont: Font {
if #available(iOS 26.0, macOS 26.0, macCatalyst 26.0, *) {
// UICTFontTextStyleHeadline, weight: .semibold
Font.headline.weight(.semibold)
} else {
// UICTFontTextStyleShortEmphasizedBody
Font.body.bold().leading(.tight)
}
}

private var resolvedAnimation: Animation {
if preferredSolariumEffect, animation == nil {
let u = 0.5 // min(abs(scrollVelocity.dy) / 2, 1)
let durationScale = 1 - 0.5 * u

return isShowNavigationTitle
? .spring(duration: 0.45 * durationScale, bounce: 0.4 * u)
: .spring(duration: 0.70 * durationScale, bounce: 0.4 * u)
}
return animation ?? .easeIn(duration: 0.15)
}
}




extension View {
public func scrollAwareTitle<V: View>(@ViewBuilder _ title: () -> V) -> some View {
modifier(ScrollAwareTitleModifier(title: title()))
modifier(ScrollAwareTitleModifier(title: title(), preferredSolariumEffect: false))
}
internal func _scrollAwareTitle<V: View>(@ViewBuilder _ title: () -> V) -> some View {
modifier(ScrollAwareTitleModifier(title: title(), preferredSolariumEffect: true))
}
}

extension View {
@_disfavoredOverload public func scrollAwareTitle<S: StringProtocol>(_ title: S) -> some View {
scrollAwareTitle{
_scrollAwareTitle{
Text(title)
}
}
public func scrollAwareTitle(_ title: LocalizedStringKey) -> some View {
scrollAwareTitle{
_scrollAwareTitle{
Text(title)
}
}
@_disfavoredOverload public func scrollAwareTitle(_ title: LocalizedStringResource) -> some View {
scrollAwareTitle{
_scrollAwareTitle{
Text(title)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/CustomNavigationTitle/ScrollAwareTitleAnimation.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import SwiftUI

extension EnvironmentValues {
@Entry var scrollAwareTitleAnimation: Animation = .easeIn(duration: 0.15)
@Entry var scrollAwareTitleAnimation: Animation? = nil
}

extension View {
public func scrollAwareTitleAnimation(_ animation: Animation) -> some View {
public func scrollAwareTitleAnimation(_ animation: Animation?) -> some View {
environment(\.scrollAwareTitleAnimation, animation)
}
}