├── .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 | [](https://img.shields.io/badge/Platform-iOS-yellowgreen)
4 | [](https://developer.apple.com/swift/)
5 | [](https://img.shields.io/badge/ObjC-incompatible-red)
6 | [](https://cocoapods.org/pods/FSPopoverView)
7 | [](https://github.com/Carthage/Carthage)
8 | [](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange)
9 | [](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 | [](https://img.shields.io/badge/Platform-iOS-yellowgreen)
4 | [](https://developer.apple.com/swift/)
5 | [](https://img.shields.io/badge/ObjC-incompatible-red)
6 | [](https://cocoapods.org/pods/FSPopoverView)
7 | [](https://github.com/Carthage/Carthage)
8 | [](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange)
9 | [](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 |
--------------------------------------------------------------------------------