-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Tooltip view * Refactor Tooltip style * Can adjust tooltip position * Add tooltip playground for testing * Refine layout tips logic * Refine layout logic * Update CharcoalTooltip.swift * Refine tooltip spacing * Add CharcoalIdentifiableOverlayView * Use Actor to prevent Data Race * Remove CharcoalIdentifiableOverlayView out * Clean code * Only update view when it is isPresenting * Clean access control * Add TooltipsView * Fix tooltipY layout logic * Use main actor and remove CharcoalContainerManagerKey * Fix access control on CharcoalContainerManager * Make viewID as @State * Use EnviromentObject to create CharcoalContainerManager for each container * Use ObservedObject on CharcoalContainerManager * Add use charcoal button as demo trigger * Add arrow logic on tooltip * Refine arrow logic * Refine arrow layout logic * Use StateObject to prevent unexpected reinit * Refactor TooltipBubbleShape * Fix edge layout logic * Add comment * Format code * Use new approach to remove adaptiveMaxWidth * Fix the tip bubble's position latency * Add dismiss when interaction * Reformat * Init CharcoalTooltipView * Init Bubble shape * Refine tooltip preview * Rename as Charcoal Bubble Shape * Add Label to tooltip * Update text frame when traitCollection did change * Update CharcoalTooltipView.swift * Add CharcoalTooltip * Can debug show on method * Can layout point * Can redraw target point * Update CharcoalTooltip.swift * Refine tooltip display * Share the logic * Use interaction mode * Use CharcoalOverlayContainerView * Update CharcoalOverlay.swift * Refactor to ChacoalOverlayManager * Makes CharcoalIdentifiableOverlayView Identifiable * Refine layout logic * Add display(view: CharcoalIdentifiableOverlayView) * Add tooltip to uikit example * Update Tooltips.swift * Add CharcoalIdentifiableOverlayDelegate * Reformat * Update StringExtension.swift * Add to UIKitSample * Fix public requirements * Reformat * Use touch began to handle dismiss on touch * Update CharcoalIdentifiableOverlayView.swift * Move show logic out * Reformat * Refine dismiss method * Add ActionContent and ActionComplete callback * Update CharcoalBubbleShape_UIKit.swift * Make use of Autolayout logic * Reformat * Refine deinit * Move config cell to TooltipTableViewCell * guard backgroundView == nil * Remove public of ChacoalOverlayManager * Make tooltipXY private * Reformat * refactor backgroundView
- Loading branch information
Showing
13 changed files
with
836 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import CharcoalShared | ||
import UIKit | ||
|
||
class TooltipTableViewCell: UITableViewCell { | ||
static let identifier = "TooltipCell" | ||
|
||
let titleLabel: UILabel = { | ||
let label = UILabel() | ||
label.font = UIFont.systemFont(ofSize: 16, weight: .medium) | ||
label.textColor = UIColor.black | ||
label.translatesAutoresizingMaskIntoConstraints = false | ||
return label | ||
}() | ||
|
||
let leadingImageView: UIImageView = { | ||
let imageView = UIImageView() | ||
imageView.contentMode = .scaleAspectFit | ||
imageView.translatesAutoresizingMaskIntoConstraints = false | ||
return imageView | ||
}() | ||
|
||
let accessoryImageView: UIImageView = { | ||
let imageView = UIImageView() | ||
imageView.contentMode = .scaleAspectFit | ||
imageView.translatesAutoresizingMaskIntoConstraints = false | ||
return imageView | ||
}() | ||
|
||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { | ||
super.init(style: style, reuseIdentifier: reuseIdentifier) | ||
|
||
contentView.addSubview(titleLabel) | ||
contentView.addSubview(leadingImageView) | ||
contentView.addSubview(accessoryImageView) | ||
|
||
NSLayoutConstraint.activate([ | ||
leadingImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), | ||
leadingImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) | ||
]) | ||
|
||
NSLayoutConstraint.activate([ | ||
titleLabel.leadingAnchor.constraint(equalTo: leadingImageView.trailingAnchor, constant: 10), | ||
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) | ||
]) | ||
|
||
NSLayoutConstraint.activate([ | ||
accessoryImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), | ||
accessoryImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) | ||
]) | ||
} | ||
|
||
func configCell(type: TooltipTitles) { | ||
titleLabel.text = type.rawValue | ||
switch type { | ||
case .leading: | ||
leadingImageView.image = CharcoalAsset.Images.info24.image | ||
case .trailing: | ||
accessoryImageView.image = CharcoalAsset.Images.info24.image | ||
case .bottom: | ||
break | ||
} | ||
} | ||
|
||
@available(*, unavailable) | ||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
override func layoutSubviews() { | ||
super.layoutSubviews() | ||
} | ||
|
||
override func prepareForReuse() { | ||
super.prepareForReuse() | ||
titleLabel.text = nil | ||
leadingImageView.image = nil | ||
} | ||
} |
135 changes: 135 additions & 0 deletions
135
CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import Charcoal | ||
import UIKit | ||
|
||
enum TooltipTitles: String, CaseIterable { | ||
case leading = "Leading" | ||
case trailing = "Trailing" | ||
case bottom = "Bottom" | ||
|
||
var text: String { | ||
switch self { | ||
case .leading: | ||
return "Hello World" | ||
case .trailing: | ||
return "Hello World This is a tooltip with mutiple line" | ||
case .bottom: | ||
return "こんにちは This is a tooltip and here is testing it's multiple line feature" | ||
} | ||
} | ||
} | ||
|
||
public final class TooltipsViewController: UIViewController { | ||
private lazy var tableView: UITableView = { | ||
let view = UITableView(frame: .zero, style: .insetGrouped) | ||
view.translatesAutoresizingMaskIntoConstraints = false | ||
return view | ||
}() | ||
|
||
lazy var bottomInfoImage: UIImageView = { | ||
let imageView = UIImageView(image: CharcoalAsset.Images.info16.image) | ||
imageView.translatesAutoresizingMaskIntoConstraints = false | ||
return imageView | ||
}() | ||
|
||
private enum Sections: Int, CaseIterable { | ||
case components | ||
|
||
var title: String { | ||
switch self { | ||
case .components: | ||
return "Tooltips" | ||
} | ||
} | ||
|
||
var items: [any CaseIterable] { | ||
switch self { | ||
case .components: | ||
return TooltipTitles.allCases | ||
} | ||
} | ||
} | ||
|
||
private enum SettingsTitles: String, CaseIterable { | ||
case darkMode = "Dark Mode" | ||
case fixedSizeCategory = "Fixed Size Category" | ||
} | ||
|
||
override public func viewDidLoad() { | ||
super.viewDidLoad() | ||
setupNavigationBar() | ||
setupUI() | ||
} | ||
|
||
private func setupNavigationBar() { | ||
navigationItem.title = "Charcoal" | ||
} | ||
|
||
private func setupUI() { | ||
view.addSubview(tableView) | ||
|
||
NSLayoutConstraint.activate([ | ||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | ||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | ||
tableView.topAnchor.constraint(equalTo: view.topAnchor), | ||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) | ||
]) | ||
|
||
tableView.dataSource = self | ||
tableView.delegate = self | ||
|
||
view.addSubview(bottomInfoImage) | ||
|
||
NSLayoutConstraint.activate([ | ||
bottomInfoImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), | ||
bottomInfoImage.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20) | ||
]) | ||
} | ||
} | ||
|
||
extension TooltipsViewController: UITableViewDelegate, UITableViewDataSource { | ||
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | ||
let section = Sections.allCases[indexPath.section] | ||
|
||
let cellIdentifier = TooltipTableViewCell.identifier | ||
let cell: TooltipTableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as? TooltipTableViewCell ?? TooltipTableViewCell(style: .default, reuseIdentifier: cellIdentifier) | ||
|
||
switch section { | ||
case .components: | ||
let titleCase = TooltipTitles.allCases[indexPath.row] | ||
cell.configCell(type: titleCase) | ||
return cell | ||
} | ||
} | ||
|
||
public func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { | ||
return Sections.allCases[section].items.count | ||
} | ||
|
||
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | ||
let cell = tableView.cellForRow(at: indexPath) as! TooltipTableViewCell | ||
tableView.deselectRow(at: indexPath, animated: true) | ||
let titleCase = TooltipTitles.allCases[indexPath.row] | ||
switch titleCase { | ||
case .leading: | ||
CharcoalTooltip.show(text: titleCase.text, anchorView: cell.leadingImageView) | ||
case .trailing: | ||
CharcoalTooltip.show(text: titleCase.text, anchorView: cell.accessoryImageView) | ||
case .bottom: | ||
CharcoalTooltip.show(text: titleCase.text, anchorView: bottomInfoImage) | ||
} | ||
} | ||
|
||
public func numberOfSections(in tableView: UITableView) -> Int { | ||
return Sections.allCases.count | ||
} | ||
|
||
public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | ||
return Sections.allCases[section].title | ||
} | ||
} | ||
|
||
@available(iOS 17.0, *) | ||
#Preview { | ||
let viewController = TooltipsViewController() | ||
return viewController | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
139 changes: 139 additions & 0 deletions
139
Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import UIKit | ||
|
||
/** | ||
Displays a overlay on the screen. | ||
*/ | ||
class ChacoalOverlayManager: UIView { | ||
/// The window to display the overlays in. | ||
var mainView: UIView! | ||
/// The background view of the overlays. | ||
var backgroundView: UIView? | ||
/// The container view array of the overlays. | ||
var overlayContainerViews: [CharcoalIdentifiableOverlayView] = [] | ||
/// Shared instance of the overlay manager. | ||
static let shared = ChacoalOverlayManager() | ||
} | ||
|
||
// MARK: - Window | ||
|
||
extension ChacoalOverlayManager { | ||
/// Initializes the spinner with the given window. | ||
func setupSuperView(view: UIView?) { | ||
if let view = view { | ||
mainView = view | ||
} else { | ||
if mainView == nil { | ||
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } | ||
.filter { $0.activationState == .foregroundActive } | ||
.first | ||
mainView = scene?.windows.filter { $0.isKeyWindow }.first ?? | ||
UIApplication.shared.windows.first | ||
} | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Background | ||
|
||
extension ChacoalOverlayManager { | ||
private func removeBackground() { | ||
backgroundView?.removeFromSuperview() | ||
backgroundView = nil | ||
} | ||
|
||
private func setupBackground() { | ||
guard backgroundView == nil else { | ||
return | ||
} | ||
|
||
let backgroundView = UIView(frame: .zero) | ||
backgroundView.isUserInteractionEnabled = false | ||
backgroundView.translatesAutoresizingMaskIntoConstraints = false | ||
mainView.addSubview(backgroundView) | ||
|
||
let constraints: [NSLayoutConstraint] = [ | ||
backgroundView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 0), | ||
backgroundView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 0), | ||
backgroundView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: 0), | ||
backgroundView.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: 0) | ||
] | ||
|
||
NSLayoutConstraint.activate(constraints) | ||
|
||
self.backgroundView = backgroundView | ||
} | ||
} | ||
|
||
// MARK: - Container | ||
|
||
extension ChacoalOverlayManager { | ||
private func setupContainer(_ interactionMode: CharcoalOverlayInteractionMode) -> CharcoalIdentifiableOverlayView { | ||
let containerView = CharcoalIdentifiableOverlayView(interactionMode: interactionMode) | ||
containerView.alpha = 0 | ||
containerView.translatesAutoresizingMaskIntoConstraints = false | ||
mainView.addSubview(containerView) | ||
overlayContainerViews.append(containerView) | ||
let constraints: [NSLayoutConstraint] = [ | ||
containerView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), | ||
containerView.topAnchor.constraint(equalTo: mainView.topAnchor), | ||
containerView.heightAnchor.constraint(equalTo: mainView.heightAnchor), | ||
containerView.widthAnchor.constraint(equalTo: mainView.widthAnchor) | ||
] | ||
|
||
NSLayoutConstraint.activate(constraints) | ||
|
||
return containerView | ||
} | ||
} | ||
|
||
// MARK: - Show, Dismiss | ||
|
||
extension ChacoalOverlayManager { | ||
@discardableResult | ||
func layout( | ||
view: UIView, | ||
transparentBackground: Bool = false, | ||
interactionMode: CharcoalOverlayInteractionMode = .dimissOnTouch, | ||
on superView: UIView? = nil | ||
) -> CharcoalIdentifiableOverlayView { | ||
setupSuperView(view: superView) | ||
setupBackground() | ||
let containerView = setupContainer(interactionMode) | ||
containerView.addSubview(view) | ||
return containerView | ||
} | ||
|
||
func display() { | ||
for containerView in overlayContainerViews { | ||
containerView.display() | ||
} | ||
} | ||
|
||
@objc func dismiss() { | ||
for containerView in overlayContainerViews { | ||
containerView.dismiss() | ||
} | ||
} | ||
|
||
/// Displays the overlay. | ||
func display(view: CharcoalIdentifiableOverlayView) { | ||
view.display() | ||
} | ||
|
||
/// Dismisses the overlay with the given identifier. | ||
func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { | ||
let containerView = overlayContainerViews.first { $0.id == id } | ||
containerView?.dismiss() | ||
} | ||
} | ||
|
||
// MARK: - CharcoalIdentifiableOverlayDelegate | ||
|
||
extension ChacoalOverlayManager: CharcoalIdentifiableOverlayDelegate { | ||
func overlayViewDidDismiss(_ overlayView: CharcoalIdentifiableOverlayView) { | ||
overlayContainerViews = overlayContainerViews.filter { $0.id != overlayView.id } | ||
if overlayContainerViews.isEmpty { | ||
removeBackground() | ||
} | ||
} | ||
} |
Oops, something went wrong.