diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da8cb8ca..337ef6b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,10 +9,10 @@ on: jobs: carthage: name: Carthage - runs-on: macos-13 + runs-on: macos-latest steps: - uses: actions/checkout@v4 - - uses: AckeeCZ/load-xcode-version@1.1.0 + - uses: AckeeCZ/load-xcode-version@v1 - name: Build run: carthage build --no-skip-current --cache-builds --use-xcframeworks - uses: actions/cache@v3 @@ -23,10 +23,10 @@ jobs: ${{ runner.os }}-carthage- spm: name: SPM - runs-on: macos-13 + runs-on: macos-latest steps: - uses: actions/checkout@v4 - - uses: AckeeCZ/load-xcode-version@1.1.0 + - uses: AckeeCZ/load-xcode-version@v1 - name: Build run: swift build -c release - uses: actions/cache@v3 diff --git a/.github/workflows/docbuild.yml b/.github/workflows/docbuild.yml index 6d99be7f..786afcce 100644 --- a/.github/workflows/docbuild.yml +++ b/.github/workflows/docbuild.yml @@ -21,12 +21,12 @@ jobs: # Must be set to this for deploying to GitHub Pages name: github-pages url: ${{ steps.deployment.outputs.page_url }} - runs-on: macos-13 + runs-on: macos-latest steps: - name: Checkout 🛎️ uses: actions/checkout@v4 - name: Check Xcode version ✅ - uses: AckeeCZ/load-xcode-version@1.1.0 + uses: AckeeCZ/load-xcode-version@v1 - name: Build DocC run: | xcodebuild docbuild -scheme ACKategories \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f735cbf..546a951d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,12 +5,12 @@ on: [workflow_call] jobs: xcodebuild: name: Xcodebuild - runs-on: macos-13 + runs-on: macos-latest env: IOS_DEVICE: iPhone 15 Pro Max steps: - uses: actions/checkout@v4 - - uses: AckeeCZ/load-xcode-version@1.1.0 + - uses: AckeeCZ/load-xcode-version@v1 - name: iOS tests run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-iOS.xcresult -sdk iphonesimulator -destination "platform=iOS Simulator,name=$IOS_DEVICE,OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty - uses: actions/upload-artifact@v4 @@ -48,10 +48,10 @@ jobs: path: Tests-tvOS.xcresult spm: name: SPM - runs-on: macos-13 + runs-on: macos-latest steps: - uses: actions/checkout@v4 - - uses: AckeeCZ/load-xcode-version@1.1.0 + - uses: AckeeCZ/load-xcode-version@v1 - name: SPM build run: swift build - name: SPM tests diff --git a/.github/xcode-version b/.github/xcode-version index 0d57595e..441e3fb3 100644 --- a/.github/xcode-version +++ b/.github/xcode-version @@ -1 +1 @@ -15.2 \ No newline at end of file +15.4 \ No newline at end of file diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index f7a3862c..7548da51 100644 --- a/ACKategories.xcodeproj/project.pbxproj +++ b/ACKategories.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 6922C77D2AFD1C1A00519CDF /* UINavigationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6922C77C2AFD1C1A00519CDF /* UINavigationControllerTests.swift */; }; 6922C7802AFD1C8B00519CDF /* Dummies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6A02AFD114600C8E8D9 /* Dummies.swift */; }; 6922C7812AFD1C8B00519CDF /* FlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6A12AFD114600C8E8D9 /* FlowCoordinatorTests.swift */; }; + 69246CFB2BDFE77400AB31A1 /* SwiftUIColorsTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69246CFA2BDFE77400AB31A1 /* SwiftUIColorsTheme.swift */; }; 693A92652B3DB290008B3DC3 /* Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 693A925D2B3DB290008B3DC3 /* Networking.framework */; }; 693A927B2B3DB2AA008B3DC3 /* RequestAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92732B3DB2AA008B3DC3 /* RequestAddress.swift */; }; 693A927C2B3DB2AA008B3DC3 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A92742B3DB2AA008B3DC3 /* APIService.swift */; }; @@ -33,6 +34,7 @@ 693A92A42B3DB394008B3DC3 /* HTTPResponse+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A929E2B3DB394008B3DC3 /* HTTPResponse+TestData.swift */; }; 693A92A52B3DB394008B3DC3 /* URL+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A929F2B3DB394008B3DC3 /* URL+TestData.swift */; }; 693A92A62B3DB39F008B3DC3 /* ACKategoriesTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 693A92912B3DB388008B3DC3 /* ACKategoriesTesting.framework */; }; + 693B39B72BF2359B00DF7C5E /* ACKHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693B39B62BF2359B00DF7C5E /* ACKHostingController.swift */; }; 694D14EB2B3DD61A0083E614 /* VersionUpdateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694D14E92B3DD6190083E614 /* VersionUpdateManager.swift */; }; 694D14EC2B3DD61A0083E614 /* MinBuildNumberFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694D14EA2B3DD6190083E614 /* MinBuildNumberFetcher.swift */; }; 694D14EE2B3DD64C0083E614 /* VersionUpdateFetcher_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694D14ED2B3DD64C0083E614 /* VersionUpdateFetcher_Mock.swift */; }; @@ -223,6 +225,7 @@ 6922C77C2AFD1C1A00519CDF /* UINavigationControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UINavigationControllerTests.swift; sourceTree = ""; }; 6922C77E2AFD1C3300519CDF /* ACKategoriesResponder.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ACKategoriesResponder.xctestplan; sourceTree = ""; }; 6922C77F2AFD1C5000519CDF /* ACKategories.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ACKategories.xctestplan; sourceTree = ""; }; + 69246CFA2BDFE77400AB31A1 /* SwiftUIColorsTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIColorsTheme.swift; sourceTree = ""; }; 693A925D2B3DB290008B3DC3 /* Networking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Networking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 693A92642B3DB290008B3DC3 /* NetworkingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetworkingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 693A92732B3DB2AA008B3DC3 /* RequestAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestAddress.swift; sourceTree = ""; }; @@ -244,6 +247,7 @@ 693A929D2B3DB394008B3DC3 /* HTTPURLResponse+TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPURLResponse+TestData.swift"; sourceTree = ""; }; 693A929E2B3DB394008B3DC3 /* HTTPResponse+TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPResponse+TestData.swift"; sourceTree = ""; }; 693A929F2B3DB394008B3DC3 /* URL+TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+TestData.swift"; sourceTree = ""; }; + 693B39B62BF2359B00DF7C5E /* ACKHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACKHostingController.swift; sourceTree = ""; }; 694D14E92B3DD6190083E614 /* VersionUpdateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionUpdateManager.swift; sourceTree = ""; }; 694D14EA2B3DD6190083E614 /* MinBuildNumberFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinBuildNumberFetcher.swift; sourceTree = ""; }; 694D14ED2B3DD64C0083E614 /* VersionUpdateFetcher_Mock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionUpdateFetcher_Mock.swift; sourceTree = ""; }; @@ -647,7 +651,9 @@ 69E0A6032AFD10BE00C8E8D9 /* SwiftUIExtensions */ = { isa = PBXGroup; children = ( + 69246CFA2BDFE77400AB31A1 /* SwiftUIColorsTheme.swift */, 69E0A6042AFD10BE00C8E8D9 /* FontModifier.swift */, + 693B39B62BF2359B00DF7C5E /* ACKHostingController.swift */, ); path = SwiftUIExtensions; sourceTree = ""; @@ -1264,6 +1270,7 @@ 69ACD6DE2AFD133A0021127B /* DateExtensions.swift in Sources */, 69ACD6DF2AFD133A0021127B /* DateFormatting.swift in Sources */, 69ACD6E02AFD133A0021127B /* DictionaryExtensions.swift in Sources */, + 69246CFB2BDFE77400AB31A1 /* SwiftUIColorsTheme.swift in Sources */, 69ACD6E12AFD133A0021127B /* ErrorHandlers.swift in Sources */, 69ACD6E32AFD133A0021127B /* IntExtensions.swift in Sources */, 69ACD6E42AFD133A0021127B /* NSAttributedStringExtensions.swift in Sources */, @@ -1290,6 +1297,7 @@ 69ACD6F62AFD133A0021127B /* UISearchBarExtensions.swift in Sources */, 69ACD6F72AFD133A0021127B /* UIStackViewExtensions.swift in Sources */, 69ACD6F82AFD133A0021127B /* UIView+Spacer.swift in Sources */, + 693B39B72BF2359B00DF7C5E /* ACKHostingController.swift in Sources */, 69ACD6F92AFD133A0021127B /* UIViewController+Children.swift in Sources */, 6A72B2222B1A15AC00A59EDD /* BackGesture.swift in Sources */, 69ACD6FA2AFD133A0021127B /* UIViewController+FrontMost.swift in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df00db1..9c6208b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ ## Next +- SwiftUI improvements ([#149](https://github.com/AckeeCZ/ACKategories/pull/149), kudos to @olejnjak) + - make `lineHeight` parameter optional for `FontModifier` + - implement color forwarding from UIKit to SwiftUI + - add `ACKHostingController` + ## 6.14.0 - Add privacy manifest ([#148](https://github.com/AckeeCZ/ACKategories/pull/148), kudos to @olejnjak) diff --git a/Sources/ACKategories/Base/ViewController.swift b/Sources/ACKategories/Base/ViewController.swift index b1a46f67..3feda3d7 100644 --- a/Sources/ACKategories/Base/ViewController.swift +++ b/Sources/ACKategories/Base/ViewController.swift @@ -22,6 +22,7 @@ extension Base { } } + @available(*, unavailable) required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Sources/ACKategories/SwiftUIExtensions/ACKHostingController.swift b/Sources/ACKategories/SwiftUIExtensions/ACKHostingController.swift new file mode 100644 index 00000000..65f029c4 --- /dev/null +++ b/Sources/ACKategories/SwiftUIExtensions/ACKHostingController.swift @@ -0,0 +1,53 @@ +#if !os(macOS) && !os(watchOS) +import os.log +import SwiftUI + +@available(iOS 13.0, tvOS 13.0, *) +open class ACKHostingController: UIHostingController { + /// Navigation bar is shown/hidden in viewWillAppear according to this flag + public var hasNavigationBar = true + + #if !os(tvOS) + public override var preferredStatusBarStyle: UIStatusBarStyle { + get { _preferredStatusBarStyle } + set { + _preferredStatusBarStyle = newValue + setNeedsStatusBarAppearanceUpdate() + } + } + + private var _preferredStatusBarStyle: UIStatusBarStyle = .default + #endif + + // MARK: - Initializers + + public override init(rootView: RootView) { + super.init(rootView: rootView) + + navigationItem.backButtonTitle = "" + + if Base.memoryLoggingEnabled && Base.viewControllerMemoryLoggingEnabled { + os_log("📱 👶 %@", log: Logger.lifecycleLog(), type: .info, self) + } + } + + @available(*, unavailable) + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if Base.memoryLoggingEnabled && Base.viewControllerMemoryLoggingEnabled { + os_log("📱 ⚰️ %@", log: Logger.lifecycleLog(), type: .info, self) + } + } + + // MARK: - View life cycle + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.setNavigationBarHidden(!hasNavigationBar, animated: animated) + } +} +#endif diff --git a/Sources/ACKategories/SwiftUIExtensions/FontModifier.swift b/Sources/ACKategories/SwiftUIExtensions/FontModifier.swift index 7a5bd7bf..55e6ce99 100644 --- a/Sources/ACKategories/SwiftUIExtensions/FontModifier.swift +++ b/Sources/ACKategories/SwiftUIExtensions/FontModifier.swift @@ -10,28 +10,22 @@ public extension View { /// - font: The font to use in this view. /// - lineHeight: The line height to use in this view. /// - textStyle: The text style for relative font scaling. If `nil`is specified then dynamic type is turned off. - func font( + @ViewBuilder func font( _ font: UIFont, - lineHeight: Double, + lineHeight: Double?, textStyle: Font.TextStyle? ) -> some View { - let customFont: Font - // Do not scale font based on dynamic type when nil `relativeTo` specified - if let textStyle = textStyle { - customFont = .custom( - font.fontName, - size: font.pointSize, - relativeTo: textStyle - ) + let customFont = textStyle + .map { Font.custom(font.fontName, size: font.pointSize, relativeTo: $0) } ?? Font(font) + + if let lineHeight { + self.font(customFont) + .lineSpacing(lineHeight - font.lineHeight) + .padding(.vertical, (lineHeight - font.lineHeight) / 2) } else { - customFont = Font(font) + self.font(customFont) } - - return self - .font(customFont) - .lineSpacing(lineHeight - font.lineHeight) - .padding(.vertical, (lineHeight - font.lineHeight) / 2) } } #endif diff --git a/Sources/ACKategories/SwiftUIExtensions/SwiftUIColorsTheme.swift b/Sources/ACKategories/SwiftUIExtensions/SwiftUIColorsTheme.swift new file mode 100644 index 00000000..eb008c9b --- /dev/null +++ b/Sources/ACKategories/SwiftUIExtensions/SwiftUIColorsTheme.swift @@ -0,0 +1,33 @@ +#if canImport(UIKit) +import SwiftUI +import UIKit + +/// Using this namespace you can define your colors in ``Theme`` extension of `UIColor` +/// and they will automatically appear in `Color.theme` namespace. +/// +/// Make sure your extensions are not defined as static, only instance variables with type `UIColor` will be bridged. +/// +/// ## Example +/// +/// ```swift +/// extension Theme { +/// var primary: UIColor { .red } +/// } +/// ``` +/// +/// Then you can use `Color.theme.primary` in SwiftUI +@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) +@dynamicMemberLookup +public struct SwiftUIColorsTheme { + public subscript(dynamicMember keyPath: KeyPath, UIColor>) -> SwiftUI.Color { + let uiColor = Theme()[keyPath: keyPath] + return .init(uiColor) + } +} + +@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) +public extension Color { + /// Namespace for bridged ``Theme`` colors from `Theme` extension, see ``SwiftUIColorsTheme``. + static let theme = SwiftUIColorsTheme() +} +#endif diff --git a/Sources/ACKategories/UI/Theme/ThemeProvider.swift b/Sources/ACKategories/UI/Theme/ThemeProvider.swift index 32db7713..bb78cfb9 100644 --- a/Sources/ACKategories/UI/Theme/ThemeProvider.swift +++ b/Sources/ACKategories/UI/Theme/ThemeProvider.swift @@ -5,7 +5,7 @@ public struct Theme { } public protocol ThemeProvider { } public extension ThemeProvider { - static var theme: Theme.Type { Theme.self } + static var theme: Theme { Theme() } // theoretically unneccessary allocation overhead every call, but SnapKit uses the same pattern so... - var theme: Theme { Theme() } // theoretically unneccessary allocation overhead every call, but SnapKit uses the same pattern so... + var theme: Theme { Self.theme } } diff --git a/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift b/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift index 1a80f4c3..99e423e5 100644 --- a/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift +++ b/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift @@ -2,19 +2,10 @@ import ACKategories import ACKategoriesTesting import XCTest -@MainActor @available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) final class VersionUpdateManager_Tests: XCTestCase { - private var fetcher: VersionUpdateFetcher_Mock! - - // MARK: - Setup - - override func setUp() { - fetcher = .init() - super.setUp() - } - func test_minBuildNumber_lower() async throws { + let fetcher = VersionUpdateFetcher_Mock() let buildNumber = Int.random(in: 1..<10_000) fetcher.minBuildNumber = buildNumber - 1 @@ -29,6 +20,7 @@ final class VersionUpdateManager_Tests: XCTestCase { } func test_minBuildNumber_equal() async throws { + let fetcher = VersionUpdateFetcher_Mock() let buildNumber = Int.random(in: 1..<10_000) fetcher.minBuildNumber = buildNumber @@ -43,6 +35,7 @@ final class VersionUpdateManager_Tests: XCTestCase { } func test_minBuildNumber_higher() async throws { + let fetcher = VersionUpdateFetcher_Mock() let buildNumber = Int.random(in: 1..<10_000) fetcher.minBuildNumber = buildNumber + 1