@shortkitsdk/react-native 0.2.28 → 0.2.29
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/android/libs/shortkit-release.aar +0 -0
- package/ios/ReactVideoCarouselOverlayHost.swift +6 -0
- package/ios/ShortKitBridge.swift +134 -35
- package/ios/ShortKitFeedView.swift +43 -44
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +696 -55
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +20 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +20 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +9 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +696 -55
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +20 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +20 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +696 -55
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +20 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +20 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +17 -17
- package/package.json +1 -1
- package/src/ShortKitFeed.tsx +31 -0
- package/src/specs/NativeShortKitModule.ts +6 -0
|
Binary file
|
|
@@ -324,7 +324,13 @@ import ShortKitSDK
|
|
|
324
324
|
} else {
|
|
325
325
|
carouselItemJSON = "{}"
|
|
326
326
|
}
|
|
327
|
+
// Tag with active surface's feedId so per-<ShortKitFeed>
|
|
328
|
+
// subscribers can filter to their own feed instance — same
|
|
329
|
+
// pattern as the other per-feed events. surfaceId continues
|
|
330
|
+
// to identify the React overlay host (a separate concern).
|
|
331
|
+
let feedId = self.bridge?.activeSurfaceFeedIdPublic() ?? ""
|
|
327
332
|
self.bridge?.emit("onCarouselActiveVideoCompleted", body: [
|
|
333
|
+
"feedId": feedId,
|
|
328
334
|
"surfaceId": self.surfaceId,
|
|
329
335
|
"contentItem": contentItemJSON,
|
|
330
336
|
"indexInCarousel": event.indexInCarousel,
|
package/ios/ShortKitBridge.swift
CHANGED
|
@@ -72,7 +72,6 @@ import ShortKitSDK
|
|
|
72
72
|
|
|
73
73
|
// Wire per-feed refresh state callback (scoped by feedId)
|
|
74
74
|
vc.onRefreshStateChanged = { [weak self] state in
|
|
75
|
-
NSLog("[ShortKit Bridge] onRefreshStateChangedPerFeed feedId=%@ status=%@", id, "\(state)")
|
|
76
75
|
var body: [String: Any] = ["feedId": id]
|
|
77
76
|
switch state {
|
|
78
77
|
case .idle:
|
|
@@ -101,6 +100,34 @@ import ShortKitSDK
|
|
|
101
100
|
])
|
|
102
101
|
}
|
|
103
102
|
|
|
103
|
+
// Wire per-feed transition event. The FVC fires this closure from
|
|
104
|
+
// handleSwipe(to:) — one per transition, bound to this feed. This
|
|
105
|
+
// replaces the global `player.feedTransition` subscription pattern
|
|
106
|
+
// (which fanned every feed's transitions out to every mounted
|
|
107
|
+
// <ShortKitFeed>, causing cross-feed state pollution). Structural
|
|
108
|
+
// fix: callback literally cannot fire on the wrong feed.
|
|
109
|
+
vc.onFeedTransition = { [weak self] event in
|
|
110
|
+
guard let self else { return }
|
|
111
|
+
var body: [String: Any] = [
|
|
112
|
+
"feedId": id,
|
|
113
|
+
"phase": Self.transitionPhaseString(event.phase),
|
|
114
|
+
"direction": Self.transitionDirectionString(event.direction)
|
|
115
|
+
]
|
|
116
|
+
// Serialize from the underlying FeedItem (not event.from/to
|
|
117
|
+
// which are nil for non-content cells). See the docstring on
|
|
118
|
+
// `serializeFeedItemIdentityJSON` for the why — in short, this
|
|
119
|
+
// ensures carousels/ads/surveys come through with a populated
|
|
120
|
+
// `id`, so JS-side hosts can track feed position for any cell
|
|
121
|
+
// type.
|
|
122
|
+
if let fromFeedItem = event.fromFeedItem {
|
|
123
|
+
body["fromItem"] = self.serializeFeedItemIdentityJSON(fromFeedItem)
|
|
124
|
+
}
|
|
125
|
+
if let toFeedItem = event.toFeedItem {
|
|
126
|
+
body["toItem"] = self.serializeFeedItemIdentityJSON(toFeedItem)
|
|
127
|
+
}
|
|
128
|
+
self.emitOnMain("onFeedTransition", body: body)
|
|
129
|
+
}
|
|
130
|
+
|
|
104
131
|
// Replay buffered operations on the next run-loop tick so the VC's
|
|
105
132
|
// view hierarchy is fully set up after didMoveToWindow returns.
|
|
106
133
|
if let ops = pendingOps.removeValue(forKey: id) {
|
|
@@ -122,6 +149,24 @@ import ShortKitSDK
|
|
|
122
149
|
return feedRegistry[id]?.vc
|
|
123
150
|
}
|
|
124
151
|
|
|
152
|
+
/// Look up the feedId for the currently-active surface. Used by events
|
|
153
|
+
/// that originate in shared singletons (player Combine publishers, the
|
|
154
|
+
/// `ShortKitDelegate`) and must be attributed to one feed for the JS
|
|
155
|
+
/// wrapper's feedId filter to work. Returns `""` if no active surface
|
|
156
|
+
/// or if the active surface isn't in the registry — consumers should
|
|
157
|
+
/// treat empty feedId as "unknown/global" (JS wrapper accepts it as a
|
|
158
|
+
/// fallback for forward compatibility with older native builds).
|
|
159
|
+
private func activeSurfaceFeedId() -> String {
|
|
160
|
+
return feedRegistry.first(where: { $0.value.vc?.isActiveSurface == true })?.key ?? ""
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Internal accessor for `activeSurfaceFeedId()` so cross-file emit
|
|
164
|
+
/// sites (e.g. `ReactVideoCarouselOverlayHost`) can tag emits with
|
|
165
|
+
/// the active feed's feedId without duplicating the lookup logic.
|
|
166
|
+
internal func activeSurfaceFeedIdPublic() -> String {
|
|
167
|
+
return activeSurfaceFeedId()
|
|
168
|
+
}
|
|
169
|
+
|
|
125
170
|
// MARK: - Init
|
|
126
171
|
|
|
127
172
|
@objc public init(
|
|
@@ -430,29 +475,21 @@ import ShortKitSDK
|
|
|
430
475
|
let config = Self.parseFeedConfig(configJSON)
|
|
431
476
|
let preload: FeedPreload?
|
|
432
477
|
|
|
433
|
-
NSLog("[ShortKit Bridge] preloadFeed called — feedSource: %@, hasItemsJSON: %@",
|
|
434
|
-
config.feedSource == .custom ? "custom" : "algorithmic",
|
|
435
|
-
itemsJSON != nil ? "yes (\(itemsJSON!.prefix(100))...)" : "no")
|
|
436
|
-
|
|
437
478
|
if config.feedSource == .custom {
|
|
438
479
|
guard let json = itemsJSON, let items = Self.parseFeedInputs(json) else {
|
|
439
|
-
NSLog("[ShortKit Bridge] ❌ preloadFeed: feedSource=custom but no valid items — returning empty")
|
|
440
480
|
completion("")
|
|
441
481
|
return
|
|
442
482
|
}
|
|
443
|
-
NSLog("[ShortKit Bridge] preloadFeed: creating custom preload with %d items", items.count)
|
|
444
483
|
preload = shortKit?.preloadFeed(items: items)
|
|
445
484
|
} else {
|
|
446
485
|
preload = shortKit?.preloadFeed(filter: config.filter)
|
|
447
486
|
}
|
|
448
487
|
|
|
449
488
|
guard let preload else {
|
|
450
|
-
NSLog("[ShortKit Bridge] ❌ preloadFeed: shortKit?.preloadFeed returned nil")
|
|
451
489
|
completion("")
|
|
452
490
|
return
|
|
453
491
|
}
|
|
454
492
|
let uuid = UUID().uuidString
|
|
455
|
-
NSLog("[ShortKit Bridge] ✅ preloadFeed: created handle %@", uuid)
|
|
456
493
|
preloadHandles[uuid] = preload
|
|
457
494
|
activeFeedId = uuid
|
|
458
495
|
completion(uuid)
|
|
@@ -553,38 +590,34 @@ import ShortKitSDK
|
|
|
553
590
|
}
|
|
554
591
|
.store(in: &cancellables)
|
|
555
592
|
|
|
556
|
-
// Did loop
|
|
593
|
+
// Did loop — tagged with active surface's feedId. `player.didLoop`
|
|
594
|
+
// is a singleton Combine publisher on the shared player (only one
|
|
595
|
+
// item plays at a time). The event semantically belongs to
|
|
596
|
+
// whichever feed owns the active surface; tag with that feedId so
|
|
597
|
+
// JS consumers bound to specific feeds can filter.
|
|
557
598
|
player.didLoop
|
|
558
599
|
.receive(on: DispatchQueue.main)
|
|
559
600
|
.sink { [weak self] event in
|
|
560
|
-
self
|
|
601
|
+
guard let self else { return }
|
|
602
|
+
let feedId = self.activeSurfaceFeedId()
|
|
603
|
+
self.emit("onDidLoop", body: [
|
|
604
|
+
"feedId": feedId,
|
|
561
605
|
"contentId": event.contentId,
|
|
562
606
|
"loopCount": event.loopCount
|
|
563
607
|
])
|
|
564
608
|
}
|
|
565
609
|
.store(in: &cancellables)
|
|
566
610
|
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if let from = event.from {
|
|
578
|
-
body["fromItem"] = self.serializeContentItemToJSON(from)
|
|
579
|
-
}
|
|
580
|
-
if let to = event.to {
|
|
581
|
-
body["toItem"] = self.serializeContentItemToJSON(to)
|
|
582
|
-
}
|
|
583
|
-
DispatchQueue.main.async { [weak self] in
|
|
584
|
-
self?.emit("onFeedTransition", body: body)
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
.store(in: &cancellables)
|
|
611
|
+
// NOTE: The global `player.feedTransition` subscription used to live
|
|
612
|
+
// here. It's been removed — feed transitions are now emitted to RN
|
|
613
|
+
// via the per-FVC `vc.onFeedTransition` closure wired in
|
|
614
|
+
// `registerFeed(id:viewController:)` above. This eliminates the
|
|
615
|
+
// cross-feed routing bug where every mounted <ShortKitFeed>
|
|
616
|
+
// consumer received every feed's transitions.
|
|
617
|
+
//
|
|
618
|
+
// `player.sendFeedTransition` is still called by FVC.handleSwipe
|
|
619
|
+
// for backward compatibility with native iOS consumers that
|
|
620
|
+
// subscribe to `ShortKit.player.feedTransition` directly.
|
|
588
621
|
|
|
589
622
|
// Feed scroll phase — coalesced: only emit .dragging on first touch
|
|
590
623
|
// (transition from settled), drop intermediate .dragging events. Always
|
|
@@ -613,11 +646,16 @@ import ShortKitSDK
|
|
|
613
646
|
}
|
|
614
647
|
.store(in: &cancellables)
|
|
615
648
|
|
|
616
|
-
// Format change
|
|
649
|
+
// Format change — tagged with active surface's feedId. Same
|
|
650
|
+
// reasoning as onDidLoop: fires from shared player singleton, but
|
|
651
|
+
// semantically belongs to whichever feed owns the active surface.
|
|
617
652
|
player.formatChange
|
|
618
653
|
.receive(on: DispatchQueue.main)
|
|
619
654
|
.sink { [weak self] event in
|
|
620
|
-
self
|
|
655
|
+
guard let self else { return }
|
|
656
|
+
let feedId = self.activeSurfaceFeedId()
|
|
657
|
+
self.emit("onFormatChange", body: [
|
|
658
|
+
"feedId": feedId,
|
|
621
659
|
"contentId": event.contentId,
|
|
622
660
|
"fromBitrate": Double(event.fromBitrate),
|
|
623
661
|
"toBitrate": Double(event.toBitrate),
|
|
@@ -683,6 +721,52 @@ import ShortKitSDK
|
|
|
683
721
|
return json
|
|
684
722
|
}
|
|
685
723
|
|
|
724
|
+
/// Serialize a `FeedItem` to a ContentItem-shaped JSON string for the
|
|
725
|
+
/// `onFeedTransition` event. For `.content` cells this is the real
|
|
726
|
+
/// `ContentItem`. For every other cell kind (adSlot, imageCarousel,
|
|
727
|
+
/// survey, videoCarousel) there is no playable `ContentItem`, so we
|
|
728
|
+
/// synthesize a minimal one whose ONLY meaningful field is `id`.
|
|
729
|
+
///
|
|
730
|
+
/// Why: JS hosts use `event.to` as an identity cursor for resume-on-
|
|
731
|
+
/// tab-return (`setFeedItems(startAt:)`). Before this synthesis, non-
|
|
732
|
+
/// content cells came through as `to=null` and the host's stored
|
|
733
|
+
/// resume id stayed pinned to the last regular video — causing the
|
|
734
|
+
/// SDK to re-seed the feed at the wrong index on tab return, which
|
|
735
|
+
/// in turn caused black carousel cells + wrong audio (see repro
|
|
736
|
+
/// analysis in debug.log, apr 2026).
|
|
737
|
+
///
|
|
738
|
+
/// Scope: this is a bridge-only synthesis. It never flows back into
|
|
739
|
+
/// the Swift SDK (pool, cache, FeedDataSource, etc. all still see the
|
|
740
|
+
/// real `FeedItem` / `ContentItem?` and treat non-content cells
|
|
741
|
+
/// honestly). JS-side consumers should ONLY read `.id` / `.playbackId`
|
|
742
|
+
/// from `onFeedTransition` items — deeper fields are placeholders for
|
|
743
|
+
/// non-content cells.
|
|
744
|
+
///
|
|
745
|
+
/// TODO: when we want to expose richer per-cell metadata on the JS
|
|
746
|
+
/// side, migrate to a proper FeedPosition event payload rather than
|
|
747
|
+
/// extending this synthesis. See the long-term fix plan.
|
|
748
|
+
private func serializeFeedItemIdentityJSON(_ feedItem: FeedItem) -> String {
|
|
749
|
+
if let contentItem = feedItem.contentItem {
|
|
750
|
+
return serializeContentItemToJSON(contentItem)
|
|
751
|
+
}
|
|
752
|
+
let synthetic = ContentItem(
|
|
753
|
+
id: feedItem.id,
|
|
754
|
+
playbackId: nil,
|
|
755
|
+
title: "",
|
|
756
|
+
description: nil,
|
|
757
|
+
duration: 0,
|
|
758
|
+
streamingUrl: "",
|
|
759
|
+
thumbnailUrl: "",
|
|
760
|
+
captionTracks: [],
|
|
761
|
+
customMetadata: nil,
|
|
762
|
+
author: nil,
|
|
763
|
+
articleUrl: nil,
|
|
764
|
+
commentCount: nil,
|
|
765
|
+
fallbackUrl: nil
|
|
766
|
+
)
|
|
767
|
+
return serializeContentItemToJSON(synthetic)
|
|
768
|
+
}
|
|
769
|
+
|
|
686
770
|
/// Build an NSDictionary from a ContentItem with fields matching the JS spec.
|
|
687
771
|
/// `captionTracks` and `customMetadata` are JSON-serialized strings.
|
|
688
772
|
private static func contentItemDict(_ item: ContentItem) -> [String: Any] {
|
|
@@ -1007,7 +1091,14 @@ import ShortKitSDK
|
|
|
1007
1091
|
|
|
1008
1092
|
extension ShortKitBridge: ShortKitDelegate {
|
|
1009
1093
|
public func shortKit(_ shortKit: ShortKit, didTapContent contentId: String, at index: Int) {
|
|
1094
|
+
// Tag with active surface's feedId. The delegate is a singleton
|
|
1095
|
+
// (the bridge), so we use the activeSurface lookup to attribute
|
|
1096
|
+
// this tap to the feed it originated from. Without the tag, every
|
|
1097
|
+
// mounted <ShortKitFeed> consumer would fire onContentTapped on
|
|
1098
|
+
// every tap — including taps in sibling feeds.
|
|
1099
|
+
let feedId = activeSurfaceFeedId()
|
|
1010
1100
|
emitOnMain("onContentTapped", body: [
|
|
1101
|
+
"feedId": feedId,
|
|
1011
1102
|
"contentId": contentId,
|
|
1012
1103
|
"index": index
|
|
1013
1104
|
])
|
|
@@ -1038,10 +1129,18 @@ extension ShortKitBridge: ShortKitDelegate {
|
|
|
1038
1129
|
self.itemCache[item.id] = item
|
|
1039
1130
|
}
|
|
1040
1131
|
}
|
|
1132
|
+
// Capture the feedId synchronously before the async Task — after
|
|
1133
|
+
// the await, activeSurface may have shifted (user swiped tabs).
|
|
1134
|
+
// The fetch semantically belongs to whichever feed initiated it,
|
|
1135
|
+
// which at this delegate-call moment is the active surface.
|
|
1136
|
+
let feedId = activeSurfaceFeedId()
|
|
1041
1137
|
Task {
|
|
1042
1138
|
let data = try? JSONEncoder().encode(items)
|
|
1043
1139
|
let json = data.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
|
|
1044
|
-
self.emitOnMain("onDidFetchContentItems", body: [
|
|
1140
|
+
self.emitOnMain("onDidFetchContentItems", body: [
|
|
1141
|
+
"feedId": feedId,
|
|
1142
|
+
"items": json
|
|
1143
|
+
])
|
|
1045
1144
|
}
|
|
1046
1145
|
}
|
|
1047
1146
|
}
|
|
@@ -38,14 +38,19 @@ import ShortKitSDK
|
|
|
38
38
|
didSet { /* used at embed time only */ }
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
///
|
|
42
|
-
///
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
/// The `active` prop is authoritative: whenever the view is deciding
|
|
42
|
+
/// whether to activate/deactivate/suspend its FeedViewController, it
|
|
43
|
+
/// consults this value directly. The `didSet` only exists to react to
|
|
44
|
+
/// transitions while the VC already exists. Transitions that arrive
|
|
45
|
+
/// before the VC is created (a common Fabric ordering: props set on the
|
|
46
|
+
/// view instance before `didMoveToWindow` runs) are stored in the Bool
|
|
47
|
+
/// property by Swift automatically — `embedFeedViewControllerIfNeeded`
|
|
48
|
+
/// reads the current value and acts on it when the VC is finally set up.
|
|
45
49
|
@objc public var active: Bool = true {
|
|
46
50
|
didSet {
|
|
47
|
-
guard active != oldValue, let feedVC = feedViewController else {
|
|
48
|
-
|
|
51
|
+
guard active != oldValue, let feedVC = feedViewController else {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
49
54
|
if active {
|
|
50
55
|
feedVC.activate()
|
|
51
56
|
} else {
|
|
@@ -62,9 +67,16 @@ import ShortKitSDK
|
|
|
62
67
|
|
|
63
68
|
// MARK: - Lifecycle
|
|
64
69
|
|
|
70
|
+
public override init(frame: CGRect) {
|
|
71
|
+
super.init(frame: frame)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public required init?(coder: NSCoder) {
|
|
75
|
+
super.init(coder: coder)
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
public override func didMoveToWindow() {
|
|
66
79
|
super.didMoveToWindow()
|
|
67
|
-
|
|
68
80
|
if window != nil {
|
|
69
81
|
embedFeedViewControllerIfNeeded()
|
|
70
82
|
}
|
|
@@ -72,8 +84,11 @@ import ShortKitSDK
|
|
|
72
84
|
|
|
73
85
|
public override func willMove(toWindow newWindow: UIWindow?) {
|
|
74
86
|
super.willMove(toWindow: newWindow)
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
// Suspend only if we're leaving the window AND the prop says this
|
|
88
|
+
// surface should be active. When the prop is false, the consumer has
|
|
89
|
+
// already driven the VC into deactivate via active.didSet (or will,
|
|
90
|
+
// once the VC exists), and we mustn't double-suspend.
|
|
91
|
+
if newWindow == nil && active {
|
|
77
92
|
suspendFeedViewController()
|
|
78
93
|
}
|
|
79
94
|
}
|
|
@@ -96,18 +111,21 @@ import ShortKitSDK
|
|
|
96
111
|
|
|
97
112
|
// Re-attach an existing suspended VC (e.g. after native stack pop)
|
|
98
113
|
if let feedVC = feedViewController {
|
|
99
|
-
NSLog("[ShortKit FeedView] re-attaching suspended VC feedId=%@", feedId ?? "nil")
|
|
100
114
|
parentVC.addChild(feedVC)
|
|
101
115
|
feedVC.view.frame = bounds
|
|
102
116
|
addSubview(feedVC.view)
|
|
103
117
|
feedVC.didMove(toParent: parentVC)
|
|
104
|
-
|
|
118
|
+
// Authoritative: read the current active prop and act on it.
|
|
119
|
+
// If prop is false, stay deactivated — do NOT claim the pool.
|
|
120
|
+
// This is the fix for the prop-arrived-before-VC race: a prop
|
|
121
|
+
// update dropped by the didSet guard (vcExists=false) lands here
|
|
122
|
+
// and is honored.
|
|
123
|
+
if active {
|
|
105
124
|
feedVC.activate()
|
|
106
125
|
}
|
|
107
126
|
if let feedId = self.feedId {
|
|
108
127
|
ShortKitBridge.shared?.registerFeed(id: feedId, viewController: feedVC)
|
|
109
128
|
}
|
|
110
|
-
NSLog("[ShortKit FeedView] re-attach complete — activate() called")
|
|
111
129
|
return
|
|
112
130
|
}
|
|
113
131
|
|
|
@@ -120,33 +138,22 @@ import ShortKitSDK
|
|
|
120
138
|
|
|
121
139
|
// Consume preload handle if available
|
|
122
140
|
if let preloadId = self.preloadId {
|
|
123
|
-
NSLog("[ShortKit FeedView] preloadId prop: %@", preloadId)
|
|
124
141
|
if let preload = ShortKitBridge.shared?.consumePreload(id: preloadId) {
|
|
125
142
|
feedConfig.preload = preload
|
|
126
|
-
NSLog("[ShortKit FeedView] ✅ Preload handle consumed for feedId: %@", preloadId)
|
|
127
|
-
} else {
|
|
128
|
-
NSLog("[ShortKit FeedView] ❌ No preload handle found for feedId: %@", preloadId)
|
|
129
|
-
NSLog("[ShortKit FeedView] Available preload handles: %@", ShortKitBridge.shared?.preloadHandles.keys.joined(separator: ", ") ?? "none")
|
|
130
143
|
}
|
|
131
144
|
} else if let json = self.feedItemsJSON,
|
|
132
145
|
let items = ShortKitBridge.parseFeedInputs(json) {
|
|
133
146
|
// feedItems prop: wrap in an immediate preload (no async prefetch
|
|
134
147
|
// work — items are available synchronously at viewDidLoad).
|
|
135
148
|
feedConfig.preload = FeedPreload(immediateItems: items)
|
|
136
|
-
NSLog("[ShortKit FeedView] feedItems prop: created immediate preload with %d items", items.count)
|
|
137
|
-
} else {
|
|
138
|
-
NSLog("[ShortKit FeedView] No preloadId or feedItems prop set")
|
|
139
149
|
}
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let feedVC = ShortKitFeedViewController(shortKit: sdk, config: feedConfig, startAtItemId: startAtItemId)
|
|
149
|
-
feedVC.setBridgeManaged()
|
|
151
|
+
let feedVC = ShortKitFeedViewController(
|
|
152
|
+
shortKit: sdk,
|
|
153
|
+
config: feedConfig,
|
|
154
|
+
startAtItemId: startAtItemId,
|
|
155
|
+
lifecycle: .manual
|
|
156
|
+
)
|
|
150
157
|
|
|
151
158
|
// Seed a thumbnail from the host app's image cache so the first cell
|
|
152
159
|
// renders with a visible thumbnail from frame zero. Synchronous
|
|
@@ -174,8 +181,6 @@ import ShortKitSDK
|
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
|
|
177
|
-
NSLog("[ShortKit FeedView] VC created successfully (bridge-managed)")
|
|
178
|
-
|
|
179
184
|
feedVC.onDismiss = {
|
|
180
185
|
ShortKitBridge.shared?.emitDismiss()
|
|
181
186
|
}
|
|
@@ -192,13 +197,12 @@ import ShortKitSDK
|
|
|
192
197
|
feedVC.didMove(toParent: parentVC)
|
|
193
198
|
|
|
194
199
|
// With FVC.viewDidAppear no longer self-claiming for bridge-managed
|
|
195
|
-
// surfaces, the bridge is the sole authority on claim timing.
|
|
196
|
-
// the
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// prop's didSet.
|
|
200
|
+
// surfaces, the bridge is the sole authority on claim timing. Read
|
|
201
|
+
// the authoritative `active` prop and act on it. If the prop is
|
|
202
|
+
// false (including the case where it was set to false before the VC
|
|
203
|
+
// existed and the didSet guard returned early), the FVC stays idle —
|
|
204
|
+
// no claim, no pool mutation, no hijack. A later prop change to
|
|
205
|
+
// active=true triggers `feedVC.activate()` via the prop's didSet.
|
|
202
206
|
if active {
|
|
203
207
|
feedVC.activate()
|
|
204
208
|
}
|
|
@@ -209,20 +213,15 @@ import ShortKitSDK
|
|
|
209
213
|
/// (e.g. pushing a new screen on top). The feedVC and its state are preserved
|
|
210
214
|
/// so they can be re-attached when the view returns to the window.
|
|
211
215
|
private func suspendFeedViewController() {
|
|
212
|
-
NSLog("[ShortKit FeedView] suspendFeedViewController ENTRY feedId=%@", feedId ?? "nil")
|
|
213
216
|
if let feedId = self.feedId {
|
|
214
217
|
ShortKitBridge.shared?.unregisterFeed(id: feedId)
|
|
215
218
|
}
|
|
216
|
-
guard let feedVC = feedViewController else {
|
|
217
|
-
NSLog("[ShortKit FeedView] suspendFeedViewController NOOP — no feedVC")
|
|
218
|
-
return
|
|
219
|
-
}
|
|
219
|
+
guard let feedVC = feedViewController else { return }
|
|
220
220
|
|
|
221
221
|
feedVC.deactivate()
|
|
222
222
|
feedVC.willMove(toParent: nil)
|
|
223
223
|
feedVC.view.removeFromSuperview()
|
|
224
224
|
feedVC.removeFromParent()
|
|
225
|
-
NSLog("[ShortKit FeedView] suspendFeedViewController EXIT — VC retained for re-attach")
|
|
226
225
|
// Keep feedViewController reference — re-attached in embedFeedViewControllerIfNeeded
|
|
227
226
|
}
|
|
228
227
|
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
<key>CFBundlePackageType</key>
|
|
12
12
|
<string>FMWK</string>
|
|
13
13
|
<key>CFBundleVersion</key>
|
|
14
|
-
<string>0.2.
|
|
14
|
+
<string>0.2.29</string>
|
|
15
15
|
<key>CFBundleShortVersionString</key>
|
|
16
|
-
<string>0.2.
|
|
16
|
+
<string>0.2.29</string>
|
|
17
17
|
<key>MinimumOSVersion</key>
|
|
18
18
|
<string>16.0</string>
|
|
19
19
|
</dict>
|