Site icon May's Notes

React Native 應用上架前的準備工作

React Native logo

這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408

Android & iOS 上架前都需要對應用進行一些基本設置,比如:應用的package (Bundle ID)、版本、icon…等,這邊簡單分享一下雙系統是如何去設置這些資訊的。

應用版本和包名

Android

versionCode, versionName 都在 android/app/build.gradle 中設置

android {
    ...
    defaultConfig {
        applicationId "com.test.pokedex"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 3
        versionName "1.0.2"
    }
    ...
}

package 在 android/app/src/main/AndroidManifest.xml 中設置,記得 AndroidManifest.xml 裡面的 package 和 android/app/build.gradle 中的 applicationId 要保持一致,不然打包的時候會失敗。

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.test.pokedex">
	...
</manifest>

iOS

iOS 應用的版本和 bundle id 在 General – Identity 設置

應用 icon

https://developer.android.com/training/multiscreen/screendensities?hl=zh-tw

上架前有一個非常重要的步驟是替換應用的 icon,如果不替換 icon 的話你的應用下載到設備中看起來就會是這樣的:

AndroidiOS

Android

應用的 icon, roundIcon 可以在 AndroidManifest.xml 看到,放在 mipmap 中

<application
  android:name=".MainApplication"
  android:label="@string/app_name"
  android:icon="@mipmap/ic_launcher"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:allowBackup="false"
  android:theme="@style/AppTheme">
  ...
</application>

android/app/src/main/res 底下有多個 mipmap 資料夾,每個資料夾中都有 ic_launcher.png, ic_launcher_round.png

這幾個資料夾是因為每台設備的像素密度不同所以需要製作不同 dpi 的圖片

可以使用工具快速製作不同像素密度的 icon

Icon Kitchen

將下載下來的檔案(android/res)全部覆蓋到 android/app/src/main/res

因為這個工具沒有生成 roundIcon,所以記得將 android/app/src/main/AndroidManifest.xml 中的 android:roundIcon="@mipmap/ic_launcher_round" 刪除。

iOS

在 Info – Information Property List 這邊添加 Icon Name 為 AppIcon

接著需要上傳應用所需要的 Icon 檔案,在左側找到 Images 然後將原本的 AppIcon 先右鍵刪除:

點擊左下角的 + -> import -> 將剛剛下載的 ios 資料夾整個導入進去:

iOS 如果沒有設置 AppIcon 的話 archive 時會失敗。

Splash screen

除了 App Icon 之外還有一個很重要的是 Splash screen,即應用開啟時的加載畫面。

要修改 Splash Screen 需要安裝 react-native-splash-screen

npm i react-native-splash-screen --save

為了在 App 啟動後關閉 Splash Screen 需要在 App.tsx 中調用 SplashScreen.hide()

import SplashScreen from 'react-native-splash-screen'

const App = () => {
  useEffect(() => {
    const ac = new AbortController()

    setTimeout(() => {
      SplashScreen.hide()
    }, 3000)

    return () => ac.abort()
  }, [])
	
  ..
}

export default App

Android

將要用作 Splash Screen 的圖片分別丟進對應的 dpi 資料夾中並改名為 launch_screen

在 android/app/src/main/res/drawable 新增 background_splash.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@color/splashscreen_bg"/>
    <item
        android:width="300dp"
        android:height="300dp"
        android:drawable="@mipmap/launch_screen"
        android:gravity="center" />
</layer-list>

在 android/app/src/main/res/values 新增 colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="splashscreen_bg">#FFFFF8</color>
	<color name="app_bg">#f2f2f2</color>
</resources>

在 android/app/src/main/res/values/styles.xml 添加

<resources>

    <em><!-- Base application theme. --></em>
    <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
        <em><!-- Customize your theme here. --></em>
        <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
        <em><!-- Add the following line to set the default status bar color for all the app. --></em>
        <item name="android:statusBarColor">@color/app_bg</item>
        <em><!-- Add the following line to set the default status bar text color for all the app 
        to be a light color (false) or a dark color (true) --></em>
        <item name="android:windowLightStatusBar">false</item>
        <em><!-- Add the following line to set the default background color for all the app. --></em>
        <item name="android:windowBackground">@color/app_bg</item>
    </style>

    <em><!-- Adds the splash screen definition --></em>
    <style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@drawable/background_splash</item>
    </style>
</resources>

修改 android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.test.pokedex">

    ...

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:allowBackup="false"
      android:theme="@style/SplashTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
      </activity>
    </application>
</manifest>

在 android/app/src/main/java/<PROJECT_NAME> 新增 SplashActivity.java

package com.test.pokedex; <em>// Change this to your package name.</em>

import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

public class SplashActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
        finish();
    }
}

在 MainActivity.java 中新增

...
import android.os.Bundle; <em>// 1.</em>
import org.devio.rn.splashscreen.SplashScreen; <em>// 2.</em>

public class MainActivity extends ReactActivity {
  ...
  @Override
    protected void onCreate(Bundle savedInstanceState) {
        SplashScreen.show(this); <em>// 3.</em>
        super.onCreate(savedInstanceState);
    }
}

在 app/src/main/res/layout(如果沒有這個資料夾就新增) 中新增 launch_screen.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="@mipmap/launch_screen" android:scaleType="centerCrop" />
</RelativeLayout>

iOS

使用工具生成 image sets,勾選 4x iOS

App Icon Generator

Xcode 左側找到 Images – + Image Set 新增 SlashIcon,將剛剛下載下來的三張圖片拖移進去

左側找到 LaunchScreen – View Controller Scene – View Controller – View 將原本畫面上的文字刪掉

右上角找到「+」新增 image view

右側 Image 選則剛剛新增的 SplashIcon

如果是手動將元素移到畫面中央的話,在不同解析度的設備上面會跑版,所以需要在右下角找到 Align 圖示新增 Constraints。

水平垂直都設為 0 的話元素會在畫面正中央:

Constraints 都設置好之後可以切換不同設備測試一下是否正常:

General – App Icons and Launch Screen 中將 Launch Screen File 設為 LaunchScreen.storyboard

接著在 ios/<PROJECT_NAME>/AppDelegate.mm 新增

...
#import "RNSplashScreen.h" <em>// Add this</em>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...

  [super application:application didFinishLaunchingWithOptions:launchOptions];
  [RNSplashScreen show];

  return YES;
}
...

Expo

https://docs.expo.dev/versions/latest/sdk/splash-screen

在 app.json 中設置

{
  "expo": {
    ...
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    ...
}

或者可以使用 expo-splash-screen

npx expo install expo-splash-screen
import React, { useCallback, useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import Entypo from '@expo/vector-icons/Entypo';
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';

<em>// Keep the splash screen visible while we fetch resources</em>
SplashScreen.preventAutoHideAsync();

export default function App() {
  const [appIsReady, setAppIsReady] = useState(false);

  useEffect(() => {
    async function prepare() {
      try {
        <em>// Pre-load fonts, make any API calls you need to do here</em>
        await Font.loadAsync(Entypo.font);
        <em>// Artificially delay for two seconds to simulate a slow loading</em>
        <em>// experience. Please remove this if you copy and paste the code!</em>
        await new Promise(resolve => setTimeout(resolve, 2000));
      } catch (e) {
        console.warn(e);
      } finally {
        <em>// Tell the application to render</em>
        setAppIsReady(true);
      }
    }

    prepare();
  }, []);

  const onLayoutRootView = useCallback(async () => {
    if (appIsReady) {
      <em>// This tells the splash screen to hide immediately! If we call this after</em>
      <em>// `setAppIsReady`, then we may see a blank screen while the app is</em>
      <em>// loading its initial state and rendering its first pixels. So instead,</em>
      <em>// we hide the splash screen once we know the root view has already</em>
      <em>// performed layout.</em>
      await SplashScreen.hideAsync();
    }
  }, [appIsReady]);

  if (!appIsReady) {
    return null;
  }

  return (
    <View
      style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}
      onLayout={onLayoutRootView}>
      <Text>SplashScreen Demo! 👋</Text>
      <Entypo name="rocket" size={30} />
    </View>
  );
}

Android 12 重複的 Splash Screen

如果是按照本篇前面的方式設定就不會發生這種情況。

在上架時收到了 Android 的相容性警告,說是使用 Android 12 以上版本會在啟動程式時出現兩個 Splash Screen:

這是因為從 Android 12 開始應用冷啟動和熱啟動時會顯示預設的 Splash Screen。預設的 Splash Screen 會是 launcher icon 和 theme 的 windowBackground 組合的。

如果應用的 Splash Screen 是自定義的,在 Android 12 或更高版本的設備上啟動應用程式就會出現重複的Splash Screen,首先顯示系統預設 Splash Screen,然後才顯示自定義的。

如果需要顯示自定義的 Splash Screen,那就需要將預設的覆蓋掉,覆蓋方式如下:

在 android/app/src/main/res/values/styles.xml 中添加 SplashTheme

<resources>
    ...

    <style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@drawable/background_splash</item>
    </style>
</resources>

打開 android/app/src/main/AndroidManifest.xml,修改 MainApplication 的 android:theme 為 @style/SplashTheme

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.test.pokedex">

    ...

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:allowBackup="false"
      android:theme="@style/SplashTheme">
      ...
    </application>
</manifest>

注意:如果不需要自定義 Splash Screen,也可以只修改 windowSplashScreenAnimatedIcon 和 windowSplashScreenBackground,更多請參考 Migrate your splash screen implementation to Android 12 and later

參考資料

Exit mobile version