Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/yusukehosonuma/swiftui-common

SwiftUI components that seem to be highly reusable.
https://github.com/yusukehosonuma/swiftui-common

swiftui swiftui-components

Last synced: about 9 hours ago
JSON representation

SwiftUI components that seem to be highly reusable.

Awesome Lists containing this project

README

        

[![Build](https://github.com/YusukeHosonuma/SwiftUI-Common/actions/workflows/main.yml/badge.svg)](https://github.com/YusukeHosonuma/SwiftUI-Common/actions/workflows/main.yml)
![iOS 14+](https://img.shields.io/badge/iOS-14+-4BC51D.svg?style=flat)
[![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Ftobi462)](https://twitter.com/tobi462)

# SwiftUI-Common

SwiftUI components and extensions that seem to be highly reusable.

Since this is an **experimental library**, we recommend that you copy (or use as refererence) and use the source.

## Control

HiddenLink

[HiddenLink](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/View/HiddenLink.swift) (`NavigationLink` that label is `EmptyView`)

```swift
HiddenLink(isActive: $isActive) {
ChildView()
}
```

TextEdit

[TextEdit.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/View/TextEdit.swift) (add placeholder to [TextEditor](https://developer.apple.com/documentation/swiftui/texteditor))

```swift
TextEdit("Please paste.", text: $text, font: .custom("SF Mono", size: 16))
```

ResizableImage

[ResizableImage](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/View/ResizableImage.swift)

The [Image](https://developer.apple.com/documentation/swiftui/image) that is resized only if it extends beyond the area.

```swift

Group {
ResizableImage(systemName: "swift", contentMode: .fit)
ResizableImage("island", contentMode: .fit)
ResizableImage("island", contentMode: .fill)
}
.frame(width: 140, height: 140)
.border(.red)
```

WebView

[WebView.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/View/WebView.swift) (bridge to [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview))

```swift
@StateObject var webViewState = WebViewState { _ in
// ๐Ÿ’ก If you want to more configuration
// webView.allowsBackForwardNavigationGestures = true
}

var body: some View {
ZStack {
WebView(url: url, state: webViewState)

if webViewState.isFirstLoading {
ProgressView()
}

// ๐Ÿ’ก Note: If you want to display an indicator at each page transition.
// if webViewState.isLoading {
// ProgressView()
// }
}
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Spacer()

// โœ… Back
Button {
webViewState.goBack()
} label: {
Image(systemName: "chevron.backward")
}
.enabled(webViewState.canGoBack)

// โœ… Forward
Button {
webViewState.goForward()
} label: {
Image(systemName: "chevron.forward")
}
.enabled(webViewState.canGoForward)
}
}
}
```

image

ActivityView

[ActivityView](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/View/UIKit/ActivityView.swift) (bridge to [UIActivityViewController](https://developer.apple.com/documentation/uikit/uiactivityviewcontroller))

```swift
@State static var isPresent = false

static var previews: some View {
Image(systemName: "square.and.arrow.up")
.sheet(isPresented: .constant(true)) {
ActivityView(activityItems: [URL(string: "https://github.com/YusukeHosonuma/SwiftUI-Common")!])
}
}
```

WindowController

[WindowController](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/View/AppKit/WindowController.swift) (bridge to [NSWindowController](https://developer.apple.com/documentation/appkit/nswindowcontroller))

T.B.D

## View extensions

[View+.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/View%2B.swift)

enabled()

```swift
@State var isEnabled = false

var body: some View {
VStack {
Button("Hello") {}
.enabled(isEnabled) // ๐Ÿ’ก Same as `.disabled(isEnabled == false)`
}
```

extend { ... }

```swift
Text("Hello")
.extend { content in
if #available(iOS 15, *) {
content
.environment(\.dynamicTypeSize, .xxxLarge)
} else {
content
}
}
```

when() { ... }

```swift
@State var condition = false

var body: some View {
Text("Hello")
.when(condition) {
$0.underline()
}
}
```

whenLet() { ... }

```swift
@State var textColor: Color? = .red

var body: some View {
Text("Hello")
.whenLet(textColor) { content, textColor in
content
.foregroundColor(textColor)
}
}
```

border()

```swift
Text("Hello")
.padding()
.border(.red, edge: .vertical) // default `width` = 1
.border(.blue, width: 8, edge: .leading)
```

image

toggleSidebar()

```swift
Button("toggle") {
toggleSidebar()
}
```

hideKeyboard()

```swift
Button("hide") {
hideKeyboard()
}
```

[View+Alert.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/View%2BAlert.swift)

alert(error: $error)

```swift
enum MyError: LocalizedError {
case warning, fatal

var errorDescription: String? {
switch self {
case .warning: return "Warning"
case .fatal: return "Fatal"
}
}

var helpMessage: String {
switch self {
case .warning: return "This is warning."
case .fatal: return "This is fatal."
}
}
}

struct ContentView: View {
@State var error: MyError? = nil

var body: some View {
VStack {
Button("Warning") { error = .warning }
Button("Fatal") { error = .fatal }
}
//
// iOS 15+
//
.alert(error: $error) {} // โœ… Not need to specify `isPresented: Binding`.
.alert(error: $error) { _ in // ๐Ÿ’ก You can specify message.
Button("OK") {}
} message: { error in
Text(error.helpMessage)
}
//
// iOS 14+
//
.alert(error: $error)
.alert(
error: $error,
message: Text(error?.helpMessage ?? "unknown"),
dismissButton: .cancel() // Optional
)
}
}
```

[View+Debug.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/View%2BDebug.swift)

debug { ... }

```swift
func content(number: Int) -> some View {
Text("\(number)")
.debug {
print("number: \(number)") // ๐Ÿ’ก Any debug code.
}
}
```

print()

```swift
func content(number: Int) -> some View {
Text("\(number)")
.print("number: \(number)") // ๐Ÿ’ก
}
```

printOnChange()

```swift
@State var number: Int = 42

var body: some View {
Text("\(number)")
.printOnChange("number: ") { number } // ๐Ÿ’ก Print "number: 42" when `number is changed.
}
```

## Binding extension

[Binding+.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/Binding%2B.swift)

[SliderValue.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/SliderValue.swift) (e.g. for use `enum` in [Slider](https://developer.apple.com/documentation/swiftui/slider))

map()

```swift
@State var boolString = "false"

var body: some View {
VStack {
TextField("isOn", text: $boolString)
.textFieldStyle(.roundedBorder)
.autocapitalization(.none)

//
// ๐Ÿ’ก Can edit `String` as `Bool`.
//
Toggle("isOn", isOn: $boolString.map( // โœ… `Binding` -> `Binding`
get: { $0 == "true" },
set: { $0 ? "true" : "false" }
))
}
}
```

inverted()

```swift
@State var isEnabled = false

var body: some View {
Toggle("Disable", isOn: $isEnabled.inverted()) // โœ… `true` -> `false` and `false` -> true`
Text("isEnabled: \(isEnabled ? "True" : "False")")
}
```

optional()

```swift
enum Menu: Int {
case all
case star
}

struct BindingOptionalView: View {
@SceneStorage("selection") var selection: Menu = .all

var body: some View {
let optionalSelection = $selection.optional() // ๐Ÿ’ก `Binding` -> `Binding

wrapped()

```swift
@Binding var optionalString: String?

var body: some View {
if let binding = $optionalString.wrapped() { // ๐Ÿ’ก `Binding` -> `Binding?`
TextField("placeholder", text: binding)
} else {
Text("nil")
}
}
```

case()

```swift
import CasePaths // โœ… Required `pointfreeco/swift-case-paths`
import SwiftUI

enum EnumValue {
case string(String) // ๐Ÿ’ก Has associated-type `String`
case bool(Bool) // ๐Ÿ’ก Has associated-type `Bool`
}

struct CaseBindingView: View {
@State var value: EnumValue = .string("Swift")

var body: some View {
VStack {
//
// ๐Ÿ’ก Note: `switch` statement is only for completeness check by compiler.
// (Removal does not affect the operation)
//
switch value {
case .string:
//
// โœ… Binding -> Binding?
//
if let binding = $value.case(/EnumValue.string) {
TextField("placeholder", text: binding)
}

case .bool:
//
// โœ… Binding -> Binding?
//
if let binding = $value.case(/EnumValue.bool) {
Toggle("isOn", isOn: binding)
}
}
}
}
}
```

slider()

```swift
// ๐Ÿ’ก Want to edit by slider.
enum TextSize: Int, CaseIterable {
case xSmall = 0
case small = 1
...

var name: String {
switch self {
case .xSmall: return "xSmall"
case .small: return "small"
...
}
}

// โœ… Implement `SliderValue` protocol.
extension TextSize: SliderValue {
static let sliderRange: ClosedRange = 0 ... Double(TextSize.allCases.count - 1)

var sliderIndex: Int { rawValue }

init(fromSliderIndex index: Int) {
self = Self(rawValue: index)!
}
}

struct SliderView: View {
@State var textSize: TextSize = .medium

var body: some View {
VStack {
Text("\(textSize.name)")
Slider(
value: $textSize.slider(), // ๐Ÿ’ก `Binding` -> `Binding`
in: TextSize.sliderRange,
step: 1
)
}
.padding()
}
}
```

image

## CGSize extensions

[CGSize+.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/CGSize%2B.swift)

Comparable

```swift
let a = CGSize(width: 10, height: 20)
let b = CGSize(width: 5, height: 10)
a < b // ๐Ÿ’ก Alias for `a.width < b.width && a.height < b.height`
```

AdditiveArithmetic

```swift
let a = CGSize(width: 10, height: 20)
let b = CGSize(width: 5, height: 10)
a + b // ๐Ÿ’ก Alias for `CGSize(width: a.width + b.width, height: a.height + b.height)
a - b // ๐Ÿ’ก Alias for `CGSize(width: a.width - b.width, height: a.height - b.height)
```


## Image extensions

[Image+.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/Image%2B.swift)

init(UIImage or NSImage)

```swift
#if os(macOS)
private typealias XImage = NSImage
#else
private typealias XImage = UIImage
#endif

struct ImageView: View {
var body: some View {
Image(image: renderImage()) // ๐Ÿ’ก
.resizable()
.scaledToFit()
}

private func renderImage() -> XImage {
// โš ๏ธ Assumes rendering code
#if os(macOS)
NSImage(named: "picture")!
#else
UIImage(named: "picture")!
#endif
}
}
```


## Compatible iOS 15+ (Can be used in iOS 14+)

[Section+iOS15.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/Compatible/Section%2BiOS15.swift)

[@Dismiss](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/Compatible/Dismiss.swift)

Section

```swift
Section("title") {
...
}
```

@Dismiss

```swift
// โœ… Compatible to `@Environment(\.dismiss) var dismiss` in iOS 15.
@Dismiss var dismiss

// ๐Ÿ’ก in iOS 14+
// @Environment(\.presentationMode) private var presentationMode

var body: some View {
VStack {
Button("Close") {
// โœ… Same as `@Environment(\.dismiss)`
dismiss()

// ๐Ÿ’ก in iOS 14
// presentationMode.wrappedValue.dismiss()
}
}
.padding()
}
```

## Misc

[Space.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/View/Space.swift.swift)

Space(...)

```swift
// Alias for `Spacer().frame(width: 10)`
Space(width: 10)

// Alias for `Spacer().frame(height: 10)`
Space(height: 10)
```

## Concurrency extensions

[Task+.swift](https://github.com/YusukeHosonuma/SwiftUI-Common/blob/main/Sources/SwiftUICommon/Extension/Task%2B.swift)

sleep()

```swift
Task {
try await Task.sleep(seconds: 1) // 1 s
try await Task.sleep(milliseconds: 500) // 500 ms
}
```


## Install

If you want.

```swift
let package = Package(
dependencies: [
.package(url: "https://github.com/YusukeHosonuma/SwiftUI-Common.git", from: "1.0.0"),
],
targets: [
.target(name: "", dependencies: [
.product(name: "SwiftUICommon", package: "SwiftUI-Common"),
]),
]
)
```

## Development

Setup:

```sh
make setup
```

Format:

```sh
make format
```

## Links

- This library is used in the following:
- [SwiftUI-Simulator](https://github.com/YusukeHosonuma/SwiftUI-Simulator)
- [UserDefaults-Browser](https://github.com/YusukeHosonuma/UserDefaultsBrowser)
- [Swift-Evolution-Browser](https://github.com/YusukeHosonuma/Swift-Evolution-Browser)
- [E2DC](https://github.com/YusukeHosonuma/E2DC)
- Document (Japanese):
- [Effective SwiftUI ๅ€™่ฃœ๏ผˆไปฎ่ชฌ๏ผ‰](https://github.com/YusukeHosonuma/Effective-SwiftUI)

## Author

Yusuke Hosonuma / [@tobi462](https://twitter.com/tobi462)