@craft-native/ios 0.0.224 → 0.0.226
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/package.json
CHANGED
|
@@ -266,6 +266,8 @@ protocol CraftHybridScreenEvents: AnyObject {
|
|
|
266
266
|
func nativeScreen(_ screen: CraftNativeScreenController, didFail message: String)
|
|
267
267
|
/// `craft.navigation.open(path)`.
|
|
268
268
|
func nativeScreen(_ screen: CraftNativeScreenController, open path: String)
|
|
269
|
+
/// `craft.navigation.replace(path)`: finish the current flow atomically.
|
|
270
|
+
func nativeScreen(_ screen: CraftNativeScreenController, replace path: String)
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
// MARK: - The shell
|
|
@@ -938,6 +940,10 @@ extension CraftHybrid: CraftHybridScreenEvents {
|
|
|
938
940
|
func nativeScreen(_ screen: CraftNativeScreenController, open path: String) {
|
|
939
941
|
open(path)
|
|
940
942
|
}
|
|
943
|
+
|
|
944
|
+
func nativeScreen(_ screen: CraftNativeScreenController, replace path: String) {
|
|
945
|
+
controller?.showNative(path: path, replace: true)
|
|
946
|
+
}
|
|
941
947
|
}
|
|
942
948
|
|
|
943
949
|
/// The page's `craftHybrid` channel, only from a trusted origin's main frame.
|
|
@@ -1519,6 +1525,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1519
1525
|
interruptTabSwipe(restoringSelection: true)
|
|
1520
1526
|
let tabs = hybrid.tabIds
|
|
1521
1527
|
if let tab = tabs.first(where: { CraftHybridRoutes.normalize($0) == match.path }) {
|
|
1528
|
+
// Finish the flow in its original tab before selecting another.
|
|
1529
|
+
if replace, let current = visibleStack, current.viewControllers.count > 1 {
|
|
1530
|
+
current.setViewControllers(Array(current.viewControllers.dropLast()), animated: false)
|
|
1531
|
+
}
|
|
1522
1532
|
if tab != currentTab {
|
|
1523
1533
|
selectTabAsIfTapped(tab)
|
|
1524
1534
|
}
|
|
@@ -1541,7 +1551,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1541
1551
|
return
|
|
1542
1552
|
}
|
|
1543
1553
|
let screen = entry(for: raw, isTabRoot: false, depth: nil)
|
|
1544
|
-
if replace, nav.viewControllers.
|
|
1554
|
+
if replace, !nav.viewControllers.isEmpty {
|
|
1545
1555
|
nav.setViewControllers(Array(nav.viewControllers.dropLast()) + [screen], animated: true)
|
|
1546
1556
|
} else {
|
|
1547
1557
|
nav.pushViewController(screen, animated: true)
|
|
@@ -1900,6 +1910,11 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1900
1910
|
open(path)
|
|
1901
1911
|
}
|
|
1902
1912
|
|
|
1913
|
+
func nativeScreen(_ screen: CraftNativeScreenController, replace path: String) {
|
|
1914
|
+
guard screen.navigationController?.topViewController === screen else { return }
|
|
1915
|
+
showNative(path: path, replace: true)
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1903
1918
|
private func log(_ message: String) {
|
|
1904
1919
|
#if DEBUG
|
|
1905
1920
|
NSLog("[craft hybrid] %@", message)
|
|
@@ -254,6 +254,12 @@ enum CraftNativeActions {
|
|
|
254
254
|
return
|
|
255
255
|
case ("Media", "usage"):
|
|
256
256
|
sync = .success(CraftNativeMedia.usage())
|
|
257
|
+
case ("Media", "clear"):
|
|
258
|
+
DispatchQueue.global(qos: .utility).async {
|
|
259
|
+
do { complete(requestToken, .success(try CraftNativeMedia.clear()), completion) }
|
|
260
|
+
catch { complete(requestToken, failure("IO_ERROR", "The video cache could not be cleared"), completion) }
|
|
261
|
+
}
|
|
262
|
+
return
|
|
257
263
|
case ("Lifecycle", "getState"):
|
|
258
264
|
sync = .success(currentAppState())
|
|
259
265
|
case ("DeepLinks", "getInitialURL"):
|
|
@@ -782,7 +788,7 @@ enum CraftNativeActions {
|
|
|
782
788
|
request.allowsExpensiveNetworkAccess = false
|
|
783
789
|
request.allowsConstrainedNetworkAccess = false
|
|
784
790
|
}
|
|
785
|
-
|
|
791
|
+
guard let flight = CraftNativeMedia.begin(address, { complete(requestToken, $0, completion) }) else { return }
|
|
786
792
|
let task = fetchSession.downloadTask(with: request) { location, response, error in
|
|
787
793
|
cancellationLock.lock()
|
|
788
794
|
fetchTasks.removeValue(forKey: requestToken)
|
|
@@ -790,25 +796,25 @@ enum CraftNativeActions {
|
|
|
790
796
|
if let error = error {
|
|
791
797
|
// Refused only because the connection is metered: not a failure, a wait.
|
|
792
798
|
if let reason = (error as? URLError)?.networkUnavailableReason, reason == .expensive || reason == .constrained {
|
|
793
|
-
CraftNativeMedia.finish(address, .success(["ok": false, "url": address, "reason": "cellular"]))
|
|
799
|
+
CraftNativeMedia.finish(address, flight: flight, .success(["ok": false, "url": address, "reason": "cellular"]))
|
|
794
800
|
return
|
|
795
801
|
}
|
|
796
802
|
let code = (error as? URLError)?.code == .timedOut ? "TIMEOUT" : "NETWORK_ERROR"
|
|
797
|
-
CraftNativeMedia.finish(address, failure(CraftNativeMedia.errorCode(error, fallback: code), error.localizedDescription))
|
|
803
|
+
CraftNativeMedia.finish(address, flight: flight, failure(CraftNativeMedia.errorCode(error, fallback: code), error.localizedDescription))
|
|
798
804
|
return
|
|
799
805
|
}
|
|
800
806
|
guard let http = response as? HTTPURLResponse, let location else {
|
|
801
|
-
CraftNativeMedia.finish(address, failure("NETWORK_ERROR", "media.keep received a response that is not HTTP"))
|
|
807
|
+
CraftNativeMedia.finish(address, flight: flight, failure("NETWORK_ERROR", "media.keep received a response that is not HTTP"))
|
|
802
808
|
return
|
|
803
809
|
}
|
|
804
810
|
// The system deletes `location` once this returns: move it now.
|
|
805
|
-
CraftNativeMedia.finish(address, CraftNativeMedia.store(address, at: location, response: http))
|
|
811
|
+
CraftNativeMedia.finish(address, flight: flight, CraftNativeMedia.store(address, at: location, response: http, flight: flight))
|
|
806
812
|
}
|
|
807
813
|
cancellationLock.lock()
|
|
808
814
|
let alreadyCancelled = cancelledRequests.contains(requestToken)
|
|
809
815
|
if !alreadyCancelled { fetchTasks[requestToken] = task }
|
|
810
816
|
cancellationLock.unlock()
|
|
811
|
-
if alreadyCancelled { CraftNativeMedia.finish(address, failure("CANCELLED", "media.keep was cancelled")) }
|
|
817
|
+
if alreadyCancelled { CraftNativeMedia.finish(address, flight: flight, failure("CANCELLED", "media.keep was cancelled")) }
|
|
812
818
|
else { task.resume() }
|
|
813
819
|
}
|
|
814
820
|
|
|
@@ -514,7 +514,11 @@ enum CraftNativeMedia {
|
|
|
514
514
|
|
|
515
515
|
private static let lock = NSLock()
|
|
516
516
|
/// Addresses being fetched now, so a second `keep` waits for the first.
|
|
517
|
-
private
|
|
517
|
+
private struct Flight {
|
|
518
|
+
let id: UUID
|
|
519
|
+
var waiting: [(Result<Any, CraftNativeActionError>) -> Void]
|
|
520
|
+
}
|
|
521
|
+
private static var inFlight: [String: Flight] = [:]
|
|
518
522
|
|
|
519
523
|
/// Where `address`'s copy lives (whether or not it is there yet). The
|
|
520
524
|
/// extension is the address's, because AVPlayer reads the type from it.
|
|
@@ -537,28 +541,29 @@ enum CraftNativeMedia {
|
|
|
537
541
|
return ["ok": true, "url": address, "path": url.path, "size": size]
|
|
538
542
|
}
|
|
539
543
|
|
|
540
|
-
///
|
|
541
|
-
static func
|
|
544
|
+
/// A new download's identity, or nil after joining an existing download.
|
|
545
|
+
static func begin(_ address: String, _ done: @escaping (Result<Any, CraftNativeActionError>) -> Void) -> UUID? {
|
|
542
546
|
lock.lock()
|
|
543
547
|
defer { lock.unlock() }
|
|
544
548
|
if inFlight[address] != nil {
|
|
545
|
-
inFlight[address]!.append(done)
|
|
546
|
-
return
|
|
549
|
+
inFlight[address]!.waiting.append(done)
|
|
550
|
+
return nil
|
|
547
551
|
}
|
|
548
|
-
|
|
549
|
-
|
|
552
|
+
let id = UUID()
|
|
553
|
+
inFlight[address] = Flight(id: id, waiting: [done])
|
|
554
|
+
return id
|
|
550
555
|
}
|
|
551
556
|
|
|
552
|
-
static func finish(_ address: String, _ result: Result<Any, CraftNativeActionError>) {
|
|
557
|
+
static func finish(_ address: String, flight: UUID, _ result: Result<Any, CraftNativeActionError>) {
|
|
553
558
|
lock.lock()
|
|
554
|
-
let waiting = inFlight.removeValue(forKey: address) ?? []
|
|
559
|
+
let waiting = inFlight[address]?.id == flight ? inFlight.removeValue(forKey: address)?.waiting ?? [] : []
|
|
555
560
|
lock.unlock()
|
|
556
561
|
for done in waiting { done(result) }
|
|
557
562
|
}
|
|
558
563
|
|
|
559
564
|
/// A finished download moved into place (atomically, so a half-written
|
|
560
565
|
/// file is never played), or a refusal with the server's status.
|
|
561
|
-
static func store(_ address: String, at location: URL, response: HTTPURLResponse) -> Result<Any, CraftNativeActionError> {
|
|
566
|
+
static func store(_ address: String, at location: URL, response: HTTPURLResponse, flight: UUID? = nil) -> Result<Any, CraftNativeActionError> {
|
|
562
567
|
guard response.statusCode == 200 else {
|
|
563
568
|
return .success(["ok": false, "url": address, "status": response.statusCode])
|
|
564
569
|
}
|
|
@@ -566,6 +571,9 @@ enum CraftNativeMedia {
|
|
|
566
571
|
// Commit and prune share a lock; neither can remove a file being committed.
|
|
567
572
|
lock.lock()
|
|
568
573
|
defer { lock.unlock() }
|
|
574
|
+
if let flight, inFlight[address]?.id != flight {
|
|
575
|
+
return .failure(CraftNativeActionError(code: "CANCELLED", message: "The video cache was cleared"))
|
|
576
|
+
}
|
|
569
577
|
let staging = root.appendingPathComponent(UUID().uuidString + ".part")
|
|
570
578
|
defer { try? fm.removeItem(at: staging) }
|
|
571
579
|
do {
|
|
@@ -596,6 +604,25 @@ enum CraftNativeMedia {
|
|
|
596
604
|
}
|
|
597
605
|
}
|
|
598
606
|
|
|
607
|
+
/// End an account's cache lifetime.
|
|
608
|
+
static func clear() throws -> Int {
|
|
609
|
+
lock.lock()
|
|
610
|
+
let waiting = inFlight.values.flatMap { $0.waiting }
|
|
611
|
+
inFlight.removeAll()
|
|
612
|
+
let fm = FileManager.default
|
|
613
|
+
let files = (try? fm.contentsOfDirectory(at: root, includingPropertiesForKeys: nil)) ?? []
|
|
614
|
+
var removed = 0
|
|
615
|
+
var failure: Error?
|
|
616
|
+
for url in files {
|
|
617
|
+
do { try fm.removeItem(at: url); removed += 1 }
|
|
618
|
+
catch { failure = error }
|
|
619
|
+
}
|
|
620
|
+
lock.unlock()
|
|
621
|
+
for done in waiting { done(.failure(CraftNativeActionError(code: "CANCELLED", message: "The video cache was cleared"))) }
|
|
622
|
+
if let failure { throw failure }
|
|
623
|
+
return removed
|
|
624
|
+
}
|
|
625
|
+
|
|
599
626
|
/// Removes every copy whose address is not in `keep`; answers how many.
|
|
600
627
|
static func prune(keep: [String]) -> Int {
|
|
601
628
|
lock.lock()
|
|
@@ -5416,6 +5416,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5416
5416
|
guard !isAppScript, navigationController?.topViewController === self,
|
|
5417
5417
|
let screen = payload["screen"] as? String, !screen.isEmpty,
|
|
5418
5418
|
let navigation = navigationController else { return }
|
|
5419
|
+
if type == "NAVIGATE_REPLACE", screen.hasPrefix("/") {
|
|
5420
|
+
hybridEvents?.nativeScreen(self, replace: screen)
|
|
5421
|
+
return
|
|
5422
|
+
}
|
|
5419
5423
|
let params = payload["params"] as? [String: Any] ?? [:]
|
|
5420
5424
|
let next = CraftNativeScreenController(config: config, routeName: screen, routeParams: params)
|
|
5421
5425
|
next.hybridEvents = hybridEvents
|
|
@@ -6067,7 +6071,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6067
6071
|
if let current = image.image { image.image = imageForDisplay(current, view: image) }
|
|
6068
6072
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
6069
6073
|
image.contentMode = imageContentMode(requestedResizeMode)
|
|
6070
|
-
image.clipsToBounds = image.contentMode == .scaleAspectFill
|
|
6074
|
+
image.clipsToBounds = image.clipsToBounds || image.contentMode == .scaleAspectFill
|
|
6071
6075
|
updateAuxiliaryHandler(events["onLoadStart"], in: &imageLoadStartHandlers, for: image)
|
|
6072
6076
|
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: image)
|
|
6073
6077
|
updateAuxiliaryHandler(events["onLoadEnd"], in: &imageLoadEndHandlers, for: image)
|
|
@@ -6086,7 +6090,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6086
6090
|
if let current = background.image { background.image = imageForDisplay(current, view: background) }
|
|
6087
6091
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
6088
6092
|
background.contentMode = imageContentMode(requestedResizeMode)
|
|
6089
|
-
background.clipsToBounds = background.contentMode == .scaleAspectFill
|
|
6093
|
+
background.clipsToBounds = background.clipsToBounds || background.contentMode == .scaleAspectFill
|
|
6090
6094
|
updateAuxiliaryHandler(events["onLoadStart"], in: &imageLoadStartHandlers, for: background)
|
|
6091
6095
|
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: background)
|
|
6092
6096
|
updateAuxiliaryHandler(events["onLoadEnd"], in: &imageLoadEndHandlers, for: background)
|
|
@@ -7006,6 +7010,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7006
7010
|
view.clipsToBounds = overflow == "hidden"
|
|
7007
7011
|
|| (view is UIScrollView && overflow != "visible")
|
|
7008
7012
|
|| (view is UILabel && (view.craftRequestedCornerRadius ?? 0) > 0)
|
|
7013
|
+
|| (view is UIImageView && (borderRadius > 0 || hasCornerRadii))
|
|
7009
7014
|
view.layer.masksToBounds = view.clipsToBounds
|
|
7010
7015
|
view.craftLayoutStyle = CraftNativeLayoutStyle(style)
|
|
7011
7016
|
(view as? CraftNativeFlowView)?.invalidateMeasurements()
|