Skip to content

Commit

Permalink
Feat UIKit Balloon (#252)
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

* Add initial Snackbar

* Add thumbnail image

* Add support for thumbnailImage and action

* Clean code

* Reformat code

* Rename ActionContent

* Replace thumbnailImage type

* Add dismissOnTouchOutside control

* Add comment on CharcoalIdentifiableOverlayView

* Update CharcoalTooltip.swift

* Add SnackBar demo

* Replace thumbnail with charcoal logo

* Use @ViewBuilder

* Clean Code

* Made code more readable

* Update ToastsView.swift

* Add auto dismiss logic

* Fix dismiss comment

* Add Identifiable to CharcoalIdentifiableOverlayView

* Make all CharcoalPopupView identifiable

* Move all control logic into CharcoalPopupView

* Reformat

* Refine CharcoalOverlayContainerChild logic of updating view

* Rename to CharcoalOverlayUpdaterContainer

* Add CharcoalToast

* Refine toast control

* Refine screen edge of toast

* Refine comments

* Rename CharcoalPopupProtocol

* Refine isActuallyPresenting logic

* Clean animation

* Add animation configuration

* Add custom animation

* Add CharcoalToastProtocol

* Makes CharcoalSnackBar adapt CharcoalToastProtocol

* Remove time delay

* Refine SnackBar Animation logic

* Add CharcoalToastAnimationModifier

* Reformat code

* Update CharcoalPopupViewEdge of direction

* Refine demo

* Fix missing animation

* Rename charcoalAnimatedToast to charcoalAnimatableToast

* Rename CharcoalAnimatableToastProtocol

* Rename for clean

* Simplify protocols

* Add drag control

* Add Dismiss timer control logic

* Refine drag damping logic

* Add CharcoalToastDraggable

* Use CharcoalToastDraggableModifier on CharcoalSnackBar

* Format code

* Add init structure

* Add action button

* Replace placeholder with ja text

* Use CGPath union for iOS 16

* Update TooltipBubbleShape.swift

* Update CharcoalTooltip.swift

* Use GeometryReader on overlay

* Use new path draw logic

* Refine arrow width

* Refine arrow width

* Clean Code

* Refine preview

* Use timer instead of DispatchQueue

* Refine layout logic

* Refine arrow logic

* Refine layout logic

* Add missing charcoalOverlayContainer

* Add Balloon to examples

* Add default tutorials

* Remove overlay when disappear

* Refine charcoalOverlayContainer place

* Refine

* Update CharcoalSnackBar.swift

* 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

* Add CharcoalToastView

* change cornerRadius

* Add CharcoalToast

* Move show logic out

* Reformat

* Refine dismiss method

* Update CharcoalToast.swift

* Add ActionContent and ActionComplete callback

* Update CharcoalToast.swift

* Update CharcoalBubbleShape_UIKit.swift

* Refine layout animation logic

* Refine animation

* Add dismiss

* Add example

* Reformat

* Add toasts example

* Add CharcoalSnackBarView

* Update project.pbxproj

* Clean code

* Refine layout logic

* Add CharcoalSnackBar

* Update CharcoalSnackBar.swift

* Update CharcoalSnackBar.swift

* Refine layout logic

* Refine toasts text

* Update CharcoalToastDraggableModifier.swift

* Add CharcoalRubberGesture

* use id to notify did dismiss

* Add rubber gesture

* Refactor

* Refactor

* Fix memory leak

* Format

* Fix memory leak

* Add example

* Refine snackbar

* Refine swift lint

* Refactor CharcoalToastView

* Refactor CharcoalSnackBarView

* Reformat

* Refine self logic

* Add initial Balloon

* Add line stroke

* Refine radius

* Fix the baseline alignment

* Layout balloon elements

* Clean

* Add balloon view

* Update layout logic

* Refine balloon layout logic

* Update CharcoalBalloonView.swift

* Add demo

* Add anchor view tracking

* Move logics into updateConstraint

* Update CharcoalBalloon.swift

* Reformat code

* reformat

* Update formats and documents

* Clean code

* Refine documents

* Update CharcoalBalloon.swift

* Update CharcoalBalloon.swift

* Reformat

* Fix name

* Fix geometry

* Revert "Fix geometry"

This reverts commit a89bf66.

* Fix proxy name

* Adjust unused text

* Replace charcoal logo

* Remove conditional modifier

* Add default dismiss time to toasts

* Reformat

* Reformat

* Fix logo image

* Fix balloon name

* Update Balloons.swift

* Remove CharcoalRubberGesture

* Remove unused UIColor Extension

* Clean Code

* Clean proxy usage

* Update Package.resolved

* Add example to dismiss balloons with view
  • Loading branch information
kevinneko authored Oct 9, 2024
1 parent 5fdd8e9 commit af55efc
Show file tree
Hide file tree
Showing 13 changed files with 801 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public final class ContentViewController: UIViewController {
case tooltips = "Tooltips"
case toasts = "Toasts"
case spinners = "Spinners"
case balloons = "Balloons"

var viewController: UIViewController {
switch self {
Expand All @@ -73,6 +74,8 @@ public final class ContentViewController: UIViewController {
return ToastsViewController()
case .spinners:
return SpinnersViewController()
case .balloons:
return BalloonsViewController()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import UIKit

class BalloonTableViewCell: 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)
])
}

@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,149 @@
import Charcoal
import UIKit

enum BalloonTitles: 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"
}
}

func configCell(cell: BalloonTableViewCell) {
cell.titleLabel.text = rawValue
switch self {
case .leading:
cell.leadingImageView.image = CharcoalAsset.Images.info24.image
case .trailing:
cell.accessoryImageView.image = CharcoalAsset.Images.info24.image
case .bottom:
break
}
}
}

public final class BalloonsViewController: 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 "Balloons"
}
}

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 BalloonsViewController: UITableViewDelegate, UITableViewDataSource {
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section = Sections.allCases[indexPath.section]

let cellIdentifier = BalloonTableViewCell.identifier
let cell: BalloonTableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as? BalloonTableViewCell ?? BalloonTableViewCell(style: .default, reuseIdentifier: cellIdentifier)

switch section {
case .components:
let titleCase = BalloonTitles.allCases[indexPath.row]
titleCase.configCell(cell: cell)
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! BalloonTableViewCell
tableView.deselectRow(at: indexPath, animated: true)
let titleCase = BalloonTitles.allCases[indexPath.row]
switch titleCase {
case .leading:
// use on: self.view to stick the balloon to the view
// so balloons will dismiss with the view
CharcoalBalloon.show(text: titleCase.text, anchorView: cell.leadingImageView, on: self.view)
case .trailing:
CharcoalBalloon.show(text: titleCase.text, anchorView: cell.accessoryImageView, on: self.view)
case .bottom:
CharcoalBalloon.show(text: titleCase.text, anchorView: bottomInfoImage, on: self.view)
}
}

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 = BalloonsViewController()
return viewController
}
35 changes: 35 additions & 0 deletions Sources/CharcoalShared/Enums/CharocoalLayoutPriority.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

public enum CharcoalTooltipLayoutPriority: Codable {
case bottom
case top
case right
case left

public var order: Int {
switch self {
case .bottom:
return 0
case .top:
return 1
case .right:
return 2
case .left:
return 3
}
}
}

public struct LayoutPriority {
public var priority: CharcoalTooltipLayoutPriority
public var spaceArea: CGSize

public var rect: CGRect {
return CGRect(x: 0, y: 0, width: spaceArea.width, height: spaceArea.height)
}

public init(priority: CharcoalTooltipLayoutPriority, spaceArea: CGSize) {
self.priority = priority
self.spaceArea = spaceArea
}
}
7 changes: 7 additions & 0 deletions Sources/CharcoalShared/Extensions/CGSize+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

public extension CGSize {
var area: CGFloat {
return width * height
}
}
10 changes: 10 additions & 0 deletions Sources/CharcoalShared/Extensions/Rect+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

public extension CGRect {
// Calculate the intersection area of two rectangles
func intersectionArea(_ rect: CGRect) -> CGFloat {
let rect = intersection(rect)

return rect.width * rect.height
}
}
22 changes: 0 additions & 22 deletions Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,5 @@
import SwiftUI

enum CharcoalTooltipLayoutPriority: Codable {
case bottom
case top
case right
case left
}

struct LayoutPriority {
var priority: CharcoalTooltipLayoutPriority
var spaceArea: CGSize

var rect: CGRect {
return CGRect(x: 0, y: 0, width: spaceArea.width, height: spaceArea.height)
}
}

extension CGSize {
var area: CGFloat {
return width * height
}
}

struct CharcoalBalloon<ActionContent: View>: CharcoalPopupProtocol, CharcoalToastActionable {
typealias IDValue = UUID

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import UIKit

class CharcoalAnchorTrackingView: UIView {
private var displayLink: CADisplayLink?
private var lastFrame: CGRect?

var locationDidUpdated: ((UIView?) -> Void)?

override init(frame: CGRect) {
super.init(frame: frame)

addObserver(self, forKeyPath: "frame", options: [.new, .old], context: nil)

displayLink = CADisplayLink(target: self, selector: #selector(checkPosition))
displayLink?.add(to: .main, forMode: .common)
}

@objc func checkPosition() {
let globalFrame = convert(bounds, to: nil)

if lastFrame != globalFrame {
lastFrame = globalFrame
locationDidUpdated?(superview)
}
}

func invalidate() {
displayLink?.invalidate()
displayLink = nil
}

func tearDown() {
invalidate()
removeFromSuperview()
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Loading

0 comments on commit af55efc

Please sign in to comment.