diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd29ec64..2bb7eb27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,16 @@ jobs: - name: Typecheck files run: yarn typecheck + swift-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: SwiftLint + uses: norio-nomura/action-swiftlint@3.2.1 + env: + WORKING_DIRECTORY: Source + build-library: runs-on: ubuntu-latest steps: @@ -94,7 +104,7 @@ jobs: - name: Build example for Android env: - JAVA_OPTS: "-XX:MaxHeapSize=6g" + JAVA_OPTS: '-XX:MaxHeapSize=6g' run: | yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" @@ -153,7 +163,7 @@ jobs: - name: Build example for Android new arch env: - JAVA_OPTS: "-XX:MaxHeapSize=6g" + JAVA_OPTS: '-XX:MaxHeapSize=6g' run: | yarn turbo run build:android:fabric --cache-dir="${{ env.TURBO_CACHE_DIR }}" @@ -200,7 +210,6 @@ jobs: run: | yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" - build-ios-newarch: runs-on: macos-15 env: diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..ed2e61a5 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,83 @@ +opt_in_rules: + - anonymous_argument_in_multiline_closure + - array_init + - closure_end_indentation + - closure_spacing + - collection_alignment + - comma_inheritance + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - direct_return + - discarded_notification_center_observer + - discouraged_none_name + - discouraged_object_literal + - empty_collection_literal + - empty_count + - empty_string + - enum_case_associated_values_count + - expiring_todo + - explicit_init + - fallthrough + - fatal_error_message + - file_name_no_space + - first_where + - flatmap_over_map_reduce + # - force_unwrapping # nice to have + - identical_operands + - implicit_return + # - implicitly_unwrapped_optional # nice to have + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_objc_type + - literal_expression_end_indentation + - local_doc_comment + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + # - no_magic_numbers # nice to have + - nslocalizedstring_key + - number_separator + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - prefer_self_type_over_type_of_self + - reduce_into + - redundant_nil_coalescing + - redundant_self_in_closure + - redundant_type_annotation + - return_value_from_void_function + - self_binding + - shorthand_optional_binding + - sorted_first_last + - sorted_imports + - strict_fileprivate + - superfluous_else + - switch_case_on_newline + - toggle_bool + - trailing_closure + - unavailable_function + - unhandled_throwing_task + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - weak_delegate + - yoda_condition + +disabled_rules: + - nesting + +identifier_name: + excluded: # excluded via string array + - id + - ID diff --git a/packages/react-native-bottom-tabs/ios/Events/OnNativeLayoutEvent.swift b/packages/react-native-bottom-tabs/ios/Events/OnNativeLayoutEvent.swift index b01e4999..fd001071 100644 --- a/packages/react-native-bottom-tabs/ios/Events/OnNativeLayoutEvent.swift +++ b/packages/react-native-bottom-tabs/ios/Events/OnNativeLayoutEvent.swift @@ -6,7 +6,7 @@ public class OnNativeLayoutEvent: NSObject, RCTEvent { public var viewTag: NSNumber public var eventName: String { - return "onNativeLayout" + "onNativeLayout" } public init(reactTag: NSNumber, size: CGSize) { @@ -16,15 +16,15 @@ public class OnNativeLayoutEvent: NSObject, RCTEvent { } public class func moduleDotMethod() -> String { - return "RCTEventEmitter.receiveEvent" + "RCTEventEmitter.receiveEvent" } - + public func canCoalesce() -> Bool { - return false + false } public func arguments() -> [Any] { - return [ + [ viewTag, RCTNormalizeInputEventName(eventName) ?? eventName, [ diff --git a/packages/react-native-bottom-tabs/ios/Events/PageSelectedEvent.swift b/packages/react-native-bottom-tabs/ios/Events/PageSelectedEvent.swift index 25700c01..ff5b79b1 100644 --- a/packages/react-native-bottom-tabs/ios/Events/PageSelectedEvent.swift +++ b/packages/react-native-bottom-tabs/ios/Events/PageSelectedEvent.swift @@ -4,27 +4,27 @@ import React public class PageSelectedEvent: NSObject, RCTEvent { private var key: NSString public var viewTag: NSNumber - + public var eventName: String { - return "onPageSelected" + "onPageSelected" } - + public init(reactTag: NSNumber, key: NSString) { self.viewTag = reactTag self.key = key super.init() } - + public class func moduleDotMethod() -> String { - return "RCTEventEmitter.receiveEvent" + "RCTEventEmitter.receiveEvent" } - + public func canCoalesce() -> Bool { - return false + false } - + public func arguments() -> [Any] { - return [ + [ viewTag, RCTNormalizeInputEventName(eventName) ?? eventName, [ diff --git a/packages/react-native-bottom-tabs/ios/Events/TabBarMeasuredEvent.swift b/packages/react-native-bottom-tabs/ios/Events/TabBarMeasuredEvent.swift index 91d27f4b..d5aa1aef 100644 --- a/packages/react-native-bottom-tabs/ios/Events/TabBarMeasuredEvent.swift +++ b/packages/react-native-bottom-tabs/ios/Events/TabBarMeasuredEvent.swift @@ -4,27 +4,27 @@ import React public class TabBarMeasuredEvent: NSObject, RCTEvent { private var height: NSInteger public var viewTag: NSNumber - + public var eventName: String { - return "onTabBarMeasured" + "onTabBarMeasured" } - + public init(reactTag: NSNumber, height: NSInteger) { self.viewTag = reactTag self.height = height super.init() } - + public class func moduleDotMethod() -> String { - return "RCTEventEmitter.receiveEvent" + "RCTEventEmitter.receiveEvent" } - + public func canCoalesce() -> Bool { - return false + false } - + public func arguments() -> [Any] { - return [ + [ viewTag, RCTNormalizeInputEventName(eventName) ?? eventName, [ diff --git a/packages/react-native-bottom-tabs/ios/Events/TabLongPressedEvent.swift b/packages/react-native-bottom-tabs/ios/Events/TabLongPressedEvent.swift index ac7a0de2..9950e40c 100644 --- a/packages/react-native-bottom-tabs/ios/Events/TabLongPressedEvent.swift +++ b/packages/react-native-bottom-tabs/ios/Events/TabLongPressedEvent.swift @@ -1,6 +1,5 @@ import React - // RCTEvent is not defined for new arch. protocol RCTEvent {} @@ -8,27 +7,27 @@ protocol RCTEvent {} public class TabLongPressEvent: NSObject, RCTEvent { private var key: NSString public var viewTag: NSNumber - + public var eventName: String { - return "onTabLongPress" + "onTabLongPress" } - + public init(reactTag: NSNumber, key: NSString) { self.viewTag = reactTag self.key = key super.init() } - + public class func moduleDotMethod() -> String { - return "RCTEventEmitter.receiveEvent" + "RCTEventEmitter.receiveEvent" } - + public func canCoalesce() -> Bool { - return false + false } - + public func arguments() -> [Any] { - return [ + [ viewTag, RCTNormalizeInputEventName(eventName) ?? eventName, [ diff --git a/packages/react-native-bottom-tabs/ios/Extensions.swift b/packages/react-native-bottom-tabs/ios/Extensions.swift index 454391ac..fed3dd23 100644 --- a/packages/react-native-bottom-tabs/ios/Extensions.swift +++ b/packages/react-native-bottom-tabs/ios/Extensions.swift @@ -11,7 +11,7 @@ import UIKit extension Collection { // Returns the element at the specified index if it is within bounds, otherwise nil. subscript(safe index: Index) -> Element? { - return indices.contains(index) ? self[index] : nil + indices.contains(index) ? self[index] : nil } } @@ -19,7 +19,7 @@ extension Collection where Element == TabInfo { func findByKey(_ key: String?) -> Element? { guard let key else { return nil } guard !isEmpty else { return nil } - return first(where: { $0.key == key }) + return first { $0.key == key } } } @@ -37,17 +37,16 @@ extension PlatformView { extension PlatformImage { func resizeImageTo(size: CGSize) -> PlatformImage? { #if os(macOS) - let newImage = NSImage(size: size, flipped: false) { (rect) -> Bool in + return NSImage(size: size, flipped: false) { rect -> Bool in self.draw(in: rect, from: CGRect(origin: .zero, size: self.size), operation: .copy, fraction: 1.0) return true } - return newImage #else let renderer = UIGraphicsImageRenderer(size: size) - return renderer.image { context in + return renderer.image { _ in self.draw(in: CGRect(origin: .zero, size: size)) } #endif @@ -55,7 +54,6 @@ extension PlatformImage { } extension View { - #if os(macOS) @MainActor @ViewBuilder @@ -82,13 +80,12 @@ extension View { #endif } #endif - - + @MainActor @ViewBuilder func measureView(onLayout: @escaping (_ size: CGSize) -> Void) -> some View { self - .background ( + .background( GeometryReader { geometry in Color.clear .onChange(of: geometry.size) { newValue in diff --git a/packages/react-native-bottom-tabs/ios/RepresentableView.swift b/packages/react-native-bottom-tabs/ios/RepresentableView.swift index d4418a64..384227ee 100644 --- a/packages/react-native-bottom-tabs/ios/RepresentableView.swift +++ b/packages/react-native-bottom-tabs/ios/RepresentableView.swift @@ -5,22 +5,22 @@ import SwiftUI */ struct RepresentableView: PlatformViewRepresentable { var view: PlatformView - + #if os(macOS) - + func makeNSView(context: Context) -> PlatformView { - return view + view } - + func updateNSView(_ nsView: PlatformView, context: Context) {} - + #else - + func makeUIView(context: Context) -> PlatformView { - return view + view } - + func updateUIView(_ uiView: PlatformView, context: Context) {} - + #endif } diff --git a/packages/react-native-bottom-tabs/ios/TabItem.swift b/packages/react-native-bottom-tabs/ios/TabItem.swift index f58a81cd..3866c59b 100644 --- a/packages/react-native-bottom-tabs/ios/TabItem.swift +++ b/packages/react-native-bottom-tabs/ios/TabItem.swift @@ -5,7 +5,7 @@ struct TabItem: View { var icon: PlatformImage? var sfSymbol: String? var labeled: Bool? - + var body: some View { if let icon { #if os(macOS) @@ -17,7 +17,7 @@ struct TabItem: View { Image(systemName: sfSymbol) .noneSymbolVariant() } - if (labeled != false) { + if labeled != false { Text(title ?? "") } } diff --git a/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift b/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift index 30cb7fa9..d298ad3b 100644 --- a/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift +++ b/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift @@ -7,8 +7,8 @@ import UIKit #if !os(macOS) && !os(visionOS) private final class TabBarDelegate: NSObject, UITabBarControllerDelegate { - var onClick: ((_ index: Int) -> Void)? = nil - + var onClick: ((_ index: Int) -> Void)? + func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { #if os(iOS) // Handle "More" Tab @@ -16,7 +16,7 @@ private final class TabBarDelegate: NSObject, UITabBarControllerDelegate { return true } #endif - + if let index = tabBarController.viewControllers?.firstIndex(of: viewController) { onClick?(index) } @@ -27,39 +27,39 @@ private final class TabBarDelegate: NSObject, UITabBarControllerDelegate { struct TabItemEventModifier: ViewModifier { let onTabEvent: (_ key: Int, _ isLongPress: Bool) -> Void private let delegate = TabBarDelegate() - + func body(content: Content) -> some View { content - .introspectTabView(closure: { tabController in + .introspectTabView { tabController in handle(tabController: tabController) - }) + } } - + func handle(tabController: UITabBarController) { delegate.onClick = { index in onTabEvent(index, false) } tabController.delegate = delegate - + // Don't register gesutre recognizer more than one time if objc_getAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler) != nil { return } - + // Remove existing long press gestures if let existingGestures = tabController.tabBar.gestureRecognizers { for gesture in existingGestures where gesture is UILongPressGestureRecognizer { tabController.tabBar.removeGestureRecognizer(gesture) } } - + // Create gesture handler let handler = LongPressGestureHandler(tabBar: tabController.tabBar, handler: onTabEvent) let gesture = UILongPressGestureRecognizer(target: handler, action: #selector(LongPressGestureHandler.handleLongPress(_:))) gesture.minimumPressDuration = 0.5 - + objc_setAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler, handler, .OBJC_ASSOCIATION_RETAIN) - + tabController.tabBar.addGestureRecognizer(gesture) } } @@ -71,22 +71,22 @@ private struct AssociatedKeys { private class LongPressGestureHandler: NSObject { private weak var tabBar: UITabBar? private let handler: (Int, Bool) -> Void - + init(tabBar: UITabBar, handler: @escaping (Int, Bool) -> Void) { self.tabBar = tabBar self.handler = handler super.init() } - + @objc func handleLongPress(_ recognizer: UILongPressGestureRecognizer) { guard recognizer.state == .began, - let tabBar = tabBar else { return } - + let tabBar else { return } + let location = recognizer.location(in: tabBar) - + // Get buttons and sort them by frames - let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).contains("UITabBarButton") }.sorted(by: { $0.frame.minX < $1.frame.minX }) - + let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).contains("UITabBarButton") }.sorted { $0.frame.minX < $1.frame.minX } + for (index, button) in tabBarButtons.enumerated() { if button.frame.contains(location) { handler(index, true) @@ -94,7 +94,7 @@ private class LongPressGestureHandler: NSObject { } } } - + deinit { if let tabBar { objc_setAssociatedObject(tabBar, &AssociatedKeys.gestureHandler, nil, .OBJC_ASSOCIATION_RETAIN) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 9cc1b10c..f87934f3 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -1,9 +1,8 @@ import Foundation -import SwiftUI import React +import SwiftUI @_spi(Advanced) import SwiftUIIntrospect - /** SwiftUI implementation of TabView used to render React Native views. */ @@ -25,12 +24,12 @@ struct TabViewImpl: View { ForEach(props.children.indices, id: \.self) { index in renderTabItem(at: index) } - .measureView(onLayout: { size in + .measureView { size in onLayout(size) - }) + } } #if !os(tvOS) && !os(macOS) && !os(visionOS) - .onTabItemEvent({ index, isLongPress in + .onTabItemEvent { index, isLongPress in let item = props.filteredItems[safe: index] guard let key = item?.key else { return } @@ -41,20 +40,20 @@ struct TabViewImpl: View { onSelect(key) emitHapticFeedback() } - }) + } #endif - .introspectTabView(closure: { tabController in + .introspectTabView { tabController in #if os(macOS) tabBar = tabController #else tabBar = tabController.tabBar - if (!props.tabBarHidden) { + if !props.tabBarHidden { onTabBarMeasured( Int(tabController.tabBar.frame.size.height) ) } #endif - }) + } #if !os(macOS) .configureAppearance(props: props, tabBar: tabBar) #endif @@ -62,7 +61,7 @@ struct TabViewImpl: View { .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) .onChange(of: props.selectedPage ?? "") { newValue in #if !os(macOS) - if (props.disablePageAnimations) { + if props.disablePageAnimations { UIView.setAnimationsEnabled(false) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { UIView.setAnimationsEnabled(true) @@ -152,7 +151,6 @@ private func createFontAttributes( ) -> [NSAttributedString.Key: Any] { var attributes: [NSAttributedString.Key: Any] = [:] - if family != nil || weight != nil { attributes[.font] = RCTFont.update( nil, @@ -176,7 +174,6 @@ let tabBarDefaultFontSize: CGFloat = 30.0 let tabBarDefaultFontSize: CGFloat = UIFont.smallSystemFontSize #endif - #if !os(macOS) private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) { tabBar.barTintColor = props.barTintColor @@ -257,7 +254,7 @@ extension View { @ViewBuilder func getSidebarAdaptable(enabled: Bool) -> some View { if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) { - if (enabled) { + if enabled { #if compiler(>=6.0) self.tabViewStyle(.sidebarAdaptable) #else @@ -274,7 +271,7 @@ extension View { @ViewBuilder func tabBadge(_ data: String?) -> some View { if #available(iOS 15.0, macOS 15.0, visionOS 2.0, tvOS 15.0, *) { - if let data = data, !data.isEmpty { + if let data, !data.isEmpty { #if !os(tvOS) self.badge(data) #else @@ -292,28 +289,28 @@ extension View { @ViewBuilder func configureAppearance(props: TabViewProps, tabBar: UITabBar?) -> some View { self - .onChange(of: props.barTintColor) { newValue in + .onChange(of: props.barTintColor) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } - .onChange(of: props.scrollEdgeAppearance) { newValue in + .onChange(of: props.scrollEdgeAppearance) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } - .onChange(of: props.translucent) { newValue in + .onChange(of: props.translucent) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } - .onChange(of: props.inactiveTintColor) { newValue in + .onChange(of: props.inactiveTintColor) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } - .onChange(of: props.selectedActiveTintColor) { newValue in + .onChange(of: props.selectedActiveTintColor) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } - .onChange(of: props.fontSize) { newValue in + .onChange(of: props.fontSize) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } - .onChange(of: props.fontFamily) { newValue in + .onChange(of: props.fontFamily) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } - .onChange(of: props.fontWeight) { newValue in + .onChange(of: props.fontWeight) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } .onChange(of: props.tabBarHidden) { newValue in diff --git a/packages/react-native-bottom-tabs/ios/TabViewProps.swift b/packages/react-native-bottom-tabs/ios/TabViewProps.swift index f38ee90b..131c7cfb 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProps.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProps.swift @@ -23,7 +23,7 @@ class TabViewProps: ObservableObject { @Published var tabBarHidden: Bool = false var selectedActiveTintColor: PlatformColor? { - if let selectedPage = selectedPage, + if let selectedPage, let tabData = items.findByKey(selectedPage), let activeTintColor = tabData.activeTintColor { return activeTintColor @@ -33,8 +33,8 @@ class TabViewProps: ObservableObject { } var filteredItems: [TabInfo] { - items.filter({ + items.filter { !$0.hidden || $0.key == selectedPage - }) + } } } diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index add46dfe..135c9a95 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -1,8 +1,8 @@ import Foundation -import SwiftUI import React import SDWebImage import SDWebImageSVGCoder +import SwiftUI @objcMembers public final class TabInfo: NSObject { @@ -72,7 +72,6 @@ public final class TabInfo: NSObject { } } - @objc public var disablePageAnimations: Bool = false { didSet { props.disablePageAnimations = disablePageAnimations @@ -171,17 +170,17 @@ public final class TabInfo: NSObject { SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared) } - public override func didUpdateReactSubviews() { + override public func didUpdateReactSubviews() { props.children = reactSubviews() } #if os(macOS) - public override func layout() { + override public func layout() { super.layout() setupView() } #else - public override func layoutSubviews() { + override public func layoutSubviews() { super.layoutSubviews() setupView() } @@ -219,7 +218,7 @@ public final class TabInfo: NSObject { guard let imageSources = icons as? [RCTImageSource?] else { return } for (index, imageSource) in imageSources.enumerated() { - guard let imageSource = imageSource, + guard let imageSource, let url = imageSource.request.url else { continue } let isSVG = url.pathExtension.lowercased() == "svg" @@ -243,8 +242,8 @@ public final class TabInfo: NSObject { options: options, context: context, progress: nil - ) { [weak self] (image, _, _, _, _, _) in - guard let self = self else { return } + ) { [weak self] image, _, _, _, _, _ in + guard let self else { return } DispatchQueue.main.async { if let image { self.props.icons[index] = image.resizeImageTo(size: self.iconSize)