├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Example ├── FSPopoverView.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── FSPopoverView-Example.xcscheme ├── FSPopoverView.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── FSPopoverView │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Examples │ │ ├── CustomizationViewController.swift │ │ ├── ListViewController.swift │ │ └── MenuViewController.swift │ ├── Feature.swift │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon-1024.png │ │ │ ├── icon-20@2x.png │ │ │ ├── icon-20@3x.png │ │ │ ├── icon-29@2x.png │ │ │ ├── icon-29@3x.png │ │ │ ├── icon-38@2x.png │ │ │ ├── icon-38@3x.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-64@2x.png │ │ │ ├── icon-64@3x.png │ │ │ ├── icon-68@2x.png │ │ │ ├── icon-76@2x.png │ │ │ └── icon-83.5@2x.png │ │ ├── Contents.json │ │ ├── avatar.imageset │ │ │ ├── Contents.json │ │ │ ├── avatar@2x.png │ │ │ └── avatar@3x.png │ │ ├── bubble_left.imageset │ │ │ ├── Contents.json │ │ │ ├── bubble_left@2x.png │ │ │ └── bubble_left@3x.png │ │ ├── bubble_right.imageset │ │ │ ├── Contents.json │ │ │ ├── bubble_right@2x.png │ │ │ └── bubble_right@3x.png │ │ ├── copy.imageset │ │ │ ├── Contents.json │ │ │ ├── copy_dark@2x.png │ │ │ ├── copy_dark@3x.png │ │ │ ├── copy_light@2x.png │ │ │ └── copy_light@3x.png │ │ ├── db.imageset │ │ │ ├── Contents.json │ │ │ ├── db_dark@2x.png │ │ │ ├── db_dark@3x.png │ │ │ ├── db_light@2x.png │ │ │ └── db_light@3x.png │ │ ├── delete.imageset │ │ │ ├── Contents.json │ │ │ ├── delete_dark@2x.png │ │ │ ├── delete_dark@3x.png │ │ │ ├── delete_light@2x.png │ │ │ └── delete_light@3x.png │ │ ├── forward.imageset │ │ │ ├── Contents.json │ │ │ ├── forward_dark@2x.png │ │ │ ├── forward_dark@3x.png │ │ │ ├── forward_light@2x.png │ │ │ └── forward_light@3x.png │ │ ├── message.imageset │ │ │ ├── Contents.json │ │ │ ├── message_dark@2x.png │ │ │ ├── message_dark@3x.png │ │ │ ├── message_light@2x.png │ │ │ └── message_light@3x.png │ │ ├── qr.imageset │ │ │ ├── Contents.json │ │ │ ├── qr_dark@2x.png │ │ │ ├── qr_dark@3x.png │ │ │ ├── qr_light@2x.png │ │ │ └── qr_light@3x.png │ │ ├── quote.imageset │ │ │ ├── Contents.json │ │ │ ├── quote_dark@2x.png │ │ │ ├── quote_dark@3x.png │ │ │ ├── quote_light@2x.png │ │ │ └── quote_light@3x.png │ │ ├── selected.imageset │ │ │ ├── Contents.json │ │ │ ├── selected@2x.png │ │ │ └── selected@3x.png │ │ ├── settings.imageset │ │ │ ├── Contents.json │ │ │ ├── settings_dark@2x.png │ │ │ ├── settings_dark@3x.png │ │ │ ├── settings_light@2x.png │ │ │ └── settings_light@3x.png │ │ ├── translate.imageset │ │ │ ├── Contents.json │ │ │ ├── translate_dark@2x.png │ │ │ ├── translate_dark@3x.png │ │ │ ├── translate_light@2x.png │ │ │ └── translate_light@3x.png │ │ └── unselected.imageset │ │ │ ├── Contents.json │ │ │ ├── unselected_dark@2x.png │ │ │ ├── unselected_dark@3x.png │ │ │ ├── unselected_light@2x.png │ │ │ └── unselected_light@3x.png │ ├── Info.plist │ └── ViewController.swift ├── Podfile ├── Podfile.lock └── Pods │ ├── Local Podspecs │ └── FSPopoverView.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── FSPopoverView.xcscheme │ └── Target Support Files │ ├── FSPopoverView │ ├── FSPopoverView-Info.plist │ ├── FSPopoverView-dummy.m │ ├── FSPopoverView-prefix.pch │ ├── FSPopoverView-umbrella.h │ ├── FSPopoverView.debug.xcconfig │ ├── FSPopoverView.modulemap │ ├── FSPopoverView.release.xcconfig │ ├── ResourceBundle-Alamofire-FSPopoverView-Info.plist │ └── ResourceBundle-FSPopoverView-FSPopoverView-Info.plist │ └── Pods-FSPopoverView_Example │ ├── Pods-FSPopoverView_Example-Info.plist │ ├── Pods-FSPopoverView_Example-acknowledgements.markdown │ ├── Pods-FSPopoverView_Example-acknowledgements.plist │ ├── Pods-FSPopoverView_Example-dummy.m │ ├── Pods-FSPopoverView_Example-frameworks.sh │ ├── Pods-FSPopoverView_Example-umbrella.h │ ├── Pods-FSPopoverView_Example.debug.xcconfig │ ├── Pods-FSPopoverView_Example.modulemap │ └── Pods-FSPopoverView_Example.release.xcconfig ├── FSPopoverView.podspec ├── LICENSE ├── Package.swift ├── README.md ├── README_CN.md ├── Screenshots ├── custom.PNG ├── custom_item.PNG ├── list_dark.PNG ├── list_light.PNG └── menu.PNG └── Source ├── Assets ├── .gitkeep └── PrivacyInfo.xcprivacy └── Classes ├── .gitkeep ├── Appearance ├── FSPopoverView+Appearance.swift └── FSPopoverViewAppearance.swift ├── FSPopoverView.swift ├── FSPopoverViewDataSource.swift ├── Internal ├── Draw │ ├── FSPopoverDrawContext.swift │ ├── FSPopoverDrawDown.swift │ ├── FSPopoverDrawLeft.swift │ ├── FSPopoverDrawRight.swift │ ├── FSPopoverDrawUp.swift │ ├── FSPopoverDrawable.swift │ └── FSPopoverDrawer.swift ├── Extensions │ ├── CoreGraphicsExtensions.swift │ ├── NSAttributedString+FSPopoverView.swift │ ├── UIColor+FSPopoverView.swift │ └── UIView+FSPopoverView.swift ├── InternalNamespace.swift ├── Tools │ └── FSPopoverViewDelegateRouter.swift └── Views │ └── _FSSeparatorView.swift ├── List ├── Cell │ └── FSPopoverListCell.swift ├── FSPopoverListView.swift └── Item │ ├── Defaults │ └── FSPopoverListTextItem.swift │ └── FSPopoverListItem.swift └── Transition ├── Defaults ├── FSPopoverViewTransitionFade.swift ├── FSPopoverViewTransitionScale.swift └── FSPopoverViewTransitionTranslate.swift ├── FSPopoverViewAnimatedTransitioning.swift └── FSPopoverViewTransitionContext.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | BuildIPA/ 9 | Carthage/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xcuserstate 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSY 31 | .DS_Store 32 | account.plist 33 | xcodebuild.log 34 | report.html 35 | compile_commands.json 36 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/FSPopoverView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/FSPopoverView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FSPopoverView.xcodeproj/xcshareddata/xcschemes/FSPopoverView-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Example/FSPopoverView.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/FSPopoverView.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FSPopoverView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 04/02/2022. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 17 | return true 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Examples/CustomizationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomizationViewController.swift 3 | // FSPopoverView_Example 4 | // 5 | // Created by Sheng on 2023/12/2. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FSPopoverView 11 | 12 | class CustomizationViewController: UIViewController { 13 | 14 | private var hiddenArrow = false 15 | private var showsDimBackground = false 16 | private var shouldDismissOnTapOutside = true 17 | private var transition: FSPopoverViewAnimatedTransitioning? 18 | private weak var transitioning: FSPopoverViewAnimatedTransitioning? 19 | 20 | private lazy var ghostView: UIView = { 21 | let label = UILabel() 22 | label.text = "👻" 23 | label.font = .systemFont(ofSize: 120.0) 24 | label.textAlignment = .center 25 | return label 26 | }() 27 | 28 | private func p_createPopoverView() -> FSPopoverView { 29 | let popoverView = FSPopoverView() 30 | popoverView.dataSource = self 31 | popoverView.showsArrow = !hiddenArrow 32 | popoverView.showsDimBackground = showsDimBackground 33 | popoverView.transitioningDelegate = transitioning 34 | do { 35 | // border 36 | // popoverView.borderWidth = 1.0 37 | // popoverView.borderColor = .red 38 | } 39 | do { 40 | // shadow 41 | // popoverView.shadowColor = .green 42 | // popoverView.shadowRadius = 3.0 43 | // popoverView.shadowOpacity = 0.6 44 | } 45 | return popoverView 46 | } 47 | 48 | override func viewDidLoad() { 49 | super.viewDidLoad() 50 | transition = FSPopoverViewTransitionScale() 51 | transitioning = transition 52 | } 53 | 54 | @IBAction func changeHiddenArrow(_ sender: UISwitch) { 55 | hiddenArrow = sender.isOn ? true : false 56 | } 57 | 58 | @IBAction func changeShowsDimBackground(_ sender: UISwitch) { 59 | showsDimBackground = sender.isOn ? true : false 60 | } 61 | 62 | @IBAction func changeDismissOnTapOutside(_ sender: UISwitch) { 63 | shouldDismissOnTapOutside = sender.isOn ? true : false 64 | } 65 | 66 | @IBAction func onDidTapRightBarItem(_ sender: UIBarButtonItem) { 67 | let popoverView = p_createPopoverView() 68 | popoverView.present(fromBarItem: sender) 69 | } 70 | 71 | @IBAction func onDidTapPrecentButton(_ sender: UIButton) { 72 | let popoverView = p_createPopoverView() 73 | popoverView.present(fromRect: sender.frame.insetBy(dx: 0.0, dy: -6.0), in: view, displayIn: view) 74 | } 75 | 76 | @IBAction func onDidTapPrecentDownButton(_ sender: UIButton) { 77 | let popoverView = p_createPopoverView() 78 | popoverView.arrowDirection = .down 79 | popoverView.autosetsArrowDirection = false 80 | popoverView.present(fromView: sender) 81 | } 82 | 83 | @IBAction func onDidTapPrecentUpButton(_ sender: UIButton) { 84 | let popoverView = p_createPopoverView() 85 | popoverView.arrowDirection = .up 86 | popoverView.autosetsArrowDirection = false 87 | popoverView.present(fromView: sender, displayIn: view) 88 | } 89 | 90 | @IBAction func onDidTapChangeTransitionButton(_ sender: UIButton) { 91 | let items: [FSPopoverListItem] = ["Scale", "Fade", "Translate", "Custom", "None"].map { text in 92 | let item = FSPopoverListTextItem() 93 | item.title = text 94 | item.isSeparatorHidden = false 95 | item.contentInset = .init(top: 8.0, left: 18.0, bottom: 8.0, right: 18.0) 96 | item.separatorInset = item.contentInset 97 | item.selectedHandler = { [unowned self] item in 98 | guard let item = item as? FSPopoverListTextItem else { return } 99 | switch item.title { 100 | case "Scale": 101 | self.transition = FSPopoverViewTransitionScale() 102 | self.transitioning = self.transition 103 | case "Fade": 104 | self.transition = FSPopoverViewTransitionFade() 105 | self.transitioning = self.transition 106 | case "Translate": 107 | self.transition = FSPopoverViewTransitionTranslate() 108 | self.transitioning = self.transition 109 | case "Custom": 110 | /// ⚠️ Here can not set `transition` to `self`, or it will cause 111 | /// a memory leak. Because `self` retains `transition`. 112 | self.transition = nil 113 | self.transitioning = self 114 | case "None": 115 | self.transition = nil 116 | self.transitioning = nil 117 | default: 118 | self.transition = FSPopoverViewTransitionScale() 119 | self.transitioning = self.transition 120 | } 121 | } 122 | item.updateLayout() 123 | return item 124 | } 125 | items.last?.isSeparatorHidden = true 126 | 127 | let listView = FSPopoverListView(scrollDirection: .vertical) 128 | listView.items = items 129 | listView.showsArrow = !hiddenArrow 130 | listView.arrowDirection = .down 131 | listView.showsDimBackground = showsDimBackground 132 | listView.transitioningDelegate = transitioning 133 | listView.autosetsArrowDirection = false 134 | listView.shouldDismissOnTapOutside = shouldDismissOnTapOutside 135 | listView.present(fromRect: sender.frame.insetBy(dx: 0.0, dy: -6.0), in: sender.superview) 136 | } 137 | } 138 | 139 | extension CustomizationViewController: FSPopoverViewDataSource { 140 | 141 | func backgroundView(for popoverView: FSPopoverView) -> UIView? { 142 | let view = UIView() 143 | view.backgroundColor = .yellow 144 | return view 145 | } 146 | 147 | func contentView(for popoverView: FSPopoverView) -> UIView? { 148 | return ghostView 149 | } 150 | 151 | func contentSize(for popoverView: FSPopoverView) -> CGSize { 152 | return ghostView.sizeThatFits(.init(width: 1000.0, height: 1000.0)) 153 | } 154 | 155 | func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets { 156 | var insets = view.safeAreaInsets 157 | insets.left = 10.0 158 | insets.right = 10.0 159 | return insets 160 | } 161 | 162 | func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool { 163 | return shouldDismissOnTapOutside 164 | } 165 | } 166 | 167 | extension CustomizationViewController: FSPopoverViewAnimatedTransitioning { 168 | 169 | func animateTransition(transitionContext context: FSPopoverViewTransitionContext) { 170 | 171 | let popoverView = context.popoverView 172 | let dimBackgroundView = context.dimBackgroundView 173 | 174 | switch context.scene { 175 | case .present: 176 | popoverView.transform = .init(scaleX: 1.1, y: 1.1) 177 | dimBackgroundView.alpha = 0.0 178 | UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { 179 | popoverView.transform = .identity 180 | dimBackgroundView.alpha = 1.0 181 | } completion: { _ in 182 | context.completeTransition() 183 | } 184 | case .dismiss(_): 185 | UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { 186 | popoverView.alpha = 0.0 187 | popoverView.transform = .init(scaleX: 0.9, y: 0.9) 188 | dimBackgroundView.alpha = 0.0 189 | } completion: { _ in 190 | popoverView.alpha = 1.0 191 | popoverView.transform = .identity 192 | dimBackgroundView.alpha = 1.0 193 | context.completeTransition() 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Examples/ListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListViewController.swift 3 | // FSPopoverView_Example 4 | // 5 | // Created by Sheng on 2023/12/3. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FSPopoverView 11 | 12 | class ListViewController: UIViewController { 13 | 14 | private let listView = FSPopoverListView() 15 | 16 | private var patterns = [FSPopoverListItem]() 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | do { 21 | let titles = ["Design", "Factory", "Singleton", "Builder", "Strategy", "Abstract Factory"] 22 | patterns = titles.map { title in 23 | let item = SelectableItem() 24 | item.title = title 25 | item.isSeparatorHidden = false 26 | item.selectedHandler = { item in 27 | if let item = item as? SelectableItem { 28 | item.isSelected = !item.isSelected 29 | item.reload(.rerender) 30 | } 31 | } 32 | item.updateLayout() 33 | return item 34 | } 35 | patterns.last?.isSeparatorHidden = true 36 | } 37 | } 38 | 39 | @IBAction func customListItem(_ sender: UIBarButtonItem) { 40 | listView.items = patterns 41 | listView.dismissWhenSelected = false 42 | listView.present(fromBarItem: sender) 43 | } 44 | 45 | @IBAction func present(_ sender: UIButton) { 46 | let features: [Feature] = [.copy, .message, .db, .qr, .settings] 47 | let items: [FSPopoverListItem] = features.map { feature in 48 | let item = FSPopoverListTextItem() 49 | item.image = feature.image 50 | item.title = feature.title 51 | item.isSeparatorHidden = false 52 | item.selectedHandler = { item in 53 | guard let item = item as? FSPopoverListTextItem else { 54 | return 55 | } 56 | print(item.title ?? "") 57 | } 58 | item.updateLayout() 59 | return item 60 | } 61 | items.last?.isSeparatorHidden = true 62 | listView.items = items 63 | listView.dismissWhenSelected = true 64 | listView.present(fromRect: sender.frame.insetBy(dx: 0.0, dy: -6.0), in: view) 65 | } 66 | } 67 | 68 | // MARK: - Custom List Item 69 | 70 | private class SelectableItem: FSPopoverListItem { 71 | 72 | // MARK: Properties/Fileprivate 73 | 74 | fileprivate var title: String? 75 | 76 | fileprivate var isSelected = false 77 | 78 | // MARK: Properties/Override 79 | 80 | override var cellType: FSPopoverListCell.Type { 81 | return SelectableCell.self 82 | } 83 | 84 | // MARK: Initialization 85 | 86 | init() { 87 | super.init(scrollDirection: .vertical) 88 | selectionStyle = .none 89 | separatorInset = SelectableCell.Consts.contentInset 90 | } 91 | 92 | // MARK: Fileprivate 93 | 94 | /// You need to call this method if you change any contents. 95 | fileprivate func updateLayout() { 96 | var size = CGSize.zero 97 | let imageSize = CGSize(width: 20.0, height: 20.0) 98 | let titleSize = p_titleSize() 99 | let contentInset = SelectableCell.Consts.contentInset 100 | let spacing = FSPopoverView.fs_appearance().spacing 101 | size.height = max(imageSize.height, titleSize?.height ?? 0) 102 | size.height += contentInset.top + contentInset.bottom 103 | size.width += contentInset.left 104 | size.width += imageSize.width 105 | if let width = titleSize?.width { 106 | size.width += spacing 107 | size.width += width 108 | } 109 | size.width += contentInset.right 110 | self.size = size 111 | } 112 | 113 | private func p_titleSize() -> CGSize? { 114 | guard let title = title, !title.isEmpty else { 115 | return nil 116 | } 117 | let titleFont = FSPopoverView.fs_appearance().textFont 118 | let att_string = NSAttributedString(string: title, attributes: [.font: titleFont]) 119 | let size = att_string.boundingRect(with: .init(width: 1000.0, height: 1000.0), 120 | options: [.usesLineFragmentOrigin, .usesFontLeading], 121 | context: nil).size 122 | return .init(width: ceil(size.width), height: ceil(size.height)) 123 | } 124 | } 125 | 126 | private class SelectableCell: FSPopoverListCell { 127 | 128 | // MARK: Consts 129 | 130 | fileprivate struct Consts { 131 | static let contentInset = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 8.0, right: 16.0) 132 | } 133 | 134 | // MARK: Properties/Private 135 | 136 | private let iconButton = UIButton() 137 | private let textLabel = UILabel() 138 | 139 | // MARK: Override 140 | 141 | override func didInitialize() { 142 | super.didInitialize() 143 | 144 | iconButton.isUserInteractionEnabled = false 145 | iconButton.translatesAutoresizingMaskIntoConstraints = false 146 | iconButton.setImage(.init(named: "unselected"), for: .normal) 147 | iconButton.setImage(.init(named: "selected"), for: .selected) 148 | 149 | textLabel.font = FSPopoverView.fs_appearance().textFont 150 | textLabel.textColor = FSPopoverView.fs_appearance().textColor 151 | textLabel.translatesAutoresizingMaskIntoConstraints = false 152 | 153 | addSubview(iconButton) 154 | addSubview(textLabel) 155 | 156 | let inset = Self.Consts.contentInset 157 | let spacing = FSPopoverView.fs_appearance().spacing 158 | addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-left-[icon]-spacing-[text]", 159 | metrics: ["left": inset.left, "spacing": spacing], 160 | views: ["icon": iconButton, "text": textLabel])) 161 | addConstraint(NSLayoutConstraint(item: iconButton, 162 | attribute: .centerY, 163 | relatedBy: .equal, 164 | toItem: self, 165 | attribute: .centerY, 166 | multiplier: 1.0, 167 | constant: 0.0)) 168 | addConstraint(NSLayoutConstraint(item: textLabel, 169 | attribute: .centerY, 170 | relatedBy: .equal, 171 | toItem: self, 172 | attribute: .centerY, 173 | multiplier: 1.0, 174 | constant: 0.0)) 175 | } 176 | 177 | override func renderContents() { 178 | super.renderContents() 179 | guard let item = item as? SelectableItem else { 180 | return 181 | } 182 | textLabel.text = item.title 183 | iconButton.isSelected = item.isSelected 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Examples/MenuViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuViewController.swift 3 | // FSPopoverView_Example 4 | // 5 | // Created by Sheng on 2023/12/3. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FSPopoverView 11 | 12 | class MenuViewController: UITableViewController { 13 | 14 | private let menuView = FSPopoverListView(scrollDirection: .horizontal) 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | do { 19 | let features: [Feature] = [.forward, .delete, .quote, .translate] 20 | let items: [FSPopoverListItem] = features.map { feature in 21 | let item = FSPopoverListTextItem(scrollDirection: .horizontal) 22 | item.image = feature.image 23 | item.title = feature.title 24 | item.isSeparatorHidden = false 25 | item.selectedHandler = { item in 26 | guard let item = item as? FSPopoverListTextItem else { 27 | return 28 | } 29 | print(item.title ?? "") 30 | } 31 | item.updateLayout() 32 | return item 33 | } 34 | items.last?.isSeparatorHidden = true 35 | menuView.items = items 36 | } 37 | } 38 | 39 | @IBAction func longPress(_ sender: UILongPressGestureRecognizer) { 40 | guard sender.state == .began, let label = sender.view else { return } 41 | menuView.present(fromView: label) 42 | } 43 | } 44 | 45 | extension MenuViewController { 46 | 47 | override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 48 | return CGFloat.leastNonzeroMagnitude 49 | } 50 | 51 | override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 52 | return CGFloat.leastNonzeroMagnitude 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Feature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Feature.swift 3 | // FSPopoverView_Example 4 | // 5 | // Created by Sheng on 2023/12/3. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Feature { 12 | 13 | case copy 14 | case message 15 | case db 16 | case qr 17 | case settings 18 | case forward 19 | case delete 20 | case quote 21 | case translate 22 | 23 | var image: UIImage? { 24 | switch self { 25 | case .copy: 26 | return .init(named: "copy") 27 | case .message: 28 | return .init(named: "message") 29 | case .db: 30 | return .init(named: "db") 31 | case .qr: 32 | return .init(named: "qr") 33 | case .settings: 34 | return .init(named: "settings") 35 | case .forward: 36 | return .init(named: "forward") 37 | case .delete: 38 | return .init(named: "delete") 39 | case .quote: 40 | return .init(named: "quote") 41 | case .translate: 42 | return .init(named: "translate") 43 | } 44 | } 45 | 46 | var title: String { 47 | switch self { 48 | case .copy: 49 | return "Copy Contents" 50 | case .message: 51 | return "Message" 52 | case .db: 53 | return "DataBase" 54 | case .qr: 55 | return "QR Code" 56 | case .settings: 57 | return "Settings" 58 | case .forward: 59 | return "Forward" 60 | case .delete: 61 | return "Delete" 62 | case .quote: 63 | return "Quote" 64 | case .translate: 65 | return "Translate" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-20@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "scale" : "2x", 8 | "size" : "20x20" 9 | }, 10 | { 11 | "filename" : "icon-20@3x.png", 12 | "idiom" : "universal", 13 | "platform" : "ios", 14 | "scale" : "3x", 15 | "size" : "20x20" 16 | }, 17 | { 18 | "filename" : "icon-29@2x.png", 19 | "idiom" : "universal", 20 | "platform" : "ios", 21 | "scale" : "2x", 22 | "size" : "29x29" 23 | }, 24 | { 25 | "filename" : "icon-29@3x.png", 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "scale" : "3x", 29 | "size" : "29x29" 30 | }, 31 | { 32 | "filename" : "icon-38@2x.png", 33 | "idiom" : "universal", 34 | "platform" : "ios", 35 | "scale" : "2x", 36 | "size" : "38x38" 37 | }, 38 | { 39 | "filename" : "icon-38@3x.png", 40 | "idiom" : "universal", 41 | "platform" : "ios", 42 | "scale" : "3x", 43 | "size" : "38x38" 44 | }, 45 | { 46 | "filename" : "icon-40@2x.png", 47 | "idiom" : "universal", 48 | "platform" : "ios", 49 | "scale" : "2x", 50 | "size" : "40x40" 51 | }, 52 | { 53 | "filename" : "icon-40@3x.png", 54 | "idiom" : "universal", 55 | "platform" : "ios", 56 | "scale" : "3x", 57 | "size" : "40x40" 58 | }, 59 | { 60 | "filename" : "icon-60@2x.png", 61 | "idiom" : "universal", 62 | "platform" : "ios", 63 | "scale" : "2x", 64 | "size" : "60x60" 65 | }, 66 | { 67 | "filename" : "icon-60@3x.png", 68 | "idiom" : "universal", 69 | "platform" : "ios", 70 | "scale" : "3x", 71 | "size" : "60x60" 72 | }, 73 | { 74 | "filename" : "icon-64@2x.png", 75 | "idiom" : "universal", 76 | "platform" : "ios", 77 | "scale" : "2x", 78 | "size" : "64x64" 79 | }, 80 | { 81 | "filename" : "icon-64@3x.png", 82 | "idiom" : "universal", 83 | "platform" : "ios", 84 | "scale" : "3x", 85 | "size" : "64x64" 86 | }, 87 | { 88 | "filename" : "icon-68@2x.png", 89 | "idiom" : "universal", 90 | "platform" : "ios", 91 | "scale" : "2x", 92 | "size" : "68x68" 93 | }, 94 | { 95 | "filename" : "icon-76@2x.png", 96 | "idiom" : "universal", 97 | "platform" : "ios", 98 | "scale" : "2x", 99 | "size" : "76x76" 100 | }, 101 | { 102 | "filename" : "icon-83.5@2x.png", 103 | "idiom" : "universal", 104 | "platform" : "ios", 105 | "scale" : "2x", 106 | "size" : "83.5x83.5" 107 | }, 108 | { 109 | "filename" : "icon-1024.png", 110 | "idiom" : "universal", 111 | "platform" : "ios", 112 | "size" : "1024x1024" 113 | } 114 | ], 115 | "info" : { 116 | "author" : "xcode", 117 | "version" : 1 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-38@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-38@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-64@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-64@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-68@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/avatar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "avatar@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "avatar@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/avatar.imageset/avatar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/avatar.imageset/avatar@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/avatar.imageset/avatar@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/avatar.imageset/avatar@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/bubble_left.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "bubble_left@2x.png", 9 | "idiom" : "universal", 10 | "resizing" : { 11 | "cap-insets" : { 12 | "bottom" : 24, 13 | "left" : 23, 14 | "right" : 24, 15 | "top" : 23 16 | }, 17 | "center" : { 18 | "height" : 1, 19 | "mode" : "tile", 20 | "width" : 1 21 | }, 22 | "mode" : "9-part" 23 | }, 24 | "scale" : "2x" 25 | }, 26 | { 27 | "filename" : "bubble_left@3x.png", 28 | "idiom" : "universal", 29 | "resizing" : { 30 | "cap-insets" : { 31 | "bottom" : 33, 32 | "left" : 34, 33 | "right" : 33, 34 | "top" : 32 35 | }, 36 | "center" : { 37 | "height" : 1, 38 | "mode" : "tile", 39 | "width" : 1 40 | }, 41 | "mode" : "9-part" 42 | }, 43 | "scale" : "3x" 44 | } 45 | ], 46 | "info" : { 47 | "author" : "xcode", 48 | "version" : 1 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/bubble_left.imageset/bubble_left@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/bubble_left.imageset/bubble_left@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/bubble_left.imageset/bubble_left@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/bubble_left.imageset/bubble_left@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/bubble_right.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "bubble_right@2x.png", 9 | "idiom" : "universal", 10 | "resizing" : { 11 | "cap-insets" : { 12 | "bottom" : 24, 13 | "left" : 23, 14 | "right" : 24, 15 | "top" : 23 16 | }, 17 | "center" : { 18 | "height" : 1, 19 | "mode" : "tile", 20 | "width" : 1 21 | }, 22 | "mode" : "9-part" 23 | }, 24 | "scale" : "2x" 25 | }, 26 | { 27 | "filename" : "bubble_right@3x.png", 28 | "idiom" : "universal", 29 | "resizing" : { 30 | "cap-insets" : { 31 | "bottom" : 36, 32 | "left" : 38, 33 | "right" : 32, 34 | "top" : 35 35 | }, 36 | "center" : { 37 | "height" : 1, 38 | "mode" : "tile", 39 | "width" : 2 40 | }, 41 | "mode" : "9-part" 42 | }, 43 | "scale" : "3x" 44 | } 45 | ], 46 | "info" : { 47 | "author" : "xcode", 48 | "version" : 1 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/bubble_right.imageset/bubble_right@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/bubble_right.imageset/bubble_right@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/bubble_right.imageset/bubble_right@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/bubble_right.imageset/bubble_right@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/copy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "copy_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "copy_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "copy_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "copy_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/copy.imageset/copy_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/copy.imageset/copy_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/copy.imageset/copy_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/copy.imageset/copy_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/copy.imageset/copy_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/copy.imageset/copy_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/copy.imageset/copy_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/copy.imageset/copy_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/db.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "db_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "db_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "db_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "db_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/db.imageset/db_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/db.imageset/db_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/db.imageset/db_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/db.imageset/db_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/db.imageset/db_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/db.imageset/db_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/db.imageset/db_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/db.imageset/db_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/delete.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "delete_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "delete_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "delete_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "delete_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/delete.imageset/delete_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/delete.imageset/delete_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/delete.imageset/delete_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/delete.imageset/delete_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/delete.imageset/delete_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/delete.imageset/delete_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/delete.imageset/delete_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/delete.imageset/delete_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/forward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "forward_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "forward_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "forward_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "forward_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/forward.imageset/forward_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/forward.imageset/forward_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/forward.imageset/forward_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/forward.imageset/forward_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/forward.imageset/forward_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/forward.imageset/forward_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/forward.imageset/forward_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/forward.imageset/forward_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/message.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "message_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "message_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "message_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "message_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/message.imageset/message_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/message.imageset/message_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/message.imageset/message_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/message.imageset/message_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/message.imageset/message_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/message.imageset/message_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/message.imageset/message_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/message.imageset/message_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/qr.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "qr_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "qr_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "qr_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "qr_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/qr.imageset/qr_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/qr.imageset/qr_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/qr.imageset/qr_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/qr.imageset/qr_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/qr.imageset/qr_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/qr.imageset/qr_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/qr.imageset/qr_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/qr.imageset/qr_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/quote.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "quote_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "quote_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "quote_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "quote_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/quote.imageset/quote_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/quote.imageset/quote_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/quote.imageset/quote_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/quote.imageset/quote_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/quote.imageset/quote_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/quote.imageset/quote_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/quote.imageset/quote_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/quote.imageset/quote_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "selected@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "selected@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/selected.imageset/selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/selected.imageset/selected@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/selected.imageset/selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/selected.imageset/selected@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "settings_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "settings_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "settings_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "settings_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/settings.imageset/settings_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/settings.imageset/settings_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/settings.imageset/settings_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/settings.imageset/settings_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/settings.imageset/settings_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/settings.imageset/settings_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/settings.imageset/settings_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/settings.imageset/settings_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/translate.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "translate_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "translate_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "translate_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "translate_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/translate.imageset/translate_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/translate.imageset/translate_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/translate.imageset/translate_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/translate.imageset/translate_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/translate.imageset/translate_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/translate.imageset/translate_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/translate.imageset/translate_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/translate.imageset/translate_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/unselected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "unselected_light@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "unselected_dark@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "unselected_light@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "unselected_dark@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/unselected.imageset/unselected_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/unselected.imageset/unselected_dark@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/unselected.imageset/unselected_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/unselected.imageset/unselected_dark@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/unselected.imageset/unselected_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/unselected.imageset/unselected_light@2x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Images.xcassets/unselected.imageset/unselected_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Example/FSPopoverView/Images.xcassets/unselected.imageset/unselected_light@3x.png -------------------------------------------------------------------------------- /Example/FSPopoverView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleSignature 6 | ???? 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FSPopoverView/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 04/02/2022. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UITableViewController { 12 | 13 | override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 14 | return CGFloat.leastNonzeroMagnitude 15 | } 16 | 17 | override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 18 | return CGFloat.leastNonzeroMagnitude 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform:ios, '12.0' 2 | use_frameworks! 3 | 4 | target 'FSPopoverView_Example' do 5 | pod 'FSPopoverView', :path => '../' 6 | end 7 | 8 | post_install do |installer| 9 | installer.pods_project.root_object.attributes["ORGANIZATIONNAME"] = "Sheng" 10 | installer.pods_project.targets.each do |target| 11 | if target.platform_name == :ios then 12 | target.build_configurations.each do |config| 13 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 14 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FSPopoverView (3.1.3) 3 | 4 | DEPENDENCIES: 5 | - FSPopoverView (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | FSPopoverView: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | FSPopoverView: 1ec986e1061ede07782fbc49396afd263423d85a 13 | 14 | PODFILE CHECKSUM: 4050e8f4b7f2ae257551e677332cd97d0f0c9aaa 15 | 16 | COCOAPODS: 1.15.2 17 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/FSPopoverView.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FSPopoverView", 3 | "version": "3.1.3", 4 | "summary": "`FSPopoverView` is an iOS customizable view that displays a popover view.", 5 | "homepage": "https://github.com/lifution/FSPopoverView", 6 | "authors": "Sheng", 7 | "license": { 8 | "type": "MIT", 9 | "file": "LICENSE" 10 | }, 11 | "source": { 12 | "git": "https://github.com/lifution/FSPopoverView.git", 13 | "tag": "3.1.3" 14 | }, 15 | "requires_arc": true, 16 | "swift_versions": "5", 17 | "platforms": { 18 | "ios": "12.0" 19 | }, 20 | "frameworks": [ 21 | "UIKit", 22 | "Foundation", 23 | "CoreGraphics" 24 | ], 25 | "source_files": "Source/**/*.swift", 26 | "resource_bundles": { 27 | "FSPopoverView": [ 28 | "Source/Assets/PrivacyInfo.xcprivacy" 29 | ] 30 | }, 31 | "swift_version": "5" 32 | } 33 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FSPopoverView (3.1.3) 3 | 4 | DEPENDENCIES: 5 | - FSPopoverView (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | FSPopoverView: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | FSPopoverView: 1ec986e1061ede07782fbc49396afd263423d85a 13 | 14 | PODFILE CHECKSUM: 4050e8f4b7f2ae257551e677332cd97d0f0c9aaa 15 | 16 | COCOAPODS: 1.15.2 17 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/FSPopoverView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/FSPopoverView-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 3.1.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/FSPopoverView-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_FSPopoverView : NSObject 3 | @end 4 | @implementation PodsDummy_FSPopoverView 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/FSPopoverView-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/FSPopoverView-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double FSPopoverViewVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char FSPopoverViewVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/FSPopoverView.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FSPopoverView 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 5 | OTHER_LDFLAGS = $(inherited) -framework "CoreGraphics" -framework "Foundation" -framework "UIKit" 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/FSPopoverView.modulemap: -------------------------------------------------------------------------------- 1 | framework module FSPopoverView { 2 | umbrella header "FSPopoverView-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/FSPopoverView.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FSPopoverView 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 5 | OTHER_LDFLAGS = $(inherited) -framework "CoreGraphics" -framework "Foundation" -framework "UIKit" 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/ResourceBundle-Alamofire-FSPopoverView-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleIdentifier 8 | ${PRODUCT_BUNDLE_IDENTIFIER} 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleName 12 | ${PRODUCT_NAME} 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 3.1.3 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/FSPopoverView/ResourceBundle-FSPopoverView-FSPopoverView-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleIdentifier 8 | ${PRODUCT_BUNDLE_IDENTIFIER} 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleName 12 | ${PRODUCT_NAME} 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 3.1.3 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## FSPopoverView 5 | 6 | Copyright © 2023 Sheng (https://github.com/lifution) 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | Generated by CocoaPods - https://cocoapods.org 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright © 2023 Sheng (https://github.com/lifution) 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | FSPopoverView 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_FSPopoverView_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_FSPopoverView_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | BCSYMBOLMAP_DIR="BCSymbolMaps" 23 | 24 | 25 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 26 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 27 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 28 | 29 | # Copies and strips a vendored framework 30 | install_framework() 31 | { 32 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 33 | local source="${BUILT_PRODUCTS_DIR}/$1" 34 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 35 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 36 | elif [ -r "$1" ]; then 37 | local source="$1" 38 | fi 39 | 40 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 41 | 42 | if [ -L "${source}" ]; then 43 | echo "Symlinked..." 44 | source="$(readlink -f "${source}")" 45 | fi 46 | 47 | if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then 48 | # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied 49 | find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do 50 | echo "Installing $f" 51 | install_bcsymbolmap "$f" "$destination" 52 | rm "$f" 53 | done 54 | rmdir "${source}/${BCSYMBOLMAP_DIR}" 55 | fi 56 | 57 | # Use filter instead of exclude so missing patterns don't throw errors. 58 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 59 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 60 | 61 | local basename 62 | basename="$(basename -s .framework "$1")" 63 | binary="${destination}/${basename}.framework/${basename}" 64 | 65 | if ! [ -r "$binary" ]; then 66 | binary="${destination}/${basename}" 67 | elif [ -L "${binary}" ]; then 68 | echo "Destination binary is symlinked..." 69 | dirname="$(dirname "${binary}")" 70 | binary="${dirname}/$(readlink "${binary}")" 71 | fi 72 | 73 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 74 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 75 | strip_invalid_archs "$binary" 76 | fi 77 | 78 | # Resign the code if required by the build settings to avoid unstable apps 79 | code_sign_if_enabled "${destination}/$(basename "$1")" 80 | 81 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 82 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 83 | local swift_runtime_libs 84 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 85 | for lib in $swift_runtime_libs; do 86 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 87 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 88 | code_sign_if_enabled "${destination}/${lib}" 89 | done 90 | fi 91 | } 92 | # Copies and strips a vendored dSYM 93 | install_dsym() { 94 | local source="$1" 95 | warn_missing_arch=${2:-true} 96 | if [ -r "$source" ]; then 97 | # Copy the dSYM into the targets temp dir. 98 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 99 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 100 | 101 | local basename 102 | basename="$(basename -s .dSYM "$source")" 103 | binary_name="$(ls "$source/Contents/Resources/DWARF")" 104 | binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" 105 | 106 | # Strip invalid architectures from the dSYM. 107 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 108 | strip_invalid_archs "$binary" "$warn_missing_arch" 109 | fi 110 | if [[ $STRIP_BINARY_RETVAL == 0 ]]; then 111 | # Move the stripped file into its final destination. 112 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 113 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 114 | else 115 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 116 | mkdir -p "${DWARF_DSYM_FOLDER_PATH}" 117 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" 118 | fi 119 | fi 120 | } 121 | 122 | # Used as a return value for each invocation of `strip_invalid_archs` function. 123 | STRIP_BINARY_RETVAL=0 124 | 125 | # Strip invalid architectures 126 | strip_invalid_archs() { 127 | binary="$1" 128 | warn_missing_arch=${2:-true} 129 | # Get architectures for current target binary 130 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 131 | # Intersect them with the architectures we are building for 132 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 133 | # If there are no archs supported by this binary then warn the user 134 | if [[ -z "$intersected_archs" ]]; then 135 | if [[ "$warn_missing_arch" == "true" ]]; then 136 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 137 | fi 138 | STRIP_BINARY_RETVAL=1 139 | return 140 | fi 141 | stripped="" 142 | for arch in $binary_archs; do 143 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 144 | # Strip non-valid architectures in-place 145 | lipo -remove "$arch" -output "$binary" "$binary" 146 | stripped="$stripped $arch" 147 | fi 148 | done 149 | if [[ "$stripped" ]]; then 150 | echo "Stripped $binary of architectures:$stripped" 151 | fi 152 | STRIP_BINARY_RETVAL=0 153 | } 154 | 155 | # Copies the bcsymbolmap files of a vendored framework 156 | install_bcsymbolmap() { 157 | local bcsymbolmap_path="$1" 158 | local destination="${BUILT_PRODUCTS_DIR}" 159 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 160 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 161 | } 162 | 163 | # Signs a framework with the provided identity 164 | code_sign_if_enabled() { 165 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 166 | # Use the current code_sign_identity 167 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 168 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 169 | 170 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 171 | code_sign_cmd="$code_sign_cmd &" 172 | fi 173 | echo "$code_sign_cmd" 174 | eval "$code_sign_cmd" 175 | fi 176 | } 177 | 178 | if [[ "$CONFIGURATION" == "Debug" ]]; then 179 | install_framework "${BUILT_PRODUCTS_DIR}/FSPopoverView/FSPopoverView.framework" 180 | fi 181 | if [[ "$CONFIGURATION" == "Release" ]]; then 182 | install_framework "${BUILT_PRODUCTS_DIR}/FSPopoverView/FSPopoverView.framework" 183 | fi 184 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 185 | wait 186 | fi 187 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_FSPopoverView_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_FSPopoverView_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FSPopoverView" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FSPopoverView/FSPopoverView.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "CoreGraphics" -framework "FSPopoverView" -framework "Foundation" -framework "UIKit" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_FSPopoverView_Example { 2 | umbrella header "Pods-FSPopoverView_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-FSPopoverView_Example/Pods-FSPopoverView_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FSPopoverView" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FSPopoverView/FSPopoverView.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -framework "CoreGraphics" -framework "FSPopoverView" -framework "Foundation" -framework "UIKit" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /FSPopoverView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'FSPopoverView' 3 | s.version = '3.1.3' 4 | s.summary = '`FSPopoverView` is an iOS customizable view that displays a popover view.' 5 | s.homepage = 'https://github.com/lifution/FSPopoverView' 6 | s.author = 'Sheng' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.source = { 9 | :git => 'https://github.com/lifution/FSPopoverView.git', 10 | :tag => s.version.to_s 11 | } 12 | 13 | s.requires_arc = true 14 | s.swift_version = '5' 15 | s.ios.deployment_target = '12.0' 16 | 17 | s.frameworks = 'UIKit', 'Foundation', 'CoreGraphics' 18 | s.source_files = 'Source/**/*.swift' 19 | s.resource_bundles = {'FSPopoverView' => ['Source/Assets/PrivacyInfo.xcprivacy']} 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2023 Sheng (https://github.com/lifution) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package(name: "FSPopoverView", 7 | platforms: [.iOS(.v12)], 8 | products: [ 9 | .library(name: "FSPopoverView", targets: ["FSPopoverView"]) 10 | ], 11 | targets: [ 12 | .target(name: "FSPopoverView", path: "Sources") 13 | ], 14 | swiftLanguageVersions: [.v5] 15 | ) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FSPopoverView 2 | 3 | [![Platform](https://img.shields.io/badge/Platform-iOS-yellowgreen)](https://img.shields.io/badge/Platform-iOS-yellowgreen) 4 | [![Swift 5.x](https://img.shields.io/badge/Swift-5.x-orange.svg?style=flat)](https://developer.apple.com/swift/) 5 | [![ObjC incompatible](https://img.shields.io/badge/ObjC-incompatible-red)](https://img.shields.io/badge/ObjC-incompatible-red) 6 | [![Version Status](https://img.shields.io/cocoapods/v/FSPopoverView.svg)](https://cocoapods.org/pods/FSPopoverView) 7 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 8 | [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange) 9 | [![license MIT](https://img.shields.io/cocoapods/l/FSPopoverView.svg)](https://github.com/lifution/FSPopoverView/blob/master/LICENSE) 10 | 11 | ### [中文介绍](https://github.com/lifution/FSPopoverView/blob/master/README_CN.md) 12 | 13 | A library to present popovers. 14 | 15 | ## Demo 16 | 17 | |
**Custom content**
|
**List (Light)**
|
**List (Dark)**
|
**List (Custom item)**
| 18 | |:--:|:--:|:--:|:--:| 19 | |
|
|
|
| 20 | |**List (Horizontal layout)**| 21 | || 22 | 23 | ## Support 24 | 25 | - [x] Custom content 26 | - [x] Arrow direction 27 | - [x] Hidden Arrow 28 | - [x] Custom border 29 | - [x] Custom shadow 30 | - [x] Custom transition animation 31 | - [x] Custom list item 32 | - [x] Dark mode (iOS13+) 33 | - [x] Global appearance 34 | - [ ] Arrow direction priority 35 | - [ ] List appends/removes item 36 | - [ ] Compatible with screen rotation 37 | 38 | ## Requirements 39 | 40 | * iOS 12+ 41 | * Swift 5 42 | * Xcode 15+ 43 | 44 | ## Installation 45 | 46 | #### [CocoaPods](http://cocoapods.org) (recommended) 47 | 48 | ```ruby 49 | pod 'FSPopoverView' 50 | ``` 51 | 52 | #### [Carthage](https://github.com/Carthage/Carthage) 53 | 54 | ````bash 55 | github "lifution/FSPopoverView" 56 | ```` 57 | 58 | #### [Swift Package Manager](https://swift.org/package-manager/) 59 | 60 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. 61 | 62 | Once you have your Swift package set up, adding FSPopoverView as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift` or the Package list in Xcode. 63 | 64 | ```Swift 65 | dependencies: [ 66 | .package(url: "https://github.com/lifution/FSPopoverView.git") 67 | ] 68 | ``` 69 | 70 | #### Copy manually 71 | 72 | Download or clone the repository, drag the folder `Source` into your project, and tick `Copy items if needed` and `Create groups`. 73 | 74 | ## Usage 75 | 76 | * If you need to customize the content, use FSPopoverView, implements the dataSource and return the contents. 77 | ```Swift 78 | let popoverView = FSPopoverView() 79 | popoverView.dataSource = self 80 | popoverView.present(fromBarItem: barItem) 81 | 82 | // data source 83 | extension viewController: FSPopoverViewDataSource { 84 | 85 | func backgroundView(for popoverView: FSPopoverView) -> UIView? { 86 | let view = UIView() 87 | view.backgroundColor = .yellow 88 | return view 89 | } 90 | 91 | func contentView(for popoverView: FSPopoverView) -> UIView? { 92 | return contentView 93 | } 94 | 95 | func contentSize(for popoverView: FSPopoverView) -> CGSize { 96 | return .init(width: 100.0, height: 100.0) 97 | } 98 | 99 | func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets { 100 | return view.safeAreaInsets 101 | } 102 | 103 | func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool { 104 | return true 105 | } 106 | } 107 | 108 | ``` 109 | * If you need to display a list, use `FSPopoverListView`, which provides `FSPopoverListTextItem` by default. `FSPopoverListView` is data-driven. Inherits `FSPopoverListItem` and `FSPopoverListCell` if you need to customize the item. 110 | ```Swift 111 | let features: [Feature] = [.copy, .message, .db, .qr, .settings] 112 | let items: [FSPopoverListItem] = features.map { feature in 113 | let item = FSPopoverListTextItem() 114 | item.image = feature.image 115 | item.title = feature.title 116 | item.isSeparatorHidden = false 117 | item.selectedHandler = { item in 118 | guard let item = item as? FSPopoverListTextItem else { 119 | return 120 | } 121 | print(item.title ?? "") 122 | } 123 | item.updateLayout() 124 | return item 125 | } 126 | items.last?.isSeparatorHidden = true 127 | let listView = FSPopoverListView() 128 | listView.items = items 129 | listView.present(fromRect: sender.frame.insetBy(dx: 0.0, dy: -6.0), in: view) 130 | ``` 131 | * Use `FSPopoverView.fs_appearance()` to customize default values for popover view. 132 | ```Swift 133 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 134 | do { 135 | let appearance = FSPopoverView.fs_appearance() 136 | appearance.showsArrow = false 137 | appearance.showsDimBackground = true 138 | ... 139 | } 140 | return true 141 | } 142 | ``` 143 | * For more information on how to use, see the example project under the repository. 144 | 145 | ## License 146 | 147 | FSPopoverView is available under the MIT license. [See the LICENSE](https://github.com/lifution/FSPopoverView/blob/master/LICENSE) file for more info. 148 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # FSPopoverView 2 | 3 | [![Platform](https://img.shields.io/badge/Platform-iOS-yellowgreen)](https://img.shields.io/badge/Platform-iOS-yellowgreen) 4 | [![Swift 5.x](https://img.shields.io/badge/Swift-5.x-orange.svg?style=flat)](https://developer.apple.com/swift/) 5 | [![ObjC incompatible](https://img.shields.io/badge/ObjC-incompatible-red)](https://img.shields.io/badge/ObjC-incompatible-red) 6 | [![Version Status](https://img.shields.io/cocoapods/v/FSPopoverView.svg)](https://cocoapods.org/pods/FSPopoverView) 7 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 8 | [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange) 9 | [![license MIT](https://img.shields.io/cocoapods/l/FSPopoverView.svg)](https://github.com/lifution/FSPopoverView/blob/master/LICENSE) 10 | 11 | FSPopoverView 是一个 popover 风格的弹窗,可自定义弹窗内容,类似 UITableView 的 data source,实现对应的协议即可自定义内容。FSPopoverView 同时提供了常用的列表功能:FSPopoverListView,该控件支持纵向和横向两个方向的布局。FSPopoverListView 中的 item 使用的是 model 驱动模式,和传统的 UITableViewCell 不一样,你只要定义 FSPopoverListItem 即可使用。 12 | 13 | ## 示例 14 | 15 | |
**自定义内容**
|
**列表(Light)**
|
**列表(Dark)**
|
**列表(自定义 item)**
| 16 | |:--:|:--:|:--:|:--:| 17 | |
|
|
|
| 18 | 19 | |**列表(横向布局)**| 20 | |:--:| 21 | || 22 | 23 | ## 支持功能(未勾选的还未完成) 24 | 25 | - [x] 内容自定义 26 | - [x] 指定箭头指向方向 27 | - [x] 隐藏箭头 28 | - [x] 自定义边框 29 | - [x] 自定义阴影 30 | - [x] 自定义显示/隐藏的转场动画 31 | - [x] 自定义列表 item 32 | - [x] Dark Mode(iOS13+) 33 | - [x] 全局外观设置 34 | - [ ] 箭头方向优先级设定 35 | - [ ] 列表添加/删除 item 36 | - [ ] 适配屏幕旋转 37 | 38 | ## 要求 39 | 40 | * iOS 12+ 41 | * Swift 5 42 | * Xcode 15+ 43 | 44 | ## 安装 45 | 46 | #### [CocoaPods](http://cocoapods.org) (推荐) 47 | 48 | ```ruby 49 | pod 'FSPopoverView' 50 | ``` 51 | 52 | #### [Carthage](https://github.com/Carthage/Carthage) 53 | 54 | ````bash 55 | github "lifution/FSPopoverView" 56 | ```` 57 | 58 | #### [Swift Package Manager](https://swift.org/package-manager/) 59 | 60 | ```swift 61 | dependencies: [ 62 | .package(url: "https://github.com/lifution/FSPopoverView.git") 63 | ] 64 | ``` 65 | 66 | #### 手动复制 67 | 68 | 下载仓库后把目录下的 `Source` 文件夹拖入你的项目中,并且勾选 `Copy items if needed` 和 `Create groups`。 69 | 70 | ## 使用 71 | 72 | * 如果需要自定义内容,使用 FSPopoverView,实现 dataSource,按照 dataSource 的需求返回对应的内容即可。 73 | ```Swift 74 | let popoverView = FSPopoverView() 75 | popoverView.dataSource = self 76 | popoverView.present(fromBarItem: barItem) 77 | 78 | // data source 79 | extension viewController: FSPopoverViewDataSource { 80 | 81 | func backgroundView(for popoverView: FSPopoverView) -> UIView? { 82 | let view = UIView() 83 | view.backgroundColor = .yellow 84 | return view 85 | } 86 | 87 | func contentView(for popoverView: FSPopoverView) -> UIView? { 88 | return contentView 89 | } 90 | 91 | func contentSize(for popoverView: FSPopoverView) -> CGSize { 92 | return .init(width: 100.0, height: 100.0) 93 | } 94 | 95 | func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets { 96 | return view.safeAreaInsets 97 | } 98 | 99 | func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool { 100 | return true 101 | } 102 | } 103 | 104 | ``` 105 | * 如果需要显示一个列表,使用 FSPopoverListView。FSPopoverListView 默认提供了 FSPopoverListTextItem。FSPopoverListView 是由数据驱动的,所以你如果需要自定义 item 则需要继承 FSPopoverListItem 和 FSPopoverListCell 定制你的 item,然后把 item 传入 FSPopoverListView 即可。 106 | ```Swift 107 | let features: [Feature] = [.copy, .message, .db, .qr, .settings] 108 | let items: [FSPopoverListItem] = features.map { feature in 109 | let item = FSPopoverListTextItem() 110 | item.image = feature.image 111 | item.title = feature.title 112 | item.isSeparatorHidden = false 113 | item.selectedHandler = { item in 114 | guard let item = item as? FSPopoverListTextItem else { 115 | return 116 | } 117 | print(item.title ?? "") 118 | } 119 | item.updateLayout() 120 | return item 121 | } 122 | items.last?.isSeparatorHidden = true 123 | let listView = FSPopoverListView() 124 | listView.items = items 125 | listView.present(fromRect: sender.frame.insetBy(dx: 0.0, dy: -6.0), in: view) 126 | ``` 127 | * 可通过 `FSPopoverView.fs_appearance()` 来给全局的 popover view 设置默认值。 128 | ```Swift 129 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 130 | do { 131 | let appearance = FSPopoverView.fs_appearance() 132 | appearance.showsArrow = false 133 | appearance.showsDimBackground = true 134 | ... 135 | } 136 | return true 137 | } 138 | ``` 139 | * 详细的使用方法可查看仓库中附带的 Example 项目。 140 | 141 | ## License 142 | 143 | FSPopoverView 基于 MIT 许可开源,更多开源许可信息可 [查看该文件](https://github.com/lifution/FSPopoverView/blob/master/LICENSE)。 -------------------------------------------------------------------------------- /Screenshots/custom.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Screenshots/custom.PNG -------------------------------------------------------------------------------- /Screenshots/custom_item.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Screenshots/custom_item.PNG -------------------------------------------------------------------------------- /Screenshots/list_dark.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Screenshots/list_dark.PNG -------------------------------------------------------------------------------- /Screenshots/list_light.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Screenshots/list_light.PNG -------------------------------------------------------------------------------- /Screenshots/menu.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Screenshots/menu.PNG -------------------------------------------------------------------------------- /Source/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Source/Assets/.gitkeep -------------------------------------------------------------------------------- /Source/Assets/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Source/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifution/FSPopoverView/99b801d876c13dd89a9b3d6f2e76e7899f2b083d/Source/Classes/.gitkeep -------------------------------------------------------------------------------- /Source/Classes/Appearance/FSPopoverView+Appearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverView+Appearance.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/12/2. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | 12 | public extension FSPopoverView { 13 | 14 | static func fs_appearance() -> FSPopoverViewAppearance { 15 | return FSPopoverViewAppearance.shared 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/Classes/Appearance/FSPopoverViewAppearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverViewAppearance.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/12/2. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | 12 | public final class FSPopoverViewAppearance { 13 | 14 | // MARK: Properties/Public 15 | 16 | public var showsArrow: Bool 17 | public var showsDimBackground: Bool 18 | public var cornerRadius: CGFloat 19 | public var arrowSize: CGSize 20 | public var borderWidth: CGFloat 21 | public var borderColor: UIColor? 22 | public var shadowColor: UIColor? 23 | public var shadowRadius: CGFloat 24 | public var shadowOpacity: Float 25 | public var backgroundColor: UIColor? 26 | // list 27 | public var spacing: CGFloat 28 | public var textFont: UIFont 29 | public var textColor: UIColor? 30 | public var separatorInset: UIEdgeInsets 31 | public var separatorColor: UIColor? 32 | public var highlightedColor: UIColor? 33 | 34 | // MARK: Initialization 35 | 36 | static let shared = FSPopoverViewAppearance() 37 | 38 | private init() { 39 | showsArrow = true 40 | showsDimBackground = false 41 | cornerRadius = 8.0 42 | arrowSize = .init(width: 22.0, height: 10.0) 43 | borderWidth = 1.0 44 | shadowRadius = 3.0 45 | shadowOpacity = 0.68 46 | spacing = 6.0 47 | textFont = .systemFont(ofSize: 18.0) 48 | separatorInset = .init(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) 49 | // colors 50 | if #available(iOS 13.0, *) { 51 | borderColor = UIColor(dynamicProvider: { trait in 52 | if trait.userInterfaceStyle == .dark { 53 | return .inner.color(hexed: "423E55") ?? .black 54 | } 55 | return .inner.color(hexed: "CFCFCF") ?? .black 56 | }) 57 | shadowColor = UIColor(dynamicProvider: { trait in 58 | if trait.userInterfaceStyle == .dark { 59 | return .inner.color(hexed: "E8E8E8") ?? .black 60 | } 61 | return .inner.color(hexed: "696969") ?? .black 62 | }) 63 | backgroundColor = UIColor(dynamicProvider: { trait in 64 | if trait.userInterfaceStyle == .dark { 65 | return .inner.color(hexed: "1A172B") ?? .black 66 | } 67 | return .white 68 | }) 69 | textColor = UIColor(dynamicProvider: { trait in 70 | if trait.userInterfaceStyle == .dark { 71 | return .inner.color(hexed: "E8E8E8") ?? .black 72 | } 73 | return .black 74 | }) 75 | separatorColor = UIColor(dynamicProvider: { trait in 76 | if trait.userInterfaceStyle == .dark { 77 | return .inner.color(hexed: "4E4A64") ?? .black 78 | } 79 | return .lightGray 80 | }) 81 | highlightedColor = UIColor(dynamicProvider: { trait in 82 | if trait.userInterfaceStyle == .dark { 83 | return .inner.color(hexed: "#322F47") ?? .black 84 | } 85 | return .black.withAlphaComponent(0.1) 86 | }) 87 | } else { 88 | borderColor = .inner.color(hexed: "CFCFCF") ?? .black 89 | shadowColor = .inner.color(hexed: "696969") ?? .black 90 | backgroundColor = .white 91 | textColor = .black 92 | separatorColor = .lightGray 93 | highlightedColor = .black.withAlphaComponent(0.1) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Source/Classes/FSPopoverViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverViewDataSource.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/3. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol FSPopoverViewDataSource: AnyObject { 12 | 13 | func backgroundView(for popoverView: FSPopoverView) -> UIView? 14 | 15 | func contentView(for popoverView: FSPopoverView) -> UIView? 16 | 17 | func contentSize(for popoverView: FSPopoverView) -> CGSize 18 | 19 | func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets 20 | 21 | func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool 22 | } 23 | 24 | /// Optional 25 | public extension FSPopoverViewDataSource { 26 | 27 | func backgroundView(for popoverView: FSPopoverView) -> UIView? { 28 | let view = UIView() 29 | view.backgroundColor = FSPopoverViewAppearance.shared.backgroundColor 30 | return view 31 | } 32 | 33 | func contentView(for popoverView: FSPopoverView) -> UIView? { 34 | return nil 35 | } 36 | 37 | func contentSize(for popoverView: FSPopoverView) -> CGSize { 38 | return .zero 39 | } 40 | 41 | func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets { 42 | return .init(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) 43 | } 44 | 45 | func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool { 46 | return true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Draw/FSPopoverDrawContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverDrawContext.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/10. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct FSPopoverDrawContext { 12 | 13 | var showsArrow = true 14 | var arrowSize: CGSize = .zero 15 | var arrowPoint: CGPoint = .zero 16 | var arrowDirection: FSPopoverView.ArrowDirection = .up 17 | 18 | var cornerRadius: CGFloat = 0.0 19 | var contentSize: CGSize = .zero 20 | var popoverSize: CGSize = .zero 21 | 22 | var borderWidth: CGFloat = 0.0 23 | var borderColor: UIColor? 24 | 25 | var shadowRadius: CGFloat = 0.0 26 | var shadowOpacity: Float = 1.0 27 | var shadowColor: UIColor? 28 | 29 | init () {} 30 | } 31 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Draw/FSPopoverDrawDown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverDrawDown.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/10. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct FSPopoverDrawDown: FSPopoverDrawable { 12 | 13 | func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath { 14 | 15 | let popoverRect = CGRect(origin: offset, size: context.popoverSize) 16 | let contentRect = CGRect(origin: offset, size: context.contentSize) 17 | let arrowSize = context.arrowSize.inner.flattedValue 18 | let arrowPoint = context.arrowPoint.inner.offset(offset).inner.flattedValue 19 | let cornerRadius = context.cornerRadius.inner.flattedValue 20 | 21 | let path = UIBezierPath() 22 | // top-left 23 | do { 24 | path.move(to: .init(x: popoverRect.minX, y: popoverRect.minY + cornerRadius)) 25 | path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.minY + cornerRadius), 26 | radius: cornerRadius, 27 | startAngle: .pi, 28 | endAngle: .pi * 1.5, 29 | clockwise: true) 30 | } 31 | // top-right 32 | do { 33 | path.addLine(to: .init(x: popoverRect.maxX - cornerRadius, y: popoverRect.minY)) 34 | path.addArc(withCenter: .init(x: popoverRect.maxX - cornerRadius, y: popoverRect.minY + cornerRadius), 35 | radius: cornerRadius, 36 | startAngle: .pi * 1.5, 37 | endAngle: 0.0, 38 | clockwise: true) 39 | } 40 | // bottom-right 41 | do { 42 | path.addLine(to: .init(x: popoverRect.maxX, y: contentRect.maxY - cornerRadius)) 43 | path.addArc(withCenter: .init(x: popoverRect.maxX - cornerRadius, y: contentRect.maxY - cornerRadius), 44 | radius: cornerRadius, 45 | startAngle: 0.0, 46 | endAngle: .pi * 0.5, 47 | clockwise: true) 48 | } 49 | // arrow 50 | if context.showsArrow { 51 | let arrowLeftPoint = CGPoint(x: arrowPoint.x - arrowSize.width / 2, y: contentRect.maxY).inner.flattedValue 52 | let arrowRightPoint = CGPoint(x: arrowPoint.x + arrowSize.width / 2, y: contentRect.maxY).inner.flattedValue 53 | path.addLine(to: arrowRightPoint) 54 | do { 55 | let controlPoint1 = CGPoint(x: arrowRightPoint.x - arrowSize.width / 4, y: arrowRightPoint.y).inner.flattedValue 56 | let controlPoint2 = CGPoint(x: arrowPoint.x + arrowSize.width / 6, y: arrowPoint.y).inner.flattedValue 57 | path.addCurve(to: arrowPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 58 | } 59 | do { 60 | let controlPoint1 = CGPoint(x: arrowPoint.x - arrowSize.width / 6, y: arrowPoint.y).inner.flattedValue 61 | let controlPoint2 = CGPoint(x: arrowLeftPoint.x + arrowSize.width / 4, y: arrowLeftPoint.y).inner.flattedValue 62 | path.addCurve(to: arrowLeftPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 63 | } 64 | } 65 | // bottom-left 66 | do { 67 | path.addLine(to: .init(x: popoverRect.minX + cornerRadius, y: contentRect.maxY)) 68 | path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: contentRect.maxY - cornerRadius), 69 | radius: cornerRadius, 70 | startAngle: .pi * 0.5, 71 | endAngle: .pi, 72 | clockwise: true) 73 | } 74 | // close 75 | path.close() 76 | 77 | return path 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Draw/FSPopoverDrawLeft.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverDrawLeft.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/10. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct FSPopoverDrawLeft: FSPopoverDrawable { 12 | 13 | func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath { 14 | 15 | let popoverRect = CGRect(origin: offset, size: context.popoverSize) 16 | let contentRect = CGRect(origin: .init(x: popoverRect.minX + context.arrowSize.height, y: popoverRect.minY), 17 | size: context.contentSize) 18 | let arrowSize = context.arrowSize.inner.flattedValue 19 | let arrowPoint = context.arrowPoint.inner.offset(offset).inner.flattedValue 20 | let cornerRadius = context.cornerRadius.inner.flattedValue 21 | 22 | let path = UIBezierPath() 23 | // top-left 24 | do { 25 | path.move(to: .init(x: contentRect.minX, y: contentRect.minY + cornerRadius)) 26 | path.addArc(withCenter: .init(x: contentRect.minX + cornerRadius, y: contentRect.minY + cornerRadius), 27 | radius: cornerRadius, 28 | startAngle: .pi, 29 | endAngle: .pi * 1.5, 30 | clockwise: true) 31 | } 32 | // top-right 33 | do { 34 | path.addLine(to: .init(x: popoverRect.maxX - cornerRadius, y: contentRect.minY)) 35 | path.addArc(withCenter: .init(x: popoverRect.maxX - cornerRadius, y: contentRect.minY + cornerRadius), 36 | radius: cornerRadius, 37 | startAngle: .pi * 1.5, 38 | endAngle: 0.0, 39 | clockwise: true) 40 | } 41 | // bottom-right 42 | do { 43 | path.addLine(to: .init(x: popoverRect.maxX, y: popoverRect.maxY - cornerRadius)) 44 | path.addArc(withCenter: .init(x: popoverRect.maxX - cornerRadius, y: popoverRect.maxY - cornerRadius), 45 | radius: cornerRadius, 46 | startAngle: 0.0, 47 | endAngle: .pi * 0.5, 48 | clockwise: true) 49 | } 50 | // bottom-left 51 | do { 52 | path.addLine(to: .init(x: contentRect.minX + cornerRadius, y: contentRect.maxY)) 53 | path.addArc(withCenter: .init(x: contentRect.minX + cornerRadius, y: contentRect.maxY - cornerRadius), 54 | radius: cornerRadius, 55 | startAngle: .pi * 0.5, 56 | endAngle: .pi, 57 | clockwise: true) 58 | } 59 | // arrow 60 | if context.showsArrow { 61 | let arrowTopPoint = CGPoint(x: contentRect.minX, y: arrowPoint.y - arrowSize.width / 2).inner.flattedValue 62 | let arrowBottomPoint = CGPoint(x: contentRect.minX, y: arrowPoint.y + arrowSize.width / 2).inner.flattedValue 63 | path.addLine(to: arrowBottomPoint) 64 | do { 65 | let controlPoint1 = CGPoint(x: arrowBottomPoint.x, y: arrowBottomPoint.y - arrowSize.width / 4).inner.flattedValue 66 | let controlPoint2 = CGPoint(x: arrowPoint.x, y: arrowPoint.y + arrowSize.width / 6).inner.flattedValue 67 | path.addCurve(to: arrowPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 68 | } 69 | do { 70 | let controlPoint1 = CGPoint(x: arrowPoint.x, y: arrowPoint.y - arrowSize.width / 6).inner.flattedValue 71 | let controlPoint2 = CGPoint(x: arrowTopPoint.x, y: arrowTopPoint.y + arrowSize.width / 4).inner.flattedValue 72 | path.addCurve(to: arrowTopPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 73 | } 74 | } 75 | // close 76 | path.close() 77 | 78 | return path 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Draw/FSPopoverDrawRight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverDrawRight.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/10. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct FSPopoverDrawRight: FSPopoverDrawable { 12 | 13 | func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath { 14 | 15 | let popoverRect = CGRect(origin: offset, size: context.popoverSize) 16 | let contentRect = CGRect(origin: offset, size: context.contentSize) 17 | let arrowSize = context.arrowSize.inner.flattedValue 18 | let arrowPoint = context.arrowPoint.inner.offset(offset).inner.flattedValue 19 | let cornerRadius = context.cornerRadius.inner.flattedValue 20 | 21 | let path = UIBezierPath() 22 | // top-left 23 | do { 24 | path.move(to: .init(x: popoverRect.minX, y: popoverRect.minY + cornerRadius)) 25 | path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.minY + cornerRadius), 26 | radius: cornerRadius, 27 | startAngle: .pi, 28 | endAngle: .pi * 1.5, 29 | clockwise: true) 30 | } 31 | // top-right 32 | do { 33 | path.addLine(to: .init(x: contentRect.maxX - cornerRadius, y: contentRect.minY)) 34 | path.addArc(withCenter: .init(x: contentRect.maxX - cornerRadius, y: contentRect.minY + cornerRadius), 35 | radius: cornerRadius, 36 | startAngle: .pi * 1.5, 37 | endAngle: 0.0, 38 | clockwise: true) 39 | } 40 | // arrow 41 | if context.showsArrow { 42 | let arrowTopPoint = CGPoint(x: contentRect.maxX, y: arrowPoint.y - arrowSize.width / 2).inner.flattedValue 43 | let arrowBottomPoint = CGPoint(x: contentRect.maxX, y: arrowPoint.y + arrowSize.width / 2).inner.flattedValue 44 | path.addLine(to: arrowTopPoint) 45 | do { 46 | let controlPoint1 = CGPoint(x: arrowTopPoint.x, y: arrowTopPoint.y + arrowSize.width / 4).inner.flattedValue 47 | let controlPoint2 = CGPoint(x: arrowPoint.x, y: arrowPoint.y - arrowSize.width / 6).inner.flattedValue 48 | path.addCurve(to: arrowPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 49 | } 50 | do { 51 | let controlPoint1 = CGPoint(x: arrowPoint.x, y: arrowPoint.y + arrowSize.width / 6).inner.flattedValue 52 | let controlPoint2 = CGPoint(x: arrowBottomPoint.x, y: arrowBottomPoint.y - arrowSize.width / 4).inner.flattedValue 53 | path.addCurve(to: arrowBottomPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 54 | } 55 | } 56 | // bottom-right 57 | do { 58 | path.addLine(to: .init(x: contentRect.maxX, y: contentRect.maxY - cornerRadius)) 59 | path.addArc(withCenter: .init(x: contentRect.maxX - cornerRadius, y: contentRect.maxY - cornerRadius), 60 | radius: cornerRadius, 61 | startAngle: 0.0, 62 | endAngle: .pi * 0.5, 63 | clockwise: true) 64 | } 65 | // bottom-left 66 | do { 67 | path.addLine(to: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.maxY)) 68 | path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.maxY - cornerRadius), 69 | radius: cornerRadius, 70 | startAngle: .pi * 0.5, 71 | endAngle: .pi, 72 | clockwise: true) 73 | } 74 | // close 75 | path.close() 76 | 77 | return path 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Draw/FSPopoverDrawUp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverDrawUp.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/10. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct FSPopoverDrawUp: FSPopoverDrawable { 12 | 13 | func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath { 14 | 15 | let popoverRect = CGRect(origin: offset, size: context.popoverSize) 16 | let contentRect = CGRect(origin: .init(x: popoverRect.minX, y: popoverRect.minY + context.arrowSize.height), 17 | size: context.contentSize) 18 | let arrowSize = context.arrowSize.inner.flattedValue 19 | let arrowPoint = context.arrowPoint.inner.offset(offset).inner.flattedValue 20 | let cornerRadius = context.cornerRadius.inner.flattedValue 21 | 22 | let path = UIBezierPath() 23 | // top-left 24 | do { 25 | path.move(to: .init(x: contentRect.minX, y: contentRect.minY + cornerRadius)) 26 | path.addArc(withCenter: .init(x: contentRect.minX + cornerRadius, y: contentRect.minY + cornerRadius), 27 | radius: cornerRadius, 28 | startAngle: .pi, 29 | endAngle: .pi * 1.5, 30 | clockwise: true) 31 | } 32 | // arrow 33 | if context.showsArrow { 34 | path.addLine(to: .init(x: arrowPoint.x - arrowSize.width / 2, y: contentRect.minY)) 35 | do { 36 | let controlPoint1 = CGPoint(x: arrowPoint.x - arrowSize.width / 4, y: contentRect.minY).inner.flattedValue 37 | let controlPoint2 = CGPoint(x: arrowPoint.x - arrowSize.width / 6, y: arrowPoint.y).inner.flattedValue 38 | path.addCurve(to: arrowPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 39 | } 40 | do { 41 | let controlPoint1 = CGPoint(x: arrowPoint.x + arrowSize.width / 6, y: arrowPoint.y).inner.flattedValue 42 | let controlPoint2 = CGPoint(x: arrowPoint.x + arrowSize.width / 4, y: contentRect.minY).inner.flattedValue 43 | path.addCurve(to: .init(x: arrowPoint.x + arrowSize.width / 2, y: contentRect.minY), 44 | controlPoint1: controlPoint1, 45 | controlPoint2: controlPoint2) 46 | } 47 | } 48 | // top-right 49 | do { 50 | path.addLine(to: .init(x: popoverRect.maxX - cornerRadius, y: contentRect.minY)) 51 | path.addArc(withCenter: .init(x: contentRect.maxX - cornerRadius, y: contentRect.minY + cornerRadius), 52 | radius: cornerRadius, 53 | startAngle: .pi * 1.5, 54 | endAngle: 0.0, 55 | clockwise: true) 56 | } 57 | // bottom-right 58 | do { 59 | path.addLine(to: .init(x: contentRect.maxX, y: popoverRect.maxY - cornerRadius)) 60 | path.addArc(withCenter: .init(x: contentRect.maxX - cornerRadius, y: popoverRect.maxY - cornerRadius), 61 | radius: cornerRadius, 62 | startAngle: 0.0, 63 | endAngle: .pi * 0.5, 64 | clockwise: true) 65 | } 66 | // bottom-left 67 | do { 68 | path.addLine(to: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.maxY)) 69 | path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.maxY - cornerRadius), 70 | radius: cornerRadius, 71 | startAngle: .pi * 0.5, 72 | endAngle: .pi, 73 | clockwise: true) 74 | } 75 | // close 76 | path.close() 77 | 78 | return path 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Draw/FSPopoverDrawable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverDrawable.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/12. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol FSPopoverDrawable { 12 | func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath 13 | } 14 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Draw/FSPopoverDrawer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverDrawer.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/10. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct FSPopoverDrawer { 12 | 13 | // MARK: Properties/Internal 14 | 15 | let context: FSPopoverDrawContext 16 | 17 | // MARK: Properties/Private 18 | 19 | private let drawable: FSPopoverDrawable 20 | 21 | // MARK: Initialization 22 | 23 | init(context: FSPopoverDrawContext) { 24 | self.context = context 25 | switch context.arrowDirection { 26 | case .up: 27 | drawable = FSPopoverDrawUp() 28 | case .down: 29 | drawable = FSPopoverDrawDown() 30 | case .left: 31 | drawable = FSPopoverDrawLeft() 32 | case .right: 33 | drawable = FSPopoverDrawRight() 34 | } 35 | } 36 | 37 | // MARK: Internal 38 | 39 | func generatePath() -> UIBezierPath { 40 | return drawable.generatePath(with: context, offset: .zero) 41 | } 42 | 43 | func generateBorderImage() -> UIImage? { 44 | 45 | guard 46 | context.borderWidth > 0.0, 47 | let borderColor = context.borderColor 48 | else { 49 | return nil 50 | } 51 | 52 | // The border is clipped in half after drawing, so make it double here. 53 | let borderWidth = context.borderWidth * 2 54 | let size = CGSize(width: context.popoverSize.width + borderWidth * 2, 55 | height: context.popoverSize.height + borderWidth * 2) 56 | 57 | // border image 58 | let borderImage: UIImage? = { 59 | 60 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 61 | 62 | let path = drawable.generatePath(with: context, offset: .init(x: borderWidth, y: borderWidth)) 63 | path.lineWidth = borderWidth 64 | borderColor.setStroke() 65 | path.stroke() 66 | 67 | let image = UIGraphicsGetImageFromCurrentImageContext() 68 | UIGraphicsEndImageContext() 69 | 70 | return image 71 | }() 72 | 73 | // mask image 74 | let maskImage: UIImage? = { 75 | 76 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 77 | 78 | let path = drawable.generatePath(with: context, offset: .init(x: borderWidth, y: borderWidth)) 79 | UIColor.white.setFill() 80 | path.fill() 81 | 82 | let image = UIGraphicsGetImageFromCurrentImageContext() 83 | UIGraphicsEndImageContext() 84 | 85 | return image 86 | }() 87 | 88 | // create image mask 89 | 90 | guard 91 | let cgimage = maskImage?.cgImage, 92 | let dataProvider = cgimage.dataProvider 93 | else { 94 | return nil 95 | } 96 | 97 | let width = cgimage.width 98 | let height = cgimage.height 99 | let bytesPerRow = cgimage.bytesPerRow 100 | let bitsPerPixel = cgimage.bitsPerPixel 101 | let bitsPerComponent = cgimage.bitsPerComponent 102 | 103 | guard 104 | let mask = CGImage(maskWidth: width, 105 | height: height, 106 | bitsPerComponent: bitsPerComponent, 107 | bitsPerPixel: bitsPerPixel, 108 | bytesPerRow: bytesPerRow, 109 | provider: dataProvider, 110 | decode: nil, 111 | shouldInterpolate: false), 112 | let maskingCgImage = borderImage?.cgImage?.masking(mask) 113 | else { 114 | return nil 115 | } 116 | 117 | return UIImage(cgImage: maskingCgImage, scale: UIScreen.main.scale, orientation: .up) 118 | } 119 | 120 | func generateShadowImage() -> UIImage? { 121 | 122 | guard 123 | context.shadowRadius > 0.0, 124 | context.shadowOpacity > 0.0, 125 | let shadowColor = context.shadowColor 126 | else { 127 | return nil 128 | } 129 | 130 | // The shadow area is gradually reduced from the inside to the outside, 131 | // and the visible part of the shadow is a bit larger than the set `shadowRadius`, 132 | // so it is increased here to avoid clipping the shadow. 133 | let radius = context.shadowRadius + context.borderWidth 134 | let inset = radius + 10.0 135 | let size = CGSize(width: context.popoverSize.width + inset * 2, 136 | height: context.popoverSize.height + inset * 2) 137 | 138 | // shadow image 139 | let shadowImage: UIImage? = { 140 | 141 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 142 | 143 | guard let context = UIGraphicsGetCurrentContext() else { 144 | return nil 145 | } 146 | 147 | let path = drawable.generatePath(with: self.context, offset: .init(x: inset, y: inset)) 148 | 149 | let layer = CAShapeLayer() 150 | layer.path = path.cgPath 151 | layer.fillColor = UIColor.white.cgColor 152 | layer.shadowColor = shadowColor.cgColor 153 | layer.shadowRadius = radius 154 | layer.shadowOffset = .zero 155 | layer.shadowOpacity = min(1.0, self.context.shadowOpacity) 156 | layer.magnificationFilter = .nearest 157 | layer.render(in: context) 158 | 159 | let image = UIGraphicsGetImageFromCurrentImageContext() 160 | UIGraphicsEndImageContext() 161 | 162 | return image 163 | }() 164 | 165 | // mask image 166 | let maskImage: UIImage? = { 167 | 168 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 169 | 170 | let path = drawable.generatePath(with: context, offset: .init(x: inset, y: inset)) 171 | UIColor.white.setFill() 172 | path.fill() 173 | 174 | let image = UIGraphicsGetImageFromCurrentImageContext() 175 | UIGraphicsEndImageContext() 176 | 177 | return image 178 | }() 179 | 180 | // create image mask 181 | 182 | guard 183 | let cgimage = maskImage?.cgImage, 184 | let dataProvider = cgimage.dataProvider 185 | else { 186 | return nil 187 | } 188 | 189 | let width = cgimage.width 190 | let height = cgimage.height 191 | let bytesPerRow = cgimage.bytesPerRow 192 | let bitsPerPixel = cgimage.bitsPerPixel 193 | let bitsPerComponent = cgimage.bitsPerComponent 194 | 195 | guard 196 | let mask = CGImage(maskWidth: width, 197 | height: height, 198 | bitsPerComponent: bitsPerComponent, 199 | bitsPerPixel: bitsPerPixel, 200 | bytesPerRow: bytesPerRow, 201 | provider: dataProvider, 202 | decode: nil, 203 | shouldInterpolate: false), 204 | let maskingCgImage = shadowImage?.cgImage?.masking(mask) 205 | else { 206 | return nil 207 | } 208 | 209 | let layer = CALayer() 210 | layer.bounds = .init(origin: .zero, size: size) 211 | layer.contents = maskingCgImage 212 | layer.contentsScale = UIScreen.main.scale 213 | layer.magnificationFilter = .nearest 214 | let renderer = UIGraphicsImageRenderer(size: size) 215 | return renderer.image { context in 216 | layer.render(in: context.cgContext) 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Extensions/CoreGraphicsExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreGraphicsExtensions.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/9. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private func _Flat(_ x: T) -> T { 12 | guard 13 | x != T.leastNormalMagnitude, 14 | x != T.leastNonzeroMagnitude, 15 | x != T.greatestFiniteMagnitude 16 | else { 17 | return x 18 | } 19 | let scale: T = T(Int(UIScreen.main.scale)) 20 | let flattedValue = ceil(x * scale) / scale 21 | return flattedValue 22 | } 23 | 24 | extension FSPopoverViewInternalWrapper where Base == CGRect { 25 | 26 | var flattedValue: CGRect { 27 | return .init(origin: base.origin.inner.flattedValue, 28 | size: base.size.inner.flattedValue) 29 | } 30 | } 31 | 32 | extension FSPopoverViewInternalWrapper where Base == CGSize { 33 | 34 | var flattedValue: CGSize { 35 | return .init(width: base.width.inner.flattedValue, 36 | height: base.height.inner.flattedValue) 37 | } 38 | } 39 | 40 | extension FSPopoverViewInternalWrapper where Base == CGPoint { 41 | 42 | var flattedValue: CGPoint { 43 | return .init(x: base.x.inner.flattedValue, 44 | y: base.y.inner.flattedValue) 45 | } 46 | 47 | func offset(_ offset: CGPoint) -> CGPoint { 48 | return .init(x: base.x + offset.x, y: base.y + offset.y) 49 | } 50 | } 51 | 52 | extension FSPopoverViewInternalWrapper where Base == CGFloat { 53 | 54 | var flattedValue: CGFloat { 55 | return _Flat(base) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Extensions/NSAttributedString+FSPopoverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+FSPopoverView.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/24. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | 12 | extension FSPopoverViewInternalWrapper where Base: NSAttributedString { 13 | 14 | static func size(of attributedString: NSAttributedString?, limitedSize: CGSize? = .zero) -> CGSize { 15 | guard let att_string = attributedString, !att_string.string.isEmpty else { 16 | return .zero 17 | } 18 | let constraints: CGSize = { 19 | if let size = limitedSize, size.width > 0, size.height > 0 { 20 | return .init(width: ceil(size.width), height: ceil(size.height)) 21 | } 22 | return CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) 23 | }() 24 | let size = att_string.boundingRect(with: constraints, 25 | options: [.usesLineFragmentOrigin, .usesFontLeading], 26 | context: nil).size 27 | return .init(width: ceil(size.width), height: ceil(size.height)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Extensions/UIColor+FSPopoverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+FSPopoverView.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/12/2. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension FSPopoverViewInternalWrapper where Base: UIColor { 12 | 13 | static func color(hexed hex: String) -> UIColor? { 14 | 15 | guard hex.count > 0 else { 16 | return nil 17 | } 18 | 19 | var colorString = hex.uppercased() 20 | do { 21 | // remove prefix `#` / `0x`。 22 | if colorString.hasPrefix("#") { 23 | colorString.removeFirst() 24 | } else if colorString.hasPrefix("0x") { 25 | colorString.removeFirst(2) 26 | } else if colorString.hasPrefix("0X") { 27 | colorString.removeFirst(2) 28 | } 29 | } 30 | 31 | if colorString.isEmpty { 32 | return nil 33 | } 34 | 35 | func _colorComponent(from string: String, location: Int, length: Int) -> CGFloat { 36 | let startIndex = string.index(string.startIndex, offsetBy: location) 37 | let endIndex = string.index(startIndex, offsetBy: length) 38 | let substring = String(string[startIndex.. { 12 | let base: Base 13 | fileprivate init(_ base: Base) { 14 | self.base = base 15 | } 16 | } 17 | 18 | protocol FSPopoverViewInternalCompatible: AnyObject {} 19 | extension FSPopoverViewInternalCompatible { 20 | static var inner: FSPopoverViewInternalWrapper.Type { 21 | get { return FSPopoverViewInternalWrapper.self } 22 | set {} 23 | } 24 | var inner: FSPopoverViewInternalWrapper { 25 | get { return FSPopoverViewInternalWrapper(self) } 26 | set {} 27 | } 28 | } 29 | 30 | protocol FSPopoverViewInternalCompatibleValue {} 31 | extension FSPopoverViewInternalCompatibleValue { 32 | static var inner: FSPopoverViewInternalWrapper.Type { 33 | get { return FSPopoverViewInternalWrapper.self } 34 | set {} 35 | } 36 | var inner: FSPopoverViewInternalWrapper { 37 | get { return FSPopoverViewInternalWrapper(self) } 38 | set {} 39 | } 40 | } 41 | 42 | extension UIView: FSPopoverViewInternalCompatible {} 43 | extension UIColor: FSPopoverViewInternalCompatible {} 44 | extension NSAttributedString: FSPopoverViewInternalCompatible {} 45 | 46 | extension CGRect: FSPopoverViewInternalCompatibleValue {} 47 | extension CGSize: FSPopoverViewInternalCompatibleValue {} 48 | extension CGPoint: FSPopoverViewInternalCompatibleValue {} 49 | extension CGFloat: FSPopoverViewInternalCompatibleValue {} 50 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Tools/FSPopoverViewDelegateRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverViewDelegateRouter.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2022/4/13. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class FSPopoverViewDelegateRouter: NSObject { 12 | 13 | var gestureRecognizerShouldBeginHandler: ((_ gestureRecognizer: UIGestureRecognizer) -> Bool)? 14 | } 15 | 16 | extension FSPopoverViewDelegateRouter: UIGestureRecognizerDelegate { 17 | 18 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 19 | if let handler = gestureRecognizerShouldBeginHandler { 20 | return handler(gestureRecognizer) 21 | } 22 | return false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Classes/Internal/Views/_FSSeparatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _FSSeparatorView.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/24. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class _FSSeparatorView: UIView { 12 | 13 | // MARK: Properties/Internal 14 | 15 | var color: UIColor? { 16 | didSet { 17 | colorLayer.backgroundColor = color?.cgColor 18 | } 19 | } 20 | 21 | // MARK: Properties/Override 22 | 23 | @available(*, unavailable) 24 | override var backgroundColor: UIColor? { 25 | get { return nil } 26 | set { super.backgroundColor = nil } 27 | } 28 | 29 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 30 | super.traitCollectionDidChange(previousTraitCollection) 31 | if #unavailable(iOS 17) { 32 | colorLayer.backgroundColor = color?.cgColor 33 | } 34 | } 35 | 36 | // MARK: Properties/Private 37 | 38 | private let colorLayer = CAShapeLayer() 39 | 40 | // MARK: Initialization 41 | 42 | override public init(frame: CGRect) { 43 | super.init(frame: frame) 44 | p_didInitialize() 45 | } 46 | 47 | required public init?(coder: NSCoder) { 48 | super.init(coder: coder) 49 | p_didInitialize() 50 | } 51 | 52 | // MARK: Override 53 | 54 | override func layoutSubviews() { 55 | super.layoutSubviews() 56 | colorLayer.frame = .init(origin: .zero, size: frame.size) 57 | } 58 | 59 | // MARK: Private 60 | 61 | private func p_didInitialize() { 62 | color = UIColor(red: 0.90, green: 0.90, blue: 0.90, alpha: 1.00) 63 | layer.addSublayer(colorLayer) 64 | if #available(iOS 17, *) { 65 | registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (self: Self, previousTraitCollection: UITraitCollection) in 66 | self.colorLayer.backgroundColor = self.color?.cgColor 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Source/Classes/List/Cell/FSPopoverListCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverListCell.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/23. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Abstract base class, cannot be used directly, must be inherited for use. 12 | open class FSPopoverListCell: UIView { 13 | 14 | // MARK: Properties/Open 15 | 16 | open var isHighlighted = false { 17 | didSet { 18 | highlightedView.isHidden = !isHighlighted 19 | } 20 | } 21 | 22 | // MARK: Properties/Public 23 | 24 | public let item: FSPopoverListItem 25 | 26 | // MARK: Properties/Private 27 | 28 | private let separatorView = _FSSeparatorView() 29 | 30 | private let highlightedView = UIView() 31 | 32 | private var separatorConstraints = [NSLayoutConstraint]() 33 | 34 | // MARK: Initialization 35 | 36 | public required init(item: FSPopoverListItem) { 37 | #if DEBUG 38 | let abstractName = String(describing: FSPopoverListCell.self) 39 | if "\(type(of: self))" == abstractName { 40 | fatalError("\(abstractName) is abstract base class, cannot be used directly, must be inherited for use.") 41 | } 42 | #endif 43 | self.item = item 44 | super.init(frame: .zero) 45 | p_didInitialize() 46 | } 47 | 48 | @available(*, unavailable) 49 | public required init?(coder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | // MARK: Override 54 | 55 | open override func layoutSubviews() { 56 | super.layoutSubviews() 57 | sendSubviewToBack(highlightedView) 58 | bringSubviewToFront(separatorView) 59 | } 60 | 61 | // MARK: Private 62 | 63 | private func p_didInitialize() { 64 | defer { 65 | didInitialize() 66 | } 67 | backgroundColor = .clear 68 | separatorView.isHidden = true 69 | highlightedView.isHidden = true 70 | separatorView.translatesAutoresizingMaskIntoConstraints = false 71 | highlightedView.translatesAutoresizingMaskIntoConstraints = false 72 | addSubview(separatorView) 73 | addSubview(highlightedView) 74 | do { 75 | addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|", 76 | metrics: nil, 77 | views: ["view": highlightedView])) 78 | addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|", 79 | metrics: nil, 80 | views: ["view": highlightedView])) 81 | } 82 | } 83 | 84 | // MARK: Open 85 | 86 | /// Invoked after initialization. 87 | /// Subclass can do initialization work in this method. 88 | open func didInitialize() {} 89 | 90 | /// update contents. 91 | /// 92 | /// - Subclass can update contents in this method. 93 | /// - This method will be called when the item that binding 94 | /// with current view calls method `reload(_:)`. 95 | /// - ⚠️ Subclasses **must** call `super.renderContents()` when 96 | /// overriding this method, otherwise some bugs may occur. 97 | /// 98 | open func renderContents() { 99 | defer { p_remakeConstraints() } 100 | separatorView.color = item.separatorColor 101 | separatorView.isHidden = item.isSeparatorHidden 102 | highlightedView.backgroundColor = item.highlightedColor 103 | } 104 | } 105 | 106 | // MARK: - Private 107 | 108 | private extension FSPopoverListCell { 109 | 110 | func p_remakeConstraints() { 111 | NSLayoutConstraint.deactivate(separatorConstraints) 112 | separatorConstraints.removeAll() 113 | switch item.scrollDirection { 114 | case .vertical: 115 | let hConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-left-[view]-right-|", 116 | metrics: [ 117 | "left": item.separatorInset.left, 118 | "right": item.separatorInset.right 119 | ], 120 | views: ["view": separatorView]) 121 | let vConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[view(==height)]|", 122 | metrics: ["height": 1.0 / UIScreen.main.scale], 123 | views: ["view": separatorView]) 124 | separatorConstraints += hConstraints + vConstraints 125 | case .horizontal: 126 | let hConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[view(==width)]|", 127 | metrics: ["width": 1.0 / UIScreen.main.scale], 128 | views: ["view": separatorView]) 129 | let vConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-top-[view]-bottom-|", 130 | metrics: [ 131 | "top": item.separatorInset.top, 132 | "bottom": item.separatorInset.bottom 133 | ], 134 | views: ["view": separatorView]) 135 | separatorConstraints += hConstraints + vConstraints 136 | } 137 | addConstraints(separatorConstraints) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Source/Classes/List/FSPopoverListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverListView.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/20. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class FSPopoverListView: FSPopoverView, FSPopoverViewDataSource { 12 | 13 | // MARK: ScrollDirection 14 | 15 | public enum ScrollDirection { 16 | case vertical 17 | case horizontal 18 | } 19 | 20 | // MARK: Properties/Open 21 | 22 | /// The scroll direction of the list. 23 | open var scrollDirection: FSPopoverListView.ScrollDirection { 24 | didSet { 25 | if scrollDirection != oldValue { 26 | setNeedsReload() 27 | } 28 | } 29 | } 30 | 31 | /// The list items. 32 | /// An item represents a cell. 33 | open var items: [FSPopoverListItem]? { 34 | didSet { 35 | setNeedsReload() 36 | } 37 | } 38 | 39 | /// Whether needs to dismiss list view when user tap the blank area. 40 | /// Defaults to true. 41 | open var shouldDismissOnTapOutside = true 42 | 43 | // MARK: Properties/Public 44 | 45 | /// Limits the number of visible items displayed in the list view. 46 | /// It means there is no limit if this value less than or equal to 0. 47 | /// Defaults to 0. 48 | public final var maximumCountOfVisibleItems = 0 { 49 | didSet { 50 | if maximumCountOfVisibleItems != oldValue { 51 | setNeedsReload() 52 | } 53 | } 54 | } 55 | 56 | /// Auto dismiss the list view when an item is selected. 57 | /// Defaults to true. 58 | public final var dismissWhenSelected = true 59 | 60 | /// The background view's background color. 61 | /// Defaults to white. 62 | public final var backgroundColor: UIColor? { 63 | get { return backgroundView.backgroundColor } 64 | set { backgroundView.backgroundColor = newValue } 65 | } 66 | 67 | // MARK: Properties/Private 68 | 69 | private let scrollView = _ListScrollView() 70 | 71 | private let backgroundView = UIView() 72 | 73 | private var contentSize = CGSize.zero 74 | 75 | private var scrollContentSize = CGSize.zero 76 | 77 | // MARK: Initialization 78 | 79 | public init(scrollDirection: FSPopoverListView.ScrollDirection = .vertical) { 80 | self.scrollDirection = scrollDirection 81 | super.init() 82 | p_didInitialize() 83 | } 84 | 85 | // MARK: Override 86 | 87 | open override func reloadData() { 88 | 89 | super.reloadData() 90 | 91 | scrollView.contentSize = scrollContentSize 92 | 93 | let items = items ?? [] 94 | 95 | scrollView.cells.forEach { $0.removeFromSuperview() } 96 | scrollView.cells.removeAll() 97 | 98 | var lastCell: FSPopoverListCell? 99 | items.forEach { item in 100 | let cell = item.cellType.init(item: item) 101 | do { 102 | var frame = lastCell?.frame ?? .zero 103 | switch scrollDirection { 104 | case .vertical: 105 | frame.origin.y = frame.maxY 106 | frame.size.width = contentSize.width 107 | frame.size.height = item.size.height 108 | case .horizontal: 109 | frame.origin.x = frame.maxX 110 | frame.size.width = item.size.width 111 | frame.size.height = contentSize.height 112 | } 113 | cell.frame = frame 114 | } 115 | cell.renderContents() 116 | scrollView.addSubview(cell) 117 | scrollView.cells.append(cell) 118 | lastCell = cell 119 | item.reloadHandler = { [unowned cell, unowned self] _, type in 120 | if type == .reload { 121 | self.setNeedsReload() 122 | } else { 123 | cell.renderContents() 124 | } 125 | } 126 | } 127 | } 128 | 129 | // MARK: Open/FSPopoverViewDataSource 130 | 131 | open func backgroundView(for popoverView: FSPopoverView) -> UIView? { 132 | return backgroundView 133 | } 134 | 135 | open func contentView(for popoverView: FSPopoverView) -> UIView? { 136 | return scrollView 137 | } 138 | 139 | open func contentSize(for popoverView: FSPopoverView) -> CGSize { 140 | guard 141 | let upMaxSize = maximumContentSizeOf(direction: .up), 142 | let downMaxSize = maximumContentSizeOf(direction: .down), 143 | let leftMaxSize = maximumContentSizeOf(direction: .left), 144 | let rightMaxSize = maximumContentSizeOf(direction: .right) 145 | else { 146 | return .zero 147 | } 148 | if let items = items { 149 | var contentSize = CGSize.zero 150 | do { 151 | // original content size. 152 | switch scrollDirection { 153 | case .vertical: 154 | contentSize.width = items.map { $0.size.width }.max() ?? 0 155 | contentSize.height = items.map { $0.size.height }.reduce(0, +) 156 | case .horizontal: 157 | contentSize.width = items.map { $0.size.width }.reduce(0, +) 158 | contentSize.height = items.map { $0.size.height }.max() ?? 0 159 | } 160 | scrollContentSize = contentSize 161 | } 162 | do { 163 | // limits maximum content size. 164 | let maxSize: CGSize 165 | if autosetsArrowDirection { 166 | // priority: up > down > left > right 167 | if upMaxSize.width >= contentSize.width && upMaxSize.height >= contentSize.height { 168 | maxSize = upMaxSize 169 | } else if downMaxSize.width >= contentSize.width && downMaxSize.height >= contentSize.height { 170 | maxSize = downMaxSize 171 | } else if leftMaxSize.width >= contentSize.width && leftMaxSize.height >= contentSize.height { 172 | maxSize = leftMaxSize 173 | } else if rightMaxSize.width >= contentSize.width && rightMaxSize.height >= contentSize.height { 174 | maxSize = rightMaxSize 175 | } else { 176 | // Use up maximum size if there is no compatible size. 177 | maxSize = upMaxSize 178 | } 179 | } else { 180 | switch arrowDirection { 181 | case .up: 182 | maxSize = upMaxSize 183 | case .down: 184 | maxSize = downMaxSize 185 | case .left: 186 | maxSize = leftMaxSize 187 | case .right: 188 | maxSize = rightMaxSize 189 | } 190 | } 191 | contentSize.width = min(contentSize.width, maxSize.width) 192 | contentSize.height = min(contentSize.height, maxSize.height) 193 | } 194 | do { 195 | // limits maximum count of visible items. 196 | if maximumCountOfVisibleItems > 0, maximumCountOfVisibleItems < items.count { 197 | let visibleItems = items[0.. UIEdgeInsets { 220 | var insets = containerView?.safeAreaInsets ?? .zero 221 | if insets.top <= 0 { insets.top = 10.0 } 222 | if insets.bottom <= 0 { insets.bottom = 10.0 } 223 | if insets.left <= 0 { insets.left = 10.0 } 224 | if insets.right <= 0 { insets.right = 10.0 } 225 | return insets 226 | } 227 | 228 | open func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool { 229 | return shouldDismissOnTapOutside 230 | } 231 | 232 | // MARK: Open 233 | 234 | open func didSelectItem(_ item: FSPopoverListItem) { 235 | let operation: () -> Void = { 236 | item.selectedHandler?(item) 237 | } 238 | if dismissWhenSelected { 239 | dismiss(animated: true, isSelection: true) { 240 | operation() 241 | } 242 | } else { 243 | operation() 244 | } 245 | } 246 | } 247 | 248 | // MARK: - Private 249 | 250 | private extension FSPopoverListView { 251 | 252 | /// Invoked after initialization. 253 | func p_didInitialize() { 254 | dataSource = self 255 | backgroundView.backgroundColor = FSPopoverView.fs_appearance().backgroundColor 256 | scrollView.selectedCellHandler = { [unowned self] cell in 257 | self.didSelectItem(cell.item) 258 | } 259 | } 260 | } 261 | 262 | 263 | // MARK: - _ListScrollView 264 | 265 | private class _ListScrollView: UIScrollView { 266 | 267 | // MARK: Properties/Fileprivate 268 | 269 | var cells = [FSPopoverListCell]() 270 | 271 | var selectedCellHandler: ((_ cell: FSPopoverListCell) -> Void)? 272 | 273 | // MARK: Properties/Private 274 | 275 | private weak var selectedCell: FSPopoverListCell? 276 | private weak var highlightedCell: FSPopoverListCell? 277 | 278 | // MARK: Override 279 | 280 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 281 | super.touchesBegan(touches, with: event) 282 | highlightedCell?.isHighlighted = false 283 | highlightedCell = nil 284 | guard let touch = touches.first else { return } 285 | let location = touch.location(in: self) 286 | if let cell = p_cell(at: location), cell.item.isEnabled { 287 | selectedCell = cell 288 | if cell.item.selectionStyle != .none { 289 | cell.isHighlighted = true 290 | highlightedCell = cell 291 | } 292 | } 293 | } 294 | 295 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 296 | super.touchesEnded(touches, with: event) 297 | guard let touch = touches.first else { return } 298 | let location = touch.location(in: self) 299 | if let cell = highlightedCell { 300 | if cell.frame.contains(location) { 301 | if !cell.isHighlighted { 302 | cell.isHighlighted = true 303 | } 304 | } else { 305 | if cell.isHighlighted { 306 | cell.isHighlighted = false 307 | } 308 | } 309 | } 310 | } 311 | 312 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 313 | super.touchesEnded(touches, with: event) 314 | guard let touch = touches.first else { return } 315 | let location = touch.location(in: self) 316 | if let cell = p_cell(at: location) { 317 | if let selectedCell = selectedCell, selectedCell === cell { 318 | selectedCellHandler?(selectedCell) 319 | } 320 | } 321 | selectedCell = nil 322 | highlightedCell = nil 323 | } 324 | 325 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 326 | super.touchesCancelled(touches, with: event) 327 | selectedCell = nil 328 | highlightedCell?.isHighlighted = false 329 | highlightedCell = nil 330 | } 331 | 332 | // MARK: Private 333 | 334 | private func p_cell(at location: CGPoint) -> FSPopoverListCell? { 335 | guard !cells.isEmpty else { return nil } 336 | var result: FSPopoverListCell? = nil 337 | for cell in cells { 338 | if cell.frame.contains(location) { 339 | result = cell 340 | break 341 | } 342 | } 343 | return result 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /Source/Classes/List/Item/Defaults/FSPopoverListTextItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverListTextItem.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/23. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | // FSPopoverListTextItem looks like: 9 | // vertical: contents center in vertical 10 | // NOTE: This mode is compatible with Right-to-Left language. 11 | // ┌────────────────────────────────┬──────────────────────────────────────┐ 12 | // │ │ 13 | // │ │ │ 14 | // │--[icon]--[title]--│ 15 | // │ │ │ 16 | // │ │ 17 | // └────────────────────────────────┴──────────────────────────────────────┘ 18 | // horizontal: contents center in horizontal 19 | // ┌─────────────────────────┬──────────────────────────┐ 20 | // │ │ 21 | // │ │ │ 22 | // │ [icon] │ 23 | // │ │ │ 24 | // │ │ 25 | // │ │ │ 26 | // ├ -[title]- ┤ 27 | // │ │ │ 28 | // │ │ 29 | // └─────────────────────────┴──────────────────────────┘ 30 | // 31 | 32 | import UIKit 33 | 34 | public final class FSPopoverListTextItem: FSPopoverListItem { 35 | 36 | // MARK: Properties/Public 37 | 38 | public var contentInset: UIEdgeInsets 39 | 40 | public var image: UIImage? 41 | 42 | public var title: String? 43 | 44 | /// The space between image and title. 45 | /// This value will not work if one of image and title is nil. 46 | /// Default value see `FSPopoverViewAppearance`. 47 | public var spacing: CGFloat = FSPopoverViewAppearance.shared.spacing 48 | 49 | /// If nil, use a default font. 50 | /// Default value see `FSPopoverViewAppearance`. 51 | public var titleFont: UIFont? = FSPopoverViewAppearance.shared.textFont 52 | 53 | /// If nil, use a default color. 54 | /// Default value see `FSPopoverViewAppearance`. 55 | public var titleColor: UIColor? = FSPopoverViewAppearance.shared.textColor 56 | 57 | // MARK: Properties/Override 58 | 59 | public override var cellType: FSPopoverListCell.Type { 60 | return FSPopoverListTextCell.self 61 | } 62 | 63 | // MARK: Initialization 64 | 65 | public override init(scrollDirection: FSPopoverListView.ScrollDirection = .vertical) { 66 | switch scrollDirection { 67 | case .vertical: 68 | contentInset = .init(top: 8.0, left: 15.0, bottom: 8.0, right: 15.0) 69 | case .horizontal: 70 | contentInset = .init(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) 71 | } 72 | super.init(scrollDirection: scrollDirection) 73 | } 74 | 75 | // MARK: Public 76 | 77 | /// You need to call this method if you change any contents. 78 | public func updateLayout() { 79 | titleFont = titleFont ?? FSPopoverViewAppearance.shared.textFont 80 | titleColor = titleColor ?? FSPopoverViewAppearance.shared.textColor 81 | switch scrollDirection { 82 | case .vertical: 83 | p_updateLayoutForVertical() 84 | case .horizontal: 85 | p_updateLayoutForHorizontal() 86 | } 87 | } 88 | 89 | // MARK: Private 90 | 91 | private func p_updateLayoutForVertical() { 92 | var size = CGSize.zero 93 | let imageSize = image?.size 94 | let titleSize = p_titleSize() 95 | size.height = max(imageSize?.height ?? 0, titleSize?.height ?? 0) 96 | size.height += contentInset.top + contentInset.bottom 97 | size.width += contentInset.left 98 | if let width = imageSize?.width { 99 | size.width += width + spacing 100 | } 101 | if let width = titleSize?.width { 102 | size.width += width 103 | } 104 | size.width += contentInset.right 105 | self.size = size 106 | } 107 | 108 | private func p_updateLayoutForHorizontal() { 109 | var size = CGSize.zero 110 | let imageSize = image?.size 111 | let titleSize = p_titleSize() 112 | size.width = max(imageSize?.width ?? 0, titleSize?.width ?? 0) 113 | size.width += contentInset.left + contentInset.right 114 | size.height += contentInset.top 115 | if let height = imageSize?.height { 116 | size.height += height + spacing 117 | } 118 | if let height = titleSize?.height { 119 | size.height += height 120 | } 121 | size.height += contentInset.bottom 122 | self.size = size 123 | } 124 | 125 | private func p_titleSize() -> CGSize? { 126 | guard let title = title, !title.isEmpty else { 127 | return nil 128 | } 129 | let titleFont = titleFont ?? FSPopoverViewAppearance.shared.textFont 130 | let attributedTitle = NSAttributedString(string: title, attributes: [.font: titleFont]) 131 | return NSAttributedString.inner.size(of: attributedTitle) 132 | } 133 | } 134 | 135 | 136 | // MARK: - FSPopoverListTextCell 137 | 138 | private class FSPopoverListTextCell: FSPopoverListCell { 139 | 140 | // MARK: Properties/Private 141 | 142 | private let imageView = UIImageView() 143 | private let titleLabel = UILabel() 144 | 145 | private lazy var stackView: UIStackView = { 146 | let stack = UIStackView() 147 | stack.axis = .horizontal 148 | stack.alignment = .center 149 | stack.translatesAutoresizingMaskIntoConstraints = false 150 | return stack 151 | }() 152 | 153 | private var stackConstraints = [NSLayoutConstraint]() 154 | 155 | // MARK: Override 156 | 157 | override func didInitialize() { 158 | addSubview(stackView) 159 | } 160 | 161 | override func renderContents() { 162 | super.renderContents() 163 | guard let item = item as? FSPopoverListTextItem else { 164 | return 165 | } 166 | stackView.axis = item.scrollDirection == .vertical ? .horizontal : .vertical 167 | stackView.alpha = item.isEnabled ? 1.0 : 0.5 168 | stackView.spacing = item.spacing 169 | do { 170 | let views = stackView.arrangedSubviews 171 | views.forEach { $0.removeFromSuperview() } 172 | } 173 | if let image = item.image { 174 | imageView.image = image 175 | stackView.addArrangedSubview(imageView) 176 | } 177 | if let title = item.title, !title.isEmpty { 178 | titleLabel.text = title 179 | titleLabel.font = item.titleFont 180 | titleLabel.textColor = item.titleColor 181 | stackView.addArrangedSubview(titleLabel) 182 | } 183 | p_remakeConstraints() 184 | } 185 | 186 | // MARK: Private 187 | 188 | private func p_remakeConstraints() { 189 | guard let item = item as? FSPopoverListTextItem else { 190 | return 191 | } 192 | NSLayoutConstraint.deactivate(stackConstraints) 193 | stackConstraints.removeAll() 194 | switch item.scrollDirection { 195 | case .vertical: 196 | let leading = NSLayoutConstraint(item: stackView, 197 | attribute: .leading, 198 | relatedBy: .equal, 199 | toItem: self, 200 | attribute: .leading, 201 | multiplier: 1.0, 202 | constant: item.contentInset.left) 203 | let trailing = NSLayoutConstraint(item: stackView, 204 | attribute: .trailing, 205 | relatedBy: .lessThanOrEqual, 206 | toItem: self, 207 | attribute: .trailing, 208 | multiplier: 1.0, 209 | constant: -item.contentInset.right) 210 | let centerY = NSLayoutConstraint(item: stackView, 211 | attribute: .centerY, 212 | relatedBy: .equal, 213 | toItem: self, 214 | attribute: .centerY, 215 | multiplier: 1.0, 216 | constant: 0.0) 217 | stackConstraints = [leading, trailing, centerY] 218 | case .horizontal: 219 | let top = NSLayoutConstraint(item: stackView, 220 | attribute: .top, 221 | relatedBy: .lessThanOrEqual, 222 | toItem: self, 223 | attribute: .top, 224 | multiplier: 1.0, 225 | constant: item.contentInset.top) 226 | let centerX = NSLayoutConstraint(item: stackView, 227 | attribute: .centerX, 228 | relatedBy: .equal, 229 | toItem: self, 230 | attribute: .centerX, 231 | multiplier: 1.0, 232 | constant: 0.0) 233 | let centerY = NSLayoutConstraint(item: stackView, 234 | attribute: .centerY, 235 | relatedBy: .lessThanOrEqual, 236 | toItem: self, 237 | attribute: .centerY, 238 | multiplier: 1.0, 239 | constant: 0.0) 240 | let width = NSLayoutConstraint(item: stackView, 241 | attribute: .width, 242 | relatedBy: .lessThanOrEqual, 243 | toItem: self, 244 | attribute: .width, 245 | multiplier: 1.0, 246 | constant: 0.0) 247 | centerY.priority = .defaultHigh 248 | stackConstraints = [top, centerX, centerY, width] 249 | } 250 | addConstraints(stackConstraints) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Source/Classes/List/Item/FSPopoverListItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverListItem.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/23. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Abstract base class, cannot be used directly, must be inherited for use. 12 | open class FSPopoverListItem { 13 | 14 | public enum ReloadType { 15 | /// Only update contents of the cell binding with the 16 | /// current item, no update in size and other cells. 17 | case rerender 18 | /// Reload the whole contents of FSPopoverListView. 19 | case reload 20 | } 21 | 22 | public enum SelectionStyle { 23 | /// The cell has no distinct style for when it's selected. 24 | case none 25 | /// The cell has a gray background when it's selected. 26 | case gray 27 | } 28 | 29 | // MARK: Properties/Open 30 | 31 | /// The size of item. 32 | /// 33 | /// - The cell's frame.size may not equal to this size. 34 | /// 35 | /// - When the scroll direction of FSPopoverListView is `vertical`, 36 | /// the list view picks out the largest `size.width` from its items as its width. 37 | /// 38 | /// - When the scroll direction of FSPopoverListView is `horizontal`, 39 | /// the list view picks out the largest `size.height` from its items as its height. 40 | /// 41 | open var size: CGSize = .zero 42 | 43 | /// Type of the cell that binding with the item. 44 | /// 45 | /// - This type must be the subclass of FSPopoverListCell. 46 | /// 47 | open var cellType: FSPopoverListCell.Type { 48 | return FSPopoverListCell.self 49 | } 50 | 51 | // MARK: Properties/Public 52 | 53 | public let scrollDirection: FSPopoverListView.ScrollDirection 54 | 55 | public final var selectionStyle: FSPopoverListItem.SelectionStyle = .gray 56 | 57 | /// A closure to execute when the user selects the item. 58 | /// This closure has no return value and takes the selected item object as its only parameter. 59 | public final var selectedHandler: ((_ item: FSPopoverListItem) -> Void)? 60 | 61 | /// Whether the item is enabled. Defaults to true. 62 | public final var isEnabled = true 63 | 64 | /// Default value see `FSPopoverViewAppearance`. 65 | public final var separatorInset: UIEdgeInsets = FSPopoverViewAppearance.shared.separatorInset 66 | 67 | /// The color of separator. 68 | /// Default value see `FSPopoverViewAppearance`. 69 | public final var separatorColor: UIColor? = FSPopoverViewAppearance.shared.separatorColor 70 | 71 | /// Whether needs hide separator, defaults to true. 72 | /// 73 | /// - The separator is on the bottom when scroll direction is vertical. 74 | /// - The separator is on the right when scroll direction is horizontal. 75 | /// 76 | public final var isSeparatorHidden = true 77 | 78 | /// The color of separator. 79 | /// Default value see `FSPopoverViewAppearance`. 80 | public final var highlightedColor: UIColor? = FSPopoverViewAppearance.shared.highlightedColor 81 | 82 | // MARK: Properties/Internal 83 | 84 | /// Used for reload operation. 85 | /// 86 | /// - Warning: 87 | /// * ⚠️ Leave this closure alone, you should never use it. 88 | /// 89 | final var reloadHandler: ((FSPopoverListItem, FSPopoverListItem.ReloadType) -> Void)? 90 | 91 | // MARK: Initialization 92 | 93 | public init(scrollDirection: FSPopoverListView.ScrollDirection = .vertical) { 94 | #if DEBUG 95 | let abstractName = String(describing: FSPopoverListItem.self) 96 | if "\(type(of: self))" == abstractName { 97 | fatalError("\(abstractName) is abstract base class, cannot be used directly, must be inherited for use.") 98 | } 99 | #endif 100 | self.scrollDirection = scrollDirection 101 | } 102 | 103 | // MARK: Public 104 | 105 | /// Requests a reload operation for current item. 106 | /// This method will not work if the cell has not been added to list view. 107 | public final func reload(_ type: FSPopoverListItem.ReloadType = .rerender) { 108 | reloadHandler?(self, type) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Source/Classes/Transition/Defaults/FSPopoverViewTransitionFade.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverViewTransitionFade.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/23. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class FSPopoverViewTransitionFade: FSPopoverViewAnimatedTransitioning { 12 | 13 | public init() {} 14 | 15 | public func animateTransition(transitionContext context: FSPopoverViewTransitionContext) { 16 | 17 | let popoverView = context.popoverView 18 | let dimBackgroundView = context.dimBackgroundView 19 | 20 | switch context.scene { 21 | case .present: 22 | popoverView.alpha = 0.0 23 | dimBackgroundView.alpha = 0.0 24 | UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { 25 | popoverView.alpha = 1.0 26 | dimBackgroundView.alpha = 1.0 27 | } completion: { _ in 28 | context.completeTransition() 29 | } 30 | case .dismiss(_): 31 | UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { 32 | popoverView.alpha = 0.0 33 | dimBackgroundView.alpha = 0.0 34 | } completion: { _ in 35 | popoverView.alpha = 1.0 36 | dimBackgroundView.alpha = 1.0 37 | context.completeTransition() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Source/Classes/Transition/Defaults/FSPopoverViewTransitionScale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverViewTransitionScale.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/15. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class FSPopoverViewTransitionScale: FSPopoverViewAnimatedTransitioning { 12 | 13 | public var usingSpring = true 14 | 15 | public init() {} 16 | 17 | public func animateTransition(transitionContext context: FSPopoverViewTransitionContext) { 18 | 19 | let popoverView = context.popoverView 20 | let arrowPoint = popoverView.arrowPoint 21 | let dimBackgroundView = context.dimBackgroundView 22 | 23 | // anchor point 24 | do { 25 | let frame = popoverView.frame 26 | popoverView.layer.anchorPoint = { 27 | var x: CGFloat = 0.0, y: CGFloat = 0.0 28 | let arrowPointInPopover = CGPoint(x: arrowPoint.x - frame.minX, y: arrowPoint.y - frame.minY) 29 | switch popoverView.arrowDirection { 30 | case .up: 31 | x = arrowPointInPopover.x / frame.width 32 | case .down: 33 | x = arrowPointInPopover.x / frame.width 34 | y = 1.0 35 | case .left: 36 | y = arrowPointInPopover.y / frame.height 37 | case .right: 38 | x = 1.0 39 | y = arrowPointInPopover.y / frame.height 40 | } 41 | return .init(x: x, y: y) 42 | }() 43 | // Needs to reset frame again after updating `layer.anchorPoint`, 44 | // or the popover view will be in the wrong place. 45 | popoverView.frame = frame 46 | } 47 | 48 | switch context.scene { 49 | case .present: 50 | dimBackgroundView.alpha = 0.0 51 | UIView.animate(withDuration: 0.18, delay: 0.0, options: .curveEaseOut) { 52 | dimBackgroundView.alpha = 1.0 53 | } 54 | 55 | popoverView.transform = .init(scaleX: 0.01, y: 0.01) 56 | let duration: TimeInterval = usingSpring ? 0.38 : 0.18 57 | let spring: CGFloat = usingSpring ? 0.68 : 1.0 58 | UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: spring, initialSpringVelocity: 0.5) { 59 | popoverView.transform = .identity 60 | } completion: { _ in 61 | context.completeTransition() 62 | } 63 | case .dismiss(let isSelection): 64 | UIView.animate(withDuration: isSelection ? 0.25 : 0.18, delay: 0.0, options: .curveEaseOut) { 65 | popoverView.alpha = 0.0 66 | dimBackgroundView.alpha = 0.0 67 | if !isSelection { 68 | popoverView.transform = .init(scaleX: 0.01, y: 0.01) 69 | } 70 | } completion: { _ in 71 | popoverView.alpha = 1.0 72 | popoverView.transform = .identity 73 | dimBackgroundView.alpha = 1.0 74 | context.completeTransition() 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Source/Classes/Transition/Defaults/FSPopoverViewTransitionTranslate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverViewTransitionTranslate.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/23. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class FSPopoverViewTransitionTranslate: FSPopoverViewAnimatedTransitioning { 12 | 13 | public init() {} 14 | 15 | public func animateTransition(transitionContext context: FSPopoverViewTransitionContext) { 16 | 17 | guard let containerView = context.popoverView.containerView else { 18 | context.completeTransition() 19 | return 20 | } 21 | 22 | let popoverView = context.popoverView 23 | let dimBackgroundView = context.dimBackgroundView 24 | 25 | let popoverFrame = popoverView.frame 26 | let containerFrame = containerView.frame 27 | let popoverInitialFrame: CGRect = { 28 | var frame = popoverFrame 29 | switch popoverView.arrowDirection { 30 | case .up: 31 | frame.origin.y = containerFrame.maxY 32 | case .down: 33 | frame.origin.y = containerFrame.minY - frame.height 34 | case .left: 35 | frame.origin.x = containerFrame.maxX 36 | case .right: 37 | frame.origin.x = containerFrame.minX - frame.width 38 | } 39 | return frame 40 | }() 41 | 42 | switch context.scene { 43 | case .present: 44 | popoverView.frame = popoverInitialFrame 45 | dimBackgroundView.alpha = 0.0 46 | UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { 47 | popoverView.frame = popoverFrame 48 | dimBackgroundView.alpha = 1.0 49 | } completion: { _ in 50 | context.completeTransition() 51 | } 52 | case .dismiss(let isSelection): 53 | UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { 54 | if isSelection { 55 | popoverView.alpha = 0.0 56 | } else { 57 | popoverView.frame = popoverInitialFrame 58 | } 59 | dimBackgroundView.alpha = 0.0 60 | } completion: { _ in 61 | popoverView.alpha = 1.0 62 | dimBackgroundView.alpha = 1.0 63 | context.completeTransition() 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Source/Classes/Transition/FSPopoverViewAnimatedTransitioning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverViewAnimatedTransitioning.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/15. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol FSPopoverViewAnimatedTransitioning: AnyObject { 12 | 13 | /// This method will be called when presenting or dismissing a popover view. 14 | /// Use this method to configure the animations associated with your custom transition. 15 | /// 16 | /// - Important: 17 | /// * You must call the method `completeTransition()` of `context` when the transition is finished. 18 | /// Otherwise the popover view will work unexpected. 19 | /// 20 | func animateTransition(transitionContext context: FSPopoverViewTransitionContext) 21 | } 22 | -------------------------------------------------------------------------------- /Source/Classes/Transition/FSPopoverViewTransitionContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPopoverViewTransitionContext.swift 3 | // FSPopoverView 4 | // 5 | // Created by Sheng on 2023/11/23. 6 | // Copyright © 2023 Sheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class FSPopoverViewTransitionContext { 12 | 13 | // MARK: Sence 14 | 15 | public enum Scene { 16 | 17 | case present 18 | 19 | /// - Prameters: 20 | /// - isSelection: This value is usually used in a list. 21 | case dismiss(_ isSelection: Bool = false) 22 | } 23 | 24 | // MARK: Properties/Public 25 | 26 | public let scene: FSPopoverViewTransitionContext.Scene 27 | 28 | public let popoverView: FSPopoverView 29 | 30 | public let dimBackgroundView: UIView 31 | 32 | // MARK: Properties/Internal 33 | 34 | var onDidCompleteTransition: (() -> Void)? 35 | 36 | // MARK: Initialization 37 | 38 | init(scene: FSPopoverViewTransitionContext.Scene, popoverView: FSPopoverView, dimBackgroundView: UIView) { 39 | self.scene = scene 40 | self.popoverView = popoverView 41 | self.dimBackgroundView = dimBackgroundView 42 | } 43 | 44 | // MARK: Public 45 | 46 | /// When the animation ends, you must call this method to 47 | /// tell the popover view that the animation has ended. 48 | public func completeTransition() { 49 | onDidCompleteTransition?() 50 | } 51 | } 52 | --------------------------------------------------------------------------------