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.
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
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)
}
}
}
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
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)
toggleSidebar()
Button("toggle") {
toggleSidebar()
}
hideKeyboard()
Button("hide") {
hideKeyboard()
}
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
)
}
}
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+.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()
}
}
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)
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
}
}
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()
}
Space(...)
// Alias for `Spacer().frame(width: 10)`
Space(width: 10)
// Alias for `Spacer().frame(height: 10)`
Space(height: 10)
sleep()
Task {
try await Task.sleep(seconds: 1) // 1 s
try await Task.sleep(milliseconds: 500) // 500 ms
}
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"),
]),
]
)
Setup:
make setup
Format:
make format
- This library is used in the following:
- Document (Japanese):
Yusuke Hosonuma / @tobi462