Skip to content

YusukeHosonuma/SwiftUI-Common

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Build iOS 14+ Twitter

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 (NavigationLink that label is EmptyView)

HiddenLink(isActive: $isActive) {
    ChildView()
}
TextEdit

TextEdit.swift (add placeholder to TextEditor)

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

ResizableImage

The Image that is resized only if it extends beyond the area.

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

WebView.swift (bridge to WKWebView)

@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 (bridge to UIActivityViewController)

@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 (bridge to NSWindowController)
T.B.D

View extensions

View+.swift

enabled()
@State var isEnabled = false

var body: some View {
    VStack {
        Button("Hello") {}
            .enabled(isEnabled) // ๐Ÿ’ก Same as `.disabled(isEnabled == false)`
}
extend { ... }
Text("Hello")
    .extend { content in
        if #available(iOS 15, *) {
            content
                .environment(\.dynamicTypeSize, .xxxLarge)
        } else {
            content
        }
    }
when() { ... }
@State var condition = false

var body: some View {
    Text("Hello")
        .when(condition) {
            $0.underline()
        }
}
whenLet() { ... }
@State var textColor: Color? = .red

var body: some View {
    Text("Hello")
        .whenLet(textColor) { content, textColor in
            content
                .foregroundColor(textColor)
        }
}
border()
Text("Hello")
    .padding()
    .border(.red, edge: .vertical) // default `width` = 1
    .border(.blue, width: 8, edge: .leading)
image
toggleSidebar()
Button("toggle") {
    toggleSidebar()
}
hideKeyboard()
Button("hide") {
    hideKeyboard()
}

View+Alert.swift

alert(error: $error)
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<Bool>`.
        .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

debug { ... }
func content(number: Int) -> some View {
    Text("\(number)")
        .debug {
            print("number: \(number)") // ๐Ÿ’ก Any debug code.
        }
}
print()
func content(number: Int) -> some View {
    Text("\(number)")
        .print("number: \(number)") // ๐Ÿ’ก
}
printOnChange()
@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
SliderValue.swift (e.g. for use enum in Slider)

map()
@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<String>` -> `Binding<Bool>`
            get: { $0 == "true" },
            set: { $0 ? "true" : "false" }
        ))
    }
}
inverted()
@State var isEnabled = false

var body: some View {
    Toggle("Disable", isOn: $isEnabled.inverted()) // โœ… `true` -> `false` and `false` -> true`
    Text("isEnabled: \(isEnabled ? "True" : "False")")
}
optional()
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<Menu>` -> `Binding<Menu?`
        NavigationView {
            List {
                NavigationLink(tag: Menu.all, selection: optionalSelection, destination: { Text("1") }) {
                    Text("One")
                }
                NavigationLink(tag: Menu.star, selection: optionalSelection, destination: { Text("2") }) {
                    Text("Two")
                }
            }
        }
    }
}
wrapped()
@Binding var optionalString: String?

var body: some View {
    if let binding = $optionalString.wrapped() { // ๐Ÿ’ก `Binding<String?>` -> `Binding<String>?`
        TextField("placeholder", text: binding)
    } else {
        Text("nil")
    }
}
case()
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<Value> -> Binding<String>?
                //
                if let binding = $value.case(/EnumValue.string) {
                    TextField("placeholder", text: binding)
                }

            case .bool:
                //
                // โœ… Binding<Value> -> Binding<Int>?
                //
                if let binding = $value.case(/EnumValue.bool) {
                    Toggle("isOn", isOn: binding)
                }
            }
        }
    }
}
slider()
// ๐Ÿ’ก 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<Double> = 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<TextSize>` -> `Binding<Double>`
                in: TextSize.sliderRange,
                step: 1
            )
        }
        .padding()
    }
}
image

CGSize extensions

CGSize+.swift

Comparable
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
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

init(UIImage or NSImage)
#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
@Dismiss

Section
Section("title") {
    ...
}
@Dismiss
// โœ… 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

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

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

Concurrency extensions

Task+.swift

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

Install

If you want.

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

Development

Setup:

make setup

Format:

make format

Links

Author

Yusuke Hosonuma / @tobi462