Skip to content

Commit e75c37f

Browse files
committed
feat(mobile): add Apple Intelligence glow effect module for iOS
- Implement AppleIntelligenceGlowEffectModule with show/hide functionality - Create SwiftUI-based glow effect with dynamic gradient animations - Update expo-module.config.json to include new native module - Add debug panel options to trigger glow effect - Enhance DebugButton with ReAnimatedTouchableOpacity Signed-off-by: Innei <tukon479@gmail.com>
1 parent 09ccd39 commit e75c37f

File tree

5 files changed

+242
-9
lines changed

5 files changed

+242
-9
lines changed

apps/mobile/native/expo-module.config.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
{
22
"platforms": ["apple", "android"],
33
"apple": {
4-
"modules": ["SharedWebViewModule", "HelperModule", "ToasterModule"]
4+
"modules": [
5+
"SharedWebViewModule",
6+
"HelperModule",
7+
"ToasterModule",
8+
"AppleIntelligenceGlowEffectModule"
9+
]
510
},
611
"android": {
712
"modules": []
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// AppleIntelligenceGlowEffect
2+
//
3+
// https://github.com/jacobamobin/AppleIntelligenceGlowEffect/blob/main/IOS.swift
4+
//
5+
import SwiftUI
6+
7+
struct GlowEffect: View {
8+
@State private var gradientStops: [Gradient.Stop] = GlowEffect.generateGradientStops()
9+
10+
var body: some View {
11+
ZStack {
12+
EffectNoBlur(gradientStops: gradientStops, width: 6)
13+
.onAppear {
14+
// Start a timer to update the gradient stops every second
15+
Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { _ in
16+
withAnimation(.easeInOut(duration: 0.5)) {
17+
gradientStops = GlowEffect.generateGradientStops()
18+
}
19+
}
20+
}
21+
Effect(gradientStops: gradientStops, width: 9, blur: 4)
22+
.onAppear {
23+
// Start a timer to update the gradient stops every second
24+
Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { _ in
25+
withAnimation(.easeInOut(duration: 0.6)) {
26+
gradientStops = GlowEffect.generateGradientStops()
27+
}
28+
}
29+
}
30+
Effect(gradientStops: gradientStops, width: 11, blur: 12)
31+
.onAppear {
32+
// Start a timer to update the gradient stops every second
33+
Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { _ in
34+
withAnimation(.easeInOut(duration: 0.8)) {
35+
gradientStops = GlowEffect.generateGradientStops()
36+
}
37+
}
38+
}
39+
Effect(gradientStops: gradientStops, width: 15, blur: 15)
40+
.onAppear {
41+
// Start a timer to update the gradient stops every second
42+
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
43+
withAnimation(.easeInOut(duration: 1)) {
44+
gradientStops = GlowEffect.generateGradientStops()
45+
}
46+
}
47+
}
48+
}
49+
}
50+
51+
// Function to generate random gradient stops
52+
static func generateGradientStops() -> [Gradient.Stop] {
53+
[
54+
Gradient.Stop(color: Color(hex: "BC82F3"), location: Double.random(in: 0...1)),
55+
Gradient.Stop(color: Color(hex: "F5B9EA"), location: Double.random(in: 0...1)),
56+
Gradient.Stop(color: Color(hex: "8D9FFF"), location: Double.random(in: 0...1)),
57+
Gradient.Stop(color: Color(hex: "FF6778"), location: Double.random(in: 0...1)),
58+
Gradient.Stop(color: Color(hex: "FFBA71"), location: Double.random(in: 0...1)),
59+
Gradient.Stop(color: Color(hex: "C686FF"), location: Double.random(in: 0...1)),
60+
].sorted { $0.location < $1.location }
61+
}
62+
}
63+
64+
struct Effect: View {
65+
var gradientStops: [Gradient.Stop]
66+
var width: CGFloat
67+
var blur: CGFloat
68+
69+
var body: some View {
70+
ZStack {
71+
RoundedRectangle(cornerRadius: 55)
72+
.strokeBorder(
73+
AngularGradient(
74+
gradient: Gradient(stops: gradientStops),
75+
center: .center
76+
),
77+
lineWidth: width
78+
)
79+
.frame(
80+
width: UIScreen.main.bounds.width,
81+
height: UIScreen.main.bounds.height
82+
)
83+
.padding(.top, -17)
84+
.blur(radius: blur)
85+
}
86+
}
87+
}
88+
89+
struct EffectNoBlur: View {
90+
var gradientStops: [Gradient.Stop]
91+
var width: CGFloat
92+
93+
var body: some View {
94+
ZStack {
95+
RoundedRectangle(cornerRadius: 55)
96+
.strokeBorder(
97+
AngularGradient(
98+
gradient: Gradient(stops: gradientStops),
99+
center: .center
100+
),
101+
lineWidth: width
102+
)
103+
.frame(
104+
width: UIScreen.main.bounds.width,
105+
height: UIScreen.main.bounds.height
106+
)
107+
.padding(.top, -26)
108+
}
109+
}
110+
}
111+
112+
extension Color {
113+
init(hex: String) {
114+
let scanner = Scanner(string: hex)
115+
_ = scanner.scanString("#")
116+
117+
var hexNumber: UInt64 = 0
118+
scanner.scanHexInt64(&hexNumber)
119+
120+
let r = Double((hexNumber & 0xff0000) >> 16) / 255
121+
let g = Double((hexNumber & 0x00ff00) >> 8) / 255
122+
let b = Double(hexNumber & 0x0000ff) / 255
123+
124+
self.init(red: r, green: g, blue: b)
125+
}
126+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// AppleIntelligenceGlowEffectModule.swift
3+
// Pods
4+
//
5+
// Created by Innei on 2025/2/24.
6+
//
7+
8+
import ExpoModulesCore
9+
import SwiftUI
10+
import UIKit
11+
12+
public class AppleIntelligenceGlowEffectModule: Module {
13+
private var hostingController: UIHostingController<GlowEffect>?
14+
15+
public func definition() -> ModuleDefinition {
16+
Name("AppleIntelligenceGlowEffect")
17+
18+
Function("show") {
19+
DispatchQueue.main.async { [weak self] in
20+
guard let rootVC = Utils.getRootVC() else { return }
21+
let hostingController = UIHostingController(rootView: GlowEffect())
22+
self?.hostingController = hostingController
23+
24+
rootVC.addChild(hostingController)
25+
rootVC.view.addSubview(hostingController.view)
26+
hostingController.didMove(toParent: rootVC)
27+
28+
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
29+
NSLayoutConstraint.activate([
30+
hostingController.view.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
31+
hostingController.view.bottomAnchor.constraint(equalTo: rootVC.view.bottomAnchor),
32+
hostingController.view.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
33+
hostingController.view.trailingAnchor.constraint(equalTo: rootVC.view.trailingAnchor),
34+
])
35+
36+
hostingController.view.backgroundColor = .clear
37+
hostingController.view.isUserInteractionEnabled = false
38+
39+
hostingController.view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
40+
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
41+
hostingController.view.transform = .identity
42+
}
43+
}
44+
}
45+
46+
Function("hide") {
47+
DispatchQueue.main.async { [weak self] in
48+
guard let hostingController = self?.hostingController else { return }
49+
50+
UIView.animate(
51+
withDuration: 0.2,
52+
animations: {
53+
hostingController.view.alpha = 0
54+
hostingController.view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
55+
}
56+
) { _ in
57+
hostingController.willMove(toParent: nil)
58+
hostingController.view.removeFromSuperview()
59+
hostingController.removeFromParent()
60+
self?.hostingController = nil
61+
}
62+
}
63+
}
64+
}
65+
}
66+
67+
final class AppleIntelligenceGlowEffectView: UIViewControllerRepresentable {
68+
func makeUIViewController(context: Context) -> UIViewController {
69+
let viewController = UIViewController()
70+
let hostingController = UIHostingController(rootView: GlowEffect())
71+
72+
viewController.addChild(hostingController)
73+
viewController.view.addSubview(hostingController.view)
74+
hostingController.didMove(toParent: viewController)
75+
76+
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
77+
NSLayoutConstraint.activate([
78+
hostingController.view.topAnchor.constraint(equalTo: viewController.view.topAnchor),
79+
hostingController.view.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor),
80+
hostingController.view.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
81+
hostingController.view.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
82+
])
83+
84+
return viewController
85+
}
86+
func updateUIViewController(_ viewController: UIViewController, context: Context) {
87+
88+
}
89+
}

