├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Package.swift
├── README.md
├── Sources
└── EasySwiftUI
│ ├── Extensions
│ ├── Appearance.swift
│ ├── Bindin.swift
│ ├── Binding.swift
│ ├── Color.swift
│ ├── ForEach.swift
│ ├── Image.swift
│ └── View.swift
│ ├── Modifier
│ ├── ButtonStyle.swift
│ ├── CornerRadius.swift
│ ├── CustomFont.swift
│ └── ShakeEffect.swift
│ ├── Utils
│ └── SFSymbol.swift
│ └── View
│ ├── ActivittyIndicator.swift
│ ├── BlurView.swift
│ ├── HUD.swift
│ ├── IfLet.swift
│ ├── MakeView.swift
│ ├── MySearchField.swift
│ ├── MyTextField.swift
│ ├── MyTextView.swift
│ ├── PagerView.swift
│ └── VisualEffectView.swift
└── Tests
├── EasySwiftUITests
├── EasySwiftUITests.swift
└── XCTestManifests.swift
└── LinuxMain.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData
7 | Build/
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "EasySwiftUI",
8 | platforms: [
9 | .macOS(.v11),
10 | .iOS(.v13)
11 | ],
12 | products: [
13 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
14 | .library(
15 | name: "EasySwiftUI",
16 | targets: ["EasySwiftUI"]
17 | ),
18 | ],
19 | dependencies: [
20 | // Dependencies declare other packages that this package depends on.
21 | // .package(url: /* package url */, from: "1.0.0"),
22 | ],
23 | targets: [
24 | .target(
25 | name: "EasySwiftUI",
26 | dependencies: []
27 | ),
28 | .testTarget(
29 | name: "EasySwiftUITests",
30 | dependencies: ["EasySwiftUI"]
31 | ),
32 | ]
33 | )
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EasySwiftUI
2 |
3 | A description of this package.
4 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Extensions/Appearance.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by khoa on 18/12/2020.
6 | //
7 |
8 | #if os(OSX)
9 | import AppKit
10 |
11 | public extension NSAppearance {
12 | var isDarkMode: Bool {
13 | if self.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
14 | return true
15 | } else {
16 | return false
17 | }
18 | }
19 | }
20 | #endif
21 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Extensions/Bindin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bindin.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 22/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension Binding {
11 | func onChange(_ handler: @escaping (Value) -> Void) -> Binding {
12 | Binding(
13 | get: { self.wrappedValue },
14 | set: { newValue in
15 | self.wrappedValue = newValue
16 | handler(newValue)
17 | }
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Extensions/Binding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Binding.swift
3 | //
4 | //
5 | // Created by khoa on 16/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension Binding {
11 | func didSet(_ didSet: @escaping (Value) -> Void) -> Binding {
12 | Binding(
13 | get: { wrappedValue },
14 | set: { newValue in
15 | self.wrappedValue = newValue
16 | didSet(newValue)
17 | }
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Extensions/Color.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color.swift
3 | // Omnia
4 | //
5 | // Created by khoa on 14/11/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
12 | public extension SwiftUI.Color {
13 | init(hex: Int, alpha: Double = 1) {
14 | let components = (
15 | R: Double((hex >> 16) & 0xff) / 255,
16 | G: Double((hex >> 08) & 0xff) / 255,
17 | B: Double((hex >> 00) & 0xff) / 255
18 | )
19 |
20 | self.init(
21 | .sRGB,
22 | red: components.R,
23 | green: components.G,
24 | blue: components.B,
25 | opacity: alpha
26 | )
27 | }
28 |
29 | init(hexString: String) {
30 | let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
31 | var int: UInt64 = 0
32 | Scanner(string: hex).scanHexInt64(&int)
33 | let a, r, g, b: UInt64
34 | switch hex.count {
35 | case 3: // RGB (12-bit)
36 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
37 | case 6: // RGB (24-bit)
38 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
39 | case 8: // ARGB (32-bit)
40 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
41 | default:
42 | (a, r, g, b) = (1, 1, 1, 0)
43 | }
44 |
45 | self.init(
46 | .sRGB,
47 | red: Double(r) / 255,
48 | green: Double(g) / 255,
49 | blue: Double(b) / 255,
50 | opacity: Double(a) / 255
51 | )
52 | }
53 | }
54 |
55 | #if canImport(UIKit)
56 | import UIKit
57 |
58 | public extension Color {
59 |
60 | // MARK: - Text Colors
61 | static let lightText = Color(UIColor.lightText)
62 | static let darkText = Color(UIColor.darkText)
63 | static let placeholderText = Color(UIColor.placeholderText)
64 |
65 | // MARK: - Label Colors
66 | static let label = Color(UIColor.label)
67 | static let secondaryLabel = Color(UIColor.secondaryLabel)
68 | static let tertiaryLabel = Color(UIColor.tertiaryLabel)
69 | static let quaternaryLabel = Color(UIColor.quaternaryLabel)
70 |
71 | // MARK: - Background Colors
72 | static let systemBackground = Color(UIColor.systemBackground)
73 | static let secondarySystemBackground = Color(UIColor.secondarySystemBackground)
74 | static let tertiarySystemBackground = Color(UIColor.tertiarySystemBackground)
75 |
76 | // MARK: - Fill Colors
77 | static let systemFill = Color(UIColor.systemFill)
78 | static let secondarySystemFill = Color(UIColor.secondarySystemFill)
79 | static let tertiarySystemFill = Color(UIColor.tertiarySystemFill)
80 | static let quaternarySystemFill = Color(UIColor.quaternarySystemFill)
81 |
82 | // MARK: - Grouped Background Colors
83 | static let systemGroupedBackground = Color(UIColor.systemGroupedBackground)
84 | static let secondarySystemGroupedBackground = Color(UIColor.secondarySystemGroupedBackground)
85 | static let tertiarySystemGroupedBackground = Color(UIColor.tertiarySystemGroupedBackground)
86 |
87 | // MARK: - Gray Colors
88 | static let systemGray = Color(UIColor.systemGray)
89 | static let systemGray2 = Color(UIColor.systemGray2)
90 | static let systemGray3 = Color(UIColor.systemGray3)
91 | static let systemGray4 = Color(UIColor.systemGray4)
92 | static let systemGray5 = Color(UIColor.systemGray5)
93 | static let systemGray6 = Color(UIColor.systemGray6)
94 |
95 | // MARK: - Other Colors
96 | static let separator = Color(UIColor.separator)
97 | static let opaqueSeparator = Color(UIColor.opaqueSeparator)
98 | static let link = Color(UIColor.link)
99 |
100 | // MARK: System Colors
101 | static let systemBlue = Color(UIColor.systemBlue)
102 | static let systemPurple = Color(UIColor.systemPurple)
103 | static let systemGreen = Color(UIColor.systemGreen)
104 | static let systemYellow = Color(UIColor.systemYellow)
105 | static let systemOrange = Color(UIColor.systemOrange)
106 | static let systemPink = Color(UIColor.systemPink)
107 | static let systemRed = Color(UIColor.systemRed)
108 | static let systemTeal = Color(UIColor.systemTeal)
109 | static let systemIndigo = Color(UIColor.systemIndigo)
110 | }
111 | #endif
112 |
113 | #if os(OSX)
114 | import AppKit
115 | public extension Color {
116 | static func dynamic(dark: Int, light: Int) -> Color {
117 | let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
118 | return isDark ? Color(hex: dark) : Color(hex: light)
119 | }
120 |
121 | // MARK: - Text Colors
122 |
123 | static let placeholderText = Color(NSColor.placeholderTextColor)
124 |
125 | // MARK: - Label Colors
126 | static let label = Color(NSColor.labelColor)
127 | static let secondaryLabel = Color(NSColor.secondaryLabelColor)
128 | static let tertiaryLabel = Color(NSColor.tertiaryLabelColor)
129 | static let quaternaryLabel = Color(NSColor.quaternaryLabelColor)
130 |
131 | // MARK: - Gray Colors
132 | static let systemGray = Color(NSColor.systemGray)
133 | static let systemGray2 = Color.dynamic(dark: 0x636366, light: 0xAEAEB2)
134 | static let systemGray3 = Color.dynamic(dark: 0x48484A, light: 0xC7C7CC)
135 | static let systemGray4 = Color.dynamic(dark: 0x3A3A3C, light: 0xD1D1D6)
136 | static let systemGray5 = Color.dynamic(dark: 0x2C2C2E, light: 0xE5E5EA)
137 | static let systemGray6 = Color.dynamic(dark: 0x1C1C1E, light: 0xF2F2F7)
138 |
139 | // MARK: - Other Colors
140 | static let separator = Color(NSColor.separatorColor)
141 | static let link = Color(NSColor.linkColor)
142 |
143 | // MARK: System Colors
144 | static let systemBlue = Color(NSColor.systemBlue)
145 | static let systemPurple = Color(NSColor.systemPurple)
146 | static let systemGreen = Color(NSColor.systemGreen)
147 | static let systemYellow = Color(NSColor.systemYellow)
148 | static let systemOrange = Color(NSColor.systemOrange)
149 | static let systemPink = Color(NSColor.systemPink)
150 | static let systemRed = Color(NSColor.systemRed)
151 | static let systemTeal = Color(NSColor.systemTeal)
152 | static let systemIndigo = Color(NSColor.systemIndigo)
153 | }
154 | #endif
155 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Extensions/ForEach.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForEach.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 04/10/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public func ForEachWithIndex<
11 | Data: RandomAccessCollection,
12 | Content: View>(
13 | _ data: Data,
14 | @ViewBuilder content: @escaping (Data.Index, Data.Element) -> Content
15 | ) -> some View where Data.Element: Identifiable, Data.Element: Hashable {
16 | ForEach(Array(zip(data.indices, data)), id: \.1) { index, element in
17 | content(index, element)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Extensions/Image.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Image.swift
3 | // Omnia
4 | //
5 | // Created by khoa on 29/10/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
12 | public extension SwiftUI.Image {
13 | func styleFit() -> some View {
14 | return self
15 | .resizable()
16 | .aspectRatio(contentMode: .fit)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Extensions/View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View.swift
3 | // Omnia
4 | //
5 | // Created by khoa on 29/10/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
12 | public extension View {
13 | func erase() -> AnyView {
14 | return AnyView(self)
15 | }
16 |
17 | @ViewBuilder
18 | func applyIf(_ condition: @autoclosure () -> Bool, apply: (Self) -> T) -> some View {
19 | if condition() {
20 | apply(self)
21 | } else {
22 | self
23 | }
24 | }
25 |
26 | @ViewBuilder
27 | func hidden(_ hides: Bool) -> some View {
28 | switch hides {
29 | case true: self.hidden()
30 | case false: self
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Modifier/ButtonStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by khoa on 26/10/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct HighlightButtonStyle: ButtonStyle {
11 | let hPadding: CGFloat
12 | let vPadding: CGFloat
13 | let cornerRadius: CGFloat
14 |
15 | public init(
16 | h: CGFloat = 8,
17 | v: CGFloat = 4,
18 | cornerRadius: CGFloat = 4
19 | ) {
20 | self.hPadding = h
21 | self.vPadding = v
22 | self.cornerRadius = cornerRadius
23 | }
24 |
25 | public func makeBody(configuration: Self.Configuration) -> some View {
26 | configuration.label
27 | .padding(.horizontal, hPadding)
28 | .padding(.vertical, vPadding)
29 | .contentShape(Rectangle())
30 | .background(configuration.isPressed ? Color.separator : Color.clear)
31 | .cornerRadius(cornerRadius)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Modifier/CornerRadius.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CornerRadius.swift
3 | //
4 | //
5 | // Created by khoa on 16/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Ref: https://stackoverflow.com/questions/56760335/round-specific-corners-swiftui
11 |
12 | #if canImport(UIKit)
13 |
14 | public struct RoundedCorner: Shape {
15 | let radius: CGFloat
16 | let corners: UIRectCorner
17 |
18 | public init(radius: CGFloat, corners: UIRectCorner = .allCorners) {
19 | self.radius = radius
20 | self.corners = corners
21 | }
22 |
23 | public func path(in rect: CGRect) -> Path {
24 | let path = UIBezierPath(
25 | roundedRect: rect,
26 | byRoundingCorners: corners,
27 | cornerRadii: CGSize(width: radius, height: radius)
28 | )
29 | return Path(path.cgPath)
30 | }
31 | }
32 |
33 | public extension View {
34 | func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
35 | clipShape( RoundedCorner(radius: radius, corners: corners) )
36 | }
37 | }
38 |
39 | #endif
40 |
41 | #if os(OSX)
42 |
43 | public struct NSRectCorner: OptionSet {
44 | public static let topLeft = NSRectCorner(rawValue: 1)
45 | public static let topRight = NSRectCorner(rawValue: 1 << 1)
46 | public static let bottomLeft = NSRectCorner(rawValue: 1 << 2)
47 | public static let bottomRight = NSRectCorner(rawValue: 1 << 3)
48 |
49 | public let rawValue: Int8
50 |
51 | public init(rawValue: Int8) {
52 | self.rawValue = rawValue
53 | }
54 | }
55 |
56 | public struct RoundedCorner: Shape {
57 | let tl: CGFloat
58 | let tr: CGFloat
59 | let bl: CGFloat
60 | let br: CGFloat
61 |
62 | public init(radius: CGFloat, corners: NSRectCorner) {
63 | self.tl = corners.contains(.topLeft) ? radius : 0
64 | self.tr = corners.contains(.topRight) ? radius : 0
65 | self.bl = corners.contains(.bottomLeft) ? radius : 0
66 | self.br = corners.contains(.bottomRight) ? radius : 0
67 | }
68 |
69 | public init(tl: CGFloat = 0, tr: CGFloat = 0, bl: CGFloat = 0, br: CGFloat = 0) {
70 | self.tl = tl
71 | self.tr = tr
72 | self.bl = bl
73 | self.br = br
74 | }
75 |
76 | public func path(in rect: CGRect) -> Path {
77 | var path = Path()
78 |
79 | let w = rect.size.width
80 | let h = rect.size.height
81 |
82 | // Make sure we do not exceed the size of the rectangle
83 | let tr = min(min(self.tr, h/2), w/2)
84 | let tl = min(min(self.tl, h/2), w/2)
85 | let bl = min(min(self.bl, h/2), w/2)
86 | let br = min(min(self.br, h/2), w/2)
87 |
88 | path.move(to: CGPoint(x: w / 2.0, y: 0))
89 | path.addLine(to: CGPoint(x: w - tr, y: 0))
90 | path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr,
91 | startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
92 |
93 | path.addLine(to: CGPoint(x: w, y: h - br))
94 | path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br,
95 | startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
96 |
97 | path.addLine(to: CGPoint(x: bl, y: h))
98 | path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl,
99 | startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
100 |
101 | path.addLine(to: CGPoint(x: 0, y: tl))
102 | path.addArc(center: CGPoint(x: tl, y: tl), radius: tl,
103 | startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
104 |
105 | return path
106 | }
107 | }
108 |
109 | public extension View {
110 | func cornerRadius(_ radius: CGFloat, corners: NSRectCorner) -> some View {
111 | clipShape(RoundedCorner(radius: radius, corners: corners))
112 | }
113 | }
114 |
115 | #endif
116 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Modifier/CustomFont.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomFont.swift
3 | // Omnia
4 | //
5 | // Created by khoa on 14/11/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | #if canImport(UIKit) && canImport(SwiftUI)
10 |
11 | import SwiftUI
12 |
13 | // https://learntalks.com/Pragma-Mark/2019/Pragma-Mark-2019-Pragma-Conference-2019-Paul-Hudson-SwiftUI-Everywhere/
14 |
15 | @available(iOS 13, macCatalyst 13, tvOS 13, watchOS 6, *)
16 | public struct CustomFont: ViewModifier {
17 |
18 | @Environment(\.sizeCategory) var sizeCategory
19 | var name: String
20 | var size: CGFloat
21 |
22 | public func body(content: Content) -> some View {
23 | let scaledSize = UIFontMetrics.default.scaledValue(for: size)
24 | return content.font(.custom(name, size: scaledSize))
25 | }
26 | }
27 |
28 | #endif
29 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/Modifier/ShakeEffect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShakeEffect.swift
3 | // Omnia
4 | //
5 | // Created by khoa on 14/11/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | #if canImport(SwiftUI)
10 |
11 | import SwiftUI
12 |
13 | // https://learntalks.com/FrenchKit/2019/FrenchKit-2019-Animations-with-SwiftUI-Chris-Eidhof/
14 |
15 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
16 | public struct ShakeEffect: GeometryEffect {
17 | var position: CGFloat = 0
18 |
19 | public var animatableData: CGFloat {
20 | get { position }
21 | set { position = newValue }
22 | }
23 |
24 | public func effectValue(size: CGSize) -> ProjectionTransform {
25 | return ProjectionTransform(
26 | CGAffineTransform(translationX: sin(position * 2 * .pi), y: 0)
27 | )
28 | }
29 | }
30 |
31 | #endif
32 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/ActivittyIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivittyIndicator.swift
3 | // Omnia
4 | //
5 | // Created by khoa on 02/11/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | #if canImport(UIKit) && os(iOS)
10 |
11 | import SwiftUI
12 |
13 | @available(iOS 13.0, tvOS 13.0, *)
14 | public struct ActivityIndicator: UIViewRepresentable {
15 | @Binding
16 | var isAnimating: Bool
17 | let style: UIActivityIndicatorView.Style
18 |
19 | public init(isAnimating: Binding, style: UIActivityIndicatorView.Style) {
20 | self._isAnimating = isAnimating
21 | self.style = style
22 | }
23 |
24 | public func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView {
25 | return UIActivityIndicatorView(style: style)
26 | }
27 |
28 | public func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) {
29 | isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
30 | }
31 | }
32 |
33 | #endif
34 |
35 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/BlurView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlurView.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 14/05/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if os(iOS)
11 | public struct BlurView: UIViewRepresentable {
12 | public init(style: UIBlurEffect.Style) {
13 | self.style = style
14 | }
15 |
16 | private let style: UIBlurEffect.Style
17 |
18 | public func makeUIView(context: Context) -> UIView {
19 | let effect = UIBlurEffect(style: style)
20 | let view = UIVisualEffectView(effect: effect)
21 | return view
22 | }
23 |
24 | public func updateUIView(_ uiView: UIView, context: Context) {}
25 | }
26 | #endif
27 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/HUD.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HUD.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 22/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct HUD: View where Content: View {
11 | let content: () -> Content
12 |
13 | public init(@ViewBuilder content: @escaping () -> Content) {
14 | self.content = content
15 | }
16 |
17 | public var body: some View {
18 | content()
19 | .frame(width: 120, height: 120)
20 | .overlay(
21 | RoundedRectangle(cornerRadius: 12)
22 | .stroke(Color.separator.opacity(0.8), lineWidth: 1)
23 | )
24 | .background(
25 | effect
26 | )
27 | }
28 |
29 | private var effect: some View {
30 | #if os(OSX)
31 | return VisualEffectView(material: .popover)
32 | .clipShape(RoundedRectangle(cornerRadius: 12))
33 | #else
34 | return VisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
35 | .clipShape(RoundedRectangle(cornerRadius: 12))
36 | #endif
37 | }
38 | }
39 |
40 | public struct HUDLabel: View {
41 | let title: String
42 | let symbol: SFSymbol
43 |
44 | public init(title: String, symbol: SFSymbol) {
45 | self.title = title
46 | self.symbol = symbol
47 | }
48 |
49 | public var body: some View {
50 | HUD {
51 | VStack(spacing: 10) {
52 | Image(systemName: symbol.rawValue)
53 | .resizable()
54 | .aspectRatio(contentMode: .fit)
55 | .foregroundColor(.accentColor)
56 | .frame(width: 40, height: 40)
57 | Text(title)
58 | .minimumScaleFactor(0.8)
59 | .multilineTextAlignment(.center)
60 | .lineLimit(3)
61 | }
62 | .padding()
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/IfLet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IfLet.swift
3 | // Omnia
4 | //
5 | // Created by khoa on 13/11/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
12 | public struct IfLet: View {
13 | let value: T?
14 | let content: (T) -> Content
15 |
16 | public init(_ value: T?, @ViewBuilder content: @escaping (T) -> Content) {
17 | self.value = value
18 | self.content = content
19 | }
20 |
21 | public var body: some View {
22 | if let value = value {
23 | content(value)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/MakeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MakeView.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 22/05/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
11 | public struct MakeView: View {
12 | let content: Content
13 |
14 | public init(@ViewBuilder make: () -> Content) {
15 | self.content = make()
16 | }
17 |
18 | public var body: some View {
19 | content
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/MySearchField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MySearchField.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 18/03/2021.
6 | //
7 |
8 | #if os(OSX)
9 | import SwiftUI
10 | import AppKit
11 |
12 | public struct MySearchField: NSViewRepresentable {
13 | @Binding
14 | var text: String
15 |
16 | public init(text: Binding) {
17 | self._text = text
18 | }
19 |
20 | public func makeCoordinator() -> Coordinator {
21 | Coordinator(parent: self)
22 | }
23 |
24 | public func makeNSView(context: Context) -> NSSearchField {
25 | let view = NSSearchField()
26 | view.delegate = context.coordinator
27 | return view
28 | }
29 |
30 | public func updateNSView(_ view: NSSearchField, context: Context) {
31 | if view.stringValue != text {
32 | view.stringValue = text
33 | }
34 | }
35 |
36 | public final class Coordinator: NSObject, NSSearchFieldDelegate {
37 | let parent: MySearchField
38 |
39 | public init(parent: MySearchField) {
40 | self.parent = parent
41 | }
42 |
43 | public func controlTextDidChange(_ obj: Notification) {
44 | guard let nsSearchField = obj.object as? NSSearchField else { return }
45 | parent.text = nsSearchField.stringValue
46 | }
47 | }
48 | }
49 |
50 | #endif
51 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/MyTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MyTextField.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 18/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if os(OSX)
11 | import AppKit
12 |
13 | public struct MyTextField: NSViewRepresentable {
14 | @Binding
15 | var text: String
16 | let placeholder: String?
17 | let font: NSFont
18 | let alignment: NSTextAlignment
19 |
20 | public init(
21 | text: Binding,
22 | placeholder: String? = nil,
23 | font: NSFont = NSFont.preferredFont(forTextStyle: .body, options: [:]),
24 | alignment: NSTextAlignment = .left
25 | ) {
26 | self._text = text
27 | self.placeholder = placeholder
28 | self.font = font
29 | self.alignment = alignment
30 | }
31 |
32 | public func makeNSView(context: Context) -> NSTextField {
33 | let tf = NSTextField()
34 | tf.focusRingType = .none
35 | tf.isBordered = false
36 | tf.drawsBackground = false
37 | tf.delegate = context.coordinator
38 | tf.font = self.font
39 | tf.alignment = alignment
40 | tf.placeholderString = placeholder
41 | return tf
42 | }
43 |
44 | public func updateNSView(_ nsView: NSTextField, context: Context) {
45 | if nsView.stringValue != text {
46 | nsView.stringValue = text
47 | }
48 | }
49 |
50 | public func makeCoordinator() -> MyTextField.Coordinator {
51 | Coordinator(parent: self)
52 | }
53 |
54 | public class Coordinator: NSObject, NSTextFieldDelegate {
55 | let parent: MyTextField
56 | public init(parent: MyTextField) {
57 | self.parent = parent
58 | }
59 |
60 | public func controlTextDidChange(_ obj: Notification) {
61 | let textField = obj.object as! NSTextField
62 | parent.text = textField.stringValue
63 | }
64 | }
65 | }
66 |
67 | #endif
68 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/MyTextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MyTextview.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 18/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if canImport(UIKit)
11 | import UIKit
12 |
13 | public struct MyTextView: UIViewRepresentable {
14 | @Binding
15 | var text: String
16 | let isEditable: Bool
17 |
18 | public init(
19 | text: Binding,
20 | isEditable: Bool = true
21 | ) {
22 | self._text = text
23 | self.isEditable = isEditable
24 | }
25 |
26 | public final class Coordinator: NSObject, UITextViewDelegate {
27 | let parent: MyTextView
28 |
29 | public init(parent: MyTextView) {
30 | self.parent = parent
31 | }
32 |
33 | public func textViewDidChange(_ textView: UITextView) {
34 | if textView.text != parent.text {
35 | parent.text = textView.text
36 | }
37 | }
38 | }
39 |
40 | public func makeCoordinator() -> Coordinator {
41 | Coordinator(parent: self)
42 | }
43 |
44 | public func makeUIView(context: Context) -> UITextView {
45 | let view = UITextView()
46 | view.isScrollEnabled = true
47 | view.isEditable = true
48 | view.isUserInteractionEnabled = true
49 | view.autocorrectionType = .no
50 | view.autocapitalizationType = .none
51 | view.smartQuotesType = .no
52 | view.smartDashesType = .no
53 | view.smartInsertDeleteType = .no
54 | view.font = UIFont.preferredFont(forTextStyle: .body)
55 | view.delegate = context.coordinator
56 | view.isEditable = isEditable
57 | return view
58 | }
59 |
60 | public func updateUIView(_ uiView: UITextView, context: Context) {
61 | if uiView.text != text {
62 | uiView.text = text
63 | }
64 | }
65 | }
66 |
67 | #endif
68 |
69 | #if os(OSX)
70 | import Combine
71 | import AppKit
72 |
73 | public struct MyTextView: NSViewRepresentable {
74 | public typealias NSViewType = NSScrollView
75 |
76 | @Binding
77 | var text: String
78 | @Binding
79 | var isFocus: Bool
80 | let isEditable: Bool
81 |
82 | public init(
83 | text: Binding,
84 | isFocus: Binding,
85 | isEditable: Bool = true
86 | ) {
87 | self._text = text
88 | self._isFocus = isFocus
89 | self.isEditable = isEditable
90 | }
91 |
92 |
93 | public class Coordinator: NSObject, NSTextViewDelegate {
94 | let parent: MyTextView
95 |
96 | public init(parent: MyTextView) {
97 | self.parent = parent
98 | }
99 |
100 | public func textDidEndEditing(_ notification: Notification) {
101 | parent.isFocus = false
102 | }
103 |
104 | public func textDidChange(_ notification: Notification) {
105 | guard let textView = notification.object as? NSTextView else { return }
106 | if parent.text != textView.string {
107 | parent.text = textView.string
108 | }
109 | }
110 | }
111 |
112 | public func makeNSView(context: Context) -> NSViewType {
113 | let scrollView: NSScrollView = FocusTextView.scrollableTextView()
114 | let textView: NSTextView = scrollView.documentView as! FocusTextView
115 |
116 | textView.drawsBackground = false
117 | textView.textColor = NSColor.labelColor
118 | textView.font = NSFont.preferredFont(forTextStyle: .body, options: [:])
119 | textView.delegate = context.coordinator
120 | textView.isSelectable = true
121 | textView.isEditable = isEditable
122 |
123 | return scrollView
124 | }
125 |
126 | public func updateNSView(_ nsView: NSViewType, context: Context) {
127 | guard let textView = nsView.documentView as? NSTextView else { return }
128 |
129 | DispatchQueue.main.async {
130 | textView.string = text
131 | }
132 |
133 | if let lineLimit = context.environment.lineLimit {
134 | textView.textContainer?.maximumNumberOfLines = lineLimit
135 | }
136 | }
137 |
138 | public func makeCoordinator() -> Coordinator {
139 | Coordinator(parent: self)
140 | }
141 | }
142 |
143 | private class FocusTextView: NSTextView {
144 | var enablesScroll: Bool = true
145 | var onFocus: () -> Void = {}
146 |
147 | override func becomeFirstResponder() -> Bool {
148 | onFocus()
149 | return super.becomeFirstResponder()
150 | }
151 |
152 | override func scrollWheel(with event: NSEvent)
153 | {
154 | if enablesScroll {
155 | super.scrollWheel(with: event)
156 | } else {
157 | // 1st nextResponder is NSClipView
158 | // 2nd nextResponder is NSScrollView
159 | // 3rd nextResponder is NSResponder SwiftUIPlatformViewHost
160 | self.nextResponder?.nextResponder?.nextResponder?.scrollWheel(with: event)
161 | }
162 | }
163 | }
164 | #endif
165 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/PagerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PagerView.swift
3 | // EasySwiftUI
4 | //
5 | // Created by khoa on 30/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct PagerView: View {
11 | private let pageCount: Int
12 | private let content: Content
13 |
14 | @Binding
15 | var currentIndex: Int
16 |
17 | @GestureState
18 | private var translation: CGFloat = 0
19 |
20 | public init(
21 | pageCount: Int,
22 | currentIndex: Binding,
23 | @ViewBuilder content: () -> Content
24 | ) {
25 | self.pageCount = pageCount
26 | self._currentIndex = currentIndex
27 | self.content = content()
28 | }
29 |
30 | public var body: some View {
31 | GeometryReader { geometry in
32 | HStack(spacing: 0) {
33 | self.content
34 | .frame(
35 | width: geometry.size.width,
36 | height: geometry.size.height
37 | )
38 | .contentShape(Rectangle())
39 | }
40 | .frame(
41 | width: geometry.size.width,
42 | alignment: .leading
43 | )
44 | .offset(x: -CGFloat(self.currentIndex ) * geometry.size.width)
45 | .offset(x: self.translation)
46 | .animation(.interactiveSpring())
47 | .gesture(
48 | DragGesture().updating(self.$translation) { value, state, _ in
49 | state = value.translation.width
50 | }.onEnded { value in
51 | let offset = value.translation.width / geometry.size.width
52 | let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
53 | self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1)
54 | }
55 | )
56 | }
57 | }
58 | }
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/Sources/EasySwiftUI/View/VisualEffectView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VisualEffectView.swift
3 | //
4 | //
5 | // Created by khoa on 10/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if os(OSX)
11 |
12 | public struct VisualEffectView: NSViewRepresentable {
13 | let material: NSVisualEffectView.Material
14 | let blendingMode: NSVisualEffectView.BlendingMode
15 |
16 | public init(
17 | material: NSVisualEffectView.Material = .contentBackground,
18 | blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
19 | ) {
20 | self.material = material
21 | self.blendingMode = blendingMode
22 | }
23 |
24 | public func makeNSView(context: Context) -> NSVisualEffectView {
25 | let visualEffectView = NSVisualEffectView()
26 | visualEffectView.material = material
27 | visualEffectView.blendingMode = blendingMode
28 | visualEffectView.state = NSVisualEffectView.State.active
29 | return visualEffectView
30 | }
31 |
32 | public func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) {
33 | visualEffectView.material = material
34 | visualEffectView.blendingMode = blendingMode
35 | }
36 | }
37 |
38 | #else
39 |
40 | public struct VisualEffectView: UIViewRepresentable {
41 | let effect: UIVisualEffect
42 |
43 | public init(effect: UIVisualEffect) {
44 | self.effect = effect
45 | }
46 |
47 | public func makeUIView(context: Context) -> UIVisualEffectView {
48 | UIVisualEffectView()
49 | }
50 |
51 | public func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
52 | uiView.effect = effect
53 | }
54 | }
55 |
56 | #endif
57 |
--------------------------------------------------------------------------------
/Tests/EasySwiftUITests/EasySwiftUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import EasySwiftUI
3 |
4 | final class EasySwiftUITests: XCTestCase {
5 | func testExample() {}
6 |
7 | static var allTests = [
8 | ("testExample", testExample),
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/EasySwiftUITests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(EasySwiftUITests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import EasySwiftUITests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += EasySwiftUITests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------