Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,10 @@ jobs:
name: "iOS 18.1"
xcode: "Xcode_16.1"
runsOn: macOS-14
- destination: "OS=17.5,name=iPhone 15 Pro"
name: "iOS 17.5"
xcode: "Xcode_15.4"
runsOn: macOS-14
- destination: "OS=17.0.1,name=iPhone 14 Pro"
name: "iOS 17.0.1"
xcode: "Xcode_15.0"
runsOn: macos-13
- destination: "OS=16.4,name=iPhone 14 Pro"
name: "iOS 16.4"
xcode: "Xcode_14.3.1"
runsOn: macos-13
steps:
- uses: actions/checkout@v3
- name: ${{ matrix.name }}
run: xcodebuild test -scheme "snacker" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1
run: xcodebuild test -scheme "Snacker" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
Expand Down
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 5 additions & 13 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,13 @@
import PackageDescription

let package = Package(
name: "snacker",
name: "Snacker",
platforms: [.iOS(.v17)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "snacker",
targets: ["snacker"]),
.library(name: "Snacker", targets: ["Snacker"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "snacker"),
.testTarget(
name: "snackerTests",
dependencies: ["snacker"]
),
.target(name: "Snacker"),
.testTarget(name: "SnackerTests", dependencies: ["Snacker"]),
]
)
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<a href="https://github.com/space-code/snacker"><img alt="CI" src="https://github.com/space-code/snacker/actions/workflows/ci.yml/badge.svg?branch=main"></a>

## Description
`snacker` description.
`snacker` is a lightweight Swift library for displaying snackbars in iOS applications.

- [Usage](#usage)
- [Requirements](#requirements)
Expand All @@ -19,8 +19,28 @@

## Usage

```swift
import Snackner

Snacker.shared.action(
.snack(
view: view,
data: SnackbarData(
snackbarAlignment: .top(spacing: 20),
insets: .zero,
animationDuration: 0.25
)
),
container: window
)
```

## Requirements

- iOS 17.0+
- Xcode 16.0
- Swift 6.0

## Installation
### Swift Package Manager

Expand Down Expand Up @@ -52,4 +72,4 @@ Please feel free to help out with this project! If you see something that could
Nikita Vasilev, nv3212@gmail.com

## License
snacker is available under the MIT license. See the LICENSE file for more info.
snacker is available under the MIT license. See the LICENSE file for more info.
110 changes: 110 additions & 0 deletions Sources/snacker/Classes/Core/Snacker/Snacker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// snacker
// Copyright © 2025 Space Code. All rights reserved.
//

import UIKit

// MARK: - ISnacker

@MainActor
public protocol ISnacker {
func action(_ action: SnackbarAction, container: UIView)
}

// MARK: - Snacker

@MainActor
public final class Snacker {
// MARK: Properties

public static let shared: ISnacker = Snacker()

private var cachedViews: [Int: [SnackbarController]] = [:]

// MARK: Initialization

private init() {}

// MARK: Private

private func save(controller: SnackbarController, id: Int) {
if var views = cachedViews[id] {
views.append(controller)
cachedViews[id] = views
} else {
cachedViews[id] = [controller]
}
}

private func remove(controller: SnackbarController, id: Int) {
controller.hide(animated: true) { [weak self] in
guard let index = self?.cachedViews[id]?.firstIndex(of: controller) else {
return
}
self?.cachedViews[id]?.remove(at: index)
}
}

private func makeController(
for view: UIView,
inContainerView containerView: UIView,
data: SnackbarData
) -> SnackbarController {
let controller = SnackbarController(
contentView: view,
containerView: containerView,
alignment: data.snackbarAlignment,
insets: data.insets
)
return controller
}
}

// MARK: ISnacker

extension Snacker: ISnacker {
// swiftlint:disable:next function_body_length
public func action(_ action: SnackbarAction, container: UIView) {
let hash = container.hash

switch action {
case let .snack(view, data):
let controller = makeController(
for: view,
inContainerView: container,
data: data
)
save(controller: controller, id: hash)

DispatchQueue.main.async { controller.show(animated: true) }

DispatchQueue.main.asyncAfter(deadline: .now() + data.animationDuration) { [weak self] in
self?.remove(controller: controller, id: hash)
}
case let .snackSingle(view, data) where (cachedViews[hash] ?? []).isEmpty:
let controller = makeController(
for: view,
inContainerView: container,
data: data
)
save(controller: controller, id: hash)

DispatchQueue.main.async { controller.show(animated: true) }

DispatchQueue.main.asyncAfter(deadline: .now() + data.animationDuration) { [weak self] in
self?.remove(controller: controller, id: hash)
}
case .removeLast:
if let controller = cachedViews[hash]?.last {
remove(controller: controller, id: hash)
}
case .removeAll:
(cachedViews[hash] ?? []).forEach {
remove(controller: $0, id: hash)
}
default:
break
}
}
}
13 changes: 13 additions & 0 deletions Sources/snacker/Classes/Model/SnackbarAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// snacker
// Copyright © 2025 Space Code. All rights reserved.
//

import UIKit

public enum SnackbarAction {
case snack(view: UIView, data: SnackbarData)
case snackSingle(view: UIView, data: SnackbarData)
case removeLast
case removeAll
}
10 changes: 10 additions & 0 deletions Sources/snacker/Classes/Model/SnackbarAlignment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// snacker
// Copyright © 2025 Space Code. All rights reserved.
//

import Foundation
public enum SnackbarAlignment: Sendable {
case top(spacing: CGFloat)
case bottom(spacing: CGFloat)
}
22 changes: 22 additions & 0 deletions Sources/snacker/Classes/Model/SnackbarData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// snacker
// Copyright © 2025 Space Code. All rights reserved.
//

import Foundation

public struct SnackbarData: Sendable {
let snackbarAlignment: SnackbarAlignment
let insets: SnackbarInsets
let animationDuration: TimeInterval

public init(
snackbarAlignment: SnackbarAlignment = .bottom(spacing: 20.0),
insets: SnackbarInsets = .zero,
animationDuration: TimeInterval = 0.5
) {
self.snackbarAlignment = snackbarAlignment
self.insets = insets
self.animationDuration = animationDuration
}
}
18 changes: 18 additions & 0 deletions Sources/snacker/Classes/Model/SnackbarInsets.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// snacker
// Copyright © 2025 Space Code. All rights reserved.
//

import Foundation

public struct SnackbarInsets: Sendable {
public static let zero = SnackbarInsets(leading: .zero, trailing: .zero)

let leading: CGFloat
let trailing: CGFloat

public init(leading: CGFloat, trailing: CGFloat) {
self.leading = leading
self.trailing = trailing
}
}
Loading