Skip to content

Commit

Permalink
Liftoff
Browse files Browse the repository at this point in the history
  • Loading branch information
vinhnx committed Dec 17, 2020
1 parent 81d2996 commit f4cad1a
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 0 deletions.
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.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
23 changes: 23 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Laden",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.watchOS(.v6)
],
products: [
.library(
name: "Laden",
targets: ["Laden"]),
],
targets: [
.target(
name: "Laden",
dependencies: [])
]
)
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<H1 align="center">Laden</H1>

<p align="center">SwiftUI loading indicator view</p>
<p align="center">
<img src="./Resources/loading.gif"/>
</p>

---

### Installation

Since this component is built using Swift Package Manager, it is pretty straight forward to use:

1. In Xcode (11+), open your project and navigate to File > Swift Packages > Add Package Dependency...
2. Paste the repository URL (https://github.com/vinhnx/Laden) and click Next.
3. For Rules, select Branch (with branch set to master).
4. Click Finish to resolve package into your Xcode project.

### Usage

At simplest form:
```swift
import SwiftUI
import Laden

struct ContentView: View {
var body: some View {
Laden.CircleLoadingView()
}
}
```

---

To show loading view on top on current view by embedding inside a `ZStack`:
```swift
ZStack {
Text("Some text") // your content view
Laden.CircleOutlineLoadingView()
}
```

![ZStack](./Resources/loading_zstack.gif "ZStack")

---

To indicate loading state, have a private loading bool `@State` and bind it to Laden's `isAnimating` initialzier:
```swift
import SwiftUI
import Laden

struct ContentView: View {
@State private var isLoading = true

var body: some View {
VStack {
Laden.CircleLoadingView(isAnimating: isLoading)
Button(isLoading ? "Stop loading" : "Start loading") {
self.isLoading.toggle()
}
}
}
}
```

---

To show or hide loading view, have a private show/hide bool `@State` and modify said loading with `.hidden` attribute, when toggled:
```swift
import SwiftUI
import Laden

struct ContentView: View {
@State private var shouldLoadingView = true

private var loadingView = SwiftUILoading.CircleOutlineLoadingView()

var body: some View {
VStack {
if shouldLoadingView {
loadingView
.hidden()
} else {
loadingView
}

Button(shouldCircleView ? "Show" : "Hide") {
self.shouldLoadingView.toggle()
}
}
}
}
```

---

To customize loading view color, use `color` initializer:

```swift
Laden.CircleOutlineLoadingView(color: .red)
```

---

### Help, feedback or suggestions?

Feel free to open issue or contact me on [Twitter](https://twitter.com/@vinhnx) for discussions, news & announcements & other projects. 🚀

I hope you like it! :)
Binary file added Resources/loading.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Resources/loading_zstack.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
188 changes: 188 additions & 0 deletions Sources/Laden/Laden.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//
// Created by Vĩnh Nguyễn on 12/15/20.
//

import SwiftUI

/// Loading view protocol that define default configurations.
public protocol LoadingAnimatable: View {
/// Whether this loading view is animating.
var isAnimating: Bool { get }

/// Default color for loading view.
var color: Color { get }

/// The default size for loading view.
var size: CGSize { get set }

/// Default stroke line width for loading bar.
var strokeLineWidth: CGFloat { get }
}

/// Loading view container, for namespacing.
public enum Laden {}

extension Laden {
/// Circular loading view
public struct CircleLoadingView: LoadingAnimatable {

public var isAnimating: Bool = true
public var color: Color = .green
public var size: CGSize = CGSize(width: 30, height: 30)
public var strokeLineWidth: CGFloat = 3

/// The left lobe fraction of the loading bar.
public var trimEndFraction: CGFloat = 0.8

/// Current rotation value.
@State private var rotation: Double = 0

/// Timer that will continously draw the animation for loading view.
private let timer = Timer
.publish(every: 0.1, on: .main, in: .common)
.autoconnect()

/// - Parameters:
/// - isAnimating: Whether this loading view is animating
/// - color: Default color for loading view.
/// - size: The default size for loading view.
/// - trimEndFraction: The left lobe fraction of the loading bar.
/// - strokeLineWidth: Default stroke line width for loading bar.
public init(isAnimating: Bool = true, color: Color = .green, size: CGSize = CGSize(width: 30, height: 30), trimEndFraction: CGFloat = 0.8, strokeLineWidth: CGFloat = 8) {
self.isAnimating = isAnimating
self.color = color
self.size = size
self.trimEndFraction = trimEndFraction
self.strokeLineWidth = strokeLineWidth
}

public var body: some View {
Circle()
.trim(from: 0, to: trimEndFraction)
.stroke(color, lineWidth: strokeLineWidth)
.frame(width: size.width, height: size.height)
.rotationEffect(.degrees(rotation))
.animation(isAnimating ? .linear : .none)
.onReceive(timer) { _ in
if isAnimating { rotation += 36 }
}
}
}

/// Circular loading view with outline background
public struct CircleOutlineLoadingView: LoadingAnimatable {
public var isAnimating: Bool = true
public var color: Color = .green
public var size: CGSize = CGSize(width: 30, height: 30)
public var strokeLineWidth: CGFloat = 8

/// The left lobe fraction of the loading bar.
public var trimEndFraction: CGFloat = 0.8

/// Outline bar color.
public var outlineBarColor: Color = Color(.systemGray5)

/// Current rotation value.
@State private var rotation: Double = 0

/// Timer that will continously draw the animation for loading view.
private let timer = Timer
.publish(every: 0.1, on: .main, in: .common)
.autoconnect()

/// - Parameters:
/// - isAnimating: Whether this loading view is animating
/// - color: Default color for loading view.
/// - size: The default size for loading view.
/// - trimEndFraction: The left lobe fraction of the loading bar.
/// - strokeLineWidth: Default stroke line width for loading bar.
public init(isAnimating: Bool = true, color: Color = .green, size: CGSize = CGSize(width: 30, height: 30), strokeLineWidth: CGFloat = 8, trimEndFraction: CGFloat = 0.8) {
self.isAnimating = isAnimating
self.color = color
self.size = size
self.strokeLineWidth = strokeLineWidth
self.trimEndFraction = trimEndFraction
}

public var body: some View {
ZStack {
Circle()
.stroke(outlineBarColor, lineWidth: strokeLineWidth)
.frame(width: size.width, height: size.height)

Circle()
.trim(from: 0, to: trimEndFraction)
.stroke(color, lineWidth: strokeLineWidth / 2)
.frame(width: size.width, height: size.height)
.rotationEffect(.degrees(rotation))
.animation(isAnimating ? .linear : .none)
.onReceive(timer) { _ in
if isAnimating { rotation += 36 }
}
}
}
}

/// Reversible loading bar view.
public struct BarLoadingView: LoadingAnimatable {
public var isAnimating: Bool = true
public var color: Color = .green
public var size: CGSize = CGSize(width: 200, height: 30)
public var strokeLineWidth: CGFloat = 3

/// Outline bar color.
public var outlineBarColor: Color = Color(.systemGray5)

/// Current indicator view's x offset.
@State private var offset: CGFloat = 0

/// The caculated width of indicator view.
private var indicatorWidth: CGFloat {
size.width * 0.3
}

/// Timer that will continously draw the animation for loading view.
private let timer = Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()

/// - Parameters:
/// - isAnimating: Whether this loading view is animating
/// - color: Default color for loading view.
/// - size: The default size for loading view.
/// - strokeLineWidth: Default stroke line width for loading bar.
public init(isAnimating: Bool = true, color: Color = .green, size: CGSize = CGSize(width: 200, height: 30), strokeLineWidth: CGFloat = 3) {
self.isAnimating = isAnimating
self.color = color
self.size = size
self.strokeLineWidth = strokeLineWidth
}

public var body: some View {
ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)) {
RoundedRectangle(cornerRadius: strokeLineWidth)
.stroke(outlineBarColor, lineWidth: strokeLineWidth)
.frame(width: size.width, height: strokeLineWidth)

RoundedRectangle(cornerRadius: strokeLineWidth)
.stroke(color, lineWidth: strokeLineWidth)
.frame(width: indicatorWidth, height: strokeLineWidth)
.offset(x: offset, y: 0)
.animation(Animation.easeInOut(duration: 0.5).repeatForever(autoreverses: true))
.onReceive(timer) { _ in
if isAnimating { offset = size.width - indicatorWidth }
}
}
}
}
}

struct Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 50) {
Laden.BarLoadingView(color: .red)
Laden.CircleLoadingView(color: .green)
Laden.CircleOutlineLoadingView(color: .orange)
}
}
}

0 comments on commit f4cad1a

Please sign in to comment.