Currently, the Splash Screen plugin uses it's own ViewController that only shows the background image. A developer may have other items like logos or text on their LaunchScreen, so it should use the LaunchScreen Storyboard instead of making its own.
Secondly, since it is difficult for javascript to intercept the app state changes fast enough to display the splash screen (without creating a background task), it would be helpful if the Splash plugin could handle showing itself when the app becomes inactive. This is for apps where you don't want to show the app when it's in the background or immediately after it becomes active in case you have some authentication code you want to run first.
import Foundation
import AudioToolbox
@objc(CAPSplashScreenPlugin)
public class CAPSplashScreenPlugin: CAPPlugin {
var viewController = UIViewController();
var spinner = UIActivityIndicatorView()
var showSpinner: Bool = false
var call: CAPPluginCall?
var hideTask: Any?
var isVisible: Bool = false
let launchShowDuration = 3000
let launchAutoHide = true
var pauseAutoShow = false
var resumeAutoHide = false
var resumeShowDuration = 3000
let defaultFadeInDuration = 200
let defaultFadeOutDuration = 200
let defaultShowDuration = 3000
let defaultAutoHide = true
public override func load() {
buildViews()
showOnLaunch()
NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(resume), name: UIApplication.didBecomeActiveNotification, object: nil)
}
// Handle pause notification
@objc func pause(_ notification: Notification) {
let showOnPause = getConfigValue("pauseAutoShow") as? Bool ?? pauseAutoShow
if showOnPause {
showSplash(showDuration: defaultShowDuration, fadeInDuration: 0, fadeOutDuration: 0, autoHide: false, backgroundColor: nil, spinnerStyle: nil, spinnerColor: nil, completion: { }, isLaunchSplash: false)
}
}
// Handle resume notification
@objc func resume(_ notification: Notification) {
let hideOnResume = getConfigValue("resumeAutoHide") as? Bool ?? resumeAutoHide
if hideOnResume {
let showDuration = getConfigValue("resumeShowDuration") as? Int ?? resumeShowDuration
self.hideTask = DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + (Double(showDuration) / 1000)
) {
self.hideSplash(fadeOutDuration: self.defaultFadeOutDuration, isLaunchSplash: false)
}
}
}
// Set the pause options
@objc public func setPauseScreen(_ call: CAPPluginCall) {
self.call = call
pauseAutoShow = call.get("pauseAutoShow", Bool.self, pauseAutoShow)!
resumeAutoHide = call.get("resumeAutoHide", Bool.self, resumeAutoHide)!
resumeShowDuration = call.get("resumeShowDuration", Int.self, resumeShowDuration)!
call.success()
}
// Show the splash screen
@objc public func show(_ call: CAPPluginCall) {
self.call = call
let showDuration = call.get("showDuration", Int.self, defaultShowDuration)!
let fadeInDuration = call.get("fadeInDuration", Int.self, defaultFadeInDuration)!
let fadeOutDuration = call.get("fadeOutDuration", Int.self, defaultFadeOutDuration)!
let autoHide = call.get("autoHide", Bool.self, defaultAutoHide)!
let backgroundColor = getConfigValue("backgroundColor") as? String ?? nil
let spinnerStyle = getConfigValue("iosSpinnerStyle") as? String ?? nil
let spinnerColor = getConfigValue("spinnerColor") as? String ?? nil
showSpinner = getConfigValue("showSpinner") as? Bool ?? false
showSplash(showDuration: showDuration, fadeInDuration: fadeInDuration, fadeOutDuration: fadeOutDuration, autoHide: autoHide, backgroundColor: backgroundColor, spinnerStyle: spinnerStyle, spinnerColor: spinnerColor, completion: {
call.success()
}, isLaunchSplash: false)
}
// Hide the splash screen
@objc public func hide(_ call: CAPPluginCall) {
self.call = call
let fadeDuration = call.get("fadeOutDuration", Int.self, defaultFadeOutDuration)!
hideSplash(fadeOutDuration: fadeDuration)
call.success()
}
func buildViews() {
viewController = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!
// Observe for changes on frame and bounds to handle rotation resizing
let parentView = bridge.viewController.view
parentView?.addObserver(self, forKeyPath: "frame", options: .new, context: nil)
parentView?.addObserver(self, forKeyPath: "bounds", options: .new, context: nil)
updateSplashImageBounds()
showSpinner = getConfigValue("showSpinner") as? Bool ?? false
if showSpinner {
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.startAnimating()
}
}
func tearDown() {
isVisible = false
bridge.viewController.view.isUserInteractionEnabled = true
viewController.view.removeFromSuperview()
if showSpinner {
spinner.removeFromSuperview()
}
}
// Update the bounds for the splash image. This will also be called when
// the parent view observers fire
func updateSplashImageBounds() {
guard let delegate = UIApplication.shared.delegate else {
bridge.modulePrint(self, "Unable to find root window object for SplashScreen bounds. Please file an issue")
return
}
guard let window = delegate.window as? UIWindow else {
bridge.modulePrint(self, "Unable to find root window object for SplashScreen bounds. Please file an issue")
return
}
viewController.view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: window.bounds.size)
}
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
updateSplashImageBounds()
}
func showOnLaunch() {
let launchShowDurationConfig = getConfigValue("launchShowDuration") as? Int ?? launchShowDuration
let launchAutoHideConfig = getConfigValue("launchAutoHide") as? Bool ?? launchAutoHide
let launchBackgroundColorConfig = getConfigValue("backgroundColor") as? String ?? nil
let launchSpinnerStyleConfig = getConfigValue("iosSpinnerStyle") as? String ?? nil
let launchSpinnerColorConfig = getConfigValue("spinnerColor") as? String ?? nil
let view = bridge.viewController.view
view?.addSubview(viewController.view)
if showSpinner {
view?.addSubview(spinner)
spinner.centerXAnchor.constraint(equalTo: view!.centerXAnchor).isActive = true
spinner.centerYAnchor.constraint(equalTo: view!.centerYAnchor).isActive = true
}
showSplash(showDuration: launchShowDurationConfig, fadeInDuration: 0, fadeOutDuration: defaultFadeOutDuration, autoHide: launchAutoHideConfig, backgroundColor: launchBackgroundColorConfig, spinnerStyle: launchSpinnerStyleConfig, spinnerColor: launchSpinnerColorConfig, completion: {
}, isLaunchSplash: true)
}
func showSplash(showDuration: Int, fadeInDuration: Int, fadeOutDuration: Int, autoHide: Bool, backgroundColor: String?, spinnerStyle: String?, spinnerColor: String?, completion: @escaping () -> Void, isLaunchSplash: Bool) {
DispatchQueue.main.async {
if backgroundColor != nil {
self.viewController.view.backgroundColor = UIColor(fromHex: backgroundColor!)
}
let view = self.bridge.viewController.view
if self.showSpinner {
if spinnerStyle != nil {
switch spinnerStyle!.lowercased() {
case "small":
self.spinner.style = .white
default:
self.spinner.style = .whiteLarge
}
}
if spinnerColor != nil {
self.spinner.color = UIColor(fromHex: spinnerColor!)
}
}
if !isLaunchSplash {
view?.addSubview(self.viewController.view)
if self.showSpinner {
view?.addSubview(self.spinner)
self.spinner.centerXAnchor.constraint(equalTo: view!.centerXAnchor).isActive = true
self.spinner.centerYAnchor.constraint(equalTo: view!.centerYAnchor).isActive = true
}
}
view?.isUserInteractionEnabled = false
UIView.transition(with: self.viewController.view, duration: TimeInterval(Double(fadeInDuration) / 1000), options: .curveLinear, animations: {
self.viewController.view.alpha = 1
if self.showSpinner {
self.spinner.alpha = 1
}
}) { (finished: Bool) in
self.isVisible = true
if autoHide {
self.hideTask = DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + (Double(showDuration) / 1000)
) {
self.hideSplash(fadeOutDuration: fadeOutDuration, isLaunchSplash: isLaunchSplash)
completion()
}
}
}
}
}
func hideSplash(fadeOutDuration: Int) {
hideSplash(fadeOutDuration: fadeOutDuration, isLaunchSplash: false)
}
func hideSplash(fadeOutDuration: Int, isLaunchSplash: Bool) {
if isLaunchSplash, isVisible {
CAPLog.print("SplashScreen.hideSplash: SplashScreen was automatically hidden after default timeout. " +
"You should call `SplashScreen.hide()` as soon as your web app is loaded (or increase the timeout). " +
"Read more at https://capacitor.ionicframework.com/docs/apis/splash-screen/#hiding-the-splash-screen")
}
if !isVisible { return }
DispatchQueue.main.async {
UIView.transition(with: self.viewController.view, duration: TimeInterval(Double(fadeOutDuration) / 1000), options: .curveLinear, animations: {
self.viewController.view.alpha = 0
if self.showSpinner {
self.spinner.alpha = 0
}
}) { (finished: Bool) in
self.tearDown()
}
}
}
}