Skip to content

Instantly share code, notes, and snippets.

@tadija
Last active January 12, 2022 14:24
Show Gist options
  • Save tadija/2fa1a99ccf3413cd0e30e3633e1a32db to your computer and use it in GitHub Desktop.
Save tadija/2fa1a99ccf3413cd0e30e3633e1a32db to your computer and use it in GitHub Desktop.
AESwiftUI
/**
* https://gist.github.com/tadija/2fa1a99ccf3413cd0e30e3633e1a32db
* Revision 18
* Copyright © 2020-2021 Marko Tadić
* Licensed under the MIT license
*/
import SwiftUI
import Combine
// swiftlint:disable file_length
// MARK: - Top Level
public var isXcodePreview: Bool {
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
// MARK: - Color
public extension Color {
static func dynamic(light: Color, dark: Color) -> Color {
Color(UIColor(
dynamicProvider: {
$0.isDark ? UIColor(dark) : UIColor(light)
})
)
}
}
public extension UITraitCollection {
var isDark: Bool {
userInterfaceStyle == .dark
}
}
// MARK: - Layout
public struct LayoutFill<T: View>: View {
let color: Color
let alignment: Alignment
var content: T
public init(_ color: Color = .clear,
alignment: Alignment = .center,
@ViewBuilder content: () -> T) {
self.color = color
self.alignment = alignment
self.content = content()
}
public var body: some View {
color.overlay(content, alignment: alignment)
}
}
public struct LayoutCenter<T: View>: View {
let axis: Axis
var content: T
public init(_ axis: Axis, @ViewBuilder content: () -> T) {
self.axis = axis
self.content = content()
}
public var body: some View {
switch axis {
case .horizontal:
HStack(spacing: 0) { centeredContent }
case .vertical:
VStack(spacing: 0) { centeredContent }
}
}
@ViewBuilder
private var centeredContent: some View {
Spacer()
content
Spacer()
}
}
public struct LayoutHalf<T: View>: View {
let edge: Edge
var content: T
public init(_ edge: Edge, @ViewBuilder content: () -> T) {
self.edge = edge
self.content = content()
}
public var body: some View {
switch edge {
case .top:
VStack(spacing: 0) {
content
Color.clear
}
case .bottom:
VStack(spacing: 0) {
Color.clear
content
}
case .leading:
HStack(spacing: 0) {
content
Color.clear
}
case .trailing:
HStack(spacing: 0) {
Color.clear
content
}
}
}
}
public struct LayoutAlign<T: View>: View {
let alignment: Alignment
var content: T
public init(_ alignment: Alignment, @ViewBuilder content: () -> T) {
self.alignment = alignment
self.content = content()
}
public var body: some View {
switch alignment {
case .top:
Top { content }
case .bottom:
Bottom { content }
case .leading:
Leading { content }
case .trailing:
Trailing { content }
case .topLeading:
Top { Leading { content } }
case .topTrailing:
Top { Trailing { content } }
case .bottomLeading:
Bottom { Leading { content } }
case .bottomTrailing:
Bottom { Trailing { content } }
default:
fatalError("\(alignment) is not supported")
}
}
private struct Top<T: View>: View {
var content: () -> T
var body: some View {
VStack(spacing: 0) {
content()
Spacer()
}
}
}
private struct Bottom<T: View>: View {
var content: () -> T
var body: some View {
VStack(spacing: 0) {
Spacer()
content()
}
}
}
private struct Leading<T: View>: View {
var content: () -> T
var body: some View {
HStack(spacing: 0) {
content()
Spacer()
}
}
}
private struct Trailing<T: View>: View {
var content: () -> T
var body: some View {
HStack(spacing: 0) {
Spacer()
content()
}
}
}
}
public struct LayoutSplit<T1: View, T2: View>: View {
let axis: Axis
var firstHalf: T1
var secondHalf: T2
public init(
_ axis: Axis,
@ViewBuilder firstHalf: () -> T1,
@ViewBuilder secondHalf: () -> T2
) {
self.axis = axis
self.firstHalf = firstHalf()
self.secondHalf = secondHalf()
}
public var body: some View {
switch axis {
case .horizontal:
HStack(spacing: 0) {
Color.clear
.overlay(firstHalf)
Color.clear
.overlay(secondHalf)
}
case .vertical:
VStack(spacing: 0) {
Color.clear
.overlay(firstHalf)
Color.clear
.overlay(secondHalf)
}
}
}
}
// MARK: - View+General
/// - See: https://swiftwithmajid.com/2019/12/04/must-have-swiftui-extensions/
public extension View {
func eraseToAnyView() -> AnyView {
AnyView(self)
}
func embedInNavigation() -> some View {
NavigationView {
self
}
}
}
// MARK: - View+Notifications
/// - See: https://twitter.com/tadija/status/1311263107247943680
public extension View {
func onReceive(_ name: Notification.Name,
center: NotificationCenter = .default,
object: AnyObject? = nil,
perform action: @escaping (Notification) -> Void) -> some View {
self.onReceive(
center.publisher(for: name, object: object), perform: action
)
}
}
// MARK: - View+Condition
/// - See: https://fivestars.blog/swiftui/conditional-modifiers.html
public extension View {
@ViewBuilder
func `if`<T: View>(_ condition: Bool, modifier: (Self) -> T) -> some View {
if condition {
modifier(self)
} else {
self
}
}
@ViewBuilder
func `if`<T: View, F: View>(
_ condition: Bool,
if ifModifier: (Self) -> T,
else elseModifier: (Self) -> F) -> some View
{
if condition {
ifModifier(self)
} else {
elseModifier(self)
}
}
@ViewBuilder
func ifLet<V, T: View>(_ value: V?, modifier: (Self, V) -> T) -> some View {
if let value = value {
modifier(self, value)
} else {
self
}
}
}
// MARK: - View+Debug
/// - See: https://www.swiftbysundell.com/articles/building-swiftui-debugging-utilities/
public extension View {
func debugAction(_ closure: () -> Void) -> Self {
#if DEBUG
closure()
#endif
return self
}
func debugLog(_ value: Any) -> Self {
debugAction {
debugPrint(value)
}
}
}
public extension View {
func debugModifier<T: View>(_ modifier: (Self) -> T) -> some View {
#if DEBUG
return modifier(self)
#else
return self
#endif
}
func debugBorder(_ color: Color = .red, width: CGFloat = 1) -> some View {
debugModifier {
$0.border(color, width: width)
}
}
func debugBackground(_ color: Color = .red) -> some View {
debugModifier {
$0.background(color)
}
}
func debugGesture<G: Gesture>(_ gesture: G) -> some View {
debugModifier {
$0.gesture(gesture)
}
}
}
// MARK: - View+AnimationCompletion
/// - See: https://www.avanderlee.com/swiftui/withanimation-completion-callback
/// An animatable modifier that is used for observing animations for a given animatable value.
public struct AnimationCompletionObserverModifier<Value>: AnimatableModifier where Value: VectorArithmetic {
/// While animating, SwiftUI changes the old input value to the new target value using this property.
/// This value is set to the old value until the animation completes.
public var animatableData: Value {
didSet {
notifyCompletionIfFinished()
}
}
/// The target value for which we're observing. This value is directly set once the animation starts.
/// During animation, `animatableData` will hold the oldValue and is only updated to the target value
/// once the animation completes.
private var targetValue: Value
/// The completion callback which is called once the animation completes.
private var completion: () -> Void
init(observedValue: Value, completion: @escaping () -> Void) {
self.completion = completion
self.animatableData = observedValue
targetValue = observedValue
}
/// Verifies whether the current animation is finished and calls the completion callback if true.
private func notifyCompletionIfFinished() {
guard animatableData == targetValue else { return }
/// Dispatching is needed to take the next runloop for the completion callback.
DispatchQueue.main.async {
self.completion()
}
}
public func body(content: Content) -> some View {
/// We're not really modifying the view so we can directly return the original input value.
return content
}
}
public extension View {
/// Calls the completion handler whenever an animation on the given value completes.
/// - Parameters:
/// - value: The value to observe for animations.
/// - completion: The completion callback to call once the animation completes.
/// - Returns: A modified `View` instance with the observer attached.
func onAnimationCompleted<Value: VectorArithmetic>(
for value: Value, completion: @escaping () -> Void
) -> ModifiedContent<Self, AnimationCompletionObserverModifier<Value>> {
modifier(AnimationCompletionObserverModifier(observedValue: value, completion: completion))
}
}
// MARK: - View+Effects
/// - See: https://www.hackingwithswift.com/plus/swiftui-special-effects/shadows-and-glows
public extension View {
func glow(color: Color = .red, radius: CGFloat = 20) -> some View {
self
.shadow(color: color, radius: radius / 3)
.shadow(color: color, radius: radius / 3)
.shadow(color: color, radius: radius / 3)
}
func innerShadow<S: Shape>(using shape: S, angle: Angle = .degrees(0),
color: Color = .black, width: CGFloat = 6, blur: CGFloat = 6) -> some View {
let finalX = CGFloat(cos(angle.radians - .pi / 2))
let finalY = CGFloat(sin(angle.radians - .pi / 2))
return self
.overlay(
shape
.stroke(color, lineWidth: width)
.offset(x: finalX * width * 0.6, y: finalY * width * 0.6)
.blur(radius: blur)
.mask(shape)
)
}
}
// MARK: - Geometry+Helpers
public extension GeometryProxy {
var isPortrait: Bool {
size.height > size.width
}
var isLandscape: Bool {
size.width > size.height
}
}
// MARK: - List+Helpers
public extension List {
func hideSeparators() -> some View {
self
.onAppear {
UITableView.appearance().separatorStyle = .none
}
.onDisappear {
UITableView.appearance().separatorStyle = .singleLine
}
}
}
// MARK: - ButtonStyle
/// - See: https://stackoverflow.com/a/58176268/2165585
public struct ScaleButtonStyle: ButtonStyle {
var scale: CGFloat = 2
var animationIn: Animation? = .none
var animationOut: Animation? = .default
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.contentShape(Rectangle())
.scaleEffect(configuration.isPressed ? scale : 1)
.animation(configuration.isPressed ? animationIn : animationOut)
}
}
// MARK: - PreferenceKey / CGSize
/// - See: https://stackoverflow.com/a/63305935/2165585
public protocol CGSizePreferenceKey: PreferenceKey where Value == CGSize {}
public extension CGSizePreferenceKey {
static func reduce(value _: inout CGSize, nextValue: () -> CGSize) {
_ = nextValue()
}
}
public extension View {
func onSizeChanged<Key: CGSizePreferenceKey>(
_ key: Key.Type,
perform action: @escaping (CGSize) -> Void) -> some View
{
self.background(GeometryReader { geo in
Color.clear
.preference(key: Key.self, value: geo.size)
})
.onPreferenceChange(key) { value in
action(value)
}
}
}
// MARK: - PreferenceKey / CGFloat
public protocol CGFloatPreferenceKey: PreferenceKey where Value == CGFloat {}
public extension CGFloatPreferenceKey {
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
public extension View {
func changePreference<Key: CGFloatPreferenceKey>(
_ key: Key.Type,
using closure: @escaping (GeometryProxy) -> CGFloat) -> some View
{
self.background(GeometryReader { geo in
Color.clear
.preference(key: Key.self, value: closure(geo))
})
}
}
// MARK: - SafeAreaView
struct SafeAreaView<T: View>: View {
var edges: Edge.Set
var content: () -> T
@State private var safeArea: UIEdgeInsets = UIWindow.safeArea
var body: some View {
content()
.padding(.top, edges.contains(.top) ? safeArea.top : 0)
.padding(.bottom, edges.contains(.bottom) ? safeArea.bottom : 0)
.padding(.leading, edges.contains(.leading) ? safeArea.left : 0)
.padding(.trailing, edges.contains(.trailing) ? safeArea.right : 0)
.onReceive(UIDevice.orientationDidChangeNotification) { _ in
safeArea = UIWindow.safeArea
}
}
}
struct SafeAreaViewModifier: ViewModifier {
var edges: Edge.Set
func body(content: Content) -> some View {
SafeAreaView(edges: edges) {
content
}
}
}
public extension View {
func edgesRespectingSafeArea(_ edges: Edge.Set) -> some View {
self.modifier(SafeAreaViewModifier(edges: edges))
}
}
public extension Edge.Set {
static let none: Edge.Set = []
}
// MARK: - UIWindow+Helpers
public extension UIWindow {
static var keyWindow: UIWindow? {
UIApplication.shared.windows
.first(where: { $0.isKeyWindow })
}
static var safeArea: UIEdgeInsets {
keyWindow?.safeAreaInsets ?? .zero
}
static var isPortrait: Bool {
scene?.interfaceOrientation.isPortrait ?? true
}
static var isLandscape: Bool {
scene?.interfaceOrientation.isLandscape ?? false
}
static var isSplitOrSlideOver: Bool {
guard let window = keyWindow else {
return false
}
return !window.frame.equalTo(window.screen.bounds)
}
static var statusBarHeight: CGFloat {
scene?.statusBarManager?.statusBarFrame.height ?? 0
}
static var navigationBarHeight: CGFloat {
topViewController?.navigationController?.navigationBar.bounds.height ?? 0
}
static var topViewController: UIViewController? {
topViewController(keyWindow?.rootViewController)
}
// MARK: Helpers
private class func topViewController(_ vc: UIViewController?) -> UIViewController? {
if let nav = vc as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = vc as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
if let presented = vc?.presentedViewController {
return topViewController(presented)
}
return vc
}
private static var statusBarFrame: CGRect? {
scene?.statusBarManager?.statusBarFrame
}
private static var scene: UIWindowScene? {
keyWindow?.windowScene
}
}
// MARK: - KeyboardAdaptive
/// - See: https://gist.github.com/scottmatthewman/722987c9ad40f852e2b6a185f390f88d
public struct KeyboardAdaptive: ViewModifier {
@State private var currentHeight: CGFloat = 0
public func body(content: Content) -> some View {
content
.padding(.bottom, currentHeight)
.edgesIgnoringSafeArea(currentHeight == 0 ? [] : .bottom)
.onAppear(perform: subscribeToKeyboardEvents)
}
private func subscribeToKeyboardEvents() {
NotificationCenter.Publisher(
center: NotificationCenter.default,
name: UIResponder.keyboardWillShowNotification
).compactMap { notification in
notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect
}.map { rect in
rect.height
}.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
NotificationCenter.Publisher(
center: NotificationCenter.default,
name: UIResponder.keyboardWillHideNotification
).compactMap { _ in
CGFloat.zero
}.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
}
}
public extension View {
func keyboardAdaptive() -> some View {
modifier(KeyboardAdaptive())
}
}
// MARK: - CornerRadius
/// - See: https://stackoverflow.com/a/58606176/2165585
public struct RoundedCorner: Shape {
public var radius: CGFloat
public var corners: UIRectCorner
public init(radius: CGFloat = .infinity,
corners: UIRectCorner = .allCorners) {
self.radius = radius
self.corners = corners
}
public func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
public extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
// MARK: - StickyHeader
/// - See: https://trailingclosure.com/sticky-header
public struct StickyHeader<Content: View>: View {
public var minHeight: CGFloat
public var content: Content
public init(
minHeight: CGFloat = 200,
@ViewBuilder content: () -> Content
) {
self.minHeight = minHeight
self.content = content()
}
public var body: some View {
GeometryReader { geo in
if geo.frame(in: .global).minY <= 0 {
content.frame(
width: geo.size.width,
height: geo.size.height,
alignment: .center
)
} else {
content
.offset(y: -geo.frame(in: .global).minY)
.frame(
width: geo.size.width,
height: geo.size.height + geo.frame(in: .global).minY
)
}
}.frame(minHeight: minHeight)
}
}
// MARK: - Placeholder
public struct Placeholder: View {
var text: String
public init(_ text: String = "placeholder") {
self.text = text
}
public var body: some View {
ZStack {
Rectangle()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [10]))
.foregroundColor(.secondary)
Text(text)
.multilineTextAlignment(.center)
.foregroundColor(.primary)
}
}
}
// MARK: - ImageName
public enum ImageName {
case custom(String)
case system(String)
}
public extension Image {
init(_ imageName: ImageName) {
switch imageName {
case .custom(let name):
self = Image(name)
case .system(let name):
self = Image(systemName: name)
}
}
}
// MARK: - Scalable Font
public extension Text {
enum ScalableFont {
case system
case custom(String)
}
func scalableFont(_ scalableFont: ScalableFont = .system,
padding: CGFloat = 0) -> some View {
self
.font(resolveFont(for: scalableFont))
.padding(padding)
.minimumScaleFactor(0.01)
.lineLimit(1)
}
private func resolveFont(for scalableFont: ScalableFont) -> Font {
switch scalableFont {
case .system:
return .system(size: 500)
case .custom(let name):
return .custom(name, size: 500)
}
}
}
// MARK: - Binding+Setter
/// - See: https://gist.github.com/Amzd/c3015c7e938076fc1e39319403c62950
public extension Binding {
func didSet(_ didSet: @escaping ((newValue: Value, oldValue: Value)) -> Void) -> Binding<Value> {
.init(
get: {
wrappedValue
},
set: { newValue in
let oldValue = wrappedValue
wrappedValue = newValue
didSet((newValue, oldValue))
}
)
}
func willSet(_ willSet: @escaping ((newValue: Value, oldValue: Value)) -> Void) -> Binding<Value> {
.init(
get: {
wrappedValue
},
set: { newValue in
willSet((newValue, wrappedValue))
wrappedValue = newValue
}
)
}
}
// MARK: - State Object for iOS 13
/// - See: https://dev.to/waj/stateobject-alternative-for-ios-13-2271
public struct StateObjectContainer<Observable, Content>: View where Observable: ObservableObject, Content: View {
@State private var object: Observable?
private var initializer: () -> Observable
private var content: Content
public init(_ initializer: @autoclosure @escaping () -> Observable,
@ViewBuilder content: () -> Content) {
self.content = content()
self.initializer = initializer
}
public var body: some View {
if let object = object {
content.environmentObject(object)
} else {
Color.clear.onAppear(perform: initialize)
}
}
private func initialize() {
object = initializer()
}
}
// MARK: - Share Sheet
/// - See: https://developer.apple.com/forums/thread/123951
public struct ShareSheet: UIViewControllerRepresentable {
public typealias Callback = (
_ activityType: UIActivity.ActivityType?,
_ completed: Bool,
_ returnedItems: [Any]?,
_ error: Error?
) -> Void
public let activityItems: [Any]
public let applicationActivities: [UIActivity]?
public let excludedActivityTypes: [UIActivity.ActivityType]?
public let callback: Callback?
public init(activityItems: [Any],
applicationActivities: [UIActivity]? = nil,
excludedActivityTypes: [UIActivity.ActivityType]? = nil,
callback: Callback? = nil) {
self.activityItems = activityItems
self.applicationActivities = applicationActivities
self.excludedActivityTypes = excludedActivityTypes
self.callback = callback
}
public func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities
)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
public func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
// MARK: - ToggleAsync
public struct ToggleAsync<T: View>: View {
@Binding var isOn: Bool
var label: () -> T
var onValueChanged: ((Bool) -> Void)?
public init(isOn: Binding<Bool>,
label: @escaping () -> T,
onValueChanged: ((Bool) -> Void)? = nil) {
self._isOn = isOn
self.label = label
self.onValueChanged = onValueChanged
}
public var body: some View {
Toggle(
isOn: $isOn
.didSet { newValue, oldValue in
if newValue != oldValue {
onValueChanged?(newValue)
}
},
label: label
)
}
}
// MARK: - Line
public struct Line: Shape {
public let x1: CGFloat
public let y1: CGFloat
public let x2: CGFloat
public let y2: CGFloat
public init(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) {
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
}
public func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: x1, y: y1))
path.addLine(to: CGPoint(x: x2, y: y2))
return path
}
}
public struct LineTop: Shape {
public init() {}
public func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}
}
public struct LineLeft: Shape {
public init() {}
public func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
return path
}
}
public struct LineBottom: Shape {
public init() {}
public func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
return path
}
}
public struct LineRight: Shape {
public init() {}
public func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
return path
}
}
// MARK: - Pie
/// - See: https://cs193p.sites.stanford.edu
public struct Pie: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool = false
public init(startAngle: Angle, endAngle: Angle, clockwise: Bool) {
self.startAngle = startAngle
self.endAngle = endAngle
self.clockwise = clockwise
}
public var animatableData: AnimatablePair<Double, Double> {
get {
AnimatablePair(startAngle.radians, endAngle.radians)
}
set {
startAngle = Angle.radians(newValue.first)
endAngle = Angle.radians(newValue.second)
}
}
public func path(in rect: CGRect) -> Path {
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2
let start = CGPoint(
x: center.x + radius * cos(CGFloat(startAngle.radians)),
y: center.y + radius * sin(CGFloat(startAngle.radians))
)
var p = Path()
p.move(to: center)
p.addLine(to: start)
p.addArc(
center: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: clockwise
)
p.addLine(to: center)
return p
}
}
// MARK: - Polygon
/// - See: https://swiftui-lab.com/swiftui-animations-part1
public struct Polygon: Shape {
var sides: Double
var scale: Double
var drawVertexLines: Bool
public init(sides: Double, scale: Double, drawVertexLines: Bool = false) {
self.sides = sides
self.scale = scale
self.drawVertexLines = drawVertexLines
}
public var animatableData: AnimatablePair<Double, Double> {
get {
AnimatablePair(sides, scale)
}
set {
sides = newValue.first
scale = newValue.second
}
}
public func path(in rect: CGRect) -> Path {
let hypotenuse = Double(min(rect.size.width, rect.size.height)) / 2.0 * scale
let center = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0)
var path = Path()
let extra: Int = sides != Double(Int(sides)) ? 1 : 0
var vertex: [CGPoint] = []
for i in 0..<Int(sides) + extra {
let angle = (Double(i) * (360.0 / sides)) * (Double.pi / 180)
// calculate vertex
let pt = CGPoint(
x: center.x + CGFloat(cos(angle) * hypotenuse),
y: center.y + CGFloat(sin(angle) * hypotenuse)
)
vertex.append(pt)
if i == 0 {
path.move(to: pt) // move to first vertex
} else {
path.addLine(to: pt) // draw line to next vertex
}
}
path.closeSubpath()
if drawVertexLines {
drawVertexLines(path: &path, vertex: vertex, n: 0)
}
return path
}
private func drawVertexLines(path: inout Path, vertex: [CGPoint], n: Int) {
if (vertex.count - n) < 3 { return }
for i in (n + 2)..<min(n + (vertex.count - 1), vertex.count) {
path.move(to: vertex[n])
path.addLine(to: vertex[i])
}
drawVertexLines(path: &path, vertex: vertex, n: n + 1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment