@capgo/capacitor-native-navigation 8.1.1 → 8.2.1
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 +35 -0
- package/android/src/main/java/app/capgo/nativenavigation/NativeNavigationPlugin.java +454 -575
- package/dist/docs.json +112 -0
- package/dist/esm/definitions.d.ts +65 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +16 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +16 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +16 -3
- package/dist/plugin.js.map +1 -1
- package/docs/demo-curved-tabbar.png +0 -0
- package/docs/native-screenshots/android-curved-tabbar.png +0 -0
- package/ios/Sources/NativeNavigationPlugin/NativeNavigationPlugin.swift +612 -37
- package/package.json +1 -1
|
@@ -42,6 +42,8 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
42
42
|
private var tabContainer: UIView?
|
|
43
43
|
private var tabEffectView: UIVisualEffectView?
|
|
44
44
|
private var tabBar: UITabBar?
|
|
45
|
+
private var floatingTabBar: NativeNavigationFloatingTabBar?
|
|
46
|
+
private var tabbarStyle = NativeNavigationTabbarStyleConfig()
|
|
45
47
|
private var tabBarController: NativeNavigationTabController?
|
|
46
48
|
private var tabViewControllers: [UIViewController] = []
|
|
47
49
|
private weak var systemTabRootContainer: UIView?
|
|
@@ -51,10 +53,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
51
53
|
private var liftedWebViewOverlays: [NativeNavigationWeakView] = []
|
|
52
54
|
private var isWebViewHostedInSystemTabController = false
|
|
53
55
|
private var navbarHeight: CGFloat = 44
|
|
54
|
-
private var tabbarHeight: CGFloat =
|
|
55
|
-
private let floatingTabbarHorizontalMargin: CGFloat = 24
|
|
56
|
-
private let floatingTabbarMaxWidth: CGFloat = 430
|
|
57
|
-
private let floatingTabbarBottomGap: CGFloat = 10
|
|
56
|
+
private var tabbarHeight: CGFloat = NativeNavigationTabbarStyleConfig().totalHeight
|
|
58
57
|
private var navbarVisible = false
|
|
59
58
|
private var tabbarVisible = false
|
|
60
59
|
private var contentInsetMode = "css"
|
|
@@ -187,33 +186,56 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
187
186
|
return
|
|
188
187
|
}
|
|
189
188
|
|
|
190
|
-
|
|
189
|
+
self.tabbarStyle = self.makeTabbarStyle(from: call)
|
|
190
|
+
self.tabbarHeight = self.tabbarStyle.totalHeight
|
|
191
|
+
|
|
191
192
|
let tabs = call.getArray("tabs") as? [[String: Any]] ?? []
|
|
192
193
|
let selectedId = call.getString("selectedId")
|
|
193
194
|
let labels = call.getBool("labels", true)
|
|
194
195
|
let labelVisibilityMode = call.getString("labelVisibilityMode") ?? (labels ? "labeled" : "unlabeled")
|
|
195
196
|
let icons = call.getBool("icons", true)
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
if self.usesSystemLiquidGlass && self.tabbarStyle.shape != .curve {
|
|
199
|
+
let tabBar = self.ensureTabBar()
|
|
200
|
+
let (items, selectedIndex) = self.makeTabBarItems(
|
|
201
|
+
tabs,
|
|
202
|
+
selectedId: selectedId,
|
|
203
|
+
labelVisibilityMode: labelVisibilityMode,
|
|
204
|
+
icons: icons
|
|
205
|
+
)
|
|
206
|
+
self.floatingTabBar?.isHidden = true
|
|
207
|
+
self.tabContainer?.isHidden = true
|
|
205
208
|
self.applySystemTabBarItems(items, selectedIndex: selectedIndex, animated: call.getBool("animated", false))
|
|
209
|
+
self.applyTabBarAppearance(tabBar: tabBar, options: call)
|
|
210
|
+
self.showTabBarChrome(tabBar)
|
|
206
211
|
} else {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
self.restoreWebViewFromSystemTabController()
|
|
213
|
+
self.setSystemTabBarHidden(true)
|
|
214
|
+
self.tabBarController?.view.isHidden = true
|
|
215
|
+
let tabBar = self.ensureFloatingTabBar()
|
|
216
|
+
let (items, selectedIndex) = self.makeFloatingTabBarItems(
|
|
217
|
+
tabs,
|
|
218
|
+
selectedId: selectedId,
|
|
219
|
+
icons: icons
|
|
220
|
+
)
|
|
221
|
+
self.applyFloatingTabBarAppearance(tabBar: tabBar, options: call)
|
|
222
|
+
let resolvedSelectedIndex = selectedIndex ?? (items.indices.contains(tabBar.selectedIndex) ? tabBar.selectedIndex : 0)
|
|
223
|
+
tabBar.configure(
|
|
224
|
+
items: items,
|
|
225
|
+
selectedIndex: resolvedSelectedIndex,
|
|
226
|
+
labelVisibilityMode: labelVisibilityMode,
|
|
227
|
+
icons: icons,
|
|
228
|
+
style: self.tabbarStyle
|
|
229
|
+
)
|
|
230
|
+
tabBar.onSelect = { [weak self] index, item in
|
|
231
|
+
self?.notifyListeners("tabSelect", data: [
|
|
232
|
+
"id": item.id,
|
|
233
|
+
"index": index,
|
|
234
|
+
"title": item.title
|
|
235
|
+
])
|
|
212
236
|
}
|
|
237
|
+
self.showFloatingTabBarChrome(tabBar)
|
|
213
238
|
}
|
|
214
|
-
|
|
215
|
-
self.applyTabBarAppearance(tabBar: tabBar, options: call)
|
|
216
|
-
self.showTabBarChrome(tabBar)
|
|
217
239
|
self.layoutChrome()
|
|
218
240
|
self.updateInsetsAndNotify()
|
|
219
241
|
call.resolve(self.insetsResult())
|
|
@@ -583,6 +605,40 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
583
605
|
return bar
|
|
584
606
|
}
|
|
585
607
|
|
|
608
|
+
private func ensureFloatingTabBar() -> NativeNavigationFloatingTabBar {
|
|
609
|
+
if let floatingTabBar = floatingTabBar {
|
|
610
|
+
return floatingTabBar
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let container = NativeNavigationChromeContainer()
|
|
614
|
+
container.hitSlop = UIEdgeInsets(top: 32, left: 0, bottom: 24, right: 0)
|
|
615
|
+
container.isUserInteractionEnabled = true
|
|
616
|
+
container.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
|
|
617
|
+
container.backgroundColor = .clear
|
|
618
|
+
container.clipsToBounds = false
|
|
619
|
+
|
|
620
|
+
container.layer.shadowColor = UIColor.black.cgColor
|
|
621
|
+
container.layer.shadowOpacity = 0.14
|
|
622
|
+
container.layer.shadowRadius = 18
|
|
623
|
+
container.layer.shadowOffset = CGSize(width: 0, height: 10)
|
|
624
|
+
|
|
625
|
+
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
|
626
|
+
effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
627
|
+
effectView.isUserInteractionEnabled = false
|
|
628
|
+
effectView.clipsToBounds = true
|
|
629
|
+
container.addSubview(effectView)
|
|
630
|
+
self.tabEffectView = effectView
|
|
631
|
+
|
|
632
|
+
let bar = NativeNavigationFloatingTabBar()
|
|
633
|
+
bar.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
|
|
634
|
+
bar.clipsToBounds = false
|
|
635
|
+
container.addSubview(bar)
|
|
636
|
+
bridge?.viewController?.view.addSubview(container)
|
|
637
|
+
self.tabContainer = container
|
|
638
|
+
self.floatingTabBar = bar
|
|
639
|
+
return bar
|
|
640
|
+
}
|
|
641
|
+
|
|
586
642
|
private func ensureSystemTabBar() -> UITabBar {
|
|
587
643
|
if let tabBarController = tabBarController {
|
|
588
644
|
hostWebViewInSelectedSystemTab()
|
|
@@ -710,20 +766,22 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
710
766
|
}
|
|
711
767
|
|
|
712
768
|
private func hideTabBarChrome() {
|
|
713
|
-
if usesSystemLiquidGlass {
|
|
769
|
+
if usesSystemLiquidGlass && tabbarStyle.shape != .curve {
|
|
714
770
|
setSystemTabBarHidden(true)
|
|
715
771
|
tabBarController?.view.isHidden = false
|
|
716
772
|
hostWebViewInSelectedSystemTab()
|
|
717
773
|
} else {
|
|
718
774
|
tabContainer?.isHidden = true
|
|
775
|
+
floatingTabBar?.isHidden = true
|
|
719
776
|
tabBar?.isHidden = true
|
|
720
777
|
}
|
|
721
778
|
}
|
|
722
779
|
|
|
723
780
|
private func showTabBarChrome(_ tabBar: UITabBar) {
|
|
724
|
-
tabContainer?.isHidden =
|
|
781
|
+
tabContainer?.isHidden = true
|
|
782
|
+
floatingTabBar?.isHidden = true
|
|
725
783
|
tabBarController?.view.isHidden = false
|
|
726
|
-
if usesSystemLiquidGlass {
|
|
784
|
+
if usesSystemLiquidGlass && tabbarStyle.shape != .curve {
|
|
727
785
|
setSystemTabBarHidden(false)
|
|
728
786
|
liftWebViewOverlaysAboveSystemTabs()
|
|
729
787
|
hostWebViewInSelectedSystemTab()
|
|
@@ -732,6 +790,13 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
732
790
|
}
|
|
733
791
|
}
|
|
734
792
|
|
|
793
|
+
private func showFloatingTabBarChrome(_ tabBar: NativeNavigationFloatingTabBar) {
|
|
794
|
+
restoreWebViewFromSystemTabController()
|
|
795
|
+
tabBarController?.view.isHidden = true
|
|
796
|
+
tabContainer?.isHidden = false
|
|
797
|
+
tabBar.isHidden = false
|
|
798
|
+
}
|
|
799
|
+
|
|
735
800
|
private func captureOriginalWebViewPlacementIfNeeded(_ webView: UIView) {
|
|
736
801
|
guard originalWebViewSuperview == nil, let superview = webView.superview else {
|
|
737
802
|
return
|
|
@@ -913,6 +978,33 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
913
978
|
return (items, selectedIndex)
|
|
914
979
|
}
|
|
915
980
|
|
|
981
|
+
private func makeFloatingTabBarItems(
|
|
982
|
+
_ tabs: [[String: Any]],
|
|
983
|
+
selectedId: String?,
|
|
984
|
+
icons: Bool
|
|
985
|
+
) -> ([NativeNavigationFloatingTabItem], Int?) {
|
|
986
|
+
var selectedIndex: Int?
|
|
987
|
+
let items = tabs.enumerated().map { index, tab -> NativeNavigationFloatingTabItem in
|
|
988
|
+
let id = tab["id"] as? String ?? "tab-\(index)"
|
|
989
|
+
let title = tab["title"] as? String ?? ""
|
|
990
|
+
let image = icons ? self.image(from: tab["icon"] as? [String: Any]) : nil
|
|
991
|
+
let selectedImage = icons ? self.image(from: tab["selectedIcon"] as? [String: Any]) : nil
|
|
992
|
+
if id == selectedId {
|
|
993
|
+
selectedIndex = index
|
|
994
|
+
}
|
|
995
|
+
return NativeNavigationFloatingTabItem(
|
|
996
|
+
id: id,
|
|
997
|
+
title: title,
|
|
998
|
+
accessibilityTitle: title.isEmpty ? id : title,
|
|
999
|
+
image: image,
|
|
1000
|
+
selectedImage: selectedImage,
|
|
1001
|
+
badge: tab["badge"].map { String(describing: $0) },
|
|
1002
|
+
enabled: tab["enabled"] as? Bool ?? true
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
1005
|
+
return (items, selectedIndex)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
916
1008
|
private func tabTitle(
|
|
917
1009
|
_ title: String?,
|
|
918
1010
|
id: String,
|
|
@@ -1078,6 +1170,35 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1078
1170
|
return imageView
|
|
1079
1171
|
}
|
|
1080
1172
|
|
|
1173
|
+
private func makeTabbarStyle(from call: CAPPluginCall) -> NativeNavigationTabbarStyleConfig {
|
|
1174
|
+
let rawStyle = call.getObject("style") ?? [:]
|
|
1175
|
+
let requestedShape = (rawStyle["shape"] as? String)?.lowercased()
|
|
1176
|
+
let shape: NativeNavigationTabbarShape = requestedShape == "curve" ? .curve : .floating
|
|
1177
|
+
let isCurve = shape == .curve
|
|
1178
|
+
let centerDiameter = max(number(from: rawStyle["centerButtonDiameter"]) ?? 76, 44)
|
|
1179
|
+
let height = max(number(from: rawStyle["height"]) ?? (isCurve ? 76 : 64), 44)
|
|
1180
|
+
let centerLift = max(number(from: rawStyle["centerButtonLift"]) ?? (centerDiameter / 2), 0)
|
|
1181
|
+
let bottomGap = max(number(from: rawStyle["bottomGap"]) ?? (isCurve ? 0 : 10), 0)
|
|
1182
|
+
let horizontalMargin = max(number(from: rawStyle["horizontalMargin"]) ?? (isCurve ? 0 : 24), 0)
|
|
1183
|
+
let maxWidth = max(number(from: rawStyle["maxWidth"]) ?? (isCurve ? 0 : 430), 0)
|
|
1184
|
+
let cornerRadius = max(number(from: rawStyle["cornerRadius"]) ?? (isCurve ? 24 : height / 2), 0)
|
|
1185
|
+
let centerButtonColor = (rawStyle["centerButtonColor"] as? String).flatMap { UIColor(hexString: $0) }
|
|
1186
|
+
let centerButtonIconColor = (rawStyle["centerButtonIconColor"] as? String).flatMap { UIColor(hexString: $0) } ?? .white
|
|
1187
|
+
|
|
1188
|
+
return NativeNavigationTabbarStyleConfig(
|
|
1189
|
+
shape: shape,
|
|
1190
|
+
height: height,
|
|
1191
|
+
horizontalMargin: horizontalMargin,
|
|
1192
|
+
maxWidth: maxWidth,
|
|
1193
|
+
bottomGap: bottomGap,
|
|
1194
|
+
cornerRadius: cornerRadius,
|
|
1195
|
+
centerItemId: rawStyle["centerItemId"] as? String,
|
|
1196
|
+
centerButtonDiameter: centerDiameter,
|
|
1197
|
+
centerButtonLift: centerLift,
|
|
1198
|
+
centerButtonColor: centerButtonColor,
|
|
1199
|
+
centerButtonIconColor: centerButtonIconColor
|
|
1200
|
+
)
|
|
1201
|
+
}
|
|
1081
1202
|
private func applyNavBarAppearance(navBar: UINavigationBar, options call: CAPPluginCall) {
|
|
1082
1203
|
let appearance = UINavigationBarAppearance()
|
|
1083
1204
|
let transparent = call.getBool("transparent", false)
|
|
@@ -1224,6 +1345,37 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1224
1345
|
}
|
|
1225
1346
|
}
|
|
1226
1347
|
|
|
1348
|
+
private func applyFloatingTabBarAppearance(tabBar: NativeNavigationFloatingTabBar, options call: CAPPluginCall) {
|
|
1349
|
+
let colors = call.getObject("colors")
|
|
1350
|
+
let backgroundTint = colorValue(colors?["background"])
|
|
1351
|
+
let backgroundColor = (backgroundTint ?? .systemBackground).withAlphaComponent(tabbarStyle.shape == .curve ? 0.96 : 0.46)
|
|
1352
|
+
|
|
1353
|
+
if tabbarStyle.shape == .curve {
|
|
1354
|
+
tabEffectView?.isHidden = true
|
|
1355
|
+
} else if let effect = blurEffect(from: call.getString("blurEffect"), fallback: .systemChromeMaterial) {
|
|
1356
|
+
tabEffectView?.effect = effect
|
|
1357
|
+
tabEffectView?.isHidden = false
|
|
1358
|
+
tabEffectView?.contentView.backgroundColor = backgroundTint?.withAlphaComponent(0.12) ?? backgroundColor
|
|
1359
|
+
} else {
|
|
1360
|
+
tabEffectView?.isHidden = true
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
tabBar.backgroundFillColor = backgroundColor
|
|
1364
|
+
if let color = colorValue(colors?["tint"]) {
|
|
1365
|
+
tabBar.selectedTintColor = color
|
|
1366
|
+
}
|
|
1367
|
+
if let color = colorValue(colors?["inactiveTint"]) {
|
|
1368
|
+
tabBar.inactiveTintColor = color
|
|
1369
|
+
}
|
|
1370
|
+
if let color = colorValue(call.getString("badgeBackgroundColor")) ?? colorValue(colors?["badgeBackground"]) {
|
|
1371
|
+
tabBar.badgeBackgroundColor = color
|
|
1372
|
+
}
|
|
1373
|
+
if let color = colorValue(call.getString("badgeTextColor")) ?? colorValue(colors?["badgeText"]) {
|
|
1374
|
+
tabBar.badgeTextColor = color
|
|
1375
|
+
}
|
|
1376
|
+
tabBar.setNeedsLayout()
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1227
1379
|
private func applyTabBarBadgeOptions(_ appearance: UITabBarAppearance, options call: CAPPluginCall) {
|
|
1228
1380
|
if let color = colorValue(call.getString("badgeBackgroundColor")) {
|
|
1229
1381
|
applyTabItemAppearances(appearance) { itemAppearance in
|
|
@@ -1264,17 +1416,18 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1264
1416
|
}
|
|
1265
1417
|
|
|
1266
1418
|
if let container = tabContainer {
|
|
1267
|
-
let availableWidth = max(0, width - (
|
|
1268
|
-
let
|
|
1419
|
+
let availableWidth = max(0, width - (tabbarStyle.horizontalMargin * 2))
|
|
1420
|
+
let maxWidth = tabbarStyle.maxWidth > 0 ? tabbarStyle.maxWidth : availableWidth
|
|
1421
|
+
let tabbarWidth = min(availableWidth, maxWidth)
|
|
1269
1422
|
let originX = (width - tabbarWidth) / 2
|
|
1270
|
-
let originY = height - safeInsets.bottom -
|
|
1271
|
-
container.frame = CGRect(x: originX, y: originY, width: tabbarWidth, height:
|
|
1272
|
-
container.layer.cornerRadius =
|
|
1273
|
-
container.layer.shadowPath =
|
|
1423
|
+
let originY = height - safeInsets.bottom - tabbarStyle.bottomGap - tabbarStyle.totalHeight
|
|
1424
|
+
container.frame = CGRect(x: originX, y: originY, width: tabbarWidth, height: tabbarStyle.totalHeight)
|
|
1425
|
+
container.layer.cornerRadius = tabbarStyle.shape == .floating ? tabbarStyle.cornerRadius : 0
|
|
1426
|
+
container.layer.shadowPath = NativeNavigationTabbarBackgroundPath.path(in: container.bounds, style: tabbarStyle).cgPath
|
|
1274
1427
|
tabEffectView?.frame = container.bounds
|
|
1275
|
-
tabEffectView?.layer.cornerRadius =
|
|
1276
|
-
|
|
1277
|
-
|
|
1428
|
+
tabEffectView?.layer.cornerRadius = tabbarStyle.cornerRadius
|
|
1429
|
+
floatingTabBar?.frame = container.bounds
|
|
1430
|
+
floatingTabBar?.layer.cornerRadius = 0
|
|
1278
1431
|
}
|
|
1279
1432
|
|
|
1280
1433
|
if let tabBarController = tabBarController {
|
|
@@ -1287,7 +1440,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1287
1440
|
}
|
|
1288
1441
|
|
|
1289
1442
|
private func bringChromeToFront() {
|
|
1290
|
-
if usesSystemLiquidGlass {
|
|
1443
|
+
if usesSystemLiquidGlass && tabbarStyle.shape != .curve {
|
|
1291
1444
|
if let navContainer = navContainer {
|
|
1292
1445
|
bridge?.viewController?.view.bringSubviewToFront(navContainer)
|
|
1293
1446
|
}
|
|
@@ -1386,10 +1539,10 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1386
1539
|
private func currentInsets() -> [String: Any] {
|
|
1387
1540
|
let safeInsets = bridge?.viewController?.view.safeAreaInsets ?? .zero
|
|
1388
1541
|
let navHeight = isEnabled && navbarVisible ? navbarHeight + safeInsets.top : 0
|
|
1542
|
+
let usesSystemTabbar = usesSystemLiquidGlass && tabbarStyle.shape != .curve
|
|
1389
1543
|
let nativeTabHeight = max(tabBar?.frame.height ?? 0, 49 + safeInsets.bottom)
|
|
1390
|
-
let
|
|
1391
|
-
|
|
1392
|
-
: 0
|
|
1544
|
+
let customTabHeight = tabbarHeight + safeInsets.bottom + tabbarStyle.bottomGap
|
|
1545
|
+
let tabHeight = isEnabled && tabbarVisible ? (usesSystemTabbar ? nativeTabHeight : customTabHeight) : 0
|
|
1393
1546
|
return [
|
|
1394
1547
|
"top": navHeight,
|
|
1395
1548
|
"right": safeInsets.right,
|
|
@@ -1489,6 +1642,428 @@ private struct SVGRenderStyle {
|
|
|
1489
1642
|
}
|
|
1490
1643
|
}
|
|
1491
1644
|
|
|
1645
|
+
private struct NativeNavigationFloatingTabItem {
|
|
1646
|
+
let id: String
|
|
1647
|
+
let title: String
|
|
1648
|
+
let accessibilityTitle: String
|
|
1649
|
+
let image: UIImage?
|
|
1650
|
+
let selectedImage: UIImage?
|
|
1651
|
+
let badge: String?
|
|
1652
|
+
let enabled: Bool
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
private enum NativeNavigationTabbarShape {
|
|
1656
|
+
case floating
|
|
1657
|
+
case curve
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
private struct NativeNavigationTabbarStyleConfig {
|
|
1661
|
+
var shape: NativeNavigationTabbarShape = .floating
|
|
1662
|
+
var height: CGFloat = 64
|
|
1663
|
+
var horizontalMargin: CGFloat = 24
|
|
1664
|
+
var maxWidth: CGFloat = 430
|
|
1665
|
+
var bottomGap: CGFloat = 10
|
|
1666
|
+
var cornerRadius: CGFloat = 32
|
|
1667
|
+
var centerItemId: String?
|
|
1668
|
+
var centerButtonDiameter: CGFloat = 76
|
|
1669
|
+
var centerButtonLift: CGFloat = 38
|
|
1670
|
+
var centerButtonColor: UIColor?
|
|
1671
|
+
var centerButtonIconColor: UIColor = .white
|
|
1672
|
+
|
|
1673
|
+
var barTop: CGFloat {
|
|
1674
|
+
shape == .curve ? centerButtonLift : 0
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
var totalHeight: CGFloat {
|
|
1678
|
+
height + barTop
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
private enum NativeNavigationTabbarBackgroundPath {
|
|
1683
|
+
static func path(in bounds: CGRect, style: NativeNavigationTabbarStyleConfig) -> UIBezierPath {
|
|
1684
|
+
guard style.shape == .curve else {
|
|
1685
|
+
return UIBezierPath(roundedRect: bounds, cornerRadius: style.cornerRadius)
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
let barRect = CGRect(x: 0, y: style.barTop, width: bounds.width, height: max(style.height, 1))
|
|
1689
|
+
let cornerRadius = min(style.cornerRadius, barRect.height / 2)
|
|
1690
|
+
let centerX = bounds.midX
|
|
1691
|
+
let notchRadius = (style.centerButtonDiameter / 2) + 7
|
|
1692
|
+
let notchDepth = min(barRect.height * 0.78, notchRadius)
|
|
1693
|
+
let leftShoulder = max(barRect.minX + cornerRadius, centerX - notchRadius)
|
|
1694
|
+
let rightShoulder = min(barRect.maxX - cornerRadius, centerX + notchRadius)
|
|
1695
|
+
let control = notchDepth * 0.55228475
|
|
1696
|
+
let path = UIBezierPath()
|
|
1697
|
+
|
|
1698
|
+
path.move(to: CGPoint(x: barRect.minX + cornerRadius, y: barRect.minY))
|
|
1699
|
+
path.addLine(to: CGPoint(x: leftShoulder, y: barRect.minY))
|
|
1700
|
+
path.addCurve(
|
|
1701
|
+
to: CGPoint(x: centerX, y: barRect.minY + notchDepth),
|
|
1702
|
+
controlPoint1: CGPoint(x: leftShoulder, y: barRect.minY + control),
|
|
1703
|
+
controlPoint2: CGPoint(x: centerX - control, y: barRect.minY + notchDepth)
|
|
1704
|
+
)
|
|
1705
|
+
path.addCurve(
|
|
1706
|
+
to: CGPoint(x: rightShoulder, y: barRect.minY),
|
|
1707
|
+
controlPoint1: CGPoint(x: centerX + control, y: barRect.minY + notchDepth),
|
|
1708
|
+
controlPoint2: CGPoint(x: rightShoulder, y: barRect.minY + control)
|
|
1709
|
+
)
|
|
1710
|
+
path.addLine(to: CGPoint(x: barRect.maxX - cornerRadius, y: barRect.minY))
|
|
1711
|
+
path.addQuadCurve(to: CGPoint(x: barRect.maxX, y: barRect.minY + cornerRadius), controlPoint: CGPoint(x: barRect.maxX, y: barRect.minY))
|
|
1712
|
+
path.addLine(to: CGPoint(x: barRect.maxX, y: barRect.maxY - cornerRadius))
|
|
1713
|
+
path.addQuadCurve(to: CGPoint(x: barRect.maxX - cornerRadius, y: barRect.maxY), controlPoint: CGPoint(x: barRect.maxX, y: barRect.maxY))
|
|
1714
|
+
path.addLine(to: CGPoint(x: barRect.minX + cornerRadius, y: barRect.maxY))
|
|
1715
|
+
path.addQuadCurve(to: CGPoint(x: barRect.minX, y: barRect.maxY - cornerRadius), controlPoint: CGPoint(x: barRect.minX, y: barRect.maxY))
|
|
1716
|
+
path.addLine(to: CGPoint(x: barRect.minX, y: barRect.minY + cornerRadius))
|
|
1717
|
+
path.addQuadCurve(to: CGPoint(x: barRect.minX + cornerRadius, y: barRect.minY), controlPoint: CGPoint(x: barRect.minX, y: barRect.minY))
|
|
1718
|
+
path.close()
|
|
1719
|
+
return path
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
private struct NativeNavigationFloatingTabStyle {
|
|
1724
|
+
let selected: Bool
|
|
1725
|
+
let labels: Bool
|
|
1726
|
+
let icons: Bool
|
|
1727
|
+
let isCenter: Bool
|
|
1728
|
+
let selectedTint: UIColor
|
|
1729
|
+
let inactiveTint: UIColor
|
|
1730
|
+
let centerButtonColor: UIColor
|
|
1731
|
+
let centerButtonIconColor: UIColor
|
|
1732
|
+
let badgeBackgroundColor: UIColor
|
|
1733
|
+
let badgeTextColor: UIColor
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
private final class NativeNavigationFloatingTabBar: UIView {
|
|
1737
|
+
private var items: [NativeNavigationFloatingTabItem] = []
|
|
1738
|
+
private var buttons: [NativeNavigationFloatingTabButton] = []
|
|
1739
|
+
private var labelVisibilityMode = "labeled"
|
|
1740
|
+
private var iconsVisible = true
|
|
1741
|
+
private let backgroundShapeView = NativeNavigationTabbarBackgroundView()
|
|
1742
|
+
private var tabbarStyle = NativeNavigationTabbarStyleConfig()
|
|
1743
|
+
|
|
1744
|
+
var selectedIndex = 0
|
|
1745
|
+
var selectedTintColor = UIColor.systemBlue {
|
|
1746
|
+
didSet { updateButtons() }
|
|
1747
|
+
}
|
|
1748
|
+
var inactiveTintColor = UIColor.secondaryLabel {
|
|
1749
|
+
didSet { updateButtons() }
|
|
1750
|
+
}
|
|
1751
|
+
var badgeBackgroundColor = UIColor.systemRed {
|
|
1752
|
+
didSet { updateButtons() }
|
|
1753
|
+
}
|
|
1754
|
+
var badgeTextColor = UIColor.white {
|
|
1755
|
+
didSet { updateButtons() }
|
|
1756
|
+
}
|
|
1757
|
+
var backgroundFillColor = UIColor.systemBackground.withAlphaComponent(0.46) {
|
|
1758
|
+
didSet {
|
|
1759
|
+
backgroundShapeView.fillColor = backgroundFillColor
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
var onSelect: ((Int, NativeNavigationFloatingTabItem) -> Void)?
|
|
1763
|
+
|
|
1764
|
+
override init(frame: CGRect) {
|
|
1765
|
+
super.init(frame: frame)
|
|
1766
|
+
isOpaque = false
|
|
1767
|
+
backgroundShapeView.isUserInteractionEnabled = false
|
|
1768
|
+
backgroundShapeView.fillColor = backgroundFillColor
|
|
1769
|
+
addSubview(backgroundShapeView)
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
required init?(coder: NSCoder) {
|
|
1773
|
+
fatalError("init(coder:) has not been implemented")
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
func configure(
|
|
1777
|
+
items: [NativeNavigationFloatingTabItem],
|
|
1778
|
+
selectedIndex: Int,
|
|
1779
|
+
labelVisibilityMode: String,
|
|
1780
|
+
icons: Bool,
|
|
1781
|
+
style: NativeNavigationTabbarStyleConfig
|
|
1782
|
+
) {
|
|
1783
|
+
self.items = items
|
|
1784
|
+
self.labelVisibilityMode = labelVisibilityMode
|
|
1785
|
+
self.iconsVisible = icons
|
|
1786
|
+
self.tabbarStyle = style
|
|
1787
|
+
self.selectedIndex = items.indices.contains(selectedIndex) ? selectedIndex : 0
|
|
1788
|
+
backgroundShapeView.style = style
|
|
1789
|
+
rebuildButtons()
|
|
1790
|
+
setNeedsLayout()
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
override func layoutSubviews() {
|
|
1794
|
+
super.layoutSubviews()
|
|
1795
|
+
backgroundShapeView.frame = bounds
|
|
1796
|
+
guard !buttons.isEmpty else {
|
|
1797
|
+
return
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if let centerIndex = centerButtonIndex(), buttons.indices.contains(centerIndex) {
|
|
1801
|
+
let buttonDiameter = tabbarStyle.centerButtonDiameter
|
|
1802
|
+
let centerGap = min(buttonDiameter + 22, bounds.width * 0.46)
|
|
1803
|
+
let barFrame = CGRect(x: 0, y: tabbarStyle.barTop, width: bounds.width, height: tabbarStyle.height)
|
|
1804
|
+
let leftWidth = max(0, bounds.midX - centerGap / 2)
|
|
1805
|
+
let rightX = min(bounds.width, bounds.midX + centerGap / 2)
|
|
1806
|
+
let centerFrame = CGRect(
|
|
1807
|
+
x: bounds.midX - buttonDiameter / 2,
|
|
1808
|
+
y: 0,
|
|
1809
|
+
width: buttonDiameter,
|
|
1810
|
+
height: buttonDiameter
|
|
1811
|
+
)
|
|
1812
|
+
|
|
1813
|
+
buttons[centerIndex].frame = centerFrame
|
|
1814
|
+
layoutButtons(Array(0..<centerIndex), in: CGRect(x: 0, y: barFrame.minY, width: leftWidth, height: barFrame.height))
|
|
1815
|
+
layoutButtons(
|
|
1816
|
+
Array((centerIndex + 1)..<buttons.count),
|
|
1817
|
+
in: CGRect(x: rightX, y: barFrame.minY, width: bounds.width - rightX, height: barFrame.height)
|
|
1818
|
+
)
|
|
1819
|
+
return
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
layoutButtons(Array(buttons.indices), in: bounds)
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
private func layoutButtons(_ indices: [Int], in rect: CGRect) {
|
|
1826
|
+
guard !indices.isEmpty else {
|
|
1827
|
+
return
|
|
1828
|
+
}
|
|
1829
|
+
let itemWidth = rect.width / CGFloat(indices.count)
|
|
1830
|
+
for (position, index) in indices.enumerated() where buttons.indices.contains(index) {
|
|
1831
|
+
buttons[index].frame = CGRect(
|
|
1832
|
+
x: rect.minX + CGFloat(position) * itemWidth,
|
|
1833
|
+
y: rect.minY,
|
|
1834
|
+
width: itemWidth,
|
|
1835
|
+
height: rect.height
|
|
1836
|
+
)
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
private func centerButtonIndex() -> Int? {
|
|
1841
|
+
guard tabbarStyle.shape == .curve, !items.isEmpty else {
|
|
1842
|
+
return nil
|
|
1843
|
+
}
|
|
1844
|
+
if let centerItemId = tabbarStyle.centerItemId,
|
|
1845
|
+
let index = items.firstIndex(where: { $0.id == centerItemId }) {
|
|
1846
|
+
return index
|
|
1847
|
+
}
|
|
1848
|
+
return items.count / 2
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
private func rebuildButtons() {
|
|
1852
|
+
buttons.forEach { $0.removeFromSuperview() }
|
|
1853
|
+
buttons = items.enumerated().map { index, item in
|
|
1854
|
+
let button = NativeNavigationFloatingTabButton()
|
|
1855
|
+
button.tag = index
|
|
1856
|
+
button.configure(
|
|
1857
|
+
item: item,
|
|
1858
|
+
style: style(for: index)
|
|
1859
|
+
)
|
|
1860
|
+
button.addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
|
|
1861
|
+
addSubview(button)
|
|
1862
|
+
return button
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
private func updateButtons() {
|
|
1867
|
+
for (index, button) in buttons.enumerated() {
|
|
1868
|
+
guard items.indices.contains(index) else {
|
|
1869
|
+
continue
|
|
1870
|
+
}
|
|
1871
|
+
button.configure(
|
|
1872
|
+
item: items[index],
|
|
1873
|
+
style: style(for: index)
|
|
1874
|
+
)
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
private func style(for index: Int) -> NativeNavigationFloatingTabStyle {
|
|
1879
|
+
let isCenter = centerButtonIndex() == index
|
|
1880
|
+
return NativeNavigationFloatingTabStyle(
|
|
1881
|
+
selected: index == selectedIndex,
|
|
1882
|
+
labels: showsLabel(for: index),
|
|
1883
|
+
icons: iconsVisible,
|
|
1884
|
+
isCenter: isCenter,
|
|
1885
|
+
selectedTint: selectedTintColor,
|
|
1886
|
+
inactiveTint: inactiveTintColor,
|
|
1887
|
+
centerButtonColor: tabbarStyle.centerButtonColor ?? selectedTintColor,
|
|
1888
|
+
centerButtonIconColor: tabbarStyle.centerButtonIconColor,
|
|
1889
|
+
badgeBackgroundColor: badgeBackgroundColor,
|
|
1890
|
+
badgeTextColor: badgeTextColor
|
|
1891
|
+
)
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
private func showsLabel(for index: Int) -> Bool {
|
|
1895
|
+
switch labelVisibilityMode {
|
|
1896
|
+
case "unlabeled":
|
|
1897
|
+
return false
|
|
1898
|
+
case "selected":
|
|
1899
|
+
return index == selectedIndex
|
|
1900
|
+
case "auto":
|
|
1901
|
+
let compact = traitCollection.horizontalSizeClass == .compact || UIDevice.current.userInterfaceIdiom == .phone
|
|
1902
|
+
return !compact || index == selectedIndex
|
|
1903
|
+
default:
|
|
1904
|
+
return true
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
@objc private func handleTap(_ sender: NativeNavigationFloatingTabButton) {
|
|
1909
|
+
let index = sender.tag
|
|
1910
|
+
guard items.indices.contains(index), items[index].enabled else {
|
|
1911
|
+
return
|
|
1912
|
+
}
|
|
1913
|
+
selectedIndex = index
|
|
1914
|
+
updateButtons()
|
|
1915
|
+
onSelect?(index, items[index])
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
private final class NativeNavigationTabbarBackgroundView: UIView {
|
|
1920
|
+
var style = NativeNavigationTabbarStyleConfig() {
|
|
1921
|
+
didSet { setNeedsDisplay() }
|
|
1922
|
+
}
|
|
1923
|
+
var fillColor = UIColor.systemBackground.withAlphaComponent(0.46) {
|
|
1924
|
+
didSet { setNeedsDisplay() }
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
override init(frame: CGRect) {
|
|
1928
|
+
super.init(frame: frame)
|
|
1929
|
+
isOpaque = false
|
|
1930
|
+
backgroundColor = .clear
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
required init?(coder: NSCoder) {
|
|
1934
|
+
fatalError("init(coder:) has not been implemented")
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
override func draw(_ rect: CGRect) {
|
|
1938
|
+
fillColor.setFill()
|
|
1939
|
+
NativeNavigationTabbarBackgroundPath.path(in: bounds, style: style).fill()
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
private final class NativeNavigationFloatingTabButton: UIControl {
|
|
1944
|
+
private let selectedView = UIView()
|
|
1945
|
+
private let imageView = UIImageView()
|
|
1946
|
+
private let titleLabel = UILabel()
|
|
1947
|
+
private let badgeLabel = UILabel()
|
|
1948
|
+
private var hasIcon = true
|
|
1949
|
+
private var hasLabel = true
|
|
1950
|
+
private var isCenterButton = false
|
|
1951
|
+
private var badgeText: String?
|
|
1952
|
+
|
|
1953
|
+
override init(frame: CGRect) {
|
|
1954
|
+
super.init(frame: frame)
|
|
1955
|
+
isAccessibilityElement = true
|
|
1956
|
+
|
|
1957
|
+
selectedView.isUserInteractionEnabled = false
|
|
1958
|
+
selectedView.alpha = 0
|
|
1959
|
+
addSubview(selectedView)
|
|
1960
|
+
|
|
1961
|
+
imageView.contentMode = .scaleAspectFit
|
|
1962
|
+
imageView.isUserInteractionEnabled = false
|
|
1963
|
+
addSubview(imageView)
|
|
1964
|
+
|
|
1965
|
+
titleLabel.textAlignment = .center
|
|
1966
|
+
titleLabel.font = .systemFont(ofSize: 11, weight: .semibold)
|
|
1967
|
+
titleLabel.adjustsFontSizeToFitWidth = true
|
|
1968
|
+
titleLabel.minimumScaleFactor = 0.78
|
|
1969
|
+
titleLabel.isUserInteractionEnabled = false
|
|
1970
|
+
addSubview(titleLabel)
|
|
1971
|
+
|
|
1972
|
+
badgeLabel.textAlignment = .center
|
|
1973
|
+
badgeLabel.font = .systemFont(ofSize: 11, weight: .bold)
|
|
1974
|
+
badgeLabel.textColor = .white
|
|
1975
|
+
badgeLabel.backgroundColor = .systemRed
|
|
1976
|
+
badgeLabel.layer.masksToBounds = true
|
|
1977
|
+
badgeLabel.isUserInteractionEnabled = false
|
|
1978
|
+
addSubview(badgeLabel)
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
required init?(coder: NSCoder) {
|
|
1982
|
+
fatalError("init(coder:) has not been implemented")
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
func configure(
|
|
1986
|
+
item: NativeNavigationFloatingTabItem,
|
|
1987
|
+
style: NativeNavigationFloatingTabStyle
|
|
1988
|
+
) {
|
|
1989
|
+
isEnabled = item.enabled
|
|
1990
|
+
alpha = item.enabled ? 1 : 0.38
|
|
1991
|
+
isCenterButton = style.isCenter
|
|
1992
|
+
hasIcon = style.icons && (item.image != nil || item.selectedImage != nil)
|
|
1993
|
+
hasLabel = style.isCenter ? (!hasIcon && !item.title.isEmpty) : (style.labels && !item.title.isEmpty)
|
|
1994
|
+
badgeText = item.badge
|
|
1995
|
+
|
|
1996
|
+
let color = style.isCenter ? style.centerButtonIconColor : (style.selected ? style.selectedTint : style.inactiveTint)
|
|
1997
|
+
selectedView.backgroundColor = style.isCenter ? style.centerButtonColor : style.selectedTint.withAlphaComponent(style.selected ? 0.16 : 0)
|
|
1998
|
+
selectedView.alpha = style.isCenter || style.selected ? 1 : 0
|
|
1999
|
+
|
|
2000
|
+
layer.shadowColor = UIColor.black.cgColor
|
|
2001
|
+
layer.shadowOpacity = style.isCenter ? 0.2 : 0
|
|
2002
|
+
layer.shadowRadius = style.isCenter ? 14 : 0
|
|
2003
|
+
layer.shadowOffset = CGSize(width: 0, height: 8)
|
|
2004
|
+
|
|
2005
|
+
let image = style.selected ? (item.selectedImage ?? item.image) : item.image
|
|
2006
|
+
imageView.image = image
|
|
2007
|
+
imageView.tintColor = color
|
|
2008
|
+
imageView.isHidden = !hasIcon
|
|
2009
|
+
|
|
2010
|
+
titleLabel.text = item.title
|
|
2011
|
+
titleLabel.textColor = color
|
|
2012
|
+
titleLabel.font = .systemFont(ofSize: style.isCenter ? 12 : 11, weight: style.selected ? .bold : .semibold)
|
|
2013
|
+
titleLabel.isHidden = !hasLabel
|
|
2014
|
+
|
|
2015
|
+
badgeLabel.text = item.badge
|
|
2016
|
+
badgeLabel.textColor = style.badgeTextColor
|
|
2017
|
+
badgeLabel.backgroundColor = style.badgeBackgroundColor
|
|
2018
|
+
badgeLabel.isHidden = item.badge == nil || item.badge == "0"
|
|
2019
|
+
accessibilityLabel = item.accessibilityTitle
|
|
2020
|
+
accessibilityTraits = style.selected ? [.button, .selected] : .button
|
|
2021
|
+
setNeedsLayout()
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
override func layoutSubviews() {
|
|
2025
|
+
super.layoutSubviews()
|
|
2026
|
+
if isCenterButton {
|
|
2027
|
+
selectedView.frame = bounds
|
|
2028
|
+
} else {
|
|
2029
|
+
let selectedSize: CGFloat = 58
|
|
2030
|
+
selectedView.frame = CGRect(x: (bounds.width - selectedSize) / 2, y: (bounds.height - selectedSize) / 2, width: selectedSize, height: selectedSize)
|
|
2031
|
+
}
|
|
2032
|
+
selectedView.layer.cornerRadius = selectedView.bounds.height / 2
|
|
2033
|
+
|
|
2034
|
+
let iconSize: CGFloat = isCenterButton ? 32 : 23
|
|
2035
|
+
if isCenterButton {
|
|
2036
|
+
if hasIcon {
|
|
2037
|
+
imageView.frame = CGRect(x: (bounds.width - iconSize) / 2, y: (bounds.height - iconSize) / 2, width: iconSize, height: iconSize)
|
|
2038
|
+
titleLabel.frame = .zero
|
|
2039
|
+
} else {
|
|
2040
|
+
imageView.frame = .zero
|
|
2041
|
+
titleLabel.frame = CGRect(x: 8, y: (bounds.height - 18) / 2, width: bounds.width - 16, height: 18)
|
|
2042
|
+
}
|
|
2043
|
+
} else if hasIcon && hasLabel {
|
|
2044
|
+
imageView.frame = CGRect(x: (bounds.width - iconSize) / 2, y: 10, width: iconSize, height: iconSize)
|
|
2045
|
+
titleLabel.frame = CGRect(x: 5, y: bounds.height - 23, width: bounds.width - 10, height: 15)
|
|
2046
|
+
} else if hasIcon {
|
|
2047
|
+
imageView.frame = CGRect(x: (bounds.width - iconSize) / 2, y: (bounds.height - iconSize) / 2, width: iconSize, height: iconSize)
|
|
2048
|
+
titleLabel.frame = .zero
|
|
2049
|
+
} else {
|
|
2050
|
+
imageView.frame = .zero
|
|
2051
|
+
titleLabel.frame = CGRect(x: 5, y: (bounds.height - 18) / 2, width: bounds.width - 10, height: 18)
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
let badgeHeight: CGFloat = 18
|
|
2055
|
+
let badgeWidth = max(badgeHeight, CGFloat((badgeText ?? "").count * 7 + 11))
|
|
2056
|
+
let anchor = hasIcon ? imageView.frame : CGRect(x: bounds.midX - 10, y: bounds.midY - 10, width: 20, height: 20)
|
|
2057
|
+
badgeLabel.frame = CGRect(
|
|
2058
|
+
x: min(bounds.width - badgeWidth - 8, anchor.midX + 7),
|
|
2059
|
+
y: max(6, anchor.minY - 6),
|
|
2060
|
+
width: badgeWidth,
|
|
2061
|
+
height: badgeHeight
|
|
2062
|
+
)
|
|
2063
|
+
badgeLabel.layer.cornerRadius = badgeHeight / 2
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1492
2067
|
private final class NativeNavigationChromeContainer: UIView {
|
|
1493
2068
|
var hitSlop = UIEdgeInsets.zero
|
|
1494
2069
|
|