diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d326b26..36c7387 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,18 +5,16 @@ on:
jobs:
test:
- runs-on: macos-latest
+ runs-on: macos-26
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v6
+ - name: Install SwiftLint
+ run: |
+ brew install swiftlint
- name: Lint
run: |
swiftlint --strict
- - name: Pod lib lint
- run: |
- gem install bundler
- bundle install --jobs 4 --retry 3
- bundle exec pod lib lint
- name: Test
run: |
- xcodebuild -scheme FuntastyKit-Package -destination 'platform=iOS Simulator,name=iPhone 8' -sdk iphonesimulator test
+ xcodebuild -scheme FuntastyKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -sdk iphonesimulator test
diff --git a/.gitignore b/.gitignore
index 3bf50c9..95c4320 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,71 +1,5 @@
-# Xcode
-#
-# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-
-## Build generated
-build/
-DerivedData/
-
-## Various settings
-*.pbxuser
-!default.pbxuser
-*.mode1v3
-!default.mode1v3
-*.mode2v3
-!default.mode2v3
-*.perspectivev3
-!default.perspectivev3
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
xcuserdata/
-
-## Other
-*.moved-aside
-*.xcuserstate
-
-## Obj-C/Swift specific
-*.hmap
-*.ipa
-*.dSYM.zip
-*.dSYM
-
-## Playgrounds
-timeline.xctimeline
-playground.xcworkspace
-
-# Swift Package Manager
-#
-# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
-# Packages/
-.build/
-.swiftpm
-Package.resolved
-
-# CocoaPods
-#
-# We recommend against adding the Pods directory to your .gitignore. However
-# you should judge for yourself, the pros and cons are mentioned at:
-# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
-#
-# Pods/
-
-# Carthage
-#
-# Add this line if you want to avoid checking in source code from Carthage dependencies.
-# Carthage/Checkouts
-
-Carthage/Build
-
-# fastlane
-#
-# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
-# screenshots whenever they are needed.
-# For more information about the recommended setup visit:
-# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
-
-fastlane/report.xml
-fastlane/Preview.html
-fastlane/screenshots
-fastlane/test_output
-fastlane/README.md
-
-# Project, because it can be generated using SPM
-*.xcodeproj
diff --git a/.swiftlint.yml b/.swiftlint.yml
index 5703ab3..6e73a8d 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -1,17 +1,18 @@
disabled_rules:
- line_length
excluded:
- - Pods
- - vendor
+ - .build
+ - .claude
+analyzer_rules:
+ - unused_declaration
+ - unused_import
opt_in_rules:
- - anyobject_protocol
- array_init
- attributes
- closure_body_length
- closure_end_indentation
- closure_spacing
- collection_alignment
- - conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
@@ -22,7 +23,6 @@ opt_in_rules:
- empty_collection_literal
- empty_count
- empty_string
- - empty_xctest_method
- enum_case_associated_values_count
- explicit_init
- fallthrough
@@ -34,24 +34,29 @@ opt_in_rules:
- function_default_parameter_at_end
- identical_operands
- implicit_return
+ - implicitly_unwrapped_optional
- joined_default_parameter
- last_where
- legacy_multiple
- legacy_random
+ - let_var_whitespace
- literal_expression_end_indentation
- lower_acl_than_parent
- modifier_order
+ - multiline_arguments
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- nimble_operator
+ - no_extension_access_modifier
- number_separator
- object_literal
- operator_usage_whitespace
- optional_enum_case_matching
- overridden_super_call
- override_in_extension
+ - pattern_matching_keywords
- prefer_self_type_over_type_of_self
- private_action
- private_outlet
@@ -69,8 +74,6 @@ opt_in_rules:
- trailing_closure
- unneeded_parentheses_in_closure_argument
- untyped_error_in_catch
- - unused_declaration
- - unused_import
- vertical_parameter_alignment_on_call
- vertical_whitespace_closing_braces
- yoda_condition
@@ -82,6 +85,9 @@ identifier_name:
- x
- y
- z
+ - pr
+
+type_body_length: 400
# Disable errors, allow only warnings
cyclomatic_complexity:
diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..276c170
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,58 @@
+# CLAUDE.md — FuntastyKit
+
+## Build & Test
+
+This is an iOS framework — use the Xcode toolchain (`xcrun`), not the local Swift toolchain:
+
+```bash
+# Build (requires iOS SDK via Xcode)
+xcodebuild build -scheme FuntastyKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -sdk iphonesimulator
+
+# Run tests (matches CI)
+xcodebuild test -scheme FuntastyKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -sdk iphonesimulator
+
+# Lint
+swiftlint --strict
+```
+
+Note: `swift build` / `swift test` do not work because UIKit is not available on macOS host. Always use `xcodebuild` with an iOS simulator destination.
+
+## Project Structure
+
+Single SPM target:
+
+- **FuntastyKit** (`Sources/FuntastyKit/`) — Core: Coordinator pattern, error handling, UIKit extensions
+
+Tests are in `FuntastyKitTests`.
+
+## Architecture
+
+Swift 6.2 with explicit `@MainActor` annotations on all public protocols and UIKit-facing types. No `defaultIsolation` — all isolation is spelled out in source. Extensions on `@MainActor` protocols and UIKit classes inherit isolation automatically; only standalone types/extensions that create UIKit objects need their own `@MainActor`.
+
+Tests use Swift Testing (`import Testing`, `@Suite`, `@Test`). Types in the test target that conform to library protocols need explicit `@MainActor` annotations.
+
+Coordinator protocol hierarchy for UIKit navigation:
+`Coordinator` → `DefaultCoordinator` → `ConfiguringCoordinator` → `PushCoordinator` / `ModalCoordinator` / `ShowCoordinator` / `TabBarItemCoordinator`
+
+Each coordinator type provides a default `start()` implementation for its navigation style. `CoordinatorDelegate` notifies on `willStop`/`didStop`.
+
+## SwiftLint
+
+Strict mode enabled. Key style rules to follow:
+- `no_extension_access_modifier`: Use `extension Foo { public func ... }`, not `public extension Foo { func ... }`
+- `let_var_whitespace`: Blank line before variable declarations
+- `pattern_matching_keywords`: Move `let`/`var` outside tuples in pattern matching
+- `implicitly_unwrapped_optional`: Avoid `!` optionals
+- `multiline_arguments`: Multi-line function calls should have each argument on its own line
+
+Key thresholds:
+- Cyclomatic complexity: warning at 10
+- Function parameters: max 5
+- Large tuple: warning 3, error 4
+- Type name/body max length: 50 / 400
+- `line_length` is disabled
+- `force_cast` and `force_try` are warnings (not errors)
+
+## CI (GitHub Actions)
+
+Runs on PRs (`test.yml`): SwiftLint --strict → xcodebuild test.
diff --git a/FuntastyKit.podspec b/FuntastyKit.podspec
deleted file mode 100644
index e67f591..0000000
--- a/FuntastyKit.podspec
+++ /dev/null
@@ -1,29 +0,0 @@
-Pod::Spec.new do |s|
- s.name = "FuntastyKit"
- s.version = "2.3.0"
- s.summary = "A collection of Swift utilities and protocols used in our projects."
- s.description = <<-DESC
- MVVM-C architecture, service holder for code injection of services
- some regularly used UIKit extensions, protocols for simple initialization from XIB files,
- storyboards and for handling keyboard, hairline and keyboard height constraints
- DESC
- s.homepage = "https://github.com/futuredapp/FuntastyKit"
- s.license = { type: "MIT", file: "LICENSE" }
- s.author = { "Matěj K. Jirásek" => "matej.jirasek@futured.app" }
- s.social_media_url = "https://twitter.com/Futuredapps"
- s.platform = :ios, "9.0"
- s.swift_version = "5.0"
- s.source = { :git => "https://github.com/futuredapp/FuntastyKit.git", :tag => s.version.to_s }
- s.frameworks = ["Foundation", "UIKit"]
- s.default_subspec = "Core"
-
- s.subspec "Core" do |ss|
- ss.source_files = Dir["Sources/FuntastyKit/*"]
- end
-
- s.subspec "IBInspectable" do |ss|
- ss.dependency "FuntastyKit/Core"
- ss.source_files = Dir["Sources/FuntastyKitIBInspectable/*"]
- end
-end
-
diff --git a/Gemfile b/Gemfile
deleted file mode 100644
index abc1a45..0000000
--- a/Gemfile
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-source "https://rubygems.org"
-
-gem "cocoapods", "~> 1.10"
diff --git a/Gemfile.lock b/Gemfile.lock
deleted file mode 100644
index cdb09b9..0000000
--- a/Gemfile.lock
+++ /dev/null
@@ -1,93 +0,0 @@
-GEM
- remote: https://rubygems.org/
- specs:
- CFPropertyList (3.0.2)
- activesupport (5.2.4.4)
- concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (>= 0.7, < 2)
- minitest (~> 5.1)
- tzinfo (~> 1.1)
- addressable (2.8.0)
- public_suffix (>= 2.0.2, < 5.0)
- algoliasearch (1.27.5)
- httpclient (~> 2.8, >= 2.8.3)
- json (>= 1.5.1)
- atomos (0.1.3)
- claide (1.0.3)
- cocoapods (1.10.0)
- addressable (~> 2.6)
- claide (>= 1.0.2, < 2.0)
- cocoapods-core (= 1.10.0)
- cocoapods-deintegrate (>= 1.0.3, < 2.0)
- cocoapods-downloader (>= 1.4.0, < 2.0)
- cocoapods-plugins (>= 1.0.0, < 2.0)
- cocoapods-search (>= 1.0.0, < 2.0)
- cocoapods-trunk (>= 1.4.0, < 2.0)
- cocoapods-try (>= 1.1.0, < 2.0)
- colored2 (~> 3.1)
- escape (~> 0.0.4)
- fourflusher (>= 2.3.0, < 3.0)
- gh_inspector (~> 1.0)
- molinillo (~> 0.6.6)
- nap (~> 1.0)
- ruby-macho (~> 1.4)
- xcodeproj (>= 1.19.0, < 2.0)
- cocoapods-core (1.10.0)
- activesupport (> 5.0, < 6)
- addressable (~> 2.6)
- algoliasearch (~> 1.0)
- concurrent-ruby (~> 1.1)
- fuzzy_match (~> 2.0.4)
- nap (~> 1.0)
- netrc (~> 0.11)
- public_suffix
- typhoeus (~> 1.0)
- cocoapods-deintegrate (1.0.4)
- cocoapods-downloader (1.4.0)
- cocoapods-plugins (1.0.0)
- nap
- cocoapods-search (1.0.0)
- cocoapods-trunk (1.5.0)
- nap (>= 0.8, < 2.0)
- netrc (~> 0.11)
- cocoapods-try (1.2.0)
- colored2 (3.1.2)
- concurrent-ruby (1.1.7)
- escape (0.0.4)
- ethon (0.12.0)
- ffi (>= 1.3.0)
- ffi (1.13.1)
- fourflusher (2.3.1)
- fuzzy_match (2.0.4)
- gh_inspector (1.1.3)
- httpclient (2.8.3)
- i18n (1.8.5)
- concurrent-ruby (~> 1.0)
- json (2.3.1)
- minitest (5.14.2)
- molinillo (0.6.6)
- nanaimo (0.3.0)
- nap (1.1.0)
- netrc (0.11.0)
- public_suffix (4.0.6)
- ruby-macho (1.4.0)
- thread_safe (0.3.6)
- typhoeus (1.4.0)
- ethon (>= 0.9.0)
- tzinfo (1.2.7)
- thread_safe (~> 0.1)
- xcodeproj (1.19.0)
- CFPropertyList (>= 2.3.3, < 4.0)
- atomos (~> 0.1.3)
- claide (>= 1.0.2, < 2.0)
- colored2 (~> 3.1)
- nanaimo (~> 0.3.0)
-
-PLATFORMS
- ruby
-
-DEPENDENCIES
- cocoapods (~> 1.10)
-
-BUNDLED WITH
- 2.1.4
diff --git a/Package.swift b/Package.swift
index 81c0f75..6ca6cca 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,26 +1,14 @@
-// swift-tools-version:5.1
+// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "FuntastyKit",
- platforms: [.iOS(.v9)],
+ platforms: [.iOS(.v16)],
products: [
- .library(
- name: "FuntastyKit",
- targets: ["FuntastyKit"]),
- .library(
- name: "FuntastyKitIBInspectable",
- targets: ["FuntastyKitIBInspectable"])
+ .library(name: "FuntastyKit", targets: ["FuntastyKit"])
],
targets: [
- .target(
- name: "FuntastyKit",
- dependencies: []),
- .target(
- name: "FuntastyKitIBInspectable",
- dependencies: []),
- .testTarget(
- name: "FuntastyKitTests",
- dependencies: ["FuntastyKit"])
+ .target(name: "FuntastyKit"),
+ .testTarget(name: "FuntastyKitTests", dependencies: ["FuntastyKit"])
]
)
diff --git a/README.md b/README.md
index 1ad5eee..3586bde 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,24 @@
-
+
# FuntastyKit
-
-
-
+
+
+> Requires **Swift 6.2** / **iOS 16+**
The FuntastyKit for iOS contains:
- MVVM-C architecture used at [Futured](https://www.futured.app/en/), the template for this architecture can be found at https://github.com/futuredapp/MVVM-C-Templates.
+- Coordinator pattern protocols for UIKit navigation (Push, Modal, Show, TabBarItem).
+- Error handling with `ResolvableError` and `AlertCoordinator`.
- Some regularly used UIKit extensions.
-- Protocols for simple initialization from XIB files, storyboards and for handling keyboard.
-- Hairline constraint for one-pixel designs.
## Installation
-If you want to use CocoaPods for dependency management, add following line to your `Podfile`:
-
-```ruby
-pod 'FuntastyKit', '~> 2.3'
-```
-
-If you also want to use IBInspectable extensions also add:
-
-```ruby
-pod 'FuntastyKit', '~> 2.3', subspecs: ['IBInspectable']
-```
-
When using Swift package manager add following line to your `Package.swift` file or add the repository using Xcode:
```swift
-.package(url: "https://github.com/futuredapp/FuntastyKit.git", from: "2.3.0")
+.package(url: "https://github.com/futuredapp/FuntastyKit.git", from: "3.0.0")
```
## Note on the name
@@ -39,10 +27,6 @@ The name of the framework comes from the name of our company before rebranding,
## Contributors
-If you have any questions or issues, please contact the current maintainer:
-
-- Matěj K. Jirásek, matej.jirasek@futured.app
-
Over the years many inside and outside contributors made improvements to this library, namely:
- [Petr Zvoníček](https://github.com/zvonicek)
diff --git a/Sources/FuntastyKit/Architecture/Coordinator.swift b/Sources/FuntastyKit/Architecture/Coordinator.swift
index 1e773ae..ecc81ea 100644
--- a/Sources/FuntastyKit/Architecture/Coordinator.swift
+++ b/Sources/FuntastyKit/Architecture/Coordinator.swift
@@ -1,5 +1,6 @@
import UIKit
+@MainActor
public protocol Coordinator {
/// Triggers navigation to the corresponding controller
func start()
@@ -10,51 +11,56 @@ public protocol Coordinator {
func stop()
}
-public extension Coordinator {
- func stop() {
- }
+extension Coordinator {
+ public func stop() {}
}
+@MainActor
public protocol DefaultCoordinator: Coordinator {
associatedtype ViewController: UIViewController
- var viewController: ViewController? { get }
+ var viewController: ViewController? { get }
var animated: Bool { get }
- var delegate: CoordinatorDelegate? { get set }
+ var delegate: (any CoordinatorDelegate)? { get set }
}
+@MainActor
public protocol ConfiguringCoordinator: DefaultCoordinator {
func configure(viewController: ViewController)
}
+@MainActor
public protocol ShowCoordinator: ConfiguringCoordinator {
/// When used on Split View Controller as Detail View Controller, sourceViewController should be 'weak', otherwise memory leak will emerge
var sourceViewController: UIViewController? { get }
var isDetail: Bool { get }
}
+@MainActor
public protocol PushCoordinator: ConfiguringCoordinator {
var navigationController: UINavigationController? { get }
}
+@MainActor
public protocol ModalCoordinator: ConfiguringCoordinator {
var sourceViewController: UIViewController { get }
var destinationNavigationController: UINavigationController? { get }
}
+@MainActor
public protocol TabBarItemCoordinator: ConfiguringCoordinator {
var tabBarController: UITabBarController? { get }
var destinationNavigationController: UINavigationController? { get }
}
-public extension DefaultCoordinator {
+extension DefaultCoordinator {
// default implementation if not overridden
- var animated: Bool {
+ public var animated: Bool {
true
}
// default implementation of nil delegate, should be overridden when needed
- var delegate: CoordinatorDelegate? {
+ public var delegate: (any CoordinatorDelegate)? {
get {
nil
}
@@ -63,21 +69,20 @@ public extension DefaultCoordinator {
}
}
- func stop() {
+ public func stop() {
delegate?.willStop(in: self)
delegate?.didStop(in: self)
}
}
-public extension ShowCoordinator {
- var isDetail: Bool {
+extension ShowCoordinator {
+ public var isDetail: Bool {
false
}
- func start() {
- guard let viewController = viewController else {
- return
- }
+ public func start() {
+ guard let viewController else { return }
+
configure(viewController: viewController)
if isDetail {
sourceViewController?.showDetailViewController(viewController, sender: nil)
@@ -87,43 +92,39 @@ public extension ShowCoordinator {
}
}
-public extension PushCoordinator {
- func start() {
- guard let viewController = viewController else {
- return
- }
+extension PushCoordinator {
+ public func start() {
+ guard let viewController else { return }
configure(viewController: viewController)
navigationController?.pushViewController(viewController, animated: animated)
}
- func stop() {
+ public func stop() {
delegate?.willStop(in: self)
navigationController?.popViewController(animated: animated)
delegate?.didStop(in: self)
}
}
-public extension ModalCoordinator {
- var destinationNavigationController: UINavigationController? { nil }
+extension ModalCoordinator {
+ public var destinationNavigationController: UINavigationController? { nil }
- func start() {
- guard let viewController = viewController else {
- return
- }
+ public func start() {
+ guard let viewController else { return }
configure(viewController: viewController)
- if let destinationNavigationController = destinationNavigationController {
+ if let destinationNavigationController {
// wrapper navigation controller given, present it
- sourceViewController.present(destinationNavigationController, animated: animated, completion: nil)
+ sourceViewController.present(destinationNavigationController, animated: animated)
} else {
// no wrapper navigation controller given, present actual controller
- sourceViewController.present(viewController, animated: animated, completion: nil)
+ sourceViewController.present(viewController, animated: animated)
}
}
- func stop() {
+ public func stop() {
delegate?.willStop(in: self)
viewController?.dismiss(animated: animated) {
self.delegate?.didStop(in: self)
@@ -131,13 +132,12 @@ public extension ModalCoordinator {
}
}
-public extension TabBarItemCoordinator {
- var destinationNavigationController: UINavigationController? { nil }
+extension TabBarItemCoordinator {
+ public var destinationNavigationController: UINavigationController? { nil }
+
+ public func start() {
+ guard let viewController else { return }
- func start() {
- guard let viewController = viewController else {
- return
- }
configure(viewController: viewController)
var viewControllers = tabBarController?.viewControllers ?? []
@@ -146,11 +146,10 @@ public extension TabBarItemCoordinator {
tabBarController?.setViewControllers(viewControllers, animated: animated)
}
- func stop() {
+ public func stop() {
delegate?.willStop(in: self)
- guard let viewController = viewController, let viewControllers = tabBarController?.viewControllers else {
- return
- }
+
+ guard let viewController, let viewControllers = tabBarController?.viewControllers else { return }
var mutableViewControllers = viewControllers
if let index = mutableViewControllers.firstIndex(of: destinationNavigationController ?? viewController) {
@@ -162,7 +161,8 @@ public extension TabBarItemCoordinator {
}
}
+@MainActor
public protocol CoordinatorDelegate: AnyObject {
- func willStop(in coordinator: Coordinator)
- func didStop(in coordinator: Coordinator)
+ func willStop(in coordinator: any Coordinator)
+ func didStop(in coordinator: any Coordinator)
}
diff --git a/Sources/FuntastyKit/AutoLayout/HairlineLayoutConstraint.swift b/Sources/FuntastyKit/AutoLayout/HairlineLayoutConstraint.swift
deleted file mode 100644
index 351a5c3..0000000
--- a/Sources/FuntastyKit/AutoLayout/HairlineLayoutConstraint.swift
+++ /dev/null
@@ -1,10 +0,0 @@
-import UIKit
-
-/// Useful to ensure 1px size on retina displays
-public class HairlineLayoutConstraint: NSLayoutConstraint {
-
- override public func awakeFromNib() {
- super.awakeFromNib()
- self.constant /= UIScreen.main.scale
- }
-}
diff --git a/Sources/FuntastyKit/AutoLayout/KeyboardHeightConstraint.swift b/Sources/FuntastyKit/AutoLayout/KeyboardHeightConstraint.swift
deleted file mode 100644
index 2dee47b..0000000
--- a/Sources/FuntastyKit/AutoLayout/KeyboardHeightConstraint.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-import UIKit
-
-public final class KeyboardHeightConstraint: NSLayoutConstraint {
-
- override public func awakeFromNib() {
- super.awakeFromNib()
-
- let center: NotificationCenter = .default
- center.addObserver(self, selector: #selector(keyboardWillChange), name: UIResponder.keyboardWillHideNotification, object: nil)
- center.addObserver(self, selector: #selector(keyboardWillChange), name: UIResponder.keyboardWillShowNotification, object: nil)
- }
-
- private var superview: UIView? {
- (secondItem as? UIView)?.superview
- }
-
- @objc
- private func keyboardWillChange(_ notification: Notification) {
- guard let userInfo = notification.userInfo else {
- return
- }
-
- let insetHeight = (notification.name == UIResponder.keyboardWillHideNotification) ? 0.0 : height(for: userInfo) - inset
-
- superview?.layoutIfNeeded()
- UIView.animate(withDuration: duration(from: userInfo), delay: 0, options: options(from: userInfo), animations: {
- self.constant = insetHeight
- self.superview?.layoutIfNeeded()
- }, completion: nil)
- }
-
- private var inset: CGFloat {
- if #available(iOS 11.0, *) {
- return superview?.safeAreaInsets.bottom ?? 0.0
- }
- return 0.0
- }
-
- private func height(for userInfo: [AnyHashable: Any]) -> CGFloat {
- userInfo[UIResponder.keyboardFrameEndUserInfoKey]
- .flatMap { $0 as? NSValue }
- .map { $0.cgRectValue.height } ?? 0.0
- }
-
- private func duration(from userInfo: [AnyHashable: Any]) -> Double {
- userInfo[UIResponder.keyboardAnimationDurationUserInfoKey]
- .flatMap { $0 as? NSNumber }
- .map { $0.doubleValue } ?? 0.0
- }
-
- private func options(from userInfo: [AnyHashable: Any]) -> UIView.AnimationOptions {
- userInfo[UIResponder.keyboardAnimationCurveUserInfoKey]
- .flatMap { $0 as? NSNumber }
- .map { $0.uintValue << 16 }
- .map(UIView.AnimationOptions.init) ?? UIView.AnimationOptions()
- }
-}
diff --git a/Sources/FuntastyKit/Errors/AlertCoordinator.swift b/Sources/FuntastyKit/Errors/AlertCoordinator.swift
index d1f7df1..0e91459 100644
--- a/Sources/FuntastyKit/Errors/AlertCoordinator.swift
+++ b/Sources/FuntastyKit/Errors/AlertCoordinator.swift
@@ -3,34 +3,35 @@ import UIKit
public class AlertCoordinator: DefaultCoordinator {
public typealias ViewController = UIAlertController
- public enum Source {
+ public enum Source: Sendable {
case button(UIBarButtonItem)
case view(UIView)
}
- public enum Style {
+ public enum Style: Sendable {
case alert
case actionSheet(source: Source?)
var controllerStyle: UIAlertController.Style {
switch self {
case .alert:
- return .alert
+ .alert
case .actionSheet:
- return .actionSheet
+ .actionSheet
}
}
}
+ @MainActor
enum InputType {
- case error(Error)
+ case error(any Error)
case custom(title: String?, message: String?, actions: [ErrorAction]?)
func alertController(preferredStyle: Style = .alert) -> UIAlertController {
switch self {
case .error(let error):
return UIAlertController(error: error, preferredStyle: preferredStyle.controllerStyle)
- case .custom(let title, let message, let actions):
+ case let .custom(title, message, actions):
let alert = UIAlertController(title: title, message: message, preferredStyle: preferredStyle.controllerStyle)
(actions ?? [ErrorAction(title: NSLocalizedString("OK", comment: "OK"))])
.forEach { action in
@@ -47,14 +48,14 @@ public class AlertCoordinator: DefaultCoordinator {
let parentViewController: UIViewController
public weak var viewController: UIAlertController?
- public weak var delegate: CoordinatorDelegate?
+ public weak var delegate: (any CoordinatorDelegate)?
private var type: InputType
private var preferredStyle: Style
// MARK: - Inits
- public init(parent: UIViewController, error: Error, preferredStyle: Style = .alert) {
+ public init(parent: UIViewController, error: any Error, preferredStyle: Style = .alert) {
self.parentViewController = parent
self.type = .error(error)
self.preferredStyle = preferredStyle
@@ -76,7 +77,7 @@ public class AlertCoordinator: DefaultCoordinator {
alert.popoverPresentationController?.sourceView = view
}
}
- parentViewController.present(alert, animated: animated, completion: nil)
+ parentViewController.present(alert, animated: animated)
viewController = alert
}
@@ -88,39 +89,36 @@ public class AlertCoordinator: DefaultCoordinator {
}
}
-public extension ErrorAction {
- func alertStyle() -> UIAlertAction.Style {
- switch self.style {
+@MainActor
+extension ErrorAction {
+ public func alertStyle() -> UIAlertAction.Style {
+ switch style {
case .cancel:
- return .cancel
+ .cancel
case .destructive:
- return .destructive
+ .destructive
default:
- return .default
+ .default
}
}
- func alertAction() -> UIAlertAction {
+ public func alertAction() -> UIAlertAction {
UIAlertAction(title: self.title, style: self.alertStyle()) { _ in
self.action?()
}
}
}
-public extension DefaultCoordinator {
- func showAlert(for error: Error, preferredStyle: AlertCoordinator.Style = .alert) {
- guard let viewController = self.viewController else {
- return
- }
+extension DefaultCoordinator {
+ public func showAlert(for error: any Error, preferredStyle: AlertCoordinator.Style = .alert) {
+ guard let viewController else { return }
let alertCoordinator = AlertCoordinator(parent: viewController, error: error, preferredStyle: preferredStyle)
alertCoordinator.start()
}
- func showAlert(title: String?, message: String?, actions: [ErrorAction]? = nil, preferredStyle: AlertCoordinator.Style = .alert) {
- guard let viewController = self.viewController else {
- return
- }
+ public func showAlert(title: String?, message: String?, actions: [ErrorAction]? = nil, preferredStyle: AlertCoordinator.Style = .alert) {
+ guard let viewController else { return }
let alertCoordinator = AlertCoordinator(parent: viewController, title: title, message: message, actions: actions, preferredStyle: preferredStyle)
alertCoordinator.start()
diff --git a/Sources/FuntastyKit/Errors/Errors.swift b/Sources/FuntastyKit/Errors/Errors.swift
index f97734c..b43c9d3 100644
--- a/Sources/FuntastyKit/Errors/Errors.swift
+++ b/Sources/FuntastyKit/Errors/Errors.swift
@@ -4,19 +4,19 @@ public protocol ResolvableError: LocalizedError {
var actions: [ErrorAction] { get }
}
-public struct ErrorAction {
- public enum Style: Int {
+public struct ErrorAction: Sendable {
+ public enum Style: Int, Sendable {
case `default`
case cancel
case destructive
case preferred
}
- public typealias ErrorHandler = () -> Void
+ public typealias ErrorHandler = @MainActor @Sendable () -> Void
- let title: String
- var action: ErrorHandler?
- var style: Style
+ public let title: String
+ public var action: ErrorHandler?
+ public var style: Style
public init(title: String, style: Style = .default, action: ErrorHandler? = nil) {
self.title = title
diff --git a/Sources/FuntastyKit/Errors/UIAlertController+Error.swift b/Sources/FuntastyKit/Errors/UIAlertController+Error.swift
index 588233b..77a929d 100644
--- a/Sources/FuntastyKit/Errors/UIAlertController+Error.swift
+++ b/Sources/FuntastyKit/Errors/UIAlertController+Error.swift
@@ -1,11 +1,13 @@
import UIKit
-public extension UIAlertController {
- convenience init(error: Error, preferredStyle: UIAlertController.Style = .alert) {
- self.init(title: UIAlertController.alertTitle(error: error),
- message: UIAlertController.alertMessage(error: error),
- preferredStyle: preferredStyle)
- if let error = error as? ResolvableError {
+extension UIAlertController {
+ public convenience init(error: any Error, preferredStyle: UIAlertController.Style = .alert) {
+ self.init(
+ title: UIAlertController.alertTitle(error: error),
+ message: UIAlertController.alertMessage(error: error),
+ preferredStyle: preferredStyle
+ )
+ if let error = error as? any ResolvableError {
error.actions.map { $0.alertAction() }.forEach(self.addAction)
if !error.actions.isEmpty {
return
@@ -14,21 +16,21 @@ public extension UIAlertController {
self.addAction(UIAlertAction(title: UIAlertController.okButtonText, style: .default))
}
- private static func alertTitle(error: Error) -> String {
+ private static func alertTitle(error: any Error) -> String {
switch error {
- case let error as LocalizedError:
- return error.errorDescription ?? defaultErrorTitle
+ case let error as any LocalizedError:
+ error.errorDescription ?? defaultErrorTitle
default:
- return defaultErrorTitle
+ defaultErrorTitle
}
}
- private static func alertMessage(error: Error) -> String {
+ private static func alertMessage(error: any Error) -> String {
switch error {
- case let error as LocalizedError:
- return error.failureReason ?? error.localizedDescription
+ case let error as any LocalizedError:
+ error.failureReason ?? error.localizedDescription
default:
- return error.localizedDescription
+ error.localizedDescription
}
}
diff --git a/Sources/FuntastyKit/Extensions/UITableView+Extensions.swift b/Sources/FuntastyKit/Extensions/UITableView+Extensions.swift
index f113465..923aafd 100644
--- a/Sources/FuntastyKit/Extensions/UITableView+Extensions.swift
+++ b/Sources/FuntastyKit/Extensions/UITableView+Extensions.swift
@@ -1,18 +1,22 @@
-import Foundation
import UIKit
-public extension UITableView {
+extension UIView {
+ public static var nibName: String {
+ String(describing: self)
+ }
+}
- func registerNib(for cellClass: T.Type) {
+extension UITableView {
+ public func registerNib(for cellClass: T.Type) {
let nib = UINib(nibName: cellClass.nibName, bundle: nil)
register(nib, forCellReuseIdentifier: cellClass.nibName)
}
- func registerCellClass(_ type: T.Type) {
+ public func registerCellClass(_ type: T.Type) {
register(type, forCellReuseIdentifier: type.nibName)
}
- func dequeueReusableCell(withType type: T.Type) -> T {
+ public func dequeueReusableCell(withType type: T.Type) -> T {
if let cell = dequeueReusableCell(withIdentifier: type.nibName) as? T {
return cell
} else {
diff --git a/Sources/FuntastyKit/Extensions/UIViewController+Deselection.swift b/Sources/FuntastyKit/Extensions/UIViewController+Deselection.swift
index 65263fa..f14930b 100644
--- a/Sources/FuntastyKit/Extensions/UIViewController+Deselection.swift
+++ b/Sources/FuntastyKit/Extensions/UIViewController+Deselection.swift
@@ -1,7 +1,9 @@
import UIKit
+@MainActor
public protocol Deselectable {
var indexPathsForSelectedItems: [IndexPath]? { get }
+
func selectItem(at indexPath: IndexPath?, animated: Bool)
func deselectItem(at indexPath: IndexPath, animated: Bool)
}
@@ -29,7 +31,7 @@ extension UICollectionView: Deselectable {
}
}
-public extension UIViewController {
+extension UIViewController {
/// Smoothly deselect selected rows in a table view during an animated
/// transition, and intelligently reselect those rows if the interactive
@@ -38,7 +40,7 @@ public extension UIViewController {
///
/// - parameter deselectable: The (de)selectable view in which to perform deselection/reselection.
@nonobjc
- func smoothlyDeselectItems(on deselectable: Deselectable?) {
+ public func smoothlyDeselectItems(on deselectable: (any Deselectable)?) {
let selectedIndexPaths = deselectable?.indexPathsForSelectedItems ?? []
if let coordinator = transitionCoordinator {
@@ -78,17 +80,19 @@ public extension UIViewController {
}
private func switchSelectedItemsState(
- on deselectable: Deselectable?,
+ on deselectable: (any Deselectable)?,
selectedIndexPaths: [IndexPath],
shouldBeSelected: Bool,
animated: Bool
) {
- guard let deselectable = deselectable else {
- return
- }
+ guard let deselectable else { return }
+
selectedIndexPaths.forEach {
- let switchSelectionFunc = shouldBeSelected ? deselectable.selectItem : deselectable.deselectItem
- switchSelectionFunc($0, animated)
+ if shouldBeSelected {
+ deselectable.selectItem(at: $0, animated: animated)
+ } else {
+ deselectable.deselectItem(at: $0, animated: animated)
+ }
}
}
}
diff --git a/Sources/FuntastyKit/Extensions/UIWindow+RootViewController.swift b/Sources/FuntastyKit/Extensions/UIWindow+RootViewController.swift
index f108a31..5f7e4d3 100644
--- a/Sources/FuntastyKit/Extensions/UIWindow+RootViewController.swift
+++ b/Sources/FuntastyKit/Extensions/UIWindow+RootViewController.swift
@@ -3,7 +3,7 @@ import UIKit
/**
* UIWindow extension for setting the rootViewController on a UIWindow instance in a safe and animatable way.
*/
-public extension UIWindow {
+extension UIWindow {
/**
Set the rootViewController on this UIWindow instance.
@@ -13,7 +13,13 @@ public extension UIWindow {
- parameter completion: Completion block to be invoked after the transition finishes
*/
@nonobjc
- func setRootViewController(_ viewController: UIViewController, animated: Bool, duration: TimeInterval = 0.3, options: UIView.AnimationOptions = .transitionCrossDissolve, completion: ((Bool) -> Void)? = nil) {
+ public func setRootViewController(
+ _ viewController: UIViewController,
+ animated: Bool,
+ duration: TimeInterval = 0.3,
+ options: UIView.AnimationOptions = .transitionCrossDissolve,
+ completion: ((Bool) -> Void)? = nil
+ ) {
rootViewController = viewController
if animated {
UIView.transition(with: self, duration: duration, options: options, animations: nil, completion: completion)
diff --git a/Sources/FuntastyKit/Protocols/Nibable.swift b/Sources/FuntastyKit/Protocols/Nibable.swift
deleted file mode 100644
index fe71c3d..0000000
--- a/Sources/FuntastyKit/Protocols/Nibable.swift
+++ /dev/null
@@ -1,34 +0,0 @@
-//
-// Nibable.swift
-// FuntastyKit
-//
-// Created by Matěj Jirásek on 07/10/2016.
-// Copyright © 2016 FUNTASTY Digital s.r.o. All rights reserved.
-//
-
-import UIKit
-
-public extension UIView {
-
- static var nibName: String {
- String(describing: self)
- }
-}
-
-public protocol Nibable {
- static var nib: UINib { get }
-}
-
-public extension Nibable where Self: UIView {
-
- static var nib: UINib {
- UINib(nibName: Self.nibName, bundle: nil)
- }
-
- init(owner: AnyObject? = nil) {
- let views = Self.nib.instantiate(withOwner: owner, options: [:])
- self = views.lazy.compactMap { $0 as? Self }.first ?? { () -> Self in
- fatalError("Nib for class \(Self.nibName) could not be loaded!")
- }()
- }
-}
diff --git a/Sources/FuntastyKit/Protocols/StoryboardType.swift b/Sources/FuntastyKit/Protocols/StoryboardType.swift
deleted file mode 100644
index dcb754d..0000000
--- a/Sources/FuntastyKit/Protocols/StoryboardType.swift
+++ /dev/null
@@ -1,33 +0,0 @@
-//
-// StoryboardType.swift
-// FuntastyKit
-//
-// Created by Aleš Kocur on 21.05.16.
-// Copyright © 2016 The Funtasty. All rights reserved.
-//
-
-import Foundation
-import UIKit
-
-public protocol StoryboardType {
- static var name: String { get }
-}
-
-public struct StoryboardReference {
-
- private let id: String
- private let bundle: Bundle?
-
- public init(id: String, bundle: Bundle? = nil) {
- self.bundle = bundle
- self.id = id
- }
-
- public func instantiate() -> T {
- if let controller = UIStoryboard(name: S.name, bundle: bundle).instantiateViewController(withIdentifier: id) as? T {
- return controller
- } else {
- fatalError("Instantiated controller with \(id) has different type than expected!")
- }
- }
-}
diff --git a/Sources/FuntastyKitIBInspectable/UIView+Extensions.swift b/Sources/FuntastyKitIBInspectable/UIView+Extensions.swift
deleted file mode 100644
index d64f005..0000000
--- a/Sources/FuntastyKitIBInspectable/UIView+Extensions.swift
+++ /dev/null
@@ -1,74 +0,0 @@
-import UIKit
-
-@IBDesignable
-extension UIView {
-
- // MARK: - Corner radius
-
- @IBInspectable open var cornerRadius: CGFloat {
- get {
- layer.cornerRadius
- }
- set {
- layer.cornerRadius = newValue
- }
- }
-
- // MARK: - Shadow
-
- @IBInspectable open var shadowColor: UIColor? {
- get {
- layer.shadowColor.flatMap(UIColor.init)
- }
- set {
- layer.shadowColor = newValue?.cgColor
- }
- }
-
- @IBInspectable open var shadowOffset: CGSize {
- get {
- layer.shadowOffset
- }
- set {
- layer.shadowOffset = newValue
- }
- }
-
- @IBInspectable open var shadowRadius: CGFloat {
- get {
- layer.shadowRadius
- }
- set {
- layer.shadowRadius = newValue
- }
- }
-
- @IBInspectable open var shadowOpacity: Float {
- get {
- layer.shadowOpacity
- }
- set {
- layer.shadowOpacity = newValue
- }
- }
-
- // MARK: - Border
-
- @IBInspectable open var borderWidth: CGFloat {
- get {
- layer.borderWidth
- }
- set {
- layer.borderWidth = newValue
- }
- }
-
- @IBInspectable open var borderColor: UIColor? {
- get {
- layer.borderColor.flatMap(UIColor.init)
- }
- set {
- layer.borderColor = newValue?.cgColor
- }
- }
-}
diff --git a/Tests/FuntastyKitTests/ExampleScene.swift b/Tests/FuntastyKitTests/ExampleScene.swift
index 7601278..1f0989f 100644
--- a/Tests/FuntastyKitTests/ExampleScene.swift
+++ b/Tests/FuntastyKitTests/ExampleScene.swift
@@ -6,12 +6,15 @@ struct Model {
// MARK: - Controller
+@MainActor
protocol ExampleViewControllerInput: AnyObject {
// TO-DO: Place your presenting methods here
}
+@MainActor
final class ExampleViewController: UIViewController {
- var viewModel: ExampleViewModel!
+
+ var viewModel: ExampleViewModel?
}
extension ExampleViewController: ExampleViewControllerInput {
@@ -20,13 +23,14 @@ extension ExampleViewController: ExampleViewControllerInput {
// MARK: - View model
+@MainActor
final class ExampleViewModel {
- private weak var viewController: ExampleViewControllerInput?
- private let coordinator: ExampleCoordinatorInput
+ private weak var viewController: (any ExampleViewControllerInput)?
+ private let coordinator: any ExampleCoordinatorInput
private let model: Model
- init(model: Model, coordinator: ExampleCoordinatorInput, viewController: ExampleViewControllerInput) {
+ init(model: Model, coordinator: any ExampleCoordinatorInput, viewController: any ExampleViewControllerInput) {
self.model = model
self.coordinator = coordinator
self.viewController = viewController
@@ -35,10 +39,12 @@ final class ExampleViewModel {
// MARK: - Coordinator
+@MainActor
protocol ExampleCoordinatorInput: Coordinator {
// TO-DO: Place your navigation methods here
}
+@MainActor
final class ExampleCoordinator: ModalCoordinator {
var sourceViewController: UIViewController
diff --git a/Tests/FuntastyKitTests/FuntastyKitTests.swift b/Tests/FuntastyKitTests/FuntastyKitTests.swift
index 108ffc4..9c16994 100644
--- a/Tests/FuntastyKitTests/FuntastyKitTests.swift
+++ b/Tests/FuntastyKitTests/FuntastyKitTests.swift
@@ -1,16 +1,24 @@
-import Foundation
import FuntastyKit
-import XCTest
+import Testing
+import UIKit
-final class FuntastyKitTests: XCTestCase {
+@Suite
+@MainActor
+struct FuntastyKitTests {
- func testArchitecture() {
+ @Test
+ func architecture() {
let model = Model()
+ let window = UIWindow()
+
let viewController = UIViewController()
- UIApplication.shared.keyWindow?.rootViewController = viewController
+ window.rootViewController = viewController
+ window.makeKeyAndVisible()
let coordinator = ExampleCoordinator(from: viewController, model: model)
coordinator.start()
+
+ #expect(coordinator.viewController?.viewModel != nil)
}
}