@craft-native/ios 0.0.368 → 0.0.374
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -3
- package/dist/cli.js +314 -158
- package/dist/index.d.ts +6 -2
- package/dist/index.js +338 -179
- package/dist/runtime-profile.d.ts +13 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +279 -3
- package/templates/CraftBluetooth.swift +12 -0
- package/templates/CraftHealth.swift +124 -8
- package/templates/CraftHeartRate.swift +13 -0
- package/templates/CraftHybrid.swift +52 -11
- package/templates/CraftLiveActivities.swift +2 -0
- package/templates/CraftMusic.swift +23 -0
- package/templates/CraftNativeActions.swift +54 -8
- package/templates/CraftNativeFiles.swift +7 -0
- package/templates/CraftNativeScreen.swift +155 -0
- package/templates/CraftNativeServices.swift +19 -13
- package/templates/CraftVoiceRecording.swift +13 -1
|
@@ -2245,6 +2245,7 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
2245
2245
|
}
|
|
2246
2246
|
|
|
2247
2247
|
private static func locationPermissionGranted() -> Bool {
|
|
2248
|
+
#if CRAFT_PERMISSION_LOCATION
|
|
2248
2249
|
let status: CLAuthorizationStatus
|
|
2249
2250
|
if #available(iOS 14.0, *) {
|
|
2250
2251
|
status = CLLocationManager().authorizationStatus
|
|
@@ -2252,6 +2253,9 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
2252
2253
|
status = CLLocationManager.authorizationStatus()
|
|
2253
2254
|
}
|
|
2254
2255
|
return status == .authorizedAlways || status == .authorizedWhenInUse
|
|
2256
|
+
#else
|
|
2257
|
+
return false
|
|
2258
|
+
#endif
|
|
2255
2259
|
}
|
|
2256
2260
|
}
|
|
2257
2261
|
|
|
@@ -5316,6 +5320,115 @@ private final class CraftNativeButton: UIButton {
|
|
|
5316
5320
|
}
|
|
5317
5321
|
}
|
|
5318
5322
|
|
|
5323
|
+
/// Opt-in solid switch. Draws only public UIControl content; it neither walks
|
|
5324
|
+
/// UISwitch's private views nor changes UIKit animations elsewhere in the app.
|
|
5325
|
+
final class CraftNativeSolidSwitch: UIControl {
|
|
5326
|
+
private(set) var isOn = false
|
|
5327
|
+
var onColor: UIColor? { didSet { setNeedsDisplay() } }
|
|
5328
|
+
var offColor: UIColor? { didSet { setNeedsDisplay() } }
|
|
5329
|
+
var thumbColor: UIColor? { didSet { setNeedsDisplay() } }
|
|
5330
|
+
private let naturalSize = UISwitch().intrinsicContentSize
|
|
5331
|
+
private var touchOrigin: CGPoint?
|
|
5332
|
+
private var dragPosition: CGFloat?
|
|
5333
|
+
override var intrinsicContentSize: CGSize { naturalSize }
|
|
5334
|
+
override var isEnabled: Bool {
|
|
5335
|
+
didSet {
|
|
5336
|
+
if !isEnabled { cancelTracking(with: nil) }
|
|
5337
|
+
refreshAccessibility()
|
|
5338
|
+
setNeedsDisplay()
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
override var canBecomeFirstResponder: Bool { isEnabled }
|
|
5342
|
+
|
|
5343
|
+
override init(frame: CGRect) {
|
|
5344
|
+
super.init(frame: frame)
|
|
5345
|
+
isOpaque = false
|
|
5346
|
+
contentMode = .redraw
|
|
5347
|
+
isAccessibilityElement = true
|
|
5348
|
+
refreshAccessibility()
|
|
5349
|
+
}
|
|
5350
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
5351
|
+
|
|
5352
|
+
func setOn(_ value: Bool) {
|
|
5353
|
+
isOn = value
|
|
5354
|
+
refreshAccessibility()
|
|
5355
|
+
setNeedsDisplay()
|
|
5356
|
+
}
|
|
5357
|
+
|
|
5358
|
+
func refreshAccessibility() {
|
|
5359
|
+
accessibilityTraits = .button
|
|
5360
|
+
if #available(iOS 17.0, *) { accessibilityTraits.insert(.toggleButton) }
|
|
5361
|
+
if isOn { accessibilityTraits.insert(.selected) }
|
|
5362
|
+
if !isEnabled { accessibilityTraits.insert(.notEnabled) }
|
|
5363
|
+
accessibilityValue = isOn ? "1" : "0"
|
|
5364
|
+
}
|
|
5365
|
+
|
|
5366
|
+
private var trackRect: CGRect {
|
|
5367
|
+
let size = CGSize(width: min(bounds.width, naturalSize.width), height: min(bounds.height, naturalSize.height))
|
|
5368
|
+
return CGRect(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2, width: size.width, height: size.height)
|
|
5369
|
+
}
|
|
5370
|
+
var thumbRect: CGRect {
|
|
5371
|
+
let track = trackRect.insetBy(dx: 2, dy: 2)
|
|
5372
|
+
let onRight = effectiveUserInterfaceLayoutDirection != .rightToLeft
|
|
5373
|
+
let position = dragPosition ?? ((isOn == onRight) ? 1 : 0)
|
|
5374
|
+
return CGRect(x: track.minX + (track.width - track.height) * position, y: track.minY, width: track.height, height: track.height)
|
|
5375
|
+
}
|
|
5376
|
+
override func draw(_ rect: CGRect) {
|
|
5377
|
+
let track = trackRect
|
|
5378
|
+
guard track.width > 4, track.height > 4 else { return }
|
|
5379
|
+
(isEnabled ? (isOn ? onColor ?? .systemGreen : offColor ?? .systemGray4) : UIColor.systemGray4).setFill()
|
|
5380
|
+
UIBezierPath(roundedRect: track, cornerRadius: track.height / 2).fill()
|
|
5381
|
+
// The solid variant deliberately ignores thumb alpha. No transient
|
|
5382
|
+
// glass, scaling, fade, implicit layer animation or delayed redraw.
|
|
5383
|
+
(thumbColor ?? .white).withAlphaComponent(1).setFill()
|
|
5384
|
+
UIBezierPath(ovalIn: thumbRect).fill()
|
|
5385
|
+
}
|
|
5386
|
+
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
5387
|
+
super.traitCollectionDidChange(previousTraitCollection)
|
|
5388
|
+
setNeedsDisplay()
|
|
5389
|
+
}
|
|
5390
|
+
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
|
5391
|
+
guard isEnabled else { return false }
|
|
5392
|
+
touchOrigin = touch.location(in: self)
|
|
5393
|
+
dragPosition = nil
|
|
5394
|
+
return true
|
|
5395
|
+
}
|
|
5396
|
+
func updateDrag(to point: CGPoint) {
|
|
5397
|
+
let track = trackRect.insetBy(dx: 2, dy: 2)
|
|
5398
|
+
dragPosition = min(1, max(0, (point.x - track.minX - track.height / 2) / max(1, track.width - track.height)))
|
|
5399
|
+
setNeedsDisplay()
|
|
5400
|
+
}
|
|
5401
|
+
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
|
5402
|
+
guard isEnabled, let origin = touchOrigin else { return false }
|
|
5403
|
+
let point = touch.location(in: self)
|
|
5404
|
+
if dragPosition != nil || abs(point.x - origin.x) > 6 { updateDrag(to: point) }
|
|
5405
|
+
return true
|
|
5406
|
+
}
|
|
5407
|
+
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
|
5408
|
+
defer { touchOrigin = nil; dragPosition = nil; setNeedsDisplay() }
|
|
5409
|
+
guard isEnabled, let touch, touchOrigin != nil,
|
|
5410
|
+
bounds.insetBy(dx: -10, dy: -10).contains(touch.location(in: self)) else { return }
|
|
5411
|
+
let next = dragPosition.map { ($0 >= 0.5) == (effectiveUserInterfaceLayoutDirection != .rightToLeft) } ?? !isOn
|
|
5412
|
+
if next != isOn { setOn(next); sendActions(for: .valueChanged) }
|
|
5413
|
+
}
|
|
5414
|
+
override func cancelTracking(with event: UIEvent?) {
|
|
5415
|
+
touchOrigin = nil
|
|
5416
|
+
dragPosition = nil
|
|
5417
|
+
setNeedsDisplay()
|
|
5418
|
+
}
|
|
5419
|
+
override func accessibilityActivate() -> Bool {
|
|
5420
|
+
guard isEnabled else { return false }
|
|
5421
|
+
setOn(!isOn)
|
|
5422
|
+
sendActions(for: .valueChanged)
|
|
5423
|
+
return true
|
|
5424
|
+
}
|
|
5425
|
+
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
|
5426
|
+
if isEnabled, presses.contains(where: { $0.key?.keyCode == .keyboardSpacebar || $0.key?.keyCode == .keyboardReturnOrEnter }) {
|
|
5427
|
+
_ = accessibilityActivate()
|
|
5428
|
+
} else { super.pressesEnded(presses, with: event) }
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5319
5432
|
/// A native checkbox button. UIKit has no checkbox control, so use the system
|
|
5320
5433
|
/// square/checkmark symbols while keeping the control's title and accessibility
|
|
5321
5434
|
/// state native and keyboard/touch reachable.
|
|
@@ -6786,6 +6899,20 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6786
6899
|
actionSheet: function(options) { return request('Dialog', 'actionSheet', [options || {}], true).then(function(id) { return typeof id === 'string' ? id : null; }); }
|
|
6787
6900
|
};
|
|
6788
6901
|
}
|
|
6902
|
+
if (Array.isArray(bridge.capabilities) && bridge.capabilities.indexOf('health') >= 0) {
|
|
6903
|
+
var health = craft.health || {};
|
|
6904
|
+
var discardWorkout = function() { return request('Health', 'discardWorkout', []); };
|
|
6905
|
+
health.discardWorkout = discardWorkout;
|
|
6906
|
+
Object.defineProperty(craft, 'health', {
|
|
6907
|
+
configurable: true, enumerable: true,
|
|
6908
|
+
get: function() { return health; },
|
|
6909
|
+
set: function(value) {
|
|
6910
|
+
if (!value || typeof value !== 'object') return;
|
|
6911
|
+
value.discardWorkout = discardWorkout;
|
|
6912
|
+
health = value;
|
|
6913
|
+
}
|
|
6914
|
+
});
|
|
6915
|
+
}
|
|
6789
6916
|
if (Array.isArray(bridge.capabilities) && bridge.capabilities.indexOf('shortcuts') >= 0) {
|
|
6790
6917
|
var shortcuts = {
|
|
6791
6918
|
set: function(items) { return request('Shortcuts', 'set', [items]); },
|
|
@@ -7017,6 +7144,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7017
7144
|
keep('navigation', {
|
|
7018
7145
|
setOptions: function(options) { craftNativeSetOptions(JSON.stringify(options || {})); }
|
|
7019
7146
|
});
|
|
7147
|
+
// shortcuts already has a capability-owned accessor above.
|
|
7148
|
+
// Do not wrap it in keep(): its adopt setter would let a
|
|
7149
|
+
// bundle replace native set/clear during bootstrap.
|
|
7020
7150
|
})();
|
|
7021
7151
|
""")
|
|
7022
7152
|
}
|
|
@@ -8263,6 +8393,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8263
8393
|
private func canReuseView(_ node: [String: Any], previous: RenderedNode) -> Bool {
|
|
8264
8394
|
let type = canonicalType(node["type"] as? String ?? "View")
|
|
8265
8395
|
guard previous.type == type else { return false }
|
|
8396
|
+
if type == "Switch" {
|
|
8397
|
+
let props = node["props"] as? [String: Any] ?? [:]
|
|
8398
|
+
return (previous.view is CraftNativeSolidSwitch) == (props["ios_solid"] as? Bool == true)
|
|
8399
|
+
}
|
|
8266
8400
|
if type == "Text" {
|
|
8267
8401
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
8268
8402
|
return (previous.view is CraftNativeSelectableTextView) == (props["selectable"] as? Bool == true)
|
|
@@ -8614,6 +8748,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8614
8748
|
pressable.pressBaseOpacity = number(style["opacity"]) ?? 1
|
|
8615
8749
|
pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
|
|
8616
8750
|
case "Switch":
|
|
8751
|
+
if let toggle = result as? CraftNativeSolidSwitch {
|
|
8752
|
+
if let value = props["value"] as? Bool ?? props["checked"] as? Bool { toggle.setOn(value) }
|
|
8753
|
+
toggle.isEnabled = props["disabled"] as? Bool != true
|
|
8754
|
+
let track = props["trackColor"] as? [String: Any]
|
|
8755
|
+
toggle.onColor = track == nil ? color(style["color"]) : color(track?["true"])
|
|
8756
|
+
toggle.offColor = color(props["ios_backgroundColor"]) ?? color(track?["false"])
|
|
8757
|
+
toggle.thumbColor = color(props["thumbColor"])
|
|
8758
|
+
updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: toggle)
|
|
8759
|
+
break
|
|
8760
|
+
}
|
|
8617
8761
|
let toggle = result as! UISwitch
|
|
8618
8762
|
// A supplied value controls the switch; without one, UIKit owns
|
|
8619
8763
|
// the state and a reconciliation must not erase a user's tap.
|
|
@@ -8928,6 +9072,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8928
9072
|
case "Modal":
|
|
8929
9073
|
return CraftNativeModalView()
|
|
8930
9074
|
case "Switch":
|
|
9075
|
+
if props["ios_solid"] as? Bool == true {
|
|
9076
|
+
let toggle = CraftNativeSolidSwitch()
|
|
9077
|
+
toggle.addTarget(self, action: #selector(solidSwitchChanged(_:)), for: .valueChanged)
|
|
9078
|
+
return toggle
|
|
9079
|
+
}
|
|
8931
9080
|
let toggle = UISwitch()
|
|
8932
9081
|
toggle.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
|
|
8933
9082
|
return toggle
|
|
@@ -10470,6 +10619,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10470
10619
|
// Assigning .button changes its legacy AX type into a button while
|
|
10471
10620
|
// modern XCTest still identifies its UIKit class as a switch.
|
|
10472
10621
|
if view is UITextView || view is UISwitch { return }
|
|
10622
|
+
if let toggle = view as? CraftNativeSolidSwitch { toggle.refreshAccessibility(); return }
|
|
10473
10623
|
view.accessibilityTraits = traits
|
|
10474
10624
|
}
|
|
10475
10625
|
|
|
@@ -10647,6 +10797,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10647
10797
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": sender.values[index], "index": index]])
|
|
10648
10798
|
}
|
|
10649
10799
|
|
|
10800
|
+
@objc private func solidSwitchChanged(_ sender: CraftNativeSolidSwitch) {
|
|
10801
|
+
guard sender.isEnabled, let handler = handlers[ObjectIdentifier(sender)] else { return }
|
|
10802
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": sender.isOn]])
|
|
10803
|
+
}
|
|
10804
|
+
|
|
10650
10805
|
@objc private func switchChanged(_ sender: UISwitch) {
|
|
10651
10806
|
let id = ObjectIdentifier(sender)
|
|
10652
10807
|
switchUserValues[id] = sender.isOn
|
|
@@ -280,12 +280,19 @@ final class CraftNativeSounds {
|
|
|
280
280
|
var volume: Float
|
|
281
281
|
var paused = false
|
|
282
282
|
var interrupted = false
|
|
283
|
+
var pending: ((Result<Any, CraftNativeActionError>) -> Void)?
|
|
283
284
|
|
|
284
285
|
init(player: AVQueuePlayer, volume: Float) {
|
|
285
286
|
self.player = player
|
|
286
287
|
self.volume = volume
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
func settle(_ result: Result<Any, CraftNativeActionError>) {
|
|
291
|
+
let answer = pending
|
|
292
|
+
pending = nil
|
|
293
|
+
answer?(result)
|
|
294
|
+
}
|
|
295
|
+
|
|
289
296
|
func end() {
|
|
290
297
|
fade?.invalidate()
|
|
291
298
|
fade = nil
|
|
@@ -357,33 +364,29 @@ final class CraftNativeSounds {
|
|
|
357
364
|
let item = AVPlayerItem(url: url)
|
|
358
365
|
let player = AVQueuePlayer()
|
|
359
366
|
let sound = Sound(player: player, volume: volume)
|
|
367
|
+
sound.pending = answer
|
|
360
368
|
sounds[id] = sound
|
|
361
|
-
var answered = false
|
|
362
|
-
let settle: (Result<Any, CraftNativeActionError>) -> Void = { result in
|
|
363
|
-
guard !answered else { return }
|
|
364
|
-
answered = true
|
|
365
|
-
answer(result)
|
|
366
|
-
}
|
|
367
369
|
let failed: (String) -> Void = { [weak self, weak sound] message in
|
|
368
370
|
guard let self, let sound, self.sounds[id] === sound else { return }
|
|
369
|
-
self.remove(id)
|
|
370
|
-
settle(.failure(CraftNativeActionError(code: "AUDIO_ERROR", message: message)))
|
|
371
|
+
self.remove(id, result: .failure(CraftNativeActionError(code: "AUDIO_ERROR", message: message)))
|
|
371
372
|
self.emit?(["id": id, "event": "error", "message": message])
|
|
372
373
|
}
|
|
373
374
|
if loop {
|
|
374
375
|
let looper = AVPlayerLooper(player: player, templateItem: item)
|
|
375
376
|
sound.looper = looper
|
|
376
|
-
sound.observation = looper.observe(\.status, options: [.initial, .new]) { looper, _ in
|
|
377
|
+
sound.observation = looper.observe(\.status, options: [.initial, .new]) { [weak self, weak sound] looper, _ in
|
|
377
378
|
DispatchQueue.main.async {
|
|
378
|
-
|
|
379
|
+
guard let self, let sound, self.sounds[id] === sound else { return }
|
|
380
|
+
if looper.status == .ready { sound.settle(.success(true)) }
|
|
379
381
|
else if looper.status == .failed { failed(looper.error?.localizedDescription ?? "The sound could not be played") }
|
|
380
382
|
}
|
|
381
383
|
}
|
|
382
384
|
} else {
|
|
383
385
|
player.insert(item, after: nil)
|
|
384
|
-
sound.observation = item.observe(\.status, options: [.initial, .new]) { item, _ in
|
|
386
|
+
sound.observation = item.observe(\.status, options: [.initial, .new]) { [weak self, weak sound] item, _ in
|
|
385
387
|
DispatchQueue.main.async {
|
|
386
|
-
|
|
388
|
+
guard let self, let sound, self.sounds[id] === sound else { return }
|
|
389
|
+
if item.status == .readyToPlay { sound.settle(.success(true)) }
|
|
387
390
|
else if item.status == .failed { failed(item.error?.localizedDescription ?? "The sound could not be played") }
|
|
388
391
|
}
|
|
389
392
|
}
|
|
@@ -451,10 +454,13 @@ final class CraftNativeSounds {
|
|
|
451
454
|
for id in Array(sounds.keys) { remove(id) }
|
|
452
455
|
}
|
|
453
456
|
|
|
454
|
-
private func remove(_ id: String) {
|
|
457
|
+
private func remove(_ id: String, result: Result<Any, CraftNativeActionError> = .failure(CraftNativeActionError(code: "CANCELLED", message: "Audio playback was stopped or replaced"))) {
|
|
455
458
|
guard let sound = sounds.removeValue(forKey: id) else { return }
|
|
456
459
|
sound.end()
|
|
457
460
|
Self.releaseSession()
|
|
461
|
+
// Settle after removing the old handle and releasing its resources so
|
|
462
|
+
// a completion can safely start another sound with the same id.
|
|
463
|
+
sound.settle(result)
|
|
458
464
|
}
|
|
459
465
|
|
|
460
466
|
/// Moves the volume in steps on the main run loop, a 30th of a second
|
|
@@ -6,7 +6,9 @@ protocol CraftVoiceRecorder: AnyObject {
|
|
|
6
6
|
func record(forDuration duration: TimeInterval) -> Bool
|
|
7
7
|
func stop()
|
|
8
8
|
}
|
|
9
|
+
#if CRAFT_PERMISSION_MICROPHONE
|
|
9
10
|
extension AVAudioRecorder: CraftVoiceRecorder {}
|
|
11
|
+
#endif
|
|
10
12
|
|
|
11
13
|
/// The main thread owns requests; audio and encoder work belong to one serial
|
|
12
14
|
/// queue. Cancellation invalidates a request immediately, even while AVFoundation
|
|
@@ -93,12 +95,22 @@ final class CraftVoiceRecording {
|
|
|
93
95
|
|
|
94
96
|
init(audioQueue: DispatchQueue = DispatchQueue(label: "craft.voice-recording", qos: .userInitiated),
|
|
95
97
|
startupTimeout: TimeInterval = 15,
|
|
96
|
-
permission: @escaping (@escaping (Bool) -> Void) -> Void = {
|
|
98
|
+
permission: @escaping (@escaping (Bool) -> Void) -> Void = { answer in
|
|
99
|
+
#if CRAFT_PERMISSION_MICROPHONE
|
|
100
|
+
AVAudioSession.sharedInstance().requestRecordPermission(answer)
|
|
101
|
+
#else
|
|
102
|
+
answer(false)
|
|
103
|
+
#endif
|
|
104
|
+
},
|
|
97
105
|
makeRecorder: @escaping (URL) throws -> CraftVoiceRecorder = { file in
|
|
106
|
+
#if CRAFT_PERMISSION_MICROPHONE
|
|
98
107
|
let session = AVAudioSession.sharedInstance()
|
|
99
108
|
try session.setCategory(.record, mode: .measurement)
|
|
100
109
|
try session.setActive(true)
|
|
101
110
|
return try AVAudioRecorder(url: file, settings: [AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 16000, AVNumberOfChannelsKey: 1, AVEncoderBitRateKey: 32000])
|
|
111
|
+
#else
|
|
112
|
+
throw NSError(domain: "CraftVoiceRecording", code: 1, userInfo: [NSLocalizedDescriptionKey: "Microphone was not enabled when this app was built"])
|
|
113
|
+
#endif
|
|
102
114
|
},
|
|
103
115
|
deactivate: @escaping () -> Void = { try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) }) {
|
|
104
116
|
Self.prepareForLaunch()
|