diff --git a/Sources/UIViewKit/FoundationExtensions/NSObjectProtocol+ibApply.swift b/Sources/UIViewKit/FoundationExtensions/NSObjectProtocol+ibApply.swift index f316c67..78a41d2 100644 --- a/Sources/UIViewKit/FoundationExtensions/NSObjectProtocol+ibApply.swift +++ b/Sources/UIViewKit/FoundationExtensions/NSObjectProtocol+ibApply.swift @@ -2,7 +2,7 @@ // NSObjectProtocol+ibApply.swift // UIViewKit // -// Created by blz on 22/08/2025. +// Created by Blazej SLEBODA on 22/08/2025. // import Foundation diff --git a/Sources/UIViewKit/IBConstraints/IBConstraints.swift b/Sources/UIViewKit/IBConstraints/IBConstraints.swift index c91ce4d..edb3a87 100644 --- a/Sources/UIViewKit/IBConstraints/IBConstraints.swift +++ b/Sources/UIViewKit/IBConstraints/IBConstraints.swift @@ -39,6 +39,9 @@ public final class IBConstraints { constraints.append(view.topAnchor.constraint(equalTo: (target as? UILayoutGuide)?.topAnchor ?? (target as! UIView).topAnchor, constant: value)) case .bottom(let value): constraints.append(view.bottomAnchor.constraint(equalTo: (target as? UILayoutGuide)?.bottomAnchor ?? (target as! UIView).bottomAnchor, constant: value)) + case .center: + constraints.append(view.centerXAnchor.constraint(equalTo: (target as? UILayoutGuide)?.centerXAnchor ?? (target as! UIView).centerXAnchor)) + constraints.append(view.centerYAnchor.constraint(equalTo: (target as? UILayoutGuide)?.centerYAnchor ?? (target as! UIView).centerYAnchor)) case .centerX(let value): constraints.append(view.centerXAnchor.constraint(equalTo: (target as? UILayoutGuide)?.centerXAnchor ?? (target as! UIView).centerXAnchor, constant: value)) case .centerY(let value): @@ -66,6 +69,7 @@ public final class IBConstraints { case right(CGFloat) case top(CGFloat) case bottom(CGFloat) + case center case centerX(CGFloat) case centerY(CGFloat) case leading(CGFloat) diff --git a/Sources/UIViewKit/IBPreviews/IBPreview+FreeFormView.swift b/Sources/UIViewKit/IBPreviews/IBPreview+FreeFormView.swift deleted file mode 100644 index 90355dd..0000000 --- a/Sources/UIViewKit/IBPreviews/IBPreview+FreeFormView.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// IBPreview+FreeFormView.swift -// UIViewKit -// -// Created by Blazej SLEBODA on 14/11/2023. -// - -import UIKit -import SwiftUI - -extension IBPreview { - - @available(iOS 13.0, *) - public struct FreeFormView: UIViewControllerRepresentable { - - private let viewMaker: (Context) -> UIView - - public init(_ view: UIView) { - self.viewMaker = { _ in - view - } - } - - public init(viewMaker: @escaping (Context) -> UIView) { - self.viewMaker = viewMaker - } - - public func makeUIViewController(context: Context) -> UIViewController { - let controller = UIViewController() - let view = viewMaker(context) - controller.view.addSubview(view) - view.frame = controller.view.bounds - view.autoresizingMask = [.flexibleHeight, .flexibleWidth] - view.translatesAutoresizingMaskIntoConstraints = true - - let freeFormContainer = ContainerViewController() - _ = freeFormContainer.view - freeFormContainer.childViewController = controller - return freeFormContainer - } - - public func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } - } -} diff --git a/Sources/UIViewKit/IBPreviews/IBPreview+FreeFormViewController.swift b/Sources/UIViewKit/IBPreviews/IBPreview+FreeFormViewController.swift deleted file mode 100644 index a5d8dc7..0000000 --- a/Sources/UIViewKit/IBPreviews/IBPreview+FreeFormViewController.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// IBPreview+FreeFormViewController.swift -// UIViewKit -// -// Created by Blazej SLEBODA on 13/11/2023. -// - -import UIKit -import SwiftUI - -extension IBPreview { - - @available(iOS 13.0, *) - public struct FreeFormViewController: UIViewControllerRepresentable { - - private let makeUIViewController: () -> UIViewController - - public init(_ maker: @autoclosure @escaping () -> UIViewController) { - makeUIViewController = maker - } - - public init(_ maker: @escaping () -> UIViewController) { - makeUIViewController = maker - } - - public func makeUIViewController(context: Context) -> UIViewController { - let containerVC = ContainerViewController() - containerVC.childViewController = makeUIViewController() - return containerVC - } - - public func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } - } -} - -extension IBPreview { - - final class ContainerViewController: UIViewController { - - var childViewController: UIViewController? { - didSet { - setupChildViewController() - } - } - - private func setupChildViewController() { - guard let childVC = childViewController else { return } - - addChild(childVC) - view.addSubview(childVC.view) - - childVC.view.frame = .init(origin: .zero, size: view.frame.size) - - childVC.didMove(toParent: self) - - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - view.addGestureRecognizer(panGesture) - } - - @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { - let translation = gesture.translation(in: view) - if let childView = childViewController?.view { - let newWidth = max(20, childView.frame.width + translation.x) - let newHeight = max(20, childView.frame.height + translation.y) - childView.frame.size = CGSize(width: newWidth, height: newHeight) - } - gesture.setTranslation(.zero, in: view) - } - } -} diff --git a/Sources/UIViewKit/IBPreviews/IBPreview+FullScreenView.swift b/Sources/UIViewKit/IBPreviews/IBPreview+FullScreenView.swift deleted file mode 100644 index 626fce2..0000000 --- a/Sources/UIViewKit/IBPreviews/IBPreview+FullScreenView.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// IBPreview+FullScreenView.swift -// UIViewKit -// -// Created by Blazej SLEBODA on 16/11/2023. -// - -import UIKit -import SwiftUI - -extension IBPreview { - - @available(iOS 13.0, *) - public struct FullScreenView: UIViewRepresentable { - - private let viewMaker: () -> UIView - - public init(_ viewMaker: @escaping @autoclosure () -> UIView) { - self.viewMaker = viewMaker - } - - public init(_ viewMaker: @escaping () -> UIView) { - self.viewMaker = viewMaker - } - - public func makeUIView(context: Context) -> UIView { - UIView().ibSubviews { containerView in - viewMaker().ibAttributes { - $0.topAnchor.constraint(equalTo: containerView.topAnchor) - $0.leftAnchor.constraint(equalTo: containerView.leftAnchor) - $0.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) - $0.centerYAnchor.constraint(equalTo: containerView.centerYAnchor) - } - } - } - - public func updateUIView(_ uiView: UIView, context: Context) { - uiView.setNeedsUpdateConstraints() - } - } -} diff --git a/Sources/UIViewKit/IBPreviews/IBPreview+FullScreenViewController.swift b/Sources/UIViewKit/IBPreviews/IBPreview+FullScreenViewController.swift deleted file mode 100644 index c16bd05..0000000 --- a/Sources/UIViewKit/IBPreviews/IBPreview+FullScreenViewController.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// IBPreview+FullScreenViewController.swift -// UIViewKit -// -// Created by Blazej SLEBODA on 20/09/2023. -// - -import SwiftUI - -extension IBPreview { - - @available(iOS 13.0, *) - public struct FullScreenViewController: UIViewControllerRepresentable { - - private let viewController: UIViewController - - public init(_ viewController: @escaping @autoclosure () -> UIViewController) { - self.viewController = viewController() - } - - public init(_ viewController: @escaping () -> UIViewController) { - self.viewController = viewController() - } - - public func makeUIViewController(context: Context) -> UIViewController { - viewController - } - - public func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } - } -} diff --git a/Sources/UIViewKit/IBPreviews/IBPreview+SizeThatFitsView.swift b/Sources/UIViewKit/IBPreviews/IBPreview+SizeThatFitsView.swift deleted file mode 100644 index 66532a2..0000000 --- a/Sources/UIViewKit/IBPreviews/IBPreview+SizeThatFitsView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// IBPreview+SizeThatFitsView.swift -// UIViewKit -// -// Created by Blazej SLEBODA on 13/11/2023. -// - -import SwiftUI - -extension IBPreview { - - @available(iOS 16.0, *) - public struct SizeThatFitsView: UIViewRepresentable { - - private let viewMaker: () -> UIView - - public init(_ viewMaker: @escaping @autoclosure () -> UIView) { - self.viewMaker = viewMaker - } - - public init(_ viewMaker: @escaping () -> UIView) { - self.viewMaker = viewMaker - } - - public func makeUIView(context: Context) -> UIView { - UIView().ibSubviews { containerView in - viewMaker().ibAttributes { - $0.topAnchor.constraint(equalTo: containerView.topAnchor).ibPriority(.init(1)) - $0.leftAnchor.constraint(equalTo: containerView.leftAnchor).ibPriority(.init(1)) - $0.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) - $0.centerYAnchor.constraint(equalTo: containerView.centerYAnchor) - } - } - } - - public func updateUIView(_ uiView: UIView, context: Context) { - uiView.setNeedsUpdateConstraints() - } - - @available(iOS 16, *) - public func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIView, context: Context) -> CGSize? { - uiView.subviews[0].systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) - } - } -} diff --git a/Sources/UIViewKit/IBPreviews/IBPreview.swift b/Sources/UIViewKit/IBPreviews/IBPreview.swift deleted file mode 100644 index 9e74dea..0000000 --- a/Sources/UIViewKit/IBPreviews/IBPreview.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// IBPreview.swift -// UIViewKit -// -// Created by Blazej SLEBODA on 10/02/2024. -// - -import Foundation - -public final class IBPreview { - - private init() { } -} diff --git a/Sources/UIViewKit/IBPreviews/IBPreviewFreeForm.swift b/Sources/UIViewKit/IBPreviews/IBPreviewFreeForm.swift new file mode 100644 index 0000000..d7277ca --- /dev/null +++ b/Sources/UIViewKit/IBPreviews/IBPreviewFreeForm.swift @@ -0,0 +1,197 @@ +// +// IBPreviewFreeForm.swift +// UIViewKit +// +// Created by Blazej SLEBODA on 13/11/2023. +// + +import UIKit +import SwiftUI + +@available(iOS 13.0, *) +public class IBPreviewFreeForm: ViewControllerFreeFormContainer { + + private var viewControllerMaker: (() -> UIViewController)? + private var viewMaker: (() -> UIView)? + + public required init?(coder: NSCoder) { + fatalError() + } + + public init(view: UIView) { + super.init(nibName: nil, bundle: nil) + self.viewMaker = { view } + } + + public init(_ viewMaker: @escaping () -> UIView) { + super.init(nibName: nil, bundle: nil) + self.viewMaker = viewMaker + } + + public init(viewController: UIViewController) { + super.init(nibName: nil, bundle: nil) + self.viewControllerMaker = { viewController } + } + + public init(_ viewControllerMaker: @escaping () -> UIViewController) { + super.init(nibName: nil, bundle: nil) + self.viewControllerMaker = viewControllerMaker + } + + public init(view: some View) { + super.init(nibName: nil, bundle: nil) + self.viewControllerMaker = { UIHostingController(rootView: view) } + } + + public init(_ viewMaker: @escaping () -> some View) { + super.init(nibName: nil, bundle: nil) + self.viewControllerMaker = { UIHostingController(rootView: viewMaker()) } + } + + public override func loadView() { + super.loadView() + if let viewMaker = viewMaker { + let viewToPreview = viewMaker() + containerView.ibSubviews { superview in + viewToPreview.ibAttributes { + $0.ibConstraints(to: superview, guide: .view, anchors: .all) + } + } + return + } else if let viewControllerMaker = viewControllerMaker { + let controller = viewControllerMaker() + controller.loadViewIfNeeded() + addChild(controller) + containerView.ibSubviews { superview in + controller.view.ibAttributes { + $0.ibConstraints(to: superview, guide: .view, anchors: .all) + } + } + controller.didMove(toParent: self) + } else { + fatalError() + } + } + +} + +public class ViewControllerFreeFormContainer: UIViewController { + + var containerView: UIView! + var heightConstraint: NSLayoutConstraint! + var widthConstraint: NSLayoutConstraint! + private var iPhoneSE2FrameView: UIView! + private var iPhoneSE2WithKeyboardFrameView: UIView! + private var snapToViewFeature: SnapToViewFeature! + private let iPhoneSE2Frame = CGRect(origin: .zero, size: .init(width: 375, height: 667)) + private let iPhoneSE2WithKeyboardFrame = CGRect(origin: .zero, size: .init(width: 375, height: 451)) + + public override func loadView() { + super.loadView() + view.backgroundColor = .lightGray + view.ibSubviews { superview in + UIView().ibOutlet(&iPhoneSE2FrameView).ibSubviews { superview in + UILabel().ibAttributes { + $0.ibConstraints(to: superview, guide: .view, anchors: .left, .bottom, .right) + $0.text = "iPhone SE2" + $0.textAlignment = .center + $0.textColor = .cyan + } + }.ibAttributes { + $0.topAnchor.constraint(equalTo: superview.topAnchor) + $0.leadingAnchor.constraint(equalTo: superview.leadingAnchor) + $0.widthAnchor.constraint(equalToConstant: iPhoneSE2Frame.width) + $0.heightAnchor.constraint(equalToConstant: iPhoneSE2Frame.height) + $0.layer.borderColor = UIColor.cyan.cgColor + $0.layer.borderWidth = 2 + } + } + view.ibSubviews { superview in + UIView().ibOutlet(&iPhoneSE2WithKeyboardFrameView).ibSubviews { superview in + UILabel().ibAttributes { + $0.ibConstraints(to: superview, guide: .view, anchors: .left, .bottom, .right) + $0.text = "iPhone SE2 With Keyboard" + $0.textAlignment = .center + $0.textColor = UIColor.yellow + } + }.ibAttributes { + $0.topAnchor.constraint(equalTo: superview.topAnchor) + $0.leadingAnchor.constraint(equalTo: superview.leadingAnchor) + $0.widthAnchor.constraint(equalToConstant: iPhoneSE2WithKeyboardFrame.width) + $0.heightAnchor.constraint(equalToConstant: iPhoneSE2WithKeyboardFrame.height) + $0.layer.borderColor = UIColor.yellow.cgColor + $0.layer.borderWidth = 2 + } + } + view.ibSubviews { superview in + UIView().ibOutlet(&containerView).ibAttributes { + $0.topAnchor.constraint(equalTo: superview.topAnchor) + $0.leadingAnchor.constraint(equalTo: superview.leadingAnchor) + $0.widthAnchor.constraint(equalToConstant: view.frame.width).ibOutlet(&widthConstraint) + $0.heightAnchor.constraint(equalToConstant: view.frame.height).ibOutlet(&heightConstraint) + } + } + snapToViewFeature = .init( + viewToSnap: [ + iPhoneSE2WithKeyboardFrameView, + iPhoneSE2FrameView, + view, + ], + containerView: containerView, + controller: self + ) + view.addGestureRecognizer(snapToViewFeature.tapGesture()) + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + workaroundHideKeyboardAndSizeContainerView() + } + + private func workaroundHideKeyboardAndSizeContainerView() { + DispatchQueue.main.async { + UIView.performWithoutAnimation { + self.view.endEditing(true) + } + } + } + + public override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + guard let touch = touches.first else { return } + let loc = touch.location(in: view) + heightConstraint.constant = loc.y + widthConstraint.constant = loc.x + } +} + +final class SnapToViewFeature { + + private let viewToSnap: [UIView] + private let containerView: UIView + private let controller: ViewControllerFreeFormContainer + + init(viewToSnap: [UIView], containerView: UIView, controller: ViewControllerFreeFormContainer) { + self.viewToSnap = viewToSnap + self.containerView = containerView + self.controller = controller + } + + func tapGesture() -> UIGestureRecognizer { + UITapGestureRecognizer(target: self, action: #selector(didTap(gesture:))) + } + + @objc + func didTap(gesture: UIGestureRecognizer) { + guard let tapGesture = gesture as? UITapGestureRecognizer else { return } + for viewToSnap in self.viewToSnap { + let loc = tapGesture.location(in: viewToSnap) + if viewToSnap.frame.contains(loc) { + controller.widthConstraint.constant = viewToSnap.frame.width + controller.heightConstraint.constant = viewToSnap.frame.height + return + } + } + } + +} diff --git a/Sources/UIViewKit/IBPreviews/IBPreviewSizeThatFits.swift b/Sources/UIViewKit/IBPreviews/IBPreviewSizeThatFits.swift new file mode 100644 index 0000000..ad98e57 --- /dev/null +++ b/Sources/UIViewKit/IBPreviews/IBPreviewSizeThatFits.swift @@ -0,0 +1,65 @@ +// +// IBPreviewSizeThatFits.swift +// UIViewKit +// +// Created by Blazej SLEBODA on 02/09/2025. +// + +import UIKit + +@available(iOS 13.0, *) +public class IBPreviewSizeThatFits: UIViewController { + + private var viewControllerMaker: (() -> UIViewController)? + private var viewMaker: (() -> UIView)? + + public required init?(coder: NSCoder) { + fatalError() + } + + public init(view: UIView) { + super.init(nibName: nil, bundle: nil) + self.viewMaker = { view } + } + + public init(_ viewMaker: @escaping () -> UIView) { + super.init(nibName: nil, bundle: nil) + self.viewMaker = viewMaker + } + + public init(viewController: UIViewController) { + super.init(nibName: nil, bundle: nil) + self.viewControllerMaker = { viewController } + } + + public init(_ viewControllerMaker: @escaping () -> UIViewController) { + super.init(nibName: nil, bundle: nil) + self.viewControllerMaker = viewControllerMaker + } + + public override func loadView() { + super.loadView() + if let viewMaker = viewMaker { + let viewToPreview = viewMaker() + view.ibSubviews { + viewToPreview.ibAttributes { + $0.ibConstraints(to: view, guide: .view, anchors: .center) + } + } + return + } else if let viewControllerMaker = viewControllerMaker { + let controller = viewControllerMaker() + controller.loadViewIfNeeded() + addChild(controller) + view.ibSubviews { superview in + controller.view.ibAttributes { + $0.ibConstraints(to: superview, guide: .view, anchors: .center) + } + } + controller.didMove(toParent: self) + } else { + fatalError() + } + } + +} diff --git a/Sources/UIViewKit/IBPreviews/IBRepresentables.swift b/Sources/UIViewKit/IBPreviews/IBRepresentables.swift new file mode 100644 index 0000000..a6cd5fe --- /dev/null +++ b/Sources/UIViewKit/IBPreviews/IBRepresentables.swift @@ -0,0 +1,53 @@ +// +// IBRepresentableViewController.swift +// UIViewKit +// +// Created by Blazej SLEBODA on 02/09/2025. +// + +import UIKit +import SwiftUI + +@available(iOS, introduced: 13, obsoleted: 17) +public struct IBRepresentableViewController: UIViewControllerRepresentable { + + public typealias UIViewControllerType = UIViewController + + private let viewControllerMaker: () -> UIViewController + + public init(_ viewController: UIViewController) { + viewControllerMaker = { viewController } + } + + public init(_ viewControllerMaker: @escaping () -> UIViewController) { + self.viewControllerMaker = viewControllerMaker + } + + public func makeUIViewController(context: Context) -> UIViewController { + viewControllerMaker() + } + + public func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } +} + +@available(iOS, introduced: 13, obsoleted: 17) +public struct IBRepresentableView: UIViewRepresentable { + + public typealias UIViewType = UIView + + private let viewMaker: () -> UIView + + public init(_ view: UIView) { + viewMaker = { view } + } + + public init (_ viewMaker: @escaping () -> UIView) { + self.viewMaker = viewMaker + } + + public func makeUIView(context: Context) -> UIView { + viewMaker() + } + + public func updateUIView(_ uiView: UIView, context: Context) { } +} diff --git a/Sources/UIViewKit/UIKitExtensions/UIStackView+Extensions.swift b/Sources/UIViewKit/UIKitExtensions/UIStackView+Extensions.swift index b783952..9d00fc1 100644 --- a/Sources/UIViewKit/UIKitExtensions/UIStackView+Extensions.swift +++ b/Sources/UIViewKit/UIKitExtensions/UIStackView+Extensions.swift @@ -9,8 +9,8 @@ import UIKit extension UIStackView { - public convenience init(axis: NSLayoutConstraint.Axis, spacing: CGFloat? = nil, alignment: UIStackView.Alignment? = nil, distribution: UIStackView.Distribution? = nil) { - self.init() + public convenience init(frame: CGRect = .zero, axis: NSLayoutConstraint.Axis, spacing: CGFloat? = nil, alignment: UIStackView.Alignment? = nil, distribution: UIStackView.Distribution? = nil) { + self.init(frame: frame) self.axis = axis if let spacing { self.spacing = spacing diff --git a/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBAttributes.swift b/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBAttributes.swift index f36ea7a..db096be 100644 --- a/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBAttributes.swift +++ b/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBAttributes.swift @@ -8,7 +8,7 @@ import UIKit @MainActor -extension UIViewDSL where Self: UIView { +extension UIViewDSL { @discardableResult public func ibAttributes(@IBLayoutConstraintBuilder _ block: (Self) -> [NSLayoutConstraint]) -> Self { diff --git a/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBOutlet.swift b/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBOutlet.swift index 7e6e2f6..0ed4803 100644 --- a/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBOutlet.swift +++ b/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBOutlet.swift @@ -7,7 +7,8 @@ import UIKit -extension UIViewDSL where Self: UIView { +@MainActor +extension UIViewDSL { @discardableResult public func ibOutlet(_ outlet: inout Self?) -> Self { @@ -34,13 +35,13 @@ extension UIViewDSL where Self: UIView { } @discardableResult - public func ibOutlet(_ owner: Owner?, _ property: ReferenceWritableKeyPath) -> Self { + public func ibOutlet(_ owner: Owner?, _ property: ReferenceWritableKeyPath) -> Self { owner?[keyPath: property] = self return self } @discardableResult - public func ibOutlet(_ owner: Owner?, _ property: ReferenceWritableKeyPath) -> Self { + public func ibOutlet(_ owner: Owner?, _ property: ReferenceWritableKeyPath) -> Self { owner?[keyPath: property] = self return self } diff --git a/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBSubviews.swift b/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBSubviews.swift index fb34d4c..a0aac24 100644 --- a/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBSubviews.swift +++ b/Sources/UIViewKit/UIViewDSL/UIViewDSL+IBSubviews.swift @@ -8,7 +8,7 @@ import UIKit @MainActor -extension UIViewDSL where Self: UIView { +extension UIViewDSL { @discardableResult public func ibSubviews(@IBSubviewsBuilder _ content: () -> [UIView]) -> Self { diff --git a/Sources/UIViewKit/UIViewDSL/UIViewDSL+ResultBuilders.swift b/Sources/UIViewKit/UIViewDSL/UIViewDSL+ResultBuilders.swift index 90083ac..17f3301 100644 --- a/Sources/UIViewKit/UIViewDSL/UIViewDSL+ResultBuilders.swift +++ b/Sources/UIViewKit/UIViewDSL/UIViewDSL+ResultBuilders.swift @@ -37,6 +37,10 @@ public enum IBSubviewsBuilder { public static func buildEither(second component: [UIView]) -> [UIView] { component } + + public static func buildArray(_ components: [[UIView]]) -> [UIView] { + components.flatMap { $0 } + } } @resultBuilder diff --git a/Sources/UIViewKit/UIViewDSL/UIViewDSL.h b/Sources/UIViewKit/UIViewDSL/UIViewDSL.h deleted file mode 100644 index c843bb4..0000000 --- a/Sources/UIViewKit/UIViewDSL/UIViewDSL.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// UIViewDSL.h -// UIViewDSL -// -// Created by MaxAir on 12/02/2024. -// - -#import - -//! Project version number for UIViewDSL. -FOUNDATION_EXPORT double UIViewDSLVersionNumber; - -//! Project version string for UIViewDSL. -FOUNDATION_EXPORT const unsigned char UIViewDSLVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/Sources/UIViewKit/UIViewDSL/UIViewDSL.swift b/Sources/UIViewKit/UIViewDSL/UIViewDSL.swift index 4e7621e..826a18f 100644 --- a/Sources/UIViewKit/UIViewDSL/UIViewDSL.swift +++ b/Sources/UIViewKit/UIViewDSL/UIViewDSL.swift @@ -7,6 +7,7 @@ import UIKit -public protocol UIViewDSL { } +@MainActor +public protocol UIViewDSL where Self: UIView { } extension UIView: UIViewDSL { } diff --git a/Sources/UIViewKit/UIViewKit.swift b/Sources/UIViewKit/UIViewKit.swift new file mode 100644 index 0000000..a38c719 --- /dev/null +++ b/Sources/UIViewKit/UIViewKit.swift @@ -0,0 +1,12 @@ +// +// UIViewKit.swift +// UIViewKit +// +// Created by Blazej SLEBODA on 03/09/2025. +// + +/// Re-exports UIKit and SwiftUI so consumers can `import UIViewKit` only. +/// Note: These transitive imports are part of the public API surface. + +@_exported import UIKit +@_exported import SwiftUI diff --git a/Sources/UIViewKit/Views/IBContainerView.swift b/Sources/UIViewKit/Views/IBContainerView.swift new file mode 100644 index 0000000..d534311 --- /dev/null +++ b/Sources/UIViewKit/Views/IBContainerView.swift @@ -0,0 +1,58 @@ +// +// ContainerView.swift +// UIViewKit +// +// Created by Blazej SLEBODA on 02/09/2025. +// + +import UIKit + +public class IBContainerView: UIView { + + private var controllerCreator: (() -> UIViewController)? + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + public init(controller: UIViewController? = nil) { + super.init(frame: .zero) + if let controller = controller { + self.controllerCreator = { controller } + } + } + + public override func didMoveToWindow() { + super.didMoveToWindow() + guard let controllerCreator else { return } + self.controllerCreator = nil + embed(controllerCreator()) + } + + public func ibEmbed(_ viewControllerToEmbed: UIViewController) { + self.controllerCreator = { viewControllerToEmbed } + } + + public func ibEmbed(maker viewControllerToEmbed: @escaping () -> UIViewController) { + self.controllerCreator = viewControllerToEmbed + } + + private func embed(_ viewControllerToEmbed: UIViewController) { + guard let parent = nearestViewController() else { return } + parent.ibEmbed(viewControllerToEmbed, self) + } +} + +private extension UIView { + /// Returns the first UIViewController up the responder chain, if any + func nearestViewController() -> UIViewController? { + var responder: UIResponder? = self + while let r = responder { + if let vc = r as? UIViewController { + return vc + } + responder = r.next + } + return nil + } +} diff --git a/Sources/UIViewKit/Views/VerticalStack.swift b/Sources/UIViewKit/Views/IBHorizontalStack.swift similarity index 61% rename from Sources/UIViewKit/Views/VerticalStack.swift rename to Sources/UIViewKit/Views/IBHorizontalStack.swift index 2366adb..5ca7c15 100644 --- a/Sources/UIViewKit/Views/VerticalStack.swift +++ b/Sources/UIViewKit/Views/IBHorizontalStack.swift @@ -1,5 +1,5 @@ // -// VerticalStack.swift +// IBHorizontalStack.swift // UIViewKit // // Created by Blazej SLEBODA on 11/02/2024. @@ -7,9 +7,9 @@ import UIKit -public func VerticalStack(spacing: CGFloat? = nil, alignment: UIStackView.Alignment? = nil, distribution: UIStackView.Distribution? = nil) -> UIStackView { +public func IBHorizontalStack(spacing: CGFloat? = nil, alignment: UIStackView.Alignment? = nil, distribution: UIStackView.Distribution? = nil) -> UIStackView { let stackView = UIStackView() - stackView.axis = .vertical + stackView.axis = .horizontal if let alignment { stackView.alignment = alignment } diff --git a/Sources/UIViewKit/Views/HorizontalStack.swift b/Sources/UIViewKit/Views/IBVerticalStack.swift similarity index 78% rename from Sources/UIViewKit/Views/HorizontalStack.swift rename to Sources/UIViewKit/Views/IBVerticalStack.swift index 905c279..871a8b5 100644 --- a/Sources/UIViewKit/Views/HorizontalStack.swift +++ b/Sources/UIViewKit/Views/IBVerticalStack.swift @@ -1,5 +1,5 @@ // -// HorizontalStack.swift +// IBVerticalStack.swift // UIViewKit // // Created by Blazej SLEBODA on 11/02/2024. @@ -7,9 +7,9 @@ import UIKit -public func HorizontalStack(spacing: CGFloat? = nil, alignment: UIStackView.Alignment? = nil, distribution: UIStackView.Distribution? = nil) -> UIStackView { +public func IBVerticalStack(spacing: CGFloat? = nil, alignment: UIStackView.Alignment? = nil, distribution: UIStackView.Distribution? = nil) -> UIStackView { let stackView = UIStackView() - stackView.axis = .horizontal + stackView.axis = .vertical if let alignment { stackView.alignment = alignment } diff --git a/Tests/UIViewKitTests/FoundationExtensionsTests.swift b/Tests/UIViewKitTests/FoundationExtensionsTests.swift index 4e55e26..af7b038 100644 --- a/Tests/UIViewKitTests/FoundationExtensionsTests.swift +++ b/Tests/UIViewKitTests/FoundationExtensionsTests.swift @@ -2,7 +2,7 @@ // FoundationExtensionsTests.swift // UIViewKit // -// Created by blz on 22/08/2025. +// Created by Blazej SLEBODA on 22/08/2025. // import Testing diff --git a/Tests/UIViewKitTests/IBSubviewsTests.swift b/Tests/UIViewKitTests/IBSubviewsTests.swift index c198d0e..6883a00 100644 --- a/Tests/UIViewKitTests/IBSubviewsTests.swift +++ b/Tests/UIViewKitTests/IBSubviewsTests.swift @@ -73,4 +73,13 @@ class IBSubviewsTests: XCTestCase { subviews.filter { _ = $0; return true } } } + + func testForLoop() throws { + let rootView = UIView().ibSubviews { + for _ in (0...2) { + UIView() + } + } + XCTAssertEqual(rootView.subviews.count, 3) + } }