
做SwiftUI项目的启动图配置我一开始也觉得只是往工程里塞一张图片的事。实际点开Xcode新建的模板工程、在Info.plist里翻到Launch Screen相关配置之后才发现这套机制和“显示一张图”之间还隔着不少细节。这篇文章就从SwiftUI项目的实际配置流程讲起把Launch Screen的原理、Storyboard的搭建方式、深浅色和多尺寸适配以及我踩过的四个比较典型的坑都拆开来说适合正在用Xcode维护SwiftUI项目、想彻底搞清楚启动图配置的开发者参考。1. 先搞清楚启动图到底是“页面”还是一张“快照”很多从UIKit转过来的同行会把Launch Screen当作App的第一个页面想着在里面放一个UIImageView或者远程拉一张图。先说结论启动图不是页面它是系统在App进程真正跑起来之前根据你的启动配置文件渲染出来的一张静态快照。搞清楚这一点后面所有配置逻辑都顺了。1.1 系统在App真正启动前做了什么用户点图标之后App进程要完成装载、初始化、连库、跑首帧这个过程需要时间。在这段时间里系统不能给用户看一片黑于是它做了两件事第一读取工程里设置的启动配置文件渲染出完整的头图第二在App首帧渲染完成后把这张头图平滑替换成真实界面。所以启动图本质上是系统级的“占位页”它不归属于你的SwiftUI视图层级也不走你的任何Swift代码。这意味着你在启动图里放什么原生控件、什么特殊动画系统都只把它当成一张图来用。理解了这一点就能解释很多现象。比如你在Storyboard里放了一个按钮但用户启动时点它没反应因为它根本不是可控的交互元素又比如你在启动图里放了个网络图片控件冷启动时却不会加载网络图片因为系统渲染这张快照时根本没跑你的网络层。1.2 SwiftUI工程的三个配置落点SwiftUI项目配置启动图通常涉及三个地方了解它们的职责就不容易搞混第一个是LaunchScreen.storyboard这是实际承载启动图布局的文件。你可以在里面用AutoLayout摆Image View、Label、背景色也可以直接放一个纯色View。第二个是Assets.xcassets里的LaunchImage图片集如果Storyboard里的Image View引用了一张图片资源就放在这里按1x、2x、3x提供多倍图。第三个是Info.plist里的UILaunchStoryboardName键它告诉系统要用哪个Storyboard作为启动页默认值是LaunchScreen对应工程里同名文件。一个常见的误区是去网上找一种叫“LaunchImage图片替换法”的老方案往Assets.xcassets里的LaunchImage塞不同尺寸的整图。这个方案在旧版iOS上管用但在新版本Xcode创建的SwiftUI模板里系统优先读取的是UILaunchStoryboardName指定的StoryboardLaunchImage已经被标记为废弃用法。如果两个都配置了系统会以Storyboard为准你费半天劲导出的多尺寸图可能根本不会生效。提示判断系统当前走的是哪套配置最直接的办法是在Info.plist里搜UILaunchStoryboardName有值就说明用了Storyboard方案没有这个键才需要检查LaunchImage图片集。2. 手把手配置Launch Screen的完整步骤下面这是一套我从零开始配的流程直接对着Xcode操作即可。前提是你已经用Xcode建好了SwiftUI工程并且选择了Interface为SwiftUI、生命周期为SwiftUI App模板。2.1 创建LaunchScreen.storyboard并绑定到工程新模板默认会带一个LaunchScreen.storyboard但如果你是从老工程迁过来的或者模板里被删掉了需要手动创建。操作路径是File New File选择Interface下的StoryboardDevice选Universal文件名一般就叫LaunchScreen放到工程根目录。创建后左侧等于是一个空画布没有初始场景。必须先拖一个View Controller到画布上并把右侧Attributes面板里的Is Initial Controller勾上。如果不勾这一项系统找不到初始控制器启动图就是一片空白甚至在某些系统版本上直接显示黑屏。然后到Info.plist里确认配置。右键Info.plist选择Open As Source Code找到或者新建一项键名叫Launch screen interface file base name对应key就是UILaunchStoryboardName值填LaunchScreen。还有一种情况是工程开启了“Generate Info.plist File”这时你可以在Target的Build Settings里搜INFOPLIST_KEY_UILaunchStoryboardName手动设成LaunchScreen。两种方式等价生效规则也一样。2.2 向启动图里添加图标、文字与背景现在画布里已经有了一个初始控制器接下来往它的view上拖控件。绝大多数App的启动图是“居中Logo 底部口号 纯色背景”这种组合。拖一个Image View进画布在右侧属性里把Image选成你图片集里的图标名。不加任何约束的情况下Image View会跟着Storyboard里的frame大小走真机上很容易错位。我通常给Image View加三个约束水平居中、垂直居中、固定宽高比。具体约束是选中控件后点击右下角的Align和Add New Constraints按钮勾选Horizontally in Container、Vertically in Container再设置Width、Height或者用Aspect Ratio约束保证不变形。文字部分建议用Label而不是直接用切好的图片。因为在Storyboard里写文字系统可以根据当前App语言做本地化如果整张“带字图”切出来深色模式下白底图上方块边缘会很难看。Label同样加居中约束再设置字体、颜色。背景色直接在最外层View的Attributes面板里改Background也可以从Assets里选一个命名颜色。这里有个细节背景色建议用命名颜色资源而不是直接取纯数值色这样后续深色模式适配更省事。2.3 用Assets配置多分辨率的启动图图片图标资源放在Assets.xcassets里更合适。新建一个Image Set比如叫LaunchLogo然后在右侧属性里把Devices勾选为Universal。对于矢量的Logo直接把PDF或者SVG格式的图片拖进Single Scale位置并把属性面板里的Preserve Vector Data打开系统会在不同尺寸下自动缩放清晰度最好这也是我比较推荐的方式。如果只有PNG位图就需要在1x、2x、3x三个槽位分别放小图、中图、大图。这里的尺寸不是固定的取决于你希望Logo在屏幕上的实际显示大小。举个例子如果希望Logo在3x屏幕上显示为100*100pt那么3x位图就该是300*300像素2x是200*200像素1x是100*100像素。只放一张3x大图在1x槽位上会让小屏设备加载一张超大纹理内存占用翻好几倍还有些老旧系统会直接压缩变形能避免尽量避免。2.4 Build Settings里的关键关联项配置完Storyboard和图片之后还要检查Target的Build Settings。搜索Asset Catalog App Icon Set Name确认图标集名称搜索Launch Screen确认没有其他陈旧配置。如果你的工程混入了老式UILaunchImageFile键建议直接删掉因为它会和Storyboard方案冲突而且已经是废弃项留着他除了制造“为什么我的启动图没变”之类的问题没有别的意义。再确认一下Target MembershipLaunchScreen.storyboard必须属于当前App Target否则打包时会直接找不到启动文件。这里还要区分一个概念ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME和Storyboard无关只在工程完全不配置UILaunchStoryboardName时才生效。所以一旦你看到Info里存在UILaunchStoryboardName就别再关心LaunchImage相关设置了免得被多套配置绕晕。3. 适配深色模式和多种屏幕的细节处理启动图不是“画完就完事”的东西用户会拿它对比第一个界面稍有违和就觉得App粗糙。深色模式、刘海屏、灵动岛这些因素都得提前考虑进布局里。3.1 深浅色两套资源如何处理系统在渲染启动图时也会读取当前用户界面的样式所以你可以像配置普通App界面一样给启动图做深浅两套背景和Logo。具体做法是在Assets里新建一个Color Set在Appearances里勾选Any, Dark分别填两套色值在Storyboard里把背景View的Background选成这个命名色字体颜色如果也想适配就给Label用另一个命名色。系统在冷启动时会根据当前颜色方案自动选对应色值不需要你在Storyboard里写任何代码。图片资源同样支持多套外观。在Image Set的属性面板里把Appearances从Universal改成Any, Dark下面会出现Dark槽位。把深色版Logo放进去系统就会自动切换。这个能力很多人没注意到结果在深色模式下把黑底白Logo和深色背景撞在一起。实际操作中我建议Logo至少做黑、白两版浅色模式用深色Logo深色模式用白色Logo这样视觉一致性会好很多。3.2 刘海屏、灵动岛与安全区对布局的影响Storyboard里有两个层级的区域容易混淆一个是View的安全区一个是更外层的全屏区域。默认情况下Storyboard的View Controller启用的是Safe Area Layout Guide你拖进去的控件会依照安全区来定位这适合居中Logo这种不需要贴边的布局。但如果你想让背景色铺满整个屏幕包括溢出刘海和圆角的区域需要注意背景View本身是填满整个屏幕的它不受安全区约束因为View的背景自动覆盖完整屏幕而控件内部的内容才受安全区影响。我在配底部标语时常犯的一个错是把Label的bottom约束直接固定到Superview的bottom结果在带Home Indicator的机型上标语被那个横条压住看起来挤成一团。正确做法是使用Safe Area约束选中Label把Bottom约束拖到View Safe Area上。如果你希望底部考虑灵动岛区域Xcode新版本画布提供了多种设备预览可以随手切换机型看效果。总的来说居中内容用Center约束底部内容用Safe Area的Bottom约束顶部内容用Safe Area的Top约束这套组合能应对绝大多数机型。3.3 让启动图和第一个SwiftUI视图看起来“连在一起”“平滑衔接”的终极原则只有一个让启动图的最终画面和第一个SwiftUI页面的首帧画面尽量一致。很多App启动时闪一下白、再闪一下黑就是因为启动图是黑的、第一个页面是白的。我的做法是在SwiftUI的第一个视图里也用同一个命名背景色struct ContentView: View { var body: some View { ZStack { Color(LaunchBackground) .ignoresSafeArea() VStack { Image(LaunchLogo) .resizable() .scaledToFit() .frame(width: 96, height: 96) Text(正在加载) .font(.footnote) .foregroundColor(Color(LaunchTextColor)) } } } }这段代码里的LaunchBackground和Storyboard引用的是同一个Color Set这样用户根本察觉不到快照被替换的瞬间。还有一点要注意SwiftUI里的.ignoresSafeArea()必须加在背景色上否则首帧画面会在安全区外露出白色背景。4. 配置启动图时最容易踩的四个坑附排查过程接下来这部分是我实际项目中反复遇到的坑每一个我都给出去掉表象后的排查思路。4.1 坑一改了图跑起来却还是老样子这是启动图相关的最高频问题。你改了Storyboard里的Logo重新Run启动时看到的还是旧图。原因多半不是配置错了而是系统对启动图有快照缓存机制。在开发阶段系统会把上次成功渲染的启动图快照存到设备里冷启动检查到相同配置项时就直接复用缓存不会每次重新渲染。模拟器上尤其明显因为模拟器会跨启动保留springboard的缓存。应对方案有几层最轻的是卸载App再重装这会强制系统重建快照如果还不行清理一下模拟器的Device Erase All Content and Settings真机上可以在设置里找到App并删除重装。这里要注意一个容易误导人的点删除App后在多任务列表里把进程滑掉并不影响快照缓存因为快照不属于App进程。真正万无一失的办法是改完配置文件之后让系统认为配置发生了变更最简单的方式是卸载重装。4.2 坑二启动图能显示但图标位置跑偏表现是模拟器上居中图标明显偏上或偏下但在Storyboard里看着完全正常。排查时我先怀疑约束没加全打开画布的约束面板后发现CenterX和CenterY都在那问题就转向画布的预览尺寸和实际运行尺寸不一致。SwiftUI模板的LaunchScreen.storyboard有时会用任意尺寸作为画布基准比如812x375是iPhone X的尺寸而你在Main.storyboard里看到的是别的设备尺寸真机运行时系统按实际屏幕尺寸拉伸中心点就会产生偏差。解决方法是给控件加跟屏无关的约束或者用四边约束保持相对位置例如用Aspect Ratio锁定图标宽高用CenterY并给一个相对于CenterY的offset这个offset可以在不同尺寸下保持稳定。我个人更倾向于把所有关键控件的约束都切到Safe Area并设置Placeholder尺寸供画布预览真机运行结果更可靠。4.3 坑三App名称被自动加上的问题如果你的启动图什么内容都没放但运行后屏幕上出现了一个居中的App名别急着删代码那是系统在Storyboard没有提供任何可见内容时自动把App名称叠加上去的“兜底显示”。反过来你想让启动图显示App名称就不要用Label去手写一个一模一样的文字应该借助系统变量。在Storyboard的Label里把文本改成%这样的格式是没用的Storyboard本身并不支持从这里读取CFBundleDisplayName。比较靠谱的做法是在Target的Info.plist里把CFBundleDisplayName设置好然后通过UILabel的文本绑定做不到就直接在Storyboard里手写品牌名并保证和CFBundleDisplayName一致。要显示带版本号的“v1.2.3”这种我建议不要放在启动图里因为启动图是静态快照每次发版都要同步改Storyboard漏改就出现版本号错乱不如把这个信息放进第一个页面。4.4 坑四模拟器不显示启动图是代码问题吗很多人在模拟器上看到的是直接闪进主界面没有启动图便怀疑代码坏了。实际上模拟器的启动行为很大程度上取决于进程是否冷启动。如果你从Xcode直接Run调试器通常会等App进程起来才附加这会绕过部分启动流程导致启动图一闪而过甚至不显示。另外模拟器在重复启动同一个App时可能因为系统保留内存直接走了热恢复。这不是你配置错了。判断方法很简单断开Debug直接点击桌面图标启动或者重启模拟器再点图标。如果这时能看到启动图说明配置没问题。真机上的表现更接近真实用户建议最终验收以真机为准。4.5 一次完整排查链路从“黑屏”到最后定位我曾经在一个SwiftUI项目里遇到启动时钟完全黑屏。先把可能因素列出来排查第一检查Storyboard的Is Initial Controller确认画布里有初始场景第二检查Info.plist的UILaunchStoryboardName值发现已经被改成别的名字指向了一个不存在的Storyboard这是最直接的黑屏原因第三检查Storyboard的Background如果背景是Clear Color在深色系统下就会显示黑色看起来像黑屏。修复步骤是把UILaunchStoryboardName改回LaunchScreen同时把最外层View的Background设置为明确颜色或命名色问题就消失了。这个案例给的经验是黑屏十有八九不是SwiftUI代码的问题而是Storyboard配置本身的不完整。排查顺序应该先从Info.plist的启动键入手再检查Storyboard是否有初始控制器和明确的背景色不要一上来就去翻网络请求或首页代码。5. 启动图之外的进阶思路如果只是居中放Logo上面的步骤已经够用。当你开始想玩点花样就会撞上启动图的“静态快照”限制下面是我在项目里实际试过、也验证过的几条路径。5.1 想放“动画/动态图”怎么办启动图本身就只能静态渲染不支持GIF、视频、逐帧动画这是硬限制。不要试图在Storyboard里拖一个Player或者Image Sequence系统渲染快照时不会执行这些。真想要动态效果目前比较成熟的替代方案是启动图展示动画的第一帧第一个SwiftUI页面紧接着播放后续动画。这种“伪动态启动图”的效果用户感知上几乎等同于一进App就有动画实际上动画发生在App进程内。具体做法是在Home页面用一个State控制动画播放首帧故意设计成和LaunchScreen一致的画面然后在.onAppear里触发动画用户看不到明显的跳变。如果你的需求是冷启动结束后才去拿远程配置再决定跳转到哪个首页那么启动图阶段不需要承担任何“等待转圈”的工作直接在第一个SwiftUI视图里判断本地缓存、考虑是否显示加载中间态即可。5.2 用第一个SwiftUI页面做平滑过渡而非硬加载现在不少项目把启动图做得极简把真正的品牌过渡放到第一个页面来做。例如在ZStack里放一个全屏背景色然后让Logo从半透明到完全不透明地淡入或者在Logo下面让进度条从0走到100后切换页面。这么做的好处是启动图只负责“掩盖冷启动空白”真正的品牌表达和加载逻辑都放在你能控制的代码里调试修复方便得多。要注意的是这段动画里的耗时操作一定要放在后台队列。我常用的结构是struct LaunchContainerView: View { State private var progress: CGFloat 0 var body: some View { ZStack { Color(LaunchBackground).ignoresSafeArea() VStack { Image(LaunchLogo) .resizable() .scaledToFit() .frame(width: 120, height: 120) ProgressView(value: progress) .padding(.horizontal, 60) } } .task { for value in stride(from: 0.0, through: 1.0, by: 0.01) { try? await Task.sleep(nanoseconds: 30_000_000) progress value } } } }这里的1秒左右过渡足够掩盖系统替换启动图的瞬间又不会让用户觉得等待太久。真机上手感比在LaunchScreen里硬堆视觉元素自然得多。5.3 简化多环境多品牌配置的经验如果公司有多个品牌或白标App共用一个工程启动图配置最容易出问题。核心矛盾是UILaunchStoryboardName只能指向一个Storyboard名不同品牌可能需要不同Logo、不同背景。我的做法是不复制多份Storyboard而是只维护一份Storyboard里的Logo图把不同品牌的Logo分别放在不同的Image Set里再通过不同的Build Configuration或User-Defined Build Setting指定不同的图片名。Storyboard本身无法直接读取自定义配置来选图所以实际操作中是在Storyboard里放一个固定ImageView然后用不同Target各自替换同名图片资源。具体来说Target A的Assets里LaunchLogo放品牌A的LogoTarget B的Assets里LaunchLogo放品牌B的LogoStoryboard的引用名始终是LaunchLogo。因为不同Target会分别打包各自的Asset Catalog所以最终App看到的就是正确Logo。这样只维护一份Storyboard改品牌时只换资源不用动布局也是我个人比较推荐的白标方案。最后说一点项目里的个人经验启动图配置这块大量时间不是花在“拖控件”上而是花在“验证系统到底有没有按新配置走”上。所以我现在的开发流程里固定加了一步每次改动启动图先在模拟器上卸载重装一次再切到真机冷启动确认效果最后把模拟器的实体Home键退出、重启模拟器再点图标看一次。这套流程看起来笨但能一次性排除快照缓存带来的干扰省下大量反复Run的时间。如果你正在做一个刚起步的SwiftUI项目我建议把启动图背景做成命名色资源、Logo用PDF矢量图、约束锚定Safe Area这三件事一开始就做好后面无论如何换设备、切深色模式、发新版本都不需要再为启动图返工。