Skip to content

Commit

Permalink
Extensions from new project template (#133)
Browse files Browse the repository at this point in the history
* ✨ Add frontmostController extensions

* ✨ Add UIView+Spacer

* ✨ Add Combine+Concurrency
  • Loading branch information
olejnjak authored Nov 3, 2023
1 parent 4f300a2 commit f7d0999
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 5 deletions.
61 changes: 61 additions & 0 deletions ACKategories-iOS/UIView+Spacer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import UIKit

// not sure this doesn't crash on iOS 11, so make it unavailable as it cannot be really tested
@available(iOS 12, *)
public extension UIView {
private final class Spacer: UIView {
fileprivate var observation: NSKeyValueObservation?

init(
size: CGFloat,
axis: NSLayoutConstraint.Axis,
priority: Float
) {
super.init(frame: .init(
origin: .zero,
size: .init(
width: axis == .horizontal ? size : 0,
height: axis == .vertical ? size : 0
)
))

translatesAutoresizingMaskIntoConstraints = false

switch axis {
case .horizontal:
let constraint = widthAnchor.constraint(equalToConstant: size)
constraint.priority = .init(priority)
constraint.isActive = true
case .vertical:
let constraint = heightAnchor.constraint(equalToConstant: size)
constraint.priority = .init(priority)
constraint.isActive = true
default: assertionFailure("Unknown axis \(axis)")
}
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

private func createSpacer(_ size: CGFloat, axis: NSLayoutConstraint.Axis, priority: Float) -> UIView {
let spacer = Spacer(size: size, axis: axis, priority: priority)
spacer.isHidden = isHidden
spacer.observation = observe(\.isHidden) { [weak spacer] sender, _ in
spacer?.isHidden = sender.isHidden
}
return spacer
}

/// Create vertical spacer whose `isHidden` is tied to `self.isHidden`
func createVSpacer(_ height: CGFloat, priority: Float = 999) -> UIView {
createSpacer(height, axis: .vertical, priority: priority)
}

/// Create horizontal spacer whose `isHidden` is tied to `self.isHidden`
func createHSpacer(_ width: CGFloat, priority: Float = 999) -> UIView {
createSpacer(width, axis: .horizontal, priority: priority)
}
}

27 changes: 27 additions & 0 deletions ACKategories-iOS/UIViewController+FrontMost.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import UIKit

/// Extend you container controllers with this protocol, to make sure `frontmostChild` and `frontmostController` properties
/// can work correctly
public protocol FrontmostContainerViewController {
/// Goes through view controller hierarchy and returns view controller on top
var frontmostChild: UIViewController? { get }
}

extension UISplitViewController: FrontmostContainerViewController {
public var frontmostChild: UIViewController? { viewControllers.last }
}

extension UINavigationController: FrontmostContainerViewController {
public var frontmostChild: UIViewController? { topViewController }
}

extension UITabBarController: FrontmostContainerViewController {
public var frontmostChild: UIViewController? { selectedViewController }
}

public extension UIViewController {
/// Returns frontmost controller that can be used e.g. for modal presentations
var frontmostController: UIViewController {
presentedViewController?.frontmostController ?? (self as? FrontmostContainerViewController)?.frontmostChild?.frontmostController ?? self
}
}
55 changes: 55 additions & 0 deletions ACKategories-iOSTests/UIViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,59 @@ final class UIViewTests: XCTestCase {
XCTAssertEqual(view.contentCompressionResistancePriority(for: .horizontal), .required)
XCTAssertEqual(view.contentCompressionResistancePriority(for: .vertical), .required)
}

func test_spacer_initialIsHidden() {
let view = UIView()
view.isHidden = false
let spacer = view.createVSpacer(10)

XCTAssertEqual(view.isHidden, spacer.isHidden)

let view2 = UIView()
view2.isHidden = true
let spacer2 = view2.createVSpacer(10)

XCTAssertEqual(view2.isHidden, spacer2.isHidden)
}

func test_spacer_isHiddenObservation() {
let view = UIView()
view.isHidden = false
let spacer = view.createVSpacer(10)

XCTAssertEqual(view.isHidden, spacer.isHidden)

view.isHidden = true
XCTAssertEqual(view.isHidden, spacer.isHidden)
}

func test_spacer_isDeinited() throws {
let parent = UIView()

weak var weakView: UIView?
weak var weakSpacer: UIView?

try autoreleasepool {
var view: UIView? = UIView()
var spacer: UIView? = try XCTUnwrap(view).createVSpacer(10)

try parent.addSubview(XCTUnwrap(view))
try parent.addSubview(XCTUnwrap(spacer))

weakView = view
weakSpacer = spacer

view = nil
spacer = nil

XCTAssertNotNil(weakView)
XCTAssertNotNil(weakSpacer)

weakView?.removeFromSuperview()
weakSpacer?.removeFromSuperview()
}

XCTAssertNil(weakView)
XCTAssertNil(weakSpacer)
}
}
24 changes: 19 additions & 5 deletions ACKategories.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,16 @@
695096EF23C790A900E8F457 /* ACKategories.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69E81A0723C773370054687B /* ACKategories.framework */; };
695096F023C790A900E8F457 /* ACKategories.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 69E81A0723C773370054687B /* ACKategories.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
696DF854245304F400A6AC69 /* ReusableViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696DF853245304F400A6AC69 /* ReusableViewTests.swift */; };
6984CE132A5C218A001EE958 /* UIViewController+FrontMost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6984CE122A5C218A001EE958 /* UIViewController+FrontMost.swift */; };
6984CE152A5C26AA001EE958 /* UIView+Spacer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6984CE142A5C26AA001EE958 /* UIView+Spacer.swift */; };
69DB1A012831839F004B32D7 /* PublisherExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3478AA27E37DC1004548B3 /* PublisherExtensions.swift */; };
69E819F423C773240054687B /* ACKategoriesCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69E819EA23C773240054687B /* ACKategoriesCore.framework */; };
69E819FB23C773240054687B /* ACKategoriesCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 69E819ED23C773240054687B /* ACKategoriesCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
69E81A1023C773370054687B /* ACKategories.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69E81A0723C773370054687B /* ACKategories.framework */; platformFilter = ios; };
69E81A1723C773370054687B /* ACKategories_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 69E81A0923C773370054687B /* ACKategories_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
69F7058E273ADBA3004DD190 /* IntExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F7058D273ADBA3004DD190 /* IntExtensions.swift */; };
69F83E962A6179B200E9C8EA /* Combine+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F83E952A6179B200E9C8EA /* Combine+Concurrency.swift */; };
69F83E972A617A2500E9C8EA /* Combine+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F83E952A6179B200E9C8EA /* Combine+Concurrency.swift */; };
69FA5FAD23C868A900B44BCD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69FA5F9023C868A900B44BCD /* Assets.xcassets */; };
69FA5FAE23C868A900B44BCD /* UIControlBlocksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FA5F9323C868A900B44BCD /* UIControlBlocksViewController.swift */; };
69FA5FAF23C868A900B44BCD /* TitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FA5F9523C868A900B44BCD /* TitleViewController.swift */; };
Expand Down Expand Up @@ -227,6 +231,8 @@
696DF853245304F400A6AC69 /* ReusableViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableViewTests.swift; sourceTree = "<group>"; };
697B023227DB65B50082F4AC /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
697CECF023C877B20019FE61 /* Aliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aliases.swift; sourceTree = "<group>"; };
6984CE122A5C218A001EE958 /* UIViewController+FrontMost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+FrontMost.swift"; sourceTree = "<group>"; };
6984CE142A5C26AA001EE958 /* UIView+Spacer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Spacer.swift"; sourceTree = "<group>"; };
69E819EA23C773240054687B /* ACKategoriesCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ACKategoriesCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
69E819ED23C773240054687B /* ACKategoriesCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ACKategoriesCore.h; sourceTree = "<group>"; };
69E819EE23C773240054687B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -238,6 +244,7 @@
69E81A0F23C773370054687B /* ACKategories-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ACKategories-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
69E81A1623C773370054687B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
69F7058D273ADBA3004DD190 /* IntExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtensions.swift; sourceTree = "<group>"; };
69F83E952A6179B200E9C8EA /* Combine+Concurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Combine+Concurrency.swift"; sourceTree = "<group>"; };
69FA5F9023C868A900B44BCD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
69FA5F9323C868A900B44BCD /* UIControlBlocksViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIControlBlocksViewController.swift; sourceTree = "<group>"; };
69FA5F9523C868A900B44BCD /* TitleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -416,6 +423,7 @@
6950965523C7754D00E8F457 /* Reusable.swift */,
6950965823C78AB900E8F457 /* StringExtensions.swift */,
6950967923C78AD000E8F457 /* UserDefaultsExtensions.swift */,
69F83E952A6179B200E9C8EA /* Combine+Concurrency.swift */,
6950963423C7749500E8F457 /* Supporting files */,
);
path = ACKategoriesCore;
Expand All @@ -442,7 +450,6 @@
69E81A0823C773370054687B /* ACKategories-iOS */ = {
isa = PBXGroup;
children = (
6950963023C7747C00E8F457 /* Base */,
697CECF023C877B20019FE61 /* Aliases.swift */,
A38883E2257E2D2D00B958DD /* ErrorHandlers.swift */,
6950964823C7751600E8F457 /* GradientView.swift */,
Expand All @@ -463,9 +470,12 @@
6950965D23C78AC800E8F457 /* UISearchBarExtensions.swift */,
6950966823C78AC900E8F457 /* UIStackViewExtensions.swift */,
6950966523C78AC800E8F457 /* UIView+SafeAreaCompat.swift */,
6984CE142A5C26AA001EE958 /* UIView+Spacer.swift */,
6950966923C78AC900E8F457 /* UIViewController+Children.swift */,
6984CE122A5C218A001EE958 /* UIViewController+FrontMost.swift */,
6950966723C78AC800E8F457 /* UIViewController+SafeAreaCompat.swift */,
6950966323C78AC800E8F457 /* UIViewExtensions.swift */,
6950963023C7747C00E8F457 /* Base */,
6950963523C7749F00E8F457 /* Supporting files */,
);
path = "ACKategories-iOS";
Expand All @@ -474,16 +484,16 @@
69E81A1323C773370054687B /* ACKategories-iOSTests */ = {
isa = PBXGroup;
children = (
A3BA6859256BEC6A006DB42F /* Extensions */,
A3BA6858256BEC56006DB42F /* FlowCoordinator */,
69E81A1623C773370054687B /* Info.plist */,
6950968D23C78CC200E8F457 /* ColorTests.swift */,
6950969423C78CC200E8F457 /* ControlBlocksTests.swift */,
69E81A1623C773370054687B /* Info.plist */,
696DF853245304F400A6AC69 /* ReusableViewTests.swift */,
6937271F257FB0A3007CE25C /* UINavigationControllerTests.swift */,
6950969123C78CC200E8F457 /* UIStackViewTests.swift */,
6950969023C78CC200E8F457 /* UIViewControllerChildrenTests.swift */,
6950969323C78CC200E8F457 /* UIViewTests.swift */,
6937271F257FB0A3007CE25C /* UINavigationControllerTests.swift */,
A3BA6859256BEC6A006DB42F /* Extensions */,
A3BA6858256BEC56006DB42F /* FlowCoordinator */,
);
path = "ACKategories-iOSTests";
sourceTree = "<group>";
Expand Down Expand Up @@ -904,6 +914,7 @@
F885BD9A245AC4240071073D /* Int+Random.swift in Sources */,
693D773229A4BD1200B1AF0C /* BetterURL.swift in Sources */,
6950965023C7753500E8F457 /* NumberFormatterExtensions.swift in Sources */,
69F83E962A6179B200E9C8EA /* Combine+Concurrency.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -947,6 +958,7 @@
F8B81695246C59F7005D1D74 /* Randomizable.swift in Sources */,
6950963D23C774DB00E8F457 /* DictionaryExtensions.swift in Sources */,
693D773329A4BD1200B1AF0C /* BetterURL.swift in Sources */,
6984CE152A5C26AA001EE958 /* UIView+Spacer.swift in Sources */,
6950967423C78AC900E8F457 /* UIView+SafeAreaCompat.swift in Sources */,
6950963323C7748D00E8F457 /* ArrayExtensions.swift in Sources */,
6950962E23C7747800E8F457 /* FlowCoordinator.swift in Sources */,
Expand All @@ -960,11 +972,13 @@
F8B81694246C59F7005D1D74 /* Date+Random.swift in Sources */,
6950966C23C78AC900E8F457 /* UISearchBarExtensions.swift in Sources */,
6950966B23C78AC900E8F457 /* UIDeviceExtensions.swift in Sources */,
6984CE132A5C218A001EE958 /* UIViewController+FrontMost.swift in Sources */,
6950966F23C78AC900E8F457 /* UIApplicationExtensions.swift in Sources */,
F8B81696246C59F7005D1D74 /* String+Random.swift in Sources */,
6950966D23C78AC900E8F457 /* UIColorExtensions.swift in Sources */,
6950967023C78AC900E8F457 /* UIControlEvents.swift in Sources */,
6950965723C7754D00E8F457 /* Reusable.swift in Sources */,
69F83E972A617A2500E9C8EA /* Combine+Concurrency.swift in Sources */,
6950962A23C7740B00E8F457 /* Logger.swift in Sources */,
6950967823C78AC900E8F457 /* UIViewController+Children.swift in Sources */,
6950967223C78AC900E8F457 /* UIViewExtensions.swift in Sources */,
Expand Down
24 changes: 24 additions & 0 deletions ACKategoriesCore/Combine+Concurrency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Combine

@available(macOS 10.15, iOS 13.0, *)
public extension Future where Failure == Error {
convenience init(operation: @escaping () async throws -> Output) {
self.init { promise in
Task {
do {
try await promise(.success(operation()))
} catch {
promise(.failure(error))
}
}
}
}
}

@available(macOS 10.15, iOS 13.0, *)
public extension AnyPublisher where Failure == Error {
init(operation: @escaping () async throws -> Output) {
self = Future { try await operation() }.eraseToAnyPublisher()
}
}

0 comments on commit f7d0999

Please sign in to comment.