apps/mobile/src/modules/debug/index.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,10 @@ import { atomWithStorage } from "jotai/utils"
44
import { useMemo } from "react"
55
import { Dimensions } from "react-native"
66
import { Gesture, GestureDetector } from "react-native-gesture-handler"
7-
import Animated, {
8-
runOnJS,
9-
useAnimatedStyle,
10-
useSharedValue,
11-
withSpring,
12-
} from "react-native-reanimated"
7+
import { runOnJS, useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
138
import { useSafeAreaInsets } from "react-native-safe-area-context"
149

10+
import { ReAnimatedTouchableOpacity } from "@/src/components/common/AnimatedComponents"
1511
import { BugCuteReIcon } from "@/src/icons/bug_cute_re"
1612
import { JotaiPersistSyncStorage } from "@/src/lib/jotai"
1713

@@ -67,7 +63,10 @@ export const DebugButton = () => {
6763

6864
return (
6965
<GestureDetector gesture={gestureEvent}>
70-
<Animated.View
66+
<ReAnimatedTouchableOpacity
67+
onPress={() => {
68+
runOnJS(router.push)("/debug")
69+
}}
7170
style={[
7271
{
7372
position: "absolute",
@@ -80,7 +79,7 @@ export const DebugButton = () => {
8079
className="absolute mt-5 flex size-8 items-center justify-center rounded-l-md bg-accent"
8180
>
8281
<BugCuteReIcon height={24} width={24} color="#fff" />
83-
</Animated.View>
82+
</ReAnimatedTouchableOpacity>
8483
</GestureDetector>
8584
)
8685
}

apps/mobile/src/screens/(headless)/debug.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { sleep } from "@follow/utils"
2+
import { requireNativeModule } from "expo"
23
import * as Clipboard from "expo-clipboard"
34
import * as FileSystem from "expo-file-system"
45
import { Sitemap } from "expo-router/build/views/Sitemap"
@@ -113,6 +114,19 @@ export default function DebugPanel() {
113114
])
114115
},
115116
},
117+
118+
{
119+
title: "Glow Effect",
120+
onPress: () => {
121+
requireNativeModule("AppleIntelligenceGlowEffect").show()
122+
},
123+
},
124+
{
125+
title: "Hide Glow Effect",
126+
onPress: () => {
127+
requireNativeModule("AppleIntelligenceGlowEffect").hide()
128+
},
129+
},
116130
],
117131
},
118132

0 commit comments

Comments
 (0)