├── .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 | --------------------------------------------------------------------------------