Skip to content

Commit f6619e6

Browse files
committed
feat(plugin): launchscreen plugin for iOS
release-npm
1 parent 92c3259 commit f6619e6

File tree

5 files changed

+145
-6
lines changed

5 files changed

+145
-6
lines changed

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,17 @@ Adding a `numic` property allows to configure script and plugin behaviour. This
141141
"oldArchitecture": true,
142142
// XCode customizations.
143143
"xcode": {
144-
developmentTeam: '123-456-789', // Automatically read from system distribution certificate if missing.
145-
category: 'public.app-category.productivity',
146-
displayName: 'My App'
147-
}
144+
"developmentTeam": "123-456-789", // Automatically read from system distribution certificate if missing.
145+
"category": "public.app-category.productivity",
146+
"displayName": "My App"
147+
},
148+
"launchscreen": {
149+
"background": "#FFFFFF",
150+
"title": "Hello World",
151+
"titleColor": "#000000",
152+
"subtitle": "year",
153+
"subtitleColor": "#EFEFEF"
154+
},
148155
// Plugins with separate documentations.
149156
"icon": {
150157
"icon": "image/icon/app-icon.png"

plugin/launchscreen.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { writeFileSync } from 'node:fs'
2+
import { join } from 'node:path'
3+
import glob from 'fast-glob'
4+
import type { PluginLog } from '../types'
5+
6+
function hexToRgbColor(hex: string) {
7+
const cleanHex = hex.replace('#', '').trim()
8+
const r = Number.parseInt(cleanHex.substring(0, 2), 16) / 255
9+
const g = Number.parseInt(cleanHex.substring(2, 4), 16) / 255
10+
const b = Number.parseInt(cleanHex.substring(4, 6), 16) / 255
11+
return `red="${r}" green="${g}" blue="${b}" alpha="1" colorSpace="calibratedRGB"`
12+
}
13+
14+
const templateApple = (
15+
backgroundColor: string,
16+
title: string,
17+
titleColor: string,
18+
subtitle: string,
19+
subtitleColor: string,
20+
) => `<?xml version="1.0" encoding="UTF-8"?>
21+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
22+
<device id="retina4_7" orientation="portrait" appearance="light"/>
23+
<dependencies>
24+
<deployment identifier="iOS"/>
25+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
26+
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
27+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
28+
</dependencies>
29+
<scenes>
30+
<!--View Controller-->
31+
<scene sceneID="EHf-IW-A2E">
32+
<objects>
33+
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
34+
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
35+
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
36+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
37+
<subviews>
38+
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="${title}" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
39+
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
40+
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
41+
<color key="textColor" ${titleColor}/>
42+
<nil key="highlightedColor"/>
43+
</label>
44+
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="${subtitle}" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="MN2-I3-ftu">
45+
<rect key="frame" x="0.0" y="626" width="375" height="21"/>
46+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
47+
<color key="textColor" ${subtitleColor}/>
48+
<nil key="highlightedColor"/>
49+
</label>
50+
</subviews>
51+
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
52+
<color key="backgroundColor" ${backgroundColor}/>
53+
<constraints>
54+
<constraint firstItem="Bcu-3y-fUS" firstAttribute="bottom" secondItem="MN2-I3-ftu" secondAttribute="bottom" constant="20" id="OZV-Vh-mqD"/>
55+
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
56+
<constraint firstItem="MN2-I3-ftu" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="akx-eg-2ui"/>
57+
<constraint firstItem="MN2-I3-ftu" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" id="i1E-0Y-4RG"/>
58+
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
59+
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="x7j-FC-K8j"/>
60+
</constraints>
61+
</view>
62+
</viewController>
63+
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
64+
</objects>
65+
<point key="canvasLocation" x="52.173913043478265" y="375"/>
66+
</scene>
67+
</scenes>
68+
</document>`
69+
70+
interface Options {
71+
launchscreen?: { background?: string; title?: string; titleColor?: string; subtitle?: 'year' | string; subtitleColor?: string }
72+
}
73+
74+
interface PluginInput {
75+
// Root project path.
76+
projectPath?: string
77+
// Location of /android or /ios folders, either root or inside /.numic.
78+
nativePath?: string
79+
log?: PluginLog
80+
options: Options
81+
// Currently installed React Native version.
82+
version?: string
83+
// App name.
84+
name: string
85+
}
86+
87+
export default ({ nativePath = process.cwd(), log = console.log, options = { launchscreen: {} }, name }: PluginInput) => {
88+
const { launchscreen } = options
89+
if (typeof launchscreen !== 'object') {
90+
return
91+
}
92+
93+
const launchScreenFilePath = glob.sync(join(nativePath, 'ios/*/LaunchScreen.storyboard'), {
94+
cwd: nativePath,
95+
})[0]
96+
97+
if (!launchScreenFilePath) {
98+
log('LaunchScreen.storyboard file not found', 'warning')
99+
return
100+
}
101+
102+
const currentYear = new Date().getFullYear().toString()
103+
const iOsLaunchScreen = templateApple(
104+
hexToRgbColor(launchscreen.background ?? '#FFFFFF'),
105+
launchscreen.title ?? name,
106+
hexToRgbColor(launchscreen.titleColor ?? '#000000'),
107+
(launchscreen.subtitle === 'year' ? currentYear : launchscreen.subtitle) ?? currentYear,
108+
hexToRgbColor(launchscreen.subtitleColor ?? '#000000'),
109+
)
110+
111+
writeFileSync(launchScreenFilePath, iOsLaunchScreen, 'utf-8')
112+
}

script/plugin.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { existsSync, readdirSync } from 'node:fs'
22
import { basename, join } from 'node:path'
33
import { commitChanges, resetRepository } from '../git'
4-
import { basePath, getFolders, log, options } from '../helper'
4+
import { basePath, getAppJsonName, getFolders, log, options } from '../helper'
55
import androidSdk from '../plugin/android-sdk/index'
66
import androidVersion from '../plugin/android-version'
77
import bundleId from '../plugin/bundle-id'
88
import icon from '../plugin/icon/index'
9+
import launchscreen from '../plugin/launchscreen'
910
import newArchitecture from '../plugin/new-architecture'
1011
import xcode from '../plugin/xcode'
1112
import type { PluginInput } from '../types'
1213

13-
const builtInPlugins = [androidVersion, bundleId, newArchitecture, xcode, androidSdk, icon]
14+
const builtInPlugins = [androidVersion, bundleId, newArchitecture, xcode, launchscreen, androidSdk, icon]
1415

1516
type PluginFunction = (options?: PluginInput) => void
1617
type Plugin = string | PluginFunction
@@ -40,6 +41,7 @@ const runPluginsIn = async (plugins: Plugin[], location: string, silent = false)
4041
// @ts-ignore
4142
options: typeof plugin === 'function' ? options() : (options()[basename(plugin)] ?? {}),
4243
version: options().reactNativeVersion,
44+
name: getAppJsonName() ?? options().pkg.name,
4345
})
4446
})
4547

test/plugin.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,20 @@ test('Enabling XCode without customization will write defaults.', async () => {
321321
// Default productivity
322322
expect(xcodeProjectContents).toContain('INFOPLIST_KEY_LSApplicationCategoryType = public.app-category.productivity;')
323323
})
324+
325+
test('Generates iOS launchscreen.', async () => {
326+
prepare([
327+
packageJson('plugin-launchscreen', {
328+
numic: { launchscreen: { background: '#EFEFEF', title: 'My Title', titleColor: '#ABABAB', subtitle: 'year' } },
329+
}),
330+
reactNativePkg,
331+
])
332+
333+
await native()
334+
335+
const launchscreenContents = readFile('ios/NumicApp/launchscreen.storyboard')
336+
expect(launchscreenContents).toContain('text="My Title"')
337+
expect(launchscreenContents).toContain(`text="${new Date().getFullYear()}"`) // Subtitle year.
338+
339+
console.log(launchscreenContents)
340+
})

types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface PluginInput {
3232
log?: (message: string, type?: 'error' | 'warning') => void
3333
options?: object
3434
version?: string
35+
name: string
3536
}
3637

3738
export enum RunLocation {

0 commit comments

Comments
 (0)