Skip to content

Commit

Permalink
Feat UIKit Tooltip (#232)
Browse files Browse the repository at this point in the history
* 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
kevinneko authored Jul 10, 2024
1 parent 5da0586 commit babc0d4
Show file tree
Hide file tree
Showing 13 changed files with 836 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public final class ContentViewController: UIViewController {
case colors = "Colors"
case typographies = "Typographies"
case icons = "Icons"
case tooltips = "Tooltips"
case spinners = "Spinners"

var viewController: UIViewController {
Expand All @@ -65,6 +66,8 @@ public final class ContentViewController: UIViewController {
return SelectionsViewController()
case .textFields:
return TextFieldsViewController()
case .tooltips:
return TooltipsViewController()
case .spinners:
return SpinnersViewController()
}
Expand Down
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
}
}
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
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let label = CharcoalTypography20()
label.isBold = true
label.text = "Hello"

let buttton = CharcoalPrimaryMButton()
let button = CharcoalPrimaryMButton()
button.setTitle("OK", for: .normal)
```

Expand Down
2 changes: 1 addition & 1 deletion Sources/Charcoal/Charcoal.docc/Charcoal.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ let label = CharcoalTypography20()
label.isBold = true
label.text = "Hello"

let buttton = CharcoalPrimaryMButton()
let button = CharcoalPrimaryMButton()
button.setTitle("OK", for: .normal)
```

Expand Down
139 changes: 139 additions & 0 deletions Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift
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()
}
}
}
Loading

0 comments on commit babc0d4

Please sign in to comment.